hash)
@@ -86,7 +88,8 @@ class Asset {
this.isFavorite = false,
this.isArchived = false,
this.isTrashed = false,
- this.stackParentId,
+ this.stackId,
+ this.stackPrimaryAssetId,
this.stackCount = 0,
this.isOffline = false,
this.thumbhash,
@@ -163,12 +166,11 @@ class Asset {
@ignore
ExifInfo? exifInfo;
- String? stackParentId;
+ String? stackId;
- @ignore
- int get stackChildrenCount => stackCount ?? 0;
+ String? stackPrimaryAssetId;
- int? stackCount;
+ int stackCount;
/// Aspect ratio of the asset
@ignore
@@ -231,7 +233,8 @@ class Asset {
isArchived == other.isArchived &&
isTrashed == other.isTrashed &&
stackCount == other.stackCount &&
- stackParentId == other.stackParentId;
+ stackPrimaryAssetId == other.stackPrimaryAssetId &&
+ stackId == other.stackId;
}
@override
@@ -256,7 +259,8 @@ class Asset {
isArchived.hashCode ^
isTrashed.hashCode ^
stackCount.hashCode ^
- stackParentId.hashCode;
+ stackPrimaryAssetId.hashCode ^
+ stackId.hashCode;
/// Returns `true` if this [Asset] can updated with values from parameter [a]
bool canUpdate(Asset a) {
@@ -269,7 +273,6 @@ class Asset {
width == null && a.width != null ||
height == null && a.height != null ||
livePhotoVideoId == null && a.livePhotoVideoId != null ||
- stackParentId == null && a.stackParentId != null ||
isFavorite != a.isFavorite ||
isArchived != a.isArchived ||
isTrashed != a.isTrashed ||
@@ -278,10 +281,9 @@ class Asset {
a.exifInfo?.longitude != exifInfo?.longitude ||
// no local stack count or different count from remote
a.thumbhash != thumbhash ||
- ((stackCount == null && a.stackCount != null) ||
- (stackCount != null &&
- a.stackCount != null &&
- stackCount != a.stackCount));
+ stackId != a.stackId ||
+ stackCount != a.stackCount ||
+ stackPrimaryAssetId == null && a.stackPrimaryAssetId != null;
}
/// Returns a new [Asset] with values from this and merged & updated with [a]
@@ -311,9 +313,11 @@ class Asset {
id: id,
remoteId: remoteId,
livePhotoVideoId: livePhotoVideoId,
- // workaround to nullify stackParentId for the parent asset until we refactor the mobile app
+ // workaround to nullify stackPrimaryAssetId for the parent asset until we refactor the mobile app
// stack handling to properly handle it
- stackParentId: stackParentId == remoteId ? null : stackParentId,
+ stackId: stackId,
+ stackPrimaryAssetId:
+ stackPrimaryAssetId == remoteId ? null : stackPrimaryAssetId,
stackCount: stackCount,
isFavorite: isFavorite,
isArchived: isArchived,
@@ -330,9 +334,12 @@ class Asset {
width: a.width,
height: a.height,
livePhotoVideoId: a.livePhotoVideoId,
- // workaround to nullify stackParentId for the parent asset until we refactor the mobile app
+ // workaround to nullify stackPrimaryAssetId for the parent asset until we refactor the mobile app
// stack handling to properly handle it
- stackParentId: a.stackParentId == a.remoteId ? null : a.stackParentId,
+ stackId: a.stackId,
+ stackPrimaryAssetId: a.stackPrimaryAssetId == a.remoteId
+ ? null
+ : a.stackPrimaryAssetId,
stackCount: a.stackCount,
// isFavorite + isArchived are not set by device-only assets
isFavorite: a.isFavorite,
@@ -374,7 +381,8 @@ class Asset {
bool? isTrashed,
bool? isOffline,
ExifInfo? exifInfo,
- String? stackParentId,
+ String? stackId,
+ String? stackPrimaryAssetId,
int? stackCount,
String? thumbhash,
}) =>
@@ -398,7 +406,8 @@ class Asset {
isTrashed: isTrashed ?? this.isTrashed,
isOffline: isOffline ?? this.isOffline,
exifInfo: exifInfo ?? this.exifInfo,
- stackParentId: stackParentId ?? this.stackParentId,
+ stackId: stackId ?? this.stackId,
+ stackPrimaryAssetId: stackPrimaryAssetId ?? this.stackPrimaryAssetId,
stackCount: stackCount ?? this.stackCount,
thumbhash: thumbhash ?? this.thumbhash,
);
@@ -445,8 +454,9 @@ class Asset {
"checksum": "$checksum",
"ownerId": $ownerId,
"livePhotoVideoId": "${livePhotoVideoId ?? "N/A"}",
+ "stackId": "${stackId ?? "N/A"}",
+ "stackPrimaryAssetId": "${stackPrimaryAssetId ?? "N/A"}",
"stackCount": "$stackCount",
- "stackParentId": "${stackParentId ?? "N/A"}",
"fileCreatedAt": "$fileCreatedAt",
"fileModifiedAt": "$fileModifiedAt",
"updatedAt": "$updatedAt",
diff --git a/mobile/lib/entities/asset.entity.g.dart b/mobile/lib/entities/asset.entity.g.dart
index 099e15eef15ca..23bf23604635d 100644
--- a/mobile/lib/entities/asset.entity.g.dart
+++ b/mobile/lib/entities/asset.entity.g.dart
@@ -92,29 +92,34 @@ const AssetSchema = CollectionSchema(
name: r'stackCount',
type: IsarType.long,
),
- r'stackParentId': PropertySchema(
+ r'stackId': PropertySchema(
id: 15,
- name: r'stackParentId',
+ name: r'stackId',
+ type: IsarType.string,
+ ),
+ r'stackPrimaryAssetId': PropertySchema(
+ id: 16,
+ name: r'stackPrimaryAssetId',
type: IsarType.string,
),
r'thumbhash': PropertySchema(
- id: 16,
+ id: 17,
name: r'thumbhash',
type: IsarType.string,
),
r'type': PropertySchema(
- id: 17,
+ id: 18,
name: r'type',
type: IsarType.byte,
enumMap: _AssettypeEnumValueMap,
),
r'updatedAt': PropertySchema(
- id: 18,
+ id: 19,
name: r'updatedAt',
type: IsarType.dateTime,
),
r'width': PropertySchema(
- id: 19,
+ id: 20,
name: r'width',
type: IsarType.int,
)
@@ -205,7 +210,13 @@ int _assetEstimateSize(
}
}
{
- final value = object.stackParentId;
+ final value = object.stackId;
+ if (value != null) {
+ bytesCount += 3 + value.length * 3;
+ }
+ }
+ {
+ final value = object.stackPrimaryAssetId;
if (value != null) {
bytesCount += 3 + value.length * 3;
}
@@ -240,11 +251,12 @@ void _assetSerialize(
writer.writeLong(offsets[12], object.ownerId);
writer.writeString(offsets[13], object.remoteId);
writer.writeLong(offsets[14], object.stackCount);
- writer.writeString(offsets[15], object.stackParentId);
- writer.writeString(offsets[16], object.thumbhash);
- writer.writeByte(offsets[17], object.type.index);
- writer.writeDateTime(offsets[18], object.updatedAt);
- writer.writeInt(offsets[19], object.width);
+ writer.writeString(offsets[15], object.stackId);
+ writer.writeString(offsets[16], object.stackPrimaryAssetId);
+ writer.writeString(offsets[17], object.thumbhash);
+ writer.writeByte(offsets[18], object.type.index);
+ writer.writeDateTime(offsets[19], object.updatedAt);
+ writer.writeInt(offsets[20], object.width);
}
Asset _assetDeserialize(
@@ -269,13 +281,14 @@ Asset _assetDeserialize(
localId: reader.readStringOrNull(offsets[11]),
ownerId: reader.readLong(offsets[12]),
remoteId: reader.readStringOrNull(offsets[13]),
- stackCount: reader.readLongOrNull(offsets[14]),
- stackParentId: reader.readStringOrNull(offsets[15]),
- thumbhash: reader.readStringOrNull(offsets[16]),
- type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[17])] ??
+ stackCount: reader.readLongOrNull(offsets[14]) ?? 0,
+ stackId: reader.readStringOrNull(offsets[15]),
+ stackPrimaryAssetId: reader.readStringOrNull(offsets[16]),
+ thumbhash: reader.readStringOrNull(offsets[17]),
+ type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[18])] ??
AssetType.other,
- updatedAt: reader.readDateTime(offsets[18]),
- width: reader.readIntOrNull(offsets[19]),
+ updatedAt: reader.readDateTime(offsets[19]),
+ width: reader.readIntOrNull(offsets[20]),
);
return object;
}
@@ -316,17 +329,19 @@ P _assetDeserializeProp(
case 13:
return (reader.readStringOrNull(offset)) as P;
case 14:
- return (reader.readLongOrNull(offset)) as P;
+ return (reader.readLongOrNull(offset) ?? 0) as P;
case 15:
return (reader.readStringOrNull(offset)) as P;
case 16:
return (reader.readStringOrNull(offset)) as P;
case 17:
+ return (reader.readStringOrNull(offset)) as P;
+ case 18:
return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ??
AssetType.other) as P;
- case 18:
- return (reader.readDateTime(offset)) as P;
case 19:
+ return (reader.readDateTime(offset)) as P;
+ case 20:
return (reader.readIntOrNull(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
@@ -1859,24 +1874,8 @@ extension AssetQueryFilter on QueryBuilder {
});
}
- QueryBuilder stackCountIsNull() {
- return QueryBuilder.apply(this, (query) {
- return query.addFilterCondition(const FilterCondition.isNull(
- property: r'stackCount',
- ));
- });
- }
-
- QueryBuilder stackCountIsNotNull() {
- return QueryBuilder.apply(this, (query) {
- return query.addFilterCondition(const FilterCondition.isNotNull(
- property: r'stackCount',
- ));
- });
- }
-
QueryBuilder stackCountEqualTo(
- int? value) {
+ int value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'stackCount',
@@ -1886,7 +1885,7 @@ extension AssetQueryFilter on QueryBuilder {
}
QueryBuilder stackCountGreaterThan(
- int? value, {
+ int value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
@@ -1899,7 +1898,7 @@ extension AssetQueryFilter on QueryBuilder {
}
QueryBuilder stackCountLessThan(
- int? value, {
+ int value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
@@ -1912,8 +1911,8 @@ extension AssetQueryFilter on QueryBuilder {
}
QueryBuilder stackCountBetween(
- int? lower,
- int? upper, {
+ int lower,
+ int upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
@@ -1928,36 +1927,36 @@ extension AssetQueryFilter on QueryBuilder {
});
}
- QueryBuilder stackParentIdIsNull() {
+ QueryBuilder stackIdIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
- property: r'stackParentId',
+ property: r'stackId',
));
});
}
- QueryBuilder stackParentIdIsNotNull() {
+ QueryBuilder stackIdIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
- property: r'stackParentId',
+ property: r'stackId',
));
});
}
- QueryBuilder stackParentIdEqualTo(
+ QueryBuilder stackIdEqualTo(
String? value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
- property: r'stackParentId',
+ property: r'stackId',
value: value,
caseSensitive: caseSensitive,
));
});
}
- QueryBuilder stackParentIdGreaterThan(
+ QueryBuilder stackIdGreaterThan(
String? value, {
bool include = false,
bool caseSensitive = true,
@@ -1965,14 +1964,14 @@ extension AssetQueryFilter on QueryBuilder {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
- property: r'stackParentId',
+ property: r'stackId',
value: value,
caseSensitive: caseSensitive,
));
});
}
- QueryBuilder stackParentIdLessThan(
+ QueryBuilder stackIdLessThan(
String? value, {
bool include = false,
bool caseSensitive = true,
@@ -1980,14 +1979,14 @@ extension AssetQueryFilter on QueryBuilder {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
- property: r'stackParentId',
+ property: r'stackId',
value: value,
caseSensitive: caseSensitive,
));
});
}
- QueryBuilder stackParentIdBetween(
+ QueryBuilder stackIdBetween(
String? lower,
String? upper, {
bool includeLower = true,
@@ -1996,7 +1995,7 @@ extension AssetQueryFilter on QueryBuilder {
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
- property: r'stackParentId',
+ property: r'stackId',
lower: lower,
includeLower: includeLower,
upper: upper,
@@ -2006,69 +2005,221 @@ extension AssetQueryFilter on QueryBuilder {
});
}
- QueryBuilder stackParentIdStartsWith(
+ QueryBuilder stackIdStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.startsWith(
- property: r'stackParentId',
+ property: r'stackId',
value: value,
caseSensitive: caseSensitive,
));
});
}
- QueryBuilder stackParentIdEndsWith(
+ QueryBuilder stackIdEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.endsWith(
- property: r'stackParentId',
+ property: r'stackId',
value: value,
caseSensitive: caseSensitive,
));
});
}
- QueryBuilder stackParentIdContains(
+ QueryBuilder stackIdContains(
String value,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.contains(
- property: r'stackParentId',
+ property: r'stackId',
value: value,
caseSensitive: caseSensitive,
));
});
}
- QueryBuilder stackParentIdMatches(
+ QueryBuilder stackIdMatches(
String pattern,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.matches(
- property: r'stackParentId',
+ property: r'stackId',
wildcard: pattern,
caseSensitive: caseSensitive,
));
});
}
- QueryBuilder stackParentIdIsEmpty() {
+ QueryBuilder stackIdIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
- property: r'stackParentId',
+ property: r'stackId',
value: '',
));
});
}
- QueryBuilder stackParentIdIsNotEmpty() {
+ QueryBuilder stackIdIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
- property: r'stackParentId',
+ property: r'stackId',
+ value: '',
+ ));
+ });
+ }
+
+ QueryBuilder
+ stackPrimaryAssetIdIsNull() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(const FilterCondition.isNull(
+ property: r'stackPrimaryAssetId',
+ ));
+ });
+ }
+
+ QueryBuilder
+ stackPrimaryAssetIdIsNotNull() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(const FilterCondition.isNotNull(
+ property: r'stackPrimaryAssetId',
+ ));
+ });
+ }
+
+ QueryBuilder stackPrimaryAssetIdEqualTo(
+ String? value, {
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.equalTo(
+ property: r'stackPrimaryAssetId',
+ value: value,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder
+ stackPrimaryAssetIdGreaterThan(
+ String? value, {
+ bool include = false,
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.greaterThan(
+ include: include,
+ property: r'stackPrimaryAssetId',
+ value: value,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder stackPrimaryAssetIdLessThan(
+ String? value, {
+ bool include = false,
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.lessThan(
+ include: include,
+ property: r'stackPrimaryAssetId',
+ value: value,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder stackPrimaryAssetIdBetween(
+ String? lower,
+ String? upper, {
+ bool includeLower = true,
+ bool includeUpper = true,
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.between(
+ property: r'stackPrimaryAssetId',
+ lower: lower,
+ includeLower: includeLower,
+ upper: upper,
+ includeUpper: includeUpper,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder
+ stackPrimaryAssetIdStartsWith(
+ String value, {
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.startsWith(
+ property: r'stackPrimaryAssetId',
+ value: value,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder stackPrimaryAssetIdEndsWith(
+ String value, {
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.endsWith(
+ property: r'stackPrimaryAssetId',
+ value: value,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder stackPrimaryAssetIdContains(
+ String value,
+ {bool caseSensitive = true}) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.contains(
+ property: r'stackPrimaryAssetId',
+ value: value,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder stackPrimaryAssetIdMatches(
+ String pattern,
+ {bool caseSensitive = true}) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.matches(
+ property: r'stackPrimaryAssetId',
+ wildcard: pattern,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder
+ stackPrimaryAssetIdIsEmpty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.equalTo(
+ property: r'stackPrimaryAssetId',
+ value: '',
+ ));
+ });
+ }
+
+ QueryBuilder
+ stackPrimaryAssetIdIsNotEmpty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.greaterThan(
+ property: r'stackPrimaryAssetId',
value: '',
));
});
@@ -2580,15 +2731,27 @@ extension AssetQuerySortBy on QueryBuilder {
});
}
- QueryBuilder sortByStackParentId() {
+ QueryBuilder sortByStackId() {
return QueryBuilder.apply(this, (query) {
- return query.addSortBy(r'stackParentId', Sort.asc);
+ return query.addSortBy(r'stackId', Sort.asc);
});
}
- QueryBuilder sortByStackParentIdDesc() {
+ QueryBuilder sortByStackIdDesc() {
return QueryBuilder.apply(this, (query) {
- return query.addSortBy(r'stackParentId', Sort.desc);
+ return query.addSortBy(r'stackId', Sort.desc);
+ });
+ }
+
+ QueryBuilder sortByStackPrimaryAssetId() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(r'stackPrimaryAssetId', Sort.asc);
+ });
+ }
+
+ QueryBuilder sortByStackPrimaryAssetIdDesc() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(r'stackPrimaryAssetId', Sort.desc);
});
}
@@ -2834,15 +2997,27 @@ extension AssetQuerySortThenBy on QueryBuilder {
});
}
- QueryBuilder thenByStackParentId() {
+ QueryBuilder thenByStackId() {
return QueryBuilder.apply(this, (query) {
- return query.addSortBy(r'stackParentId', Sort.asc);
+ return query.addSortBy(r'stackId', Sort.asc);
});
}
- QueryBuilder thenByStackParentIdDesc() {
+ QueryBuilder thenByStackIdDesc() {
return QueryBuilder.apply(this, (query) {
- return query.addSortBy(r'stackParentId', Sort.desc);
+ return query.addSortBy(r'stackId', Sort.desc);
+ });
+ }
+
+ QueryBuilder thenByStackPrimaryAssetId() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(r'stackPrimaryAssetId', Sort.asc);
+ });
+ }
+
+ QueryBuilder thenByStackPrimaryAssetIdDesc() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(r'stackPrimaryAssetId', Sort.desc);
});
}
@@ -2992,10 +3167,17 @@ extension AssetQueryWhereDistinct on QueryBuilder {
});
}
- QueryBuilder distinctByStackParentId(
+ QueryBuilder distinctByStackId(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
- return query.addDistinctBy(r'stackParentId',
+ return query.addDistinctBy(r'stackId', caseSensitive: caseSensitive);
+ });
+ }
+
+ QueryBuilder distinctByStackPrimaryAssetId(
+ {bool caseSensitive = true}) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addDistinctBy(r'stackPrimaryAssetId',
caseSensitive: caseSensitive);
});
}
@@ -3117,15 +3299,21 @@ extension AssetQueryProperty on QueryBuilder {
});
}
- QueryBuilder stackCountProperty() {
+ QueryBuilder stackCountProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'stackCount');
});
}
- QueryBuilder stackParentIdProperty() {
+ QueryBuilder stackIdProperty() {
return QueryBuilder.apply(this, (query) {
- return query.addPropertyName(r'stackParentId');
+ return query.addPropertyName(r'stackId');
+ });
+ }
+
+ QueryBuilder stackPrimaryAssetIdProperty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addPropertyName(r'stackPrimaryAssetId');
});
}
diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart
index cc62620dfb239..d8ea7cd89b47f 100644
--- a/mobile/lib/pages/common/gallery_viewer.page.dart
+++ b/mobile/lib/pages/common/gallery_viewer.page.dart
@@ -68,7 +68,7 @@ class GalleryViewerPage extends HookConsumerWidget {
});
final stackIndex = useState(-1);
- final stack = showStack && currentAsset.stackChildrenCount > 0
+ final stack = showStack && currentAsset.stackCount > 0
? ref.watch(assetStackStateProvider(currentAsset))
: [];
final stackElements = showStack ? [currentAsset, ...stack] : [];
diff --git a/mobile/lib/providers/asset.provider.dart b/mobile/lib/providers/asset.provider.dart
index a0a3879db54f1..3c1a5ecc0119b 100644
--- a/mobile/lib/providers/asset.provider.dart
+++ b/mobile/lib/providers/asset.provider.dart
@@ -360,7 +360,7 @@ QueryBuilder? getRemoteAssetQuery(WidgetRef ref) {
.filter()
.ownerIdEqualTo(userId)
.isTrashedEqualTo(false)
- .stackParentIdIsNull()
+ .stackPrimaryAssetIdIsNull()
.sortByFileCreatedAtDesc();
}
@@ -374,6 +374,6 @@ QueryBuilder _commonFilterAndSort(
.filter()
.isArchivedEqualTo(false)
.isTrashedEqualTo(false)
- .stackParentIdIsNull()
+ .stackPrimaryAssetIdIsNull()
.sortByFileCreatedAtDesc();
}
diff --git a/mobile/lib/providers/asset_viewer/asset_stack.provider.dart b/mobile/lib/providers/asset_viewer/asset_stack.provider.dart
index 0883ed92dbc10..c3e4414b3935a 100644
--- a/mobile/lib/providers/asset_viewer/asset_stack.provider.dart
+++ b/mobile/lib/providers/asset_viewer/asset_stack.provider.dart
@@ -48,7 +48,7 @@ final assetStackProvider =
.filter()
.isArchivedEqualTo(false)
.isTrashedEqualTo(false)
- .stackParentIdEqualTo(asset.remoteId)
+ .stackPrimaryAssetIdEqualTo(asset.remoteId)
.sortByFileCreatedAtDesc()
.findAll();
});
diff --git a/mobile/lib/services/api.service.dart b/mobile/lib/services/api.service.dart
index c128a2c2fccf3..6ff62d4b3ab1f 100644
--- a/mobile/lib/services/api.service.dart
+++ b/mobile/lib/services/api.service.dart
@@ -29,6 +29,7 @@ class ApiService implements Authentication {
late ActivitiesApi activitiesApi;
late DownloadApi downloadApi;
late TrashApi trashApi;
+ late StacksApi stacksApi;
ApiService() {
final endpoint = Store.tryGet(StoreKey.serverEndpoint);
@@ -61,6 +62,7 @@ class ApiService implements Authentication {
activitiesApi = ActivitiesApi(_apiClient);
downloadApi = DownloadApi(_apiClient);
trashApi = TrashApi(_apiClient);
+ stacksApi = StacksApi(_apiClient);
}
Future resolveAndSetEndpoint(String serverUrl) async {
diff --git a/mobile/lib/services/asset_stack.service.dart b/mobile/lib/services/asset_stack.service.dart
deleted file mode 100644
index 9eff495f3740f..0000000000000
--- a/mobile/lib/services/asset_stack.service.dart
+++ /dev/null
@@ -1,72 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/entities/asset.entity.dart';
-import 'package:immich_mobile/providers/api.provider.dart';
-import 'package:immich_mobile/services/api.service.dart';
-import 'package:openapi/api.dart';
-
-class AssetStackService {
- AssetStackService(this._api);
-
- final ApiService _api;
-
- Future updateStack(
- Asset parentAsset, {
- List? childrenToAdd,
- List? childrenToRemove,
- }) async {
- // Guard [local asset]
- if (parentAsset.remoteId == null) {
- return;
- }
-
- try {
- if (childrenToAdd != null) {
- final toAdd = childrenToAdd
- .where((e) => e.isRemote)
- .map((e) => e.remoteId!)
- .toList();
-
- await _api.assetsApi.updateAssets(
- AssetBulkUpdateDto(ids: toAdd, stackParentId: parentAsset.remoteId),
- );
- }
-
- if (childrenToRemove != null) {
- final toRemove = childrenToRemove
- .where((e) => e.isRemote)
- .map((e) => e.remoteId!)
- .toList();
- await _api.assetsApi.updateAssets(
- AssetBulkUpdateDto(ids: toRemove, removeParent: true),
- );
- }
- } catch (error) {
- debugPrint("Error while updating stack children: ${error.toString()}");
- }
- }
-
- Future updateStackParent(Asset oldParent, Asset newParent) async {
- // Guard [local asset]
- if (oldParent.remoteId == null || newParent.remoteId == null) {
- return;
- }
-
- try {
- await _api.assetsApi.updateStackParent(
- UpdateStackParentDto(
- oldParentId: oldParent.remoteId!,
- newParentId: newParent.remoteId!,
- ),
- );
- } catch (error) {
- debugPrint("Error while updating stack parent: ${error.toString()}");
- }
- }
-}
-
-final assetStackServiceProvider = Provider(
- (ref) => AssetStackService(
- ref.watch(apiServiceProvider),
- ),
-);
diff --git a/mobile/lib/services/stack.service.dart b/mobile/lib/services/stack.service.dart
new file mode 100644
index 0000000000000..75074101c2ff8
--- /dev/null
+++ b/mobile/lib/services/stack.service.dart
@@ -0,0 +1,79 @@
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
+import 'package:immich_mobile/providers/api.provider.dart';
+import 'package:immich_mobile/providers/db.provider.dart';
+import 'package:immich_mobile/services/api.service.dart';
+import 'package:isar/isar.dart';
+import 'package:openapi/api.dart';
+
+class StackService {
+ StackService(this._api, this._db);
+
+ final ApiService _api;
+ final Isar _db;
+
+ Future getStack(String stackId) async {
+ try {
+ return _api.stacksApi.getStack(stackId);
+ } catch (error) {
+ debugPrint("Error while fetching stack: $error");
+ }
+ return null;
+ }
+
+ Future createStack(List assetIds) async {
+ try {
+ return _api.stacksApi.createStack(
+ StackCreateDto(assetIds: assetIds),
+ );
+ } catch (error) {
+ debugPrint("Error while creating stack: $error");
+ }
+ return null;
+ }
+
+ Future updateStack(
+ String stackId,
+ String primaryAssetId,
+ ) async {
+ try {
+ return await _api.stacksApi.updateStack(
+ stackId,
+ StackUpdateDto(primaryAssetId: primaryAssetId),
+ );
+ } catch (error) {
+ debugPrint("Error while updating stack children: $error");
+ }
+ return null;
+ }
+
+ Future deleteStack(String stackId, List assets) async {
+ try {
+ await _api.stacksApi.deleteStack(stackId);
+
+ // Update local database to trigger rerendering
+ final List removeAssets = [];
+ for (final asset in assets) {
+ asset.stackId = null;
+ asset.stackPrimaryAssetId = null;
+ asset.stackCount = 0;
+
+ removeAssets.add(asset);
+ }
+
+ _db.writeTxn(() async {
+ await _db.assets.putAll(removeAssets);
+ });
+ } catch (error) {
+ debugPrint("Error while deleting stack: $error");
+ }
+ }
+}
+
+final stackServiceProvider = Provider(
+ (ref) => StackService(
+ ref.watch(apiServiceProvider),
+ ref.watch(dbProvider),
+ ),
+);
diff --git a/mobile/lib/widgets/asset_grid/multiselect_grid.dart b/mobile/lib/widgets/asset_grid/multiselect_grid.dart
index e50a9a5ece6e3..3263373554df2 100644
--- a/mobile/lib/widgets/asset_grid/multiselect_grid.dart
+++ b/mobile/lib/widgets/asset_grid/multiselect_grid.dart
@@ -11,7 +11,7 @@ import 'package:immich_mobile/extensions/collection_extensions.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
import 'package:immich_mobile/services/album.service.dart';
-import 'package:immich_mobile/services/asset_stack.service.dart';
+import 'package:immich_mobile/services/stack.service.dart';
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
import 'package:immich_mobile/models/asset_selection_state.dart';
import 'package:immich_mobile/providers/multiselect.provider.dart';
@@ -344,11 +344,9 @@ class MultiselectGrid extends HookConsumerWidget {
if (!selectionEnabledHook.value || selection.value.length < 2) {
return;
}
- final parent = selection.value.elementAt(0);
- selection.value.remove(parent);
- await ref.read(assetStackServiceProvider).updateStack(
- parent,
- childrenToAdd: selection.value.toList(),
+
+ await ref.read(stackServiceProvider).createStack(
+ selection.value.map((e) => e.remoteId!).toList(),
);
} finally {
processing.value = false;
diff --git a/mobile/lib/widgets/asset_grid/thumbnail_image.dart b/mobile/lib/widgets/asset_grid/thumbnail_image.dart
index 2480f44278bb1..8e818f64fb7cc 100644
--- a/mobile/lib/widgets/asset_grid/thumbnail_image.dart
+++ b/mobile/lib/widgets/asset_grid/thumbnail_image.dart
@@ -107,16 +107,16 @@ class ThumbnailImage extends ConsumerWidget {
right: 8,
child: Row(
children: [
- if (asset.stackChildrenCount > 1)
+ if (asset.stackCount > 1)
Text(
- "${asset.stackChildrenCount}",
+ "${asset.stackCount}",
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
- if (asset.stackChildrenCount > 1)
+ if (asset.stackCount > 1)
const SizedBox(
width: 3,
),
@@ -208,7 +208,7 @@ class ThumbnailImage extends ConsumerWidget {
),
),
if (!asset.isImage) buildVideoIcon(),
- if (asset.stackChildrenCount > 0) buildStackIcon(),
+ if (asset.stackCount > 0) buildStackIcon(),
],
);
}
diff --git a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart
index fb70ac309ed7f..7d9e49bd29305 100644
--- a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart
+++ b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart
@@ -11,7 +11,7 @@ import 'package:immich_mobile/providers/album/shared_album.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/image_viewer_page_state.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
-import 'package:immich_mobile/services/asset_stack.service.dart';
+import 'package:immich_mobile/services/stack.service.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart';
import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart';
@@ -49,11 +49,10 @@ class BottomGalleryBar extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final isOwner = asset.ownerId == ref.watch(currentUserProvider)?.isarId;
- final stack = showStack && asset.stackChildrenCount > 0
+ final stackItems = showStack && asset.stackCount > 0
? ref.watch(assetStackStateProvider(asset))
: [];
- final stackElements = showStack ? [asset, ...stack] : [];
- bool isParent = stackIndex == -1 || stackIndex == 0;
+ bool isStackPrimaryAsset = asset.stackPrimaryAssetId == null;
final navStack = AutoRouter.of(context).stackData;
final isTrashEnabled =
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
@@ -76,7 +75,7 @@ class BottomGalleryBar extends ConsumerWidget {
{asset},
force: force,
);
- if (isDeleted && isParent) {
+ if (isDeleted && isStackPrimaryAsset) {
// Workaround for asset remaining in the gallery
renderList.deleteAsset(asset);
@@ -98,7 +97,7 @@ class BottomGalleryBar extends ConsumerWidget {
final isDeleted = await onDelete(false);
if (isDeleted) {
// Can only trash assets stored in server. Local assets are always permanently removed for now
- if (context.mounted && asset.isRemote && isParent) {
+ if (context.mounted && asset.isRemote && isStackPrimaryAsset) {
ImmichToast.show(
durationInSecond: 1,
context: context,
@@ -127,6 +126,16 @@ class BottomGalleryBar extends ConsumerWidget {
);
}
+ unStack() async {
+ if (asset.stackId == null) {
+ return;
+ }
+
+ await ref
+ .read(stackServiceProvider)
+ .deleteStack(asset.stackId!, [asset, ...stackItems]);
+ }
+
void showStackActionItems() {
showModalBottomSheet(
context: context,
@@ -138,74 +147,13 @@ class BottomGalleryBar extends ConsumerWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
- if (!isParent)
- ListTile(
- leading: const Icon(
- Icons.bookmark_border_outlined,
- size: 24,
- ),
- onTap: () async {
- await ref
- .read(assetStackServiceProvider)
- .updateStackParent(
- asset,
- stackElements.elementAt(stackIndex),
- );
- ctx.pop();
- context.maybePop();
- },
- title: const Text(
- "viewer_stack_use_as_main_asset",
- style: TextStyle(fontWeight: FontWeight.bold),
- ).tr(),
- ),
- ListTile(
- leading: const Icon(
- Icons.copy_all_outlined,
- size: 24,
- ),
- onTap: () async {
- if (isParent) {
- await ref
- .read(assetStackServiceProvider)
- .updateStackParent(
- asset,
- stackElements
- .elementAt(1), // Next asset as parent
- );
- // Remove itself from stack
- await ref.read(assetStackServiceProvider).updateStack(
- stackElements.elementAt(1),
- childrenToRemove: [asset],
- );
- ctx.pop();
- context.maybePop();
- } else {
- await ref.read(assetStackServiceProvider).updateStack(
- asset,
- childrenToRemove: [
- stackElements.elementAt(stackIndex),
- ],
- );
- removeAssetFromStack();
- ctx.pop();
- }
- },
- title: const Text(
- "viewer_remove_from_stack",
- style: TextStyle(fontWeight: FontWeight.bold),
- ).tr(),
- ),
ListTile(
leading: const Icon(
Icons.filter_none_outlined,
size: 18,
),
onTap: () async {
- await ref.read(assetStackServiceProvider).updateStack(
- asset,
- childrenToRemove: stack,
- );
+ await unStack();
ctx.pop();
context.maybePop();
},
@@ -255,7 +203,7 @@ class BottomGalleryBar extends ConsumerWidget {
handleArchive() {
ref.read(assetProvider.notifier).toggleArchive([asset]);
- if (isParent) {
+ if (isStackPrimaryAsset) {
context.maybePop();
return;
}
@@ -346,7 +294,7 @@ class BottomGalleryBar extends ConsumerWidget {
tooltip: 'control_bottom_app_bar_archive'.tr(),
): (_) => handleArchive(),
},
- if (isOwner && stack.isNotEmpty)
+ if (isOwner && asset.stackCount > 0)
{
BottomNavigationBarItem(
icon: const Icon(Icons.burst_mode_outlined),
diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md
index 657dad9d5b33b..f2effe1c2060b 100644
--- a/mobile/openapi/README.md
+++ b/mobile/openapi/README.md
@@ -107,7 +107,6 @@ Class | Method | HTTP request | Description
*AssetsApi* | [**runAssetJobs**](doc//AssetsApi.md#runassetjobs) | **POST** /assets/jobs |
*AssetsApi* | [**updateAsset**](doc//AssetsApi.md#updateasset) | **PUT** /assets/{id} |
*AssetsApi* | [**updateAssets**](doc//AssetsApi.md#updateassets) | **PUT** /assets |
-*AssetsApi* | [**updateStackParent**](doc//AssetsApi.md#updatestackparent) | **PUT** /assets/stack/parent |
*AssetsApi* | [**uploadAsset**](doc//AssetsApi.md#uploadasset) | **POST** /assets |
*AssetsApi* | [**viewAsset**](doc//AssetsApi.md#viewasset) | **GET** /assets/{id}/thumbnail |
*AuditApi* | [**getAuditDeletes**](doc//AuditApi.md#getauditdeletes) | **GET** /audit/deletes |
@@ -205,6 +204,12 @@ Class | Method | HTTP request | Description
*SharedLinksApi* | [**removeSharedLink**](doc//SharedLinksApi.md#removesharedlink) | **DELETE** /shared-links/{id} |
*SharedLinksApi* | [**removeSharedLinkAssets**](doc//SharedLinksApi.md#removesharedlinkassets) | **DELETE** /shared-links/{id}/assets |
*SharedLinksApi* | [**updateSharedLink**](doc//SharedLinksApi.md#updatesharedlink) | **PATCH** /shared-links/{id} |
+*StacksApi* | [**createStack**](doc//StacksApi.md#createstack) | **POST** /stacks |
+*StacksApi* | [**deleteStack**](doc//StacksApi.md#deletestack) | **DELETE** /stacks/{id} |
+*StacksApi* | [**deleteStacks**](doc//StacksApi.md#deletestacks) | **DELETE** /stacks |
+*StacksApi* | [**getStack**](doc//StacksApi.md#getstack) | **GET** /stacks/{id} |
+*StacksApi* | [**searchStacks**](doc//StacksApi.md#searchstacks) | **GET** /stacks |
+*StacksApi* | [**updateStack**](doc//StacksApi.md#updatestack) | **PUT** /stacks/{id} |
*SyncApi* | [**getDeltaSync**](doc//SyncApi.md#getdeltasync) | **POST** /sync/delta-sync |
*SyncApi* | [**getFullSyncForUser**](doc//SyncApi.md#getfullsyncforuser) | **POST** /sync/full-sync |
*SystemConfigApi* | [**getConfig**](doc//SystemConfigApi.md#getconfig) | **GET** /system-config |
@@ -289,6 +294,7 @@ Class | Method | HTTP request | Description
- [AssetMediaStatus](doc//AssetMediaStatus.md)
- [AssetOrder](doc//AssetOrder.md)
- [AssetResponseDto](doc//AssetResponseDto.md)
+ - [AssetStackResponseDto](doc//AssetStackResponseDto.md)
- [AssetStatsResponseDto](doc//AssetStatsResponseDto.md)
- [AssetTypeEnum](doc//AssetTypeEnum.md)
- [AudioCodec](doc//AudioCodec.md)
@@ -404,6 +410,9 @@ Class | Method | HTTP request | Description
- [SignUpDto](doc//SignUpDto.md)
- [SmartInfoResponseDto](doc//SmartInfoResponseDto.md)
- [SmartSearchDto](doc//SmartSearchDto.md)
+ - [StackCreateDto](doc//StackCreateDto.md)
+ - [StackResponseDto](doc//StackResponseDto.md)
+ - [StackUpdateDto](doc//StackUpdateDto.md)
- [SystemConfigDto](doc//SystemConfigDto.md)
- [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md)
- [SystemConfigImageDto](doc//SystemConfigImageDto.md)
@@ -439,7 +448,6 @@ Class | Method | HTTP request | Description
- [UpdateAssetDto](doc//UpdateAssetDto.md)
- [UpdateLibraryDto](doc//UpdateLibraryDto.md)
- [UpdatePartnerDto](doc//UpdatePartnerDto.md)
- - [UpdateStackParentDto](doc//UpdateStackParentDto.md)
- [UpdateTagDto](doc//UpdateTagDto.md)
- [UsageByUserDto](doc//UsageByUserDto.md)
- [UserAdminCreateDto](doc//UserAdminCreateDto.md)
diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart
index 4d33f1018cb52..6ee06d53042bb 100644
--- a/mobile/openapi/lib/api.dart
+++ b/mobile/openapi/lib/api.dart
@@ -54,6 +54,7 @@ part 'api/server_api.dart';
part 'api/server_info_api.dart';
part 'api/sessions_api.dart';
part 'api/shared_links_api.dart';
+part 'api/stacks_api.dart';
part 'api/sync_api.dart';
part 'api/system_config_api.dart';
part 'api/system_metadata_api.dart';
@@ -101,6 +102,7 @@ part 'model/asset_media_size.dart';
part 'model/asset_media_status.dart';
part 'model/asset_order.dart';
part 'model/asset_response_dto.dart';
+part 'model/asset_stack_response_dto.dart';
part 'model/asset_stats_response_dto.dart';
part 'model/asset_type_enum.dart';
part 'model/audio_codec.dart';
@@ -216,6 +218,9 @@ part 'model/shared_link_type.dart';
part 'model/sign_up_dto.dart';
part 'model/smart_info_response_dto.dart';
part 'model/smart_search_dto.dart';
+part 'model/stack_create_dto.dart';
+part 'model/stack_response_dto.dart';
+part 'model/stack_update_dto.dart';
part 'model/system_config_dto.dart';
part 'model/system_config_f_fmpeg_dto.dart';
part 'model/system_config_image_dto.dart';
@@ -251,7 +256,6 @@ part 'model/update_album_user_dto.dart';
part 'model/update_asset_dto.dart';
part 'model/update_library_dto.dart';
part 'model/update_partner_dto.dart';
-part 'model/update_stack_parent_dto.dart';
part 'model/update_tag_dto.dart';
part 'model/usage_by_user_dto.dart';
part 'model/user_admin_create_dto.dart';
diff --git a/mobile/openapi/lib/api/assets_api.dart b/mobile/openapi/lib/api/assets_api.dart
index d7d386130bc02..ceba3574cd17a 100644
--- a/mobile/openapi/lib/api/assets_api.dart
+++ b/mobile/openapi/lib/api/assets_api.dart
@@ -804,45 +804,6 @@ class AssetsApi {
}
}
- /// Performs an HTTP 'PUT /assets/stack/parent' operation and returns the [Response].
- /// Parameters:
- ///
- /// * [UpdateStackParentDto] updateStackParentDto (required):
- Future updateStackParentWithHttpInfo(UpdateStackParentDto updateStackParentDto,) async {
- // ignore: prefer_const_declarations
- final path = r'/assets/stack/parent';
-
- // ignore: prefer_final_locals
- Object? postBody = updateStackParentDto;
-
- final queryParams = [];
- final headerParams = {};
- final formParams = {};
-
- const contentTypes = ['application/json'];
-
-
- return apiClient.invokeAPI(
- path,
- 'PUT',
- queryParams,
- postBody,
- headerParams,
- formParams,
- contentTypes.isEmpty ? null : contentTypes.first,
- );
- }
-
- /// Parameters:
- ///
- /// * [UpdateStackParentDto] updateStackParentDto (required):
- Future updateStackParent(UpdateStackParentDto updateStackParentDto,) async {
- final response = await updateStackParentWithHttpInfo(updateStackParentDto,);
- if (response.statusCode >= HttpStatus.badRequest) {
- throw ApiException(response.statusCode, await _decodeBodyBytes(response));
- }
- }
-
/// Performs an HTTP 'POST /assets' operation and returns the [Response].
/// Parameters:
///
diff --git a/mobile/openapi/lib/api/stacks_api.dart b/mobile/openapi/lib/api/stacks_api.dart
new file mode 100644
index 0000000000000..aa1d9b341615d
--- /dev/null
+++ b/mobile/openapi/lib/api/stacks_api.dart
@@ -0,0 +1,298 @@
+//
+// 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 StacksApi {
+ StacksApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
+
+ final ApiClient apiClient;
+
+ /// Performs an HTTP 'POST /stacks' operation and returns the [Response].
+ /// Parameters:
+ ///
+ /// * [StackCreateDto] stackCreateDto (required):
+ Future createStackWithHttpInfo(StackCreateDto stackCreateDto,) async {
+ // ignore: prefer_const_declarations
+ final path = r'/stacks';
+
+ // ignore: prefer_final_locals
+ Object? postBody = stackCreateDto;
+
+ final queryParams = [];
+ final headerParams = {};
+ final formParams = {};
+
+ const contentTypes = ['application/json'];
+
+
+ return apiClient.invokeAPI(
+ path,
+ 'POST',
+ queryParams,
+ postBody,
+ headerParams,
+ formParams,
+ contentTypes.isEmpty ? null : contentTypes.first,
+ );
+ }
+
+ /// Parameters:
+ ///
+ /// * [StackCreateDto] stackCreateDto (required):
+ Future createStack(StackCreateDto stackCreateDto,) async {
+ final response = await createStackWithHttpInfo(stackCreateDto,);
+ 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), 'StackResponseDto',) as StackResponseDto;
+
+ }
+ return null;
+ }
+
+ /// Performs an HTTP 'DELETE /stacks/{id}' operation and returns the [Response].
+ /// Parameters:
+ ///
+ /// * [String] id (required):
+ Future deleteStackWithHttpInfo(String id,) async {
+ // ignore: prefer_const_declarations
+ final path = r'/stacks/{id}'
+ .replaceAll('{id}', id);
+
+ // ignore: prefer_final_locals
+ Object? postBody;
+
+ final queryParams = [];
+ final headerParams = {};
+ final formParams = {};
+
+ const contentTypes = [];
+
+
+ return apiClient.invokeAPI(
+ path,
+ 'DELETE',
+ queryParams,
+ postBody,
+ headerParams,
+ formParams,
+ contentTypes.isEmpty ? null : contentTypes.first,
+ );
+ }
+
+ /// Parameters:
+ ///
+ /// * [String] id (required):
+ Future deleteStack(String id,) async {
+ final response = await deleteStackWithHttpInfo(id,);
+ if (response.statusCode >= HttpStatus.badRequest) {
+ throw ApiException(response.statusCode, await _decodeBodyBytes(response));
+ }
+ }
+
+ /// Performs an HTTP 'DELETE /stacks' operation and returns the [Response].
+ /// Parameters:
+ ///
+ /// * [BulkIdsDto] bulkIdsDto (required):
+ Future deleteStacksWithHttpInfo(BulkIdsDto bulkIdsDto,) async {
+ // ignore: prefer_const_declarations
+ final path = r'/stacks';
+
+ // ignore: prefer_final_locals
+ Object? postBody = bulkIdsDto;
+
+ final queryParams = [];
+ final headerParams = {};
+ final formParams = {};
+
+ const contentTypes = ['application/json'];
+
+
+ return apiClient.invokeAPI(
+ path,
+ 'DELETE',
+ queryParams,
+ postBody,
+ headerParams,
+ formParams,
+ contentTypes.isEmpty ? null : contentTypes.first,
+ );
+ }
+
+ /// Parameters:
+ ///
+ /// * [BulkIdsDto] bulkIdsDto (required):
+ Future deleteStacks(BulkIdsDto bulkIdsDto,) async {
+ final response = await deleteStacksWithHttpInfo(bulkIdsDto,);
+ if (response.statusCode >= HttpStatus.badRequest) {
+ throw ApiException(response.statusCode, await _decodeBodyBytes(response));
+ }
+ }
+
+ /// Performs an HTTP 'GET /stacks/{id}' operation and returns the [Response].
+ /// Parameters:
+ ///
+ /// * [String] id (required):
+ Future getStackWithHttpInfo(String id,) async {
+ // ignore: prefer_const_declarations
+ final path = r'/stacks/{id}'
+ .replaceAll('{id}', id);
+
+ // ignore: prefer_final_locals
+ Object? postBody;
+
+ final queryParams = [];
+ final headerParams = {};
+ final formParams = {};
+
+ const contentTypes = [];
+
+
+ return apiClient.invokeAPI(
+ path,
+ 'GET',
+ queryParams,
+ postBody,
+ headerParams,
+ formParams,
+ contentTypes.isEmpty ? null : contentTypes.first,
+ );
+ }
+
+ /// Parameters:
+ ///
+ /// * [String] id (required):
+ Future getStack(String id,) async {
+ final response = await getStackWithHttpInfo(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), 'StackResponseDto',) as StackResponseDto;
+
+ }
+ return null;
+ }
+
+ /// Performs an HTTP 'GET /stacks' operation and returns the [Response].
+ /// Parameters:
+ ///
+ /// * [String] primaryAssetId:
+ Future searchStacksWithHttpInfo({ String? primaryAssetId, }) async {
+ // ignore: prefer_const_declarations
+ final path = r'/stacks';
+
+ // ignore: prefer_final_locals
+ Object? postBody;
+
+ final queryParams = [];
+ final headerParams = {};
+ final formParams = {};
+
+ if (primaryAssetId != null) {
+ queryParams.addAll(_queryParams('', 'primaryAssetId', primaryAssetId));
+ }
+
+ const contentTypes = [];
+
+
+ return apiClient.invokeAPI(
+ path,
+ 'GET',
+ queryParams,
+ postBody,
+ headerParams,
+ formParams,
+ contentTypes.isEmpty ? null : contentTypes.first,
+ );
+ }
+
+ /// Parameters:
+ ///
+ /// * [String] primaryAssetId:
+ Future?> searchStacks({ String? primaryAssetId, }) async {
+ final response = await searchStacksWithHttpInfo( primaryAssetId: primaryAssetId, );
+ if (response.statusCode >= HttpStatus.badRequest) {
+ throw ApiException(response.statusCode, await _decodeBodyBytes(response));
+ }
+ // When a remote server returns no body with a status of 204, we shall not decode it.
+ // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
+ // FormatException when trying to decode an empty string.
+ if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
+ final responseBody = await _decodeBodyBytes(response);
+ return (await apiClient.deserializeAsync(responseBody, 'List') as List)
+ .cast()
+ .toList(growable: false);
+
+ }
+ return null;
+ }
+
+ /// Performs an HTTP 'PUT /stacks/{id}' operation and returns the [Response].
+ /// Parameters:
+ ///
+ /// * [String] id (required):
+ ///
+ /// * [StackUpdateDto] stackUpdateDto (required):
+ Future updateStackWithHttpInfo(String id, StackUpdateDto stackUpdateDto,) async {
+ // ignore: prefer_const_declarations
+ final path = r'/stacks/{id}'
+ .replaceAll('{id}', id);
+
+ // ignore: prefer_final_locals
+ Object? postBody = stackUpdateDto;
+
+ final queryParams = [];
+ final headerParams = {};
+ final formParams = {};
+
+ const contentTypes = ['application/json'];
+
+
+ return apiClient.invokeAPI(
+ path,
+ 'PUT',
+ queryParams,
+ postBody,
+ headerParams,
+ formParams,
+ contentTypes.isEmpty ? null : contentTypes.first,
+ );
+ }
+
+ /// Parameters:
+ ///
+ /// * [String] id (required):
+ ///
+ /// * [StackUpdateDto] stackUpdateDto (required):
+ Future updateStack(String id, StackUpdateDto stackUpdateDto,) async {
+ final response = await updateStackWithHttpInfo(id, stackUpdateDto,);
+ 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), 'StackResponseDto',) as StackResponseDto;
+
+ }
+ return null;
+ }
+}
diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart
index b5b79be8b143c..935324272d7b5 100644
--- a/mobile/openapi/lib/api_client.dart
+++ b/mobile/openapi/lib/api_client.dart
@@ -259,6 +259,8 @@ class ApiClient {
return AssetOrderTypeTransformer().decode(value);
case 'AssetResponseDto':
return AssetResponseDto.fromJson(value);
+ case 'AssetStackResponseDto':
+ return AssetStackResponseDto.fromJson(value);
case 'AssetStatsResponseDto':
return AssetStatsResponseDto.fromJson(value);
case 'AssetTypeEnum':
@@ -489,6 +491,12 @@ class ApiClient {
return SmartInfoResponseDto.fromJson(value);
case 'SmartSearchDto':
return SmartSearchDto.fromJson(value);
+ case 'StackCreateDto':
+ return StackCreateDto.fromJson(value);
+ case 'StackResponseDto':
+ return StackResponseDto.fromJson(value);
+ case 'StackUpdateDto':
+ return StackUpdateDto.fromJson(value);
case 'SystemConfigDto':
return SystemConfigDto.fromJson(value);
case 'SystemConfigFFmpegDto':
@@ -559,8 +567,6 @@ class ApiClient {
return UpdateLibraryDto.fromJson(value);
case 'UpdatePartnerDto':
return UpdatePartnerDto.fromJson(value);
- case 'UpdateStackParentDto':
- return UpdateStackParentDto.fromJson(value);
case 'UpdateTagDto':
return UpdateTagDto.fromJson(value);
case 'UsageByUserDto':
diff --git a/mobile/openapi/lib/model/asset_bulk_update_dto.dart b/mobile/openapi/lib/model/asset_bulk_update_dto.dart
index 452dd2f9a51f1..c9b21683fbcec 100644
--- a/mobile/openapi/lib/model/asset_bulk_update_dto.dart
+++ b/mobile/openapi/lib/model/asset_bulk_update_dto.dart
@@ -21,8 +21,6 @@ class AssetBulkUpdateDto {
this.latitude,
this.longitude,
this.rating,
- this.removeParent,
- this.stackParentId,
});
///
@@ -79,22 +77,6 @@ class AssetBulkUpdateDto {
///
num? rating;
- ///
- /// 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.
- ///
- bool? removeParent;
-
- ///
- /// 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? stackParentId;
-
@override
bool operator ==(Object other) => identical(this, other) || other is AssetBulkUpdateDto &&
other.dateTimeOriginal == dateTimeOriginal &&
@@ -104,9 +86,7 @@ class AssetBulkUpdateDto {
other.isFavorite == isFavorite &&
other.latitude == latitude &&
other.longitude == longitude &&
- other.rating == rating &&
- other.removeParent == removeParent &&
- other.stackParentId == stackParentId;
+ other.rating == rating;
@override
int get hashCode =>
@@ -118,12 +98,10 @@ class AssetBulkUpdateDto {
(isFavorite == null ? 0 : isFavorite!.hashCode) +
(latitude == null ? 0 : latitude!.hashCode) +
(longitude == null ? 0 : longitude!.hashCode) +
- (rating == null ? 0 : rating!.hashCode) +
- (removeParent == null ? 0 : removeParent!.hashCode) +
- (stackParentId == null ? 0 : stackParentId!.hashCode);
+ (rating == null ? 0 : rating!.hashCode);
@override
- String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, duplicateId=$duplicateId, ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, rating=$rating, removeParent=$removeParent, stackParentId=$stackParentId]';
+ String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, duplicateId=$duplicateId, ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, rating=$rating]';
Map toJson() {
final json = {};
@@ -163,16 +141,6 @@ class AssetBulkUpdateDto {
} else {
// json[r'rating'] = null;
}
- if (this.removeParent != null) {
- json[r'removeParent'] = this.removeParent;
- } else {
- // json[r'removeParent'] = null;
- }
- if (this.stackParentId != null) {
- json[r'stackParentId'] = this.stackParentId;
- } else {
- // json[r'stackParentId'] = null;
- }
return json;
}
@@ -194,8 +162,6 @@ class AssetBulkUpdateDto {
latitude: num.parse('${json[r'latitude']}'),
longitude: num.parse('${json[r'longitude']}'),
rating: num.parse('${json[r'rating']}'),
- removeParent: mapValueOfType(json, r'removeParent'),
- stackParentId: mapValueOfType(json, r'stackParentId'),
);
}
return null;
diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart
index 61e33ef4e0728..561a42cc852cf 100644
--- a/mobile/openapi/lib/model/asset_response_dto.dart
+++ b/mobile/openapi/lib/model/asset_response_dto.dart
@@ -38,9 +38,7 @@ class AssetResponseDto {
this.people = const [],
required this.resized,
this.smartInfo,
- this.stack = const [],
- required this.stackCount,
- this.stackParentId,
+ this.stack,
this.tags = const [],
required this.thumbhash,
required this.type,
@@ -124,11 +122,7 @@ class AssetResponseDto {
///
SmartInfoResponseDto? smartInfo;
- List stack;
-
- int? stackCount;
-
- String? stackParentId;
+ AssetStackResponseDto? stack;
List tags;
@@ -167,9 +161,7 @@ class AssetResponseDto {
_deepEquality.equals(other.people, people) &&
other.resized == resized &&
other.smartInfo == smartInfo &&
- _deepEquality.equals(other.stack, stack) &&
- other.stackCount == stackCount &&
- other.stackParentId == stackParentId &&
+ other.stack == stack &&
_deepEquality.equals(other.tags, tags) &&
other.thumbhash == thumbhash &&
other.type == type &&
@@ -204,9 +196,7 @@ class AssetResponseDto {
(people.hashCode) +
(resized.hashCode) +
(smartInfo == null ? 0 : smartInfo!.hashCode) +
- (stack.hashCode) +
- (stackCount == null ? 0 : stackCount!.hashCode) +
- (stackParentId == null ? 0 : stackParentId!.hashCode) +
+ (stack == null ? 0 : stack!.hashCode) +
(tags.hashCode) +
(thumbhash == null ? 0 : thumbhash!.hashCode) +
(type.hashCode) +
@@ -214,7 +204,7 @@ class AssetResponseDto {
(updatedAt.hashCode);
@override
- String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, 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, smartInfo=$smartInfo, stack=$stack, stackCount=$stackCount, stackParentId=$stackParentId, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt]';
+ String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, 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, smartInfo=$smartInfo, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt]';
Map toJson() {
final json = {};
@@ -271,16 +261,10 @@ class AssetResponseDto {
} else {
// json[r'smartInfo'] = null;
}
+ if (this.stack != null) {
json[r'stack'] = this.stack;
- if (this.stackCount != null) {
- json[r'stackCount'] = this.stackCount;
} else {
- // json[r'stackCount'] = null;
- }
- if (this.stackParentId != null) {
- json[r'stackParentId'] = this.stackParentId;
- } else {
- // json[r'stackParentId'] = null;
+ // json[r'stack'] = null;
}
json[r'tags'] = this.tags;
if (this.thumbhash != null) {
@@ -327,9 +311,7 @@ class AssetResponseDto {
people: PersonWithFacesResponseDto.listFromJson(json[r'people']),
resized: mapValueOfType(json, r'resized')!,
smartInfo: SmartInfoResponseDto.fromJson(json[r'smartInfo']),
- stack: AssetResponseDto.listFromJson(json[r'stack']),
- stackCount: mapValueOfType(json, r'stackCount'),
- stackParentId: mapValueOfType(json, r'stackParentId'),
+ stack: AssetStackResponseDto.fromJson(json[r'stack']),
tags: TagResponseDto.listFromJson(json[r'tags']),
thumbhash: mapValueOfType(json, r'thumbhash'),
type: AssetTypeEnum.fromJson(json[r'type'])!,
@@ -399,7 +381,6 @@ class AssetResponseDto {
'originalPath',
'ownerId',
'resized',
- 'stackCount',
'thumbhash',
'type',
'updatedAt',
diff --git a/mobile/openapi/lib/model/asset_stack_response_dto.dart b/mobile/openapi/lib/model/asset_stack_response_dto.dart
new file mode 100644
index 0000000000000..89d30f7810682
--- /dev/null
+++ b/mobile/openapi/lib/model/asset_stack_response_dto.dart
@@ -0,0 +1,114 @@
+//
+// 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 AssetStackResponseDto {
+ /// Returns a new [AssetStackResponseDto] instance.
+ AssetStackResponseDto({
+ required this.assetCount,
+ required this.id,
+ required this.primaryAssetId,
+ });
+
+ int assetCount;
+
+ String id;
+
+ String primaryAssetId;
+
+ @override
+ bool operator ==(Object other) => identical(this, other) || other is AssetStackResponseDto &&
+ other.assetCount == assetCount &&
+ other.id == id &&
+ other.primaryAssetId == primaryAssetId;
+
+ @override
+ int get hashCode =>
+ // ignore: unnecessary_parenthesis
+ (assetCount.hashCode) +
+ (id.hashCode) +
+ (primaryAssetId.hashCode);
+
+ @override
+ String toString() => 'AssetStackResponseDto[assetCount=$assetCount, id=$id, primaryAssetId=$primaryAssetId]';
+
+ Map toJson() {
+ final json = {};
+ json[r'assetCount'] = this.assetCount;
+ json[r'id'] = this.id;
+ json[r'primaryAssetId'] = this.primaryAssetId;
+ return json;
+ }
+
+ /// Returns a new [AssetStackResponseDto] instance and imports its values from
+ /// [value] if it's a [Map], null otherwise.
+ // ignore: prefer_constructors_over_static_methods
+ static AssetStackResponseDto? fromJson(dynamic value) {
+ if (value is Map) {
+ final json = value.cast();
+
+ return AssetStackResponseDto(
+ assetCount: mapValueOfType(json, r'assetCount')!,
+ id: mapValueOfType(json, r'id')!,
+ primaryAssetId: mapValueOfType(json, r'primaryAssetId')!,
+ );
+ }
+ return null;
+ }
+
+ static List listFromJson(dynamic json, {bool growable = false,}) {
+ final result = [];
+ if (json is List && json.isNotEmpty) {
+ for (final row in json) {
+ final value = AssetStackResponseDto.fromJson(row);
+ if (value != null) {
+ result.add(value);
+ }
+ }
+ }
+ return result.toList(growable: growable);
+ }
+
+ static Map mapFromJson(dynamic json) {
+ final map = {};
+ if (json is Map && json.isNotEmpty) {
+ json = json.cast(); // ignore: parameter_assignments
+ for (final entry in json.entries) {
+ final value = AssetStackResponseDto.fromJson(entry.value);
+ if (value != null) {
+ map[entry.key] = value;
+ }
+ }
+ }
+ return map;
+ }
+
+ // maps a json object with a list of AssetStackResponseDto-objects as value to a dart map
+ static Map> mapListFromJson(dynamic json, {bool growable = false,}) {
+ final map = >{};
+ if (json is Map && json.isNotEmpty) {
+ // ignore: parameter_assignments
+ json = json.cast();
+ for (final entry in json.entries) {
+ map[entry.key] = AssetStackResponseDto.listFromJson(entry.value, growable: growable,);
+ }
+ }
+ return map;
+ }
+
+ /// The list of required keys that must be present in a JSON.
+ static const requiredKeys = {
+ 'assetCount',
+ 'id',
+ 'primaryAssetId',
+ };
+}
+
diff --git a/mobile/openapi/lib/model/permission.dart b/mobile/openapi/lib/model/permission.dart
index 30dc89a47ca45..3a9b61d81c1b6 100644
--- a/mobile/openapi/lib/model/permission.dart
+++ b/mobile/openapi/lib/model/permission.dart
@@ -82,6 +82,10 @@ class Permission {
static const sharedLinkPeriodRead = Permission._(r'sharedLink.read');
static const sharedLinkPeriodUpdate = Permission._(r'sharedLink.update');
static const sharedLinkPeriodDelete = Permission._(r'sharedLink.delete');
+ static const stackPeriodCreate = Permission._(r'stack.create');
+ static const stackPeriodRead = Permission._(r'stack.read');
+ static const stackPeriodUpdate = Permission._(r'stack.update');
+ static const stackPeriodDelete = Permission._(r'stack.delete');
static const systemConfigPeriodRead = Permission._(r'systemConfig.read');
static const systemConfigPeriodUpdate = Permission._(r'systemConfig.update');
static const systemMetadataPeriodRead = Permission._(r'systemMetadata.read');
@@ -156,6 +160,10 @@ class Permission {
sharedLinkPeriodRead,
sharedLinkPeriodUpdate,
sharedLinkPeriodDelete,
+ stackPeriodCreate,
+ stackPeriodRead,
+ stackPeriodUpdate,
+ stackPeriodDelete,
systemConfigPeriodRead,
systemConfigPeriodUpdate,
systemMetadataPeriodRead,
@@ -265,6 +273,10 @@ class PermissionTypeTransformer {
case r'sharedLink.read': return Permission.sharedLinkPeriodRead;
case r'sharedLink.update': return Permission.sharedLinkPeriodUpdate;
case r'sharedLink.delete': return Permission.sharedLinkPeriodDelete;
+ case r'stack.create': return Permission.stackPeriodCreate;
+ case r'stack.read': return Permission.stackPeriodRead;
+ case r'stack.update': return Permission.stackPeriodUpdate;
+ case r'stack.delete': return Permission.stackPeriodDelete;
case r'systemConfig.read': return Permission.systemConfigPeriodRead;
case r'systemConfig.update': return Permission.systemConfigPeriodUpdate;
case r'systemMetadata.read': return Permission.systemMetadataPeriodRead;
diff --git a/mobile/openapi/lib/model/stack_create_dto.dart b/mobile/openapi/lib/model/stack_create_dto.dart
new file mode 100644
index 0000000000000..9b37bc6e2e9aa
--- /dev/null
+++ b/mobile/openapi/lib/model/stack_create_dto.dart
@@ -0,0 +1,101 @@
+//
+// 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 StackCreateDto {
+ /// Returns a new [StackCreateDto] instance.
+ StackCreateDto({
+ this.assetIds = const [],
+ });
+
+ /// first asset becomes the primary
+ List assetIds;
+
+ @override
+ bool operator ==(Object other) => identical(this, other) || other is StackCreateDto &&
+ _deepEquality.equals(other.assetIds, assetIds);
+
+ @override
+ int get hashCode =>
+ // ignore: unnecessary_parenthesis
+ (assetIds.hashCode);
+
+ @override
+ String toString() => 'StackCreateDto[assetIds=$assetIds]';
+
+ Map toJson() {
+ final json = {};
+ json[r'assetIds'] = this.assetIds;
+ return json;
+ }
+
+ /// Returns a new [StackCreateDto] instance and imports its values from
+ /// [value] if it's a [Map], null otherwise.
+ // ignore: prefer_constructors_over_static_methods
+ static StackCreateDto? fromJson(dynamic value) {
+ if (value is Map) {
+ final json = value.cast();
+
+ return StackCreateDto(
+ assetIds: json[r'assetIds'] is Iterable
+ ? (json[r'assetIds'] as Iterable).cast().toList(growable: false)
+ : const [],
+ );
+ }
+ return null;
+ }
+
+ static List listFromJson(dynamic json, {bool growable = false,}) {
+ final result = [];
+ if (json is List && json.isNotEmpty) {
+ for (final row in json) {
+ final value = StackCreateDto.fromJson(row);
+ if (value != null) {
+ result.add(value);
+ }
+ }
+ }
+ return result.toList(growable: growable);
+ }
+
+ static Map mapFromJson(dynamic json) {
+ final map = {};
+ if (json is Map && json.isNotEmpty) {
+ json = json.cast(); // ignore: parameter_assignments
+ for (final entry in json.entries) {
+ final value = StackCreateDto.fromJson(entry.value);
+ if (value != null) {
+ map[entry.key] = value;
+ }
+ }
+ }
+ return map;
+ }
+
+ // maps a json object with a list of StackCreateDto-objects as value to a dart map
+ static Map> mapListFromJson(dynamic json, {bool growable = false,}) {
+ final map = >{};
+ if (json is Map && json.isNotEmpty) {
+ // ignore: parameter_assignments
+ json = json.cast();
+ for (final entry in json.entries) {
+ map[entry.key] = StackCreateDto.listFromJson(entry.value, growable: growable,);
+ }
+ }
+ return map;
+ }
+
+ /// The list of required keys that must be present in a JSON.
+ static const requiredKeys = {
+ 'assetIds',
+ };
+}
+
diff --git a/mobile/openapi/lib/model/stack_response_dto.dart b/mobile/openapi/lib/model/stack_response_dto.dart
new file mode 100644
index 0000000000000..3d0aaf91d17cc
--- /dev/null
+++ b/mobile/openapi/lib/model/stack_response_dto.dart
@@ -0,0 +1,114 @@
+//
+// 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 StackResponseDto {
+ /// Returns a new [StackResponseDto] instance.
+ StackResponseDto({
+ this.assets = const [],
+ required this.id,
+ required this.primaryAssetId,
+ });
+
+ List assets;
+
+ String id;
+
+ String primaryAssetId;
+
+ @override
+ bool operator ==(Object other) => identical(this, other) || other is StackResponseDto &&
+ _deepEquality.equals(other.assets, assets) &&
+ other.id == id &&
+ other.primaryAssetId == primaryAssetId;
+
+ @override
+ int get hashCode =>
+ // ignore: unnecessary_parenthesis
+ (assets.hashCode) +
+ (id.hashCode) +
+ (primaryAssetId.hashCode);
+
+ @override
+ String toString() => 'StackResponseDto[assets=$assets, id=$id, primaryAssetId=$primaryAssetId]';
+
+ Map toJson() {
+ final json = {};
+ json[r'assets'] = this.assets;
+ json[r'id'] = this.id;
+ json[r'primaryAssetId'] = this.primaryAssetId;
+ return json;
+ }
+
+ /// Returns a new [StackResponseDto] instance and imports its values from
+ /// [value] if it's a [Map], null otherwise.
+ // ignore: prefer_constructors_over_static_methods
+ static StackResponseDto? fromJson(dynamic value) {
+ if (value is Map) {
+ final json = value.cast();
+
+ return StackResponseDto(
+ assets: AssetResponseDto.listFromJson(json[r'assets']),
+ id: mapValueOfType(json, r'id')!,
+ primaryAssetId: mapValueOfType(json, r'primaryAssetId')!,
+ );
+ }
+ return null;
+ }
+
+ static List listFromJson(dynamic json, {bool growable = false,}) {
+ final result = [];
+ if (json is List && json.isNotEmpty) {
+ for (final row in json) {
+ final value = StackResponseDto.fromJson(row);
+ if (value != null) {
+ result.add(value);
+ }
+ }
+ }
+ return result.toList(growable: growable);
+ }
+
+ static Map mapFromJson(dynamic json) {
+ final map = {};
+ if (json is Map && json.isNotEmpty) {
+ json = json.cast(); // ignore: parameter_assignments
+ for (final entry in json.entries) {
+ final value = StackResponseDto.fromJson(entry.value);
+ if (value != null) {
+ map[entry.key] = value;
+ }
+ }
+ }
+ return map;
+ }
+
+ // maps a json object with a list of StackResponseDto-objects as value to a dart map
+ static Map> mapListFromJson(dynamic json, {bool growable = false,}) {
+ final map = >{};
+ if (json is Map && json.isNotEmpty) {
+ // ignore: parameter_assignments
+ json = json.cast();
+ for (final entry in json.entries) {
+ map[entry.key] = StackResponseDto.listFromJson(entry.value, growable: growable,);
+ }
+ }
+ return map;
+ }
+
+ /// The list of required keys that must be present in a JSON.
+ static const requiredKeys = {
+ 'assets',
+ 'id',
+ 'primaryAssetId',
+ };
+}
+
diff --git a/mobile/openapi/lib/model/stack_update_dto.dart b/mobile/openapi/lib/model/stack_update_dto.dart
new file mode 100644
index 0000000000000..0e9712721048a
--- /dev/null
+++ b/mobile/openapi/lib/model/stack_update_dto.dart
@@ -0,0 +1,107 @@
+//
+// 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 StackUpdateDto {
+ /// Returns a new [StackUpdateDto] instance.
+ StackUpdateDto({
+ this.primaryAssetId,
+ });
+
+ ///
+ /// 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? primaryAssetId;
+
+ @override
+ bool operator ==(Object other) => identical(this, other) || other is StackUpdateDto &&
+ other.primaryAssetId == primaryAssetId;
+
+ @override
+ int get hashCode =>
+ // ignore: unnecessary_parenthesis
+ (primaryAssetId == null ? 0 : primaryAssetId!.hashCode);
+
+ @override
+ String toString() => 'StackUpdateDto[primaryAssetId=$primaryAssetId]';
+
+ Map toJson() {
+ final json = {};
+ if (this.primaryAssetId != null) {
+ json[r'primaryAssetId'] = this.primaryAssetId;
+ } else {
+ // json[r'primaryAssetId'] = null;
+ }
+ return json;
+ }
+
+ /// Returns a new [StackUpdateDto] instance and imports its values from
+ /// [value] if it's a [Map], null otherwise.
+ // ignore: prefer_constructors_over_static_methods
+ static StackUpdateDto? fromJson(dynamic value) {
+ if (value is Map) {
+ final json = value.cast();
+
+ return StackUpdateDto(
+ primaryAssetId: mapValueOfType(json, r'primaryAssetId'),
+ );
+ }
+ return null;
+ }
+
+ static List listFromJson(dynamic json, {bool growable = false,}) {
+ final result = [];
+ if (json is List && json.isNotEmpty) {
+ for (final row in json) {
+ final value = StackUpdateDto.fromJson(row);
+ if (value != null) {
+ result.add(value);
+ }
+ }
+ }
+ return result.toList(growable: growable);
+ }
+
+ static Map mapFromJson(dynamic json) {
+ final map = {};
+ if (json is Map && json.isNotEmpty) {
+ json = json.cast(); // ignore: parameter_assignments
+ for (final entry in json.entries) {
+ final value = StackUpdateDto.fromJson(entry.value);
+ if (value != null) {
+ map[entry.key] = value;
+ }
+ }
+ }
+ return map;
+ }
+
+ // maps a json object with a list of StackUpdateDto-objects as value to a dart map
+ static Map> mapListFromJson(dynamic json, {bool growable = false,}) {
+ final map = >{};
+ if (json is Map && json.isNotEmpty) {
+ // ignore: parameter_assignments
+ json = json.cast();
+ for (final entry in json.entries) {
+ map[entry.key] = StackUpdateDto.listFromJson(entry.value, growable: growable,);
+ }
+ }
+ return map;
+ }
+
+ /// The list of required keys that must be present in a JSON.
+ static const requiredKeys = {
+ };
+}
+
diff --git a/mobile/openapi/lib/model/update_stack_parent_dto.dart b/mobile/openapi/lib/model/update_stack_parent_dto.dart
deleted file mode 100644
index 4247c2e29fe73..0000000000000
--- a/mobile/openapi/lib/model/update_stack_parent_dto.dart
+++ /dev/null
@@ -1,106 +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 UpdateStackParentDto {
- /// Returns a new [UpdateStackParentDto] instance.
- UpdateStackParentDto({
- required this.newParentId,
- required this.oldParentId,
- });
-
- String newParentId;
-
- String oldParentId;
-
- @override
- bool operator ==(Object other) => identical(this, other) || other is UpdateStackParentDto &&
- other.newParentId == newParentId &&
- other.oldParentId == oldParentId;
-
- @override
- int get hashCode =>
- // ignore: unnecessary_parenthesis
- (newParentId.hashCode) +
- (oldParentId.hashCode);
-
- @override
- String toString() => 'UpdateStackParentDto[newParentId=$newParentId, oldParentId=$oldParentId]';
-
- Map toJson() {
- final json = {};
- json[r'newParentId'] = this.newParentId;
- json[r'oldParentId'] = this.oldParentId;
- return json;
- }
-
- /// Returns a new [UpdateStackParentDto] instance and imports its values from
- /// [value] if it's a [Map], null otherwise.
- // ignore: prefer_constructors_over_static_methods
- static UpdateStackParentDto? fromJson(dynamic value) {
- if (value is Map) {
- final json = value.cast();
-
- return UpdateStackParentDto(
- newParentId: mapValueOfType(json, r'newParentId')!,
- oldParentId: mapValueOfType(json, r'oldParentId')!,
- );
- }
- return null;
- }
-
- static List listFromJson(dynamic json, {bool growable = false,}) {
- final result = [];
- if (json is List && json.isNotEmpty) {
- for (final row in json) {
- final value = UpdateStackParentDto.fromJson(row);
- if (value != null) {
- result.add(value);
- }
- }
- }
- return result.toList(growable: growable);
- }
-
- static Map mapFromJson(dynamic json) {
- final map = {};
- if (json is Map && json.isNotEmpty) {
- json = json.cast(); // ignore: parameter_assignments
- for (final entry in json.entries) {
- final value = UpdateStackParentDto.fromJson(entry.value);
- if (value != null) {
- map[entry.key] = value;
- }
- }
- }
- return map;
- }
-
- // maps a json object with a list of UpdateStackParentDto-objects as value to a dart map
- static Map> mapListFromJson(dynamic json, {bool growable = false,}) {
- final map = >{};
- if (json is Map && json.isNotEmpty) {
- // ignore: parameter_assignments
- json = json.cast();
- for (final entry in json.entries) {
- map[entry.key] = UpdateStackParentDto.listFromJson(entry.value, growable: growable,);
- }
- }
- return map;
- }
-
- /// The list of required keys that must be present in a JSON.
- static const requiredKeys = {
- 'newParentId',
- 'oldParentId',
- };
-}
-
diff --git a/mobile/test/fixtures/asset.stub.dart b/mobile/test/fixtures/asset.stub.dart
index b173dd2ac5b9b..26108d63b2f46 100644
--- a/mobile/test/fixtures/asset.stub.dart
+++ b/mobile/test/fixtures/asset.stub.dart
@@ -17,7 +17,6 @@ final class AssetStub {
isFavorite: true,
isArchived: false,
isTrashed: false,
- stackCount: 0,
);
static final image2 = Asset(
@@ -34,6 +33,5 @@ final class AssetStub {
isFavorite: false,
isArchived: false,
isTrashed: false,
- stackCount: 0,
);
}
diff --git a/mobile/test/modules/extensions/asset_extensions_test.dart b/mobile/test/modules/extensions/asset_extensions_test.dart
index b90879acc7f70..d2b9b93d6274a 100644
--- a/mobile/test/modules/extensions/asset_extensions_test.dart
+++ b/mobile/test/modules/extensions/asset_extensions_test.dart
@@ -34,7 +34,6 @@ Asset makeAsset({
isFavorite: false,
isArchived: false,
isTrashed: false,
- stackCount: 0,
exifInfo: exifInfo,
);
}
diff --git a/mobile/test/modules/home/asset_grid_data_structure_test.dart b/mobile/test/modules/home/asset_grid_data_structure_test.dart
index f12b9b219021a..b4ee85196986d 100644
--- a/mobile/test/modules/home/asset_grid_data_structure_test.dart
+++ b/mobile/test/modules/home/asset_grid_data_structure_test.dart
@@ -25,7 +25,6 @@ void main() {
isFavorite: false,
isArchived: false,
isTrashed: false,
- stackCount: 0,
),
);
}
diff --git a/mobile/test/modules/shared/sync_service_test.dart b/mobile/test/modules/shared/sync_service_test.dart
index 24f0c443ba833..07437289beeff 100644
--- a/mobile/test/modules/shared/sync_service_test.dart
+++ b/mobile/test/modules/shared/sync_service_test.dart
@@ -32,7 +32,6 @@ void main() {
isFavorite: false,
isArchived: false,
isTrashed: false,
- stackCount: 0,
);
}
diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json
index 0d0793c263aae..a9b08fc400646 100644
--- a/open-api/immich-openapi-specs.json
+++ b/open-api/immich-openapi-specs.json
@@ -1689,41 +1689,6 @@
]
}
},
- "/assets/stack/parent": {
- "put": {
- "operationId": "updateStackParent",
- "parameters": [],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/UpdateStackParentDto"
- }
- }
- },
- "required": true
- },
- "responses": {
- "200": {
- "description": ""
- }
- },
- "security": [
- {
- "bearer": []
- },
- {
- "cookie": []
- },
- {
- "api_key": []
- }
- ],
- "tags": [
- "Assets"
- ]
- }
- },
"/assets/statistics": {
"get": {
"operationId": "getAssetStatistics",
@@ -5655,6 +5620,248 @@
]
}
},
+ "/stacks": {
+ "delete": {
+ "operationId": "deleteStacks",
+ "parameters": [],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BulkIdsDto"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "204": {
+ "description": ""
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ },
+ {
+ "cookie": []
+ },
+ {
+ "api_key": []
+ }
+ ],
+ "tags": [
+ "Stacks"
+ ]
+ },
+ "get": {
+ "operationId": "searchStacks",
+ "parameters": [
+ {
+ "name": "primaryAssetId",
+ "required": false,
+ "in": "query",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "items": {
+ "$ref": "#/components/schemas/StackResponseDto"
+ },
+ "type": "array"
+ }
+ }
+ },
+ "description": ""
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ },
+ {
+ "cookie": []
+ },
+ {
+ "api_key": []
+ }
+ ],
+ "tags": [
+ "Stacks"
+ ]
+ },
+ "post": {
+ "operationId": "createStack",
+ "parameters": [],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/StackCreateDto"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "201": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/StackResponseDto"
+ }
+ }
+ },
+ "description": ""
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ },
+ {
+ "cookie": []
+ },
+ {
+ "api_key": []
+ }
+ ],
+ "tags": [
+ "Stacks"
+ ]
+ }
+ },
+ "/stacks/{id}": {
+ "delete": {
+ "operationId": "deleteStack",
+ "parameters": [
+ {
+ "name": "id",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "format": "uuid",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": ""
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ },
+ {
+ "cookie": []
+ },
+ {
+ "api_key": []
+ }
+ ],
+ "tags": [
+ "Stacks"
+ ]
+ },
+ "get": {
+ "operationId": "getStack",
+ "parameters": [
+ {
+ "name": "id",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "format": "uuid",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/StackResponseDto"
+ }
+ }
+ },
+ "description": ""
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ },
+ {
+ "cookie": []
+ },
+ {
+ "api_key": []
+ }
+ ],
+ "tags": [
+ "Stacks"
+ ]
+ },
+ "put": {
+ "operationId": "updateStack",
+ "parameters": [
+ {
+ "name": "id",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "format": "uuid",
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/StackUpdateDto"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/StackResponseDto"
+ }
+ }
+ },
+ "description": ""
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ },
+ {
+ "cookie": []
+ },
+ {
+ "api_key": []
+ }
+ ],
+ "tags": [
+ "Stacks"
+ ]
+ }
+ },
"/sync/delta-sync": {
"post": {
"operationId": "getDeltaSync",
@@ -7570,13 +7777,6 @@
"maximum": 5,
"minimum": 0,
"type": "number"
- },
- "removeParent": {
- "type": "boolean"
- },
- "stackParentId": {
- "format": "uuid",
- "type": "string"
}
},
"required": [
@@ -8117,18 +8317,12 @@
"$ref": "#/components/schemas/SmartInfoResponseDto"
},
"stack": {
- "items": {
- "$ref": "#/components/schemas/AssetResponseDto"
- },
- "type": "array"
- },
- "stackCount": {
- "nullable": true,
- "type": "integer"
- },
- "stackParentId": {
- "nullable": true,
- "type": "string"
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/AssetStackResponseDto"
+ }
+ ],
+ "nullable": true
},
"tags": {
"items": {
@@ -8172,13 +8366,31 @@
"originalPath",
"ownerId",
"resized",
- "stackCount",
"thumbhash",
"type",
"updatedAt"
],
"type": "object"
},
+ "AssetStackResponseDto": {
+ "properties": {
+ "assetCount": {
+ "type": "integer"
+ },
+ "id": {
+ "type": "string"
+ },
+ "primaryAssetId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "assetCount",
+ "id",
+ "primaryAssetId"
+ ],
+ "type": "object"
+ },
"AssetStatsResponseDto": {
"properties": {
"images": {
@@ -9806,6 +10018,10 @@
"sharedLink.read",
"sharedLink.update",
"sharedLink.delete",
+ "stack.create",
+ "stack.read",
+ "stack.update",
+ "stack.delete",
"systemConfig.read",
"systemConfig.update",
"systemMetadata.read",
@@ -10882,6 +11098,53 @@
],
"type": "object"
},
+ "StackCreateDto": {
+ "properties": {
+ "assetIds": {
+ "description": "first asset becomes the primary",
+ "items": {
+ "format": "uuid",
+ "type": "string"
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "assetIds"
+ ],
+ "type": "object"
+ },
+ "StackResponseDto": {
+ "properties": {
+ "assets": {
+ "items": {
+ "$ref": "#/components/schemas/AssetResponseDto"
+ },
+ "type": "array"
+ },
+ "id": {
+ "type": "string"
+ },
+ "primaryAssetId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "assets",
+ "id",
+ "primaryAssetId"
+ ],
+ "type": "object"
+ },
+ "StackUpdateDto": {
+ "properties": {
+ "primaryAssetId": {
+ "format": "uuid",
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
"SystemConfigDto": {
"properties": {
"ffmpeg": {
@@ -11735,23 +11998,6 @@
],
"type": "object"
},
- "UpdateStackParentDto": {
- "properties": {
- "newParentId": {
- "format": "uuid",
- "type": "string"
- },
- "oldParentId": {
- "format": "uuid",
- "type": "string"
- }
- },
- "required": [
- "newParentId",
- "oldParentId"
- ],
- "type": "object"
- },
"UpdateTagDto": {
"properties": {
"name": {
diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts
index 89e03603689a8..8b503821f7af1 100644
--- a/open-api/typescript-sdk/src/fetch-client.ts
+++ b/open-api/typescript-sdk/src/fetch-client.ts
@@ -192,6 +192,11 @@ export type SmartInfoResponseDto = {
objects?: string[] | null;
tags?: string[] | null;
};
+export type AssetStackResponseDto = {
+ assetCount: number;
+ id: string;
+ primaryAssetId: string;
+};
export type TagResponseDto = {
id: string;
name: string;
@@ -226,9 +231,7 @@ export type AssetResponseDto = {
people?: PersonWithFacesResponseDto[];
resized: boolean;
smartInfo?: SmartInfoResponseDto;
- stack?: AssetResponseDto[];
- stackCount: number | null;
- stackParentId?: string | null;
+ stack?: (AssetStackResponseDto) | null;
tags?: TagResponseDto[];
thumbhash: string | null;
"type": AssetTypeEnum;
@@ -344,8 +347,6 @@ export type AssetBulkUpdateDto = {
latitude?: number;
longitude?: number;
rating?: number;
- removeParent?: boolean;
- stackParentId?: string;
};
export type AssetBulkUploadCheckItem = {
/** base64 or hex encoded sha1 hash */
@@ -379,10 +380,6 @@ export type MemoryLaneResponseDto = {
assets: AssetResponseDto[];
yearsAgo: number;
};
-export type UpdateStackParentDto = {
- newParentId: string;
- oldParentId: string;
-};
export type AssetStatsResponseDto = {
images: number;
total: number;
@@ -973,6 +970,18 @@ export type AssetIdsResponseDto = {
error?: Error2;
success: boolean;
};
+export type StackResponseDto = {
+ assets: AssetResponseDto[];
+ id: string;
+ primaryAssetId: string;
+};
+export type StackCreateDto = {
+ /** first asset becomes the primary */
+ assetIds: string[];
+};
+export type StackUpdateDto = {
+ primaryAssetId?: string;
+};
export type AssetDeltaSyncDto = {
updatedAfter: string;
userIds: string[];
@@ -1632,15 +1641,6 @@ export function getRandom({ count }: {
...opts
}));
}
-export function updateStackParent({ updateStackParentDto }: {
- updateStackParentDto: UpdateStackParentDto;
-}, opts?: Oazapfts.RequestOpts) {
- return oazapfts.ok(oazapfts.fetchText("/assets/stack/parent", oazapfts.json({
- ...opts,
- method: "PUT",
- body: updateStackParentDto
- })));
-}
export function getAssetStatistics({ isArchived, isFavorite, isTrashed }: {
isArchived?: boolean;
isFavorite?: boolean;
@@ -2706,6 +2706,70 @@ export function addSharedLinkAssets({ id, key, assetIdsDto }: {
body: assetIdsDto
})));
}
+export function deleteStacks({ bulkIdsDto }: {
+ bulkIdsDto: BulkIdsDto;
+}, opts?: Oazapfts.RequestOpts) {
+ return oazapfts.ok(oazapfts.fetchText("/stacks", oazapfts.json({
+ ...opts,
+ method: "DELETE",
+ body: bulkIdsDto
+ })));
+}
+export function searchStacks({ primaryAssetId }: {
+ primaryAssetId?: string;
+}, opts?: Oazapfts.RequestOpts) {
+ return oazapfts.ok(oazapfts.fetchJson<{
+ status: 200;
+ data: StackResponseDto[];
+ }>(`/stacks${QS.query(QS.explode({
+ primaryAssetId
+ }))}`, {
+ ...opts
+ }));
+}
+export function createStack({ stackCreateDto }: {
+ stackCreateDto: StackCreateDto;
+}, opts?: Oazapfts.RequestOpts) {
+ return oazapfts.ok(oazapfts.fetchJson<{
+ status: 201;
+ data: StackResponseDto;
+ }>("/stacks", oazapfts.json({
+ ...opts,
+ method: "POST",
+ body: stackCreateDto
+ })));
+}
+export function deleteStack({ id }: {
+ id: string;
+}, opts?: Oazapfts.RequestOpts) {
+ return oazapfts.ok(oazapfts.fetchText(`/stacks/${encodeURIComponent(id)}`, {
+ ...opts,
+ method: "DELETE"
+ }));
+}
+export function getStack({ id }: {
+ id: string;
+}, opts?: Oazapfts.RequestOpts) {
+ return oazapfts.ok(oazapfts.fetchJson<{
+ status: 200;
+ data: StackResponseDto;
+ }>(`/stacks/${encodeURIComponent(id)}`, {
+ ...opts
+ }));
+}
+export function updateStack({ id, stackUpdateDto }: {
+ id: string;
+ stackUpdateDto: StackUpdateDto;
+}, opts?: Oazapfts.RequestOpts) {
+ return oazapfts.ok(oazapfts.fetchJson<{
+ status: 200;
+ data: StackResponseDto;
+ }>(`/stacks/${encodeURIComponent(id)}`, oazapfts.json({
+ ...opts,
+ method: "PUT",
+ body: stackUpdateDto
+ })));
+}
export function getDeltaSync({ assetDeltaSyncDto }: {
assetDeltaSyncDto: AssetDeltaSyncDto;
}, opts?: Oazapfts.RequestOpts) {
@@ -3187,6 +3251,10 @@ export enum Permission {
SharedLinkRead = "sharedLink.read",
SharedLinkUpdate = "sharedLink.update",
SharedLinkDelete = "sharedLink.delete",
+ StackCreate = "stack.create",
+ StackRead = "stack.read",
+ StackUpdate = "stack.update",
+ StackDelete = "stack.delete",
SystemConfigRead = "systemConfig.read",
SystemConfigUpdate = "systemConfig.update",
SystemMetadataRead = "systemMetadata.read",
diff --git a/server/src/controllers/asset.controller.ts b/server/src/controllers/asset.controller.ts
index 8c70bed1666ac..f275aa72422aa 100644
--- a/server/src/controllers/asset.controller.ts
+++ b/server/src/controllers/asset.controller.ts
@@ -13,7 +13,6 @@ import {
} from 'src/dtos/asset.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { MemoryLaneDto } from 'src/dtos/search.dto';
-import { UpdateStackParentDto } from 'src/dtos/stack.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { Route } from 'src/middleware/file-upload.interceptor';
import { AssetService } from 'src/services/asset.service';
@@ -72,13 +71,6 @@ export class AssetController {
return this.service.deleteAll(auth, dto);
}
- @Put('stack/parent')
- @HttpCode(HttpStatus.OK)
- @Authenticated()
- updateStackParent(@Auth() auth: AuthDto, @Body() dto: UpdateStackParentDto): Promise {
- return this.service.updateStackParent(auth, dto);
- }
-
@Get(':id')
@Authenticated({ sharedLink: true })
getAssetInfo(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise {
diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts
index 9675cf6d3be20..3a832c1a1ba7e 100644
--- a/server/src/controllers/index.ts
+++ b/server/src/controllers/index.ts
@@ -23,6 +23,7 @@ import { ServerInfoController } from 'src/controllers/server-info.controller';
import { ServerController } from 'src/controllers/server.controller';
import { SessionController } from 'src/controllers/session.controller';
import { SharedLinkController } from 'src/controllers/shared-link.controller';
+import { StackController } from 'src/controllers/stack.controller';
import { SyncController } from 'src/controllers/sync.controller';
import { SystemConfigController } from 'src/controllers/system-config.controller';
import { SystemMetadataController } from 'src/controllers/system-metadata.controller';
@@ -58,6 +59,7 @@ export const controllers = [
ServerInfoController,
SessionController,
SharedLinkController,
+ StackController,
SyncController,
SystemConfigController,
SystemMetadataController,
diff --git a/server/src/controllers/stack.controller.ts b/server/src/controllers/stack.controller.ts
new file mode 100644
index 0000000000000..184fa96b380e0
--- /dev/null
+++ b/server/src/controllers/stack.controller.ts
@@ -0,0 +1,57 @@
+import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
+import { ApiTags } from '@nestjs/swagger';
+import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
+import { StackCreateDto, StackResponseDto, StackSearchDto, StackUpdateDto } from 'src/dtos/stack.dto';
+import { Permission } from 'src/enum';
+import { Auth, Authenticated } from 'src/middleware/auth.guard';
+import { StackService } from 'src/services/stack.service';
+import { UUIDParamDto } from 'src/validation';
+
+@ApiTags('Stacks')
+@Controller('stacks')
+export class StackController {
+ constructor(private service: StackService) {}
+
+ @Get()
+ @Authenticated({ permission: Permission.STACK_READ })
+ searchStacks(@Auth() auth: AuthDto, @Query() query: StackSearchDto): Promise {
+ return this.service.search(auth, query);
+ }
+
+ @Post()
+ @Authenticated({ permission: Permission.STACK_CREATE })
+ createStack(@Auth() auth: AuthDto, @Body() dto: StackCreateDto): Promise {
+ return this.service.create(auth, dto);
+ }
+
+ @Delete()
+ @HttpCode(HttpStatus.NO_CONTENT)
+ @Authenticated({ permission: Permission.STACK_DELETE })
+ deleteStacks(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise {
+ return this.service.deleteAll(auth, dto);
+ }
+
+ @Get(':id')
+ @Authenticated({ permission: Permission.STACK_READ })
+ getStack(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise {
+ return this.service.get(auth, id);
+ }
+
+ @Put(':id')
+ @Authenticated({ permission: Permission.STACK_UPDATE })
+ updateStack(
+ @Auth() auth: AuthDto,
+ @Param() { id }: UUIDParamDto,
+ @Body() dto: StackUpdateDto,
+ ): Promise {
+ return this.service.update(auth, id, dto);
+ }
+
+ @Delete(':id')
+ @HttpCode(HttpStatus.NO_CONTENT)
+ @Authenticated({ permission: Permission.STACK_DELETE })
+ deleteStack(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise {
+ return this.service.delete(auth, id);
+ }
+}
diff --git a/server/src/cores/access.core.ts b/server/src/cores/access.core.ts
index b8ba88b59d388..f0050b3947253 100644
--- a/server/src/cores/access.core.ts
+++ b/server/src/cores/access.core.ts
@@ -292,6 +292,18 @@ export class AccessCore {
return await this.repository.partner.checkUpdateAccess(auth.user.id, ids);
}
+ case Permission.STACK_READ: {
+ return this.repository.stack.checkOwnerAccess(auth.user.id, ids);
+ }
+
+ case Permission.STACK_UPDATE: {
+ return this.repository.stack.checkOwnerAccess(auth.user.id, ids);
+ }
+
+ case Permission.STACK_DELETE: {
+ return this.repository.stack.checkOwnerAccess(auth.user.id, ids);
+ }
+
default: {
return new Set();
}
diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts
index 4238fd3490310..6ed1125253c3c 100644
--- a/server/src/dtos/asset-response.dto.ts
+++ b/server/src/dtos/asset-response.dto.ts
@@ -52,13 +52,19 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
unassignedFaces?: AssetFaceWithoutPersonResponseDto[];
/**base64 encoded sha1 hash */
checksum!: string;
- stackParentId?: string | null;
- stack?: AssetResponseDto[];
- @ApiProperty({ type: 'integer' })
- stackCount!: number | null;
+ stack?: AssetStackResponseDto | null;
duplicateId?: string | null;
}
+export class AssetStackResponseDto {
+ id!: string;
+
+ primaryAssetId!: string;
+
+ @ApiProperty({ type: 'integer' })
+ assetCount!: number;
+}
+
export type AssetMapOptions = {
stripMetadata?: boolean;
withStack?: boolean;
@@ -83,6 +89,18 @@ const peopleWithFaces = (faces: AssetFaceEntity[]): PersonWithFacesResponseDto[]
return result;
};
+const mapStack = (entity: AssetEntity) => {
+ if (!entity.stack) {
+ return null;
+ }
+
+ return {
+ id: entity.stack.id,
+ primaryAssetId: entity.stack.primaryAssetId,
+ assetCount: entity.stack.assetCount ?? entity.stack.assets.length,
+ };
+};
+
export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): AssetResponseDto {
const { stripMetadata = false, withStack = false } = options;
@@ -129,13 +147,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
people: peopleWithFaces(entity.faces),
unassignedFaces: entity.faces?.filter((face) => !face.person).map((a) => mapFacesWithoutPerson(a)),
checksum: entity.checksum.toString('base64'),
- stackParentId: withStack ? entity.stack?.primaryAssetId : undefined,
- stack: withStack
- ? entity.stack?.assets
- ?.filter((a) => a.id !== entity.stack?.primaryAssetId)
- ?.map((a) => mapAsset(a, { stripMetadata, auth: options.auth }))
- : undefined,
- stackCount: entity.stack?.assetCount ?? entity.stack?.assets?.length ?? null,
+ stack: withStack ? mapStack(entity) : undefined,
isOffline: entity.isOffline,
hasMetadata: true,
duplicateId: entity.duplicateId,
diff --git a/server/src/dtos/asset.dto.ts b/server/src/dtos/asset.dto.ts
index 9bc007543a893..5a2fdb51200d7 100644
--- a/server/src/dtos/asset.dto.ts
+++ b/server/src/dtos/asset.dto.ts
@@ -60,12 +60,6 @@ export class AssetBulkUpdateDto extends UpdateAssetBase {
@ValidateUUID({ each: true })
ids!: string[];
- @ValidateUUID({ optional: true })
- stackParentId?: string;
-
- @ValidateBoolean({ optional: true })
- removeParent?: boolean;
-
@Optional()
duplicateId?: string | null;
}
diff --git a/server/src/dtos/stack.dto.ts b/server/src/dtos/stack.dto.ts
index 3ff04ee5ed7f4..3b867b02fea74 100644
--- a/server/src/dtos/stack.dto.ts
+++ b/server/src/dtos/stack.dto.ts
@@ -1,9 +1,38 @@
+import { ArrayMinSize } from 'class-validator';
+import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
+import { StackEntity } from 'src/entities/stack.entity';
import { ValidateUUID } from 'src/validation';
-export class UpdateStackParentDto {
- @ValidateUUID()
- oldParentId!: string;
-
- @ValidateUUID()
- newParentId!: string;
+export class StackCreateDto {
+ /** first asset becomes the primary */
+ @ValidateUUID({ each: true })
+ @ArrayMinSize(2)
+ assetIds!: string[];
}
+
+export class StackSearchDto {
+ primaryAssetId?: string;
+}
+
+export class StackUpdateDto {
+ @ValidateUUID({ optional: true })
+ primaryAssetId?: string;
+}
+
+export class StackResponseDto {
+ id!: string;
+ primaryAssetId!: string;
+ assets!: AssetResponseDto[];
+}
+
+export const mapStack = (stack: StackEntity, { auth }: { auth?: AuthDto }) => {
+ const primary = stack.assets.filter((asset) => asset.id === stack.primaryAssetId);
+ const others = stack.assets.filter((asset) => asset.id !== stack.primaryAssetId);
+
+ return {
+ id: stack.id,
+ primaryAssetId: stack.primaryAssetId,
+ assets: [...primary, ...others].map((asset) => mapAsset(asset, { auth })),
+ };
+};
diff --git a/server/src/enum.ts b/server/src/enum.ts
index da4b2d76fc580..4a81d54218fc6 100644
--- a/server/src/enum.ts
+++ b/server/src/enum.ts
@@ -107,6 +107,11 @@ export enum Permission {
SHARED_LINK_UPDATE = 'sharedLink.update',
SHARED_LINK_DELETE = 'sharedLink.delete',
+ STACK_CREATE = 'stack.create',
+ STACK_READ = 'stack.read',
+ STACK_UPDATE = 'stack.update',
+ STACK_DELETE = 'stack.delete',
+
SYSTEM_CONFIG_READ = 'systemConfig.read',
SYSTEM_CONFIG_UPDATE = 'systemConfig.update',
diff --git a/server/src/interfaces/access.interface.ts b/server/src/interfaces/access.interface.ts
index cf5ebbd0052ab..2dcf9d6b942a7 100644
--- a/server/src/interfaces/access.interface.ts
+++ b/server/src/interfaces/access.interface.ts
@@ -42,4 +42,8 @@ export interface IAccessRepository {
partner: {
checkUpdateAccess(userId: string, partnerIds: Set): Promise>;
};
+
+ stack: {
+ checkOwnerAccess(userId: string, stackIds: Set): Promise>;
+ };
}
diff --git a/server/src/interfaces/stack.interface.ts b/server/src/interfaces/stack.interface.ts
index 0e6baf0a34a2e..378f63fd95a6a 100644
--- a/server/src/interfaces/stack.interface.ts
+++ b/server/src/interfaces/stack.interface.ts
@@ -2,9 +2,16 @@ import { StackEntity } from 'src/entities/stack.entity';
export const IStackRepository = 'IStackRepository';
+export interface StackSearch {
+ ownerId: string;
+ primaryAssetId?: string;
+}
+
export interface IStackRepository {
- create(stack: Partial & { ownerId: string }): Promise;
+ search(query: StackSearch): Promise;
+ create(stack: { ownerId: string; assetIds: string[] }): Promise;
update(stack: Pick & Partial): Promise;
delete(id: string): Promise;
+ deleteAll(ids: string[]): Promise;
getById(id: string): Promise