mirror of
https://github.com/immich-app/immich.git
synced 2026-05-25 09:02:31 -04:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| faaeaace1d | |||
| 2837de2029 | |||
| eb27635f22 |
@@ -536,7 +536,7 @@ test.describe('Timeline', () => {
|
|||||||
force: false,
|
force: false,
|
||||||
ids: [assetToTrash.id],
|
ids: [assetToTrash.id],
|
||||||
});
|
});
|
||||||
await page.locator('#asset-selection-app-bar').getByLabel('Close').click();
|
await page.keyboard.press('Escape');
|
||||||
await page.getByText('Trash', { exact: true }).click();
|
await page.getByText('Trash', { exact: true }).click();
|
||||||
await timelineUtils.waitForTimelineLoad(page);
|
await timelineUtils.waitForTimelineLoad(page);
|
||||||
await thumbnailUtils.expectInViewport(page, assetToTrash.id);
|
await thumbnailUtils.expectInViewport(page, assetToTrash.id);
|
||||||
@@ -676,7 +676,7 @@ test.describe('Timeline', () => {
|
|||||||
ids: [assetToArchive.id],
|
ids: [assetToArchive.id],
|
||||||
});
|
});
|
||||||
await thumbnailUtils.expectThumbnailIsArchive(page, assetToArchive.id);
|
await thumbnailUtils.expectThumbnailIsArchive(page, assetToArchive.id);
|
||||||
await page.locator('#asset-selection-app-bar').getByLabel('Close').click();
|
await page.keyboard.press('Escape');
|
||||||
await page.getByRole('link').getByText('Archive').click();
|
await page.getByRole('link').getByText('Archive').click();
|
||||||
await timelineUtils.waitForTimelineLoad(page);
|
await timelineUtils.waitForTimelineLoad(page);
|
||||||
await thumbnailUtils.expectInViewport(page, assetToArchive.id);
|
await thumbnailUtils.expectInViewport(page, assetToArchive.id);
|
||||||
@@ -823,7 +823,7 @@ test.describe('Timeline', () => {
|
|||||||
});
|
});
|
||||||
// ensure thumbnail still exists and has favorite icon
|
// ensure thumbnail still exists and has favorite icon
|
||||||
await thumbnailUtils.expectThumbnailIsFavorite(page, assetToFavorite.id);
|
await thumbnailUtils.expectThumbnailIsFavorite(page, assetToFavorite.id);
|
||||||
await page.locator('#asset-selection-app-bar').getByLabel('Close').click();
|
await page.keyboard.press('Escape');
|
||||||
await page.getByRole('link').getByText('Favorites').click();
|
await page.getByRole('link').getByText('Favorites').click();
|
||||||
await timelineUtils.waitForTimelineLoad(page);
|
await timelineUtils.waitForTimelineLoad(page);
|
||||||
await pageUtils.goToAsset(page, assetToFavorite.fileCreatedAt);
|
await pageUtils.goToAsset(page, assetToFavorite.fileCreatedAt);
|
||||||
|
|||||||
Generated
+1
-6
@@ -92,12 +92,10 @@ Class | Method | HTTP request | Description
|
|||||||
*AlbumsApi* | [**getAlbumMapMarkers**](doc//AlbumsApi.md#getalbummapmarkers) | **GET** /albums/{id}/map-markers | Retrieve album map markers
|
*AlbumsApi* | [**getAlbumMapMarkers**](doc//AlbumsApi.md#getalbummapmarkers) | **GET** /albums/{id}/map-markers | Retrieve album map markers
|
||||||
*AlbumsApi* | [**getAlbumStatistics**](doc//AlbumsApi.md#getalbumstatistics) | **GET** /albums/statistics | Retrieve album statistics
|
*AlbumsApi* | [**getAlbumStatistics**](doc//AlbumsApi.md#getalbumstatistics) | **GET** /albums/statistics | Retrieve album statistics
|
||||||
*AlbumsApi* | [**getAllAlbums**](doc//AlbumsApi.md#getallalbums) | **GET** /albums | List all albums
|
*AlbumsApi* | [**getAllAlbums**](doc//AlbumsApi.md#getallalbums) | **GET** /albums | List all albums
|
||||||
*AlbumsApi* | [**getOwnAlbumUser**](doc//AlbumsApi.md#getownalbumuser) | **GET** /albums/{id}/user/self | Get own sharing permissions
|
|
||||||
*AlbumsApi* | [**removeAssetFromAlbum**](doc//AlbumsApi.md#removeassetfromalbum) | **DELETE** /albums/{id}/assets | Remove assets from an album
|
*AlbumsApi* | [**removeAssetFromAlbum**](doc//AlbumsApi.md#removeassetfromalbum) | **DELETE** /albums/{id}/assets | Remove assets from an album
|
||||||
*AlbumsApi* | [**removeUserFromAlbum**](doc//AlbumsApi.md#removeuserfromalbum) | **DELETE** /albums/{id}/user/{userId} | Remove user from album
|
*AlbumsApi* | [**removeUserFromAlbum**](doc//AlbumsApi.md#removeuserfromalbum) | **DELETE** /albums/{id}/user/{userId} | Remove user from album
|
||||||
*AlbumsApi* | [**updateAlbumInfo**](doc//AlbumsApi.md#updatealbuminfo) | **PATCH** /albums/{id} | Update an album
|
*AlbumsApi* | [**updateAlbumInfo**](doc//AlbumsApi.md#updatealbuminfo) | **PATCH** /albums/{id} | Update an album
|
||||||
*AlbumsApi* | [**updateAlbumUser**](doc//AlbumsApi.md#updatealbumuser) | **PUT** /albums/{id}/user/{userId} | Update user role
|
*AlbumsApi* | [**updateAlbumUser**](doc//AlbumsApi.md#updatealbumuser) | **PUT** /albums/{id}/user/{userId} | Update user role
|
||||||
*AlbumsApi* | [**updateOwnAlbumUser**](doc//AlbumsApi.md#updateownalbumuser) | **PUT** /albums/{id}/user/self | Update own sharing permissions
|
|
||||||
*AssetsApi* | [**checkBulkUpload**](doc//AssetsApi.md#checkbulkupload) | **POST** /assets/bulk-upload-check | Check bulk upload
|
*AssetsApi* | [**checkBulkUpload**](doc//AssetsApi.md#checkbulkupload) | **POST** /assets/bulk-upload-check | Check bulk upload
|
||||||
*AssetsApi* | [**copyAsset**](doc//AssetsApi.md#copyasset) | **PUT** /assets/copy | Copy asset
|
*AssetsApi* | [**copyAsset**](doc//AssetsApi.md#copyasset) | **PUT** /assets/copy | Copy asset
|
||||||
*AssetsApi* | [**deleteAssetMetadata**](doc//AssetsApi.md#deleteassetmetadata) | **DELETE** /assets/{id}/metadata/{key} | Delete asset metadata by key
|
*AssetsApi* | [**deleteAssetMetadata**](doc//AssetsApi.md#deleteassetmetadata) | **DELETE** /assets/{id}/metadata/{key} | Delete asset metadata by key
|
||||||
@@ -454,7 +452,7 @@ Class | Method | HTTP request | Description
|
|||||||
- [MemoryStatisticsResponseDto](doc//MemoryStatisticsResponseDto.md)
|
- [MemoryStatisticsResponseDto](doc//MemoryStatisticsResponseDto.md)
|
||||||
- [MemoryType](doc//MemoryType.md)
|
- [MemoryType](doc//MemoryType.md)
|
||||||
- [MemoryUpdateDto](doc//MemoryUpdateDto.md)
|
- [MemoryUpdateDto](doc//MemoryUpdateDto.md)
|
||||||
- [MergeFaceClusterDto](doc//MergeFaceClusterDto.md)
|
- [MergePersonDto](doc//MergePersonDto.md)
|
||||||
- [MetadataSearchDto](doc//MetadataSearchDto.md)
|
- [MetadataSearchDto](doc//MetadataSearchDto.md)
|
||||||
- [MirrorAxis](doc//MirrorAxis.md)
|
- [MirrorAxis](doc//MirrorAxis.md)
|
||||||
- [MirrorParameters](doc//MirrorParameters.md)
|
- [MirrorParameters](doc//MirrorParameters.md)
|
||||||
@@ -546,8 +544,6 @@ Class | Method | HTTP request | Description
|
|||||||
- [SharedLinkType](doc//SharedLinkType.md)
|
- [SharedLinkType](doc//SharedLinkType.md)
|
||||||
- [SharedLinksResponse](doc//SharedLinksResponse.md)
|
- [SharedLinksResponse](doc//SharedLinksResponse.md)
|
||||||
- [SharedLinksUpdate](doc//SharedLinksUpdate.md)
|
- [SharedLinksUpdate](doc//SharedLinksUpdate.md)
|
||||||
- [SharingOptionsResponseDto](doc//SharingOptionsResponseDto.md)
|
|
||||||
- [SharingPermission](doc//SharingPermission.md)
|
|
||||||
- [SignUpDto](doc//SignUpDto.md)
|
- [SignUpDto](doc//SignUpDto.md)
|
||||||
- [SmartSearchDto](doc//SmartSearchDto.md)
|
- [SmartSearchDto](doc//SmartSearchDto.md)
|
||||||
- [SourceType](doc//SourceType.md)
|
- [SourceType](doc//SourceType.md)
|
||||||
@@ -647,7 +643,6 @@ Class | Method | HTTP request | Description
|
|||||||
- [UpdateAlbumUserDto](doc//UpdateAlbumUserDto.md)
|
- [UpdateAlbumUserDto](doc//UpdateAlbumUserDto.md)
|
||||||
- [UpdateAssetDto](doc//UpdateAssetDto.md)
|
- [UpdateAssetDto](doc//UpdateAssetDto.md)
|
||||||
- [UpdateLibraryDto](doc//UpdateLibraryDto.md)
|
- [UpdateLibraryDto](doc//UpdateLibraryDto.md)
|
||||||
- [UpdateSharingOptionsDto](doc//UpdateSharingOptionsDto.md)
|
|
||||||
- [UsageByUserDto](doc//UsageByUserDto.md)
|
- [UsageByUserDto](doc//UsageByUserDto.md)
|
||||||
- [UserAdminCreateDto](doc//UserAdminCreateDto.md)
|
- [UserAdminCreateDto](doc//UserAdminCreateDto.md)
|
||||||
- [UserAdminDeleteDto](doc//UserAdminDeleteDto.md)
|
- [UserAdminDeleteDto](doc//UserAdminDeleteDto.md)
|
||||||
|
|||||||
Generated
+1
-4
@@ -198,7 +198,7 @@ part 'model/memory_search_order.dart';
|
|||||||
part 'model/memory_statistics_response_dto.dart';
|
part 'model/memory_statistics_response_dto.dart';
|
||||||
part 'model/memory_type.dart';
|
part 'model/memory_type.dart';
|
||||||
part 'model/memory_update_dto.dart';
|
part 'model/memory_update_dto.dart';
|
||||||
part 'model/merge_face_cluster_dto.dart';
|
part 'model/merge_person_dto.dart';
|
||||||
part 'model/metadata_search_dto.dart';
|
part 'model/metadata_search_dto.dart';
|
||||||
part 'model/mirror_axis.dart';
|
part 'model/mirror_axis.dart';
|
||||||
part 'model/mirror_parameters.dart';
|
part 'model/mirror_parameters.dart';
|
||||||
@@ -290,8 +290,6 @@ part 'model/shared_link_response_dto.dart';
|
|||||||
part 'model/shared_link_type.dart';
|
part 'model/shared_link_type.dart';
|
||||||
part 'model/shared_links_response.dart';
|
part 'model/shared_links_response.dart';
|
||||||
part 'model/shared_links_update.dart';
|
part 'model/shared_links_update.dart';
|
||||||
part 'model/sharing_options_response_dto.dart';
|
|
||||||
part 'model/sharing_permission.dart';
|
|
||||||
part 'model/sign_up_dto.dart';
|
part 'model/sign_up_dto.dart';
|
||||||
part 'model/smart_search_dto.dart';
|
part 'model/smart_search_dto.dart';
|
||||||
part 'model/source_type.dart';
|
part 'model/source_type.dart';
|
||||||
@@ -391,7 +389,6 @@ part 'model/update_album_dto.dart';
|
|||||||
part 'model/update_album_user_dto.dart';
|
part 'model/update_album_user_dto.dart';
|
||||||
part 'model/update_asset_dto.dart';
|
part 'model/update_asset_dto.dart';
|
||||||
part 'model/update_library_dto.dart';
|
part 'model/update_library_dto.dart';
|
||||||
part 'model/update_sharing_options_dto.dart';
|
|
||||||
part 'model/usage_by_user_dto.dart';
|
part 'model/usage_by_user_dto.dart';
|
||||||
part 'model/user_admin_create_dto.dart';
|
part 'model/user_admin_create_dto.dart';
|
||||||
part 'model/user_admin_delete_dto.dart';
|
part 'model/user_admin_delete_dto.dart';
|
||||||
|
|||||||
Generated
-110
@@ -580,63 +580,6 @@ class AlbumsApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get own sharing permissions
|
|
||||||
///
|
|
||||||
/// Get the own sharing permissions in a specific album.
|
|
||||||
///
|
|
||||||
/// Note: This method returns the HTTP [Response].
|
|
||||||
///
|
|
||||||
/// Parameters:
|
|
||||||
///
|
|
||||||
/// * [String] id (required):
|
|
||||||
Future<Response> getOwnAlbumUserWithHttpInfo(String id,) async {
|
|
||||||
// ignore: prefer_const_declarations
|
|
||||||
final apiPath = r'/albums/{id}/user/self'
|
|
||||||
.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,
|
|
||||||
'GET',
|
|
||||||
queryParams,
|
|
||||||
postBody,
|
|
||||||
headerParams,
|
|
||||||
formParams,
|
|
||||||
contentTypes.isEmpty ? null : contentTypes.first,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get own sharing permissions
|
|
||||||
///
|
|
||||||
/// Get the own sharing permissions in a specific album.
|
|
||||||
///
|
|
||||||
/// Parameters:
|
|
||||||
///
|
|
||||||
/// * [String] id (required):
|
|
||||||
Future<SharingOptionsResponseDto?> getOwnAlbumUser(String id,) async {
|
|
||||||
final response = await getOwnAlbumUserWithHttpInfo(id,);
|
|
||||||
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), 'SharingOptionsResponseDto',) as SharingOptionsResponseDto;
|
|
||||||
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove assets from an album
|
/// Remove assets from an album
|
||||||
///
|
///
|
||||||
/// Remove multiple assets from a specific album by its ID.
|
/// Remove multiple assets from a specific album by its ID.
|
||||||
@@ -873,57 +816,4 @@ class AlbumsApi {
|
|||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update own sharing permissions
|
|
||||||
///
|
|
||||||
/// Change the own sharing permissions in a specific album.
|
|
||||||
///
|
|
||||||
/// Note: This method returns the HTTP [Response].
|
|
||||||
///
|
|
||||||
/// Parameters:
|
|
||||||
///
|
|
||||||
/// * [String] id (required):
|
|
||||||
///
|
|
||||||
/// * [UpdateSharingOptionsDto] updateSharingOptionsDto (required):
|
|
||||||
Future<Response> updateOwnAlbumUserWithHttpInfo(String id, UpdateSharingOptionsDto updateSharingOptionsDto,) async {
|
|
||||||
// ignore: prefer_const_declarations
|
|
||||||
final apiPath = r'/albums/{id}/user/self'
|
|
||||||
.replaceAll('{id}', id);
|
|
||||||
|
|
||||||
// ignore: prefer_final_locals
|
|
||||||
Object? postBody = updateSharingOptionsDto;
|
|
||||||
|
|
||||||
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 own sharing permissions
|
|
||||||
///
|
|
||||||
/// Change the own sharing permissions in a specific album.
|
|
||||||
///
|
|
||||||
/// Parameters:
|
|
||||||
///
|
|
||||||
/// * [String] id (required):
|
|
||||||
///
|
|
||||||
/// * [UpdateSharingOptionsDto] updateSharingOptionsDto (required):
|
|
||||||
Future<void> updateOwnAlbumUser(String id, UpdateSharingOptionsDto updateSharingOptionsDto,) async {
|
|
||||||
final response = await updateOwnAlbumUserWithHttpInfo(id, updateSharingOptionsDto,);
|
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+6
-6
@@ -448,14 +448,14 @@ class PeopleApi {
|
|||||||
///
|
///
|
||||||
/// * [String] id (required):
|
/// * [String] id (required):
|
||||||
///
|
///
|
||||||
/// * [MergeFaceClusterDto] mergeFaceClusterDto (required):
|
/// * [MergePersonDto] mergePersonDto (required):
|
||||||
Future<Response> mergePersonWithHttpInfo(String id, MergeFaceClusterDto mergeFaceClusterDto,) async {
|
Future<Response> mergePersonWithHttpInfo(String id, MergePersonDto mergePersonDto,) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/people/{id}/merge'
|
final apiPath = r'/people/{id}/merge'
|
||||||
.replaceAll('{id}', id);
|
.replaceAll('{id}', id);
|
||||||
|
|
||||||
// ignore: prefer_final_locals
|
// ignore: prefer_final_locals
|
||||||
Object? postBody = mergeFaceClusterDto;
|
Object? postBody = mergePersonDto;
|
||||||
|
|
||||||
final queryParams = <QueryParam>[];
|
final queryParams = <QueryParam>[];
|
||||||
final headerParams = <String, String>{};
|
final headerParams = <String, String>{};
|
||||||
@@ -483,9 +483,9 @@ class PeopleApi {
|
|||||||
///
|
///
|
||||||
/// * [String] id (required):
|
/// * [String] id (required):
|
||||||
///
|
///
|
||||||
/// * [MergeFaceClusterDto] mergeFaceClusterDto (required):
|
/// * [MergePersonDto] mergePersonDto (required):
|
||||||
Future<List<BulkIdResponseDto>?> mergePerson(String id, MergeFaceClusterDto mergeFaceClusterDto,) async {
|
Future<List<BulkIdResponseDto>?> mergePerson(String id, MergePersonDto mergePersonDto,) async {
|
||||||
final response = await mergePersonWithHttpInfo(id, mergeFaceClusterDto,);
|
final response = await mergePersonWithHttpInfo(id, mergePersonDto,);
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+2
-8
@@ -442,8 +442,8 @@ class ApiClient {
|
|||||||
return MemoryTypeTypeTransformer().decode(value);
|
return MemoryTypeTypeTransformer().decode(value);
|
||||||
case 'MemoryUpdateDto':
|
case 'MemoryUpdateDto':
|
||||||
return MemoryUpdateDto.fromJson(value);
|
return MemoryUpdateDto.fromJson(value);
|
||||||
case 'MergeFaceClusterDto':
|
case 'MergePersonDto':
|
||||||
return MergeFaceClusterDto.fromJson(value);
|
return MergePersonDto.fromJson(value);
|
||||||
case 'MetadataSearchDto':
|
case 'MetadataSearchDto':
|
||||||
return MetadataSearchDto.fromJson(value);
|
return MetadataSearchDto.fromJson(value);
|
||||||
case 'MirrorAxis':
|
case 'MirrorAxis':
|
||||||
@@ -626,10 +626,6 @@ class ApiClient {
|
|||||||
return SharedLinksResponse.fromJson(value);
|
return SharedLinksResponse.fromJson(value);
|
||||||
case 'SharedLinksUpdate':
|
case 'SharedLinksUpdate':
|
||||||
return SharedLinksUpdate.fromJson(value);
|
return SharedLinksUpdate.fromJson(value);
|
||||||
case 'SharingOptionsResponseDto':
|
|
||||||
return SharingOptionsResponseDto.fromJson(value);
|
|
||||||
case 'SharingPermission':
|
|
||||||
return SharingPermissionTypeTransformer().decode(value);
|
|
||||||
case 'SignUpDto':
|
case 'SignUpDto':
|
||||||
return SignUpDto.fromJson(value);
|
return SignUpDto.fromJson(value);
|
||||||
case 'SmartSearchDto':
|
case 'SmartSearchDto':
|
||||||
@@ -828,8 +824,6 @@ class ApiClient {
|
|||||||
return UpdateAssetDto.fromJson(value);
|
return UpdateAssetDto.fromJson(value);
|
||||||
case 'UpdateLibraryDto':
|
case 'UpdateLibraryDto':
|
||||||
return UpdateLibraryDto.fromJson(value);
|
return UpdateLibraryDto.fromJson(value);
|
||||||
case 'UpdateSharingOptionsDto':
|
|
||||||
return UpdateSharingOptionsDto.fromJson(value);
|
|
||||||
case 'UsageByUserDto':
|
case 'UsageByUserDto':
|
||||||
return UsageByUserDto.fromJson(value);
|
return UsageByUserDto.fromJson(value);
|
||||||
case 'UserAdminCreateDto':
|
case 'UserAdminCreateDto':
|
||||||
|
|||||||
Generated
-3
@@ -163,9 +163,6 @@ String parameterToString(dynamic value) {
|
|||||||
if (value is SharedLinkType) {
|
if (value is SharedLinkType) {
|
||||||
return SharedLinkTypeTypeTransformer().encode(value).toString();
|
return SharedLinkTypeTypeTransformer().encode(value).toString();
|
||||||
}
|
}
|
||||||
if (value is SharingPermission) {
|
|
||||||
return SharingPermissionTypeTransformer().encode(value).toString();
|
|
||||||
}
|
|
||||||
if (value is SourceType) {
|
if (value is SourceType) {
|
||||||
return SourceTypeTypeTransformer().encode(value).toString();
|
return SourceTypeTypeTransformer().encode(value).toString();
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-9
@@ -37,7 +37,6 @@ class AssetResponseDto {
|
|||||||
this.owner,
|
this.owner,
|
||||||
required this.ownerId,
|
required this.ownerId,
|
||||||
this.people = const [],
|
this.people = const [],
|
||||||
this.permissions = const [],
|
|
||||||
this.resized,
|
this.resized,
|
||||||
this.stack,
|
this.stack,
|
||||||
this.tags = const [],
|
this.tags = const [],
|
||||||
@@ -141,8 +140,6 @@ class AssetResponseDto {
|
|||||||
|
|
||||||
List<PersonResponseDto> people;
|
List<PersonResponseDto> people;
|
||||||
|
|
||||||
List<SharingPermission> permissions;
|
|
||||||
|
|
||||||
/// Is resized
|
/// Is resized
|
||||||
///
|
///
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
@@ -198,7 +195,6 @@ class AssetResponseDto {
|
|||||||
other.owner == owner &&
|
other.owner == owner &&
|
||||||
other.ownerId == ownerId &&
|
other.ownerId == ownerId &&
|
||||||
_deepEquality.equals(other.people, people) &&
|
_deepEquality.equals(other.people, people) &&
|
||||||
_deepEquality.equals(other.permissions, permissions) &&
|
|
||||||
other.resized == resized &&
|
other.resized == resized &&
|
||||||
other.stack == stack &&
|
other.stack == stack &&
|
||||||
_deepEquality.equals(other.tags, tags) &&
|
_deepEquality.equals(other.tags, tags) &&
|
||||||
@@ -235,7 +231,6 @@ class AssetResponseDto {
|
|||||||
(owner == null ? 0 : owner!.hashCode) +
|
(owner == null ? 0 : owner!.hashCode) +
|
||||||
(ownerId.hashCode) +
|
(ownerId.hashCode) +
|
||||||
(people.hashCode) +
|
(people.hashCode) +
|
||||||
(permissions.hashCode) +
|
|
||||||
(resized == null ? 0 : resized!.hashCode) +
|
(resized == null ? 0 : resized!.hashCode) +
|
||||||
(stack == null ? 0 : stack!.hashCode) +
|
(stack == null ? 0 : stack!.hashCode) +
|
||||||
(tags.hashCode) +
|
(tags.hashCode) +
|
||||||
@@ -246,7 +241,7 @@ class AssetResponseDto {
|
|||||||
(width == null ? 0 : width!.hashCode);
|
(width == null ? 0 : width!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'AssetResponseDto[checksum=$checksum, createdAt=$createdAt, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, height=$height, id=$id, isArchived=$isArchived, isEdited=$isEdited, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, permissions=$permissions, resized=$resized, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, updatedAt=$updatedAt, visibility=$visibility, width=$width]';
|
String toString() => 'AssetResponseDto[checksum=$checksum, createdAt=$createdAt, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, height=$height, id=$id, isArchived=$isArchived, isEdited=$isEdited, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, updatedAt=$updatedAt, visibility=$visibility, width=$width]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@@ -306,7 +301,6 @@ class AssetResponseDto {
|
|||||||
}
|
}
|
||||||
json[r'ownerId'] = this.ownerId;
|
json[r'ownerId'] = this.ownerId;
|
||||||
json[r'people'] = this.people;
|
json[r'people'] = this.people;
|
||||||
json[r'permissions'] = this.permissions;
|
|
||||||
if (this.resized != null) {
|
if (this.resized != null) {
|
||||||
json[r'resized'] = this.resized;
|
json[r'resized'] = this.resized;
|
||||||
} else {
|
} else {
|
||||||
@@ -367,7 +361,6 @@ class AssetResponseDto {
|
|||||||
owner: UserResponseDto.fromJson(json[r'owner']),
|
owner: UserResponseDto.fromJson(json[r'owner']),
|
||||||
ownerId: mapValueOfType<String>(json, r'ownerId')!,
|
ownerId: mapValueOfType<String>(json, r'ownerId')!,
|
||||||
people: PersonResponseDto.listFromJson(json[r'people']),
|
people: PersonResponseDto.listFromJson(json[r'people']),
|
||||||
permissions: SharingPermission.listFromJson(json[r'permissions']),
|
|
||||||
resized: mapValueOfType<bool>(json, r'resized'),
|
resized: mapValueOfType<bool>(json, r'resized'),
|
||||||
stack: AssetStackResponseDto.fromJson(json[r'stack']),
|
stack: AssetStackResponseDto.fromJson(json[r'stack']),
|
||||||
tags: TagResponseDto.listFromJson(json[r'tags']),
|
tags: TagResponseDto.listFromJson(json[r'tags']),
|
||||||
@@ -440,7 +433,6 @@ class AssetResponseDto {
|
|||||||
'originalFileName',
|
'originalFileName',
|
||||||
'originalPath',
|
'originalPath',
|
||||||
'ownerId',
|
'ownerId',
|
||||||
'permissions',
|
|
||||||
'thumbhash',
|
'thumbhash',
|
||||||
'type',
|
'type',
|
||||||
'updatedAt',
|
'updatedAt',
|
||||||
|
|||||||
Generated
-3
@@ -42,7 +42,6 @@ class JobName {
|
|||||||
static const databaseBackup = JobName._(r'DatabaseBackup');
|
static const databaseBackup = JobName._(r'DatabaseBackup');
|
||||||
static const facialRecognitionQueueAll = JobName._(r'FacialRecognitionQueueAll');
|
static const facialRecognitionQueueAll = JobName._(r'FacialRecognitionQueueAll');
|
||||||
static const facialRecognition = JobName._(r'FacialRecognition');
|
static const facialRecognition = JobName._(r'FacialRecognition');
|
||||||
static const facialRecognitionMerge = JobName._(r'FacialRecognitionMerge');
|
|
||||||
static const fileDelete = JobName._(r'FileDelete');
|
static const fileDelete = JobName._(r'FileDelete');
|
||||||
static const fileMigrationQueueAll = JobName._(r'FileMigrationQueueAll');
|
static const fileMigrationQueueAll = JobName._(r'FileMigrationQueueAll');
|
||||||
static const libraryDeleteCheck = JobName._(r'LibraryDeleteCheck');
|
static const libraryDeleteCheck = JobName._(r'LibraryDeleteCheck');
|
||||||
@@ -101,7 +100,6 @@ class JobName {
|
|||||||
databaseBackup,
|
databaseBackup,
|
||||||
facialRecognitionQueueAll,
|
facialRecognitionQueueAll,
|
||||||
facialRecognition,
|
facialRecognition,
|
||||||
facialRecognitionMerge,
|
|
||||||
fileDelete,
|
fileDelete,
|
||||||
fileMigrationQueueAll,
|
fileMigrationQueueAll,
|
||||||
libraryDeleteCheck,
|
libraryDeleteCheck,
|
||||||
@@ -195,7 +193,6 @@ class JobNameTypeTransformer {
|
|||||||
case r'DatabaseBackup': return JobName.databaseBackup;
|
case r'DatabaseBackup': return JobName.databaseBackup;
|
||||||
case r'FacialRecognitionQueueAll': return JobName.facialRecognitionQueueAll;
|
case r'FacialRecognitionQueueAll': return JobName.facialRecognitionQueueAll;
|
||||||
case r'FacialRecognition': return JobName.facialRecognition;
|
case r'FacialRecognition': return JobName.facialRecognition;
|
||||||
case r'FacialRecognitionMerge': return JobName.facialRecognitionMerge;
|
|
||||||
case r'FileDelete': return JobName.fileDelete;
|
case r'FileDelete': return JobName.fileDelete;
|
||||||
case r'FileMigrationQueueAll': return JobName.fileMigrationQueueAll;
|
case r'FileMigrationQueueAll': return JobName.fileMigrationQueueAll;
|
||||||
case r'LibraryDeleteCheck': return JobName.libraryDeleteCheck;
|
case r'LibraryDeleteCheck': return JobName.libraryDeleteCheck;
|
||||||
|
|||||||
-3
@@ -29,7 +29,6 @@ class ManualJobName {
|
|||||||
static const memoryCleanup = ManualJobName._(r'memory-cleanup');
|
static const memoryCleanup = ManualJobName._(r'memory-cleanup');
|
||||||
static const memoryCreate = ManualJobName._(r'memory-create');
|
static const memoryCreate = ManualJobName._(r'memory-create');
|
||||||
static const backupDatabase = ManualJobName._(r'backup-database');
|
static const backupDatabase = ManualJobName._(r'backup-database');
|
||||||
static const personGroupMerge = ManualJobName._(r'person-group-merge');
|
|
||||||
|
|
||||||
/// List of all possible values in this [enum][ManualJobName].
|
/// List of all possible values in this [enum][ManualJobName].
|
||||||
static const values = <ManualJobName>[
|
static const values = <ManualJobName>[
|
||||||
@@ -39,7 +38,6 @@ class ManualJobName {
|
|||||||
memoryCleanup,
|
memoryCleanup,
|
||||||
memoryCreate,
|
memoryCreate,
|
||||||
backupDatabase,
|
backupDatabase,
|
||||||
personGroupMerge,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
static ManualJobName? fromJson(dynamic value) => ManualJobNameTypeTransformer().decode(value);
|
static ManualJobName? fromJson(dynamic value) => ManualJobNameTypeTransformer().decode(value);
|
||||||
@@ -84,7 +82,6 @@ class ManualJobNameTypeTransformer {
|
|||||||
case r'memory-cleanup': return ManualJobName.memoryCleanup;
|
case r'memory-cleanup': return ManualJobName.memoryCleanup;
|
||||||
case r'memory-create': return ManualJobName.memoryCreate;
|
case r'memory-create': return ManualJobName.memoryCreate;
|
||||||
case r'backup-database': return ManualJobName.backupDatabase;
|
case r'backup-database': return ManualJobName.backupDatabase;
|
||||||
case r'person-group-merge': return ManualJobName.personGroupMerge;
|
|
||||||
default:
|
default:
|
||||||
if (!allowNull) {
|
if (!allowNull) {
|
||||||
throw ArgumentError('Unknown enum value to decode: $data');
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
|||||||
+20
-20
@@ -10,17 +10,17 @@
|
|||||||
|
|
||||||
part of openapi.api;
|
part of openapi.api;
|
||||||
|
|
||||||
class MergeFaceClusterDto {
|
class MergePersonDto {
|
||||||
/// Returns a new [MergeFaceClusterDto] instance.
|
/// Returns a new [MergePersonDto] instance.
|
||||||
MergeFaceClusterDto({
|
MergePersonDto({
|
||||||
this.ids = const [],
|
this.ids = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Face cluster IDs to merge
|
/// Person IDs to merge
|
||||||
List<String> ids;
|
List<String> ids;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is MergeFaceClusterDto &&
|
bool operator ==(Object other) => identical(this, other) || other is MergePersonDto &&
|
||||||
_deepEquality.equals(other.ids, ids);
|
_deepEquality.equals(other.ids, ids);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -29,7 +29,7 @@ class MergeFaceClusterDto {
|
|||||||
(ids.hashCode);
|
(ids.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'MergeFaceClusterDto[ids=$ids]';
|
String toString() => 'MergePersonDto[ids=$ids]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@@ -37,15 +37,15 @@ class MergeFaceClusterDto {
|
|||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a new [MergeFaceClusterDto] instance and imports its values from
|
/// Returns a new [MergePersonDto] instance and imports its values from
|
||||||
/// [value] if it's a [Map], null otherwise.
|
/// [value] if it's a [Map], null otherwise.
|
||||||
// ignore: prefer_constructors_over_static_methods
|
// ignore: prefer_constructors_over_static_methods
|
||||||
static MergeFaceClusterDto? fromJson(dynamic value) {
|
static MergePersonDto? fromJson(dynamic value) {
|
||||||
upgradeDto(value, "MergeFaceClusterDto");
|
upgradeDto(value, "MergePersonDto");
|
||||||
if (value is Map) {
|
if (value is Map) {
|
||||||
final json = value.cast<String, dynamic>();
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
return MergeFaceClusterDto(
|
return MergePersonDto(
|
||||||
ids: json[r'ids'] is Iterable
|
ids: json[r'ids'] is Iterable
|
||||||
? (json[r'ids'] as Iterable).cast<String>().toList(growable: false)
|
? (json[r'ids'] as Iterable).cast<String>().toList(growable: false)
|
||||||
: const [],
|
: const [],
|
||||||
@@ -54,11 +54,11 @@ class MergeFaceClusterDto {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<MergeFaceClusterDto> listFromJson(dynamic json, {bool growable = false,}) {
|
static List<MergePersonDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
final result = <MergeFaceClusterDto>[];
|
final result = <MergePersonDto>[];
|
||||||
if (json is List && json.isNotEmpty) {
|
if (json is List && json.isNotEmpty) {
|
||||||
for (final row in json) {
|
for (final row in json) {
|
||||||
final value = MergeFaceClusterDto.fromJson(row);
|
final value = MergePersonDto.fromJson(row);
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
result.add(value);
|
result.add(value);
|
||||||
}
|
}
|
||||||
@@ -67,12 +67,12 @@ class MergeFaceClusterDto {
|
|||||||
return result.toList(growable: growable);
|
return result.toList(growable: growable);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Map<String, MergeFaceClusterDto> mapFromJson(dynamic json) {
|
static Map<String, MergePersonDto> mapFromJson(dynamic json) {
|
||||||
final map = <String, MergeFaceClusterDto>{};
|
final map = <String, MergePersonDto>{};
|
||||||
if (json is Map && json.isNotEmpty) {
|
if (json is Map && json.isNotEmpty) {
|
||||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
for (final entry in json.entries) {
|
for (final entry in json.entries) {
|
||||||
final value = MergeFaceClusterDto.fromJson(entry.value);
|
final value = MergePersonDto.fromJson(entry.value);
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
map[entry.key] = value;
|
map[entry.key] = value;
|
||||||
}
|
}
|
||||||
@@ -81,14 +81,14 @@ class MergeFaceClusterDto {
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
// maps a json object with a list of MergeFaceClusterDto-objects as value to a dart map
|
// maps a json object with a list of MergePersonDto-objects as value to a dart map
|
||||||
static Map<String, List<MergeFaceClusterDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
static Map<String, List<MergePersonDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
final map = <String, List<MergeFaceClusterDto>>{};
|
final map = <String, List<MergePersonDto>>{};
|
||||||
if (json is Map && json.isNotEmpty) {
|
if (json is Map && json.isNotEmpty) {
|
||||||
// ignore: parameter_assignments
|
// ignore: parameter_assignments
|
||||||
json = json.cast<String, dynamic>();
|
json = json.cast<String, dynamic>();
|
||||||
for (final entry in json.entries) {
|
for (final entry in json.entries) {
|
||||||
map[entry.key] = MergeFaceClusterDto.listFromJson(entry.value, growable: growable,);
|
map[entry.key] = MergePersonDto.listFromJson(entry.value, growable: growable,);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
+1
-14
@@ -15,7 +15,6 @@ class PersonResponseDto {
|
|||||||
PersonResponseDto({
|
PersonResponseDto({
|
||||||
required this.birthDate,
|
required this.birthDate,
|
||||||
this.color,
|
this.color,
|
||||||
required this.faceClusterId,
|
|
||||||
required this.id,
|
required this.id,
|
||||||
this.isFavorite,
|
this.isFavorite,
|
||||||
required this.isHidden,
|
required this.isHidden,
|
||||||
@@ -36,9 +35,6 @@ class PersonResponseDto {
|
|||||||
///
|
///
|
||||||
String? color;
|
String? color;
|
||||||
|
|
||||||
/// Face cluster ID
|
|
||||||
String? faceClusterId;
|
|
||||||
|
|
||||||
/// Person ID
|
/// Person ID
|
||||||
String id;
|
String id;
|
||||||
|
|
||||||
@@ -73,7 +69,6 @@ class PersonResponseDto {
|
|||||||
bool operator ==(Object other) => identical(this, other) || other is PersonResponseDto &&
|
bool operator ==(Object other) => identical(this, other) || other is PersonResponseDto &&
|
||||||
other.birthDate == birthDate &&
|
other.birthDate == birthDate &&
|
||||||
other.color == color &&
|
other.color == color &&
|
||||||
other.faceClusterId == faceClusterId &&
|
|
||||||
other.id == id &&
|
other.id == id &&
|
||||||
other.isFavorite == isFavorite &&
|
other.isFavorite == isFavorite &&
|
||||||
other.isHidden == isHidden &&
|
other.isHidden == isHidden &&
|
||||||
@@ -86,7 +81,6 @@ class PersonResponseDto {
|
|||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(birthDate == null ? 0 : birthDate!.hashCode) +
|
(birthDate == null ? 0 : birthDate!.hashCode) +
|
||||||
(color == null ? 0 : color!.hashCode) +
|
(color == null ? 0 : color!.hashCode) +
|
||||||
(faceClusterId == null ? 0 : faceClusterId!.hashCode) +
|
|
||||||
(id.hashCode) +
|
(id.hashCode) +
|
||||||
(isFavorite == null ? 0 : isFavorite!.hashCode) +
|
(isFavorite == null ? 0 : isFavorite!.hashCode) +
|
||||||
(isHidden.hashCode) +
|
(isHidden.hashCode) +
|
||||||
@@ -95,7 +89,7 @@ class PersonResponseDto {
|
|||||||
(updatedAt == null ? 0 : updatedAt!.hashCode);
|
(updatedAt == null ? 0 : updatedAt!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'PersonResponseDto[birthDate=$birthDate, color=$color, faceClusterId=$faceClusterId, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]';
|
String toString() => 'PersonResponseDto[birthDate=$birthDate, color=$color, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@@ -108,11 +102,6 @@ class PersonResponseDto {
|
|||||||
json[r'color'] = this.color;
|
json[r'color'] = this.color;
|
||||||
} else {
|
} else {
|
||||||
// json[r'color'] = null;
|
// json[r'color'] = null;
|
||||||
}
|
|
||||||
if (this.faceClusterId != null) {
|
|
||||||
json[r'faceClusterId'] = this.faceClusterId;
|
|
||||||
} else {
|
|
||||||
// json[r'faceClusterId'] = null;
|
|
||||||
}
|
}
|
||||||
json[r'id'] = this.id;
|
json[r'id'] = this.id;
|
||||||
if (this.isFavorite != null) {
|
if (this.isFavorite != null) {
|
||||||
@@ -142,7 +131,6 @@ class PersonResponseDto {
|
|||||||
return PersonResponseDto(
|
return PersonResponseDto(
|
||||||
birthDate: mapDateTime(json, r'birthDate', r''),
|
birthDate: mapDateTime(json, r'birthDate', r''),
|
||||||
color: mapValueOfType<String>(json, r'color'),
|
color: mapValueOfType<String>(json, r'color'),
|
||||||
faceClusterId: mapValueOfType<String>(json, r'faceClusterId'),
|
|
||||||
id: mapValueOfType<String>(json, r'id')!,
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
||||||
isHidden: mapValueOfType<bool>(json, r'isHidden')!,
|
isHidden: mapValueOfType<bool>(json, r'isHidden')!,
|
||||||
@@ -197,7 +185,6 @@ class PersonResponseDto {
|
|||||||
/// The list of required keys that must be present in a JSON.
|
/// The list of required keys that must be present in a JSON.
|
||||||
static const requiredKeys = <String>{
|
static const requiredKeys = <String>{
|
||||||
'birthDate',
|
'birthDate',
|
||||||
'faceClusterId',
|
|
||||||
'id',
|
'id',
|
||||||
'isHidden',
|
'isHidden',
|
||||||
'name',
|
'name',
|
||||||
|
|||||||
@@ -1,107 +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 SharingOptionsResponseDto {
|
|
||||||
/// Returns a new [SharingOptionsResponseDto] instance.
|
|
||||||
SharingOptionsResponseDto({
|
|
||||||
required this.inTimeline,
|
|
||||||
this.permissions = const [],
|
|
||||||
});
|
|
||||||
|
|
||||||
bool inTimeline;
|
|
||||||
|
|
||||||
List<SharingPermission> permissions;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) => identical(this, other) || other is SharingOptionsResponseDto &&
|
|
||||||
other.inTimeline == inTimeline &&
|
|
||||||
_deepEquality.equals(other.permissions, permissions);
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode =>
|
|
||||||
// ignore: unnecessary_parenthesis
|
|
||||||
(inTimeline.hashCode) +
|
|
||||||
(permissions.hashCode);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'SharingOptionsResponseDto[inTimeline=$inTimeline, permissions=$permissions]';
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
final json = <String, dynamic>{};
|
|
||||||
json[r'inTimeline'] = this.inTimeline;
|
|
||||||
json[r'permissions'] = this.permissions;
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a new [SharingOptionsResponseDto] instance and imports its values from
|
|
||||||
/// [value] if it's a [Map], null otherwise.
|
|
||||||
// ignore: prefer_constructors_over_static_methods
|
|
||||||
static SharingOptionsResponseDto? fromJson(dynamic value) {
|
|
||||||
upgradeDto(value, "SharingOptionsResponseDto");
|
|
||||||
if (value is Map) {
|
|
||||||
final json = value.cast<String, dynamic>();
|
|
||||||
|
|
||||||
return SharingOptionsResponseDto(
|
|
||||||
inTimeline: mapValueOfType<bool>(json, r'inTimeline')!,
|
|
||||||
permissions: SharingPermission.listFromJson(json[r'permissions']),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<SharingOptionsResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
|
||||||
final result = <SharingOptionsResponseDto>[];
|
|
||||||
if (json is List && json.isNotEmpty) {
|
|
||||||
for (final row in json) {
|
|
||||||
final value = SharingOptionsResponseDto.fromJson(row);
|
|
||||||
if (value != null) {
|
|
||||||
result.add(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result.toList(growable: growable);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Map<String, SharingOptionsResponseDto> mapFromJson(dynamic json) {
|
|
||||||
final map = <String, SharingOptionsResponseDto>{};
|
|
||||||
if (json is Map && json.isNotEmpty) {
|
|
||||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
|
||||||
for (final entry in json.entries) {
|
|
||||||
final value = SharingOptionsResponseDto.fromJson(entry.value);
|
|
||||||
if (value != null) {
|
|
||||||
map[entry.key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
// maps a json object with a list of SharingOptionsResponseDto-objects as value to a dart map
|
|
||||||
static Map<String, List<SharingOptionsResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
|
||||||
final map = <String, List<SharingOptionsResponseDto>>{};
|
|
||||||
if (json is Map && json.isNotEmpty) {
|
|
||||||
// ignore: parameter_assignments
|
|
||||||
json = json.cast<String, dynamic>();
|
|
||||||
for (final entry in json.entries) {
|
|
||||||
map[entry.key] = SharingOptionsResponseDto.listFromJson(entry.value, growable: growable,);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The list of required keys that must be present in a JSON.
|
|
||||||
static const requiredKeys = <String>{
|
|
||||||
'inTimeline',
|
|
||||||
'permissions',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
-112
@@ -1,112 +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;
|
|
||||||
|
|
||||||
/// Sharing permission schema
|
|
||||||
class SharingPermission {
|
|
||||||
/// Instantiate a new enum with the provided [value].
|
|
||||||
const SharingPermission._(this.value);
|
|
||||||
|
|
||||||
/// The underlying value of this enum member.
|
|
||||||
final String value;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => value;
|
|
||||||
|
|
||||||
String toJson() => value;
|
|
||||||
|
|
||||||
static const all = SharingPermission._(r'all');
|
|
||||||
static const assetPeriodRead = SharingPermission._(r'asset.read');
|
|
||||||
static const assetPeriodUpdate = SharingPermission._(r'asset.update');
|
|
||||||
static const assetPeriodEdit = SharingPermission._(r'asset.edit');
|
|
||||||
static const assetPeriodDelete = SharingPermission._(r'asset.delete');
|
|
||||||
static const assetPeriodShare = SharingPermission._(r'asset.share');
|
|
||||||
static const exifPeriodRead = SharingPermission._(r'exif.read');
|
|
||||||
static const personPeriodRead = SharingPermission._(r'person.read');
|
|
||||||
static const personPeriodUpdate = SharingPermission._(r'person.update');
|
|
||||||
static const personPeriodMerge = SharingPermission._(r'person.merge');
|
|
||||||
static const personPeriodDelete = SharingPermission._(r'person.delete');
|
|
||||||
|
|
||||||
/// List of all possible values in this [enum][SharingPermission].
|
|
||||||
static const values = <SharingPermission>[
|
|
||||||
all,
|
|
||||||
assetPeriodRead,
|
|
||||||
assetPeriodUpdate,
|
|
||||||
assetPeriodEdit,
|
|
||||||
assetPeriodDelete,
|
|
||||||
assetPeriodShare,
|
|
||||||
exifPeriodRead,
|
|
||||||
personPeriodRead,
|
|
||||||
personPeriodUpdate,
|
|
||||||
personPeriodMerge,
|
|
||||||
personPeriodDelete,
|
|
||||||
];
|
|
||||||
|
|
||||||
static SharingPermission? fromJson(dynamic value) => SharingPermissionTypeTransformer().decode(value);
|
|
||||||
|
|
||||||
static List<SharingPermission> listFromJson(dynamic json, {bool growable = false,}) {
|
|
||||||
final result = <SharingPermission>[];
|
|
||||||
if (json is List && json.isNotEmpty) {
|
|
||||||
for (final row in json) {
|
|
||||||
final value = SharingPermission.fromJson(row);
|
|
||||||
if (value != null) {
|
|
||||||
result.add(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result.toList(growable: growable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Transformation class that can [encode] an instance of [SharingPermission] to String,
|
|
||||||
/// and [decode] dynamic data back to [SharingPermission].
|
|
||||||
class SharingPermissionTypeTransformer {
|
|
||||||
factory SharingPermissionTypeTransformer() => _instance ??= const SharingPermissionTypeTransformer._();
|
|
||||||
|
|
||||||
const SharingPermissionTypeTransformer._();
|
|
||||||
|
|
||||||
String encode(SharingPermission data) => data.value;
|
|
||||||
|
|
||||||
/// Decodes a [dynamic value][data] to a SharingPermission.
|
|
||||||
///
|
|
||||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
|
||||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
|
||||||
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
|
||||||
///
|
|
||||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
|
||||||
/// and users are still using an old app with the old code.
|
|
||||||
SharingPermission? decode(dynamic data, {bool allowNull = true}) {
|
|
||||||
if (data != null) {
|
|
||||||
switch (data) {
|
|
||||||
case r'all': return SharingPermission.all;
|
|
||||||
case r'asset.read': return SharingPermission.assetPeriodRead;
|
|
||||||
case r'asset.update': return SharingPermission.assetPeriodUpdate;
|
|
||||||
case r'asset.edit': return SharingPermission.assetPeriodEdit;
|
|
||||||
case r'asset.delete': return SharingPermission.assetPeriodDelete;
|
|
||||||
case r'asset.share': return SharingPermission.assetPeriodShare;
|
|
||||||
case r'exif.read': return SharingPermission.exifPeriodRead;
|
|
||||||
case r'person.read': return SharingPermission.personPeriodRead;
|
|
||||||
case r'person.update': return SharingPermission.personPeriodUpdate;
|
|
||||||
case r'person.merge': return SharingPermission.personPeriodMerge;
|
|
||||||
case r'person.delete': return SharingPermission.personPeriodDelete;
|
|
||||||
default:
|
|
||||||
if (!allowNull) {
|
|
||||||
throw ArgumentError('Unknown enum value to decode: $data');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Singleton [SharingPermissionTypeTransformer] instance.
|
|
||||||
static SharingPermissionTypeTransformer? _instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
+14
-14
@@ -19,11 +19,11 @@ class SyncAssetFaceV2 {
|
|||||||
required this.boundingBoxY1,
|
required this.boundingBoxY1,
|
||||||
required this.boundingBoxY2,
|
required this.boundingBoxY2,
|
||||||
required this.deletedAt,
|
required this.deletedAt,
|
||||||
required this.faceClusterId,
|
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.imageHeight,
|
required this.imageHeight,
|
||||||
required this.imageWidth,
|
required this.imageWidth,
|
||||||
required this.isVisible,
|
required this.isVisible,
|
||||||
|
required this.personId,
|
||||||
required this.sourceType,
|
required this.sourceType,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -57,9 +57,6 @@ class SyncAssetFaceV2 {
|
|||||||
/// Face deleted at
|
/// Face deleted at
|
||||||
DateTime? deletedAt;
|
DateTime? deletedAt;
|
||||||
|
|
||||||
/// Person ID
|
|
||||||
String? faceClusterId;
|
|
||||||
|
|
||||||
/// Asset face ID
|
/// Asset face ID
|
||||||
String id;
|
String id;
|
||||||
|
|
||||||
@@ -78,6 +75,9 @@ class SyncAssetFaceV2 {
|
|||||||
/// Is the face visible in the asset
|
/// Is the face visible in the asset
|
||||||
bool isVisible;
|
bool isVisible;
|
||||||
|
|
||||||
|
/// Person ID
|
||||||
|
String? personId;
|
||||||
|
|
||||||
/// Source type
|
/// Source type
|
||||||
String sourceType;
|
String sourceType;
|
||||||
|
|
||||||
@@ -89,11 +89,11 @@ class SyncAssetFaceV2 {
|
|||||||
other.boundingBoxY1 == boundingBoxY1 &&
|
other.boundingBoxY1 == boundingBoxY1 &&
|
||||||
other.boundingBoxY2 == boundingBoxY2 &&
|
other.boundingBoxY2 == boundingBoxY2 &&
|
||||||
other.deletedAt == deletedAt &&
|
other.deletedAt == deletedAt &&
|
||||||
other.faceClusterId == faceClusterId &&
|
|
||||||
other.id == id &&
|
other.id == id &&
|
||||||
other.imageHeight == imageHeight &&
|
other.imageHeight == imageHeight &&
|
||||||
other.imageWidth == imageWidth &&
|
other.imageWidth == imageWidth &&
|
||||||
other.isVisible == isVisible &&
|
other.isVisible == isVisible &&
|
||||||
|
other.personId == personId &&
|
||||||
other.sourceType == sourceType;
|
other.sourceType == sourceType;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -105,15 +105,15 @@ class SyncAssetFaceV2 {
|
|||||||
(boundingBoxY1.hashCode) +
|
(boundingBoxY1.hashCode) +
|
||||||
(boundingBoxY2.hashCode) +
|
(boundingBoxY2.hashCode) +
|
||||||
(deletedAt == null ? 0 : deletedAt!.hashCode) +
|
(deletedAt == null ? 0 : deletedAt!.hashCode) +
|
||||||
(faceClusterId == null ? 0 : faceClusterId!.hashCode) +
|
|
||||||
(id.hashCode) +
|
(id.hashCode) +
|
||||||
(imageHeight.hashCode) +
|
(imageHeight.hashCode) +
|
||||||
(imageWidth.hashCode) +
|
(imageWidth.hashCode) +
|
||||||
(isVisible.hashCode) +
|
(isVisible.hashCode) +
|
||||||
|
(personId == null ? 0 : personId!.hashCode) +
|
||||||
(sourceType.hashCode);
|
(sourceType.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SyncAssetFaceV2[assetId=$assetId, boundingBoxX1=$boundingBoxX1, boundingBoxX2=$boundingBoxX2, boundingBoxY1=$boundingBoxY1, boundingBoxY2=$boundingBoxY2, deletedAt=$deletedAt, faceClusterId=$faceClusterId, id=$id, imageHeight=$imageHeight, imageWidth=$imageWidth, isVisible=$isVisible, sourceType=$sourceType]';
|
String toString() => 'SyncAssetFaceV2[assetId=$assetId, boundingBoxX1=$boundingBoxX1, boundingBoxX2=$boundingBoxX2, boundingBoxY1=$boundingBoxY1, boundingBoxY2=$boundingBoxY2, deletedAt=$deletedAt, id=$id, imageHeight=$imageHeight, imageWidth=$imageWidth, isVisible=$isVisible, personId=$personId, sourceType=$sourceType]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@@ -128,16 +128,16 @@ class SyncAssetFaceV2 {
|
|||||||
: this.deletedAt!.toUtc().toIso8601String();
|
: this.deletedAt!.toUtc().toIso8601String();
|
||||||
} else {
|
} else {
|
||||||
// json[r'deletedAt'] = null;
|
// json[r'deletedAt'] = null;
|
||||||
}
|
|
||||||
if (this.faceClusterId != null) {
|
|
||||||
json[r'faceClusterId'] = this.faceClusterId;
|
|
||||||
} else {
|
|
||||||
// json[r'faceClusterId'] = null;
|
|
||||||
}
|
}
|
||||||
json[r'id'] = this.id;
|
json[r'id'] = this.id;
|
||||||
json[r'imageHeight'] = this.imageHeight;
|
json[r'imageHeight'] = this.imageHeight;
|
||||||
json[r'imageWidth'] = this.imageWidth;
|
json[r'imageWidth'] = this.imageWidth;
|
||||||
json[r'isVisible'] = this.isVisible;
|
json[r'isVisible'] = this.isVisible;
|
||||||
|
if (this.personId != null) {
|
||||||
|
json[r'personId'] = this.personId;
|
||||||
|
} else {
|
||||||
|
// json[r'personId'] = null;
|
||||||
|
}
|
||||||
json[r'sourceType'] = this.sourceType;
|
json[r'sourceType'] = this.sourceType;
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
@@ -157,11 +157,11 @@ class SyncAssetFaceV2 {
|
|||||||
boundingBoxY1: mapValueOfType<int>(json, r'boundingBoxY1')!,
|
boundingBoxY1: mapValueOfType<int>(json, r'boundingBoxY1')!,
|
||||||
boundingBoxY2: mapValueOfType<int>(json, r'boundingBoxY2')!,
|
boundingBoxY2: mapValueOfType<int>(json, r'boundingBoxY2')!,
|
||||||
deletedAt: mapDateTime(json, r'deletedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
|
deletedAt: mapDateTime(json, r'deletedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
|
||||||
faceClusterId: mapValueOfType<String>(json, r'faceClusterId'),
|
|
||||||
id: mapValueOfType<String>(json, r'id')!,
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
imageHeight: mapValueOfType<int>(json, r'imageHeight')!,
|
imageHeight: mapValueOfType<int>(json, r'imageHeight')!,
|
||||||
imageWidth: mapValueOfType<int>(json, r'imageWidth')!,
|
imageWidth: mapValueOfType<int>(json, r'imageWidth')!,
|
||||||
isVisible: mapValueOfType<bool>(json, r'isVisible')!,
|
isVisible: mapValueOfType<bool>(json, r'isVisible')!,
|
||||||
|
personId: mapValueOfType<String>(json, r'personId'),
|
||||||
sourceType: mapValueOfType<String>(json, r'sourceType')!,
|
sourceType: mapValueOfType<String>(json, r'sourceType')!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -216,11 +216,11 @@ class SyncAssetFaceV2 {
|
|||||||
'boundingBoxY1',
|
'boundingBoxY1',
|
||||||
'boundingBoxY2',
|
'boundingBoxY2',
|
||||||
'deletedAt',
|
'deletedAt',
|
||||||
'faceClusterId',
|
|
||||||
'id',
|
'id',
|
||||||
'imageHeight',
|
'imageHeight',
|
||||||
'imageWidth',
|
'imageWidth',
|
||||||
'isVisible',
|
'isVisible',
|
||||||
|
'personId',
|
||||||
'sourceType',
|
'sourceType',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,107 +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 UpdateSharingOptionsDto {
|
|
||||||
/// Returns a new [UpdateSharingOptionsDto] instance.
|
|
||||||
UpdateSharingOptionsDto({
|
|
||||||
required this.inTimeline,
|
|
||||||
this.permissions = const [],
|
|
||||||
});
|
|
||||||
|
|
||||||
bool inTimeline;
|
|
||||||
|
|
||||||
List<SharingPermission> permissions;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) => identical(this, other) || other is UpdateSharingOptionsDto &&
|
|
||||||
other.inTimeline == inTimeline &&
|
|
||||||
_deepEquality.equals(other.permissions, permissions);
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode =>
|
|
||||||
// ignore: unnecessary_parenthesis
|
|
||||||
(inTimeline.hashCode) +
|
|
||||||
(permissions.hashCode);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'UpdateSharingOptionsDto[inTimeline=$inTimeline, permissions=$permissions]';
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
final json = <String, dynamic>{};
|
|
||||||
json[r'inTimeline'] = this.inTimeline;
|
|
||||||
json[r'permissions'] = this.permissions;
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a new [UpdateSharingOptionsDto] instance and imports its values from
|
|
||||||
/// [value] if it's a [Map], null otherwise.
|
|
||||||
// ignore: prefer_constructors_over_static_methods
|
|
||||||
static UpdateSharingOptionsDto? fromJson(dynamic value) {
|
|
||||||
upgradeDto(value, "UpdateSharingOptionsDto");
|
|
||||||
if (value is Map) {
|
|
||||||
final json = value.cast<String, dynamic>();
|
|
||||||
|
|
||||||
return UpdateSharingOptionsDto(
|
|
||||||
inTimeline: mapValueOfType<bool>(json, r'inTimeline')!,
|
|
||||||
permissions: SharingPermission.listFromJson(json[r'permissions']),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<UpdateSharingOptionsDto> listFromJson(dynamic json, {bool growable = false,}) {
|
|
||||||
final result = <UpdateSharingOptionsDto>[];
|
|
||||||
if (json is List && json.isNotEmpty) {
|
|
||||||
for (final row in json) {
|
|
||||||
final value = UpdateSharingOptionsDto.fromJson(row);
|
|
||||||
if (value != null) {
|
|
||||||
result.add(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result.toList(growable: growable);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Map<String, UpdateSharingOptionsDto> mapFromJson(dynamic json) {
|
|
||||||
final map = <String, UpdateSharingOptionsDto>{};
|
|
||||||
if (json is Map && json.isNotEmpty) {
|
|
||||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
|
||||||
for (final entry in json.entries) {
|
|
||||||
final value = UpdateSharingOptionsDto.fromJson(entry.value);
|
|
||||||
if (value != null) {
|
|
||||||
map[entry.key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
// maps a json object with a list of UpdateSharingOptionsDto-objects as value to a dart map
|
|
||||||
static Map<String, List<UpdateSharingOptionsDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
|
||||||
final map = <String, List<UpdateSharingOptionsDto>>{};
|
|
||||||
if (json is Map && json.isNotEmpty) {
|
|
||||||
// ignore: parameter_assignments
|
|
||||||
json = json.cast<String, dynamic>();
|
|
||||||
for (final entry in json.entries) {
|
|
||||||
map[entry.key] = UpdateSharingOptionsDto.listFromJson(entry.value, growable: growable,);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The list of required keys that must be present in a JSON.
|
|
||||||
static const requiredKeys = <String>{
|
|
||||||
'inTimeline',
|
|
||||||
'permissions',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -2277,121 +2277,6 @@
|
|||||||
"x-immich-permission": "album.read"
|
"x-immich-permission": "album.read"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/albums/{id}/user/self": {
|
|
||||||
"get": {
|
|
||||||
"description": "Get the own sharing permissions in a specific album.",
|
|
||||||
"operationId": "getOwnAlbumUser",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"required": true,
|
|
||||||
"in": "path",
|
|
||||||
"schema": {
|
|
||||||
"format": "uuid",
|
|
||||||
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/SharingOptionsResponseDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearer": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"api_key": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"summary": "Get own sharing permissions",
|
|
||||||
"tags": [
|
|
||||||
"Albums"
|
|
||||||
],
|
|
||||||
"x-immich-history": [
|
|
||||||
{
|
|
||||||
"version": "v3",
|
|
||||||
"state": "Added"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "v3",
|
|
||||||
"state": "Stable"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"x-immich-permission": "albumAsset.create",
|
|
||||||
"x-immich-state": "Stable"
|
|
||||||
},
|
|
||||||
"put": {
|
|
||||||
"description": "Change the own sharing permissions in a specific album.",
|
|
||||||
"operationId": "updateOwnAlbumUser",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"required": true,
|
|
||||||
"in": "path",
|
|
||||||
"schema": {
|
|
||||||
"format": "uuid",
|
|
||||||
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/UpdateSharingOptionsDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"204": {
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearer": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"api_key": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"summary": "Update own sharing permissions",
|
|
||||||
"tags": [
|
|
||||||
"Albums"
|
|
||||||
],
|
|
||||||
"x-immich-history": [
|
|
||||||
{
|
|
||||||
"version": "v3",
|
|
||||||
"state": "Added"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "v3",
|
|
||||||
"state": "Stable"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"x-immich-permission": "albumAsset.create",
|
|
||||||
"x-immich-state": "Stable"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/albums/{id}/user/{userId}": {
|
"/albums/{id}/user/{userId}": {
|
||||||
"delete": {
|
"delete": {
|
||||||
"description": "Remove a user from an album. Use an ID of \"me\" to leave a shared album.",
|
"description": "Remove a user from an album. Use an ID of \"me\" to leave a shared album.",
|
||||||
@@ -8460,7 +8345,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/MergeFaceClusterDto"
|
"$ref": "#/components/schemas/MergePersonDto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -17057,12 +16942,6 @@
|
|||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
"permissions": {
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/SharingPermission"
|
|
||||||
},
|
|
||||||
"type": "array"
|
|
||||||
},
|
|
||||||
"resized": {
|
"resized": {
|
||||||
"description": "Is resized",
|
"description": "Is resized",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
@@ -17134,7 +17013,6 @@
|
|||||||
"originalFileName",
|
"originalFileName",
|
||||||
"originalPath",
|
"originalPath",
|
||||||
"ownerId",
|
"ownerId",
|
||||||
"permissions",
|
|
||||||
"thumbhash",
|
"thumbhash",
|
||||||
"type",
|
"type",
|
||||||
"updatedAt",
|
"updatedAt",
|
||||||
@@ -18194,7 +18072,6 @@
|
|||||||
"DatabaseBackup",
|
"DatabaseBackup",
|
||||||
"FacialRecognitionQueueAll",
|
"FacialRecognitionQueueAll",
|
||||||
"FacialRecognition",
|
"FacialRecognition",
|
||||||
"FacialRecognitionMerge",
|
|
||||||
"FileDelete",
|
"FileDelete",
|
||||||
"FileMigrationQueueAll",
|
"FileMigrationQueueAll",
|
||||||
"LibraryDeleteCheck",
|
"LibraryDeleteCheck",
|
||||||
@@ -18604,8 +18481,7 @@
|
|||||||
"user-cleanup",
|
"user-cleanup",
|
||||||
"memory-cleanup",
|
"memory-cleanup",
|
||||||
"memory-create",
|
"memory-create",
|
||||||
"backup-database",
|
"backup-database"
|
||||||
"person-group-merge"
|
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -18931,10 +18807,10 @@
|
|||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"MergeFaceClusterDto": {
|
"MergePersonDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"ids": {
|
"ids": {
|
||||||
"description": "Face cluster IDs to merge",
|
"description": "Person IDs to merge",
|
||||||
"items": {
|
"items": {
|
||||||
"format": "uuid",
|
"format": "uuid",
|
||||||
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
|
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
|
||||||
@@ -19959,11 +19835,6 @@
|
|||||||
],
|
],
|
||||||
"x-immich-state": "Stable"
|
"x-immich-state": "Stable"
|
||||||
},
|
},
|
||||||
"faceClusterId": {
|
|
||||||
"description": "Face cluster ID",
|
|
||||||
"nullable": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"id": {
|
"id": {
|
||||||
"description": "Person ID",
|
"description": "Person ID",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -20014,7 +19885,6 @@
|
|||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"birthDate",
|
"birthDate",
|
||||||
"faceClusterId",
|
|
||||||
"id",
|
"id",
|
||||||
"isHidden",
|
"isHidden",
|
||||||
"name",
|
"name",
|
||||||
@@ -21927,41 +21797,6 @@
|
|||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"SharingOptionsResponseDto": {
|
|
||||||
"properties": {
|
|
||||||
"inTimeline": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"permissions": {
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/SharingPermission"
|
|
||||||
},
|
|
||||||
"type": "array"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"inTimeline",
|
|
||||||
"permissions"
|
|
||||||
],
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"SharingPermission": {
|
|
||||||
"description": "Sharing permission schema",
|
|
||||||
"enum": [
|
|
||||||
"all",
|
|
||||||
"asset.read",
|
|
||||||
"asset.update",
|
|
||||||
"asset.edit",
|
|
||||||
"asset.delete",
|
|
||||||
"asset.share",
|
|
||||||
"exif.read",
|
|
||||||
"person.read",
|
|
||||||
"person.update",
|
|
||||||
"person.merge",
|
|
||||||
"person.delete"
|
|
||||||
],
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"SignUpDto": {
|
"SignUpDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"email": {
|
"email": {
|
||||||
@@ -23058,11 +22893,6 @@
|
|||||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"faceClusterId": {
|
|
||||||
"description": "Person ID",
|
|
||||||
"nullable": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"id": {
|
"id": {
|
||||||
"description": "Asset face ID",
|
"description": "Asset face ID",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -23083,6 +22913,11 @@
|
|||||||
"description": "Is the face visible in the asset",
|
"description": "Is the face visible in the asset",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"personId": {
|
||||||
|
"description": "Person ID",
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"sourceType": {
|
"sourceType": {
|
||||||
"description": "Source type",
|
"description": "Source type",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -23095,11 +22930,11 @@
|
|||||||
"boundingBoxY1",
|
"boundingBoxY1",
|
||||||
"boundingBoxY2",
|
"boundingBoxY2",
|
||||||
"deletedAt",
|
"deletedAt",
|
||||||
"faceClusterId",
|
|
||||||
"id",
|
"id",
|
||||||
"imageHeight",
|
"imageHeight",
|
||||||
"imageWidth",
|
"imageWidth",
|
||||||
"isVisible",
|
"isVisible",
|
||||||
|
"personId",
|
||||||
"sourceType"
|
"sourceType"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
@@ -25591,24 +25426,6 @@
|
|||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"UpdateSharingOptionsDto": {
|
|
||||||
"properties": {
|
|
||||||
"inTimeline": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"permissions": {
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/SharingPermission"
|
|
||||||
},
|
|
||||||
"type": "array"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"inTimeline",
|
|
||||||
"permissions"
|
|
||||||
],
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"UsageByUserDto": {
|
"UsageByUserDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"photos": {
|
"photos": {
|
||||||
|
|||||||
@@ -555,14 +555,6 @@ export type MapMarkerResponseDto = {
|
|||||||
/** State/Province name */
|
/** State/Province name */
|
||||||
state: string | null;
|
state: string | null;
|
||||||
};
|
};
|
||||||
export type SharingOptionsResponseDto = {
|
|
||||||
inTimeline: boolean;
|
|
||||||
permissions: SharingPermission[];
|
|
||||||
};
|
|
||||||
export type UpdateSharingOptionsDto = {
|
|
||||||
inTimeline: boolean;
|
|
||||||
permissions: SharingPermission[];
|
|
||||||
};
|
|
||||||
export type UpdateAlbumUserDto = {
|
export type UpdateAlbumUserDto = {
|
||||||
role: AlbumUserRole;
|
role: AlbumUserRole;
|
||||||
};
|
};
|
||||||
@@ -800,8 +792,6 @@ export type PersonResponseDto = {
|
|||||||
birthDate: string | null;
|
birthDate: string | null;
|
||||||
/** Person color (hex) */
|
/** Person color (hex) */
|
||||||
color?: string;
|
color?: string;
|
||||||
/** Face cluster ID */
|
|
||||||
faceClusterId: string | null;
|
|
||||||
/** Person ID */
|
/** Person ID */
|
||||||
id: string;
|
id: string;
|
||||||
/** Is favorite */
|
/** Is favorite */
|
||||||
@@ -885,7 +875,6 @@ export type AssetResponseDto = {
|
|||||||
/** Owner user ID */
|
/** Owner user ID */
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
people?: PersonResponseDto[];
|
people?: PersonResponseDto[];
|
||||||
permissions: SharingPermission[];
|
|
||||||
/** Is resized */
|
/** Is resized */
|
||||||
resized?: boolean;
|
resized?: boolean;
|
||||||
stack?: (AssetStackResponseDto) | null;
|
stack?: (AssetStackResponseDto) | null;
|
||||||
@@ -1471,8 +1460,8 @@ export type PersonUpdateDto = {
|
|||||||
/** Person name */
|
/** Person name */
|
||||||
name?: string;
|
name?: string;
|
||||||
};
|
};
|
||||||
export type MergeFaceClusterDto = {
|
export type MergePersonDto = {
|
||||||
/** Face cluster IDs to merge */
|
/** Person IDs to merge */
|
||||||
ids: string[];
|
ids: string[];
|
||||||
};
|
};
|
||||||
export type AssetFaceUpdateItem = {
|
export type AssetFaceUpdateItem = {
|
||||||
@@ -2933,8 +2922,6 @@ export type SyncAssetFaceV2 = {
|
|||||||
boundingBoxY2: number;
|
boundingBoxY2: number;
|
||||||
/** Face deleted at */
|
/** Face deleted at */
|
||||||
deletedAt: string | null;
|
deletedAt: string | null;
|
||||||
/** Person ID */
|
|
||||||
faceClusterId: string | null;
|
|
||||||
/** Asset face ID */
|
/** Asset face ID */
|
||||||
id: string;
|
id: string;
|
||||||
/** Image height */
|
/** Image height */
|
||||||
@@ -2943,6 +2930,8 @@ export type SyncAssetFaceV2 = {
|
|||||||
imageWidth: number;
|
imageWidth: number;
|
||||||
/** Is the face visible in the asset */
|
/** Is the face visible in the asset */
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
|
/** Person ID */
|
||||||
|
personId: string | null;
|
||||||
/** Source type */
|
/** Source type */
|
||||||
sourceType: string;
|
sourceType: string;
|
||||||
};
|
};
|
||||||
@@ -3738,32 +3727,6 @@ export function getAlbumMapMarkers({ id, key, slug }: {
|
|||||||
...opts
|
...opts
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Get own sharing permissions
|
|
||||||
*/
|
|
||||||
export function getOwnAlbumUser({ id }: {
|
|
||||||
id: string;
|
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
|
||||||
status: 200;
|
|
||||||
data: SharingOptionsResponseDto;
|
|
||||||
}>(`/albums/${encodeURIComponent(id)}/user/self`, {
|
|
||||||
...opts
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Update own sharing permissions
|
|
||||||
*/
|
|
||||||
export function updateOwnAlbumUser({ id, updateSharingOptionsDto }: {
|
|
||||||
id: string;
|
|
||||||
updateSharingOptionsDto: UpdateSharingOptionsDto;
|
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
|
||||||
return oazapfts.ok(oazapfts.fetchText(`/albums/${encodeURIComponent(id)}/user/self`, oazapfts.json({
|
|
||||||
...opts,
|
|
||||||
method: "PUT",
|
|
||||||
body: updateSharingOptionsDto
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* Remove user from album
|
* Remove user from album
|
||||||
*/
|
*/
|
||||||
@@ -5168,9 +5131,9 @@ export function updatePerson({ id, personUpdateDto }: {
|
|||||||
/**
|
/**
|
||||||
* Merge people
|
* Merge people
|
||||||
*/
|
*/
|
||||||
export function mergePerson({ id, mergeFaceClusterDto }: {
|
export function mergePerson({ id, mergePersonDto }: {
|
||||||
id: string;
|
id: string;
|
||||||
mergeFaceClusterDto: MergeFaceClusterDto;
|
mergePersonDto: MergePersonDto;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
@@ -5178,7 +5141,7 @@ export function mergePerson({ id, mergeFaceClusterDto }: {
|
|||||||
}>(`/people/${encodeURIComponent(id)}/merge`, oazapfts.json({
|
}>(`/people/${encodeURIComponent(id)}/merge`, oazapfts.json({
|
||||||
...opts,
|
...opts,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: mergeFaceClusterDto
|
body: mergePersonDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@@ -6825,19 +6788,6 @@ export enum BulkIdErrorReason {
|
|||||||
Unknown = "unknown",
|
Unknown = "unknown",
|
||||||
Validation = "validation"
|
Validation = "validation"
|
||||||
}
|
}
|
||||||
export enum SharingPermission {
|
|
||||||
All = "all",
|
|
||||||
AssetRead = "asset.read",
|
|
||||||
AssetUpdate = "asset.update",
|
|
||||||
AssetEdit = "asset.edit",
|
|
||||||
AssetDelete = "asset.delete",
|
|
||||||
AssetShare = "asset.share",
|
|
||||||
ExifRead = "exif.read",
|
|
||||||
PersonRead = "person.read",
|
|
||||||
PersonUpdate = "person.update",
|
|
||||||
PersonMerge = "person.merge",
|
|
||||||
PersonDelete = "person.delete"
|
|
||||||
}
|
|
||||||
export enum Permission {
|
export enum Permission {
|
||||||
All = "all",
|
All = "all",
|
||||||
ActivityCreate = "activity.create",
|
ActivityCreate = "activity.create",
|
||||||
@@ -7045,8 +6995,7 @@ export enum ManualJobName {
|
|||||||
UserCleanup = "user-cleanup",
|
UserCleanup = "user-cleanup",
|
||||||
MemoryCleanup = "memory-cleanup",
|
MemoryCleanup = "memory-cleanup",
|
||||||
MemoryCreate = "memory-create",
|
MemoryCreate = "memory-create",
|
||||||
BackupDatabase = "backup-database",
|
BackupDatabase = "backup-database"
|
||||||
PersonGroupMerge = "person-group-merge"
|
|
||||||
}
|
}
|
||||||
export enum QueueName {
|
export enum QueueName {
|
||||||
ThumbnailGeneration = "thumbnailGeneration",
|
ThumbnailGeneration = "thumbnailGeneration",
|
||||||
@@ -7123,7 +7072,6 @@ export enum JobName {
|
|||||||
DatabaseBackup = "DatabaseBackup",
|
DatabaseBackup = "DatabaseBackup",
|
||||||
FacialRecognitionQueueAll = "FacialRecognitionQueueAll",
|
FacialRecognitionQueueAll = "FacialRecognitionQueueAll",
|
||||||
FacialRecognition = "FacialRecognition",
|
FacialRecognition = "FacialRecognition",
|
||||||
FacialRecognitionMerge = "FacialRecognitionMerge",
|
|
||||||
FileDelete = "FileDelete",
|
FileDelete = "FileDelete",
|
||||||
FileMigrationQueueAll = "FileMigrationQueueAll",
|
FileMigrationQueueAll = "FileMigrationQueueAll",
|
||||||
LibraryDeleteCheck = "LibraryDeleteCheck",
|
LibraryDeleteCheck = "LibraryDeleteCheck",
|
||||||
|
|||||||
Generated
+5
-5
@@ -758,8 +758,8 @@ importers:
|
|||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/sdk
|
version: link:../packages/sdk
|
||||||
'@immich/ui':
|
'@immich/ui':
|
||||||
specifier: ^0.77.0
|
specifier: ^0.79.0
|
||||||
version: 0.77.3(@sveltejs/kit@2.60.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.55.8(@typescript-eslint/types@8.59.4))(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4))(typescript@6.0.3)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4))
|
version: 0.79.0(@sveltejs/kit@2.60.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.55.8(@typescript-eslint/types@8.59.4))(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4))(typescript@6.0.3)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4))
|
||||||
'@mapbox/mapbox-gl-rtl-text':
|
'@mapbox/mapbox-gl-rtl-text':
|
||||||
specifier: 0.4.0
|
specifier: 0.4.0
|
||||||
version: 0.4.0
|
version: 0.4.0
|
||||||
@@ -3204,8 +3204,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-O1SJ+BbeFVsUTF4af1MfagJZM+lPgLjI8lQ3SZNjpo8SGJReSbUl2ii03OKuGni/G0yp2GnRLpOTNSHYGtVrcg==}
|
resolution: {integrity: sha512-O1SJ+BbeFVsUTF4af1MfagJZM+lPgLjI8lQ3SZNjpo8SGJReSbUl2ii03OKuGni/G0yp2GnRLpOTNSHYGtVrcg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@immich/ui@0.77.3':
|
'@immich/ui@0.79.0':
|
||||||
resolution: {integrity: sha512-h3jrYE3JyGDOwXF7A4tVUHenP0L7TsDV22FyFInBTdwlWjjXoknwE1HWeTvvLxLeMuO5SHCZ9Q2D2al84xVjNw==}
|
resolution: {integrity: sha512-UEQZrP8CTc4Kth1xCV8/6Xmk1P51GQKISC7vKqcrM0BO0fxCaNwJK8Ocn6R8baVqH52JYfPb1yQR9bweBnCjXw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@sveltejs/kit': ^2.13.0
|
'@sveltejs/kit': ^2.13.0
|
||||||
svelte: ^5.0.0
|
svelte: ^5.0.0
|
||||||
@@ -15879,7 +15879,7 @@ snapshots:
|
|||||||
pg-connection-string: 2.13.0
|
pg-connection-string: 2.13.0
|
||||||
postgres: 3.4.9
|
postgres: 3.4.9
|
||||||
|
|
||||||
'@immich/ui@0.77.3(@sveltejs/kit@2.60.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.55.8(@typescript-eslint/types@8.59.4))(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4))(typescript@6.0.3)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4))':
|
'@immich/ui@0.79.0(@sveltejs/kit@2.60.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.55.8(@typescript-eslint/types@8.59.4))(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4))(typescript@6.0.3)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@internationalized/date': 3.12.1
|
'@internationalized/date': 3.12.1
|
||||||
'@mdi/js': 7.4.47
|
'@mdi/js': 7.4.47
|
||||||
|
|||||||
+2
-2
@@ -88,8 +88,8 @@ ENV NODE_ENV=production \
|
|||||||
COPY --from=server /output/server-pruned ./server
|
COPY --from=server /output/server-pruned ./server
|
||||||
COPY --from=web /usr/src/app/web/build /build/www
|
COPY --from=web /usr/src/app/web/build /build/www
|
||||||
COPY --from=cli /output/cli-pruned ./cli
|
COPY --from=cli /output/cli-pruned ./cli
|
||||||
COPY --from=plugins /app/packages/plugin-core/dist /build/plugins/immich-plugin-core/dist
|
COPY --from=plugins /app/packages/plugin-core/dist /build/plugins/immich-core-plugin/dist
|
||||||
COPY --from=plugins /app/packages/plugin-core/manifest.json /build/plugins/immich-plugin-core/manifest.json
|
COPY --from=plugins /app/packages/plugin-core/manifest.json /build/plugins/immich-core-plugin/manifest.json
|
||||||
RUN ln -s ../../cli/bin/immich server/bin/immich
|
RUN ln -s ../../cli/bin/immich server/bin/immich
|
||||||
COPY LICENSE /licenses/LICENSE.txt
|
COPY LICENSE /licenses/LICENSE.txt
|
||||||
COPY LICENSE /LICENSE
|
COPY LICENSE /LICENSE
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
GetAlbumsDto,
|
GetAlbumsDto,
|
||||||
UpdateAlbumDto,
|
UpdateAlbumDto,
|
||||||
UpdateAlbumUserDto,
|
UpdateAlbumUserDto,
|
||||||
UpdateSharingPermissionsDto as UpdateSharingOptionsDto,
|
|
||||||
} from 'src/dtos/album.dto';
|
} from 'src/dtos/album.dto';
|
||||||
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
@@ -166,33 +165,6 @@ export class AlbumController {
|
|||||||
return this.service.addUsers(auth, id, dto);
|
return this.service.addUsers(auth, id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id/user/self')
|
|
||||||
@Authenticated({ permission: Permission.AlbumAssetCreate })
|
|
||||||
@Endpoint({
|
|
||||||
summary: 'Get own sharing permissions',
|
|
||||||
description: 'Get the own sharing permissions in a specific album.',
|
|
||||||
history: new HistoryBuilder().added('v3').stable('v3'),
|
|
||||||
})
|
|
||||||
getOwnAlbumUser(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) {
|
|
||||||
return this.service.getSelf(auth, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Put(':id/user/self')
|
|
||||||
@Authenticated({ permission: Permission.AlbumAssetCreate })
|
|
||||||
@HttpCode(HttpStatus.NO_CONTENT)
|
|
||||||
@Endpoint({
|
|
||||||
summary: 'Update own sharing permissions',
|
|
||||||
description: 'Change the own sharing permissions in a specific album.',
|
|
||||||
history: new HistoryBuilder().added('v3').stable('v3'),
|
|
||||||
})
|
|
||||||
updateOwnAlbumUser(
|
|
||||||
@Auth() auth: AuthDto,
|
|
||||||
@Param() { id }: UUIDParamDto,
|
|
||||||
@Body() dto: UpdateSharingOptionsDto,
|
|
||||||
): Promise<void> {
|
|
||||||
return this.service.updateSelf(auth, id, dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Put(':id/user/:userId')
|
@Put(':id/user/:userId')
|
||||||
@Authenticated({ permission: Permission.AlbumUserUpdate })
|
@Authenticated({ permission: Permission.AlbumUserUpdate })
|
||||||
@HttpCode(HttpStatus.NO_CONTENT)
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
|||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import {
|
import {
|
||||||
AssetFaceUpdateDto,
|
AssetFaceUpdateDto,
|
||||||
MergeFaceClusterDto,
|
MergePersonDto,
|
||||||
PeopleResponseDto,
|
PeopleResponseDto,
|
||||||
PeopleUpdateDto,
|
PeopleUpdateDto,
|
||||||
PersonCreateDto,
|
PersonCreateDto,
|
||||||
@@ -182,7 +182,7 @@ export class PersonController {
|
|||||||
mergePerson(
|
mergePerson(
|
||||||
@Auth() auth: AuthDto,
|
@Auth() auth: AuthDto,
|
||||||
@Param() { id }: UUIDParamDto,
|
@Param() { id }: UUIDParamDto,
|
||||||
@Body() dto: MergeFaceClusterDto,
|
@Body() dto: MergePersonDto,
|
||||||
): Promise<BulkIdResponseDto[]> {
|
): Promise<BulkIdResponseDto[]> {
|
||||||
return this.service.mergePerson(auth, id, dto);
|
return this.service.mergePerson(auth, id, dto);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
MemoryType,
|
MemoryType,
|
||||||
Permission,
|
Permission,
|
||||||
SharedLinkType,
|
SharedLinkType,
|
||||||
SharingPermission,
|
|
||||||
SourceType,
|
SourceType,
|
||||||
UserAvatarColor,
|
UserAvatarColor,
|
||||||
UserStatus,
|
UserStatus,
|
||||||
@@ -210,7 +209,6 @@ export type Partner = {
|
|||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
updateId: string;
|
updateId: string;
|
||||||
inTimeline: boolean;
|
inTimeline: boolean;
|
||||||
permissions: SharingPermission[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Place = {
|
export type Place = {
|
||||||
@@ -254,7 +252,6 @@ export type Person = {
|
|||||||
faceAssetId: string | null;
|
faceAssetId: string | null;
|
||||||
isHidden: boolean;
|
isHidden: boolean;
|
||||||
thumbnailPath: string;
|
thumbnailPath: string;
|
||||||
faceClusterId: string | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AssetFace = {
|
export type AssetFace = {
|
||||||
@@ -267,7 +264,7 @@ export type AssetFace = {
|
|||||||
boundingBoxY2: number;
|
boundingBoxY2: number;
|
||||||
imageHeight: number;
|
imageHeight: number;
|
||||||
imageWidth: number;
|
imageWidth: number;
|
||||||
faceClusterId: string | null;
|
personId: string | null;
|
||||||
sourceType: SourceType;
|
sourceType: SourceType;
|
||||||
person?: ShallowDehydrateObject<Person> | null;
|
person?: ShallowDehydrateObject<Person> | null;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { createZodDto } from 'nestjs-zod';
|
|||||||
import { AlbumUser, AuthSharedLink } from 'src/database';
|
import { AlbumUser, AuthSharedLink } from 'src/database';
|
||||||
import { BulkIdErrorReasonSchema } from 'src/dtos/asset-ids.response.dto';
|
import { BulkIdErrorReasonSchema } from 'src/dtos/asset-ids.response.dto';
|
||||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import { mapUser, UserResponseSchema } from 'src/dtos/user.dto';
|
import { UserResponseSchema, mapUser } from 'src/dtos/user.dto';
|
||||||
import { AlbumUserRole, AlbumUserRoleSchema, AssetOrder, AssetOrderSchema, SharingPermissionSchema } from 'src/enum';
|
import { AlbumUserRole, AlbumUserRoleSchema, AssetOrder, AssetOrderSchema } from 'src/enum';
|
||||||
import { MaybeDehydrated } from 'src/types';
|
import { MaybeDehydrated } from 'src/types';
|
||||||
import { asDateString } from 'src/utils/date';
|
import { asDateString } from 'src/utils/date';
|
||||||
import { stringToBool } from 'src/validation';
|
import { stringToBool } from 'src/validation';
|
||||||
@@ -63,14 +63,6 @@ const UpdateAlbumSchema = z
|
|||||||
})
|
})
|
||||||
.meta({ id: 'UpdateAlbumDto' });
|
.meta({ id: 'UpdateAlbumDto' });
|
||||||
|
|
||||||
const UpdateSharingOptionsSchema = z
|
|
||||||
.object({ inTimeline: z.boolean(), permissions: z.array(SharingPermissionSchema) })
|
|
||||||
.meta({ id: 'UpdateSharingOptionsDto' });
|
|
||||||
|
|
||||||
const SharingOptionsResponseSchema = z
|
|
||||||
.object({ inTimeline: z.boolean(), permissions: z.array(SharingPermissionSchema) })
|
|
||||||
.meta({ id: 'SharingOptionsResponseDto' });
|
|
||||||
|
|
||||||
const GetAlbumsSchema = z
|
const GetAlbumsSchema = z
|
||||||
.object({
|
.object({
|
||||||
isOwned: stringToBool
|
isOwned: stringToBool
|
||||||
@@ -155,8 +147,6 @@ export class UpdateAlbumDto extends createZodDto(UpdateAlbumSchema) {}
|
|||||||
export class GetAlbumsDto extends createZodDto(GetAlbumsSchema) {}
|
export class GetAlbumsDto extends createZodDto(GetAlbumsSchema) {}
|
||||||
export class AlbumStatisticsResponseDto extends createZodDto(AlbumStatisticsResponseSchema) {}
|
export class AlbumStatisticsResponseDto extends createZodDto(AlbumStatisticsResponseSchema) {}
|
||||||
export class UpdateAlbumUserDto extends createZodDto(UpdateAlbumUserSchema) {}
|
export class UpdateAlbumUserDto extends createZodDto(UpdateAlbumUserSchema) {}
|
||||||
export class UpdateSharingPermissionsDto extends createZodDto(UpdateSharingOptionsSchema) {}
|
|
||||||
export class SharingPermissionsResponseDto extends createZodDto(SharingOptionsResponseSchema) {}
|
|
||||||
export class AlbumResponseDto extends createZodDto(AlbumResponseSchema) {}
|
export class AlbumResponseDto extends createZodDto(AlbumResponseSchema) {}
|
||||||
class AlbumUserResponseDto extends createZodDto(AlbumUserResponseSchema) {}
|
class AlbumUserResponseDto extends createZodDto(AlbumUserResponseSchema) {}
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ import {
|
|||||||
AssetVisibility,
|
AssetVisibility,
|
||||||
AssetVisibilitySchema,
|
AssetVisibilitySchema,
|
||||||
ChecksumAlgorithm,
|
ChecksumAlgorithm,
|
||||||
SharingPermission,
|
|
||||||
SharingPermissionSchema,
|
|
||||||
} from 'src/enum';
|
} from 'src/enum';
|
||||||
import { MaybeDehydrated } from 'src/types';
|
import { MaybeDehydrated } from 'src/types';
|
||||||
import { hexOrBufferToBase64 } from 'src/utils/bytes';
|
import { hexOrBufferToBase64 } from 'src/utils/bytes';
|
||||||
@@ -47,7 +45,6 @@ const SanitizedAssetResponseSchema = z
|
|||||||
hasMetadata: z.boolean().describe('Whether asset has metadata'),
|
hasMetadata: z.boolean().describe('Whether asset has metadata'),
|
||||||
width: z.int().min(0).nullable().describe('Asset width'),
|
width: z.int().min(0).nullable().describe('Asset width'),
|
||||||
height: z.int().min(0).nullable().describe('Asset height'),
|
height: z.int().min(0).nullable().describe('Asset height'),
|
||||||
permissions: z.array(SharingPermissionSchema),
|
|
||||||
})
|
})
|
||||||
.meta({ id: 'SanitizedAssetResponseDto' });
|
.meta({ id: 'SanitizedAssetResponseDto' });
|
||||||
|
|
||||||
@@ -116,7 +113,6 @@ export const AssetResponseSchema = SanitizedAssetResponseSchema.extend(
|
|||||||
.boolean()
|
.boolean()
|
||||||
.describe('Is edited')
|
.describe('Is edited')
|
||||||
.meta(new HistoryBuilder().added('v2.5.0').beta('v2.5.0').getExtensions()),
|
.meta(new HistoryBuilder().added('v2.5.0').beta('v2.5.0').getExtensions()),
|
||||||
permissions: z.array(SharingPermissionSchema),
|
|
||||||
}).shape,
|
}).shape,
|
||||||
).meta({ id: 'AssetResponseDto' });
|
).meta({ id: 'AssetResponseDto' });
|
||||||
|
|
||||||
@@ -158,7 +154,6 @@ export type MapAsset = {
|
|||||||
width: number | null;
|
width: number | null;
|
||||||
height: number | null;
|
height: number | null;
|
||||||
isEdited: boolean;
|
isEdited: boolean;
|
||||||
permissions?: { permission: SharingPermission }[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AssetMapOptions = {
|
export type AssetMapOptions = {
|
||||||
@@ -197,16 +192,8 @@ const mapStack = (entity: { stack?: Stack | null }) => {
|
|||||||
|
|
||||||
export function mapAsset(entity: MaybeDehydrated<MapAsset>, options: AssetMapOptions = {}): AssetResponseDto {
|
export function mapAsset(entity: MaybeDehydrated<MapAsset>, options: AssetMapOptions = {}): AssetResponseDto {
|
||||||
const { stripMetadata = false, withStack = false } = options;
|
const { stripMetadata = false, withStack = false } = options;
|
||||||
const permissions =
|
|
||||||
options.auth?.user.id === entity.ownerId
|
|
||||||
? [SharingPermission.All]
|
|
||||||
: (entity.permissions?.map(({ permission }) => permission) ?? []);
|
|
||||||
|
|
||||||
if (
|
if (stripMetadata) {
|
||||||
stripMetadata ||
|
|
||||||
(entity.permissions &&
|
|
||||||
!(permissions.includes(SharingPermission.All) || permissions.includes(SharingPermission.ExifRead)))
|
|
||||||
) {
|
|
||||||
const sanitizedAssetResponse: SanitizedAssetResponseDto = {
|
const sanitizedAssetResponse: SanitizedAssetResponseDto = {
|
||||||
id: entity.id,
|
id: entity.id,
|
||||||
type: entity.type,
|
type: entity.type,
|
||||||
@@ -218,7 +205,6 @@ export function mapAsset(entity: MaybeDehydrated<MapAsset>, options: AssetMapOpt
|
|||||||
hasMetadata: false,
|
hasMetadata: false,
|
||||||
width: entity.width,
|
width: entity.width,
|
||||||
height: entity.height,
|
height: entity.height,
|
||||||
permissions,
|
|
||||||
};
|
};
|
||||||
return sanitizedAssetResponse as AssetResponseDto;
|
return sanitizedAssetResponse as AssetResponseDto;
|
||||||
}
|
}
|
||||||
@@ -256,6 +242,5 @@ export function mapAsset(entity: MaybeDehydrated<MapAsset>, options: AssetMapOpt
|
|||||||
width: entity.width,
|
width: entity.width,
|
||||||
height: entity.height,
|
height: entity.height,
|
||||||
isEdited: entity.isEdited,
|
isEdited: entity.isEdited,
|
||||||
permissions,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Selectable } from 'kysely';
|
|||||||
import { createZodDto } from 'nestjs-zod';
|
import { createZodDto } from 'nestjs-zod';
|
||||||
import { AssetFace, Person } from 'src/database';
|
import { AssetFace, Person } from 'src/database';
|
||||||
import { HistoryBuilder } from 'src/decorators';
|
import { HistoryBuilder } from 'src/decorators';
|
||||||
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { AssetEditActionItem } from 'src/dtos/editing.dto';
|
import { AssetEditActionItem } from 'src/dtos/editing.dto';
|
||||||
import { SourceTypeSchema } from 'src/enum';
|
import { SourceTypeSchema } from 'src/enum';
|
||||||
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
||||||
@@ -39,11 +40,11 @@ const PeopleUpdateSchema = z
|
|||||||
})
|
})
|
||||||
.meta({ id: 'PeopleUpdateDto' });
|
.meta({ id: 'PeopleUpdateDto' });
|
||||||
|
|
||||||
const MergeFaceClusterSchema = z
|
const MergePersonSchema = z
|
||||||
.object({
|
.object({
|
||||||
ids: z.array(z.uuidv4()).describe('Face cluster IDs to merge'),
|
ids: z.array(z.uuidv4()).describe('Person IDs to merge'),
|
||||||
})
|
})
|
||||||
.meta({ id: 'MergeFaceClusterDto' });
|
.meta({ id: 'MergePersonDto' });
|
||||||
|
|
||||||
const PersonSearchSchema = z
|
const PersonSearchSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -80,14 +81,13 @@ export const PersonResponseSchema = z
|
|||||||
.optional()
|
.optional()
|
||||||
.describe('Person color (hex)')
|
.describe('Person color (hex)')
|
||||||
.meta(new HistoryBuilder().added('v1.126.0').stable('v2').getExtensions()),
|
.meta(new HistoryBuilder().added('v1.126.0').stable('v2').getExtensions()),
|
||||||
faceClusterId: z.string().nullable().describe('Face cluster ID'),
|
|
||||||
})
|
})
|
||||||
.meta({ id: 'PersonResponseDto' });
|
.meta({ id: 'PersonResponseDto' });
|
||||||
|
|
||||||
export class PersonCreateDto extends createZodDto(PersonCreateSchema) {}
|
export class PersonCreateDto extends createZodDto(PersonCreateSchema) {}
|
||||||
export class PersonUpdateDto extends createZodDto(PersonUpdateSchema) {}
|
export class PersonUpdateDto extends createZodDto(PersonUpdateSchema) {}
|
||||||
export class PeopleUpdateDto extends createZodDto(PeopleUpdateSchema) {}
|
export class PeopleUpdateDto extends createZodDto(PeopleUpdateSchema) {}
|
||||||
export class MergeFaceClusterDto extends createZodDto(MergeFaceClusterSchema) {}
|
export class MergePersonDto extends createZodDto(MergePersonSchema) {}
|
||||||
export class PersonSearchDto extends createZodDto(PersonSearchSchema) {}
|
export class PersonSearchDto extends createZodDto(PersonSearchSchema) {}
|
||||||
export class PersonResponseDto extends createZodDto(PersonResponseSchema) {}
|
export class PersonResponseDto extends createZodDto(PersonResponseSchema) {}
|
||||||
|
|
||||||
@@ -179,7 +179,6 @@ export function mapPerson(person: MaybeDehydrated<Person>): PersonResponseDto {
|
|||||||
isFavorite: person.isFavorite,
|
isFavorite: person.isFavorite,
|
||||||
color: person.color ?? undefined,
|
color: person.color ?? undefined,
|
||||||
updatedAt: asDateString(person.updatedAt),
|
updatedAt: asDateString(person.updatedAt),
|
||||||
faceClusterId: person.faceClusterId,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,11 +207,12 @@ function mapFacesWithoutPerson(
|
|||||||
|
|
||||||
export function mapFaces(
|
export function mapFaces(
|
||||||
face: AssetFace,
|
face: AssetFace,
|
||||||
|
auth: AuthDto,
|
||||||
edits?: AssetEditActionItem[],
|
edits?: AssetEditActionItem[],
|
||||||
assetDimensions?: ImageDimensions,
|
assetDimensions?: ImageDimensions,
|
||||||
): AssetFaceResponseDto {
|
): AssetFaceResponseDto {
|
||||||
return {
|
return {
|
||||||
...mapFacesWithoutPerson(face, edits, assetDimensions),
|
...mapFacesWithoutPerson(face, edits, assetDimensions),
|
||||||
person: face.person ? mapPerson(face.person) : null,
|
person: face.person?.ownerId === auth.user.id ? mapPerson(face.person) : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -374,13 +374,10 @@ const SyncAssetFaceV1Schema = z
|
|||||||
})
|
})
|
||||||
.meta({ id: 'SyncAssetFaceV1' });
|
.meta({ id: 'SyncAssetFaceV1' });
|
||||||
|
|
||||||
const SyncAssetFaceV2Schema = SyncAssetFaceV1Schema.omit({ personId: true })
|
const SyncAssetFaceV2Schema = SyncAssetFaceV1Schema.extend({
|
||||||
.extend({
|
deletedAt: isoDatetimeToDate.nullable().describe('Face deleted at'),
|
||||||
deletedAt: isoDatetimeToDate.nullable().describe('Face deleted at'),
|
isVisible: z.boolean().describe('Is the face visible in the asset'),
|
||||||
isVisible: z.boolean().describe('Is the face visible in the asset'),
|
}).meta({ id: 'SyncAssetFaceV2' });
|
||||||
faceClusterId: z.string().nullable().describe('Person ID'),
|
|
||||||
})
|
|
||||||
.meta({ id: 'SyncAssetFaceV2' });
|
|
||||||
|
|
||||||
const SyncAssetFaceDeleteV1Schema = z
|
const SyncAssetFaceDeleteV1Schema = z
|
||||||
.object({ assetFaceId: z.string().describe('Asset face ID') })
|
.object({ assetFaceId: z.string().describe('Asset face ID') })
|
||||||
|
|||||||
@@ -306,28 +306,6 @@ export enum Permission {
|
|||||||
AdminAuthUnlinkAll = 'adminAuth.unlinkAll',
|
AdminAuthUnlinkAll = 'adminAuth.unlinkAll',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SharingPermission {
|
|
||||||
All = 'all',
|
|
||||||
|
|
||||||
AssetRead = 'asset.read',
|
|
||||||
AssetUpdate = 'asset.update',
|
|
||||||
AssetEdit = 'asset.edit',
|
|
||||||
AssetDelete = 'asset.delete',
|
|
||||||
AssetShare = 'asset.share',
|
|
||||||
|
|
||||||
ExifRead = 'exif.read',
|
|
||||||
|
|
||||||
PersonRead = 'person.read',
|
|
||||||
PersonUpdate = 'person.update',
|
|
||||||
PersonMerge = 'person.merge',
|
|
||||||
PersonDelete = 'person.delete',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SharingPermissionSchema = z
|
|
||||||
.enum(SharingPermission)
|
|
||||||
.describe('Sharing permission schema')
|
|
||||||
.meta({ id: 'SharingPermission' });
|
|
||||||
|
|
||||||
export enum SharedLinkType {
|
export enum SharedLinkType {
|
||||||
Album = 'ALBUM',
|
Album = 'ALBUM',
|
||||||
|
|
||||||
@@ -426,7 +404,6 @@ export enum ManualJobName {
|
|||||||
MemoryCleanup = 'memory-cleanup',
|
MemoryCleanup = 'memory-cleanup',
|
||||||
MemoryCreate = 'memory-create',
|
MemoryCreate = 'memory-create',
|
||||||
BackupDatabase = 'backup-database',
|
BackupDatabase = 'backup-database',
|
||||||
PersonGroupMerge = 'person-group-merge',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ManualJobNameSchema = z.enum(ManualJobName).describe('Manual job name').meta({ id: 'ManualJobName' });
|
export const ManualJobNameSchema = z.enum(ManualJobName).describe('Manual job name').meta({ id: 'ManualJobName' });
|
||||||
@@ -836,7 +813,6 @@ export enum JobName {
|
|||||||
|
|
||||||
FacialRecognitionQueueAll = 'FacialRecognitionQueueAll',
|
FacialRecognitionQueueAll = 'FacialRecognitionQueueAll',
|
||||||
FacialRecognition = 'FacialRecognition',
|
FacialRecognition = 'FacialRecognition',
|
||||||
FacialRecognitionMerge = 'FacialRecognitionMerge',
|
|
||||||
|
|
||||||
FileDelete = 'FileDelete',
|
FileDelete = 'FileDelete',
|
||||||
FileMigrationQueueAll = 'FileMigrationQueueAll',
|
FileMigrationQueueAll = 'FileMigrationQueueAll',
|
||||||
|
|||||||
@@ -149,40 +149,6 @@ where
|
|||||||
"albumAssets"."livePhotoVideoId"
|
"albumAssets"."livePhotoVideoId"
|
||||||
] && array[$2]::uuid[]
|
] && array[$2]::uuid[]
|
||||||
|
|
||||||
-- AccessRepository.asset.checkSharedAccess
|
|
||||||
select
|
|
||||||
"album_asset"."assetId"
|
|
||||||
from
|
|
||||||
"album_asset"
|
|
||||||
inner join "album_user" on "album_asset"."albumId" = "album_user"."albumId"
|
|
||||||
and "album_user"."userId" = $1
|
|
||||||
where
|
|
||||||
"album_asset"."assetId" in ($2)
|
|
||||||
and "album_asset"."albumId" in (
|
|
||||||
select
|
|
||||||
"album_user"."albumId"
|
|
||||||
from
|
|
||||||
"album_user"
|
|
||||||
where
|
|
||||||
(
|
|
||||||
"album_user"."permissions" @> $3::sharing_permission_enum[]
|
|
||||||
or $4 = any ("album_user"."permissions")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
union
|
|
||||||
select
|
|
||||||
"asset"."id" as "assetId"
|
|
||||||
from
|
|
||||||
"partner"
|
|
||||||
inner join "asset" on "asset"."ownerId" = "partner"."sharedById"
|
|
||||||
and "asset"."id" in ($5)
|
|
||||||
where
|
|
||||||
"partner"."sharedWithId" = $6
|
|
||||||
and (
|
|
||||||
"partner"."permissions" @> $7::sharing_permission_enum[]
|
|
||||||
or $8 = any ("partner"."permissions")
|
|
||||||
)
|
|
||||||
|
|
||||||
-- AccessRepository.authDevice.checkOwnerAccess
|
-- AccessRepository.authDevice.checkOwnerAccess
|
||||||
select
|
select
|
||||||
"session"."id"
|
"session"."id"
|
||||||
|
|||||||
@@ -182,25 +182,18 @@ select
|
|||||||
from
|
from
|
||||||
(
|
(
|
||||||
select
|
select
|
||||||
(
|
"asset_face".*,
|
||||||
select
|
"person" as "person"
|
||||||
to_json(obj)
|
|
||||||
from
|
|
||||||
(
|
|
||||||
select
|
|
||||||
"person".*
|
|
||||||
from
|
|
||||||
"face_cluster"
|
|
||||||
inner join "person" on "person"."faceClusterId" = "face_cluster"."id"
|
|
||||||
where
|
|
||||||
"face_cluster"."id" = "asset_face"."faceClusterId"
|
|
||||||
limit
|
|
||||||
$1
|
|
||||||
) as obj
|
|
||||||
) as "person",
|
|
||||||
"asset_face".*
|
|
||||||
from
|
from
|
||||||
"asset_face"
|
"asset_face"
|
||||||
|
left join lateral (
|
||||||
|
select
|
||||||
|
"person".*
|
||||||
|
from
|
||||||
|
"person"
|
||||||
|
where
|
||||||
|
"asset_face"."personId" = "person"."id"
|
||||||
|
) as "person" on true
|
||||||
where
|
where
|
||||||
"asset_face"."assetId" = "asset"."id"
|
"asset_face"."assetId" = "asset"."id"
|
||||||
and "asset_face"."deletedAt" is null
|
and "asset_face"."deletedAt" is null
|
||||||
@@ -231,7 +224,7 @@ from
|
|||||||
"asset"
|
"asset"
|
||||||
left join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
left join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||||
where
|
where
|
||||||
"asset"."id" = any ($2::uuid[])
|
"asset"."id" = any ($1::uuid[])
|
||||||
|
|
||||||
-- AssetRepository.deleteAll
|
-- AssetRepository.deleteAll
|
||||||
delete from "asset"
|
delete from "asset"
|
||||||
@@ -297,44 +290,13 @@ limit
|
|||||||
|
|
||||||
-- AssetRepository.getById
|
-- AssetRepository.getById
|
||||||
select
|
select
|
||||||
"asset".*,
|
"asset".*
|
||||||
(
|
|
||||||
select
|
|
||||||
coalesce(json_agg(agg), '[]')
|
|
||||||
from
|
|
||||||
(
|
|
||||||
select distinct
|
|
||||||
unnest("album_user"."permissions") as "permission"
|
|
||||||
from
|
|
||||||
"album_user"
|
|
||||||
inner join "album_asset" on "album_user"."albumId" = "album_asset"."albumId"
|
|
||||||
where
|
|
||||||
"album_asset"."assetId" = "asset"."id"
|
|
||||||
and "album_user"."userId" = "asset"."ownerId"
|
|
||||||
and "album_user"."albumId" in (
|
|
||||||
select
|
|
||||||
"album_user"."albumId"
|
|
||||||
from
|
|
||||||
"album_user"
|
|
||||||
where
|
|
||||||
"album_user"."userId" = $1
|
|
||||||
)
|
|
||||||
union
|
|
||||||
select distinct
|
|
||||||
unnest("partner"."permissions") as "permission"
|
|
||||||
from
|
|
||||||
"partner"
|
|
||||||
where
|
|
||||||
"partner"."sharedById" = "asset"."ownerId"
|
|
||||||
and "partner"."sharedWithId" = $2
|
|
||||||
) as agg
|
|
||||||
) as "permissions"
|
|
||||||
from
|
from
|
||||||
"asset"
|
"asset"
|
||||||
where
|
where
|
||||||
"asset"."id" = $3::uuid
|
"asset"."id" = $1::uuid
|
||||||
limit
|
limit
|
||||||
$4
|
$2
|
||||||
|
|
||||||
-- AssetRepository.updateAll
|
-- AssetRepository.updateAll
|
||||||
update "asset"
|
update "asset"
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ select
|
|||||||
$1 as "one"
|
$1 as "one"
|
||||||
from
|
from
|
||||||
"asset_face"
|
"asset_face"
|
||||||
inner join "person" on "person"."faceClusterId" = "asset_face"."faceClusterId"
|
inner join "person" on "person"."id" = "asset_face"."personId"
|
||||||
where
|
where
|
||||||
"asset_face"."assetId" = "asset"."id"
|
"asset_face"."assetId" = "asset"."id"
|
||||||
and "person"."isHidden" = $2
|
and "person"."isHidden" = $2
|
||||||
@@ -86,7 +86,7 @@ select
|
|||||||
$1 as "one"
|
$1 as "one"
|
||||||
from
|
from
|
||||||
"asset_face"
|
"asset_face"
|
||||||
inner join "person" on "person"."faceClusterId" = "asset_face"."faceClusterId"
|
inner join "person" on "person"."id" = "asset_face"."personId"
|
||||||
where
|
where
|
||||||
"asset_face"."assetId" = "asset"."id"
|
"asset_face"."assetId" = "asset"."id"
|
||||||
and "person"."isHidden" = $2
|
and "person"."isHidden" = $2
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
-- PersonRepository.reassignFaces
|
-- PersonRepository.reassignFaces
|
||||||
update "asset_face"
|
update "asset_face"
|
||||||
set
|
set
|
||||||
|
"personId" = $1
|
||||||
|
where
|
||||||
|
"asset_face"."personId" = $2
|
||||||
|
|
||||||
-- PersonRepository.delete
|
-- PersonRepository.delete
|
||||||
delete from "person"
|
delete from "person"
|
||||||
@@ -21,64 +24,27 @@ limit
|
|||||||
3
|
3
|
||||||
|
|
||||||
-- PersonRepository.getAllForUser
|
-- PersonRepository.getAllForUser
|
||||||
select distinct
|
select
|
||||||
on ("person"."faceClusterId") "person".*
|
"person".*
|
||||||
from
|
from
|
||||||
"person"
|
"person"
|
||||||
inner join "asset_face" on "asset_face"."faceClusterId" = "person"."faceClusterId"
|
inner join "asset_face" on "asset_face"."personId" = "person"."id"
|
||||||
inner join "asset" on "asset_face"."assetId" = "asset"."id"
|
inner join "asset" on "asset_face"."assetId" = "asset"."id"
|
||||||
and "asset"."visibility" = 'timeline'
|
and "asset"."visibility" = 'timeline'
|
||||||
and "asset"."deletedAt" is null
|
and "asset"."deletedAt" is null
|
||||||
where
|
where
|
||||||
(
|
"person"."ownerId" = $1
|
||||||
"person"."ownerId" = $1
|
|
||||||
or (
|
|
||||||
exists (
|
|
||||||
select
|
|
||||||
from
|
|
||||||
"partner"
|
|
||||||
where
|
|
||||||
"partner"."sharedById" = "person"."ownerId"
|
|
||||||
and "partner"."sharedWithId" = $2
|
|
||||||
and (
|
|
||||||
$3 = any ("partner"."permissions")
|
|
||||||
or "partner"."permissions" @> $4
|
|
||||||
)
|
|
||||||
)
|
|
||||||
or exists (
|
|
||||||
select
|
|
||||||
from
|
|
||||||
"album_user"
|
|
||||||
where
|
|
||||||
"album_user"."albumId" in (
|
|
||||||
select
|
|
||||||
"album_user"."albumId"
|
|
||||||
from
|
|
||||||
"album_user"
|
|
||||||
where
|
|
||||||
"album_user"."userId" = $5
|
|
||||||
)
|
|
||||||
and "album_user"."userId" = "person"."ownerId"
|
|
||||||
and (
|
|
||||||
$6 = any ("album_user"."permissions")
|
|
||||||
or "album_user"."permissions" @> $7
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
and "asset_face"."deletedAt" is null
|
and "asset_face"."deletedAt" is null
|
||||||
and "asset_face"."isVisible" is true
|
and "asset_face"."isVisible" is true
|
||||||
and "person"."isHidden" = $8
|
and "person"."isHidden" = $2
|
||||||
group by
|
group by
|
||||||
"person"."id"
|
"person"."id"
|
||||||
having
|
having
|
||||||
(
|
(
|
||||||
"person"."name" != $9
|
"person"."name" != $3
|
||||||
or count("asset_face"."assetId") >= $10
|
or count("asset_face"."assetId") >= $4
|
||||||
)
|
)
|
||||||
order by
|
order by
|
||||||
"person"."faceClusterId",
|
|
||||||
"person"."ownerId" = $11 desc,
|
|
||||||
"person"."isHidden" asc,
|
"person"."isHidden" asc,
|
||||||
"person"."isFavorite" desc,
|
"person"."isFavorite" desc,
|
||||||
NULLIF(person.name, '') is null asc,
|
NULLIF(person.name, '') is null asc,
|
||||||
@@ -86,16 +52,16 @@ order by
|
|||||||
NULLIF(person.name, '') asc nulls last,
|
NULLIF(person.name, '') asc nulls last,
|
||||||
"person"."createdAt"
|
"person"."createdAt"
|
||||||
limit
|
limit
|
||||||
$12
|
$5
|
||||||
offset
|
offset
|
||||||
$13
|
$6
|
||||||
|
|
||||||
-- PersonRepository.getAllWithoutFaces
|
-- PersonRepository.getAllWithoutFaces
|
||||||
select
|
select
|
||||||
"person".*
|
"person".*
|
||||||
from
|
from
|
||||||
"person"
|
"person"
|
||||||
left join "asset_face" on "asset_face"."faceClusterId" = "person"."faceClusterId"
|
left join "asset_face" on "asset_face"."personId" = "person"."id"
|
||||||
where
|
where
|
||||||
"asset_face"."deletedAt" is null
|
"asset_face"."deletedAt" is null
|
||||||
and "asset_face"."isVisible" is true
|
and "asset_face"."isVisible" is true
|
||||||
@@ -117,26 +83,15 @@ select
|
|||||||
from
|
from
|
||||||
"person"
|
"person"
|
||||||
where
|
where
|
||||||
"person"."faceClusterId" = "asset_face"."faceClusterId"
|
"person"."id" = "asset_face"."personId"
|
||||||
order by
|
|
||||||
"person"."ownerId" = (
|
|
||||||
select
|
|
||||||
"asset"."ownerId"
|
|
||||||
from
|
|
||||||
"asset"
|
|
||||||
where
|
|
||||||
"asset"."id" = "asset_face"."assetId"
|
|
||||||
) desc
|
|
||||||
limit
|
|
||||||
$1
|
|
||||||
) as obj
|
) as obj
|
||||||
) as "person"
|
) as "person"
|
||||||
from
|
from
|
||||||
"asset_face"
|
"asset_face"
|
||||||
where
|
where
|
||||||
"asset_face"."assetId" = $2
|
"asset_face"."assetId" = $1
|
||||||
and "asset_face"."deletedAt" is null
|
and "asset_face"."deletedAt" is null
|
||||||
and "asset_face"."isVisible" = $3
|
and "asset_face"."isVisible" = $2
|
||||||
order by
|
order by
|
||||||
"asset_face"."boundingBoxX1" asc
|
"asset_face"."boundingBoxX1" asc
|
||||||
|
|
||||||
@@ -153,30 +108,19 @@ select
|
|||||||
from
|
from
|
||||||
"person"
|
"person"
|
||||||
where
|
where
|
||||||
"person"."faceClusterId" = "asset_face"."faceClusterId"
|
"person"."id" = "asset_face"."personId"
|
||||||
order by
|
|
||||||
"person"."ownerId" = (
|
|
||||||
select
|
|
||||||
"asset"."ownerId"
|
|
||||||
from
|
|
||||||
"asset"
|
|
||||||
where
|
|
||||||
"asset"."id" = "asset_face"."assetId"
|
|
||||||
) desc
|
|
||||||
limit
|
|
||||||
$1
|
|
||||||
) as obj
|
) as obj
|
||||||
) as "person"
|
) as "person"
|
||||||
from
|
from
|
||||||
"asset_face"
|
"asset_face"
|
||||||
where
|
where
|
||||||
"asset_face"."id" = $2
|
"asset_face"."id" = $1
|
||||||
and "asset_face"."deletedAt" is null
|
and "asset_face"."deletedAt" is null
|
||||||
|
|
||||||
-- PersonRepository.getFaceForFacialRecognitionJob
|
-- PersonRepository.getFaceForFacialRecognitionJob
|
||||||
select
|
select
|
||||||
"asset_face"."id",
|
"asset_face"."id",
|
||||||
"asset_face"."faceClusterId",
|
"asset_face"."personId",
|
||||||
"asset_face"."sourceType",
|
"asset_face"."sourceType",
|
||||||
(
|
(
|
||||||
select
|
select
|
||||||
@@ -246,7 +190,7 @@ where
|
|||||||
-- PersonRepository.reassignFace
|
-- PersonRepository.reassignFace
|
||||||
update "asset_face"
|
update "asset_face"
|
||||||
set
|
set
|
||||||
"faceClusterId" = $1
|
"personId" = $1
|
||||||
where
|
where
|
||||||
"asset_face"."id" = $2
|
"asset_face"."id" = $2
|
||||||
|
|
||||||
@@ -265,10 +209,9 @@ where
|
|||||||
"person"."ownerId" = $1
|
"person"."ownerId" = $1
|
||||||
and f_unaccent ("person"."name") %> f_unaccent ($2)
|
and f_unaccent ("person"."name") %> f_unaccent ($2)
|
||||||
order by
|
order by
|
||||||
f_unaccent ("person"."name") <->>> f_unaccent ($3),
|
f_unaccent ("person"."name") <->>> f_unaccent ($3)
|
||||||
"person"."ownerId" = $4 desc
|
|
||||||
limit
|
limit
|
||||||
$5
|
$4
|
||||||
|
|
||||||
-- PersonRepository.getDistinctNames
|
-- PersonRepository.getDistinctNames
|
||||||
select distinct
|
select distinct
|
||||||
@@ -291,52 +234,9 @@ from
|
|||||||
and "asset"."visibility" = 'timeline'
|
and "asset"."visibility" = 'timeline'
|
||||||
and "asset"."deletedAt" is null
|
and "asset"."deletedAt" is null
|
||||||
where
|
where
|
||||||
(
|
"asset_face"."deletedAt" is null
|
||||||
"asset"."ownerId" = $1
|
|
||||||
or exists (
|
|
||||||
select
|
|
||||||
from
|
|
||||||
"partner"
|
|
||||||
where
|
|
||||||
"partner"."sharedById" = "asset"."ownerId"
|
|
||||||
and "partner"."sharedWithId" = $2
|
|
||||||
and (
|
|
||||||
$3 = any ("partner"."permissions")
|
|
||||||
or "partner"."permissions" @> $4
|
|
||||||
)
|
|
||||||
)
|
|
||||||
or exists (
|
|
||||||
select
|
|
||||||
from
|
|
||||||
"album_asset"
|
|
||||||
inner join "album_user" on "album_user"."albumId" = "album_asset"."albumId"
|
|
||||||
and "album_user"."userId" = $5
|
|
||||||
where
|
|
||||||
"album_asset"."assetId" = "asset"."id"
|
|
||||||
and "album_user"."albumId" in (
|
|
||||||
select
|
|
||||||
"album_user"."albumId"
|
|
||||||
from
|
|
||||||
"album_user"
|
|
||||||
where
|
|
||||||
"album_user"."userId" = "asset"."ownerId"
|
|
||||||
and (
|
|
||||||
$6 = any ("album_user"."permissions")
|
|
||||||
or "album_user"."permissions" @> $7
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
and "asset_face"."deletedAt" is null
|
|
||||||
and "asset_face"."isVisible" is true
|
and "asset_face"."isVisible" is true
|
||||||
and "asset_face"."faceClusterId" = (
|
and "asset_face"."personId" = $1
|
||||||
select
|
|
||||||
"person"."faceClusterId"
|
|
||||||
from
|
|
||||||
"person"
|
|
||||||
where
|
|
||||||
"person"."id" = $8
|
|
||||||
)
|
|
||||||
|
|
||||||
-- PersonRepository.getNumberOfPeople
|
-- PersonRepository.getNumberOfPeople
|
||||||
select
|
select
|
||||||
@@ -356,7 +256,7 @@ where
|
|||||||
from
|
from
|
||||||
"asset_face"
|
"asset_face"
|
||||||
where
|
where
|
||||||
"asset_face"."faceClusterId" = "person"."faceClusterId"
|
"asset_face"."personId" = "person"."id"
|
||||||
and "asset_face"."deletedAt" is null
|
and "asset_face"."deletedAt" is null
|
||||||
and "asset_face"."isVisible" = $2
|
and "asset_face"."isVisible" = $2
|
||||||
and exists (
|
and exists (
|
||||||
@@ -369,42 +269,7 @@ where
|
|||||||
and "asset"."deletedAt" is null
|
and "asset"."deletedAt" is null
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
and (
|
and "person"."ownerId" = $3
|
||||||
"person"."ownerId" = $3
|
|
||||||
or (
|
|
||||||
exists (
|
|
||||||
select
|
|
||||||
from
|
|
||||||
"partner"
|
|
||||||
where
|
|
||||||
"partner"."sharedById" = "person"."ownerId"
|
|
||||||
and "partner"."sharedWithId" = $4
|
|
||||||
and (
|
|
||||||
$5 = any ("partner"."permissions")
|
|
||||||
or "partner"."permissions" @> $6
|
|
||||||
)
|
|
||||||
)
|
|
||||||
or exists (
|
|
||||||
select
|
|
||||||
from
|
|
||||||
"album_user"
|
|
||||||
where
|
|
||||||
"album_user"."albumId" in (
|
|
||||||
select
|
|
||||||
"album_user"."albumId"
|
|
||||||
from
|
|
||||||
"album_user"
|
|
||||||
where
|
|
||||||
"album_user"."userId" = $7
|
|
||||||
)
|
|
||||||
and "album_user"."userId" = "person"."ownerId"
|
|
||||||
and (
|
|
||||||
$8 = any ("album_user"."permissions")
|
|
||||||
or "album_user"."permissions" @> $9
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
-- PersonRepository.refreshFaces
|
-- PersonRepository.refreshFaces
|
||||||
with
|
with
|
||||||
@@ -434,26 +299,14 @@ select
|
|||||||
from
|
from
|
||||||
"person"
|
"person"
|
||||||
where
|
where
|
||||||
"person"."faceClusterId" = "asset_face"."faceClusterId"
|
"person"."id" = "asset_face"."personId"
|
||||||
order by
|
|
||||||
"person"."ownerId" = (
|
|
||||||
select
|
|
||||||
"asset"."ownerId"
|
|
||||||
from
|
|
||||||
"asset"
|
|
||||||
where
|
|
||||||
"asset"."id" = "asset_face"."assetId"
|
|
||||||
) desc
|
|
||||||
limit
|
|
||||||
$1
|
|
||||||
) as obj
|
) as obj
|
||||||
) as "person"
|
) as "person"
|
||||||
from
|
from
|
||||||
"asset_face"
|
"asset_face"
|
||||||
inner join "person" on "person"."faceClusterId" = "asset_face"."faceClusterId"
|
|
||||||
where
|
where
|
||||||
"person"."id" in ($2)
|
"asset_face"."assetId" in ($1)
|
||||||
and "asset_face"."assetId" in ($3)
|
and "asset_face"."personId" in ($2)
|
||||||
and "asset_face"."deletedAt" is null
|
and "asset_face"."deletedAt" is null
|
||||||
|
|
||||||
-- PersonRepository.getRandomFace
|
-- PersonRepository.getRandomFace
|
||||||
@@ -461,52 +314,8 @@ select
|
|||||||
"asset_face".*
|
"asset_face".*
|
||||||
from
|
from
|
||||||
"asset_face"
|
"asset_face"
|
||||||
inner join "person" on "asset_face"."faceClusterId" = "person"."faceClusterId"
|
|
||||||
and "person"."id" = $1
|
|
||||||
where
|
where
|
||||||
"asset_face"."assetId" in (
|
"asset_face"."personId" = $1
|
||||||
select
|
|
||||||
"asset"."id"
|
|
||||||
from
|
|
||||||
"asset"
|
|
||||||
where
|
|
||||||
(
|
|
||||||
"asset"."ownerId" = "person"."ownerId"
|
|
||||||
or exists (
|
|
||||||
select
|
|
||||||
from
|
|
||||||
"partner"
|
|
||||||
where
|
|
||||||
"partner"."sharedById" = "asset"."ownerId"
|
|
||||||
and "partner"."sharedWithId" = "person"."ownerId"
|
|
||||||
and (
|
|
||||||
$2 = any ("partner"."permissions")
|
|
||||||
or "partner"."permissions" @> $3
|
|
||||||
)
|
|
||||||
)
|
|
||||||
or exists (
|
|
||||||
select
|
|
||||||
from
|
|
||||||
"album_asset"
|
|
||||||
inner join "album_user" on "album_user"."albumId" = "album_asset"."albumId"
|
|
||||||
and "album_user"."userId" = "person"."ownerId"
|
|
||||||
where
|
|
||||||
"album_asset"."assetId" = "asset"."id"
|
|
||||||
and "album_user"."albumId" in (
|
|
||||||
select
|
|
||||||
"album_user"."albumId"
|
|
||||||
from
|
|
||||||
"album_user"
|
|
||||||
where
|
|
||||||
"album_user"."userId" = "asset"."ownerId"
|
|
||||||
and (
|
|
||||||
$4 = any ("album_user"."permissions")
|
|
||||||
or "album_user"."permissions" @> $5
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
and "asset_face"."deletedAt" is null
|
and "asset_face"."deletedAt" is null
|
||||||
and "asset_face"."isVisible" is true
|
and "asset_face"."isVisible" is true
|
||||||
|
|
||||||
@@ -542,9 +351,8 @@ select
|
|||||||
"asset_face"."id"
|
"asset_face"."id"
|
||||||
from
|
from
|
||||||
"asset_face"
|
"asset_face"
|
||||||
inner join "person" on "person"."faceClusterId" = "asset_face"."faceClusterId"
|
|
||||||
and "person"."id" = $1
|
|
||||||
inner join "asset" on "asset"."id" = "asset_face"."assetId"
|
inner join "asset" on "asset"."id" = "asset_face"."assetId"
|
||||||
and "asset"."isOffline" = $2
|
and "asset"."isOffline" = $1
|
||||||
where
|
where
|
||||||
"asset_face"."assetId" = $3
|
"asset_face"."assetId" = $2
|
||||||
|
and "asset_face"."personId" = $3
|
||||||
|
|||||||
@@ -10,52 +10,15 @@ where
|
|||||||
"asset"."visibility" = $1
|
"asset"."visibility" = $1
|
||||||
and "asset"."fileCreatedAt" >= $2
|
and "asset"."fileCreatedAt" >= $2
|
||||||
and "asset_exif"."lensModel" = $3
|
and "asset_exif"."lensModel" = $3
|
||||||
and (
|
and "asset"."ownerId" = any ($4::uuid[])
|
||||||
"asset"."ownerId" = $4
|
and "asset"."isFavorite" = $5
|
||||||
or exists (
|
|
||||||
select
|
|
||||||
from
|
|
||||||
"partner"
|
|
||||||
where
|
|
||||||
"partner"."sharedById" = "asset"."ownerId"
|
|
||||||
and "partner"."sharedWithId" = $5
|
|
||||||
and (
|
|
||||||
$6 = any ("partner"."permissions")
|
|
||||||
or "partner"."permissions" @> $7
|
|
||||||
)
|
|
||||||
and "partner"."inTimeline" = $8
|
|
||||||
)
|
|
||||||
or exists (
|
|
||||||
select
|
|
||||||
from
|
|
||||||
"album_asset"
|
|
||||||
inner join "album_user" on "album_user"."albumId" = "album_asset"."albumId"
|
|
||||||
and "album_user"."userId" = $9
|
|
||||||
where
|
|
||||||
"album_asset"."assetId" = "asset"."id"
|
|
||||||
and "album_user"."inTimeline" = $10
|
|
||||||
and "album_user"."albumId" in (
|
|
||||||
select
|
|
||||||
"album_user"."albumId"
|
|
||||||
from
|
|
||||||
"album_user"
|
|
||||||
where
|
|
||||||
"album_user"."userId" = "asset"."ownerId"
|
|
||||||
and (
|
|
||||||
$11 = any ("album_user"."permissions")
|
|
||||||
or "album_user"."permissions" @> $12
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
and "asset"."isFavorite" = $13
|
|
||||||
and "asset"."deletedAt" is null
|
and "asset"."deletedAt" is null
|
||||||
order by
|
order by
|
||||||
"asset"."fileCreatedAt" desc
|
"asset"."fileCreatedAt" desc
|
||||||
limit
|
limit
|
||||||
$14
|
$6
|
||||||
offset
|
offset
|
||||||
$15
|
$7
|
||||||
|
|
||||||
-- SearchRepository.searchStatistics
|
-- SearchRepository.searchStatistics
|
||||||
select
|
select
|
||||||
@@ -67,45 +30,8 @@ where
|
|||||||
"asset"."visibility" = $1
|
"asset"."visibility" = $1
|
||||||
and "asset"."fileCreatedAt" >= $2
|
and "asset"."fileCreatedAt" >= $2
|
||||||
and "asset_exif"."lensModel" = $3
|
and "asset_exif"."lensModel" = $3
|
||||||
and (
|
and "asset"."ownerId" = any ($4::uuid[])
|
||||||
"asset"."ownerId" = $4
|
and "asset"."isFavorite" = $5
|
||||||
or exists (
|
|
||||||
select
|
|
||||||
from
|
|
||||||
"partner"
|
|
||||||
where
|
|
||||||
"partner"."sharedById" = "asset"."ownerId"
|
|
||||||
and "partner"."sharedWithId" = $5
|
|
||||||
and (
|
|
||||||
$6 = any ("partner"."permissions")
|
|
||||||
or "partner"."permissions" @> $7
|
|
||||||
)
|
|
||||||
and "partner"."inTimeline" = $8
|
|
||||||
)
|
|
||||||
or exists (
|
|
||||||
select
|
|
||||||
from
|
|
||||||
"album_asset"
|
|
||||||
inner join "album_user" on "album_user"."albumId" = "album_asset"."albumId"
|
|
||||||
and "album_user"."userId" = $9
|
|
||||||
where
|
|
||||||
"album_asset"."assetId" = "asset"."id"
|
|
||||||
and "album_user"."inTimeline" = $10
|
|
||||||
and "album_user"."albumId" in (
|
|
||||||
select
|
|
||||||
"album_user"."albumId"
|
|
||||||
from
|
|
||||||
"album_user"
|
|
||||||
where
|
|
||||||
"album_user"."userId" = "asset"."ownerId"
|
|
||||||
and (
|
|
||||||
$11 = any ("album_user"."permissions")
|
|
||||||
or "album_user"."permissions" @> $12
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
and "asset"."isFavorite" = $13
|
|
||||||
and "asset"."deletedAt" is null
|
and "asset"."deletedAt" is null
|
||||||
|
|
||||||
-- SearchRepository.searchRandom
|
-- SearchRepository.searchRandom
|
||||||
@@ -118,50 +44,13 @@ where
|
|||||||
"asset"."visibility" = $1
|
"asset"."visibility" = $1
|
||||||
and "asset"."fileCreatedAt" >= $2
|
and "asset"."fileCreatedAt" >= $2
|
||||||
and "asset_exif"."lensModel" = $3
|
and "asset_exif"."lensModel" = $3
|
||||||
and (
|
and "asset"."ownerId" = any ($4::uuid[])
|
||||||
"asset"."ownerId" = $4
|
and "asset"."isFavorite" = $5
|
||||||
or exists (
|
|
||||||
select
|
|
||||||
from
|
|
||||||
"partner"
|
|
||||||
where
|
|
||||||
"partner"."sharedById" = "asset"."ownerId"
|
|
||||||
and "partner"."sharedWithId" = $5
|
|
||||||
and (
|
|
||||||
$6 = any ("partner"."permissions")
|
|
||||||
or "partner"."permissions" @> $7
|
|
||||||
)
|
|
||||||
and "partner"."inTimeline" = $8
|
|
||||||
)
|
|
||||||
or exists (
|
|
||||||
select
|
|
||||||
from
|
|
||||||
"album_asset"
|
|
||||||
inner join "album_user" on "album_user"."albumId" = "album_asset"."albumId"
|
|
||||||
and "album_user"."userId" = $9
|
|
||||||
where
|
|
||||||
"album_asset"."assetId" = "asset"."id"
|
|
||||||
and "album_user"."inTimeline" = $10
|
|
||||||
and "album_user"."albumId" in (
|
|
||||||
select
|
|
||||||
"album_user"."albumId"
|
|
||||||
from
|
|
||||||
"album_user"
|
|
||||||
where
|
|
||||||
"album_user"."userId" = "asset"."ownerId"
|
|
||||||
and (
|
|
||||||
$11 = any ("album_user"."permissions")
|
|
||||||
or "album_user"."permissions" @> $12
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
and "asset"."isFavorite" = $13
|
|
||||||
and "asset"."deletedAt" is null
|
and "asset"."deletedAt" is null
|
||||||
order by
|
order by
|
||||||
random()
|
random()
|
||||||
limit
|
limit
|
||||||
$14
|
$6
|
||||||
|
|
||||||
-- SearchRepository.searchLargeAssets
|
-- SearchRepository.searchLargeAssets
|
||||||
select
|
select
|
||||||
@@ -174,51 +63,14 @@ where
|
|||||||
"asset"."visibility" = $1
|
"asset"."visibility" = $1
|
||||||
and "asset"."fileCreatedAt" >= $2
|
and "asset"."fileCreatedAt" >= $2
|
||||||
and "asset_exif"."lensModel" = $3
|
and "asset_exif"."lensModel" = $3
|
||||||
and (
|
and "asset"."ownerId" = any ($4::uuid[])
|
||||||
"asset"."ownerId" = $4
|
and "asset"."isFavorite" = $5
|
||||||
or exists (
|
|
||||||
select
|
|
||||||
from
|
|
||||||
"partner"
|
|
||||||
where
|
|
||||||
"partner"."sharedById" = "asset"."ownerId"
|
|
||||||
and "partner"."sharedWithId" = $5
|
|
||||||
and (
|
|
||||||
$6 = any ("partner"."permissions")
|
|
||||||
or "partner"."permissions" @> $7
|
|
||||||
)
|
|
||||||
and "partner"."inTimeline" = $8
|
|
||||||
)
|
|
||||||
or exists (
|
|
||||||
select
|
|
||||||
from
|
|
||||||
"album_asset"
|
|
||||||
inner join "album_user" on "album_user"."albumId" = "album_asset"."albumId"
|
|
||||||
and "album_user"."userId" = $9
|
|
||||||
where
|
|
||||||
"album_asset"."assetId" = "asset"."id"
|
|
||||||
and "album_user"."inTimeline" = $10
|
|
||||||
and "album_user"."albumId" in (
|
|
||||||
select
|
|
||||||
"album_user"."albumId"
|
|
||||||
from
|
|
||||||
"album_user"
|
|
||||||
where
|
|
||||||
"album_user"."userId" = "asset"."ownerId"
|
|
||||||
and (
|
|
||||||
$11 = any ("album_user"."permissions")
|
|
||||||
or "album_user"."permissions" @> $12
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
and "asset"."isFavorite" = $13
|
|
||||||
and "asset"."deletedAt" is null
|
and "asset"."deletedAt" is null
|
||||||
and "asset_exif"."fileSizeInByte" > $14
|
and "asset_exif"."fileSizeInByte" > $6
|
||||||
order by
|
order by
|
||||||
"asset_exif"."fileSizeInByte" desc
|
"asset_exif"."fileSizeInByte" desc
|
||||||
limit
|
limit
|
||||||
$15
|
$7
|
||||||
|
|
||||||
-- SearchRepository.searchSmart
|
-- SearchRepository.searchSmart
|
||||||
begin
|
begin
|
||||||
@@ -234,52 +86,15 @@ where
|
|||||||
"asset"."visibility" = $1
|
"asset"."visibility" = $1
|
||||||
and "asset"."fileCreatedAt" >= $2
|
and "asset"."fileCreatedAt" >= $2
|
||||||
and "asset_exif"."lensModel" = $3
|
and "asset_exif"."lensModel" = $3
|
||||||
and (
|
and "asset"."ownerId" = any ($4::uuid[])
|
||||||
"asset"."ownerId" = $4
|
and "asset"."isFavorite" = $5
|
||||||
or exists (
|
|
||||||
select
|
|
||||||
from
|
|
||||||
"partner"
|
|
||||||
where
|
|
||||||
"partner"."sharedById" = "asset"."ownerId"
|
|
||||||
and "partner"."sharedWithId" = $5
|
|
||||||
and (
|
|
||||||
$6 = any ("partner"."permissions")
|
|
||||||
or "partner"."permissions" @> $7
|
|
||||||
)
|
|
||||||
and "partner"."inTimeline" = $8
|
|
||||||
)
|
|
||||||
or exists (
|
|
||||||
select
|
|
||||||
from
|
|
||||||
"album_asset"
|
|
||||||
inner join "album_user" on "album_user"."albumId" = "album_asset"."albumId"
|
|
||||||
and "album_user"."userId" = $9
|
|
||||||
where
|
|
||||||
"album_asset"."assetId" = "asset"."id"
|
|
||||||
and "album_user"."inTimeline" = $10
|
|
||||||
and "album_user"."albumId" in (
|
|
||||||
select
|
|
||||||
"album_user"."albumId"
|
|
||||||
from
|
|
||||||
"album_user"
|
|
||||||
where
|
|
||||||
"album_user"."userId" = "asset"."ownerId"
|
|
||||||
and (
|
|
||||||
$11 = any ("album_user"."permissions")
|
|
||||||
or "album_user"."permissions" @> $12
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
and "asset"."isFavorite" = $13
|
|
||||||
and "asset"."deletedAt" is null
|
and "asset"."deletedAt" is null
|
||||||
order by
|
order by
|
||||||
smart_search.embedding <=> $14
|
smart_search.embedding <=> $6
|
||||||
limit
|
limit
|
||||||
$15
|
$7
|
||||||
offset
|
offset
|
||||||
$16
|
$8
|
||||||
commit
|
commit
|
||||||
|
|
||||||
-- SearchRepository.getEmbedding
|
-- SearchRepository.getEmbedding
|
||||||
@@ -298,30 +113,15 @@ with
|
|||||||
"cte" as (
|
"cte" as (
|
||||||
select
|
select
|
||||||
"asset_face"."id",
|
"asset_face"."id",
|
||||||
"asset_face"."faceClusterId",
|
"asset_face"."personId",
|
||||||
face_search.embedding <=> $1 as "distance",
|
face_search.embedding <=> $1 as "distance"
|
||||||
"asset"."ownerId"
|
|
||||||
from
|
from
|
||||||
"asset_face"
|
"asset_face"
|
||||||
inner join "asset" on "asset"."id" = "asset_face"."assetId"
|
inner join "asset" on "asset"."id" = "asset_face"."assetId"
|
||||||
inner join "face_search" on "face_search"."faceId" = "asset_face"."id"
|
inner join "face_search" on "face_search"."faceId" = "asset_face"."id"
|
||||||
left join "person" on "person"."faceClusterId" = "asset_face"."faceClusterId"
|
left join "person" on "person"."id" = "asset_face"."personId"
|
||||||
where
|
where
|
||||||
"asset"."ownerId" in (
|
"asset"."ownerId" = any ($2::uuid[])
|
||||||
select
|
|
||||||
"user"."id"
|
|
||||||
from
|
|
||||||
"user"
|
|
||||||
where
|
|
||||||
"user"."trustedGroupId" in (
|
|
||||||
select
|
|
||||||
"user"."trustedGroupId"
|
|
||||||
from
|
|
||||||
"user"
|
|
||||||
where
|
|
||||||
"user"."id" = any ($2::uuid[])
|
|
||||||
)
|
|
||||||
)
|
|
||||||
and "asset"."deletedAt" is null
|
and "asset"."deletedAt" is null
|
||||||
order by
|
order by
|
||||||
"distance"
|
"distance"
|
||||||
|
|||||||
@@ -527,7 +527,7 @@ order by
|
|||||||
select
|
select
|
||||||
"asset_face"."id",
|
"asset_face"."id",
|
||||||
"assetId",
|
"assetId",
|
||||||
"faceClusterId",
|
"personId",
|
||||||
"imageWidth",
|
"imageWidth",
|
||||||
"imageHeight",
|
"imageHeight",
|
||||||
"boundingBoxX1",
|
"boundingBoxX1",
|
||||||
|
|||||||
@@ -397,73 +397,3 @@ set
|
|||||||
where
|
where
|
||||||
"user"."deletedAt" is null
|
"user"."deletedAt" is null
|
||||||
and "user"."id" = $2::uuid
|
and "user"."id" = $2::uuid
|
||||||
|
|
||||||
-- UserRepository.getInSameTrustedGroup
|
|
||||||
select
|
|
||||||
"user"."id"
|
|
||||||
from
|
|
||||||
"user"
|
|
||||||
where
|
|
||||||
"user"."trustedGroupId" = (
|
|
||||||
select
|
|
||||||
"user"."trustedGroupId"
|
|
||||||
from
|
|
||||||
"user"
|
|
||||||
where
|
|
||||||
"user"."id" = $1
|
|
||||||
)
|
|
||||||
|
|
||||||
-- UserRepository.mergeTrustedGroups
|
|
||||||
update "user"
|
|
||||||
set
|
|
||||||
"trustedGroupId" = "u"."trustedGroupId"
|
|
||||||
from
|
|
||||||
"user" as "u"
|
|
||||||
where
|
|
||||||
"u"."id" = $1
|
|
||||||
and "user"."trustedGroupId" = (
|
|
||||||
select
|
|
||||||
"user"."trustedGroupId"
|
|
||||||
from
|
|
||||||
"user"
|
|
||||||
where
|
|
||||||
"user"."id" = $2
|
|
||||||
and "user"."trustedGroupId" != "u"."trustedGroupId"
|
|
||||||
)
|
|
||||||
|
|
||||||
-- UserRepository.updateTrustedGroups
|
|
||||||
update "user"
|
|
||||||
set
|
|
||||||
"trustedGroupId" = uuid_generate_v4 ()
|
|
||||||
where
|
|
||||||
"user"."trustedGroupId" = (
|
|
||||||
select
|
|
||||||
"user"."trustedGroupId"
|
|
||||||
from
|
|
||||||
"user"
|
|
||||||
where
|
|
||||||
"user"."id" = $1
|
|
||||||
)
|
|
||||||
and "user"."id" != $2
|
|
||||||
and "user"."id" not in (
|
|
||||||
select
|
|
||||||
"partner"."sharedById" as "userId"
|
|
||||||
from
|
|
||||||
"partner"
|
|
||||||
where
|
|
||||||
"sharedWithId" = $3
|
|
||||||
union
|
|
||||||
select
|
|
||||||
"album_user"."userId"
|
|
||||||
from
|
|
||||||
"album_user"
|
|
||||||
where
|
|
||||||
"album_user"."albumId" in (
|
|
||||||
select
|
|
||||||
"album_user"."albumId"
|
|
||||||
from
|
|
||||||
"album_user"
|
|
||||||
where
|
|
||||||
"album_user"."userId" = $4
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { Kysely, NotNull, sql } from 'kysely';
|
import { Kysely, NotNull, sql } from 'kysely';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
|
import { ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { AlbumUserRole, AssetVisibility, SharingPermission } from 'src/enum';
|
import { AlbumUserRole, AssetVisibility } from 'src/enum';
|
||||||
import { hasAssetPermissions } from 'src/repositories/asset.repository';
|
|
||||||
import { hasPermissions } from 'src/repositories/person.repository';
|
|
||||||
import { DB } from 'src/schema';
|
import { DB } from 'src/schema';
|
||||||
import { asUuid } from 'src/utils/database';
|
import { asUuid } from 'src/utils/database';
|
||||||
|
|
||||||
@@ -275,46 +273,6 @@ class AssetAccess {
|
|||||||
return allowedIds;
|
return allowedIds;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET, [SharingPermission.All]] })
|
|
||||||
async checkSharedAccess(userId: string, assetIds: Set<string>, permissions: SharingPermission[]) {
|
|
||||||
const ids = await this.db
|
|
||||||
.selectFrom('album_asset')
|
|
||||||
.select('album_asset.assetId')
|
|
||||||
.where('album_asset.assetId', 'in', [...assetIds])
|
|
||||||
.where('album_asset.albumId', 'in', (eb) =>
|
|
||||||
eb
|
|
||||||
.selectFrom('album_user')
|
|
||||||
.select('album_user.albumId')
|
|
||||||
.where((eb) =>
|
|
||||||
eb.or([
|
|
||||||
eb('album_user.permissions', '@>', sql<SharingPermission[]>`${permissions}::sharing_permission_enum[]`),
|
|
||||||
eb(eb.val(SharingPermission.All), '=', eb.fn.any('album_user.permissions')),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.innerJoin('album_user', (join) =>
|
|
||||||
join.onRef('album_asset.albumId', '=', 'album_user.albumId').on('album_user.userId', '=', userId),
|
|
||||||
)
|
|
||||||
.union((eb) =>
|
|
||||||
eb
|
|
||||||
.selectFrom('partner')
|
|
||||||
.where('partner.sharedWithId', '=', userId)
|
|
||||||
.where((eb) =>
|
|
||||||
eb.or([
|
|
||||||
eb('partner.permissions', '@>', sql<SharingPermission[]>`${permissions}::sharing_permission_enum[]`),
|
|
||||||
eb(eb.val(SharingPermission.All), '=', eb.fn.any('partner.permissions')),
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
.innerJoin('asset', (join) =>
|
|
||||||
join.onRef('asset.ownerId', '=', 'partner.sharedById').on('asset.id', 'in', [...assetIds]),
|
|
||||||
)
|
|
||||||
.select('asset.id as assetId'),
|
|
||||||
)
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
return new Set(ids.map(({ assetId }) => assetId));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthDeviceAccess {
|
class AuthDeviceAccess {
|
||||||
@@ -494,37 +452,6 @@ class PersonAccess {
|
|||||||
.execute()
|
.execute()
|
||||||
.then((faces) => new Set(faces.map((face) => face.id)));
|
.then((faces) => new Set(faces.map((face) => face.id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkSharedAccess(userId: string, personIds: Set<string>, permissions: SharingPermission[]) {
|
|
||||||
if (personIds.size === 0) {
|
|
||||||
return new Set<string>();
|
|
||||||
}
|
|
||||||
|
|
||||||
const ids = await this.db
|
|
||||||
.selectFrom('person')
|
|
||||||
.select('person.id')
|
|
||||||
.where('person.id', 'in', [...personIds])
|
|
||||||
.where(hasPermissions(userId, permissions))
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
return new Set(ids.map(({ id }) => id));
|
|
||||||
}
|
|
||||||
|
|
||||||
async checkSharedFaceAccess(userId: string, faceIds: Set<string>, permissions: SharingPermission[]) {
|
|
||||||
if (faceIds.size === 0) {
|
|
||||||
return new Set<string>();
|
|
||||||
}
|
|
||||||
|
|
||||||
const ids = await this.db
|
|
||||||
.selectFrom('asset_face')
|
|
||||||
.select('asset_face.id')
|
|
||||||
.leftJoin('asset', (join) => join.onRef('asset.id', '=', 'asset_face.assetId'))
|
|
||||||
.where('asset_face.id', 'in', [...faceIds])
|
|
||||||
.where(hasAssetPermissions(userId, permissions))
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
return new Set(ids.map(({ id }) => id));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class PartnerAccess {
|
class PartnerAccess {
|
||||||
|
|||||||
@@ -38,13 +38,4 @@ export class AlbumUserRepository {
|
|||||||
async delete({ userId, albumId }: AlbumPermissionId): Promise<void> {
|
async delete({ userId, albumId }: AlbumPermissionId): Promise<void> {
|
||||||
await this.db.deleteFrom('album_user').where('userId', '=', userId).where('albumId', '=', albumId).execute();
|
await this.db.deleteFrom('album_user').where('userId', '=', userId).where('albumId', '=', albumId).execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
get({ userId, albumId }: AlbumPermissionId) {
|
|
||||||
return this.db
|
|
||||||
.selectFrom('album_user')
|
|
||||||
.select(['permissions', 'inTimeline'])
|
|
||||||
.where('userId', '=', userId)
|
|
||||||
.where('albumId', '=', albumId)
|
|
||||||
.executeTakeFirstOrThrow();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
SelectQueryBuilder,
|
SelectQueryBuilder,
|
||||||
ShallowDehydrateObject,
|
ShallowDehydrateObject,
|
||||||
sql,
|
sql,
|
||||||
StringReference,
|
|
||||||
Updateable,
|
Updateable,
|
||||||
UpdateResult,
|
UpdateResult,
|
||||||
} from 'kysely';
|
} from 'kysely';
|
||||||
@@ -18,15 +17,7 @@ import { InjectKysely } from 'nestjs-kysely';
|
|||||||
import { LockableProperty, Stack } from 'src/database';
|
import { LockableProperty, Stack } from 'src/database';
|
||||||
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import {
|
import { AssetFileType, AssetOrder, AssetOrderBy, AssetStatus, AssetType, AssetVisibility } from 'src/enum';
|
||||||
AssetFileType,
|
|
||||||
AssetOrder,
|
|
||||||
AssetOrderBy,
|
|
||||||
AssetStatus,
|
|
||||||
AssetType,
|
|
||||||
AssetVisibility,
|
|
||||||
SharingPermission,
|
|
||||||
} from 'src/enum';
|
|
||||||
import { DB } from 'src/schema';
|
import { DB } from 'src/schema';
|
||||||
import { AssetAudioTable, AssetKeyframeTable, AssetVideoTable } from 'src/schema/tables/asset-av.table';
|
import { AssetAudioTable, AssetKeyframeTable, AssetVideoTable } from 'src/schema/tables/asset-av.table';
|
||||||
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||||
@@ -50,7 +41,6 @@ import {
|
|||||||
withFiles,
|
withFiles,
|
||||||
withLibrary,
|
withLibrary,
|
||||||
withOwner,
|
withOwner,
|
||||||
withPermissions,
|
|
||||||
withSmartSearch,
|
withSmartSearch,
|
||||||
withTagId,
|
withTagId,
|
||||||
withTags,
|
withTags,
|
||||||
@@ -175,93 +165,6 @@ const withBoundingBox = <T>(qb: SelectQueryBuilder<DB, 'asset' | 'asset_exif', T
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hasAssetPermissions =
|
|
||||||
(userId: string, permissions: SharingPermission[], ignoreTimelineVisibility: boolean = false) =>
|
|
||||||
(eb: ExpressionBuilder<DB, 'asset'>) =>
|
|
||||||
eb.or([
|
|
||||||
eb('asset.ownerId', '=', userId),
|
|
||||||
eb.exists(
|
|
||||||
eb
|
|
||||||
.selectFrom('partner')
|
|
||||||
.whereRef('partner.sharedById', '=', 'asset.ownerId')
|
|
||||||
.where('partner.sharedWithId', '=', userId)
|
|
||||||
.where((eb) =>
|
|
||||||
eb.or([
|
|
||||||
eb(eb.val(SharingPermission.All), '=', eb.fn.any('partner.permissions')),
|
|
||||||
eb('partner.permissions', '@>', eb.val(permissions)),
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
.$if(!ignoreTimelineVisibility, (qb) => qb.where('partner.inTimeline', '=', true)),
|
|
||||||
),
|
|
||||||
eb.exists(
|
|
||||||
eb
|
|
||||||
.selectFrom('album_asset')
|
|
||||||
.whereRef('album_asset.assetId', '=', 'asset.id')
|
|
||||||
.innerJoin('album_user', (join) =>
|
|
||||||
join.onRef('album_user.albumId', '=', 'album_asset.albumId').on('album_user.userId', '=', userId),
|
|
||||||
)
|
|
||||||
.$if(!ignoreTimelineVisibility, (qb) => qb.where('album_user.inTimeline', '=', true))
|
|
||||||
.where('album_user.albumId', 'in', (eb) =>
|
|
||||||
eb
|
|
||||||
.selectFrom('album_user')
|
|
||||||
.select('album_user.albumId')
|
|
||||||
.whereRef('album_user.userId', '=', 'asset.ownerId')
|
|
||||||
.where((eb) =>
|
|
||||||
eb.or([
|
|
||||||
eb(eb.val(SharingPermission.All), '=', eb.fn.any('album_user.permissions')),
|
|
||||||
eb('album_user.permissions', '@>', eb.val(permissions)),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const hasAssetPermissionsRef = <T extends keyof DB>(
|
|
||||||
eb: ExpressionBuilder<DB, 'asset'>,
|
|
||||||
userIdRef: StringReference<DB, 'asset' | T>,
|
|
||||||
permissions: SharingPermission[],
|
|
||||||
ignoreTimelineVisibility: boolean = false,
|
|
||||||
) =>
|
|
||||||
eb.or([
|
|
||||||
eb('asset.ownerId', '=', eb.ref(userIdRef as never)),
|
|
||||||
eb.exists(
|
|
||||||
eb
|
|
||||||
.selectFrom('partner')
|
|
||||||
.whereRef('partner.sharedById', '=', 'asset.ownerId')
|
|
||||||
.whereRef('partner.sharedWithId', '=', userIdRef as never)
|
|
||||||
.where((eb) =>
|
|
||||||
eb.or([
|
|
||||||
eb(eb.val(SharingPermission.All), '=', eb.fn.any('partner.permissions')),
|
|
||||||
eb('partner.permissions', '@>', eb.val(permissions)),
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
.$if(!ignoreTimelineVisibility, (qb) => qb.where('partner.inTimeline', '=', true)),
|
|
||||||
),
|
|
||||||
eb.exists(
|
|
||||||
eb
|
|
||||||
.selectFrom('album_asset')
|
|
||||||
.whereRef('album_asset.assetId', '=', 'asset.id')
|
|
||||||
.innerJoin('album_user', (join) =>
|
|
||||||
join
|
|
||||||
.onRef('album_user.albumId', '=', 'album_asset.albumId')
|
|
||||||
.onRef('album_user.userId', '=', userIdRef as never),
|
|
||||||
)
|
|
||||||
.$if(!ignoreTimelineVisibility, (qb) => qb.where('album_user.inTimeline', '=', true))
|
|
||||||
.where('album_user.albumId', 'in', (eb) =>
|
|
||||||
eb
|
|
||||||
.selectFrom('album_user')
|
|
||||||
.select('album_user.albumId')
|
|
||||||
.whereRef('album_user.userId', '=', 'asset.ownerId')
|
|
||||||
.where((eb) =>
|
|
||||||
eb.or([
|
|
||||||
eb(eb.val(SharingPermission.All), '=', eb.fn.any('album_user.permissions')),
|
|
||||||
eb('album_user.permissions', '@>', eb.val(permissions)),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AssetRepository {
|
export class AssetRepository {
|
||||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||||
@@ -653,22 +556,17 @@ export class AssetRepository {
|
|||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, {}, DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
getById(
|
getById(
|
||||||
id: string,
|
id: string,
|
||||||
{ exifInfo, faces, files, library, owner, smartSearch, stack, tags, edits }: GetByIdsRelations = {},
|
{ exifInfo, faces, files, library, owner, smartSearch, stack, tags, edits }: GetByIdsRelations = {},
|
||||||
userId?: string,
|
|
||||||
) {
|
) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset')
|
.selectFrom('asset')
|
||||||
.selectAll('asset')
|
.selectAll('asset')
|
||||||
.where('asset.id', '=', asUuid(id))
|
.where('asset.id', '=', asUuid(id))
|
||||||
.$if(!!exifInfo, withExif)
|
.$if(!!exifInfo, withExif)
|
||||||
.$if(!!faces, (qb) =>
|
.$if(!!faces, (qb) => qb.select(faces?.person ? withFacesAndPeople : withFaces).$narrowType<{ faces: NotNull }>())
|
||||||
qb
|
|
||||||
.select(faces?.person ? (eb) => withFacesAndPeople(eb, { userId }) : withFaces)
|
|
||||||
.$narrowType<{ faces: NotNull }>(),
|
|
||||||
)
|
|
||||||
.$if(!!library, (qb) => qb.select(withLibrary))
|
.$if(!!library, (qb) => qb.select(withLibrary))
|
||||||
.$if(!!owner, (qb) => qb.select(withOwner))
|
.$if(!!owner, (qb) => qb.select(withOwner))
|
||||||
.$if(!!smartSearch, withSmartSearch)
|
.$if(!!smartSearch, withSmartSearch)
|
||||||
@@ -704,7 +602,6 @@ export class AssetRepository {
|
|||||||
.$if(!!files, (qb) => qb.select(withFiles))
|
.$if(!!files, (qb) => qb.select(withFiles))
|
||||||
.$if(!!tags, (qb) => qb.select(withTags))
|
.$if(!!tags, (qb) => qb.select(withTags))
|
||||||
.$if(!!edits, (qb) => qb.select(withEdits))
|
.$if(!!edits, (qb) => qb.select(withEdits))
|
||||||
.$if(!!userId, (qb) => qb.select(withPermissions(userId!)))
|
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
@@ -847,9 +744,7 @@ export class AssetRepository {
|
|||||||
)
|
)
|
||||||
.where((eb) => eb.or([eb('asset.stackId', 'is', null), eb(eb.table('stack'), 'is not', null)])),
|
.where((eb) => eb.or([eb('asset.stackId', 'is', null), eb(eb.table('stack'), 'is not', null)])),
|
||||||
)
|
)
|
||||||
.$if(!!options.userIds, (qb) =>
|
.$if(!!options.userIds, (qb) => qb.where('asset.ownerId', '=', anyUuid(options.userIds!)))
|
||||||
qb.where(hasAssetPermissions(options.userIds![0], [SharingPermission.AssetRead], !!options.personId)),
|
|
||||||
)
|
|
||||||
.$if(options.isFavorite !== undefined, (qb) => qb.where('asset.isFavorite', '=', options.isFavorite!))
|
.$if(options.isFavorite !== undefined, (qb) => qb.where('asset.isFavorite', '=', options.isFavorite!))
|
||||||
.$if(!!options.assetType, (qb) => qb.where('asset.type', '=', options.assetType!))
|
.$if(!!options.assetType, (qb) => qb.where('asset.type', '=', options.assetType!))
|
||||||
.$if(options.isDuplicate !== undefined, (qb) =>
|
.$if(options.isDuplicate !== undefined, (qb) =>
|
||||||
@@ -934,9 +829,7 @@ export class AssetRepository {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.$if(!!options.personId, (qb) => hasPeople(qb, [options.personId!]))
|
.$if(!!options.personId, (qb) => hasPeople(qb, [options.personId!]))
|
||||||
.$if(!!options.userIds, (qb) =>
|
.$if(!!options.userIds, (qb) => qb.where('asset.ownerId', '=', anyUuid(options.userIds!)))
|
||||||
qb.where(hasAssetPermissions(options.userIds![0], [SharingPermission.AssetRead], !!options.personId)),
|
|
||||||
)
|
|
||||||
.$if(options.isFavorite !== undefined, (qb) => qb.where('asset.isFavorite', '=', options.isFavorite!))
|
.$if(options.isFavorite !== undefined, (qb) => qb.where('asset.isFavorite', '=', options.isFavorite!))
|
||||||
.$if(!!options.withStacked, (qb) =>
|
.$if(!!options.withStacked, (qb) =>
|
||||||
qb
|
qb
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { getKeyByValue, getMethodNames, ImmichStartupError } from 'src/utils/mis
|
|||||||
type JobMapItem = {
|
type JobMapItem = {
|
||||||
jobName: JobName;
|
jobName: JobName;
|
||||||
queueName: QueueName;
|
queueName: QueueName;
|
||||||
handler: (job?: JobOf<any>) => Promise<JobStatus>;
|
handler: (job: JobOf<any>) => Promise<JobStatus>;
|
||||||
label: string;
|
label: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -95,17 +95,14 @@ export class JobRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(job: JobItem) {
|
async run({ name, data }: JobItem) {
|
||||||
const item = this.handlers[job.name];
|
const item = this.handlers[name as JobName];
|
||||||
if (!item) {
|
if (!item) {
|
||||||
this.logger.warn(`Skipping unknown job: "${job.name}"`);
|
this.logger.warn(`Skipping unknown job: "${name}"`);
|
||||||
return JobStatus.Skipped;
|
return JobStatus.Skipped;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('data' in job) {
|
return item.handler(data);
|
||||||
return item.handler(job.data);
|
|
||||||
}
|
|
||||||
return item.handler();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setConcurrency(queueName: QueueName, concurrency: number) {
|
setConcurrency(queueName: QueueName, concurrency: number) {
|
||||||
@@ -170,7 +167,7 @@ export class JobRepository {
|
|||||||
const queueName = this.getQueueName(item.name);
|
const queueName = this.getQueueName(item.name);
|
||||||
const job = {
|
const job = {
|
||||||
name: item.name,
|
name: item.name,
|
||||||
data: ('data' in item ? item.data : undefined) || {},
|
data: item.data || {},
|
||||||
options: this.getJobOptions(item) || undefined,
|
options: this.getJobOptions(item) || undefined,
|
||||||
} as JobItem & { data: any; options: JobsOptions | undefined };
|
} as JobItem & { data: any; options: JobsOptions | undefined };
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export class MemoryRepository implements IBulkAsset {
|
|||||||
eb.exists(
|
eb.exists(
|
||||||
eb
|
eb
|
||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.innerJoin('person', 'person.faceClusterId', 'asset_face.faceClusterId')
|
.innerJoin('person', 'person.id', 'asset_face.personId')
|
||||||
.select((eb) => eb.val(1).as('one'))
|
.select((eb) => eb.val(1).as('one'))
|
||||||
.whereRef('asset_face.assetId', '=', 'asset.id')
|
.whereRef('asset_face.assetId', '=', 'asset.id')
|
||||||
.where('person.isHidden', '=', true),
|
.where('person.isHidden', '=', true),
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
|||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { AssetFace } from 'src/database';
|
import { AssetFace } from 'src/database';
|
||||||
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { AssetFileType, AssetVisibility, SharingPermission, SourceType } from 'src/enum';
|
import { AssetFileType, AssetVisibility, SourceType } from 'src/enum';
|
||||||
import { hasAssetPermissions, hasAssetPermissionsRef } from 'src/repositories/asset.repository';
|
|
||||||
import { DB } from 'src/schema';
|
import { DB } from 'src/schema';
|
||||||
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
||||||
import { FaceSearchTable } from 'src/schema/tables/face-search.table';
|
import { FaceSearchTable } from 'src/schema/tables/face-search.table';
|
||||||
@@ -34,9 +33,9 @@ export interface AssetFaceId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateFacesData {
|
export interface UpdateFacesData {
|
||||||
oldFaceClusterId?: string;
|
oldPersonId?: string;
|
||||||
faceIds?: string[];
|
faceIds?: string[];
|
||||||
newFaceClusterId: string;
|
newPersonId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PersonStatistics {
|
export interface PersonStatistics {
|
||||||
@@ -55,7 +54,7 @@ export interface GetAllPeopleOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface GetAllFacesOptions {
|
export interface GetAllFacesOptions {
|
||||||
faceClusterId?: string | null;
|
personId?: string | null;
|
||||||
assetId?: string;
|
assetId?: string;
|
||||||
sourceType?: SourceType;
|
sourceType?: SourceType;
|
||||||
}
|
}
|
||||||
@@ -64,27 +63,9 @@ export type UnassignFacesOptions = DeleteFacesOptions;
|
|||||||
|
|
||||||
export type SelectFaceOptions = (keyof Selectable<AssetFaceTable>)[];
|
export type SelectFaceOptions = (keyof Selectable<AssetFaceTable>)[];
|
||||||
|
|
||||||
const withPerson = (eb: ExpressionBuilder<DB, 'asset_face'>, userId?: string) => {
|
const withPerson = (eb: ExpressionBuilder<DB, 'asset_face'>) => {
|
||||||
return jsonObjectFrom(
|
return jsonObjectFrom(
|
||||||
eb
|
eb.selectFrom('person').selectAll('person').whereRef('person.id', '=', 'asset_face.personId'),
|
||||||
.selectFrom('person')
|
|
||||||
.selectAll('person')
|
|
||||||
.whereRef('person.faceClusterId', '=', 'asset_face.faceClusterId')
|
|
||||||
.$if(!!userId, (qb) =>
|
|
||||||
qb.where((eb) =>
|
|
||||||
eb.or([eb('person.ownerId', '=', userId!), hasPermissions(userId!, [SharingPermission.PersonRead])(eb)]),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.orderBy(
|
|
||||||
(eb) =>
|
|
||||||
eb(
|
|
||||||
'person.ownerId',
|
|
||||||
'=',
|
|
||||||
eb.selectFrom('asset').select('asset.ownerId').whereRef('asset.id', '=', 'asset_face.assetId'),
|
|
||||||
),
|
|
||||||
'desc',
|
|
||||||
)
|
|
||||||
.limit(1),
|
|
||||||
).as('person');
|
).as('person');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -94,47 +75,16 @@ const withFaceSearch = (eb: ExpressionBuilder<DB, 'asset_face'>) => {
|
|||||||
).as('faceSearch');
|
).as('faceSearch');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hasPermissions =
|
|
||||||
(userId: string, permissions: SharingPermission[]) => (eb: ExpressionBuilder<DB, 'person'>) =>
|
|
||||||
eb.or([
|
|
||||||
eb.exists((eb) =>
|
|
||||||
eb
|
|
||||||
.selectFrom('partner')
|
|
||||||
.whereRef('partner.sharedById', '=', 'person.ownerId')
|
|
||||||
.where('partner.sharedWithId', '=', userId)
|
|
||||||
.where((eb) =>
|
|
||||||
eb.or([
|
|
||||||
eb(eb.val(SharingPermission.All), '=', eb.fn.any('partner.permissions')),
|
|
||||||
eb('partner.permissions', '@>', eb.val(permissions)),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
eb.exists((eb) =>
|
|
||||||
eb
|
|
||||||
.selectFrom('album_user')
|
|
||||||
.where('album_user.albumId', 'in', (eb) =>
|
|
||||||
eb.selectFrom('album_user').select('album_user.albumId').where('album_user.userId', '=', userId),
|
|
||||||
)
|
|
||||||
.whereRef('album_user.userId', '=', 'person.ownerId')
|
|
||||||
.where((eb) =>
|
|
||||||
eb.or([
|
|
||||||
eb(eb.val(SharingPermission.All), '=', eb.fn.any('album_user.permissions')),
|
|
||||||
eb('album_user.permissions', '@>', eb.val(permissions)),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PersonRepository {
|
export class PersonRepository {
|
||||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||||
|
|
||||||
@GenerateSql({ params: [{ oldPersonId: DummyValue.UUID, newPersonId: DummyValue.UUID }] })
|
@GenerateSql({ params: [{ oldPersonId: DummyValue.UUID, newPersonId: DummyValue.UUID }] })
|
||||||
async reassignFaces({ oldFaceClusterId, faceIds, newFaceClusterId }: UpdateFacesData): Promise<number> {
|
async reassignFaces({ oldPersonId, faceIds, newPersonId }: UpdateFacesData): Promise<number> {
|
||||||
const result = await this.db
|
const result = await this.db
|
||||||
.updateTable('asset_face')
|
.updateTable('asset_face')
|
||||||
.set({ faceClusterId: newFaceClusterId })
|
.set({ personId: newPersonId })
|
||||||
.$if(!!oldFaceClusterId, (qb) => qb.where('asset_face.faceClusterId', '=', oldFaceClusterId!))
|
.$if(!!oldPersonId, (qb) => qb.where('asset_face.personId', '=', oldPersonId!))
|
||||||
.$if(!!faceIds, (qb) => qb.where('asset_face.id', 'in', faceIds!))
|
.$if(!!faceIds, (qb) => qb.where('asset_face.id', 'in', faceIds!))
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|
||||||
@@ -144,7 +94,7 @@ export class PersonRepository {
|
|||||||
async unassignFaces({ sourceType }: UnassignFacesOptions): Promise<void> {
|
async unassignFaces({ sourceType }: UnassignFacesOptions): Promise<void> {
|
||||||
await this.db
|
await this.db
|
||||||
.updateTable('asset_face')
|
.updateTable('asset_face')
|
||||||
.set({ faceClusterId: null })
|
.set({ personId: null })
|
||||||
.where('asset_face.sourceType', '=', sourceType)
|
.where('asset_face.sourceType', '=', sourceType)
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
@@ -167,8 +117,8 @@ export class PersonRepository {
|
|||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.selectAll('asset_face')
|
.selectAll('asset_face')
|
||||||
.$if(options.faceClusterId === null, (qb) => qb.where('asset_face.faceClusterId', 'is', null))
|
.$if(options.personId === null, (qb) => qb.where('asset_face.personId', 'is', null))
|
||||||
.$if(!!options.faceClusterId, (qb) => qb.where('asset_face.faceClusterId', '=', options.faceClusterId!))
|
.$if(!!options.personId, (qb) => qb.where('asset_face.personId', '=', options.personId!))
|
||||||
.$if(!!options.sourceType, (qb) => qb.where('asset_face.sourceType', '=', options.sourceType!))
|
.$if(!!options.sourceType, (qb) => qb.where('asset_face.sourceType', '=', options.sourceType!))
|
||||||
.$if(!!options.assetId, (qb) => qb.where('asset_face.assetId', '=', options.assetId!))
|
.$if(!!options.assetId, (qb) => qb.where('asset_face.assetId', '=', options.assetId!))
|
||||||
.where('asset_face.deletedAt', 'is', null)
|
.where('asset_face.deletedAt', 'is', null)
|
||||||
@@ -203,20 +153,16 @@ export class PersonRepository {
|
|||||||
const items = await this.db
|
const items = await this.db
|
||||||
.selectFrom('person')
|
.selectFrom('person')
|
||||||
.selectAll('person')
|
.selectAll('person')
|
||||||
.innerJoin('asset_face', 'asset_face.faceClusterId', 'person.faceClusterId')
|
.innerJoin('asset_face', 'asset_face.personId', 'person.id')
|
||||||
.innerJoin('asset', (join) =>
|
.innerJoin('asset', (join) =>
|
||||||
join
|
join
|
||||||
.onRef('asset_face.assetId', '=', 'asset.id')
|
.onRef('asset_face.assetId', '=', 'asset.id')
|
||||||
.on('asset.visibility', '=', sql.lit(AssetVisibility.Timeline))
|
.on('asset.visibility', '=', sql.lit(AssetVisibility.Timeline))
|
||||||
.on('asset.deletedAt', 'is', null),
|
.on('asset.deletedAt', 'is', null),
|
||||||
)
|
)
|
||||||
.where((eb) =>
|
.where('person.ownerId', '=', userId)
|
||||||
eb.or([eb('person.ownerId', '=', userId), hasPermissions(userId, [SharingPermission.PersonRead])(eb)]),
|
|
||||||
)
|
|
||||||
.where('asset_face.deletedAt', 'is', null)
|
.where('asset_face.deletedAt', 'is', null)
|
||||||
.where('asset_face.isVisible', 'is', true)
|
.where('asset_face.isVisible', 'is', true)
|
||||||
.orderBy('person.faceClusterId')
|
|
||||||
.orderBy((eb) => eb('person.ownerId', '=', userId), 'desc')
|
|
||||||
.orderBy('person.isHidden', 'asc')
|
.orderBy('person.isHidden', 'asc')
|
||||||
.orderBy('person.isFavorite', 'desc')
|
.orderBy('person.isFavorite', 'desc')
|
||||||
.having((eb) =>
|
.having((eb) =>
|
||||||
@@ -225,7 +171,6 @@ export class PersonRepository {
|
|||||||
eb((innerEb) => innerEb.fn.count('asset_face.assetId'), '>=', options?.minimumFaceCount || 1),
|
eb((innerEb) => innerEb.fn.count('asset_face.assetId'), '>=', options?.minimumFaceCount || 1),
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
.distinctOn('person.faceClusterId')
|
|
||||||
.groupBy('person.id')
|
.groupBy('person.id')
|
||||||
.$if(!!options?.closestFaceAssetId, (qb) =>
|
.$if(!!options?.closestFaceAssetId, (qb) =>
|
||||||
qb.orderBy((eb) =>
|
qb.orderBy((eb) =>
|
||||||
@@ -264,7 +209,7 @@ export class PersonRepository {
|
|||||||
return this.db
|
return this.db
|
||||||
.selectFrom('person')
|
.selectFrom('person')
|
||||||
.selectAll('person')
|
.selectAll('person')
|
||||||
.leftJoin('asset_face', 'asset_face.faceClusterId', 'person.faceClusterId')
|
.leftJoin('asset_face', 'asset_face.personId', 'person.id')
|
||||||
.where('asset_face.deletedAt', 'is', null)
|
.where('asset_face.deletedAt', 'is', null)
|
||||||
.where('asset_face.isVisible', 'is', true)
|
.where('asset_face.isVisible', 'is', true)
|
||||||
.having((eb) => eb.fn.count('asset_face.assetId'), '=', 0)
|
.having((eb) => eb.fn.count('asset_face.assetId'), '=', 0)
|
||||||
@@ -273,13 +218,13 @@ export class PersonRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
getFaces(assetId: string, options: { isVisible?: boolean; userId?: string } = {}) {
|
getFaces(assetId: string, options?: { isVisible?: boolean }) {
|
||||||
const { isVisible = true, userId } = options;
|
const isVisible = options === undefined ? true : options.isVisible;
|
||||||
|
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.selectAll('asset_face')
|
.selectAll('asset_face')
|
||||||
.select((eb) => withPerson(eb, userId))
|
.select(withPerson)
|
||||||
.where('asset_face.assetId', '=', assetId)
|
.where('asset_face.assetId', '=', assetId)
|
||||||
.where('asset_face.deletedAt', 'is', null)
|
.where('asset_face.deletedAt', 'is', null)
|
||||||
.$if(isVisible !== undefined, (qb) => qb.where('asset_face.isVisible', '=', isVisible!))
|
.$if(isVisible !== undefined, (qb) => qb.where('asset_face.isVisible', '=', isVisible!))
|
||||||
@@ -303,7 +248,7 @@ export class PersonRepository {
|
|||||||
getFaceForFacialRecognitionJob(id: string) {
|
getFaceForFacialRecognitionJob(id: string) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.select(['asset_face.id', 'asset_face.faceClusterId', 'asset_face.sourceType'])
|
.select(['asset_face.id', 'asset_face.personId', 'asset_face.sourceType'])
|
||||||
.select((eb) =>
|
.select((eb) =>
|
||||||
jsonObjectFrom(
|
jsonObjectFrom(
|
||||||
eb
|
eb
|
||||||
@@ -344,10 +289,10 @@ export class PersonRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
||||||
async reassignFace(assetFaceId: string, newFaceClusterId: string): Promise<number> {
|
async reassignFace(assetFaceId: string, newPersonId: string): Promise<number> {
|
||||||
const result = await this.db
|
const result = await this.db
|
||||||
.updateTable('asset_face')
|
.updateTable('asset_face')
|
||||||
.set({ faceClusterId: newFaceClusterId })
|
.set({ personId: newPersonId })
|
||||||
.where('asset_face.id', '=', assetFaceId)
|
.where('asset_face.id', '=', assetFaceId)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|
||||||
@@ -373,7 +318,6 @@ export class PersonRepository {
|
|||||||
.where('person.ownerId', '=', userId)
|
.where('person.ownerId', '=', userId)
|
||||||
.where(() => sql`f_unaccent("person"."name") %> f_unaccent(${personName})`)
|
.where(() => sql`f_unaccent("person"."name") %> f_unaccent(${personName})`)
|
||||||
.orderBy(sql`f_unaccent("person"."name") <->>> f_unaccent(${personName})`)
|
.orderBy(sql`f_unaccent("person"."name") <->>> f_unaccent(${personName})`)
|
||||||
.orderBy((eb) => eb('person.ownerId', '=', userId), 'desc')
|
|
||||||
.limit(100)
|
.limit(100)
|
||||||
.$if(!withHidden, (qb) => qb.where('person.isHidden', '=', false))
|
.$if(!withHidden, (qb) => qb.where('person.isHidden', '=', false))
|
||||||
.execute();
|
.execute();
|
||||||
@@ -391,7 +335,7 @@ export class PersonRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
async getStatistics(userId: string, personId: string): Promise<PersonStatistics> {
|
async getStatistics(personId: string): Promise<PersonStatistics> {
|
||||||
const result = await this.db
|
const result = await this.db
|
||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.leftJoin('asset', (join) =>
|
.leftJoin('asset', (join) =>
|
||||||
@@ -400,13 +344,10 @@ export class PersonRepository {
|
|||||||
.on('asset.visibility', '=', sql.lit(AssetVisibility.Timeline))
|
.on('asset.visibility', '=', sql.lit(AssetVisibility.Timeline))
|
||||||
.on('asset.deletedAt', 'is', null),
|
.on('asset.deletedAt', 'is', null),
|
||||||
)
|
)
|
||||||
.where(hasAssetPermissions(userId, [SharingPermission.AssetRead], true))
|
|
||||||
.select((eb) => eb.fn.count(eb.fn('distinct', ['asset.id'])).as('count'))
|
.select((eb) => eb.fn.count(eb.fn('distinct', ['asset.id'])).as('count'))
|
||||||
.where('asset_face.deletedAt', 'is', null)
|
.where('asset_face.deletedAt', 'is', null)
|
||||||
.where('asset_face.isVisible', 'is', true)
|
.where('asset_face.isVisible', 'is', true)
|
||||||
.where('asset_face.faceClusterId', '=', (eb) =>
|
.where('asset_face.personId', '=', personId)
|
||||||
eb.selectFrom('person').select('person.faceClusterId').where('person.id', '=', personId),
|
|
||||||
)
|
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -423,7 +364,7 @@ export class PersonRepository {
|
|||||||
eb.exists((eb) =>
|
eb.exists((eb) =>
|
||||||
eb
|
eb
|
||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.whereRef('asset_face.faceClusterId', '=', 'person.faceClusterId')
|
.whereRef('asset_face.personId', '=', 'person.id')
|
||||||
.where('asset_face.deletedAt', 'is', null)
|
.where('asset_face.deletedAt', 'is', null)
|
||||||
.where('asset_face.isVisible', '=', true)
|
.where('asset_face.isVisible', '=', true)
|
||||||
.where((eb) =>
|
.where((eb) =>
|
||||||
@@ -437,20 +378,13 @@ export class PersonRepository {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.where((eb) =>
|
.where('person.ownerId', '=', userId)
|
||||||
eb.or([eb('person.ownerId', '=', userId), hasPermissions(userId, [SharingPermission.PersonRead])(eb)]),
|
|
||||||
)
|
|
||||||
.select((eb) => eb.fn.coalesce(eb.fn.countAll<number>(), zero).as('total'))
|
.select((eb) => eb.fn.coalesce(eb.fn.countAll<number>(), zero).as('total'))
|
||||||
.select((eb) => eb.fn.coalesce(eb.fn.countAll<number>().filterWhere('isHidden', '=', true), zero).as('hidden'))
|
.select((eb) => eb.fn.coalesce(eb.fn.countAll<number>().filterWhere('isHidden', '=', true), zero).as('hidden'))
|
||||||
.executeTakeFirstOrThrow();
|
.executeTakeFirstOrThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(person: Insertable<PersonTable>) {
|
create(person: Insertable<PersonTable>) {
|
||||||
if (!person.faceClusterId) {
|
|
||||||
const { id } = await this.db.insertInto('face_cluster').defaultValues().returning('id').executeTakeFirstOrThrow();
|
|
||||||
person.faceClusterId = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.db.insertInto('person').values(person).returningAll().executeTakeFirstOrThrow();
|
return this.db.insertInto('person').values(person).returningAll().executeTakeFirstOrThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -541,9 +475,8 @@ export class PersonRepository {
|
|||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.selectAll('asset_face')
|
.selectAll('asset_face')
|
||||||
.select(withPerson)
|
.select(withPerson)
|
||||||
.innerJoin('person', (join) => join.onRef('person.faceClusterId', '=', 'asset_face.faceClusterId'))
|
|
||||||
.where('person.id', 'in', personIds)
|
|
||||||
.where('asset_face.assetId', 'in', assetIds)
|
.where('asset_face.assetId', 'in', assetIds)
|
||||||
|
.where('asset_face.personId', 'in', personIds)
|
||||||
.where('asset_face.deletedAt', 'is', null)
|
.where('asset_face.deletedAt', 'is', null)
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
@@ -553,15 +486,7 @@ export class PersonRepository {
|
|||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.selectAll('asset_face')
|
.selectAll('asset_face')
|
||||||
.innerJoin('person', (join) =>
|
.where('asset_face.personId', '=', personId)
|
||||||
join.onRef('asset_face.faceClusterId', '=', 'person.faceClusterId').on('person.id', '=', personId),
|
|
||||||
)
|
|
||||||
.where('asset_face.assetId', 'in', (eb) =>
|
|
||||||
eb
|
|
||||||
.selectFrom('asset')
|
|
||||||
.select('asset.id')
|
|
||||||
.where((eb) => hasAssetPermissionsRef(eb, 'person.ownerId', [SharingPermission.AssetRead], true)),
|
|
||||||
)
|
|
||||||
.where('asset_face.deletedAt', 'is', null)
|
.where('asset_face.deletedAt', 'is', null)
|
||||||
.where('asset_face.isVisible', 'is', true)
|
.where('asset_face.isVisible', 'is', true)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
@@ -648,14 +573,8 @@ export class PersonRepository {
|
|||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.select('asset_face.id')
|
.select('asset_face.id')
|
||||||
.where('asset_face.assetId', '=', assetId)
|
.where('asset_face.assetId', '=', assetId)
|
||||||
.innerJoin('person', (join) =>
|
.where('asset_face.personId', '=', personId)
|
||||||
join.onRef('person.faceClusterId', '=', 'asset_face.faceClusterId').on('person.id', '=', personId),
|
|
||||||
)
|
|
||||||
.innerJoin('asset', (join) => join.onRef('asset.id', '=', 'asset_face.assetId').on('asset.isOffline', '=', false))
|
.innerJoin('asset', (join) => join.onRef('asset.id', '=', 'asset_face.assetId').on('asset.isOffline', '=', false))
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
getByFaceClusterId(faceClusterId: string) {
|
|
||||||
return this.db.selectFrom('person').selectAll().where('person.faceClusterId', '=', faceClusterId).execute();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -325,23 +325,15 @@ export class SearchRepository {
|
|||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.select([
|
.select([
|
||||||
'asset_face.id',
|
'asset_face.id',
|
||||||
'asset_face.faceClusterId',
|
'asset_face.personId',
|
||||||
sql<number>`face_search.embedding <=> ${embedding}`.as('distance'),
|
sql<number>`face_search.embedding <=> ${embedding}`.as('distance'),
|
||||||
])
|
])
|
||||||
.innerJoin('asset', 'asset.id', 'asset_face.assetId')
|
.innerJoin('asset', 'asset.id', 'asset_face.assetId')
|
||||||
.select('asset.ownerId')
|
|
||||||
.innerJoin('face_search', 'face_search.faceId', 'asset_face.id')
|
.innerJoin('face_search', 'face_search.faceId', 'asset_face.id')
|
||||||
.leftJoin('person', 'person.faceClusterId', 'asset_face.faceClusterId')
|
.leftJoin('person', 'person.id', 'asset_face.personId')
|
||||||
.where('asset.ownerId', 'in', (eb) =>
|
.where('asset.ownerId', '=', anyUuid(userIds))
|
||||||
eb
|
|
||||||
.selectFrom('user')
|
|
||||||
.select('user.id')
|
|
||||||
.where('user.trustedGroupId', 'in', (eb) =>
|
|
||||||
eb.selectFrom('user').select('user.trustedGroupId').where('user.id', '=', anyUuid(userIds)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.where('asset.deletedAt', 'is', null)
|
.where('asset.deletedAt', 'is', null)
|
||||||
.$if(!!hasPerson, (qb) => qb.where('asset_face.faceClusterId', 'is not', null))
|
.$if(!!hasPerson, (qb) => qb.where('asset_face.personId', 'is not', null))
|
||||||
.$if(!!minBirthDate, (qb) =>
|
.$if(!!minBirthDate, (qb) =>
|
||||||
qb.where((eb) =>
|
qb.where((eb) =>
|
||||||
eb.or([eb('person.birthDate', 'is', null), eb('person.birthDate', '<=', minBirthDate!)]),
|
eb.or([eb('person.birthDate', 'is', null), eb('person.birthDate', '<=', minBirthDate!)]),
|
||||||
|
|||||||
@@ -443,7 +443,7 @@ class AssetFaceSync extends BaseSync {
|
|||||||
.select([
|
.select([
|
||||||
'asset_face.id',
|
'asset_face.id',
|
||||||
'assetId',
|
'assetId',
|
||||||
'faceClusterId',
|
'personId',
|
||||||
'imageWidth',
|
'imageWidth',
|
||||||
'imageHeight',
|
'imageHeight',
|
||||||
'boundingBoxX1',
|
'boundingBoxX1',
|
||||||
|
|||||||
@@ -325,61 +325,4 @@ export class UserRepository {
|
|||||||
|
|
||||||
await query.execute();
|
await query.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
|
||||||
async getInSameTrustedGroup(userId: string) {
|
|
||||||
return this.db
|
|
||||||
.selectFrom('user')
|
|
||||||
.select('user.id')
|
|
||||||
.where('user.trustedGroupId', '=', (eb) =>
|
|
||||||
eb.selectFrom('user').select('user.trustedGroupId').where('user.id', '=', userId),
|
|
||||||
)
|
|
||||||
.execute()
|
|
||||||
.then((result) => result.map(({ id }) => id));
|
|
||||||
}
|
|
||||||
|
|
||||||
@GenerateSql({ params: [{ userId: DummyValue.UUID, userIdToMerge: DummyValue.UUID }] })
|
|
||||||
async mergeTrustedGroups({ userId, userIdToMerge }: { userId: string; userIdToMerge: string }) {
|
|
||||||
return this.db
|
|
||||||
.updateTable('user')
|
|
||||||
.from('user as u')
|
|
||||||
.where('u.id', '=', userId)
|
|
||||||
.where('user.trustedGroupId', '=', (eb) =>
|
|
||||||
eb
|
|
||||||
.selectFrom('user')
|
|
||||||
.select('user.trustedGroupId')
|
|
||||||
.where('user.id', '=', userIdToMerge)
|
|
||||||
.whereRef('user.trustedGroupId', '!=', 'u.trustedGroupId'),
|
|
||||||
)
|
|
||||||
.set((eb) => ({
|
|
||||||
trustedGroupId: eb.ref('u.trustedGroupId'),
|
|
||||||
}))
|
|
||||||
.executeTakeFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
|
||||||
async updateTrustedGroups(userId: string) {
|
|
||||||
return this.db
|
|
||||||
.updateTable('user')
|
|
||||||
.set((eb) => ({ trustedGroupId: eb.fn('uuid_generate_v4') }))
|
|
||||||
.where('user.trustedGroupId', '=', (eb) =>
|
|
||||||
eb.selectFrom('user').select('user.trustedGroupId').where('user.id', '=', userId),
|
|
||||||
)
|
|
||||||
.where('user.id', '!=', userId)
|
|
||||||
.where('user.id', 'not in', (eb) =>
|
|
||||||
eb
|
|
||||||
.selectFrom('partner')
|
|
||||||
.select('partner.sharedById as userId')
|
|
||||||
.where('sharedWithId', '=', userId)
|
|
||||||
.union((eb) =>
|
|
||||||
eb
|
|
||||||
.selectFrom('album_user')
|
|
||||||
.select('album_user.userId')
|
|
||||||
.where('album_user.albumId', 'in', (eb) =>
|
|
||||||
eb.selectFrom('album_user').select('album_user.albumId').where('album_user.userId', '=', userId),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.executeTakeFirst();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
AssetStatus,
|
AssetStatus,
|
||||||
AssetVisibility,
|
AssetVisibility,
|
||||||
ChecksumAlgorithm,
|
ChecksumAlgorithm,
|
||||||
SharingPermission,
|
|
||||||
SourceType,
|
SourceType,
|
||||||
VideoSegmentCodec,
|
VideoSegmentCodec,
|
||||||
} from 'src/enum';
|
} from 'src/enum';
|
||||||
@@ -38,8 +37,3 @@ export const video_stream_variant_codec_enum = registerEnum({
|
|||||||
name: 'video_stream_variant_codec_enum',
|
name: 'video_stream_variant_codec_enum',
|
||||||
values: Object.values(VideoSegmentCodec),
|
values: Object.values(VideoSegmentCodec),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const sharing_permission_enum = registerEnum({
|
|
||||||
name: 'sharing_permission_enum',
|
|
||||||
values: Object.values(SharingPermission),
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
asset_face_source_type,
|
asset_face_source_type,
|
||||||
asset_visibility_enum,
|
asset_visibility_enum,
|
||||||
assets_status_enum,
|
assets_status_enum,
|
||||||
sharing_permission_enum,
|
|
||||||
} from 'src/schema/enums';
|
} from 'src/schema/enums';
|
||||||
import {
|
import {
|
||||||
album_user_after_insert,
|
album_user_after_insert,
|
||||||
@@ -46,7 +45,6 @@ import { AssetMetadataAuditTable } from 'src/schema/tables/asset-metadata-audit.
|
|||||||
import { AssetMetadataTable } from 'src/schema/tables/asset-metadata.table';
|
import { AssetMetadataTable } from 'src/schema/tables/asset-metadata.table';
|
||||||
import { AssetOcrTable } from 'src/schema/tables/asset-ocr.table';
|
import { AssetOcrTable } from 'src/schema/tables/asset-ocr.table';
|
||||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||||
import { FaceClusterTable } from 'src/schema/tables/face-cluster.table';
|
|
||||||
import { FaceSearchTable } from 'src/schema/tables/face-search.table';
|
import { FaceSearchTable } from 'src/schema/tables/face-search.table';
|
||||||
import { GeodataPlacesTable } from 'src/schema/tables/geodata-places.table';
|
import { GeodataPlacesTable } from 'src/schema/tables/geodata-places.table';
|
||||||
import { LibraryTable } from 'src/schema/tables/library.table';
|
import { LibraryTable } from 'src/schema/tables/library.table';
|
||||||
@@ -112,7 +110,6 @@ export class ImmichDatabase {
|
|||||||
AssetTable,
|
AssetTable,
|
||||||
AssetFileTable,
|
AssetFileTable,
|
||||||
AssetExifTable,
|
AssetExifTable,
|
||||||
FaceClusterTable,
|
|
||||||
FaceSearchTable,
|
FaceSearchTable,
|
||||||
GeodataPlacesTable,
|
GeodataPlacesTable,
|
||||||
LibraryTable,
|
LibraryTable,
|
||||||
@@ -173,13 +170,7 @@ export class ImmichDatabase {
|
|||||||
asset_face_audit,
|
asset_face_audit,
|
||||||
];
|
];
|
||||||
|
|
||||||
enum = [
|
enum = [album_user_role_enum, assets_status_enum, asset_face_source_type, asset_visibility_enum];
|
||||||
album_user_role_enum,
|
|
||||||
assets_status_enum,
|
|
||||||
asset_face_source_type,
|
|
||||||
asset_visibility_enum,
|
|
||||||
sharing_permission_enum,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Migrations {
|
export interface Migrations {
|
||||||
@@ -220,7 +211,6 @@ export interface DB {
|
|||||||
ocr_search: OcrSearchTable;
|
ocr_search: OcrSearchTable;
|
||||||
|
|
||||||
face_search: FaceSearchTable;
|
face_search: FaceSearchTable;
|
||||||
face_cluster: FaceClusterTable;
|
|
||||||
|
|
||||||
geodata_places: GeodataPlacesTable;
|
geodata_places: GeodataPlacesTable;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`ALTER TABLE "workflow" ADD "updateId" uuid NOT NULL DEFAULT immich_uuid_v7();`.execute(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`ALTER TABLE "workflow" DROP COLUMN "updateId";`.execute(db);
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { Kysely, sql } from 'kysely';
|
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
|
||||||
await sql`CREATE TYPE "sharing_permission_enum" AS ENUM ('all','asset.read','asset.update','asset.edit','asset.delete','asset.share','exif.read','person.read','person.update','person.merge','person.delete');`.execute(db);
|
|
||||||
await sql`ALTER TABLE "user" ADD "trustedGroupId" uuid NOT NULL DEFAULT uuid_generate_v4();`.execute(db);
|
|
||||||
await sql`ALTER TABLE "album_user" ADD "permissions" sharing_permission_enum[] NOT NULL DEFAULT '{asset.read,exif.read}';`.execute(db);
|
|
||||||
await sql`ALTER TABLE "album_user" ADD "inTimeline" boolean NOT NULL DEFAULT false;`.execute(db);
|
|
||||||
await sql`ALTER TABLE "partner" ADD "permissions" sharing_permission_enum[] NOT NULL DEFAULT '{all}';`.execute(db);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
|
||||||
await sql`DROP TYPE "sharing_permission_enum";`.execute(db);
|
|
||||||
await sql`ALTER TABLE "partner" DROP COLUMN "permissions";`.execute(db);
|
|
||||||
await sql`ALTER TABLE "user" DROP COLUMN "trustedGroupId";`.execute(db);
|
|
||||||
await sql`ALTER TABLE "album_user" DROP COLUMN "permissions";`.execute(db);
|
|
||||||
await sql`ALTER TABLE "album_user" DROP COLUMN "inTimeline";`.execute(db);
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { Kysely, sql } from 'kysely';
|
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
|
||||||
await sql`ALTER TABLE "asset_face" RENAME COLUMN "personId" TO "faceClusterId";`.execute(db);
|
|
||||||
await sql`CREATE INDEX "asset_face_faceClusterId_assetId_idx" ON "asset_face" ("faceClusterId", "assetId");`.execute(db);
|
|
||||||
await sql`CREATE INDEX "asset_face_faceClusterId_assetId_notDeleted_isVisible_idx" ON "asset_face" ("faceClusterId", "assetId") WHERE ("deletedAt" IS NULL AND "isVisible" IS TRUE);`.execute(db);
|
|
||||||
await sql`CREATE INDEX "asset_face_assetId_faceClusterId_idx" ON "asset_face" ("assetId", "faceClusterId");`.execute(db);
|
|
||||||
await sql`DROP INDEX "asset_face_personId_assetId_notDeleted_isVisible_idx";`.execute(db);
|
|
||||||
await sql`DROP INDEX "asset_face_assetId_personId_idx";`.execute(db);
|
|
||||||
await sql`DROP INDEX "asset_face_personId_assetId_idx";`.execute(db);
|
|
||||||
await sql`CREATE TABLE "face_cluster" (
|
|
||||||
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
|
||||||
"createdAt" timestamp with time zone NOT NULL DEFAULT now(),
|
|
||||||
"updatedAt" timestamp with time zone NOT NULL DEFAULT now(),
|
|
||||||
"updateId" uuid NOT NULL DEFAULT immich_uuid_v7(),
|
|
||||||
CONSTRAINT "face_cluster_pkey" PRIMARY KEY ("id")
|
|
||||||
);`.execute(db);
|
|
||||||
await sql`ALTER TABLE "asset_face" ADD CONSTRAINT "asset_face_faceClusterId_fkey" FOREIGN KEY ("faceClusterId") REFERENCES "face_cluster" ("id") ON UPDATE CASCADE ON DELETE SET NULL;`.execute(db);
|
|
||||||
await sql`ALTER TABLE "asset_face" DROP CONSTRAINT "asset_face_personId_fkey";`.execute(db);
|
|
||||||
await sql`ALTER TABLE "person" ADD "faceClusterId" uuid;`.execute(db);
|
|
||||||
await sql`CREATE INDEX "person_faceClusterId_idx" ON "person" ("faceClusterId");`.execute(db);
|
|
||||||
await sql`ALTER TABLE "person" ADD CONSTRAINT "person_faceClusterId_fkey" FOREIGN KEY ("faceClusterId") REFERENCES "face_cluster" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
|
||||||
await sql`CREATE INDEX "face_cluster_updateId_idx" ON "face_cluster" ("updateId");`.execute(db);
|
|
||||||
await sql`CREATE OR REPLACE TRIGGER "face_cluster_updatedAt"
|
|
||||||
BEFORE UPDATE ON "face_cluster"
|
|
||||||
FOR EACH ROW
|
|
||||||
EXECUTE FUNCTION updated_at();`.execute(db);
|
|
||||||
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_face_cluster_updatedAt', '{"type":"trigger","name":"face_cluster_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"face_cluster_updatedAt\\"\\n BEFORE UPDATE ON \\"face_cluster\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db);
|
|
||||||
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('index_asset_face_faceClusterId_assetId_notDeleted_isVisible_idx', '{"type":"index","name":"asset_face_faceClusterId_assetId_notDeleted_isVisible_idx","sql":"CREATE INDEX \\"asset_face_faceClusterId_assetId_notDeleted_isVisible_idx\\" ON \\"asset_face\\" (\\"faceClusterId\\", \\"assetId\\") WHERE (\\"deletedAt\\" IS NULL AND \\"isVisible\\" IS TRUE);"}'::jsonb);`.execute(db);
|
|
||||||
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'index_asset_face_personId_assetId_notDeleted_isVisible_idx';`.execute(db);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
|
||||||
await sql`ALTER TABLE "person" DROP COLUMN "faceClusterId";`.execute(db);
|
|
||||||
await sql`DROP INDEX "person_faceClusterId_idx";`.execute(db);
|
|
||||||
await sql`ALTER TABLE "person" DROP CONSTRAINT "person_faceClusterId_fkey";`.execute(db);
|
|
||||||
await sql`ALTER TABLE "asset_face" RENAME COLUMN "faceClusterId" TO "personId";`.execute(db);
|
|
||||||
await sql`CREATE INDEX "asset_face_personId_assetId_notDeleted_isVisible_idx" ON "asset_face" ("personId", "assetId") WHERE ((("deletedAt" IS NULL) AND ("isVisible" IS TRUE)));`.execute(db);
|
|
||||||
await sql`CREATE INDEX "asset_face_assetId_personId_idx" ON "asset_face" ("assetId", "personId");`.execute(db);
|
|
||||||
await sql`CREATE INDEX "asset_face_personId_assetId_idx" ON "asset_face" ("personId", "assetId");`.execute(db);
|
|
||||||
await sql`DROP INDEX "asset_face_faceClusterId_assetId_idx";`.execute(db);
|
|
||||||
await sql`DROP INDEX "asset_face_faceClusterId_assetId_notDeleted_isVisible_idx";`.execute(db);
|
|
||||||
await sql`DROP INDEX "asset_face_assetId_faceClusterId_idx";`.execute(db);
|
|
||||||
await sql`ALTER TABLE "asset_face" ADD CONSTRAINT "asset_face_personId_fkey" FOREIGN KEY ("personId") REFERENCES "person" ("id") ON UPDATE CASCADE ON DELETE SET NULL;`.execute(db);
|
|
||||||
await sql`ALTER TABLE "asset_face" DROP CONSTRAINT "asset_face_faceClusterId_fkey";`.execute(db);
|
|
||||||
await sql`DROP TABLE "face_cluster";`.execute(db);
|
|
||||||
await sql`DROP TRIGGER "face_cluster_updatedAt" ON "face_cluster";`.execute(db);
|
|
||||||
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('index_asset_face_personId_assetId_notDeleted_isVisible_idx', '{"sql":"CREATE INDEX \\"asset_face_personId_assetId_notDeleted_isVisible_idx\\" ON \\"asset_face\\" (\\"personId\\", \\"assetId\\") WHERE (\\"deletedAt\\" IS NULL AND \\"isVisible\\" IS TRUE);","name":"asset_face_personId_assetId_notDeleted_isVisible_idx","type":"index"}'::jsonb);`.execute(db);
|
|
||||||
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_face_cluster_updatedAt';`.execute(db);
|
|
||||||
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'index_asset_face_faceClusterId_assetId_notDeleted_isVisible_idx';`.execute(db);
|
|
||||||
}
|
|
||||||
@@ -11,8 +11,8 @@ import {
|
|||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from '@immich/sql-tools';
|
} from '@immich/sql-tools';
|
||||||
import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { AlbumUserRole, SharingPermission } from 'src/enum';
|
import { AlbumUserRole } from 'src/enum';
|
||||||
import { album_user_role_enum, sharing_permission_enum } from 'src/schema/enums';
|
import { album_user_role_enum } from 'src/schema/enums';
|
||||||
import { album_user_after_insert, album_user_delete_audit } from 'src/schema/functions';
|
import { album_user_after_insert, album_user_delete_audit } from 'src/schema/functions';
|
||||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
@@ -69,14 +69,4 @@ export class AlbumUserTable {
|
|||||||
|
|
||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
updatedAt!: Generated<Timestamp>;
|
updatedAt!: Generated<Timestamp>;
|
||||||
|
|
||||||
@Column({
|
|
||||||
array: true,
|
|
||||||
enum: sharing_permission_enum,
|
|
||||||
default: [SharingPermission.AssetRead, SharingPermission.ExifRead],
|
|
||||||
})
|
|
||||||
permissions!: Generated<SharingPermission[]>;
|
|
||||||
|
|
||||||
@Column({ type: 'boolean', default: false })
|
|
||||||
inTimeline!: Generated<boolean>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { SourceType } from 'src/enum';
|
|||||||
import { asset_face_source_type } from 'src/schema/enums';
|
import { asset_face_source_type } from 'src/schema/enums';
|
||||||
import { asset_face_audit } from 'src/schema/functions';
|
import { asset_face_audit } from 'src/schema/functions';
|
||||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||||
import { FaceClusterTable } from 'src/schema/tables/face-cluster.table';
|
import { PersonTable } from 'src/schema/tables/person.table';
|
||||||
|
|
||||||
@Table({ name: 'asset_face' })
|
@Table({ name: 'asset_face' })
|
||||||
@UpdatedAtTrigger('asset_face_updatedAt')
|
@UpdatedAtTrigger('asset_face_updatedAt')
|
||||||
@@ -26,13 +26,13 @@ import { FaceClusterTable } from 'src/schema/tables/face-cluster.table';
|
|||||||
when: 'pg_trigger_depth() = 0',
|
when: 'pg_trigger_depth() = 0',
|
||||||
})
|
})
|
||||||
// schemaFromDatabase does not preserve column order
|
// schemaFromDatabase does not preserve column order
|
||||||
@Index({ name: 'asset_face_assetId_faceClusterId_idx', columns: ['assetId', 'faceClusterId'] })
|
@Index({ name: 'asset_face_assetId_personId_idx', columns: ['assetId', 'personId'] })
|
||||||
@Index({
|
@Index({
|
||||||
name: 'asset_face_faceClusterId_assetId_notDeleted_isVisible_idx',
|
name: 'asset_face_personId_assetId_notDeleted_isVisible_idx',
|
||||||
columns: ['faceClusterId', 'assetId'],
|
columns: ['personId', 'assetId'],
|
||||||
where: '"deletedAt" IS NULL AND "isVisible" IS TRUE',
|
where: '"deletedAt" IS NULL AND "isVisible" IS TRUE',
|
||||||
})
|
})
|
||||||
@Index({ columns: ['faceClusterId', 'assetId'] })
|
@Index({ columns: ['personId', 'assetId'] })
|
||||||
export class AssetFaceTable {
|
export class AssetFaceTable {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id!: Generated<string>;
|
id!: Generated<string>;
|
||||||
@@ -45,14 +45,14 @@ export class AssetFaceTable {
|
|||||||
})
|
})
|
||||||
assetId!: string;
|
assetId!: string;
|
||||||
|
|
||||||
@ForeignKeyColumn(() => FaceClusterTable, {
|
@ForeignKeyColumn(() => PersonTable, {
|
||||||
onDelete: 'SET NULL',
|
onDelete: 'SET NULL',
|
||||||
onUpdate: 'CASCADE',
|
onUpdate: 'CASCADE',
|
||||||
nullable: true,
|
nullable: true,
|
||||||
// [faceClusterId, assetId] makes this redundant
|
// [personId, assetId] makes this redundant
|
||||||
index: false,
|
index: false,
|
||||||
})
|
})
|
||||||
faceClusterId!: string | null;
|
personId!: string | null;
|
||||||
|
|
||||||
@Column({ default: 0, type: 'integer' })
|
@Column({ default: 0, type: 'integer' })
|
||||||
imageWidth!: Generated<number>;
|
imageWidth!: Generated<number>;
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import {
|
|
||||||
CreateDateColumn,
|
|
||||||
Generated,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
Table,
|
|
||||||
Timestamp,
|
|
||||||
UpdateDateColumn,
|
|
||||||
} from '@immich/sql-tools';
|
|
||||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
|
||||||
|
|
||||||
@Table('face_cluster')
|
|
||||||
@UpdatedAtTrigger('face_cluster_updatedAt')
|
|
||||||
export class FaceClusterTable {
|
|
||||||
@PrimaryGeneratedColumn('uuid')
|
|
||||||
id!: Generated<string>;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
|
||||||
createdAt!: Generated<Timestamp>;
|
|
||||||
|
|
||||||
@UpdateDateColumn()
|
|
||||||
updatedAt!: Generated<Timestamp>;
|
|
||||||
|
|
||||||
@UpdateIdColumn({ index: true })
|
|
||||||
updateId!: Generated<string>;
|
|
||||||
}
|
|
||||||
@@ -9,8 +9,6 @@ import {
|
|||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from '@immich/sql-tools';
|
} from '@immich/sql-tools';
|
||||||
import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { SharingPermission } from 'src/enum';
|
|
||||||
import { sharing_permission_enum } from 'src/schema/enums';
|
|
||||||
import { partner_delete_audit } from 'src/schema/functions';
|
import { partner_delete_audit } from 'src/schema/functions';
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
|
|
||||||
@@ -48,7 +46,4 @@ export class PartnerTable {
|
|||||||
|
|
||||||
@UpdateIdColumn({ index: true })
|
@UpdateIdColumn({ index: true })
|
||||||
updateId!: Generated<string>;
|
updateId!: Generated<string>;
|
||||||
|
|
||||||
@Column({ array: true, enum: sharing_permission_enum, default: [SharingPermission.All] })
|
|
||||||
permissions!: Generated<SharingPermission[]>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { person_delete_audit } from 'src/schema/functions';
|
import { person_delete_audit } from 'src/schema/functions';
|
||||||
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
||||||
import { FaceClusterTable } from 'src/schema/tables/face-cluster.table';
|
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
|
|
||||||
@Table('person')
|
@Table('person')
|
||||||
@@ -44,6 +43,9 @@ export class PersonTable {
|
|||||||
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
|
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
|
||||||
ownerId!: string;
|
ownerId!: string;
|
||||||
|
|
||||||
|
@Column({ default: '' })
|
||||||
|
name!: Generated<string>;
|
||||||
|
|
||||||
@Column({ default: '' })
|
@Column({ default: '' })
|
||||||
thumbnailPath!: Generated<string>;
|
thumbnailPath!: Generated<string>;
|
||||||
|
|
||||||
@@ -53,9 +55,6 @@ export class PersonTable {
|
|||||||
@Column({ type: 'date', nullable: true })
|
@Column({ type: 'date', nullable: true })
|
||||||
birthDate!: Timestamp | null;
|
birthDate!: Timestamp | null;
|
||||||
|
|
||||||
@Column({ default: '' })
|
|
||||||
name!: Generated<string>;
|
|
||||||
|
|
||||||
@ForeignKeyColumn(() => AssetFaceTable, { onDelete: 'SET NULL', nullable: true })
|
@ForeignKeyColumn(() => AssetFaceTable, { onDelete: 'SET NULL', nullable: true })
|
||||||
faceAssetId!: string | null;
|
faceAssetId!: string | null;
|
||||||
|
|
||||||
@@ -67,7 +66,4 @@ export class PersonTable {
|
|||||||
|
|
||||||
@UpdateIdColumn({ index: true })
|
@UpdateIdColumn({ index: true })
|
||||||
updateId!: Generated<string>;
|
updateId!: Generated<string>;
|
||||||
|
|
||||||
@ForeignKeyColumn(() => FaceClusterTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: true, index: true })
|
|
||||||
faceClusterId!: string | null;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
DeleteDateColumn,
|
DeleteDateColumn,
|
||||||
Generated,
|
Generated,
|
||||||
GeneratedColumn,
|
|
||||||
Index,
|
Index,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
Table,
|
Table,
|
||||||
@@ -83,7 +82,4 @@ export class UserTable {
|
|||||||
|
|
||||||
@UpdateIdColumn({ index: true })
|
@UpdateIdColumn({ index: true })
|
||||||
updateId!: Generated<string>;
|
updateId!: Generated<string>;
|
||||||
|
|
||||||
@GeneratedColumn('uuid')
|
|
||||||
trustedGroupId!: Generated<string>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,15 +8,13 @@ import {
|
|||||||
CreateAlbumDto,
|
CreateAlbumDto,
|
||||||
GetAlbumsDto,
|
GetAlbumsDto,
|
||||||
mapAlbum,
|
mapAlbum,
|
||||||
SharingPermissionsResponseDto,
|
|
||||||
UpdateAlbumDto,
|
UpdateAlbumDto,
|
||||||
UpdateAlbumUserDto,
|
UpdateAlbumUserDto,
|
||||||
UpdateSharingPermissionsDto,
|
|
||||||
} from 'src/dtos/album.dto';
|
} from 'src/dtos/album.dto';
|
||||||
import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { MapMarkerResponseDto } from 'src/dtos/map.dto';
|
import { MapMarkerResponseDto } from 'src/dtos/map.dto';
|
||||||
import { AlbumUserRole, Permission, SharingPermission } from 'src/enum';
|
import { AlbumUserRole, Permission } from 'src/enum';
|
||||||
import { AlbumAssetCount, AlbumInfoOptions } from 'src/repositories/album.repository';
|
import { AlbumAssetCount, AlbumInfoOptions } from 'src/repositories/album.repository';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { addAssets, removeAssets } from 'src/utils/asset.util';
|
import { addAssets, removeAssets } from 'src/utils/asset.util';
|
||||||
@@ -139,11 +137,6 @@ export class AlbumService extends BaseService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
for (const { userId } of albumUsers) {
|
for (const { userId } of albumUsers) {
|
||||||
await this.userRepository.mergeTrustedGroups({
|
|
||||||
userId: auth.user.id,
|
|
||||||
userIdToMerge: userId,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.eventRepository.emit('AlbumInvite', { id: album.id, userId, senderName: auth.user.name });
|
await this.eventRepository.emit('AlbumInvite', { id: album.id, userId, senderName: auth.user.name });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,17 +306,7 @@ export class AlbumService extends BaseService {
|
|||||||
throw new BadRequestException('Invalid user');
|
throw new BadRequestException('Invalid user');
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.userRepository.mergeTrustedGroups({
|
await this.albumUserRepository.create({ userId, albumId: id, role });
|
||||||
userId: auth.user.id,
|
|
||||||
userIdToMerge: userId,
|
|
||||||
});
|
|
||||||
await this.albumUserRepository.create({
|
|
||||||
userId,
|
|
||||||
albumId: id,
|
|
||||||
role,
|
|
||||||
permissions: [SharingPermission.AssetRead, SharingPermission.ExifRead],
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.eventRepository.emit('AlbumInvite', { id, userId, senderName: auth.user.name });
|
await this.eventRepository.emit('AlbumInvite', { id, userId, senderName: auth.user.name });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,19 +345,6 @@ export class AlbumService extends BaseService {
|
|||||||
await this.albumUserRepository.update({ albumId: id, userId }, { role: dto.role });
|
await this.albumUserRepository.update({ albumId: id, userId }, { role: dto.role });
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateSelf(auth: AuthDto, albumId: string, dto: UpdateSharingPermissionsDto): Promise<void> {
|
|
||||||
await this.requireAccess({ auth, permission: Permission.AlbumAssetCreate, ids: [albumId] });
|
|
||||||
await this.albumUserRepository.update(
|
|
||||||
{ albumId, userId: auth.user.id },
|
|
||||||
{ permissions: dto.permissions, inTimeline: dto.inTimeline },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSelf(auth: AuthDto, albumId: string): Promise<SharingPermissionsResponseDto> {
|
|
||||||
await this.requireAccess({ auth, permission: Permission.AlbumAssetCreate, ids: [albumId] });
|
|
||||||
return this.albumUserRepository.get({ userId: auth.user.id, albumId });
|
|
||||||
}
|
|
||||||
|
|
||||||
private async findOrFail(id: string, authUserId: string, options: AlbumInfoOptions) {
|
private async findOrFail(id: string, authUserId: string, options: AlbumInfoOptions) {
|
||||||
const album = await this.albumRepository.getById(id, options, authUserId);
|
const album = await this.albumRepository.getById(id, options, authUserId);
|
||||||
if (!album) {
|
if (!album) {
|
||||||
|
|||||||
@@ -32,11 +32,10 @@ import {
|
|||||||
JobStatus,
|
JobStatus,
|
||||||
Permission,
|
Permission,
|
||||||
QueueName,
|
QueueName,
|
||||||
SharingPermission,
|
|
||||||
} from 'src/enum';
|
} from 'src/enum';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { JobItem, JobOf } from 'src/types';
|
import { JobItem, JobOf } from 'src/types';
|
||||||
import { hasPermissions, requireElevatedPermission } from 'src/utils/access';
|
import { requireElevatedPermission } from 'src/utils/access';
|
||||||
import {
|
import {
|
||||||
getAssetFiles,
|
getAssetFiles,
|
||||||
getDimensions,
|
getDimensions,
|
||||||
@@ -63,18 +62,14 @@ export class AssetService extends BaseService {
|
|||||||
async get(auth: AuthDto, id: string): Promise<AssetResponseDto | SanitizedAssetResponseDto> {
|
async get(auth: AuthDto, id: string): Promise<AssetResponseDto | SanitizedAssetResponseDto> {
|
||||||
await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [id] });
|
||||||
|
|
||||||
const asset = await this.assetRepository.getById(
|
const asset = await this.assetRepository.getById(id, {
|
||||||
id,
|
exifInfo: true,
|
||||||
{
|
owner: true,
|
||||||
exifInfo: true,
|
faces: { person: true },
|
||||||
owner: true,
|
stack: { assets: true },
|
||||||
faces: { person: true },
|
edits: true,
|
||||||
stack: { assets: true },
|
tags: true,
|
||||||
edits: true,
|
});
|
||||||
tags: true,
|
|
||||||
},
|
|
||||||
auth.user.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
throw new BadRequestException('Asset not found');
|
throw new BadRequestException('Asset not found');
|
||||||
@@ -90,7 +85,7 @@ export class AssetService extends BaseService {
|
|||||||
delete data.owner;
|
delete data.owner;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPermissions(data, SharingPermission.PersonRead)) {
|
if (data.ownerId !== auth.user.id || auth.sharedLink) {
|
||||||
data.people = [];
|
data.people = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -85,11 +85,7 @@ export class NotificationService extends BaseService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.error(
|
this.logger.error(`Unable to run job handler (${job.name}): ${error}`, error?.stack, JSON.stringify(job.data));
|
||||||
`Unable to run job handler (${job.name}): ${error}`,
|
|
||||||
error?.stack,
|
|
||||||
'data' in job ? JSON.stringify(job.data) : {},
|
|
||||||
);
|
|
||||||
|
|
||||||
switch (job.name) {
|
switch (job.name) {
|
||||||
case JobName.DatabaseBackup: {
|
case JobName.DatabaseBackup: {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Partner } from 'src/database';
|
|||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { PartnerCreateDto, PartnerResponseDto, PartnerSearchDto, PartnerUpdateDto } from 'src/dtos/partner.dto';
|
import { PartnerCreateDto, PartnerResponseDto, PartnerSearchDto, PartnerUpdateDto } from 'src/dtos/partner.dto';
|
||||||
import { mapUser } from 'src/dtos/user.dto';
|
import { mapUser } from 'src/dtos/user.dto';
|
||||||
import { JobName, Permission, SharingPermission } from 'src/enum';
|
import { Permission } from 'src/enum';
|
||||||
import { PartnerDirection, PartnerIds } from 'src/repositories/partner.repository';
|
import { PartnerDirection, PartnerIds } from 'src/repositories/partner.repository';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
|
|
||||||
@@ -16,15 +16,7 @@ export class PartnerService extends BaseService {
|
|||||||
throw new BadRequestException(`Partner already exists`);
|
throw new BadRequestException(`Partner already exists`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { numUpdatedRows } = await this.userRepository.mergeTrustedGroups({
|
const partner = await this.partnerRepository.create(partnerId);
|
||||||
userId: auth.user.id,
|
|
||||||
userIdToMerge: sharedWithId,
|
|
||||||
});
|
|
||||||
const partner = await this.partnerRepository.create({ ...partnerId, permissions: [SharingPermission.All] });
|
|
||||||
if (numUpdatedRows > 0) {
|
|
||||||
await this.jobRepository.queue({ name: JobName.FacialRecognitionMerge, data: { id: sharedWithId } });
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.mapPartner(partner, PartnerDirection.SharedBy);
|
return this.mapPartner(partner, PartnerDirection.SharedBy);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,10 +28,6 @@ export class PartnerService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.partnerRepository.remove(partnerId);
|
await this.partnerRepository.remove(partnerId);
|
||||||
const { numUpdatedRows } = await this.userRepository.updateTrustedGroups(auth.user.id);
|
|
||||||
if (numUpdatedRows > 0) {
|
|
||||||
await this.jobRepository.queue({ name: JobName.FacialRecognitionQueueAll, data: { force: true } });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(auth: AuthDto, { direction }: PartnerSearchDto): Promise<PartnerResponseDto[]> {
|
async search(auth: AuthDto, { direction }: PartnerSearchDto): Promise<PartnerResponseDto[]> {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
FaceDto,
|
FaceDto,
|
||||||
mapFaces,
|
mapFaces,
|
||||||
mapPerson,
|
mapPerson,
|
||||||
MergeFaceClusterDto,
|
MergePersonDto,
|
||||||
PeopleResponseDto,
|
PeopleResponseDto,
|
||||||
PeopleUpdateDto,
|
PeopleUpdateDto,
|
||||||
PersonCreateDto,
|
PersonCreateDto,
|
||||||
@@ -127,11 +127,11 @@ export class PersonService extends BaseService {
|
|||||||
|
|
||||||
async getFacesById(auth: AuthDto, dto: FaceDto): Promise<AssetFaceResponseDto[]> {
|
async getFacesById(auth: AuthDto, dto: FaceDto): Promise<AssetFaceResponseDto[]> {
|
||||||
await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [dto.id] });
|
await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [dto.id] });
|
||||||
const faces = await this.personRepository.getFaces(dto.id, { userId: auth.user.id });
|
const faces = await this.personRepository.getFaces(dto.id);
|
||||||
const asset = await this.assetRepository.getForFaces(dto.id);
|
const asset = await this.assetRepository.getForFaces(dto.id);
|
||||||
const assetDimensions = getDimensions(asset);
|
const assetDimensions = getDimensions(asset);
|
||||||
|
|
||||||
return faces.map((face) => mapFaces(face, asset.edits, assetDimensions));
|
return faces.map((face) => mapFaces(face, auth, asset.edits, assetDimensions));
|
||||||
}
|
}
|
||||||
|
|
||||||
async createNewFeaturePhoto(changeFeaturePhoto: string[]) {
|
async createNewFeaturePhoto(changeFeaturePhoto: string[]) {
|
||||||
@@ -159,7 +159,7 @@ export class PersonService extends BaseService {
|
|||||||
|
|
||||||
async getStatistics(auth: AuthDto, id: string): Promise<PersonStatisticsResponseDto> {
|
async getStatistics(auth: AuthDto, id: string): Promise<PersonStatisticsResponseDto> {
|
||||||
await this.requireAccess({ auth, permission: Permission.PersonRead, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.PersonRead, ids: [id] });
|
||||||
return this.personRepository.getStatistics(auth.user.id, id);
|
return this.personRepository.getStatistics(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getThumbnail(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
|
async getThumbnail(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
|
||||||
@@ -438,7 +438,7 @@ export class PersonService extends BaseService {
|
|||||||
|
|
||||||
const lastRun = new Date().toISOString();
|
const lastRun = new Date().toISOString();
|
||||||
const facePagination = this.personRepository.getAllFaces(
|
const facePagination = this.personRepository.getAllFaces(
|
||||||
force ? undefined : { faceClusterId: null, sourceType: SourceType.MachineLearning },
|
force ? undefined : { personId: null, sourceType: SourceType.MachineLearning },
|
||||||
);
|
);
|
||||||
|
|
||||||
let jobs: { name: JobName.FacialRecognition; data: { id: string; deferred: false } }[] = [];
|
let jobs: { name: JobName.FacialRecognition; data: { id: string; deferred: false } }[] = [];
|
||||||
@@ -481,8 +481,8 @@ export class PersonService extends BaseService {
|
|||||||
return JobStatus.Failed;
|
return JobStatus.Failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (face.faceClusterId) {
|
if (face.personId) {
|
||||||
this.logger.debug(`Face ${id} already belongs to a face cluster`);
|
this.logger.debug(`Face ${id} already has a person assigned`);
|
||||||
return JobStatus.Skipped;
|
return JobStatus.Skipped;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,8 +511,8 @@ export class PersonService extends BaseService {
|
|||||||
return JobStatus.Skipped;
|
return JobStatus.Skipped;
|
||||||
}
|
}
|
||||||
|
|
||||||
let faceClusterId = matches.find((match) => match.faceClusterId)?.faceClusterId;
|
let personId = matches.find((match) => match.personId)?.personId;
|
||||||
if (!faceClusterId) {
|
if (!personId) {
|
||||||
const matchWithPerson = await this.searchRepository.searchFaces({
|
const matchWithPerson = await this.searchRepository.searchFaces({
|
||||||
userIds: [face.asset.ownerId],
|
userIds: [face.asset.ownerId],
|
||||||
embedding: face.faceSearch.embedding,
|
embedding: face.faceSearch.embedding,
|
||||||
@@ -523,108 +523,20 @@ export class PersonService extends BaseService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (matchWithPerson.length > 0) {
|
if (matchWithPerson.length > 0) {
|
||||||
faceClusterId = matchWithPerson[0].faceClusterId;
|
personId = matchWithPerson[0].personId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCore && !faceClusterId) {
|
if (isCore && !personId) {
|
||||||
this.logger.log(`Creating new person for face ${id}`);
|
this.logger.log(`Creating new person for face ${id}`);
|
||||||
const newPerson = await this.personRepository.create({ ownerId: face.asset.ownerId, faceAssetId: face.id });
|
const newPerson = await this.personRepository.create({ ownerId: face.asset.ownerId, faceAssetId: face.id });
|
||||||
await this.jobRepository.queue({ name: JobName.PersonGenerateThumbnail, data: { id: newPerson.id } });
|
await this.jobRepository.queue({ name: JobName.PersonGenerateThumbnail, data: { id: newPerson.id } });
|
||||||
faceClusterId = newPerson.faceClusterId;
|
personId = newPerson.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (faceClusterId) {
|
if (personId) {
|
||||||
this.logger.debug(`Assigning face ${id} to face cluster ${faceClusterId}`);
|
this.logger.debug(`Assigning face ${id} to person ${personId}`);
|
||||||
await this.personRepository.reassignFaces({ faceIds: [id], newFaceClusterId: faceClusterId });
|
await this.personRepository.reassignFaces({ faceIds: [id], newPersonId: personId });
|
||||||
}
|
|
||||||
|
|
||||||
return JobStatus.Success;
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnJob({ name: JobName.FacialRecognitionMerge, queue: QueueName.FacialRecognition })
|
|
||||||
async mergeClusters({ id: userId }: JobOf<JobName.FacialRecognitionMerge>): Promise<JobStatus> {
|
|
||||||
const { machineLearning } = await this.getConfig({ withCache: true });
|
|
||||||
if (!isFacialRecognitionEnabled(machineLearning)) {
|
|
||||||
return JobStatus.Skipped;
|
|
||||||
}
|
|
||||||
|
|
||||||
const faces = this.personRepository.getAllFaces({ sourceType: SourceType.MachineLearning });
|
|
||||||
for await (const { id } of faces) {
|
|
||||||
const face = await this.personRepository.getFaceForFacialRecognitionJob(id);
|
|
||||||
if (!face?.faceSearch || !face.asset) {
|
|
||||||
this.logger.warn(`Face ${id} does not have an embedding`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (face.asset.ownerId === userId) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let faceClusterId: string | null = null;
|
|
||||||
const matchWithPerson = await this.searchRepository.searchFaces({
|
|
||||||
userIds: [face.asset.ownerId],
|
|
||||||
embedding: face.faceSearch.embedding,
|
|
||||||
maxDistance: machineLearning.facialRecognition.maxDistance,
|
|
||||||
numResults: 100,
|
|
||||||
hasPerson: true,
|
|
||||||
minBirthDate: new Date(face.asset.fileCreatedAt),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (matchWithPerson.length > 0) {
|
|
||||||
// favor a person that's not owned by us to merge people with a newly shared with user
|
|
||||||
// probably do smarter stuff here like pick the person with a name, if both have a name set aliases or whatever
|
|
||||||
const match = matchWithPerson.find((match) => match.ownerId !== userId) ?? matchWithPerson[0];
|
|
||||||
if (match.faceClusterId && face.asset.ownerId !== match.ownerId) {
|
|
||||||
// TODO should probably be a DB constraint?
|
|
||||||
const people = await this.personRepository.getByFaceClusterId(match.faceClusterId);
|
|
||||||
if (!people.some((person) => person.ownerId === face.asset?.ownerId)) {
|
|
||||||
const person = await this.personRepository.create({
|
|
||||||
ownerId: face.asset.ownerId,
|
|
||||||
faceClusterId: match.faceClusterId,
|
|
||||||
});
|
|
||||||
await this.createNewFeaturePhoto([person.id]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
faceClusterId = match.faceClusterId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!faceClusterId) {
|
|
||||||
const matches = await this.searchRepository.searchFaces({
|
|
||||||
userIds: [userId],
|
|
||||||
embedding: face.faceSearch.embedding,
|
|
||||||
maxDistance: machineLearning.facialRecognition.maxDistance,
|
|
||||||
numResults: machineLearning.facialRecognition.minFaces,
|
|
||||||
minBirthDate: new Date(face.asset.fileCreatedAt),
|
|
||||||
});
|
|
||||||
|
|
||||||
const match = matches.find((match) => match.faceClusterId);
|
|
||||||
if (
|
|
||||||
match &&
|
|
||||||
match.faceClusterId &&
|
|
||||||
face.asset.ownerId !== match.ownerId &&
|
|
||||||
matches.length >= machineLearning.facialRecognition.minFaces
|
|
||||||
) {
|
|
||||||
// TODO should probably be a DB constraint?
|
|
||||||
const people = await this.personRepository.getByFaceClusterId(match.faceClusterId);
|
|
||||||
|
|
||||||
if (!people.some((person) => person.ownerId === face.asset?.ownerId)) {
|
|
||||||
const person = await this.personRepository.create({
|
|
||||||
ownerId: face.asset.ownerId,
|
|
||||||
faceClusterId: match.faceClusterId,
|
|
||||||
});
|
|
||||||
await this.createNewFeaturePhoto([person.id]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
faceClusterId = match?.faceClusterId ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (faceClusterId) {
|
|
||||||
this.logger.log(`Assigning face ${id} to face cluster ${faceClusterId}`);
|
|
||||||
await this.personRepository.reassignFaces({ faceIds: [id], newFaceClusterId: faceClusterId });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return JobStatus.Success;
|
return JobStatus.Success;
|
||||||
@@ -642,7 +554,7 @@ export class PersonService extends BaseService {
|
|||||||
return JobStatus.Success;
|
return JobStatus.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
async mergePerson(auth: AuthDto, id: string, dto: MergeFaceClusterDto): Promise<BulkIdResponseDto[]> {
|
async mergePerson(auth: AuthDto, id: string, dto: MergePersonDto): Promise<BulkIdResponseDto[]> {
|
||||||
const mergeIds = dto.ids;
|
const mergeIds = dto.ids;
|
||||||
if (mergeIds.includes(id)) {
|
if (mergeIds.includes(id)) {
|
||||||
throw new BadRequestException('Cannot merge a person into themselves');
|
throw new BadRequestException('Cannot merge a person into themselves');
|
||||||
@@ -688,7 +600,7 @@ export class PersonService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mergeName = mergePerson.name || mergePerson.id;
|
const mergeName = mergePerson.name || mergePerson.id;
|
||||||
const mergeData: UpdateFacesData = { oldFaceClusterId: mergeId, newFaceClusterId: id };
|
const mergeData: UpdateFacesData = { oldPersonId: mergeId, newPersonId: id };
|
||||||
this.logger.log(`Merging ${mergeName} into ${primaryName}`);
|
this.logger.log(`Merging ${mergeName} into ${primaryName}`);
|
||||||
|
|
||||||
await this.personRepository.reassignFaces(mergeData);
|
await this.personRepository.reassignFaces(mergeData);
|
||||||
@@ -701,7 +613,6 @@ export class PersonService extends BaseService {
|
|||||||
results.push({ id: mergeId, success: false, error: BulkIdErrorReason.UNKNOWN });
|
results.push({ id: mergeId, success: false, error: BulkIdErrorReason.UNKNOWN });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -771,12 +682,8 @@ export class PersonService extends BaseService {
|
|||||||
dto.imageHeight = originalDimensions.height;
|
dto.imageHeight = originalDimensions.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!person?.faceClusterId) {
|
|
||||||
throw new Error('Person must already have some recognized faces and belong to a face cluster');
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.personRepository.createAssetFace({
|
await this.personRepository.createAssetFace({
|
||||||
faceClusterId: person.faceClusterId,
|
personId: dto.personId,
|
||||||
assetId: dto.assetId,
|
assetId: dto.assetId,
|
||||||
imageHeight: dto.imageHeight,
|
imageHeight: dto.imageHeight,
|
||||||
imageWidth: dto.imageWidth,
|
imageWidth: dto.imageWidth,
|
||||||
|
|||||||
@@ -208,7 +208,6 @@ export class SearchService extends BaseService {
|
|||||||
repository: this.partnerRepository,
|
repository: this.partnerRepository,
|
||||||
timelineEnabled: true,
|
timelineEnabled: true,
|
||||||
});
|
});
|
||||||
console.log(auth.user.id, partnerIds);
|
|
||||||
return [auth.user.id, ...partnerIds];
|
return [auth.user.id, ...partnerIds];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-4
@@ -204,9 +204,7 @@ export type ConcurrentQueueName = Exclude<
|
|||||||
| QueueName.BackupDatabase
|
| QueueName.BackupDatabase
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type Jobs = {
|
export type Jobs = { [K in JobItem['name']]: (JobItem & { name: K })['data'] };
|
||||||
[K in JobItem['name']]: 'data' extends keyof (JobItem & { name: K }) ? (JobItem & { name: K })['data'] : never;
|
|
||||||
};
|
|
||||||
export type JobOf<T extends JobName> = Jobs[T];
|
export type JobOf<T extends JobName> = Jobs[T];
|
||||||
|
|
||||||
export interface IBaseJob {
|
export interface IBaseJob {
|
||||||
@@ -353,7 +351,6 @@ export type JobItem =
|
|||||||
| { name: JobName.AssetDetectFaces; data: IEntityJob }
|
| { name: JobName.AssetDetectFaces; data: IEntityJob }
|
||||||
| { name: JobName.FacialRecognitionQueueAll; data: INightlyJob }
|
| { name: JobName.FacialRecognitionQueueAll; data: INightlyJob }
|
||||||
| { name: JobName.FacialRecognition; data: IDeferrableJob }
|
| { name: JobName.FacialRecognition; data: IDeferrableJob }
|
||||||
| { name: JobName.FacialRecognitionMerge; data: IEntityJob }
|
|
||||||
| { name: JobName.PersonGenerateThumbnail; data: IEntityJob }
|
| { name: JobName.PersonGenerateThumbnail; data: IEntityJob }
|
||||||
|
|
||||||
// Smart Search
|
// Smart Search
|
||||||
|
|||||||
+22
-82
@@ -1,7 +1,7 @@
|
|||||||
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
|
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
|
||||||
import { AuthSharedLink } from 'src/database';
|
import { AuthSharedLink } from 'src/database';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { AlbumUserRole, Permission, SharingPermission } from 'src/enum';
|
import { AlbumUserRole, Permission } from 'src/enum';
|
||||||
import { AccessRepository } from 'src/repositories/access.repository';
|
import { AccessRepository } from 'src/repositories/access.repository';
|
||||||
import { setDifference, setIsEqual, setIsSuperset, setUnion } from 'src/utils/set';
|
import { setDifference, setIsEqual, setIsSuperset, setUnion } from 'src/utils/set';
|
||||||
|
|
||||||
@@ -115,41 +115,37 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
|
|||||||
|
|
||||||
case Permission.AssetRead: {
|
case Permission.AssetRead: {
|
||||||
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
||||||
const isShared = await access.asset.checkSharedAccess(auth.user.id, ids, [SharingPermission.AssetRead]);
|
const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
|
||||||
return setUnion(isOwner, isShared);
|
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum));
|
||||||
|
return setUnion(isOwner, isAlbum, isPartner);
|
||||||
}
|
}
|
||||||
|
|
||||||
case Permission.AssetShare: {
|
case Permission.AssetShare: {
|
||||||
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, false);
|
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, false);
|
||||||
const isShared = await access.asset.checkSharedAccess(auth.user.id, ids, [SharingPermission.AssetShare]);
|
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner));
|
||||||
return setUnion(isOwner, isShared);
|
return setUnion(isOwner, isPartner);
|
||||||
}
|
}
|
||||||
|
|
||||||
case Permission.AssetView: {
|
case Permission.AssetView: {
|
||||||
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
||||||
const isShared = await access.asset.checkSharedAccess(auth.user.id, ids, [SharingPermission.AssetRead]);
|
const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
|
||||||
return setUnion(isOwner, isShared);
|
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum));
|
||||||
|
return setUnion(isOwner, isAlbum, isPartner);
|
||||||
}
|
}
|
||||||
|
|
||||||
case Permission.AssetDownload: {
|
case Permission.AssetDownload: {
|
||||||
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
||||||
const isShared = await access.asset.checkSharedAccess(auth.user.id, ids, [
|
const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
|
||||||
SharingPermission.AssetRead,
|
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum));
|
||||||
SharingPermission.ExifRead,
|
return setUnion(isOwner, isAlbum, isPartner);
|
||||||
]);
|
|
||||||
return setUnion(isOwner, isShared);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case Permission.AssetUpdate: {
|
case Permission.AssetUpdate: {
|
||||||
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
return await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
||||||
const isShared = await access.asset.checkSharedAccess(auth.user.id, ids, [SharingPermission.AssetUpdate]);
|
|
||||||
return setUnion(isOwner, isShared);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case Permission.AssetDelete: {
|
case Permission.AssetDelete: {
|
||||||
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
return await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
||||||
const isShared = await access.asset.checkSharedAccess(auth.user.id, ids, [SharingPermission.AssetDelete]);
|
|
||||||
return setUnion(isOwner, isShared);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case Permission.AssetCopy: {
|
case Permission.AssetCopy: {
|
||||||
@@ -157,21 +153,15 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
case Permission.AssetEditGet: {
|
case Permission.AssetEditGet: {
|
||||||
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
return await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
||||||
const isShared = await access.asset.checkSharedAccess(auth.user.id, ids, [SharingPermission.AssetEdit]);
|
|
||||||
return setUnion(isOwner, isShared);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case Permission.AssetEditCreate: {
|
case Permission.AssetEditCreate: {
|
||||||
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
return await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
||||||
const isShared = await access.asset.checkSharedAccess(auth.user.id, ids, [SharingPermission.AssetEdit]);
|
|
||||||
return setUnion(isOwner, isShared);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case Permission.AssetEditDelete: {
|
case Permission.AssetEditDelete: {
|
||||||
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
return await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
||||||
const isShared = await access.asset.checkSharedAccess(auth.user.id, ids, [SharingPermission.AssetEdit]);
|
|
||||||
return setUnion(isOwner, isShared);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case Permission.AlbumRead: {
|
case Permission.AlbumRead: {
|
||||||
@@ -256,11 +246,7 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
case Permission.FaceDelete: {
|
case Permission.FaceDelete: {
|
||||||
const isOwner = await access.person.checkFaceOwnerAccess(auth.user.id, ids);
|
return access.person.checkFaceOwnerAccess(auth.user.id, ids);
|
||||||
const isShared = await access.person.checkSharedFaceAccess(auth.user.id, setDifference(ids, isOwner), [
|
|
||||||
SharingPermission.AssetUpdate,
|
|
||||||
]);
|
|
||||||
return setUnion(isOwner, isShared);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case Permission.NotificationRead:
|
case Permission.NotificationRead:
|
||||||
@@ -302,40 +288,11 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
|
|||||||
return access.person.checkFaceOwnerAccess(auth.user.id, ids);
|
return access.person.checkFaceOwnerAccess(auth.user.id, ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
case Permission.PersonRead: {
|
case Permission.PersonRead:
|
||||||
const isOwner = await access.person.checkOwnerAccess(auth.user.id, ids);
|
case Permission.PersonUpdate:
|
||||||
const isShared = await access.person.checkSharedAccess(auth.user.id, setDifference(ids, isOwner), [
|
case Permission.PersonDelete:
|
||||||
SharingPermission.PersonRead,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return setUnion(isOwner, isShared);
|
|
||||||
}
|
|
||||||
|
|
||||||
case Permission.PersonMerge: {
|
case Permission.PersonMerge: {
|
||||||
const isOwner = await access.person.checkOwnerAccess(auth.user.id, ids);
|
return await access.person.checkOwnerAccess(auth.user.id, ids);
|
||||||
const isShared = await access.person.checkSharedAccess(auth.user.id, setDifference(ids, isOwner), [
|
|
||||||
SharingPermission.PersonMerge,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return setUnion(isOwner, isShared);
|
|
||||||
}
|
|
||||||
|
|
||||||
case Permission.PersonUpdate: {
|
|
||||||
const isOwner = await access.person.checkOwnerAccess(auth.user.id, ids);
|
|
||||||
const isShared = await access.person.checkSharedAccess(auth.user.id, setDifference(ids, isOwner), [
|
|
||||||
SharingPermission.PersonUpdate,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return setUnion(isOwner, isShared);
|
|
||||||
}
|
|
||||||
|
|
||||||
case Permission.PersonDelete: {
|
|
||||||
const isOwner = await access.person.checkOwnerAccess(auth.user.id, ids);
|
|
||||||
const isShared = await access.person.checkSharedAccess(auth.user.id, setDifference(ids, isOwner), [
|
|
||||||
SharingPermission.PersonDelete,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return setUnion(isOwner, isShared);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case Permission.PersonReassign: {
|
case Permission.PersonReassign: {
|
||||||
@@ -382,20 +339,3 @@ export const requireElevatedPermission = (auth: AuthDto) => {
|
|||||||
throw new UnauthorizedException('Elevated permission is required');
|
throw new UnauthorizedException('Elevated permission is required');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hasPermissions = (
|
|
||||||
assetLike: { permissions: SharingPermission[] },
|
|
||||||
...permissions: SharingPermission[]
|
|
||||||
) => {
|
|
||||||
if (assetLike.permissions.includes(SharingPermission.All)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const permission of permissions) {
|
|
||||||
if (!assetLike.permissions.includes(permission)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { AssetFile } from 'src/database';
|
|||||||
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
|
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
|
||||||
import { UploadFieldName } from 'src/dtos/asset-media.dto';
|
import { UploadFieldName } from 'src/dtos/asset-media.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { AssetFileType, AssetType, AssetVisibility, Permission, SharingPermission } from 'src/enum';
|
import { AssetFileType, AssetType, AssetVisibility, Permission } from 'src/enum';
|
||||||
import { AuthRequest } from 'src/middleware/auth.guard';
|
import { AuthRequest } from 'src/middleware/auth.guard';
|
||||||
import { AccessRepository } from 'src/repositories/access.repository';
|
import { AccessRepository } from 'src/repositories/access.repository';
|
||||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||||
@@ -134,11 +134,6 @@ export const getMyPartnerIds = async ({ userId, repository, timelineEnabled }: P
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const permissions = [SharingPermission.All, SharingPermission.AssetRead];
|
|
||||||
if (!permissions.some((permission) => partner.permissions.includes(permission))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
partnerIds.add(partner.sharedById);
|
partnerIds.add(partner.sharedById);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,17 +15,9 @@ import {
|
|||||||
import { PostgresJSDialect } from 'kysely-postgres-js';
|
import { PostgresJSDialect } from 'kysely-postgres-js';
|
||||||
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||||
import { Notice, PostgresError } from 'postgres';
|
import { Notice, PostgresError } from 'postgres';
|
||||||
import { columns, lockableProperties, LockableProperty } from 'src/database';
|
import { columns, lockableProperties, LockableProperty, Person } from 'src/database';
|
||||||
import { AssetEditActionItem } from 'src/dtos/editing.dto';
|
import { AssetEditActionItem } from 'src/dtos/editing.dto';
|
||||||
import {
|
import { AssetFileType, AssetOrderBy, AssetVisibility, DatabaseExtension, ExifOrientation } from 'src/enum';
|
||||||
AssetFileType,
|
|
||||||
AssetOrderBy,
|
|
||||||
AssetVisibility,
|
|
||||||
DatabaseExtension,
|
|
||||||
ExifOrientation,
|
|
||||||
SharingPermission,
|
|
||||||
} from 'src/enum';
|
|
||||||
import { hasAssetPermissions } from 'src/repositories/asset.repository';
|
|
||||||
import { AssetSearchBuilderOptions } from 'src/repositories/search.repository';
|
import { AssetSearchBuilderOptions } from 'src/repositories/search.repository';
|
||||||
import { DB } from 'src/schema';
|
import { DB } from 'src/schema';
|
||||||
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||||
@@ -220,22 +212,19 @@ export function withFilePath(eb: ExpressionBuilder<DB, 'asset'>, type: AssetFile
|
|||||||
|
|
||||||
export function withFacesAndPeople(
|
export function withFacesAndPeople(
|
||||||
eb: ExpressionBuilder<DB, 'asset'>,
|
eb: ExpressionBuilder<DB, 'asset'>,
|
||||||
{ withHidden, withDeletedFace, userId: _ }: { withHidden?: boolean; withDeletedFace?: boolean; userId?: string } = {},
|
withHidden?: boolean,
|
||||||
|
withDeletedFace?: boolean,
|
||||||
) {
|
) {
|
||||||
return jsonArrayFrom(
|
return jsonArrayFrom(
|
||||||
eb
|
eb
|
||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.select((eb) =>
|
.leftJoinLateral(
|
||||||
jsonObjectFrom(
|
(eb) =>
|
||||||
eb
|
eb.selectFrom('person').selectAll('person').whereRef('asset_face.personId', '=', 'person.id').as('person'),
|
||||||
.selectFrom('face_cluster')
|
(join) => join.onTrue(),
|
||||||
.whereRef('face_cluster.id', '=', 'asset_face.faceClusterId')
|
|
||||||
.innerJoin('person', 'person.faceClusterId', 'face_cluster.id')
|
|
||||||
.selectAll('person')
|
|
||||||
.limit(1),
|
|
||||||
).as('person'),
|
|
||||||
)
|
)
|
||||||
.selectAll('asset_face')
|
.selectAll('asset_face')
|
||||||
|
.select((eb) => eb.table('person').$castTo<ShallowDehydrateObject<Person>>().as('person'))
|
||||||
.whereRef('asset_face.assetId', '=', 'asset.id')
|
.whereRef('asset_face.assetId', '=', 'asset.id')
|
||||||
.$if(!withDeletedFace, (qb) => qb.where('asset_face.deletedAt', 'is', null))
|
.$if(!withDeletedFace, (qb) => qb.where('asset_face.deletedAt', 'is', null))
|
||||||
.$if(!withHidden, (qb) => qb.where('asset_face.isVisible', 'is', true)),
|
.$if(!withHidden, (qb) => qb.where('asset_face.isVisible', 'is', true)),
|
||||||
@@ -248,12 +237,11 @@ export function hasPeople<O>(qb: SelectQueryBuilder<DB, 'asset', O>, personIds:
|
|||||||
eb
|
eb
|
||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.select('assetId')
|
.select('assetId')
|
||||||
.innerJoin('person', 'person.faceClusterId', 'asset_face.faceClusterId')
|
.where('personId', '=', anyUuid(personIds!))
|
||||||
.where('person.id', '=', anyUuid(personIds!))
|
|
||||||
.where('deletedAt', 'is', null)
|
.where('deletedAt', 'is', null)
|
||||||
.where('isVisible', 'is', true)
|
.where('isVisible', 'is', true)
|
||||||
.groupBy('assetId')
|
.groupBy('assetId')
|
||||||
.having((eb) => eb.fn.count('person.id').distinct(), '=', personIds.length)
|
.having((eb) => eb.fn.count('personId').distinct(), '=', personIds.length)
|
||||||
.as('has_people'),
|
.as('has_people'),
|
||||||
(join) => join.onRef('has_people.assetId', '=', 'asset.id'),
|
(join) => join.onRef('has_people.assetId', '=', 'asset.id'),
|
||||||
);
|
);
|
||||||
@@ -314,30 +302,6 @@ export function truncatedDate<O>(order: AssetOrderBy = AssetOrderBy.TakenAt) {
|
|||||||
return sql<O>`date_trunc(${sql.lit('MONTH')}, ${sql.ref(order === AssetOrderBy.CreatedAt ? 'asset.createdAt' : 'localDateTime')} AT TIME ZONE 'UTC') AT TIME ZONE 'UTC'`;
|
return sql<O>`date_trunc(${sql.lit('MONTH')}, ${sql.ref(order === AssetOrderBy.CreatedAt ? 'asset.createdAt' : 'localDateTime')} AT TIME ZONE 'UTC') AT TIME ZONE 'UTC'`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withPermissions(userId: string) {
|
|
||||||
return (eb: ExpressionBuilder<DB, 'asset'>) =>
|
|
||||||
jsonArrayFrom(
|
|
||||||
eb
|
|
||||||
.selectFrom('album_user')
|
|
||||||
.select((eb) => eb.fn<SharingPermission>('unnest', ['album_user.permissions']).as('permission'))
|
|
||||||
.distinct()
|
|
||||||
.innerJoin('album_asset', 'album_user.albumId', 'album_asset.albumId')
|
|
||||||
.whereRef('album_asset.assetId', '=', 'asset.id')
|
|
||||||
.whereRef('album_user.userId', '=', 'asset.ownerId')
|
|
||||||
.where('album_user.albumId', 'in', (eb) =>
|
|
||||||
eb.selectFrom('album_user').select('album_user.albumId').where('album_user.userId', '=', userId),
|
|
||||||
)
|
|
||||||
.union(
|
|
||||||
eb
|
|
||||||
.selectFrom('partner')
|
|
||||||
.select((eb) => eb.fn<SharingPermission>('unnest', ['partner.permissions']).as('permission'))
|
|
||||||
.distinct()
|
|
||||||
.whereRef('partner.sharedById', '=', 'asset.ownerId')
|
|
||||||
.where('partner.sharedWithId', '=', userId),
|
|
||||||
),
|
|
||||||
).as('permissions');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function withTagId<O>(qb: SelectQueryBuilder<DB, 'asset', O>, tagId: string) {
|
export function withTagId<O>(qb: SelectQueryBuilder<DB, 'asset', O>, tagId: string) {
|
||||||
return qb.where((eb) =>
|
return qb.where((eb) =>
|
||||||
eb.exists(
|
eb.exists(
|
||||||
@@ -464,7 +428,7 @@ export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuild
|
|||||||
.$if(!!options.checksum, (qb) => qb.where('asset.checksum', '=', options.checksum!))
|
.$if(!!options.checksum, (qb) => qb.where('asset.checksum', '=', options.checksum!))
|
||||||
.$if(!!options.id, (qb) => qb.where('asset.id', '=', asUuid(options.id!)))
|
.$if(!!options.id, (qb) => qb.where('asset.id', '=', asUuid(options.id!)))
|
||||||
.$if(!!options.libraryId, (qb) => qb.where('asset.libraryId', '=', asUuid(options.libraryId!)))
|
.$if(!!options.libraryId, (qb) => qb.where('asset.libraryId', '=', asUuid(options.libraryId!)))
|
||||||
.$if(!!options.userIds, (qb) => qb.where(hasAssetPermissions(options.userIds![0], [SharingPermission.AssetRead])))
|
.$if(!!options.userIds, (qb) => qb.where('asset.ownerId', '=', anyUuid(options.userIds!)))
|
||||||
.$if(!!options.encodedVideoPath, (qb) =>
|
.$if(!!options.encodedVideoPath, (qb) =>
|
||||||
qb
|
qb
|
||||||
.innerJoin('asset_file', (join) =>
|
.innerJoin('asset_file', (join) =>
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ const createAsset = (
|
|||||||
fileSizeInByte !== null || Object.keys(exifFields).length > 0
|
fileSizeInByte !== null || Object.keys(exifFields).length > 0
|
||||||
? ExifResponseSchema.parse({ fileSizeInByte, ...exifFields })
|
? ExifResponseSchema.parse({ fileSizeInByte, ...exifFields })
|
||||||
: undefined,
|
: undefined,
|
||||||
permissions: [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('duplicate utils', () => {
|
describe('duplicate utils', () => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AssetFace } from 'src/database';
|
||||||
import { AssetOcrResponseDto } from 'src/dtos/ocr.dto';
|
import { AssetOcrResponseDto } from 'src/dtos/ocr.dto';
|
||||||
import { ImageDimensions } from 'src/types';
|
import { ImageDimensions } from 'src/types';
|
||||||
|
|
||||||
@@ -30,21 +31,11 @@ const scale = (box: BoundingBox, target: ImageDimensions, source?: ImageDimensio
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const checkFaceVisibility = <
|
export const checkFaceVisibility = (
|
||||||
T extends {
|
faces: AssetFace[],
|
||||||
isVisible: boolean;
|
|
||||||
boundingBoxX1: number;
|
|
||||||
boundingBoxX2: number;
|
|
||||||
boundingBoxY1: number;
|
|
||||||
boundingBoxY2: number;
|
|
||||||
imageHeight: number;
|
|
||||||
imageWidth: number;
|
|
||||||
},
|
|
||||||
>(
|
|
||||||
faces: T[],
|
|
||||||
originalAssetDimensions: ImageDimensions,
|
originalAssetDimensions: ImageDimensions,
|
||||||
crop?: BoundingBox,
|
crop?: BoundingBox,
|
||||||
): { visible: T[]; hidden: T[] } => {
|
): { visible: AssetFace[]; hidden: AssetFace[] } => {
|
||||||
if (!crop) {
|
if (!crop) {
|
||||||
return {
|
return {
|
||||||
visible: faces.filter((face) => !face.isVisible),
|
visible: faces.filter((face) => !face.isVisible),
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ export class AlbumUserFactory {
|
|||||||
createdAt: newDate(),
|
createdAt: newDate(),
|
||||||
updateId: newUuidV7(),
|
updateId: newUuidV7(),
|
||||||
updatedAt: newDate(),
|
updatedAt: newDate(),
|
||||||
permissions: [],
|
|
||||||
inTimeline: false,
|
|
||||||
...dto,
|
...dto,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export class PartnerFactory {
|
|||||||
sharedWithId,
|
sharedWithId,
|
||||||
updatedAt: newDate(),
|
updatedAt: newDate(),
|
||||||
updateId: newUuidV7(),
|
updateId: newUuidV7(),
|
||||||
permissions: [],
|
|
||||||
...dto,
|
...dto,
|
||||||
})
|
})
|
||||||
.sharedBy({ id: sharedById })
|
.sharedBy({ id: sharedById })
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ export class UserFactory {
|
|||||||
status: UserStatus.Active,
|
status: UserStatus.Active,
|
||||||
profileChangedAt: newDate(),
|
profileChangedAt: newDate(),
|
||||||
updateId: newUuidV7(),
|
updateId: newUuidV7(),
|
||||||
trustedGroupId: newUuid(),
|
|
||||||
...dto,
|
...dto,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ export const newAccessRepositoryMock = (): IAccessRepositoryMock => {
|
|||||||
checkAlbumAccess: vitest.fn().mockResolvedValue(new Set()),
|
checkAlbumAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||||
checkPartnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
checkPartnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||||
checkSharedLinkAccess: vitest.fn().mockResolvedValue(new Set()),
|
checkSharedLinkAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||||
checkSharedAccess: vitest.fn().mockResolvedValue(new Set()),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
album: {
|
album: {
|
||||||
@@ -49,8 +48,6 @@ export const newAccessRepositoryMock = (): IAccessRepositoryMock => {
|
|||||||
person: {
|
person: {
|
||||||
checkFaceOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
checkFaceOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||||
checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||||
checkSharedAccess: vitest.fn().mockResolvedValue(new Set()),
|
|
||||||
checkSharedFaceAccess: vitest.fn().mockResolvedValue(new Set()),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
partner: {
|
partner: {
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@
|
|||||||
"@formatjs/icu-messageformat-parser": "^3.0.0",
|
"@formatjs/icu-messageformat-parser": "^3.0.0",
|
||||||
"@immich/justified-layout-wasm": "^0.4.3",
|
"@immich/justified-layout-wasm": "^0.4.3",
|
||||||
"@immich/sdk": "workspace:*",
|
"@immich/sdk": "workspace:*",
|
||||||
"@immich/ui": "^0.77.0",
|
"@immich/ui": "^0.79.0",
|
||||||
"@mapbox/mapbox-gl-rtl-text": "0.4.0",
|
"@mapbox/mapbox-gl-rtl-text": "0.4.0",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@noble/hashes": "^2.2.0",
|
"@noble/hashes": "^2.2.0",
|
||||||
|
|||||||
@@ -103,7 +103,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</AssetSelectControlBar>
|
</AssetSelectControlBar>
|
||||||
{:else}
|
{:else}
|
||||||
<ControlAppBar showBackButton={false}>
|
<ControlAppBar>
|
||||||
{#snippet leading()}
|
{#snippet leading()}
|
||||||
<a data-sveltekit-preload-data="hover" class="ms-4" href="/">
|
<a data-sveltekit-preload-data="hover" class="ms-4" href="/">
|
||||||
<Logo variant={mediaQueryManager.maxMd ? 'icon' : 'inline'} class="min-w-10" />
|
<Logo variant={mediaQueryManager.maxMd ? 'icon' : 'inline'} class="min-w-10" />
|
||||||
|
|||||||
@@ -26,13 +26,12 @@
|
|||||||
import { Route } from '$lib/route';
|
import { Route } from '$lib/route';
|
||||||
import { getGlobalActions } from '$lib/services/app.service';
|
import { getGlobalActions } from '$lib/services/app.service';
|
||||||
import { getAssetActions } from '$lib/services/asset.service';
|
import { getAssetActions } from '$lib/services/asset.service';
|
||||||
import { getSharedLink, hasPermissions, withoutIcons } from '$lib/utils';
|
import { getSharedLink, withoutIcons } from '$lib/utils';
|
||||||
import type { OnUndoDelete } from '$lib/utils/actions';
|
import type { OnUndoDelete } from '$lib/utils/actions';
|
||||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import {
|
import {
|
||||||
AssetTypeEnum,
|
AssetTypeEnum,
|
||||||
AssetVisibility,
|
AssetVisibility,
|
||||||
SharingPermission,
|
|
||||||
type AlbumResponseDto,
|
type AlbumResponseDto,
|
||||||
type AssetResponseDto,
|
type AssetResponseDto,
|
||||||
type PersonResponseDto,
|
type PersonResponseDto,
|
||||||
@@ -142,7 +141,7 @@
|
|||||||
|
|
||||||
<ActionButton action={Actions.Edit} />
|
<ActionButton action={Actions.Edit} />
|
||||||
|
|
||||||
{#if hasPermissions(asset, SharingPermission.AssetDelete)}
|
{#if isOwner}
|
||||||
<DeleteAction {asset} {onAction} {preAction} {onUndoDelete} />
|
<DeleteAction {asset} {onAction} {preAction} {onUndoDelete} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -160,7 +159,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<ActionMenuItem action={Actions.AddToAlbum} />
|
<ActionMenuItem action={Actions.AddToAlbum} />
|
||||||
{#if album && (hasPermissions(asset, SharingPermission.AssetShare) || isAlbumOwner)}
|
{#if album && (isOwner || isAlbumOwner)}
|
||||||
<RemoveFromAlbumAction {album} onRemove={onRemoveFromAlbum} assetIds={[asset.id]} menuItem />
|
<RemoveFromAlbumAction {album} onRemove={onRemoveFromAlbum} assetIds={[asset.id]} menuItem />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -188,7 +187,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !isLocked}
|
{#if !isLocked}
|
||||||
{#if hasPermissions(asset, SharingPermission.AssetUpdate)}
|
{#if isOwner}
|
||||||
<ArchiveAction {asset} {onAction} {preAction} />
|
<ArchiveAction {asset} {onAction} {preAction} />
|
||||||
{#if !asset.isArchived && !asset.isTrashed}
|
{#if !asset.isArchived && !asset.isTrashed}
|
||||||
<MenuOption
|
<MenuOption
|
||||||
@@ -218,7 +217,7 @@
|
|||||||
text={playOriginalVideo ? $t('play_transcoded_video') : $t('play_original_video')}
|
text={playOriginalVideo ? $t('play_transcoded_video') : $t('play_original_video')}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if hasPermissions(asset, SharingPermission.AssetUpdate)}
|
{#if isOwner}
|
||||||
<hr />
|
<hr />
|
||||||
<ActionMenuItem action={Actions.RefreshFacesJob} />
|
<ActionMenuItem action={Actions.RefreshFacesJob} />
|
||||||
<ActionMenuItem action={Actions.RefreshMetadataJob} />
|
<ActionMenuItem action={Actions.RefreshMetadataJob} />
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import DetailPanelDate from '$lib/components/asset-viewer/DetailPanelDate.svelte';
|
import DetailPanelDate from '$lib/components/asset-viewer/DetailPanelDate.svelte';
|
||||||
import DetailPanelDescription from '$lib/components/asset-viewer/DetailPanelDescription.svelte';
|
import DetailPanelDescription from '$lib/components/asset-viewer/DetailPanelDescription.svelte';
|
||||||
import DetailPanelLocation from '$lib/components/asset-viewer/DetailPanelLocation.svelte';
|
import DetailPanelLocation from '$lib/components/asset-viewer/DetailPanelLocation.svelte';
|
||||||
import DetailPanelPeople from '$lib/components/asset-viewer/DetailPanelPeople.svelte';
|
|
||||||
import DetailPanelRating from '$lib/components/asset-viewer/DetailPanelStarRating.svelte';
|
import DetailPanelRating from '$lib/components/asset-viewer/DetailPanelStarRating.svelte';
|
||||||
import DetailPanelTags from '$lib/components/asset-viewer/DetailPanelTags.svelte';
|
import DetailPanelTags from '$lib/components/asset-viewer/DetailPanelTags.svelte';
|
||||||
import { timeToLoadTheMap } from '$lib/constants';
|
import { timeToLoadTheMap } from '$lib/constants';
|
||||||
@@ -12,7 +11,7 @@
|
|||||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||||
import { Route } from '$lib/route';
|
import { Route } from '$lib/route';
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { getAssetMediaUrl, hasPermissions } from '$lib/utils';
|
import { getAssetMediaUrl } from '$lib/utils';
|
||||||
import { delay, getDimensions } from '$lib/utils/asset-utils';
|
import { delay, getDimensions } from '$lib/utils/asset-utils';
|
||||||
import { getByteUnitString } from '$lib/utils/byte-units';
|
import { getByteUnitString } from '$lib/utils/byte-units';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
@@ -21,7 +20,6 @@
|
|||||||
AssetMediaSize,
|
AssetMediaSize,
|
||||||
getAllAlbums,
|
getAllAlbums,
|
||||||
getAssetInfo,
|
getAssetInfo,
|
||||||
SharingPermission,
|
|
||||||
type AlbumResponseDto,
|
type AlbumResponseDto,
|
||||||
type AssetResponseDto,
|
type AssetResponseDto,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
@@ -34,6 +32,7 @@
|
|||||||
import OnEvents from '../OnEvents.svelte';
|
import OnEvents from '../OnEvents.svelte';
|
||||||
import UserAvatar from '../shared-components/UserAvatar.svelte';
|
import UserAvatar from '../shared-components/UserAvatar.svelte';
|
||||||
import AlbumListItemDetails from './AlbumListItemDetails.svelte';
|
import AlbumListItemDetails from './AlbumListItemDetails.svelte';
|
||||||
|
import DetailPanelPeople from '$lib/components/asset-viewer/DetailPanelPeople.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
asset: AssetResponseDto;
|
asset: AssetResponseDto;
|
||||||
@@ -43,7 +42,6 @@
|
|||||||
let { asset, currentAlbum = null }: Props = $props();
|
let { asset, currentAlbum = null }: Props = $props();
|
||||||
|
|
||||||
let isOwner = $derived(authManager.authenticated && authManager.user.id === asset.ownerId);
|
let isOwner = $derived(authManager.authenticated && authManager.user.id === asset.ownerId);
|
||||||
const allowExifUpdate = $derived(hasPermissions(asset, SharingPermission.AssetUpdate, SharingPermission.ExifRead));
|
|
||||||
let latlng = $derived(
|
let latlng = $derived(
|
||||||
(() => {
|
(() => {
|
||||||
const lat = asset.exifInfo?.latitude;
|
const lat = asset.exifInfo?.latitude;
|
||||||
@@ -149,9 +147,9 @@
|
|||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<DetailPanelDescription {asset} {allowExifUpdate} />
|
<DetailPanelDescription {asset} {isOwner} />
|
||||||
<DetailPanelRating {asset} {allowExifUpdate} />
|
<DetailPanelRating {asset} {isOwner} />
|
||||||
<DetailPanelPeople {asset} {previousRoute} />
|
<DetailPanelPeople {asset} {isOwner} {previousRoute} />
|
||||||
|
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
{#if asset.exifInfo}
|
{#if asset.exifInfo}
|
||||||
@@ -162,7 +160,7 @@
|
|||||||
<Text size="small" color="muted">{$t('no_exif_info_available')}</Text>
|
<Text size="small" color="muted">{$t('no_exif_info_available')}</Text>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<DetailPanelDate {asset} {allowExifUpdate} />
|
<DetailPanelDate {asset} />
|
||||||
|
|
||||||
<div class="flex gap-4 py-4">
|
<div class="flex gap-4 py-4">
|
||||||
<div><Icon icon={mdiImageOutline} size="24" /></div>
|
<div><Icon icon={mdiImageOutline} size="24" /></div>
|
||||||
@@ -170,7 +168,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<p class="flex place-items-center gap-2 break-all whitespace-pre-wrap">
|
<p class="flex place-items-center gap-2 break-all whitespace-pre-wrap">
|
||||||
{asset.originalFileName}
|
{asset.originalFileName}
|
||||||
{#if allowExifUpdate}
|
{#if isOwner}
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={mdiInformationOutline}
|
icon={mdiInformationOutline}
|
||||||
aria-label={$t('show_file_location')}
|
aria-label={$t('show_file_location')}
|
||||||
@@ -273,7 +271,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<DetailPanelLocation {allowExifUpdate} {asset} />
|
<DetailPanelLocation {isOwner} {asset} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,9 @@
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
asset: AssetResponseDto;
|
asset: AssetResponseDto;
|
||||||
allowExifUpdate: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { asset, allowExifUpdate }: Props = $props();
|
const { asset }: Props = $props();
|
||||||
|
|
||||||
const timeZone = $derived(asset.exifInfo?.timeZone ?? undefined);
|
const timeZone = $derived(asset.exifInfo?.timeZone ?? undefined);
|
||||||
const dateTime = $derived(
|
const dateTime = $derived(
|
||||||
@@ -21,8 +20,13 @@
|
|||||||
? fromISODateTime(asset.exifInfo.dateTimeOriginal, timeZone)
|
? fromISODateTime(asset.exifInfo.dateTimeOriginal, timeZone)
|
||||||
: fromISODateTimeUTC(asset.localDateTime),
|
: fromISODateTimeUTC(asset.localDateTime),
|
||||||
);
|
);
|
||||||
|
const isOwner = $derived(authManager.authenticated && asset.ownerId === authManager.user.id);
|
||||||
|
|
||||||
const handleChangeDate = async () => {
|
const handleChangeDate = async () => {
|
||||||
|
if (!isOwner) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await modalManager.show(AssetChangeDateModal, {
|
await modalManager.show(AssetChangeDateModal, {
|
||||||
asset: toTimelineAsset(asset),
|
asset: toTimelineAsset(asset),
|
||||||
initialDate: dateTime,
|
initialDate: dateTime,
|
||||||
@@ -36,8 +40,8 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="flex w-full place-items-start justify-between gap-4 py-4 text-start"
|
class="flex w-full place-items-start justify-between gap-4 py-4 text-start"
|
||||||
onclick={handleChangeDate}
|
onclick={handleChangeDate}
|
||||||
title={allowExifUpdate ? $t('edit_date') : ''}
|
title={isOwner ? $t('edit_date') : ''}
|
||||||
class:hover:text-primary={allowExifUpdate}
|
class:hover:text-primary={isOwner}
|
||||||
data-testid="detail-panel-edit-date-button"
|
data-testid="detail-panel-edit-date-button"
|
||||||
>
|
>
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
@@ -64,13 +68,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if allowExifUpdate}
|
{#if isOwner}
|
||||||
<div class="p-1">
|
<div class="p-1">
|
||||||
<Icon icon={mdiPencil} size="20" />
|
<Icon icon={mdiPencil} size="20" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{:else if !dateTime && allowExifUpdate}
|
{:else if !dateTime && isOwner}
|
||||||
<div class="flex place-items-start justify-between gap-4 py-4">
|
<div class="flex place-items-start justify-between gap-4 py-4">
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<Icon icon={mdiCalendar} size="24" />
|
<Icon icon={mdiCalendar} size="24" />
|
||||||
|
|||||||
@@ -8,10 +8,10 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
asset: AssetResponseDto;
|
asset: AssetResponseDto;
|
||||||
allowExifUpdate: boolean;
|
isOwner: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { asset, allowExifUpdate }: Props = $props();
|
let { asset, isOwner }: Props = $props();
|
||||||
|
|
||||||
let description = $derived(asset.exifInfo?.description ?? '');
|
let description = $derived(asset.exifInfo?.description ?? '');
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if allowExifUpdate}
|
{#if isOwner}
|
||||||
<section class="mt-10 px-4">
|
<section class="mt-10 px-4">
|
||||||
<Textarea
|
<Textarea
|
||||||
bind:value={description}
|
bind:value={description}
|
||||||
|
|||||||
@@ -7,11 +7,11 @@
|
|||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
allowExifUpdate: boolean;
|
isOwner: boolean;
|
||||||
asset: AssetResponseDto;
|
asset: AssetResponseDto;
|
||||||
};
|
};
|
||||||
|
|
||||||
let { allowExifUpdate, asset = $bindable() }: Props = $props();
|
let { isOwner, asset = $bindable() }: Props = $props();
|
||||||
|
|
||||||
const onAction = async () => {
|
const onAction = async () => {
|
||||||
const point = await modalManager.show(GeolocationPointPickerModal, { asset });
|
const point = await modalManager.show(GeolocationPointPickerModal, { asset });
|
||||||
@@ -34,9 +34,9 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex w-full place-items-start justify-between gap-4 py-4 text-start"
|
class="flex w-full place-items-start justify-between gap-4 py-4 text-start"
|
||||||
onclick={allowExifUpdate ? onAction : undefined}
|
onclick={isOwner ? onAction : undefined}
|
||||||
title={allowExifUpdate ? $t('edit_location') : ''}
|
title={isOwner ? $t('edit_location') : ''}
|
||||||
class:hover:text-primary={allowExifUpdate}
|
class:hover:text-primary={isOwner}
|
||||||
>
|
>
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<div><Icon icon={mdiMapMarkerOutline} size="24" /></div>
|
<div><Icon icon={mdiMapMarkerOutline} size="24" /></div>
|
||||||
@@ -58,13 +58,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if allowExifUpdate}
|
{#if isOwner}
|
||||||
<div>
|
<div>
|
||||||
<Icon icon={mdiPencil} size="20" />
|
<Icon icon={mdiPencil} size="20" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{:else if !asset.exifInfo?.city && allowExifUpdate}
|
{:else if !asset.exifInfo?.city && isOwner}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex w-full place-items-start justify-between gap-4 rounded-lg py-4 text-start hover:text-primary"
|
class="flex w-full place-items-start justify-between gap-4 rounded-lg py-4 text-start hover:text-primary"
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
import { Route } from '$lib/route';
|
import { Route } from '$lib/route';
|
||||||
import { faceManager } from '$lib/stores/face.svelte';
|
import { faceManager } from '$lib/stores/face.svelte';
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { getPeopleThumbnailUrl, hasPermissions } from '$lib/utils';
|
import { getPeopleThumbnailUrl } from '$lib/utils';
|
||||||
import { SharingPermission, type AssetResponseDto } from '@immich/sdk';
|
import { type AssetResponseDto } from '@immich/sdk';
|
||||||
import { IconButton, Text } from '@immich/ui';
|
import { IconButton, Text } from '@immich/ui';
|
||||||
import { mdiEye, mdiEyeOff, mdiPencil, mdiPlus } from '@mdi/js';
|
import { mdiEye, mdiEyeOff, mdiPencil, mdiPlus } from '@mdi/js';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
@@ -14,13 +14,13 @@
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
asset: AssetResponseDto;
|
asset: AssetResponseDto;
|
||||||
|
isOwner: boolean;
|
||||||
previousRoute: string;
|
previousRoute: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { asset, previousRoute }: Props = $props();
|
const { asset, isOwner, previousRoute }: Props = $props();
|
||||||
|
|
||||||
const people = $derived(Array.from(faceManager.people));
|
const people = $derived(Array.from(faceManager.people));
|
||||||
$effect(() => console.log(people));
|
|
||||||
const visiblePeople = $derived(
|
const visiblePeople = $derived(
|
||||||
people
|
people
|
||||||
.filter((p) => assetViewerManager.isShowingHiddenPeople || !p.isHidden)
|
.filter((p) => assetViewerManager.isShowingHiddenPeople || !p.isHidden)
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !authManager.isSharedLink && hasPermissions(asset, SharingPermission.PersonRead)}
|
{#if !authManager.isSharedLink && isOwner}
|
||||||
<section class="px-4 pt-4 text-sm">
|
<section class="px-4 pt-4 text-sm">
|
||||||
<div class="flex h-10 w-full items-center justify-between">
|
<div class="flex h-10 w-full items-center justify-between">
|
||||||
<Text size="small" color="muted">{$t('people')}</Text>
|
<Text size="small" color="muted">{$t('people')}</Text>
|
||||||
|
|||||||
@@ -8,10 +8,10 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
asset: AssetResponseDto;
|
asset: AssetResponseDto;
|
||||||
allowExifUpdate: boolean;
|
isOwner: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { asset, allowExifUpdate }: Props = $props();
|
let { asset, isOwner }: Props = $props();
|
||||||
|
|
||||||
let rating = $derived(asset.exifInfo?.rating || null) as Rating;
|
let rating = $derived(asset.exifInfo?.rating || null) as Rating;
|
||||||
|
|
||||||
@@ -26,10 +26,6 @@
|
|||||||
|
|
||||||
{#if !authManager.isSharedLink && authManager.authenticated && authManager.preferences.ratings.enabled}
|
{#if !authManager.isSharedLink && authManager.authenticated && authManager.preferences.ratings.enabled}
|
||||||
<section class="px-4 pt-4">
|
<section class="px-4 pt-4">
|
||||||
<StarRating
|
<StarRating {rating} readOnly={!isOwner} onRating={(rating) => handlePromiseError(handleChangeRating(rating))} />
|
||||||
{rating}
|
|
||||||
readOnly={!allowExifUpdate}
|
|
||||||
onRating={(rating) => handlePromiseError(handleChangeRating(rating))}
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -91,7 +91,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<header>
|
<header>
|
||||||
<ControlAppBar showBackButton={false}>
|
<ControlAppBar>
|
||||||
{#snippet leading()}
|
{#snippet leading()}
|
||||||
<a data-sveltekit-preload-data="hover" class="ms-4" href="/">
|
<a data-sveltekit-preload-data="hover" class="ms-4" href="/">
|
||||||
<Logo variant="inline" />
|
<Logo variant="inline" />
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import { getAssetInfo, type SharedLinkResponseDto } from '@immich/sdk';
|
import { getAssetInfo, type SharedLinkResponseDto } from '@immich/sdk';
|
||||||
import { IconButton, Logo, toastManager } from '@immich/ui';
|
import { IconButton, Logo, toastManager } from '@immich/ui';
|
||||||
import { mdiArrowLeft, mdiDownload, mdiFileImagePlusOutline, mdiSelectAll } from '@mdi/js';
|
import { mdiDownload, mdiFileImagePlusOutline, mdiSelectAll } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import ControlAppBar from '../shared-components/ControlAppBar.svelte';
|
import ControlAppBar from '../shared-components/ControlAppBar.svelte';
|
||||||
import GalleryViewer from '../shared-components/gallery-viewer/GalleryViewer.svelte';
|
import GalleryViewer from '../shared-components/gallery-viewer/GalleryViewer.svelte';
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</AssetSelectControlBar>
|
</AssetSelectControlBar>
|
||||||
{:else}
|
{:else}
|
||||||
<ControlAppBar onClose={() => goto(Route.photos())} backIcon={mdiArrowLeft} showBackButton={false}>
|
<ControlAppBar>
|
||||||
{#snippet leading()}
|
{#snippet leading()}
|
||||||
<a data-sveltekit-preload-data="hover" class="ms-4" href="/">
|
<a data-sveltekit-preload-data="hover" class="ms-4" href="/">
|
||||||
<Logo variant={mediaQueryManager.maxMd ? 'icon' : 'inline'} class="min-w-10" />
|
<Logo variant={mediaQueryManager.maxMd ? 'icon' : 'inline'} class="min-w-10" />
|
||||||
|
|||||||
@@ -1,97 +1,49 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { browser } from '$app/environment';
|
import { ControlBar, ControlBarContent, ControlBarHeader, ControlBarOverflow, ControlBarTitle } from '@immich/ui';
|
||||||
import { IconButton } from '@immich/ui';
|
|
||||||
import { mdiClose } from '@mdi/js';
|
import { mdiClose } from '@mdi/js';
|
||||||
import { onDestroy, onMount, type Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import type { ClassValue } from 'svelte/elements';
|
||||||
import { fly } from 'svelte/transition';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
showBackButton?: boolean;
|
|
||||||
backIcon?: string;
|
backIcon?: string;
|
||||||
tailwindClasses?: string;
|
class?: ClassValue;
|
||||||
forceDark?: boolean;
|
|
||||||
multiRow?: boolean;
|
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
|
title?: Snippet | string;
|
||||||
leading?: Snippet;
|
leading?: Snippet;
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
trailing?: Snippet;
|
trailing?: Snippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let { backIcon = mdiClose, class: className = '', onClose, title, leading, children, trailing }: Props = $props();
|
||||||
showBackButton = true,
|
|
||||||
backIcon = mdiClose,
|
|
||||||
tailwindClasses = '',
|
|
||||||
forceDark = false,
|
|
||||||
multiRow = false,
|
|
||||||
onClose = () => {},
|
|
||||||
leading,
|
|
||||||
children,
|
|
||||||
trailing,
|
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
let appBarBorder = $state('border border-subtle');
|
|
||||||
|
|
||||||
const onScroll = () => {
|
|
||||||
if (window.scrollY > 80) {
|
|
||||||
appBarBorder = 'border border-gray-200 bg-gray-50 dark:border-gray-600';
|
|
||||||
|
|
||||||
if (forceDark) {
|
|
||||||
appBarBorder = 'border border-gray-600';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
appBarBorder = 'border border-subtle';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
if (browser) {
|
|
||||||
document.addEventListener('scroll', onScroll, { passive: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
if (browser) {
|
|
||||||
document.removeEventListener('scroll', onScroll);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div in:fly={{ y: 10, duration: 200 }} class="absolute top-0 w-full bg-transparent">
|
<div class={['absolute top-0 w-full bg-transparent p-2']}>
|
||||||
<nav
|
<ControlBar closeIcon={backIcon} {onClose} shape="round" class={className}>
|
||||||
id="asset-selection-app-bar"
|
{#if title || leading}
|
||||||
class={[
|
<ControlBarHeader>
|
||||||
'grid',
|
{#if title}
|
||||||
multiRow && 'grid-cols-[100%] md:grid-cols-[25%_50%_25%]',
|
<ControlBarTitle>
|
||||||
!multiRow && 'grid-cols-[10%_80%_10%] sm:grid-cols-[25%_50%_25%]',
|
{#if typeof title === 'string'}
|
||||||
'justify-between lg:grid-cols-[25%_50%_25%]',
|
{title}
|
||||||
appBarBorder,
|
{:else}
|
||||||
'm-2 place-items-center rounded-full p-2 transition-all max-md:p-0',
|
{@render title()}
|
||||||
tailwindClasses,
|
{/if}
|
||||||
forceDark ? 'bg-immich-dark-gray! text-white' : 'bg-light-50 dark:bg-immich-dark-gray',
|
</ControlBarTitle>
|
||||||
]}
|
{/if}
|
||||||
>
|
{@render leading?.()}
|
||||||
<div class="flex place-items-center justify-self-start sm:gap-6 dark:text-immich-dark-fg {forceDark ? 'dark' : ''}">
|
</ControlBarHeader>
|
||||||
{#if showBackButton}
|
{/if}
|
||||||
<IconButton
|
|
||||||
aria-label={$t('close')}
|
|
||||||
onclick={onClose}
|
|
||||||
color="secondary"
|
|
||||||
shape="round"
|
|
||||||
variant="ghost"
|
|
||||||
icon={backIcon}
|
|
||||||
size="large"
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{@render leading?.()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-full">
|
{#if children}
|
||||||
{@render children?.()}
|
<ControlBarContent>
|
||||||
</div>
|
{@render children()}
|
||||||
|
</ControlBarContent>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="me-4 flex place-items-center gap-1 justify-self-end max-[350px]:me-0 max-[350px]:gap-0">
|
{#if trailing}
|
||||||
{@render trailing?.()}
|
<ControlBarOverflow>
|
||||||
</div>
|
{@render trailing()}
|
||||||
</nav>
|
</ControlBarOverflow>
|
||||||
|
{/if}
|
||||||
|
</ControlBar>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,19 +7,18 @@
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
forceDark?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let { children, forceDark }: Props = $props();
|
let { children }: Props = $props();
|
||||||
|
|
||||||
const onClose = () => assetMultiSelectManager.clear();
|
const onClose = () => assetMultiSelectManager.clear();
|
||||||
|
|
||||||
const assets = $derived(assetMultiSelectManager.assets);
|
const assets = $derived(assetMultiSelectManager.assets);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ControlAppBar {onClose} {forceDark} backIcon={mdiClose} tailwindClasses="bg-white shadow-md">
|
<ControlAppBar {onClose} backIcon={mdiClose}>
|
||||||
{#snippet leading()}
|
{#snippet leading()}
|
||||||
<div class="font-medium {forceDark ? 'text-immich-dark-primary' : 'text-primary'}">
|
<div class="font-medium text-primary">
|
||||||
<p class="block sm:hidden">{assets.length}</p>
|
<p class="block sm:hidden">{assets.length}</p>
|
||||||
<p class="hidden sm:block">{$t('selected_count', { values: { count: assets.length } })}</p>
|
<p class="hidden sm:block">{$t('selected_count', { values: { count: assets.length } })}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
{ title: $t('admin.memory_cleanup_job'), value: ManualJobName.MemoryCleanup },
|
{ title: $t('admin.memory_cleanup_job'), value: ManualJobName.MemoryCleanup },
|
||||||
{ title: $t('admin.memory_generate_job'), value: ManualJobName.MemoryCreate },
|
{ title: $t('admin.memory_generate_job'), value: ManualJobName.MemoryCreate },
|
||||||
{ title: $t('admin.backup_database'), value: ManualJobName.BackupDatabase },
|
{ title: $t('admin.backup_database'), value: ManualJobName.BackupDatabase },
|
||||||
{ title: 'Person grouping', value: ManualJobName.PersonGroupMerge },
|
|
||||||
].map(({ value, title }) => ({ id: value, label: title, value }));
|
].map(({ value, title }) => ({ id: value, label: title, value }));
|
||||||
|
|
||||||
let selectedJob: ComboBoxOption | undefined = $state(undefined);
|
let selectedJob: ComboBoxOption | undefined = $state(undefined);
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
try {
|
try {
|
||||||
await mergePerson({
|
await mergePerson({
|
||||||
id: personToBeMergedInto.id,
|
id: personToBeMergedInto.id,
|
||||||
mergeFaceClusterDto: { ids: [personToMerge.id] },
|
mergePersonDto: { ids: [personToMerge.id] },
|
||||||
});
|
});
|
||||||
toastManager.primary($t('merge_people_successfully'));
|
toastManager.primary($t('merge_people_successfully'));
|
||||||
onClose([personToMerge, personToBeMergedInto]);
|
onClose([personToMerge, personToBeMergedInto]);
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { getOwnAlbumUser, SharingPermission, updateOwnAlbumUser } from '@immich/sdk';
|
|
||||||
import { Checkbox, Field, FormModal, Heading, Stack, Switch, toastManager } from '@immich/ui';
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { init } from 'svelte-i18n';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
onClose: () => void;
|
|
||||||
albumId?: string;
|
|
||||||
partnerId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const { onClose, ...rest }: Props = $props();
|
|
||||||
|
|
||||||
let checkedPermissions = $state<SharingPermission[]>([]);
|
|
||||||
let viewInTimeline = $state<boolean>(false);
|
|
||||||
|
|
||||||
const onCheckedChange = (permission: SharingPermission, checked: boolean) => {
|
|
||||||
if (checked) {
|
|
||||||
checkedPermissions.push(permission);
|
|
||||||
} else {
|
|
||||||
checkedPermissions = checkedPermissions.filter((perm) => perm !== permission);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = async () => {
|
|
||||||
const permissions =
|
|
||||||
checkedPermissions.length === Object.values(SharingPermission).length - 1
|
|
||||||
? [SharingPermission.All]
|
|
||||||
: checkedPermissions;
|
|
||||||
if (rest.albumId) {
|
|
||||||
await updateOwnAlbumUser({
|
|
||||||
id: rest.albumId,
|
|
||||||
updateSharingOptionsDto: { permissions, inTimeline: viewInTimeline },
|
|
||||||
});
|
|
||||||
toastManager.success();
|
|
||||||
}
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
if (rest.albumId) {
|
|
||||||
const { permissions, inTimeline } = await getOwnAlbumUser({ id: rest.albumId });
|
|
||||||
checkedPermissions = permissions;
|
|
||||||
viewInTimeline = inTimeline;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<FormModal title="Sharing options" {onClose} {onSubmit}>
|
|
||||||
<Stack>
|
|
||||||
<Field label="View in timeline">
|
|
||||||
<Switch bind:checked={viewInTimeline} />
|
|
||||||
</Field>
|
|
||||||
<Heading>Permissions</Heading>
|
|
||||||
<Field label={SharingPermission.All}>
|
|
||||||
<Checkbox
|
|
||||||
id="permission-{SharingPermission.All}"
|
|
||||||
checked={checkedPermissions.length === Object.values(SharingPermission).length - 1}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
checked
|
|
||||||
? (checkedPermissions = Object.values(SharingPermission).filter(
|
|
||||||
(permission) => permission !== SharingPermission.All,
|
|
||||||
))
|
|
||||||
: (checkedPermissions = [])}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
{#each Object.values(SharingPermission).filter((permission) => permission !== SharingPermission.All) as permission (permission)}
|
|
||||||
<Field label={permission}>
|
|
||||||
<Checkbox
|
|
||||||
id="permission-{permission}"
|
|
||||||
checked={checkedPermissions.includes(permission)}
|
|
||||||
onCheckedChange={(checked) => onCheckedChange(permission, checked)}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
{/each}
|
|
||||||
</Stack>
|
|
||||||
</FormModal>
|
|
||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
AssetVisibility,
|
AssetVisibility,
|
||||||
getAssetInfo,
|
getAssetInfo,
|
||||||
runAssetJobs,
|
runAssetJobs,
|
||||||
SharingPermission,
|
|
||||||
updateAsset,
|
updateAsset,
|
||||||
type AssetJobsDto,
|
type AssetJobsDto,
|
||||||
type AssetResponseDto,
|
type AssetResponseDto,
|
||||||
@@ -42,7 +41,7 @@ import { eventManager } from '$lib/managers/event-manager.svelte';
|
|||||||
import AssetAddToAlbumModal from '$lib/modals/AssetAddToAlbumModal.svelte';
|
import AssetAddToAlbumModal from '$lib/modals/AssetAddToAlbumModal.svelte';
|
||||||
import AssetTagModal from '$lib/modals/AssetTagModal.svelte';
|
import AssetTagModal from '$lib/modals/AssetTagModal.svelte';
|
||||||
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
|
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
|
||||||
import { getAssetMediaUrl, getSharedLink, hasPermissions, sleep } from '$lib/utils';
|
import { getAssetMediaUrl, getSharedLink, sleep } from '$lib/utils';
|
||||||
import { downloadUrl } from '$lib/utils/asset-utils';
|
import { downloadUrl } from '$lib/utils/asset-utils';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { getFormatter } from '$lib/utils/i18n';
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
@@ -99,12 +98,7 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
|||||||
const Share: ActionItem = {
|
const Share: ActionItem = {
|
||||||
title: $t('share'),
|
title: $t('share'),
|
||||||
icon: mdiShareVariantOutline,
|
icon: mdiShareVariantOutline,
|
||||||
$if: () =>
|
$if: () => !!(authUser && !asset.isTrashed && asset.visibility !== AssetVisibility.Locked),
|
||||||
!!(
|
|
||||||
hasPermissions(asset, SharingPermission.AssetShare) &&
|
|
||||||
!asset.isTrashed &&
|
|
||||||
asset.visibility !== AssetVisibility.Locked
|
|
||||||
),
|
|
||||||
onAction: () => modalManager.show(SharedLinkCreateModal, { assetIds: [asset.id] }),
|
onAction: () => modalManager.show(SharedLinkCreateModal, { assetIds: [asset.id] }),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -125,7 +119,7 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
|||||||
|
|
||||||
const SharedLinkDownload: ActionItem = {
|
const SharedLinkDownload: ActionItem = {
|
||||||
...Download,
|
...Download,
|
||||||
$if: () => hasPermissions(asset, SharingPermission.AssetShare) || !!sharedLink?.allowDownload,
|
$if: () => isOwner || !!sharedLink?.allowDownload,
|
||||||
};
|
};
|
||||||
|
|
||||||
const PlayMotionPhoto: ActionItem = {
|
const PlayMotionPhoto: ActionItem = {
|
||||||
@@ -228,7 +222,7 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
|||||||
icon: mdiTune,
|
icon: mdiTune,
|
||||||
$if: () =>
|
$if: () =>
|
||||||
!sharedLink &&
|
!sharedLink &&
|
||||||
hasPermissions(asset, SharingPermission.AssetEdit) &&
|
isOwner &&
|
||||||
asset.type === AssetTypeEnum.Image &&
|
asset.type === AssetTypeEnum.Image &&
|
||||||
!asset.livePhotoVideoId &&
|
!asset.livePhotoVideoId &&
|
||||||
asset.exifInfo?.projectionType !== ProjectionType.EQUIRECTANGULAR &&
|
asset.exifInfo?.projectionType !== ProjectionType.EQUIRECTANGULAR &&
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import {
|
|||||||
AssetMediaSize,
|
AssetMediaSize,
|
||||||
AssetTypeEnum,
|
AssetTypeEnum,
|
||||||
MemoryType,
|
MemoryType,
|
||||||
SharingPermission,
|
|
||||||
finishOAuth,
|
finishOAuth,
|
||||||
getAssetOriginalPath,
|
getAssetOriginalPath,
|
||||||
getAssetPlaybackPath,
|
getAssetPlaybackPath,
|
||||||
@@ -414,17 +413,3 @@ export const transformToTitleCase = (text: string) => {
|
|||||||
}
|
}
|
||||||
return result.trim();
|
return result.trim();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hasPermissions = (asset: AssetResponseDto, ...permissions: SharingPermission[]) => {
|
|
||||||
if (asset.permissions.includes(SharingPermission.All)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const permission of permissions) {
|
|
||||||
if (!asset.permissions.includes(permission)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|||||||
+6
-15
@@ -1,10 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto, invalidate, onNavigate } from '$app/navigation';
|
import { goto, invalidate, onNavigate } from '$app/navigation';
|
||||||
import { scrollMemoryClearer } from '$lib/actions/scroll-memory';
|
import { scrollMemoryClearer } from '$lib/actions/scroll-memory';
|
||||||
import AlbumDescription from './AlbumDescription.svelte';
|
|
||||||
import AlbumMap from '$lib/components/album-page/AlbumMap.svelte';
|
import AlbumMap from '$lib/components/album-page/AlbumMap.svelte';
|
||||||
import AlbumSummary from '$lib/components/album-page/AlbumSummary.svelte';
|
import AlbumSummary from '$lib/components/album-page/AlbumSummary.svelte';
|
||||||
import AlbumTitle from './AlbumTitle.svelte';
|
|
||||||
import ActivityStatus from '$lib/components/asset-viewer/ActivityStatus.svelte';
|
import ActivityStatus from '$lib/components/asset-viewer/ActivityStatus.svelte';
|
||||||
import ActivityViewer from '$lib/components/asset-viewer/ActivityViewer.svelte';
|
import ActivityViewer from '$lib/components/asset-viewer/ActivityViewer.svelte';
|
||||||
import HeaderActionButton from '$lib/components/HeaderActionButton.svelte';
|
import HeaderActionButton from '$lib/components/HeaderActionButton.svelte';
|
||||||
@@ -73,13 +71,13 @@
|
|||||||
mdiLink,
|
mdiLink,
|
||||||
mdiPlus,
|
mdiPlus,
|
||||||
mdiPresentationPlay,
|
mdiPresentationPlay,
|
||||||
mdiShareVariant,
|
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import SharingOptionsModal from '$lib/modals/SharingOptionsModal.svelte';
|
import AlbumDescription from './AlbumDescription.svelte';
|
||||||
|
import AlbumTitle from './AlbumTitle.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: PageData;
|
data: PageData;
|
||||||
@@ -407,16 +405,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<IconButton
|
{#if isOwned}
|
||||||
shape="round"
|
<ActionButton action={Share} />
|
||||||
aria-label="Sharing permissions"
|
{/if}
|
||||||
color="secondary"
|
|
||||||
size="medium"
|
|
||||||
icon={mdiShareVariant}
|
|
||||||
onclick={() => modalManager.show(SharingOptionsModal, { albumId: album.id })}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ActionButton action={Share} />
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<AlbumDescription
|
<AlbumDescription
|
||||||
@@ -508,7 +499,7 @@
|
|||||||
</AssetSelectControlBar>
|
</AssetSelectControlBar>
|
||||||
{:else}
|
{:else}
|
||||||
{#if viewMode === AlbumPageViewMode.VIEW}
|
{#if viewMode === AlbumPageViewMode.VIEW}
|
||||||
<ControlAppBar showBackButton backIcon={mdiArrowLeft} onClose={() => goto(Route.albums())}>
|
<ControlAppBar backIcon={mdiArrowLeft} onClose={() => goto(Route.albums())}>
|
||||||
{#snippet trailing()}
|
{#snippet trailing()}
|
||||||
<ActionButton action={Cast} />
|
<ActionButton action={Cast} />
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,8 @@
|
|||||||
import { afterNavigate, goto } from '$app/navigation';
|
import { afterNavigate, goto } from '$app/navigation';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { shortcuts } from '$lib/actions/shortcut';
|
import { shortcuts } from '$lib/actions/shortcut';
|
||||||
import MemoryPhotoViewer from './MemoryPhotoViewer.svelte';
|
|
||||||
import MemoryVideoViewer from './MemoryVideoViewer.svelte';
|
|
||||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/ButtonContextMenu.svelte';
|
import ButtonContextMenu from '$lib/components/shared-components/context-menu/ButtonContextMenu.svelte';
|
||||||
import MenuOption from '$lib/components/shared-components/context-menu/MenuOption.svelte';
|
import MenuOption from '$lib/components/shared-components/context-menu/MenuOption.svelte';
|
||||||
import ControlAppBar from '$lib/components/shared-components/ControlAppBar.svelte';
|
|
||||||
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/GalleryViewer.svelte';
|
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/GalleryViewer.svelte';
|
||||||
import ArchiveAction from '$lib/components/timeline/actions/ArchiveAction.svelte';
|
import ArchiveAction from '$lib/components/timeline/actions/ArchiveAction.svelte';
|
||||||
import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte';
|
import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte';
|
||||||
@@ -37,6 +34,7 @@
|
|||||||
mdiChevronLeft,
|
mdiChevronLeft,
|
||||||
mdiChevronRight,
|
mdiChevronRight,
|
||||||
mdiChevronUp,
|
mdiChevronUp,
|
||||||
|
mdiClose,
|
||||||
mdiDotsVertical,
|
mdiDotsVertical,
|
||||||
mdiHeart,
|
mdiHeart,
|
||||||
mdiHeartOutline,
|
mdiHeartOutline,
|
||||||
@@ -54,6 +52,8 @@
|
|||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import type { Attachment } from 'svelte/attachments';
|
import type { Attachment } from 'svelte/attachments';
|
||||||
import { Tween } from 'svelte/motion';
|
import { Tween } from 'svelte/motion';
|
||||||
|
import MemoryPhotoViewer from './MemoryPhotoViewer.svelte';
|
||||||
|
import MemoryVideoViewer from './MemoryVideoViewer.svelte';
|
||||||
|
|
||||||
let memoryGallery: HTMLElement | undefined = $state();
|
let memoryGallery: HTMLElement | undefined = $state();
|
||||||
let memoryWrapper: HTMLElement | undefined = $state();
|
let memoryWrapper: HTMLElement | undefined = $state();
|
||||||
@@ -327,8 +327,8 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{#if assetMultiSelectManager.selectionActive}
|
{#if assetMultiSelectManager.selectionActive}
|
||||||
<div class="dark sticky top-0 z-1">
|
<div class="sticky top-0 z-1 dark">
|
||||||
<AssetSelectControlBar forceDark>
|
<AssetSelectControlBar>
|
||||||
{@const Actions = getAssetBulkActions($t)}
|
{@const Actions = getAssetBulkActions($t)}
|
||||||
<CreateSharedLink />
|
<CreateSharedLink />
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -365,22 +365,33 @@
|
|||||||
|
|
||||||
<section
|
<section
|
||||||
id="memory-viewer"
|
id="memory-viewer"
|
||||||
class="w-full bg-immich-dark-gray"
|
class="dark w-full text-white bg-immich-dark-gray"
|
||||||
bind:this={memoryWrapper}
|
bind:this={memoryWrapper}
|
||||||
bind:clientHeight={viewport.height}
|
bind:clientHeight={viewport.height}
|
||||||
bind:clientWidth={viewport.width}
|
bind:clientWidth={viewport.width}
|
||||||
>
|
>
|
||||||
{#if current}
|
{#if current}
|
||||||
<ControlAppBar onClose={() => goto(Route.photos())} forceDark multiRow>
|
<div
|
||||||
{#snippet leading()}
|
class="max-md:h-auto max-md:flex-col dark grid grid-cols-[100%] md:grid-cols-[25%_50%_25%] px-2 py-2 md:px-4 md:py-4"
|
||||||
{#if current}
|
>
|
||||||
|
{#if current}
|
||||||
|
<div class="flex gap-2 md:gap-6 items-center">
|
||||||
|
<IconButton
|
||||||
|
shape="round"
|
||||||
|
variant="ghost"
|
||||||
|
color="secondary"
|
||||||
|
icon={mdiClose}
|
||||||
|
aria-label={$t('close')}
|
||||||
|
size="large"
|
||||||
|
onclick={() => goto(Route.photos())}
|
||||||
|
/>
|
||||||
<p class="text-lg">
|
<p class="text-lg">
|
||||||
{$memoryLaneTitle(current.memory)}
|
{$memoryLaneTitle(current.memory)}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
</div>
|
||||||
{/snippet}
|
{/if}
|
||||||
|
|
||||||
<div class="dark flex place-content-center place-items-center gap-2">
|
<div class="dark flex w-full place-content-center place-items-center gap-2">
|
||||||
<IconButton
|
<IconButton
|
||||||
shape="round"
|
shape="round"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -438,7 +449,7 @@
|
|||||||
</media-mute-button>
|
</media-mute-button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</ControlAppBar>
|
</div>
|
||||||
|
|
||||||
{#if galleryInView}
|
{#if galleryInView}
|
||||||
<div
|
<div
|
||||||
@@ -462,7 +473,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<!-- Viewer -->
|
<!-- Viewer -->
|
||||||
<section class="overflow-hidden pt-32 md:pt-20" bind:clientHeight={viewerHeight}>
|
<section class="overflow-hidden pt-6 md:pt-0" bind:clientHeight={viewerHeight}>
|
||||||
<div
|
<div
|
||||||
class="ms-[-100%] box-border flex h-[calc(100vh-224px)] w-[300%] items-center justify-center gap-10 overflow-hidden md:h-[calc(100vh-180px)]"
|
class="ms-[-100%] box-border flex h-[calc(100vh-224px)] w-[300%] items-center justify-center gap-10 overflow-hidden md:h-[calc(100vh-180px)]"
|
||||||
>
|
>
|
||||||
|
|||||||
+1
-1
@@ -47,7 +47,7 @@
|
|||||||
<DownloadAction />
|
<DownloadAction />
|
||||||
</AssetSelectControlBar>
|
</AssetSelectControlBar>
|
||||||
{:else}
|
{:else}
|
||||||
<ControlAppBar showBackButton backIcon={mdiArrowLeft} onClose={() => goto(Route.sharing())}>
|
<ControlAppBar backIcon={mdiArrowLeft} onClose={() => goto(Route.sharing())}>
|
||||||
{#snippet leading()}
|
{#snippet leading()}
|
||||||
<p class="whitespace-nowrap text-immich-fg dark:text-immich-dark-fg">
|
<p class="whitespace-nowrap text-immich-fg dark:text-immich-dark-fg">
|
||||||
{$t('partner_list_user_photos', { values: { user: data.partner.name } })}
|
{$t('partner_list_user_photos', { values: { user: data.partner.name } })}
|
||||||
|
|||||||
@@ -231,7 +231,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const personWithSimilarName = await findPeopleWithSimilarName(name, targetPerson.id);
|
const personWithSimilarName = await findPeopleWithSimilarName(name, targetPerson.id);
|
||||||
if (personWithSimilarName && personWithSimilarName.faceClusterId !== targetPerson.faceClusterId) {
|
if (personWithSimilarName) {
|
||||||
personMerge1 = targetPerson;
|
personMerge1 = targetPerson;
|
||||||
personMerge2 = personWithSimilarName;
|
personMerge2 = personWithSimilarName;
|
||||||
potentialMergePeople = people
|
potentialMergePeople = people
|
||||||
|
|||||||
+4
-4
@@ -5,9 +5,6 @@
|
|||||||
import { listNavigation } from '$lib/actions/list-navigation';
|
import { listNavigation } from '$lib/actions/list-navigation';
|
||||||
import { scrollMemoryClearer } from '$lib/actions/scroll-memory';
|
import { scrollMemoryClearer } from '$lib/actions/scroll-memory';
|
||||||
import ImageThumbnail from '$lib/components/assets/thumbnail/ImageThumbnail.svelte';
|
import ImageThumbnail from '$lib/components/assets/thumbnail/ImageThumbnail.svelte';
|
||||||
import EditNameInput from './EditNameInput.svelte';
|
|
||||||
import MergeFaceSelector from './MergeFaceSelector.svelte';
|
|
||||||
import UnmergeFaceSelector from './UnmergeFaceSelector.svelte';
|
|
||||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/ButtonContextMenu.svelte';
|
import ButtonContextMenu from '$lib/components/shared-components/context-menu/ButtonContextMenu.svelte';
|
||||||
import MenuOption from '$lib/components/shared-components/context-menu/MenuOption.svelte';
|
import MenuOption from '$lib/components/shared-components/context-menu/MenuOption.svelte';
|
||||||
@@ -54,6 +51,9 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
import EditNameInput from './EditNameInput.svelte';
|
||||||
|
import MergeFaceSelector from './MergeFaceSelector.svelte';
|
||||||
|
import UnmergeFaceSelector from './UnmergeFaceSelector.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: PageData;
|
data: PageData;
|
||||||
@@ -493,7 +493,7 @@
|
|||||||
</AssetSelectControlBar>
|
</AssetSelectControlBar>
|
||||||
{:else}
|
{:else}
|
||||||
{#if viewMode === PersonPageViewMode.VIEW_ASSETS}
|
{#if viewMode === PersonPageViewMode.VIEW_ASSETS}
|
||||||
<ControlAppBar showBackButton backIcon={mdiArrowLeft} onClose={() => goto(previousRoute)}>
|
<ControlAppBar backIcon={mdiArrowLeft} onClose={() => goto(previousRoute)}>
|
||||||
{#snippet trailing()}
|
{#snippet trailing()}
|
||||||
<ContextMenuButton
|
<ContextMenuButton
|
||||||
items={[SelectFeaturePhoto, HidePerson, ShowPerson, SetDateOfBirth, Merge, Favorite, Unfavorite]}
|
items={[SelectFeaturePhoto, HidePerson, ShowPerson, SetDateOfBirth, Merge, Favorite, Unfavorite]}
|
||||||
|
|||||||
+1
-1
@@ -68,7 +68,7 @@
|
|||||||
try {
|
try {
|
||||||
let results = await mergePerson({
|
let results = await mergePerson({
|
||||||
id: person.id,
|
id: person.id,
|
||||||
mergeFaceClusterDto: { ids: selectedPeople.map(({ id }) => id) },
|
mergePersonDto: { ids: selectedPeople.map(({ id }) => id) },
|
||||||
});
|
});
|
||||||
const mergedPerson = await getPerson({ id: person.id });
|
const mergedPerson = await getPerson({ id: person.id });
|
||||||
const count = results.filter(({ success }) => success).length;
|
const count = results.filter(({ success }) => success).length;
|
||||||
|
|||||||
@@ -387,8 +387,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="fixed inset-s-0 top-0 z-2 w-full">
|
<div class="fixed inset-s-0 top-0 z-2 w-full">
|
||||||
<ControlAppBar onClose={() => goto(previousRoute)} backIcon={mdiArrowLeft}>
|
<ControlAppBar onClose={() => goto(previousRoute)} backIcon={mdiArrowLeft}>
|
||||||
<div class="absolute bg-light"></div>
|
<div class="mx-auto w-full max-w-2xl pe-2">
|
||||||
<div class="w-full flex-1 ps-4">
|
|
||||||
<SearchBar grayTheme={false} value={terms?.query ?? ''} searchQuery={terms} />
|
<SearchBar grayTheme={false} value={terms?.query ?? ''} searchQuery={terms} />
|
||||||
</div>
|
</div>
|
||||||
</ControlAppBar>
|
</ControlAppBar>
|
||||||
|
|||||||
Reference in New Issue
Block a user