chore: rebase and clean-up
This commit is contained in:
Generated
+36
@@ -447,6 +447,12 @@ export interface AssetBulkDeleteDto {
|
|||||||
* @interface AssetBulkUpdateDto
|
* @interface AssetBulkUpdateDto
|
||||||
*/
|
*/
|
||||||
export interface AssetBulkUpdateDto {
|
export interface AssetBulkUpdateDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof AssetBulkUpdateDto
|
||||||
|
*/
|
||||||
|
'dateTimeOriginal'?: string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {Array<string>}
|
* @type {Array<string>}
|
||||||
@@ -465,6 +471,18 @@ export interface AssetBulkUpdateDto {
|
|||||||
* @memberof AssetBulkUpdateDto
|
* @memberof AssetBulkUpdateDto
|
||||||
*/
|
*/
|
||||||
'isFavorite'?: boolean;
|
'isFavorite'?: boolean;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof AssetBulkUpdateDto
|
||||||
|
*/
|
||||||
|
'latitude'?: number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof AssetBulkUpdateDto
|
||||||
|
*/
|
||||||
|
'longitude'?: number;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
@@ -4161,6 +4179,12 @@ export interface UpdateAlbumDto {
|
|||||||
* @interface UpdateAssetDto
|
* @interface UpdateAssetDto
|
||||||
*/
|
*/
|
||||||
export interface UpdateAssetDto {
|
export interface UpdateAssetDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof UpdateAssetDto
|
||||||
|
*/
|
||||||
|
'dateTimeOriginal'?: string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@@ -4179,6 +4203,18 @@ export interface UpdateAssetDto {
|
|||||||
* @memberof UpdateAssetDto
|
* @memberof UpdateAssetDto
|
||||||
*/
|
*/
|
||||||
'isFavorite'?: boolean;
|
'isFavorite'?: boolean;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof UpdateAssetDto
|
||||||
|
*/
|
||||||
|
'latitude'?: number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof UpdateAssetDto
|
||||||
|
*/
|
||||||
|
'longitude'?: number;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
|||||||
Generated
+3
@@ -8,9 +8,12 @@ import 'package:openapi/api.dart';
|
|||||||
## Properties
|
## Properties
|
||||||
Name | Type | Description | Notes
|
Name | Type | Description | Notes
|
||||||
------------ | ------------- | ------------- | -------------
|
------------ | ------------- | ------------- | -------------
|
||||||
|
**dateTimeOriginal** | **String** | | [optional]
|
||||||
**ids** | **List<String>** | | [default to const []]
|
**ids** | **List<String>** | | [default to const []]
|
||||||
**isArchived** | **bool** | | [optional]
|
**isArchived** | **bool** | | [optional]
|
||||||
**isFavorite** | **bool** | | [optional]
|
**isFavorite** | **bool** | | [optional]
|
||||||
|
**latitude** | **num** | | [optional]
|
||||||
|
**longitude** | **num** | | [optional]
|
||||||
**removeParent** | **bool** | | [optional]
|
**removeParent** | **bool** | | [optional]
|
||||||
**stackParentId** | **String** | | [optional]
|
**stackParentId** | **String** | | [optional]
|
||||||
|
|
||||||
|
|||||||
Generated
+3
@@ -8,9 +8,12 @@ import 'package:openapi/api.dart';
|
|||||||
## Properties
|
## Properties
|
||||||
Name | Type | Description | Notes
|
Name | Type | Description | Notes
|
||||||
------------ | ------------- | ------------- | -------------
|
------------ | ------------- | ------------- | -------------
|
||||||
|
**dateTimeOriginal** | **String** | | [optional]
|
||||||
**description** | **String** | | [optional]
|
**description** | **String** | | [optional]
|
||||||
**isArchived** | **bool** | | [optional]
|
**isArchived** | **bool** | | [optional]
|
||||||
**isFavorite** | **bool** | | [optional]
|
**isFavorite** | **bool** | | [optional]
|
||||||
|
**latitude** | **num** | | [optional]
|
||||||
|
**longitude** | **num** | | [optional]
|
||||||
|
|
||||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
|||||||
+56
-1
@@ -13,13 +13,24 @@ part of openapi.api;
|
|||||||
class AssetBulkUpdateDto {
|
class AssetBulkUpdateDto {
|
||||||
/// Returns a new [AssetBulkUpdateDto] instance.
|
/// Returns a new [AssetBulkUpdateDto] instance.
|
||||||
AssetBulkUpdateDto({
|
AssetBulkUpdateDto({
|
||||||
|
this.dateTimeOriginal,
|
||||||
this.ids = const [],
|
this.ids = const [],
|
||||||
this.isArchived,
|
this.isArchived,
|
||||||
this.isFavorite,
|
this.isFavorite,
|
||||||
|
this.latitude,
|
||||||
|
this.longitude,
|
||||||
this.removeParent,
|
this.removeParent,
|
||||||
this.stackParentId,
|
this.stackParentId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
String? dateTimeOriginal;
|
||||||
|
|
||||||
List<String> ids;
|
List<String> ids;
|
||||||
|
|
||||||
///
|
///
|
||||||
@@ -38,6 +49,22 @@ class AssetBulkUpdateDto {
|
|||||||
///
|
///
|
||||||
bool? isFavorite;
|
bool? isFavorite;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
num? latitude;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
num? longitude;
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
/// does not include a default value (using the "default:" property), however, the generated
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
@@ -56,26 +83,37 @@ class AssetBulkUpdateDto {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is AssetBulkUpdateDto &&
|
bool operator ==(Object other) => identical(this, other) || other is AssetBulkUpdateDto &&
|
||||||
|
other.dateTimeOriginal == dateTimeOriginal &&
|
||||||
other.ids == ids &&
|
other.ids == ids &&
|
||||||
other.isArchived == isArchived &&
|
other.isArchived == isArchived &&
|
||||||
other.isFavorite == isFavorite &&
|
other.isFavorite == isFavorite &&
|
||||||
|
other.latitude == latitude &&
|
||||||
|
other.longitude == longitude &&
|
||||||
other.removeParent == removeParent &&
|
other.removeParent == removeParent &&
|
||||||
other.stackParentId == stackParentId;
|
other.stackParentId == stackParentId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
|
(dateTimeOriginal == null ? 0 : dateTimeOriginal!.hashCode) +
|
||||||
(ids.hashCode) +
|
(ids.hashCode) +
|
||||||
(isArchived == null ? 0 : isArchived!.hashCode) +
|
(isArchived == null ? 0 : isArchived!.hashCode) +
|
||||||
(isFavorite == null ? 0 : isFavorite!.hashCode) +
|
(isFavorite == null ? 0 : isFavorite!.hashCode) +
|
||||||
|
(latitude == null ? 0 : latitude!.hashCode) +
|
||||||
|
(longitude == null ? 0 : longitude!.hashCode) +
|
||||||
(removeParent == null ? 0 : removeParent!.hashCode) +
|
(removeParent == null ? 0 : removeParent!.hashCode) +
|
||||||
(stackParentId == null ? 0 : stackParentId!.hashCode);
|
(stackParentId == null ? 0 : stackParentId!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'AssetBulkUpdateDto[ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, removeParent=$removeParent, stackParentId=$stackParentId]';
|
String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, removeParent=$removeParent, stackParentId=$stackParentId]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
|
if (this.dateTimeOriginal != null) {
|
||||||
|
json[r'dateTimeOriginal'] = this.dateTimeOriginal;
|
||||||
|
} else {
|
||||||
|
// json[r'dateTimeOriginal'] = null;
|
||||||
|
}
|
||||||
json[r'ids'] = this.ids;
|
json[r'ids'] = this.ids;
|
||||||
if (this.isArchived != null) {
|
if (this.isArchived != null) {
|
||||||
json[r'isArchived'] = this.isArchived;
|
json[r'isArchived'] = this.isArchived;
|
||||||
@@ -87,6 +125,16 @@ class AssetBulkUpdateDto {
|
|||||||
} else {
|
} else {
|
||||||
// json[r'isFavorite'] = null;
|
// json[r'isFavorite'] = null;
|
||||||
}
|
}
|
||||||
|
if (this.latitude != null) {
|
||||||
|
json[r'latitude'] = this.latitude;
|
||||||
|
} else {
|
||||||
|
// json[r'latitude'] = null;
|
||||||
|
}
|
||||||
|
if (this.longitude != null) {
|
||||||
|
json[r'longitude'] = this.longitude;
|
||||||
|
} else {
|
||||||
|
// json[r'longitude'] = null;
|
||||||
|
}
|
||||||
if (this.removeParent != null) {
|
if (this.removeParent != null) {
|
||||||
json[r'removeParent'] = this.removeParent;
|
json[r'removeParent'] = this.removeParent;
|
||||||
} else {
|
} else {
|
||||||
@@ -108,11 +156,18 @@ class AssetBulkUpdateDto {
|
|||||||
final json = value.cast<String, dynamic>();
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
return AssetBulkUpdateDto(
|
return AssetBulkUpdateDto(
|
||||||
|
dateTimeOriginal: mapValueOfType<String>(json, r'dateTimeOriginal'),
|
||||||
ids: json[r'ids'] is List
|
ids: json[r'ids'] is List
|
||||||
? (json[r'ids'] as List).cast<String>()
|
? (json[r'ids'] as List).cast<String>()
|
||||||
: const [],
|
: const [],
|
||||||
isArchived: mapValueOfType<bool>(json, r'isArchived'),
|
isArchived: mapValueOfType<bool>(json, r'isArchived'),
|
||||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
||||||
|
latitude: json[r'latitude'] == null
|
||||||
|
? null
|
||||||
|
: num.parse(json[r'latitude'].toString()),
|
||||||
|
longitude: json[r'longitude'] == null
|
||||||
|
? null
|
||||||
|
: num.parse(json[r'longitude'].toString()),
|
||||||
removeParent: mapValueOfType<bool>(json, r'removeParent'),
|
removeParent: mapValueOfType<bool>(json, r'removeParent'),
|
||||||
stackParentId: mapValueOfType<String>(json, r'stackParentId'),
|
stackParentId: mapValueOfType<String>(json, r'stackParentId'),
|
||||||
);
|
);
|
||||||
|
|||||||
+58
-3
@@ -13,11 +13,22 @@ part of openapi.api;
|
|||||||
class UpdateAssetDto {
|
class UpdateAssetDto {
|
||||||
/// Returns a new [UpdateAssetDto] instance.
|
/// Returns a new [UpdateAssetDto] instance.
|
||||||
UpdateAssetDto({
|
UpdateAssetDto({
|
||||||
|
this.dateTimeOriginal,
|
||||||
this.description,
|
this.description,
|
||||||
this.isArchived,
|
this.isArchived,
|
||||||
this.isFavorite,
|
this.isFavorite,
|
||||||
|
this.latitude,
|
||||||
|
this.longitude,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
String? dateTimeOriginal;
|
||||||
|
|
||||||
///
|
///
|
||||||
/// 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
|
||||||
/// does not include a default value (using the "default:" property), however, the generated
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
@@ -42,24 +53,51 @@ class UpdateAssetDto {
|
|||||||
///
|
///
|
||||||
bool? isFavorite;
|
bool? isFavorite;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
num? latitude;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
num? longitude;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is UpdateAssetDto &&
|
bool operator ==(Object other) => identical(this, other) || other is UpdateAssetDto &&
|
||||||
|
other.dateTimeOriginal == dateTimeOriginal &&
|
||||||
other.description == description &&
|
other.description == description &&
|
||||||
other.isArchived == isArchived &&
|
other.isArchived == isArchived &&
|
||||||
other.isFavorite == isFavorite;
|
other.isFavorite == isFavorite &&
|
||||||
|
other.latitude == latitude &&
|
||||||
|
other.longitude == longitude;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
|
(dateTimeOriginal == null ? 0 : dateTimeOriginal!.hashCode) +
|
||||||
(description == null ? 0 : description!.hashCode) +
|
(description == null ? 0 : description!.hashCode) +
|
||||||
(isArchived == null ? 0 : isArchived!.hashCode) +
|
(isArchived == null ? 0 : isArchived!.hashCode) +
|
||||||
(isFavorite == null ? 0 : isFavorite!.hashCode);
|
(isFavorite == null ? 0 : isFavorite!.hashCode) +
|
||||||
|
(latitude == null ? 0 : latitude!.hashCode) +
|
||||||
|
(longitude == null ? 0 : longitude!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'UpdateAssetDto[description=$description, isArchived=$isArchived, isFavorite=$isFavorite]';
|
String toString() => 'UpdateAssetDto[dateTimeOriginal=$dateTimeOriginal, description=$description, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
|
if (this.dateTimeOriginal != null) {
|
||||||
|
json[r'dateTimeOriginal'] = this.dateTimeOriginal;
|
||||||
|
} else {
|
||||||
|
// json[r'dateTimeOriginal'] = null;
|
||||||
|
}
|
||||||
if (this.description != null) {
|
if (this.description != null) {
|
||||||
json[r'description'] = this.description;
|
json[r'description'] = this.description;
|
||||||
} else {
|
} else {
|
||||||
@@ -75,6 +113,16 @@ class UpdateAssetDto {
|
|||||||
} else {
|
} else {
|
||||||
// json[r'isFavorite'] = null;
|
// json[r'isFavorite'] = null;
|
||||||
}
|
}
|
||||||
|
if (this.latitude != null) {
|
||||||
|
json[r'latitude'] = this.latitude;
|
||||||
|
} else {
|
||||||
|
// json[r'latitude'] = null;
|
||||||
|
}
|
||||||
|
if (this.longitude != null) {
|
||||||
|
json[r'longitude'] = this.longitude;
|
||||||
|
} else {
|
||||||
|
// json[r'longitude'] = null;
|
||||||
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,9 +134,16 @@ class UpdateAssetDto {
|
|||||||
final json = value.cast<String, dynamic>();
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
return UpdateAssetDto(
|
return UpdateAssetDto(
|
||||||
|
dateTimeOriginal: mapValueOfType<String>(json, r'dateTimeOriginal'),
|
||||||
description: mapValueOfType<String>(json, r'description'),
|
description: mapValueOfType<String>(json, r'description'),
|
||||||
isArchived: mapValueOfType<bool>(json, r'isArchived'),
|
isArchived: mapValueOfType<bool>(json, r'isArchived'),
|
||||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
||||||
|
latitude: json[r'latitude'] == null
|
||||||
|
? null
|
||||||
|
: num.parse(json[r'latitude'].toString()),
|
||||||
|
longitude: json[r'longitude'] == null
|
||||||
|
? null
|
||||||
|
: num.parse(json[r'longitude'].toString()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+15
@@ -16,6 +16,11 @@ void main() {
|
|||||||
// final instance = AssetBulkUpdateDto();
|
// final instance = AssetBulkUpdateDto();
|
||||||
|
|
||||||
group('test AssetBulkUpdateDto', () {
|
group('test AssetBulkUpdateDto', () {
|
||||||
|
// String dateTimeOriginal
|
||||||
|
test('to test the property `dateTimeOriginal`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
// List<String> ids (default value: const [])
|
// List<String> ids (default value: const [])
|
||||||
test('to test the property `ids`', () async {
|
test('to test the property `ids`', () async {
|
||||||
// TODO
|
// TODO
|
||||||
@@ -31,6 +36,16 @@ void main() {
|
|||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// num latitude
|
||||||
|
test('to test the property `latitude`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// num longitude
|
||||||
|
test('to test the property `longitude`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
// bool removeParent
|
// bool removeParent
|
||||||
test('to test the property `removeParent`', () async {
|
test('to test the property `removeParent`', () async {
|
||||||
// TODO
|
// TODO
|
||||||
|
|||||||
+15
@@ -16,6 +16,11 @@ void main() {
|
|||||||
// final instance = UpdateAssetDto();
|
// final instance = UpdateAssetDto();
|
||||||
|
|
||||||
group('test UpdateAssetDto', () {
|
group('test UpdateAssetDto', () {
|
||||||
|
// String dateTimeOriginal
|
||||||
|
test('to test the property `dateTimeOriginal`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
// String description
|
// String description
|
||||||
test('to test the property `description`', () async {
|
test('to test the property `description`', () async {
|
||||||
// TODO
|
// TODO
|
||||||
@@ -31,6 +36,16 @@ void main() {
|
|||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// num latitude
|
||||||
|
test('to test the property `latitude`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// num longitude
|
||||||
|
test('to test the property `longitude`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6404,6 +6404,9 @@
|
|||||||
},
|
},
|
||||||
"AssetBulkUpdateDto": {
|
"AssetBulkUpdateDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"dateTimeOriginal": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"ids": {
|
"ids": {
|
||||||
"items": {
|
"items": {
|
||||||
"format": "uuid",
|
"format": "uuid",
|
||||||
@@ -6417,6 +6420,12 @@
|
|||||||
"isFavorite": {
|
"isFavorite": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"latitude": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"longitude": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
"removeParent": {
|
"removeParent": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
@@ -9311,6 +9320,9 @@
|
|||||||
},
|
},
|
||||||
"UpdateAssetDto": {
|
"UpdateAssetDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"dateTimeOriginal": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -9319,6 +9331,12 @@
|
|||||||
},
|
},
|
||||||
"isFavorite": {
|
"isFavorite": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"latitude": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"longitude": {
|
||||||
|
"type": "number"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { AccessCore, Permission } from '../access';
|
|||||||
import { AuthUserDto } from '../auth';
|
import { AuthUserDto } from '../auth';
|
||||||
import { mimeTypes } from '../domain.constant';
|
import { mimeTypes } from '../domain.constant';
|
||||||
import { HumanReadableSize, usePagination } from '../domain.util';
|
import { HumanReadableSize, usePagination } from '../domain.util';
|
||||||
import { IAssetDeletionJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job';
|
import { IAssetDeletionJob, ISidecarWriteJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job';
|
||||||
import {
|
import {
|
||||||
CommunicationEvent,
|
CommunicationEvent,
|
||||||
IAccessRepository,
|
IAccessRepository,
|
||||||
@@ -389,10 +389,8 @@ export class AssetService {
|
|||||||
async update(authUser: AuthUserDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
|
async update(authUser: AuthUserDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
|
||||||
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, id);
|
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, id);
|
||||||
|
|
||||||
const { description, ...rest } = dto;
|
const { description, dateTimeOriginal, latitude, longitude, ...rest } = dto;
|
||||||
if (description !== undefined) {
|
await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude });
|
||||||
await this.assetRepository.upsertExif({ assetId: id, description });
|
|
||||||
}
|
|
||||||
|
|
||||||
const asset = await this.assetRepository.save({ id, ...rest });
|
const asset = await this.assetRepository.save({ id, ...rest });
|
||||||
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids: [id] } });
|
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids: [id] } });
|
||||||
@@ -400,7 +398,7 @@ export class AssetService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateAll(authUser: AuthUserDto, dto: AssetBulkUpdateDto): Promise<void> {
|
async updateAll(authUser: AuthUserDto, dto: AssetBulkUpdateDto): Promise<void> {
|
||||||
const { ids, removeParent, ...options } = dto;
|
const { ids, removeParent, dateTimeOriginal, latitude, longitude, ...options } = dto;
|
||||||
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, ids);
|
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, ids);
|
||||||
|
|
||||||
if (removeParent) {
|
if (removeParent) {
|
||||||
@@ -420,6 +418,10 @@ export class AssetService {
|
|||||||
await this.assetRepository.updateAll([options.stackParentId], { stackParentId: null });
|
await this.assetRepository.updateAll([options.stackParentId], { stackParentId: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const id of ids) {
|
||||||
|
await this.updateMetadata({ id, dateTimeOriginal, latitude, longitude });
|
||||||
|
}
|
||||||
|
|
||||||
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids } });
|
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids } });
|
||||||
await this.assetRepository.updateAll(ids, options);
|
await this.assetRepository.updateAll(ids, options);
|
||||||
this.communicationRepository.send(CommunicationEvent.ASSET_UPDATE, authUser.id, ids);
|
this.communicationRepository.send(CommunicationEvent.ASSET_UPDATE, authUser.id, ids);
|
||||||
@@ -583,4 +585,17 @@ export class AssetService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async updateMetadata(dto: ISidecarWriteJob & { description?: string }) {
|
||||||
|
const { id, description, dateTimeOriginal, latitude, longitude } = dto;
|
||||||
|
|
||||||
|
if (description !== undefined) {
|
||||||
|
await this.assetRepository.upsertExif({ assetId: id, description });
|
||||||
|
}
|
||||||
|
|
||||||
|
const writes = _.omitBy({ dateTimeOriginal, latitude, longitude }, _.isUndefined);
|
||||||
|
if (Object.keys(writes).length > 0) {
|
||||||
|
await this.jobRepository.queue({ name: JobName.SIDECAR_WRITE, data: { id, ...writes } });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,19 @@
|
|||||||
import { AssetType } from '@app/infra/entities';
|
import { AssetType } from '@app/infra/entities';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { IsBoolean, IsEnum, IsInt, IsPositive, IsString, Min } from 'class-validator';
|
import {
|
||||||
|
IsBoolean,
|
||||||
|
IsDateString,
|
||||||
|
IsEnum,
|
||||||
|
IsInt,
|
||||||
|
IsLatitude,
|
||||||
|
IsLongitude,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsPositive,
|
||||||
|
IsString,
|
||||||
|
Min,
|
||||||
|
ValidateIf,
|
||||||
|
} from 'class-validator';
|
||||||
import { Optional, QueryBoolean, QueryDate, ValidateUUID } from '../../domain.util';
|
import { Optional, QueryBoolean, QueryDate, ValidateUUID } from '../../domain.util';
|
||||||
import { BulkIdsDto } from '../response-dto';
|
import { BulkIdsDto } from '../response-dto';
|
||||||
|
|
||||||
@@ -10,6 +22,10 @@ export enum AssetOrder {
|
|||||||
DESC = 'desc',
|
DESC = 'desc',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasGPS = (o: { latitude: undefined; longitude: undefined }) =>
|
||||||
|
o.latitude !== undefined || o.longitude !== undefined;
|
||||||
|
const ValidateGPS = () => ValidateIf(hasGPS);
|
||||||
|
|
||||||
export class AssetSearchDto {
|
export class AssetSearchDto {
|
||||||
@ValidateUUID({ optional: true })
|
@ValidateUUID({ optional: true })
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -172,6 +188,20 @@ export class AssetBulkUpdateDto extends BulkIdsDto {
|
|||||||
@Optional()
|
@Optional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
removeParent?: boolean;
|
removeParent?: boolean;
|
||||||
|
|
||||||
|
@Optional()
|
||||||
|
@IsDateString()
|
||||||
|
dateTimeOriginal?: string;
|
||||||
|
|
||||||
|
@ValidateGPS()
|
||||||
|
@IsLatitude()
|
||||||
|
@IsNotEmpty()
|
||||||
|
latitude?: number;
|
||||||
|
|
||||||
|
@ValidateGPS()
|
||||||
|
@IsLongitude()
|
||||||
|
@IsNotEmpty()
|
||||||
|
longitude?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateAssetDto {
|
export class UpdateAssetDto {
|
||||||
@@ -186,6 +216,20 @@ export class UpdateAssetDto {
|
|||||||
@Optional()
|
@Optional()
|
||||||
@IsString()
|
@IsString()
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
|
@Optional()
|
||||||
|
@IsDateString()
|
||||||
|
dateTimeOriginal?: string;
|
||||||
|
|
||||||
|
@ValidateGPS()
|
||||||
|
@IsLatitude()
|
||||||
|
@IsNotEmpty()
|
||||||
|
latitude?: number;
|
||||||
|
|
||||||
|
@ValidateGPS()
|
||||||
|
@IsLongitude()
|
||||||
|
@IsNotEmpty()
|
||||||
|
longitude?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RandomAssetsDto {
|
export class RandomAssetsDto {
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ export enum JobName {
|
|||||||
QUEUE_SIDECAR = 'queue-sidecar',
|
QUEUE_SIDECAR = 'queue-sidecar',
|
||||||
SIDECAR_DISCOVERY = 'sidecar-discovery',
|
SIDECAR_DISCOVERY = 'sidecar-discovery',
|
||||||
SIDECAR_SYNC = 'sidecar-sync',
|
SIDECAR_SYNC = 'sidecar-sync',
|
||||||
|
SIDECAR_WRITE = 'sidecar-write',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const JOBS_ASSET_PAGINATION_SIZE = 1000;
|
export const JOBS_ASSET_PAGINATION_SIZE = 1000;
|
||||||
@@ -168,6 +169,7 @@ export const JOBS_TO_QUEUE: Record<JobName, QueueName> = {
|
|||||||
[JobName.QUEUE_SIDECAR]: QueueName.SIDECAR,
|
[JobName.QUEUE_SIDECAR]: QueueName.SIDECAR,
|
||||||
[JobName.SIDECAR_DISCOVERY]: QueueName.SIDECAR,
|
[JobName.SIDECAR_DISCOVERY]: QueueName.SIDECAR,
|
||||||
[JobName.SIDECAR_SYNC]: QueueName.SIDECAR,
|
[JobName.SIDECAR_SYNC]: QueueName.SIDECAR,
|
||||||
|
[JobName.SIDECAR_WRITE]: QueueName.SIDECAR,
|
||||||
|
|
||||||
// Library management
|
// Library management
|
||||||
[JobName.LIBRARY_SCAN_ASSET]: QueueName.LIBRARY,
|
[JobName.LIBRARY_SCAN_ASSET]: QueueName.LIBRARY,
|
||||||
|
|||||||
@@ -33,3 +33,9 @@ export interface IBulkEntityJob extends IBaseJob {
|
|||||||
export interface IDeleteFilesJob extends IBaseJob {
|
export interface IDeleteFilesJob extends IBaseJob {
|
||||||
files: Array<string | null | undefined>;
|
files: Array<string | null | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ISidecarWriteJob extends IEntityJob {
|
||||||
|
dateTimeOriginal?: string;
|
||||||
|
latitude?: number;
|
||||||
|
longitude?: number;
|
||||||
|
}
|
||||||
|
|||||||
@@ -253,11 +253,11 @@ describe(MetadataService.name, () => {
|
|||||||
const originalDate = new Date('2023-11-21T16:13:17.517Z');
|
const originalDate = new Date('2023-11-21T16:13:17.517Z');
|
||||||
const sidecarDate = new Date('2022-01-01T00:00:00.000Z');
|
const sidecarDate = new Date('2022-01-01T00:00:00.000Z');
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.sidecar]);
|
assetMock.getByIds.mockResolvedValue([assetStub.sidecar]);
|
||||||
when(metadataMock.getExifTags)
|
when(metadataMock.readTags)
|
||||||
.calledWith(assetStub.sidecar.originalPath)
|
.calledWith(assetStub.sidecar.originalPath)
|
||||||
// higher priority tag
|
// higher priority tag
|
||||||
.mockResolvedValue({ CreationDate: originalDate.toISOString() });
|
.mockResolvedValue({ CreationDate: originalDate.toISOString() });
|
||||||
when(metadataMock.getExifTags)
|
when(metadataMock.readTags)
|
||||||
.calledWith(assetStub.sidecar.sidecarPath as string)
|
.calledWith(assetStub.sidecar.sidecarPath as string)
|
||||||
// lower priority tag, but in sidecar
|
// lower priority tag, but in sidecar
|
||||||
.mockResolvedValue({ CreateDate: sidecarDate.toISOString() });
|
.mockResolvedValue({ CreateDate: sidecarDate.toISOString() });
|
||||||
@@ -275,7 +275,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should handle lists of numbers', async () => {
|
it('should handle lists of numbers', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
metadataMock.getExifTags.mockResolvedValue({ ISO: [160] as any });
|
metadataMock.readTags.mockResolvedValue({ ISO: [160] as any });
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
||||||
@@ -292,7 +292,7 @@ describe(MetadataService.name, () => {
|
|||||||
assetMock.getByIds.mockResolvedValue([assetStub.withLocation]);
|
assetMock.getByIds.mockResolvedValue([assetStub.withLocation]);
|
||||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.REVERSE_GEOCODING_ENABLED, value: true }]);
|
configMock.load.mockResolvedValue([{ key: SystemConfigKey.REVERSE_GEOCODING_ENABLED, value: true }]);
|
||||||
metadataMock.reverseGeocode.mockResolvedValue({ city: 'City', state: 'State', country: 'Country' });
|
metadataMock.reverseGeocode.mockResolvedValue({ city: 'City', state: 'State', country: 'Country' });
|
||||||
metadataMock.getExifTags.mockResolvedValue({
|
metadataMock.readTags.mockResolvedValue({
|
||||||
GPSLatitude: assetStub.withLocation.exifInfo!.latitude!,
|
GPSLatitude: assetStub.withLocation.exifInfo!.latitude!,
|
||||||
GPSLongitude: assetStub.withLocation.exifInfo!.longitude!,
|
GPSLongitude: assetStub.withLocation.exifInfo!.longitude!,
|
||||||
});
|
});
|
||||||
@@ -324,7 +324,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should apply motion photos', async () => {
|
it('should apply motion photos', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoStillAsset, livePhotoVideoId: null }]);
|
assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoStillAsset, livePhotoVideoId: null }]);
|
||||||
metadataMock.getExifTags.mockResolvedValue({
|
metadataMock.readTags.mockResolvedValue({
|
||||||
Directory: 'foo/bar/',
|
Directory: 'foo/bar/',
|
||||||
MotionPhoto: 1,
|
MotionPhoto: 1,
|
||||||
MicroVideo: 1,
|
MicroVideo: 1,
|
||||||
@@ -345,7 +345,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should create new motion asset if not found and link it with the photo', async () => {
|
it('should create new motion asset if not found and link it with the photo', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoStillAsset, livePhotoVideoId: null }]);
|
assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoStillAsset, livePhotoVideoId: null }]);
|
||||||
metadataMock.getExifTags.mockResolvedValue({
|
metadataMock.readTags.mockResolvedValue({
|
||||||
Directory: 'foo/bar/',
|
Directory: 'foo/bar/',
|
||||||
MotionPhoto: 1,
|
MotionPhoto: 1,
|
||||||
MicroVideo: 1,
|
MicroVideo: 1,
|
||||||
@@ -402,7 +402,7 @@ describe(MetadataService.name, () => {
|
|||||||
tz: '+02:00',
|
tz: '+02:00',
|
||||||
};
|
};
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
metadataMock.getExifTags.mockResolvedValue(tags);
|
metadataMock.readTags.mockResolvedValue(tags);
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
||||||
@@ -441,7 +441,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should handle duration', async () => {
|
it('should handle duration', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
metadataMock.getExifTags.mockResolvedValue({ Duration: 6.21 });
|
metadataMock.readTags.mockResolvedValue({ Duration: 6.21 });
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
|
|
||||||
@@ -457,7 +457,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should handle duration as an object without Scale', async () => {
|
it('should handle duration as an object without Scale', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
metadataMock.getExifTags.mockResolvedValue({ Duration: { Value: 6.2 } });
|
metadataMock.readTags.mockResolvedValue({ Duration: { Value: 6.2 } });
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
|
|
||||||
@@ -473,7 +473,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should handle duration with scale', async () => {
|
it('should handle duration with scale', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
metadataMock.getExifTags.mockResolvedValue({ Duration: { Scale: 1.11111111111111e-5, Value: 558720 } });
|
metadataMock.readTags.mockResolvedValue({ Duration: { Scale: 1.11111111111111e-5, Value: 558720 } });
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import { Inject, Injectable, Logger } from '@nestjs/common';
|
|||||||
import { ExifDateTime, Tags } from 'exiftool-vendored';
|
import { ExifDateTime, Tags } from 'exiftool-vendored';
|
||||||
import { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime';
|
import { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime';
|
||||||
import { constants } from 'fs/promises';
|
import { constants } from 'fs/promises';
|
||||||
|
import _ from 'lodash';
|
||||||
import { Duration } from 'luxon';
|
import { Duration } from 'luxon';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { usePagination } from '../domain.util';
|
import { usePagination } from '../domain.util';
|
||||||
import { IBaseJob, IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from '../job';
|
import { IBaseJob, IEntityJob, ISidecarWriteJob, JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from '../job';
|
||||||
import {
|
import {
|
||||||
ExifDuration,
|
ExifDuration,
|
||||||
IAlbumRepository,
|
IAlbumRepository,
|
||||||
@@ -251,6 +252,38 @@ export class MetadataService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleSidecarWrite(job: ISidecarWriteJob) {
|
||||||
|
const { id, dateTimeOriginal, latitude, longitude } = job;
|
||||||
|
const asset = await this.assetRepository.getById(id);
|
||||||
|
if (!asset) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sidecarPath = asset.sidecarPath || `${asset.originalPath}.xmp`;
|
||||||
|
const exif = _.omitBy(
|
||||||
|
{
|
||||||
|
CreationDate: dateTimeOriginal,
|
||||||
|
GPSLatitude: latitude,
|
||||||
|
GPSLongitude: longitude,
|
||||||
|
},
|
||||||
|
_.isUndefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Object.keys(exif).length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.repository.writeTags(sidecarPath, exif);
|
||||||
|
|
||||||
|
if (!asset.sidecarPath) {
|
||||||
|
await this.assetRepository.save({ id, sidecarPath });
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: id } });
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private async applyReverseGeocoding(asset: AssetEntity, exifData: ExifEntityWithoutGeocodeAndTypeOrm) {
|
private async applyReverseGeocoding(asset: AssetEntity, exifData: ExifEntityWithoutGeocodeAndTypeOrm) {
|
||||||
const { latitude, longitude } = exifData;
|
const { latitude, longitude } = exifData;
|
||||||
if (!(await this.configCore.hasFeature(FeatureFlag.REVERSE_GEOCODING)) || !longitude || !latitude) {
|
if (!(await this.configCore.hasFeature(FeatureFlag.REVERSE_GEOCODING)) || !longitude || !latitude) {
|
||||||
@@ -350,8 +383,8 @@ export class MetadataService {
|
|||||||
asset: AssetEntity,
|
asset: AssetEntity,
|
||||||
): Promise<{ exifData: ExifEntityWithoutGeocodeAndTypeOrm; tags: ImmichTags }> {
|
): Promise<{ exifData: ExifEntityWithoutGeocodeAndTypeOrm; tags: ImmichTags }> {
|
||||||
const stats = await this.storageRepository.stat(asset.originalPath);
|
const stats = await this.storageRepository.stat(asset.originalPath);
|
||||||
const mediaTags = await this.repository.getExifTags(asset.originalPath);
|
const mediaTags = await this.repository.readTags(asset.originalPath);
|
||||||
const sidecarTags = asset.sidecarPath ? await this.repository.getExifTags(asset.sidecarPath) : null;
|
const sidecarTags = asset.sidecarPath ? await this.repository.readTags(asset.sidecarPath) : null;
|
||||||
|
|
||||||
// ensure date from sidecar is used if present
|
// ensure date from sidecar is used if present
|
||||||
const hasDateOverride = !!this.getDateTimeOriginal(sidecarTags);
|
const hasDateOverride = !!this.getDateTimeOriginal(sidecarTags);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
IEntityJob,
|
IEntityJob,
|
||||||
ILibraryFileJob,
|
ILibraryFileJob,
|
||||||
ILibraryRefreshJob,
|
ILibraryRefreshJob,
|
||||||
|
ISidecarWriteJob,
|
||||||
} from '../job/job.interface';
|
} from '../job/job.interface';
|
||||||
|
|
||||||
export interface JobCounts {
|
export interface JobCounts {
|
||||||
@@ -54,7 +55,7 @@ export type JobItem =
|
|||||||
| { name: JobName.QUEUE_METADATA_EXTRACTION; data: IBaseJob }
|
| { name: JobName.QUEUE_METADATA_EXTRACTION; data: IBaseJob }
|
||||||
| { name: JobName.METADATA_EXTRACTION; data: IEntityJob }
|
| { name: JobName.METADATA_EXTRACTION; data: IEntityJob }
|
||||||
| { name: JobName.LINK_LIVE_PHOTOS; data: IEntityJob }
|
| { name: JobName.LINK_LIVE_PHOTOS; data: IEntityJob }
|
||||||
|
| { name: JobName.SIDECAR_WRITE; data: ISidecarWriteJob }
|
||||||
// Sidecar Scanning
|
// Sidecar Scanning
|
||||||
| { name: JobName.QUEUE_SIDECAR; data: IBaseJob }
|
| { name: JobName.QUEUE_SIDECAR; data: IBaseJob }
|
||||||
| { name: JobName.SIDECAR_DISCOVERY; data: IEntityJob }
|
| { name: JobName.SIDECAR_DISCOVERY; data: IEntityJob }
|
||||||
|
|||||||
@@ -35,5 +35,6 @@ export interface IMetadataRepository {
|
|||||||
teardown(): Promise<void>;
|
teardown(): Promise<void>;
|
||||||
reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult>;
|
reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult>;
|
||||||
deleteCache(): Promise<void>;
|
deleteCache(): Promise<void>;
|
||||||
getExifTags(path: string): Promise<ImmichTags | null>;
|
readTags(path: string): Promise<ImmichTags | null>;
|
||||||
|
writeTags(path: string, tags: Partial<Tags>): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { GeoPoint, IMetadataRepository, ImmichTags, ReverseGeocodeResult } from '@app/domain';
|
import { GeoPoint, IMetadataRepository, ImmichTags, ReverseGeocodeResult } from '@app/domain';
|
||||||
import { REVERSE_GEOCODING_DUMP_DIRECTORY } from '@app/infra';
|
import { REVERSE_GEOCODING_DUMP_DIRECTORY } from '@app/infra';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { DefaultReadTaskOptions, exiftool } from 'exiftool-vendored';
|
import { DefaultReadTaskOptions, Tags, exiftool } from 'exiftool-vendored';
|
||||||
import { readdir, rm } from 'fs/promises';
|
import { readdir, rm } from 'fs/promises';
|
||||||
import * as geotz from 'geo-tz';
|
import * as geotz from 'geo-tz';
|
||||||
import { getName } from 'i18n-iso-countries';
|
import { getName } from 'i18n-iso-countries';
|
||||||
@@ -76,7 +76,7 @@ export class MetadataRepository implements IMetadataRepository {
|
|||||||
return { country, state, city };
|
return { country, state, city };
|
||||||
}
|
}
|
||||||
|
|
||||||
getExifTags(path: string): Promise<ImmichTags | null> {
|
readTags(path: string): Promise<ImmichTags | null> {
|
||||||
return exiftool
|
return exiftool
|
||||||
.read(path, undefined, {
|
.read(path, undefined, {
|
||||||
...DefaultReadTaskOptions,
|
...DefaultReadTaskOptions,
|
||||||
@@ -93,4 +93,8 @@ export class MetadataRepository implements IMetadataRepository {
|
|||||||
return null;
|
return null;
|
||||||
}) as Promise<ImmichTags | null>;
|
}) as Promise<ImmichTags | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async writeTags(path: string, tags: Partial<Tags>): Promise<void> {
|
||||||
|
await exiftool.write(path, tags, ['-overwrite_original']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ export class AppService {
|
|||||||
[JobName.QUEUE_SIDECAR]: (data) => this.metadataService.handleQueueSidecar(data),
|
[JobName.QUEUE_SIDECAR]: (data) => this.metadataService.handleQueueSidecar(data),
|
||||||
[JobName.SIDECAR_DISCOVERY]: (data) => this.metadataService.handleSidecarDiscovery(data),
|
[JobName.SIDECAR_DISCOVERY]: (data) => this.metadataService.handleSidecarDiscovery(data),
|
||||||
[JobName.SIDECAR_SYNC]: () => this.metadataService.handleSidecarSync(),
|
[JobName.SIDECAR_SYNC]: () => this.metadataService.handleSidecarSync(),
|
||||||
|
[JobName.SIDECAR_WRITE]: (data) => this.metadataService.handleSidecarWrite(data),
|
||||||
[JobName.LIBRARY_SCAN_ASSET]: (data) => this.libraryService.handleAssetRefresh(data),
|
[JobName.LIBRARY_SCAN_ASSET]: (data) => this.libraryService.handleAssetRefresh(data),
|
||||||
[JobName.LIBRARY_SCAN]: (data) => this.libraryService.handleQueueAssetRefresh(data),
|
[JobName.LIBRARY_SCAN]: (data) => this.libraryService.handleQueueAssetRefresh(data),
|
||||||
[JobName.LIBRARY_DELETE]: (data) => this.libraryService.handleDeleteLibrary(data),
|
[JobName.LIBRARY_DELETE]: (data) => this.libraryService.handleDeleteLibrary(data),
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import { IMetadataRepository } from '@app/domain';
|
|||||||
export const newMetadataRepositoryMock = (): jest.Mocked<IMetadataRepository> => {
|
export const newMetadataRepositoryMock = (): jest.Mocked<IMetadataRepository> => {
|
||||||
return {
|
return {
|
||||||
deleteCache: jest.fn(),
|
deleteCache: jest.fn(),
|
||||||
getExifTags: jest.fn(),
|
|
||||||
init: jest.fn(),
|
init: jest.fn(),
|
||||||
teardown: jest.fn(),
|
teardown: jest.fn(),
|
||||||
reverseGeocode: jest.fn(),
|
reverseGeocode: jest.fn(),
|
||||||
|
readTags: jest.fn(),
|
||||||
|
writeTags: jest.fn(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
+36
@@ -447,6 +447,12 @@ export interface AssetBulkDeleteDto {
|
|||||||
* @interface AssetBulkUpdateDto
|
* @interface AssetBulkUpdateDto
|
||||||
*/
|
*/
|
||||||
export interface AssetBulkUpdateDto {
|
export interface AssetBulkUpdateDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof AssetBulkUpdateDto
|
||||||
|
*/
|
||||||
|
'dateTimeOriginal'?: string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {Array<string>}
|
* @type {Array<string>}
|
||||||
@@ -465,6 +471,18 @@ export interface AssetBulkUpdateDto {
|
|||||||
* @memberof AssetBulkUpdateDto
|
* @memberof AssetBulkUpdateDto
|
||||||
*/
|
*/
|
||||||
'isFavorite'?: boolean;
|
'isFavorite'?: boolean;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof AssetBulkUpdateDto
|
||||||
|
*/
|
||||||
|
'latitude'?: number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof AssetBulkUpdateDto
|
||||||
|
*/
|
||||||
|
'longitude'?: number;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
@@ -4161,6 +4179,12 @@ export interface UpdateAlbumDto {
|
|||||||
* @interface UpdateAssetDto
|
* @interface UpdateAssetDto
|
||||||
*/
|
*/
|
||||||
export interface UpdateAssetDto {
|
export interface UpdateAssetDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof UpdateAssetDto
|
||||||
|
*/
|
||||||
|
'dateTimeOriginal'?: string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@@ -4179,6 +4203,18 @@ export interface UpdateAssetDto {
|
|||||||
* @memberof UpdateAssetDto
|
* @memberof UpdateAssetDto
|
||||||
*/
|
*/
|
||||||
'isFavorite'?: boolean;
|
'isFavorite'?: boolean;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof UpdateAssetDto
|
||||||
|
*/
|
||||||
|
'latitude'?: number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof UpdateAssetDto
|
||||||
|
*/
|
||||||
|
'longitude'?: number;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -10,17 +10,24 @@
|
|||||||
import { asByteUnitString } from '../../utils/byte-units';
|
import { asByteUnitString } from '../../utils/byte-units';
|
||||||
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
|
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
|
||||||
import UserAvatar from '../shared-components/user-avatar.svelte';
|
import UserAvatar from '../shared-components/user-avatar.svelte';
|
||||||
|
import ChangeDate from '$lib/components/shared-components/change-date.svelte';
|
||||||
import {
|
import {
|
||||||
mdiCalendar,
|
mdiCalendar,
|
||||||
mdiCameraIris,
|
mdiCameraIris,
|
||||||
mdiClose,
|
mdiClose,
|
||||||
|
mdiPencil,
|
||||||
mdiImageOutline,
|
mdiImageOutline,
|
||||||
mdiMapMarkerOutline,
|
mdiMapMarkerOutline,
|
||||||
mdiInformationOutline,
|
mdiInformationOutline,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
|
import {
|
||||||
|
notificationController,
|
||||||
|
NotificationType,
|
||||||
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
import Map from '../shared-components/map/map.svelte';
|
import Map from '../shared-components/map/map.svelte';
|
||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
|
import ChangeLocation from '../shared-components/change-location.svelte';
|
||||||
|
|
||||||
export let asset: AssetResponseDto;
|
export let asset: AssetResponseDto;
|
||||||
export let albums: AlbumResponseDto[] = [];
|
export let albums: AlbumResponseDto[] = [];
|
||||||
@@ -90,6 +97,45 @@
|
|||||||
|
|
||||||
let showAssetPath = false;
|
let showAssetPath = false;
|
||||||
const toggleAssetPath = () => (showAssetPath = !showAssetPath);
|
const toggleAssetPath = () => (showAssetPath = !showAssetPath);
|
||||||
|
|
||||||
|
let isShowChangeDate = false;
|
||||||
|
|
||||||
|
async function handleConfirmChangeDate(event: CustomEvent<string>) {
|
||||||
|
isShowChangeDate = false;
|
||||||
|
if (asset.exifInfo) {
|
||||||
|
asset.exifInfo.dateTimeOriginal = event.detail;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await api.assetApi.updateAsset({
|
||||||
|
id: asset.id,
|
||||||
|
updateAssetDto: {
|
||||||
|
dateTimeOriginal: event.detail,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
notificationController.show({ message: 'Metadata updated please reload to apply', type: NotificationType.Info });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let isShowChangeLocation = false;
|
||||||
|
|
||||||
|
async function handleConfirmChangeLocation(event: CustomEvent<{ lng: number; lat: number }>) {
|
||||||
|
isShowChangeLocation = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.assetApi.updateAsset({
|
||||||
|
id: asset.id,
|
||||||
|
updateAssetDto: {
|
||||||
|
latitude: event.detail.lat,
|
||||||
|
longitude: event.detail.lng,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
notificationController.show({ message: 'Metadata updated please reload to apply', type: NotificationType.Info });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg">
|
<section class="p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg">
|
||||||
@@ -195,37 +241,101 @@
|
|||||||
{@const assetDateTimeOriginal = DateTime.fromISO(asset.exifInfo.dateTimeOriginal, {
|
{@const assetDateTimeOriginal = DateTime.fromISO(asset.exifInfo.dateTimeOriginal, {
|
||||||
zone: asset.exifInfo.timeZone ?? undefined,
|
zone: asset.exifInfo.timeZone ?? undefined,
|
||||||
})}
|
})}
|
||||||
<div class="flex gap-4 py-4">
|
<div
|
||||||
<div>
|
class="flex justify-between gap-4 py-4 hover:bg-gray-200 hover:text-black cursor-pointer"
|
||||||
<Icon path={mdiCalendar} size="24" />
|
on:click={() => (isShowChangeDate = true)}
|
||||||
</div>
|
on:keydown={(event) => event.key === 'Enter' && (isShowChangeDate = true)}
|
||||||
|
tabindex="0"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<div>
|
||||||
|
<Icon path={mdiCalendar} size="24" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p>
|
|
||||||
{assetDateTimeOriginal.toLocaleString(
|
|
||||||
{
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric',
|
|
||||||
},
|
|
||||||
{ locale: $locale },
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<div class="flex gap-2 text-sm">
|
|
||||||
<p>
|
<p>
|
||||||
{assetDateTimeOriginal.toLocaleString(
|
{assetDateTimeOriginal.toLocaleString(
|
||||||
{
|
{
|
||||||
weekday: 'short',
|
month: 'short',
|
||||||
hour: 'numeric',
|
day: 'numeric',
|
||||||
minute: '2-digit',
|
year: 'numeric',
|
||||||
timeZoneName: 'longOffset',
|
|
||||||
},
|
},
|
||||||
{ locale: $locale },
|
{ locale: $locale },
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
<div class="flex gap-2 text-sm">
|
||||||
|
<p>
|
||||||
|
{assetDateTimeOriginal.toLocaleString(
|
||||||
|
{
|
||||||
|
weekday: 'short',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit',
|
||||||
|
timeZoneName: 'longOffset',
|
||||||
|
},
|
||||||
|
{ locale: $locale },
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>{/if}
|
<button class="focus:outline-none">
|
||||||
|
<Icon path={mdiPencil} size="24" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="flex justify-between gap-4 py-4 hover:bg-gray-200 hover:text-black cursor-pointer"
|
||||||
|
on:click={() => (isShowChangeDate = true)}
|
||||||
|
on:keydown={(event) => event.key === 'Enter' && (isShowChangeDate = true)}
|
||||||
|
tabindex="0"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<div>
|
||||||
|
<Icon path={mdiCalendar} size="24" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>No date available for this asset, click to add one.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="focus:outline-none">
|
||||||
|
<Icon path={mdiPencil} size="24" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if isShowChangeDate}
|
||||||
|
{#if asset.exifInfo?.dateTimeOriginal}
|
||||||
|
{@const assetDateTimeOriginal = DateTime.fromISO(asset.exifInfo.dateTimeOriginal, {
|
||||||
|
zone: asset.exifInfo.timeZone ?? undefined,
|
||||||
|
})}
|
||||||
|
<ChangeDate
|
||||||
|
title="Change Date"
|
||||||
|
confirmText="Confirm"
|
||||||
|
initialDate={assetDateTimeOriginal}
|
||||||
|
on:confirm={handleConfirmChangeDate}
|
||||||
|
on:cancel={() => (isShowChangeDate = false)}
|
||||||
|
>
|
||||||
|
<svelte:fragment slot="prompt">
|
||||||
|
<p>Please select a new date:</p>
|
||||||
|
</svelte:fragment>
|
||||||
|
</ChangeDate>
|
||||||
|
{:else}
|
||||||
|
<ChangeDate
|
||||||
|
title="Change Date"
|
||||||
|
confirmText="Confirm"
|
||||||
|
initialDate={DateTime.now()}
|
||||||
|
on:confirm={handleConfirmChangeDate}
|
||||||
|
on:cancel={() => (isShowChangeDate = false)}
|
||||||
|
>
|
||||||
|
<svelte:fragment slot="prompt">
|
||||||
|
<p>Please select a new date:</p>
|
||||||
|
</svelte:fragment>
|
||||||
|
</ChangeDate>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if asset.exifInfo?.fileSizeInByte}
|
{#if asset.exifInfo?.fileSizeInByte}
|
||||||
<div class="flex gap-4 py-4">
|
<div class="flex gap-4 py-4">
|
||||||
@@ -293,7 +403,13 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if asset.exifInfo?.city}
|
{#if asset.exifInfo?.city}
|
||||||
<div class="flex gap-4 py-4">
|
<div
|
||||||
|
class="flex justify-between gap-4 py-4 hover:bg-gray-200 hover:text-black cursor-pointer"
|
||||||
|
on:click={() => (isShowChangeLocation = true)}
|
||||||
|
on:keydown={(event) => event.key === 'Enter' && (isShowChangeLocation = true)}
|
||||||
|
tabindex="0"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
<div><Icon path={mdiMapMarkerOutline} size="24" /></div>
|
<div><Icon path={mdiMapMarkerOutline} size="24" /></div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -309,7 +425,57 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button class="focus:outline-none">
|
||||||
|
<Icon path={mdiPencil} size="24" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="flex justify-between gap-4 py-4 hover:bg-gray-200 hover:text-black cursor-pointer"
|
||||||
|
on:click={() => (isShowChangeLocation = true)}
|
||||||
|
on:keydown={(event) => event.key === 'Enter' && (isShowChangeLocation = true)}
|
||||||
|
tabindex="0"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<div>
|
||||||
|
<div><Icon path={mdiMapMarkerOutline} size="24" /></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>No location available for this asset, click to add one.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="focus:outline-none">
|
||||||
|
<Icon path={mdiPencil} size="24" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if isShowChangeLocation}
|
||||||
|
{#if latlng}
|
||||||
|
<ChangeLocation
|
||||||
|
confirmText="Confirm"
|
||||||
|
location={latlng}
|
||||||
|
id_asset={asset.id}
|
||||||
|
on:confirm={handleConfirmChangeLocation}
|
||||||
|
on:cancel={() => (isShowChangeLocation = false)}
|
||||||
|
>
|
||||||
|
<svelte:fragment slot="prompt">
|
||||||
|
<p>Please select a new location:</p>
|
||||||
|
</svelte:fragment>
|
||||||
|
</ChangeLocation>
|
||||||
|
{:else}
|
||||||
|
<ChangeLocation
|
||||||
|
confirmText="Confirm"
|
||||||
|
on:confirm={handleConfirmChangeLocation}
|
||||||
|
on:cancel={() => (isShowChangeLocation = false)}
|
||||||
|
>
|
||||||
|
<svelte:fragment slot="prompt">
|
||||||
|
<p>Please select a new location:</p>
|
||||||
|
</svelte:fragment>
|
||||||
|
</ChangeLocation>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { api } from '@api';
|
||||||
|
import {
|
||||||
|
notificationController,
|
||||||
|
NotificationType,
|
||||||
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
|
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
||||||
|
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||||
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
|
import ChangeDate from '$lib/components/shared-components/change-date.svelte';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
export let force = !$featureFlags.trash;
|
||||||
|
export let menuItem = false;
|
||||||
|
const { clearSelect, getOwnedAssets } = getAssetControlContext();
|
||||||
|
|
||||||
|
let isShowChangeDate = false;
|
||||||
|
|
||||||
|
async function handleConfirmChangeDate(event: CustomEvent<string>) {
|
||||||
|
isShowChangeDate = false;
|
||||||
|
const ids = Array.from(getOwnedAssets())
|
||||||
|
.filter((a) => !a.isExternal)
|
||||||
|
.map((a) => a.id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.assetApi.updateAssets({
|
||||||
|
assetBulkUpdateDto: {
|
||||||
|
ids: ids,
|
||||||
|
dateTimeOriginal: event.detail,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
notificationController.show({ message: 'Metadata updated please reload to apply', type: NotificationType.Info });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
clearSelect();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if menuItem}
|
||||||
|
<MenuOption text={force ? 'Change date' : 'Change date'} on:click={() => (isShowChangeDate = true)} />
|
||||||
|
{/if}
|
||||||
|
{#if isShowChangeDate}
|
||||||
|
<ChangeDate
|
||||||
|
title="Change Date"
|
||||||
|
confirmText="Confirm"
|
||||||
|
initialDate={DateTime.now()}
|
||||||
|
on:confirm={handleConfirmChangeDate}
|
||||||
|
on:cancel={() => (isShowChangeDate = false)}
|
||||||
|
>
|
||||||
|
<svelte:fragment slot="prompt">
|
||||||
|
<p>Please select a new date:</p>
|
||||||
|
</svelte:fragment>
|
||||||
|
</ChangeDate>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { api } from '@api';
|
||||||
|
import {
|
||||||
|
notificationController,
|
||||||
|
NotificationType,
|
||||||
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
|
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
||||||
|
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||||
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
|
import ChangeLocation from '$lib/components/shared-components/change-location.svelte';
|
||||||
|
|
||||||
|
export let force = !$featureFlags.trash;
|
||||||
|
export let menuItem = false;
|
||||||
|
const { clearSelect, getOwnedAssets } = getAssetControlContext();
|
||||||
|
|
||||||
|
let isShowChangeLocation = false;
|
||||||
|
|
||||||
|
async function handleConfirmChangeLocation(event: CustomEvent<{ lng: number; lat: number }>) {
|
||||||
|
isShowChangeLocation = false;
|
||||||
|
const ids = Array.from(getOwnedAssets())
|
||||||
|
.filter((a) => !a.isExternal)
|
||||||
|
.map((a) => a.id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.assetApi.updateAssets({
|
||||||
|
assetBulkUpdateDto: {
|
||||||
|
ids: ids,
|
||||||
|
latitude: event.detail.lat,
|
||||||
|
longitude: event.detail.lng,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
notificationController.show({ message: 'Metadata updated please reload to apply', type: NotificationType.Info });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
clearSelect();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if menuItem}
|
||||||
|
<MenuOption text={force ? 'Change location' : 'Change location'} on:click={() => (isShowChangeLocation = true)} />
|
||||||
|
{/if}
|
||||||
|
{#if isShowChangeLocation}
|
||||||
|
<ChangeLocation
|
||||||
|
confirmText="Confirm"
|
||||||
|
on:confirm={handleConfirmChangeLocation}
|
||||||
|
on:cancel={() => (isShowChangeLocation = false)}
|
||||||
|
>
|
||||||
|
<svelte:fragment slot="prompt">
|
||||||
|
<p>Please select a new location:</p>
|
||||||
|
</svelte:fragment>
|
||||||
|
</ChangeLocation>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import FullScreenModal from './full-screen-modal.svelte';
|
||||||
|
import Button from '../elements/buttons/button.svelte';
|
||||||
|
import type { Color } from '$lib/components/elements/buttons/button.svelte';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
export let title = 'Change Date';
|
||||||
|
export let prompt = 'Please select a new date:';
|
||||||
|
export let confirmText = 'Confirm';
|
||||||
|
export let confirmColor: Color = 'primary';
|
||||||
|
export let cancelText = 'Cancel';
|
||||||
|
export let cancelColor: Color = 'secondary';
|
||||||
|
export let hideCancelButton = false;
|
||||||
|
export let initialDate: DateTime = DateTime.now();
|
||||||
|
|
||||||
|
let selectedDate: string;
|
||||||
|
let selectedTimezone: string | null;
|
||||||
|
|
||||||
|
let timezones = [
|
||||||
|
'',
|
||||||
|
'UTC-12:00',
|
||||||
|
'UTC-11:00',
|
||||||
|
'UTC-10:00',
|
||||||
|
'UTC-09:30',
|
||||||
|
'UTC-09:00',
|
||||||
|
'UTC-08:00',
|
||||||
|
'UTC-07:00',
|
||||||
|
'UTC-06:00',
|
||||||
|
'UTC-05:00',
|
||||||
|
'UTC-04:00',
|
||||||
|
'UTC-03:30',
|
||||||
|
'UTC-03:00',
|
||||||
|
'UTC-02:00',
|
||||||
|
'UTC-01:00',
|
||||||
|
'UTC+00:00',
|
||||||
|
'UTC+01:00',
|
||||||
|
'UTC+02:00',
|
||||||
|
'UTC+03:00',
|
||||||
|
'UTC+03:30',
|
||||||
|
'UTC+04:00',
|
||||||
|
'UTC+04:30',
|
||||||
|
'UTC+05:00',
|
||||||
|
'UTC+05:30',
|
||||||
|
'UTC+05:45',
|
||||||
|
'UTC+06:00',
|
||||||
|
'UTC+06:30',
|
||||||
|
'UTC+07:00',
|
||||||
|
'UTC+08:00',
|
||||||
|
'UTC+08:45',
|
||||||
|
'UTC+09:00',
|
||||||
|
'UTC+09:30',
|
||||||
|
'UTC+10:00',
|
||||||
|
'UTC+10:30',
|
||||||
|
'UTC+11:00',
|
||||||
|
'UTC+12:00',
|
||||||
|
'UTC+12:45',
|
||||||
|
'UTC+13:00',
|
||||||
|
'UTC+14:00',
|
||||||
|
];
|
||||||
|
|
||||||
|
selectedDate = initialDate.toFormat("yyyy-MM-dd'T'HH:mm");
|
||||||
|
selectedTimezone = 'UTC' + initialDate.toFormat('ZZ');
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
let isConfirmButtonDisabled = false;
|
||||||
|
|
||||||
|
const handleCancel = () => dispatch('cancel');
|
||||||
|
const handleEscape = () => {
|
||||||
|
if (!isConfirmButtonDisabled) {
|
||||||
|
dispatch('cancel');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
let date = DateTime.fromISO(selectedDate);
|
||||||
|
if (selectedTimezone != null) {
|
||||||
|
date = date.setZone(selectedTimezone);
|
||||||
|
}
|
||||||
|
|
||||||
|
isConfirmButtonDisabled = true;
|
||||||
|
dispatch('confirm', date.toISO());
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FullScreenModal on:clickOutside={handleCancel} on:escape={() => handleEscape()}>
|
||||||
|
<div
|
||||||
|
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
|
||||||
|
>
|
||||||
|
<h1 class="pb-2 text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-md px-4 py-5 text-center">
|
||||||
|
<slot name="prompt">
|
||||||
|
<p>{prompt}</p>
|
||||||
|
</slot>
|
||||||
|
<div class="mt-2" />
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<label for="datetime">Date and Time</label>
|
||||||
|
<input id="datetime" type="datetime-local" bind:value={selectedDate} class="mt-2 w-full text-black" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<label for="timezone">Timezone</label>
|
||||||
|
<select id="timezone" bind:value={selectedTimezone} class="mt-2 w-full text-black">
|
||||||
|
{#each timezones as timezone}
|
||||||
|
<option value={timezone}>{timezone || 'Choose a timezone'}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 flex w-full gap-4 px-4">
|
||||||
|
{#if !hideCancelButton}
|
||||||
|
<Button color={cancelColor} fullwidth on:click={handleCancel}>
|
||||||
|
{cancelText}
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
<Button color={confirmColor} fullwidth on:click={handleConfirm} disabled={isConfirmButtonDisabled}>
|
||||||
|
{confirmText}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FullScreenModal>
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import FullScreenModal from './full-screen-modal.svelte';
|
||||||
|
import Button from '../elements/buttons/button.svelte';
|
||||||
|
import type { Color } from '$lib/components/elements/buttons/button.svelte';
|
||||||
|
import Map from './map/map.svelte';
|
||||||
|
export const title = 'Change Location';
|
||||||
|
export let prompt = 'Please select a new date:';
|
||||||
|
export let confirmText = 'Confirm';
|
||||||
|
export let confirmColor: Color = 'primary';
|
||||||
|
export let cancelText = 'Cancel';
|
||||||
|
export let cancelColor: Color = 'secondary';
|
||||||
|
export let hideCancelButton = false;
|
||||||
|
export let location: { lng: number; lat: number } = { lng: 0, lat: 0 };
|
||||||
|
export let id_asset: string | null = null;
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
let isConfirmButtonDisabled = false;
|
||||||
|
|
||||||
|
const originalLat = location.lat;
|
||||||
|
const originalLng = location.lng;
|
||||||
|
|
||||||
|
const handleCancel = () => dispatch('cancel');
|
||||||
|
const handleEscape = () => {
|
||||||
|
if (!isConfirmButtonDisabled) {
|
||||||
|
dispatch('cancel');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
dispatch('confirm', location);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FullScreenModal on:clickOutside={handleCancel} on:escape={() => handleEscape()}>
|
||||||
|
<div
|
||||||
|
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div class="text-md px-4 py-5 text-center">
|
||||||
|
<slot name="prompt">
|
||||||
|
<p>{prompt}</p>
|
||||||
|
</slot>
|
||||||
|
<div class="mt-2" />
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<label for="datetime">Pick a location</label>
|
||||||
|
</div>
|
||||||
|
<div style="height: 500px;">
|
||||||
|
<Map
|
||||||
|
mapMarkers={id_asset
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: id_asset,
|
||||||
|
lat: originalLat,
|
||||||
|
lon: originalLng,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []}
|
||||||
|
zoom={id_asset ? 15 : 1}
|
||||||
|
center={location}
|
||||||
|
simplified={true}
|
||||||
|
clickable={true}
|
||||||
|
on:clickedPoint={(e) => {
|
||||||
|
location = e.detail;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 flex w-full gap-4 px-4">
|
||||||
|
{#if !hideCancelButton}
|
||||||
|
<Button color={cancelColor} fullwidth on:click={handleCancel}>
|
||||||
|
{cancelText}
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
<Button color={confirmColor} fullwidth on:click={handleConfirm} disabled={isConfirmButtonDisabled}>
|
||||||
|
{confirmText}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FullScreenModal>
|
||||||
@@ -28,6 +28,14 @@
|
|||||||
export let zoom: number | undefined = undefined;
|
export let zoom: number | undefined = undefined;
|
||||||
export let center: LngLatLike | undefined = undefined;
|
export let center: LngLatLike | undefined = undefined;
|
||||||
export let simplified = false;
|
export let simplified = false;
|
||||||
|
export let clickable = false;
|
||||||
|
|
||||||
|
let map: maplibregl.Map;
|
||||||
|
let marker: maplibregl.Marker | null = null;
|
||||||
|
|
||||||
|
$: if (map) {
|
||||||
|
map.on('click', handleMapClick);
|
||||||
|
}
|
||||||
|
|
||||||
$: style = (async () => {
|
$: style = (async () => {
|
||||||
const { data } = await api.systemConfigApi.getMapStyle({
|
const { data } = await api.systemConfigApi.getMapStyle({
|
||||||
@@ -36,7 +44,10 @@
|
|||||||
return data as StyleSpecification;
|
return data as StyleSpecification;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{ selected: string[] }>();
|
const dispatch = createEventDispatcher<{
|
||||||
|
selected: string[];
|
||||||
|
clickedPoint: { lat: number; lng: number };
|
||||||
|
}>();
|
||||||
|
|
||||||
function handleAssetClick(assetId: string, map: Map | null) {
|
function handleAssetClick(assetId: string, map: Map | null) {
|
||||||
if (!map) {
|
if (!map) {
|
||||||
@@ -63,6 +74,19 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleMapClick(event: maplibregl.MapMouseEvent) {
|
||||||
|
if (clickable) {
|
||||||
|
const { lng, lat } = event.lngLat;
|
||||||
|
dispatch('clickedPoint', { lng, lat });
|
||||||
|
|
||||||
|
if (marker) {
|
||||||
|
marker.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
marker = new maplibregl.Marker().setLngLat([lng, lat]).addTo(map);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type FeaturePoint = Feature<Point, { id: string }>;
|
type FeaturePoint = Feature<Point, { id: string }>;
|
||||||
|
|
||||||
const asFeature = (marker: MapMarkerResponseDto): FeaturePoint => {
|
const asFeature = (marker: MapMarkerResponseDto): FeaturePoint => {
|
||||||
@@ -87,7 +111,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#await style then style}
|
{#await style then style}
|
||||||
<MapLibre {style} class="h-full" {center} {zoom} attributionControl={false} diffStyleUpdates={true} let:map>
|
<MapLibre {style} class="h-full" {center} {zoom} attributionControl={false} diffStyleUpdates={true} let:map bind:map>
|
||||||
<NavigationControl position="top-left" showCompass={!simplified} />
|
<NavigationControl position="top-left" showCompass={!simplified} />
|
||||||
{#if !simplified}
|
{#if !simplified}
|
||||||
<GeolocateControl position="top-left" fitBoundsOptions={{ maxZoom: 12 }} />
|
<GeolocateControl position="top-left" fitBoundsOptions={{ maxZoom: 12 }} />
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
||||||
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
||||||
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
|
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
|
||||||
|
import ChangeDate from '$lib/components/photos-page/actions/change-date.svelte';
|
||||||
|
import ChangeLocation from '$lib/components/photos-page/actions/change-location.svelte';
|
||||||
import RemoveFromAlbum from '$lib/components/photos-page/actions/remove-from-album.svelte';
|
import RemoveFromAlbum from '$lib/components/photos-page/actions/remove-from-album.svelte';
|
||||||
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
|
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
|
||||||
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
|
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
|
||||||
@@ -446,6 +448,8 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{#if isAllUserOwned}
|
{#if isAllUserOwned}
|
||||||
<DeleteAssets menuItem onAssetDelete={(assetId) => assetStore.removeAsset(assetId)} />
|
<DeleteAssets menuItem onAssetDelete={(assetId) => assetStore.removeAsset(assetId)} />
|
||||||
|
<ChangeDate menuItem />
|
||||||
|
<ChangeLocation menuItem />
|
||||||
{/if}
|
{/if}
|
||||||
</AssetSelectContextMenu>
|
</AssetSelectContextMenu>
|
||||||
</AssetSelectControlBar>
|
</AssetSelectControlBar>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||||
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
||||||
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
|
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
|
||||||
|
import ChangeDate from '$lib/components/photos-page/actions/change-date.svelte';
|
||||||
|
import ChangeLocation from '$lib/components/photos-page/actions/change-location.svelte';
|
||||||
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
||||||
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
||||||
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
||||||
@@ -40,6 +42,8 @@
|
|||||||
<AssetSelectContextMenu icon={mdiDotsVertical} title="Menu">
|
<AssetSelectContextMenu icon={mdiDotsVertical} title="Menu">
|
||||||
<DownloadAction menuItem />
|
<DownloadAction menuItem />
|
||||||
<ArchiveAction menuItem unarchive={isAllArchive} />
|
<ArchiveAction menuItem unarchive={isAllArchive} />
|
||||||
|
<ChangeDate menuItem />
|
||||||
|
<ChangeLocation menuItem />
|
||||||
</AssetSelectContextMenu>
|
</AssetSelectContextMenu>
|
||||||
</AssetSelectControlBar>
|
</AssetSelectControlBar>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
import SetBirthDateModal from '$lib/components/faces-page/set-birth-date-modal.svelte';
|
import SetBirthDateModal from '$lib/components/faces-page/set-birth-date-modal.svelte';
|
||||||
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
||||||
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
|
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
|
||||||
|
import ChangeDate from '$lib/components/photos-page/actions/change-date.svelte';
|
||||||
|
import ChangeLocation from '$lib/components/photos-page/actions/change-location.svelte';
|
||||||
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
||||||
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
||||||
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
||||||
@@ -377,6 +379,8 @@
|
|||||||
<DownloadAction menuItem filename="{data.person.name || 'immich'}.zip" />
|
<DownloadAction menuItem filename="{data.person.name || 'immich'}.zip" />
|
||||||
<FavoriteAction menuItem removeFavorite={isAllFavorite} />
|
<FavoriteAction menuItem removeFavorite={isAllFavorite} />
|
||||||
<ArchiveAction menuItem unarchive={isAllArchive} onArchive={(ids) => $assetStore.removeAssets(ids)} />
|
<ArchiveAction menuItem unarchive={isAllArchive} onArchive={(ids) => $assetStore.removeAssets(ids)} />
|
||||||
|
<ChangeDate menuItem />
|
||||||
|
<ChangeLocation menuItem />
|
||||||
</AssetSelectContextMenu>
|
</AssetSelectContextMenu>
|
||||||
</AssetSelectControlBar>
|
</AssetSelectControlBar>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||||
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
||||||
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
|
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
|
||||||
|
import ChangeDate from '$lib/components/photos-page/actions/change-date.svelte';
|
||||||
|
import ChangeLocation from '$lib/components/photos-page/actions/change-location.svelte';
|
||||||
import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte';
|
import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte';
|
||||||
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
||||||
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
||||||
@@ -70,6 +72,8 @@
|
|||||||
{#if $selectedAssets.size > 1}
|
{#if $selectedAssets.size > 1}
|
||||||
<StackAction onStack={(ids) => assetStore.removeAssets(ids)} />
|
<StackAction onStack={(ids) => assetStore.removeAssets(ids)} />
|
||||||
{/if}
|
{/if}
|
||||||
|
<ChangeDate menuItem />
|
||||||
|
<ChangeLocation menuItem />
|
||||||
<AssetJobActions />
|
<AssetJobActions />
|
||||||
</AssetSelectContextMenu>
|
</AssetSelectContextMenu>
|
||||||
</AssetSelectControlBar>
|
</AssetSelectControlBar>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
||||||
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
|
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
|
||||||
|
import ChangeDate from '$lib/components/photos-page/actions/change-date.svelte';
|
||||||
|
import ChangeLocation from '$lib/components/photos-page/actions/change-location.svelte';
|
||||||
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
||||||
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
||||||
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
||||||
@@ -117,6 +119,8 @@
|
|||||||
<DownloadAction menuItem />
|
<DownloadAction menuItem />
|
||||||
<FavoriteAction menuItem removeFavorite={isAllFavorite} />
|
<FavoriteAction menuItem removeFavorite={isAllFavorite} />
|
||||||
<ArchiveAction menuItem unarchive={isAllArchived} />
|
<ArchiveAction menuItem unarchive={isAllArchived} />
|
||||||
|
<ChangeDate menuItem />
|
||||||
|
<ChangeLocation menuItem />
|
||||||
</AssetSelectContextMenu>
|
</AssetSelectContextMenu>
|
||||||
</AssetSelectControlBar>
|
</AssetSelectControlBar>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
Reference in New Issue
Block a user