mirror of
https://github.com/immich-app/immich.git
synced 2025-05-31 20:25:32 -04:00
feat: lighter buckets
This commit is contained in:
parent
242a559e0f
commit
8c6df5ef6f
23
.vscode/settings.json
vendored
23
.vscode/settings.json
vendored
@ -8,7 +8,11 @@
|
|||||||
"[typescript]": {
|
"[typescript]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.tabSize": 2,
|
"editor.tabSize": 2,
|
||||||
"editor.formatOnSave": true
|
"editor.formatOnSave": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.removeUnusedImports": "explicit",
|
||||||
|
"source.organizeImports": "explicit"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"[css]": {
|
"[css]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
@ -17,13 +21,14 @@
|
|||||||
},
|
},
|
||||||
"[svelte]": {
|
"[svelte]": {
|
||||||
"editor.defaultFormatter": "svelte.svelte-vscode",
|
"editor.defaultFormatter": "svelte.svelte-vscode",
|
||||||
"editor.tabSize": 2
|
"editor.tabSize": 2,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.removeUnusedImports": "explicit",
|
||||||
|
"source.organizeImports": "explicit"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"svelte.enable-ts-plugin": true,
|
"svelte.enable-ts-plugin": true,
|
||||||
"eslint.validate": [
|
"eslint.validate": ["javascript", "svelte"],
|
||||||
"javascript",
|
|
||||||
"svelte"
|
|
||||||
],
|
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||||
"[dart]": {
|
"[dart]": {
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
@ -34,12 +39,10 @@
|
|||||||
"editor.wordBasedSuggestions": "off",
|
"editor.wordBasedSuggestions": "off",
|
||||||
"editor.defaultFormatter": "Dart-Code.dart-code"
|
"editor.defaultFormatter": "Dart-Code.dart-code"
|
||||||
},
|
},
|
||||||
"cSpell.words": [
|
"cSpell.words": ["immich"],
|
||||||
"immich"
|
|
||||||
],
|
|
||||||
"explorer.fileNesting.enabled": true,
|
"explorer.fileNesting.enabled": true,
|
||||||
"explorer.fileNesting.patterns": {
|
"explorer.fileNesting.patterns": {
|
||||||
"*.ts": "${capture}.spec.ts,${capture}.mock.ts",
|
"*.ts": "${capture}.spec.ts,${capture}.mock.ts",
|
||||||
"*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart"
|
"*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
194
api.mustache
Normal file
194
api.mustache
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
{{>header}}
|
||||||
|
{{>part_of}}
|
||||||
|
{{#operations}}
|
||||||
|
|
||||||
|
class {{{classname}}} {
|
||||||
|
{{{classname}}}([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
|
||||||
|
|
||||||
|
final ApiClient apiClient;
|
||||||
|
{{#operation}}
|
||||||
|
|
||||||
|
{{#summary}}
|
||||||
|
/// {{{.}}}
|
||||||
|
{{/summary}}
|
||||||
|
{{#notes}}
|
||||||
|
{{#summary}}
|
||||||
|
///
|
||||||
|
{{/summary}}
|
||||||
|
/// {{{notes}}}
|
||||||
|
///
|
||||||
|
/// Note: This method returns the HTTP [Response].
|
||||||
|
{{/notes}}
|
||||||
|
{{^notes}}
|
||||||
|
{{#summary}}
|
||||||
|
///
|
||||||
|
/// Note: This method returns the HTTP [Response].
|
||||||
|
{{/summary}}
|
||||||
|
{{^summary}}
|
||||||
|
/// Performs an HTTP '{{{httpMethod}}} {{{path}}}' operation and returns the [Response].
|
||||||
|
{{/summary}}
|
||||||
|
{{/notes}}
|
||||||
|
{{#hasParams}}
|
||||||
|
{{#summary}}
|
||||||
|
///
|
||||||
|
{{/summary}}
|
||||||
|
{{^summary}}
|
||||||
|
{{#notes}}
|
||||||
|
///
|
||||||
|
{{/notes}}
|
||||||
|
{{/summary}}
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
{{/hasParams}}
|
||||||
|
{{#allParams}}
|
||||||
|
/// * [{{{dataType}}}] {{{paramName}}}{{#required}} (required){{/required}}{{#optional}} (optional){{/optional}}:
|
||||||
|
{{#description}}
|
||||||
|
/// {{{.}}}
|
||||||
|
{{/description}}
|
||||||
|
{{^-last}}
|
||||||
|
///
|
||||||
|
{{/-last}}
|
||||||
|
{{/allParams}}
|
||||||
|
Future<Response> {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} }{{/hasOptionalParams}}) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final path = r'{{{path}}}'{{#pathParams}}
|
||||||
|
.replaceAll({{=<% %>=}}'{<% baseName %>}'<%={{ }}=%>, {{{paramName}}}{{^isString}}.toString(){{/isString}}){{/pathParams}};
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody{{#bodyParam}} = {{{paramName}}}{{/bodyParam}};
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
{{#hasQueryParams}}
|
||||||
|
|
||||||
|
{{#queryParams}}
|
||||||
|
{{^required}}
|
||||||
|
if ({{{paramName}}} != null) {
|
||||||
|
{{/required}}
|
||||||
|
queryParams.addAll(_queryParams('{{{collectionFormat}}}', '{{{baseName}}}', {{{paramName}}}));
|
||||||
|
{{^required}}
|
||||||
|
}
|
||||||
|
{{/required}}
|
||||||
|
{{/queryParams}}
|
||||||
|
{{/hasQueryParams}}
|
||||||
|
{{#hasHeaderParams}}
|
||||||
|
|
||||||
|
{{#headerParams}}
|
||||||
|
{{#required}}
|
||||||
|
headerParams[r'{{{baseName}}}'] = parameterToString({{{paramName}}});
|
||||||
|
{{/required}}
|
||||||
|
{{^required}}
|
||||||
|
if ({{{paramName}}} != null) {
|
||||||
|
headerParams[r'{{{baseName}}}'] = parameterToString({{{paramName}}});
|
||||||
|
}
|
||||||
|
{{/required}}
|
||||||
|
{{/headerParams}}
|
||||||
|
{{/hasHeaderParams}}
|
||||||
|
|
||||||
|
const contentTypes = <String>[{{#prioritizedContentTypes}}'{{{mediaType}}}'{{^-last}}, {{/-last}}{{/prioritizedContentTypes}}];
|
||||||
|
|
||||||
|
{{#isMultipart}}
|
||||||
|
bool hasFields = false;
|
||||||
|
final mp = MultipartRequest('{{{httpMethod}}}', Uri.parse(path));
|
||||||
|
{{#formParams}}
|
||||||
|
{{^isFile}}
|
||||||
|
if ({{{paramName}}} != null) {
|
||||||
|
hasFields = true;
|
||||||
|
mp.fields[r'{{{baseName}}}'] = parameterToString({{{paramName}}});
|
||||||
|
}
|
||||||
|
{{/isFile}}
|
||||||
|
{{#isFile}}
|
||||||
|
if ({{{paramName}}} != null) {
|
||||||
|
hasFields = true;
|
||||||
|
mp.fields[r'{{{baseName}}}'] = {{{paramName}}}.field;
|
||||||
|
mp.files.add({{{paramName}}});
|
||||||
|
}
|
||||||
|
{{/isFile}}
|
||||||
|
{{/formParams}}
|
||||||
|
if (hasFields) {
|
||||||
|
postBody = mp;
|
||||||
|
}
|
||||||
|
{{/isMultipart}}
|
||||||
|
{{^isMultipart}}
|
||||||
|
{{#formParams}}
|
||||||
|
{{^isFile}}
|
||||||
|
if ({{{paramName}}} != null) {
|
||||||
|
formParams[r'{{{baseName}}}'] = parameterToString({{{paramName}}});
|
||||||
|
}
|
||||||
|
{{/isFile}}
|
||||||
|
{{/formParams}}
|
||||||
|
{{/isMultipart}}
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
path,
|
||||||
|
'{{{httpMethod}}}',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{{#summary}}
|
||||||
|
/// {{{.}}}
|
||||||
|
{{/summary}}
|
||||||
|
{{#notes}}
|
||||||
|
{{#summary}}
|
||||||
|
///
|
||||||
|
{{/summary}}
|
||||||
|
/// {{{notes}}}
|
||||||
|
{{/notes}}
|
||||||
|
{{#hasParams}}
|
||||||
|
{{#summary}}
|
||||||
|
///
|
||||||
|
{{/summary}}
|
||||||
|
{{^summary}}
|
||||||
|
{{#notes}}
|
||||||
|
///
|
||||||
|
{{/notes}}
|
||||||
|
{{/summary}}
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
{{/hasParams}}
|
||||||
|
{{#allParams}}
|
||||||
|
/// * [{{{dataType}}}] {{{paramName}}}{{#required}} (required){{/required}}{{#optional}} (optional){{/optional}}:
|
||||||
|
{{#description}}
|
||||||
|
/// {{{.}}}
|
||||||
|
{{/description}}
|
||||||
|
{{^-last}}
|
||||||
|
///
|
||||||
|
{{/-last}}
|
||||||
|
{{/allParams}}
|
||||||
|
Future<{{#returnType}}{{{.}}}?{{/returnType}}{{^returnType}}void{{/returnType}}> {{{nickname}}}({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} }{{/hasOptionalParams}}) async {
|
||||||
|
final response = await {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}} {{#allParams}}{{^required}}{{{paramName}}}: {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} {{/hasOptionalParams}});
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
{{#returnType}}
|
||||||
|
// 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) {
|
||||||
|
{{#native_serialization}}
|
||||||
|
{{#isArray}}
|
||||||
|
final responseBody = await _decodeBodyBytes(response);
|
||||||
|
return (await apiClient.deserializeAsync(responseBody, '{{{returnType}}}') as List)
|
||||||
|
.cast<{{{returnBaseType}}}>()
|
||||||
|
.{{#uniqueItems}}toSet(){{/uniqueItems}}{{^uniqueItems}}toList(growable: false){{/uniqueItems}};
|
||||||
|
{{/isArray}}
|
||||||
|
{{^isArray}}
|
||||||
|
{{#isMap}}
|
||||||
|
return {{{returnType}}}.from(await apiClient.deserializeAsync(await _decodeBodyBytes(response), '{{{returnType}}}'),);
|
||||||
|
{{/isMap}}
|
||||||
|
{{^isMap}}
|
||||||
|
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), '{{{returnType}}}',) as {{{returnType}}};
|
||||||
|
{{/isMap}}{{/isArray}}{{/native_serialization}}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
{{/returnType}}
|
||||||
|
}
|
||||||
|
{{/operation}}
|
||||||
|
}
|
||||||
|
{{/operations}}
|
4
mobile/openapi/README.md
generated
4
mobile/openapi/README.md
generated
@ -475,8 +475,12 @@ Class | Method | HTTP request | Description
|
|||||||
- [TemplateDto](doc//TemplateDto.md)
|
- [TemplateDto](doc//TemplateDto.md)
|
||||||
- [TemplateResponseDto](doc//TemplateResponseDto.md)
|
- [TemplateResponseDto](doc//TemplateResponseDto.md)
|
||||||
- [TestEmailResponseDto](doc//TestEmailResponseDto.md)
|
- [TestEmailResponseDto](doc//TestEmailResponseDto.md)
|
||||||
|
- [TimeBucketAssetResponseDto](doc//TimeBucketAssetResponseDto.md)
|
||||||
|
- [TimeBucketAssetResponseDtoDurationInner](doc//TimeBucketAssetResponseDtoDurationInner.md)
|
||||||
- [TimeBucketResponseDto](doc//TimeBucketResponseDto.md)
|
- [TimeBucketResponseDto](doc//TimeBucketResponseDto.md)
|
||||||
- [TimeBucketSize](doc//TimeBucketSize.md)
|
- [TimeBucketSize](doc//TimeBucketSize.md)
|
||||||
|
- [TimeBucketsResponseDto](doc//TimeBucketsResponseDto.md)
|
||||||
|
- [TimelineStackResponseDto](doc//TimelineStackResponseDto.md)
|
||||||
- [ToneMapping](doc//ToneMapping.md)
|
- [ToneMapping](doc//ToneMapping.md)
|
||||||
- [TranscodeHWAccel](doc//TranscodeHWAccel.md)
|
- [TranscodeHWAccel](doc//TranscodeHWAccel.md)
|
||||||
- [TranscodePolicy](doc//TranscodePolicy.md)
|
- [TranscodePolicy](doc//TranscodePolicy.md)
|
||||||
|
4
mobile/openapi/lib/api.dart
generated
4
mobile/openapi/lib/api.dart
generated
@ -282,8 +282,12 @@ part 'model/tags_update.dart';
|
|||||||
part 'model/template_dto.dart';
|
part 'model/template_dto.dart';
|
||||||
part 'model/template_response_dto.dart';
|
part 'model/template_response_dto.dart';
|
||||||
part 'model/test_email_response_dto.dart';
|
part 'model/test_email_response_dto.dart';
|
||||||
|
part 'model/time_bucket_asset_response_dto.dart';
|
||||||
|
part 'model/time_bucket_asset_response_dto_duration_inner.dart';
|
||||||
part 'model/time_bucket_response_dto.dart';
|
part 'model/time_bucket_response_dto.dart';
|
||||||
part 'model/time_bucket_size.dart';
|
part 'model/time_bucket_size.dart';
|
||||||
|
part 'model/time_buckets_response_dto.dart';
|
||||||
|
part 'model/timeline_stack_response_dto.dart';
|
||||||
part 'model/tone_mapping.dart';
|
part 'model/tone_mapping.dart';
|
||||||
part 'model/transcode_hw_accel.dart';
|
part 'model/transcode_hw_accel.dart';
|
||||||
part 'model/transcode_policy.dart';
|
part 'model/transcode_policy.dart';
|
||||||
|
33
mobile/openapi/lib/api/timeline_api.dart
generated
33
mobile/openapi/lib/api/timeline_api.dart
generated
@ -35,6 +35,10 @@ class TimelineApi {
|
|||||||
///
|
///
|
||||||
/// * [AssetOrder] order:
|
/// * [AssetOrder] order:
|
||||||
///
|
///
|
||||||
|
/// * [num] page:
|
||||||
|
///
|
||||||
|
/// * [num] pageSize:
|
||||||
|
///
|
||||||
/// * [String] personId:
|
/// * [String] personId:
|
||||||
///
|
///
|
||||||
/// * [String] tagId:
|
/// * [String] tagId:
|
||||||
@ -44,7 +48,7 @@ class TimelineApi {
|
|||||||
/// * [bool] withPartners:
|
/// * [bool] withPartners:
|
||||||
///
|
///
|
||||||
/// * [bool] withStacked:
|
/// * [bool] withStacked:
|
||||||
Future<Response> getTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async {
|
Future<Response> getTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, num? page, num? pageSize, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/timeline/bucket';
|
final apiPath = r'/timeline/bucket';
|
||||||
|
|
||||||
@ -73,6 +77,12 @@ class TimelineApi {
|
|||||||
if (order != null) {
|
if (order != null) {
|
||||||
queryParams.addAll(_queryParams('', 'order', order));
|
queryParams.addAll(_queryParams('', 'order', order));
|
||||||
}
|
}
|
||||||
|
if (page != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'page', page));
|
||||||
|
}
|
||||||
|
if (pageSize != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'pageSize', pageSize));
|
||||||
|
}
|
||||||
if (personId != null) {
|
if (personId != null) {
|
||||||
queryParams.addAll(_queryParams('', 'personId', personId));
|
queryParams.addAll(_queryParams('', 'personId', personId));
|
||||||
}
|
}
|
||||||
@ -123,6 +133,10 @@ class TimelineApi {
|
|||||||
///
|
///
|
||||||
/// * [AssetOrder] order:
|
/// * [AssetOrder] order:
|
||||||
///
|
///
|
||||||
|
/// * [num] page:
|
||||||
|
///
|
||||||
|
/// * [num] pageSize:
|
||||||
|
///
|
||||||
/// * [String] personId:
|
/// * [String] personId:
|
||||||
///
|
///
|
||||||
/// * [String] tagId:
|
/// * [String] tagId:
|
||||||
@ -132,8 +146,8 @@ class TimelineApi {
|
|||||||
/// * [bool] withPartners:
|
/// * [bool] withPartners:
|
||||||
///
|
///
|
||||||
/// * [bool] withStacked:
|
/// * [bool] withStacked:
|
||||||
Future<List<AssetResponseDto>?> getTimeBucket(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async {
|
Future<TimeBucketResponseDto?> getTimeBucket(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, num? page, num? pageSize, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async {
|
||||||
final response = await getTimeBucketWithHttpInfo(size, timeBucket, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, withPartners: withPartners, withStacked: withStacked, );
|
final response = await getTimeBucketWithHttpInfo(size, timeBucket, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, page: page, pageSize: pageSize, personId: personId, tagId: tagId, userId: userId, withPartners: withPartners, withStacked: withStacked, );
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
@ -141,11 +155,8 @@ class TimelineApi {
|
|||||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||||
// FormatException when trying to decode an empty string.
|
// FormatException when trying to decode an empty string.
|
||||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
final responseBody = await _decodeBodyBytes(response);
|
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'TimeBucketResponseDto',) as TimeBucketResponseDto;
|
||||||
return (await apiClient.deserializeAsync(responseBody, 'List<AssetResponseDto>') as List)
|
|
||||||
.cast<AssetResponseDto>()
|
|
||||||
.toList(growable: false);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -261,7 +272,7 @@ class TimelineApi {
|
|||||||
/// * [bool] withPartners:
|
/// * [bool] withPartners:
|
||||||
///
|
///
|
||||||
/// * [bool] withStacked:
|
/// * [bool] withStacked:
|
||||||
Future<List<TimeBucketResponseDto>?> getTimeBuckets(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async {
|
Future<List<TimeBucketsResponseDto>?> getTimeBuckets(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async {
|
||||||
final response = await getTimeBucketsWithHttpInfo(size, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, withPartners: withPartners, withStacked: withStacked, );
|
final response = await getTimeBucketsWithHttpInfo(size, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, withPartners: withPartners, withStacked: withStacked, );
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
@ -271,8 +282,8 @@ class TimelineApi {
|
|||||||
// FormatException when trying to decode an empty string.
|
// FormatException when trying to decode an empty string.
|
||||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
final responseBody = await _decodeBodyBytes(response);
|
final responseBody = await _decodeBodyBytes(response);
|
||||||
return (await apiClient.deserializeAsync(responseBody, 'List<TimeBucketResponseDto>') as List)
|
return (await apiClient.deserializeAsync(responseBody, 'List<TimeBucketsResponseDto>') as List)
|
||||||
.cast<TimeBucketResponseDto>()
|
.cast<TimeBucketsResponseDto>()
|
||||||
.toList(growable: false);
|
.toList(growable: false);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
8
mobile/openapi/lib/api_client.dart
generated
8
mobile/openapi/lib/api_client.dart
generated
@ -620,10 +620,18 @@ class ApiClient {
|
|||||||
return TemplateResponseDto.fromJson(value);
|
return TemplateResponseDto.fromJson(value);
|
||||||
case 'TestEmailResponseDto':
|
case 'TestEmailResponseDto':
|
||||||
return TestEmailResponseDto.fromJson(value);
|
return TestEmailResponseDto.fromJson(value);
|
||||||
|
case 'TimeBucketAssetResponseDto':
|
||||||
|
return TimeBucketAssetResponseDto.fromJson(value);
|
||||||
|
case 'TimeBucketAssetResponseDtoDurationInner':
|
||||||
|
return TimeBucketAssetResponseDtoDurationInner.fromJson(value);
|
||||||
case 'TimeBucketResponseDto':
|
case 'TimeBucketResponseDto':
|
||||||
return TimeBucketResponseDto.fromJson(value);
|
return TimeBucketResponseDto.fromJson(value);
|
||||||
case 'TimeBucketSize':
|
case 'TimeBucketSize':
|
||||||
return TimeBucketSizeTypeTransformer().decode(value);
|
return TimeBucketSizeTypeTransformer().decode(value);
|
||||||
|
case 'TimeBucketsResponseDto':
|
||||||
|
return TimeBucketsResponseDto.fromJson(value);
|
||||||
|
case 'TimelineStackResponseDto':
|
||||||
|
return TimelineStackResponseDto.fromJson(value);
|
||||||
case 'ToneMapping':
|
case 'ToneMapping':
|
||||||
return ToneMappingTypeTransformer().decode(value);
|
return ToneMappingTypeTransformer().decode(value);
|
||||||
case 'TranscodeHWAccel':
|
case 'TranscodeHWAccel':
|
||||||
|
219
mobile/openapi/lib/model/time_bucket_asset_response_dto.dart
generated
Normal file
219
mobile/openapi/lib/model/time_bucket_asset_response_dto.dart
generated
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
//
|
||||||
|
// 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 TimeBucketAssetResponseDto {
|
||||||
|
/// Returns a new [TimeBucketAssetResponseDto] instance.
|
||||||
|
TimeBucketAssetResponseDto({
|
||||||
|
this.duration = const [],
|
||||||
|
this.id = const [],
|
||||||
|
this.isArchived = const [],
|
||||||
|
this.isFavorite = const [],
|
||||||
|
this.isImage = const [],
|
||||||
|
this.isTrashed = const [],
|
||||||
|
this.isVideo = const [],
|
||||||
|
this.livePhotoVideoId = const [],
|
||||||
|
this.localDateTime = const [],
|
||||||
|
this.ownerId = const [],
|
||||||
|
this.projectionType = const [],
|
||||||
|
this.ratio = const [],
|
||||||
|
this.stack = const [],
|
||||||
|
this.thumbhash = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
List<TimeBucketAssetResponseDtoDurationInner> duration;
|
||||||
|
|
||||||
|
List<String> id;
|
||||||
|
|
||||||
|
List<num> isArchived;
|
||||||
|
|
||||||
|
List<num> isFavorite;
|
||||||
|
|
||||||
|
List<num> isImage;
|
||||||
|
|
||||||
|
List<num> isTrashed;
|
||||||
|
|
||||||
|
List<num> isVideo;
|
||||||
|
|
||||||
|
List<TimeBucketAssetResponseDtoDurationInner> livePhotoVideoId;
|
||||||
|
|
||||||
|
List<DateTime> localDateTime;
|
||||||
|
|
||||||
|
List<String> ownerId;
|
||||||
|
|
||||||
|
List<TimeBucketAssetResponseDtoDurationInner> projectionType;
|
||||||
|
|
||||||
|
List<num> ratio;
|
||||||
|
|
||||||
|
List<TimelineStackResponseDto> stack;
|
||||||
|
|
||||||
|
List<TimeBucketAssetResponseDtoDurationInner> thumbhash;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is TimeBucketAssetResponseDto &&
|
||||||
|
_deepEquality.equals(other.duration, duration) &&
|
||||||
|
_deepEquality.equals(other.id, id) &&
|
||||||
|
_deepEquality.equals(other.isArchived, isArchived) &&
|
||||||
|
_deepEquality.equals(other.isFavorite, isFavorite) &&
|
||||||
|
_deepEquality.equals(other.isImage, isImage) &&
|
||||||
|
_deepEquality.equals(other.isTrashed, isTrashed) &&
|
||||||
|
_deepEquality.equals(other.isVideo, isVideo) &&
|
||||||
|
_deepEquality.equals(other.livePhotoVideoId, livePhotoVideoId) &&
|
||||||
|
_deepEquality.equals(other.localDateTime, localDateTime) &&
|
||||||
|
_deepEquality.equals(other.ownerId, ownerId) &&
|
||||||
|
_deepEquality.equals(other.projectionType, projectionType) &&
|
||||||
|
_deepEquality.equals(other.ratio, ratio) &&
|
||||||
|
_deepEquality.equals(other.stack, stack) &&
|
||||||
|
_deepEquality.equals(other.thumbhash, thumbhash);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(duration.hashCode) +
|
||||||
|
(id.hashCode) +
|
||||||
|
(isArchived.hashCode) +
|
||||||
|
(isFavorite.hashCode) +
|
||||||
|
(isImage.hashCode) +
|
||||||
|
(isTrashed.hashCode) +
|
||||||
|
(isVideo.hashCode) +
|
||||||
|
(livePhotoVideoId.hashCode) +
|
||||||
|
(localDateTime.hashCode) +
|
||||||
|
(ownerId.hashCode) +
|
||||||
|
(projectionType.hashCode) +
|
||||||
|
(ratio.hashCode) +
|
||||||
|
(stack.hashCode) +
|
||||||
|
(thumbhash.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'TimeBucketAssetResponseDto[duration=$duration, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isImage=$isImage, isTrashed=$isTrashed, isVideo=$isVideo, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, ownerId=$ownerId, projectionType=$projectionType, ratio=$ratio, stack=$stack, thumbhash=$thumbhash]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'duration'] = this.duration;
|
||||||
|
json[r'id'] = this.id;
|
||||||
|
json[r'isArchived'] = this.isArchived;
|
||||||
|
json[r'isFavorite'] = this.isFavorite;
|
||||||
|
json[r'isImage'] = this.isImage;
|
||||||
|
json[r'isTrashed'] = this.isTrashed;
|
||||||
|
json[r'isVideo'] = this.isVideo;
|
||||||
|
json[r'livePhotoVideoId'] = this.livePhotoVideoId;
|
||||||
|
json[r'localDateTime'] = this.localDateTime;
|
||||||
|
json[r'ownerId'] = this.ownerId;
|
||||||
|
json[r'projectionType'] = this.projectionType;
|
||||||
|
json[r'ratio'] = this.ratio;
|
||||||
|
json[r'stack'] = this.stack;
|
||||||
|
json[r'thumbhash'] = this.thumbhash;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [TimeBucketAssetResponseDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static TimeBucketAssetResponseDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "TimeBucketAssetResponseDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return TimeBucketAssetResponseDto(
|
||||||
|
duration: TimeBucketAssetResponseDtoDurationInner.listFromJson(json[r'duration']),
|
||||||
|
id: json[r'id'] is Iterable
|
||||||
|
? (json[r'id'] as Iterable).cast<String>().toList(growable: false)
|
||||||
|
: const [],
|
||||||
|
isArchived: json[r'isArchived'] is Iterable
|
||||||
|
? (json[r'isArchived'] as Iterable).cast<num>().toList(growable: false)
|
||||||
|
: const [],
|
||||||
|
isFavorite: json[r'isFavorite'] is Iterable
|
||||||
|
? (json[r'isFavorite'] as Iterable).cast<num>().toList(growable: false)
|
||||||
|
: const [],
|
||||||
|
isImage: json[r'isImage'] is Iterable
|
||||||
|
? (json[r'isImage'] as Iterable).cast<num>().toList(growable: false)
|
||||||
|
: const [],
|
||||||
|
isTrashed: json[r'isTrashed'] is Iterable
|
||||||
|
? (json[r'isTrashed'] as Iterable).cast<num>().toList(growable: false)
|
||||||
|
: const [],
|
||||||
|
isVideo: json[r'isVideo'] is Iterable
|
||||||
|
? (json[r'isVideo'] as Iterable).cast<num>().toList(growable: false)
|
||||||
|
: const [],
|
||||||
|
livePhotoVideoId: TimeBucketAssetResponseDtoDurationInner.listFromJson(json[r'livePhotoVideoId']),
|
||||||
|
localDateTime: DateTime.listFromJson(json[r'localDateTime']),
|
||||||
|
ownerId: json[r'ownerId'] is Iterable
|
||||||
|
? (json[r'ownerId'] as Iterable).cast<String>().toList(growable: false)
|
||||||
|
: const [],
|
||||||
|
projectionType: TimeBucketAssetResponseDtoDurationInner.listFromJson(json[r'projectionType']),
|
||||||
|
ratio: json[r'ratio'] is Iterable
|
||||||
|
? (json[r'ratio'] as Iterable).cast<num>().toList(growable: false)
|
||||||
|
: const [],
|
||||||
|
stack: TimelineStackResponseDto.listFromJson(json[r'stack']),
|
||||||
|
thumbhash: TimeBucketAssetResponseDtoDurationInner.listFromJson(json[r'thumbhash']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<TimeBucketAssetResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <TimeBucketAssetResponseDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = TimeBucketAssetResponseDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, TimeBucketAssetResponseDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, TimeBucketAssetResponseDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = TimeBucketAssetResponseDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of TimeBucketAssetResponseDto-objects as value to a dart map
|
||||||
|
static Map<String, List<TimeBucketAssetResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<TimeBucketAssetResponseDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = TimeBucketAssetResponseDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'duration',
|
||||||
|
'id',
|
||||||
|
'isArchived',
|
||||||
|
'isFavorite',
|
||||||
|
'isImage',
|
||||||
|
'isTrashed',
|
||||||
|
'isVideo',
|
||||||
|
'livePhotoVideoId',
|
||||||
|
'localDateTime',
|
||||||
|
'ownerId',
|
||||||
|
'projectionType',
|
||||||
|
'ratio',
|
||||||
|
'stack',
|
||||||
|
'thumbhash',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
91
mobile/openapi/lib/model/time_bucket_asset_response_dto_duration_inner.dart
generated
Normal file
91
mobile/openapi/lib/model/time_bucket_asset_response_dto_duration_inner.dart
generated
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
//
|
||||||
|
// 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 TimeBucketAssetResponseDtoDurationInner {
|
||||||
|
/// Returns a new [TimeBucketAssetResponseDtoDurationInner] instance.
|
||||||
|
TimeBucketAssetResponseDtoDurationInner({
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is TimeBucketAssetResponseDtoDurationInner &&
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'TimeBucketAssetResponseDtoDurationInner[]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [TimeBucketAssetResponseDtoDurationInner] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static TimeBucketAssetResponseDtoDurationInner? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "TimeBucketAssetResponseDtoDurationInner");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return TimeBucketAssetResponseDtoDurationInner(
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<TimeBucketAssetResponseDtoDurationInner> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <TimeBucketAssetResponseDtoDurationInner>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = TimeBucketAssetResponseDtoDurationInner.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, TimeBucketAssetResponseDtoDurationInner> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, TimeBucketAssetResponseDtoDurationInner>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = TimeBucketAssetResponseDtoDurationInner.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of TimeBucketAssetResponseDtoDurationInner-objects as value to a dart map
|
||||||
|
static Map<String, List<TimeBucketAssetResponseDtoDurationInner>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<TimeBucketAssetResponseDtoDurationInner>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = TimeBucketAssetResponseDtoDurationInner.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -13,32 +13,32 @@ part of openapi.api;
|
|||||||
class TimeBucketResponseDto {
|
class TimeBucketResponseDto {
|
||||||
/// Returns a new [TimeBucketResponseDto] instance.
|
/// Returns a new [TimeBucketResponseDto] instance.
|
||||||
TimeBucketResponseDto({
|
TimeBucketResponseDto({
|
||||||
required this.count,
|
required this.bucketAssets,
|
||||||
required this.timeBucket,
|
required this.hasNextPage,
|
||||||
});
|
});
|
||||||
|
|
||||||
int count;
|
TimeBucketAssetResponseDto bucketAssets;
|
||||||
|
|
||||||
String timeBucket;
|
bool hasNextPage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is TimeBucketResponseDto &&
|
bool operator ==(Object other) => identical(this, other) || other is TimeBucketResponseDto &&
|
||||||
other.count == count &&
|
other.bucketAssets == bucketAssets &&
|
||||||
other.timeBucket == timeBucket;
|
other.hasNextPage == hasNextPage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(count.hashCode) +
|
(bucketAssets.hashCode) +
|
||||||
(timeBucket.hashCode);
|
(hasNextPage.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'TimeBucketResponseDto[count=$count, timeBucket=$timeBucket]';
|
String toString() => 'TimeBucketResponseDto[bucketAssets=$bucketAssets, hasNextPage=$hasNextPage]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
json[r'count'] = this.count;
|
json[r'bucketAssets'] = this.bucketAssets;
|
||||||
json[r'timeBucket'] = this.timeBucket;
|
json[r'hasNextPage'] = this.hasNextPage;
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,8 +51,8 @@ class TimeBucketResponseDto {
|
|||||||
final json = value.cast<String, dynamic>();
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
return TimeBucketResponseDto(
|
return TimeBucketResponseDto(
|
||||||
count: mapValueOfType<int>(json, r'count')!,
|
bucketAssets: TimeBucketAssetResponseDto.fromJson(json[r'bucketAssets'])!,
|
||||||
timeBucket: mapValueOfType<String>(json, r'timeBucket')!,
|
hasNextPage: mapValueOfType<bool>(json, r'hasNextPage')!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -100,8 +100,8 @@ class TimeBucketResponseDto {
|
|||||||
|
|
||||||
/// The list of required keys that must be present in a JSON.
|
/// The list of required keys that must be present in a JSON.
|
||||||
static const requiredKeys = <String>{
|
static const requiredKeys = <String>{
|
||||||
'count',
|
'bucketAssets',
|
||||||
'timeBucket',
|
'hasNextPage',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
107
mobile/openapi/lib/model/time_buckets_response_dto.dart
generated
Normal file
107
mobile/openapi/lib/model/time_buckets_response_dto.dart
generated
Normal file
@ -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 TimeBucketsResponseDto {
|
||||||
|
/// Returns a new [TimeBucketsResponseDto] instance.
|
||||||
|
TimeBucketsResponseDto({
|
||||||
|
required this.count,
|
||||||
|
required this.timeBucket,
|
||||||
|
});
|
||||||
|
|
||||||
|
int count;
|
||||||
|
|
||||||
|
String timeBucket;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is TimeBucketsResponseDto &&
|
||||||
|
other.count == count &&
|
||||||
|
other.timeBucket == timeBucket;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(count.hashCode) +
|
||||||
|
(timeBucket.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'TimeBucketsResponseDto[count=$count, timeBucket=$timeBucket]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'count'] = this.count;
|
||||||
|
json[r'timeBucket'] = this.timeBucket;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [TimeBucketsResponseDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static TimeBucketsResponseDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "TimeBucketsResponseDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return TimeBucketsResponseDto(
|
||||||
|
count: mapValueOfType<int>(json, r'count')!,
|
||||||
|
timeBucket: mapValueOfType<String>(json, r'timeBucket')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<TimeBucketsResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <TimeBucketsResponseDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = TimeBucketsResponseDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, TimeBucketsResponseDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, TimeBucketsResponseDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = TimeBucketsResponseDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of TimeBucketsResponseDto-objects as value to a dart map
|
||||||
|
static Map<String, List<TimeBucketsResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<TimeBucketsResponseDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = TimeBucketsResponseDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'count',
|
||||||
|
'timeBucket',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
115
mobile/openapi/lib/model/timeline_stack_response_dto.dart
generated
Normal file
115
mobile/openapi/lib/model/timeline_stack_response_dto.dart
generated
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class TimelineStackResponseDto {
|
||||||
|
/// Returns a new [TimelineStackResponseDto] instance.
|
||||||
|
TimelineStackResponseDto({
|
||||||
|
required this.assetCount,
|
||||||
|
required this.id,
|
||||||
|
required this.primaryAssetId,
|
||||||
|
});
|
||||||
|
|
||||||
|
num assetCount;
|
||||||
|
|
||||||
|
String id;
|
||||||
|
|
||||||
|
String primaryAssetId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is TimelineStackResponseDto &&
|
||||||
|
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() => 'TimelineStackResponseDto[assetCount=$assetCount, id=$id, primaryAssetId=$primaryAssetId]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'assetCount'] = this.assetCount;
|
||||||
|
json[r'id'] = this.id;
|
||||||
|
json[r'primaryAssetId'] = this.primaryAssetId;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [TimelineStackResponseDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static TimelineStackResponseDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "TimelineStackResponseDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return TimelineStackResponseDto(
|
||||||
|
assetCount: num.parse('${json[r'assetCount']}'),
|
||||||
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
|
primaryAssetId: mapValueOfType<String>(json, r'primaryAssetId')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<TimelineStackResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <TimelineStackResponseDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = TimelineStackResponseDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, TimelineStackResponseDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, TimelineStackResponseDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = TimelineStackResponseDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of TimelineStackResponseDto-objects as value to a dart map
|
||||||
|
static Map<String, List<TimelineStackResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<TimelineStackResponseDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = TimelineStackResponseDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'assetCount',
|
||||||
|
'id',
|
||||||
|
'primaryAssetId',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
301
open-api/bin/native_class.mustache
Normal file
301
open-api/bin/native_class.mustache
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
class {{{classname}}} {
|
||||||
|
{{>dart_constructor}}
|
||||||
|
{{#vars}}
|
||||||
|
{{#description}}
|
||||||
|
/// {{{.}}}
|
||||||
|
{{/description}}
|
||||||
|
{{^isEnum}}
|
||||||
|
{{#minimum}}
|
||||||
|
{{#description}}
|
||||||
|
///
|
||||||
|
{{/description}}
|
||||||
|
/// Minimum value: {{{.}}}
|
||||||
|
{{/minimum}}
|
||||||
|
{{#maximum}}
|
||||||
|
{{#description}}
|
||||||
|
{{^minimum}}
|
||||||
|
///
|
||||||
|
{{/minimum}}
|
||||||
|
{{/description}}
|
||||||
|
/// Maximum value: {{{.}}}
|
||||||
|
{{/maximum}}
|
||||||
|
{{^isNullable}}
|
||||||
|
{{^required}}
|
||||||
|
{{^defaultValue}}
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
{{/defaultValue}}
|
||||||
|
{{/required}}
|
||||||
|
{{/isNullable}}
|
||||||
|
{{/isEnum}}
|
||||||
|
{{{datatypeWithEnum}}}{{#isNullable}}?{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}?{{/defaultValue}}{{/required}}{{/isNullable}} {{{name}}};
|
||||||
|
|
||||||
|
{{/vars}}
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is {{{classname}}} &&
|
||||||
|
{{#vars}}
|
||||||
|
{{#isMap}}_deepEquality.equals(other.{{{name}}}, {{{name}}}){{/isMap}}{{^isMap}}{{#isArray}}_deepEquality.equals(other.{{{name}}}, {{{name}}}){{/isArray}}{{^isArray}}other.{{{name}}} == {{{name}}}{{/isArray}}{{/isMap}}{{^-last}} &&{{/-last}}{{#-last}};{{/-last}}
|
||||||
|
{{/vars}}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
{{#vars}}
|
||||||
|
({{#isNullable}}{{{name}}} == null ? 0 : {{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}{{{name}}} == null ? 0 : {{/defaultValue}}{{/required}}{{/isNullable}}{{{name}}}{{#isNullable}}!{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}!{{/defaultValue}}{{/required}}{{/isNullable}}.hashCode){{^-last}} +{{/-last}}{{#-last}};{{/-last}}
|
||||||
|
{{/vars}}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => '{{{classname}}}[{{#vars}}{{{name}}}=${{{name}}}{{^-last}}, {{/-last}}{{/vars}}]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
{{#vars}}
|
||||||
|
{{#isNullable}}
|
||||||
|
if (this.{{{name}}} != null) {
|
||||||
|
{{/isNullable}}
|
||||||
|
{{^isNullable}}
|
||||||
|
{{^required}}
|
||||||
|
{{^defaultValue}}
|
||||||
|
if (this.{{{name}}} != null) {
|
||||||
|
{{/defaultValue}}
|
||||||
|
{{/required}}
|
||||||
|
{{/isNullable}}
|
||||||
|
{{#isDateTime}}
|
||||||
|
{{#pattern}}
|
||||||
|
json[r'{{{baseName}}}'] = _isEpochMarker(r'{{{pattern}}}')
|
||||||
|
? this.{{{name}}}{{#isNullable}}!{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}!{{/defaultValue}}{{/required}}{{/isNullable}}.millisecondsSinceEpoch
|
||||||
|
: this.{{{name}}}{{#isNullable}}!{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}!{{/defaultValue}}{{/required}}{{/isNullable}}.toUtc().toIso8601String();
|
||||||
|
{{/pattern}}
|
||||||
|
{{^pattern}}
|
||||||
|
json[r'{{{baseName}}}'] = this.{{{name}}}{{#isNullable}}!{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}!{{/defaultValue}}{{/required}}{{/isNullable}}.toUtc().toIso8601String();
|
||||||
|
{{/pattern}}
|
||||||
|
{{/isDateTime}}
|
||||||
|
{{#isDate}}
|
||||||
|
{{#pattern}}
|
||||||
|
json[r'{{{baseName}}}'] = _isEpochMarker(r'{{{pattern}}}')
|
||||||
|
? this.{{{name}}}{{#isNullable}}!{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}!{{/defaultValue}}{{/required}}{{/isNullable}}.millisecondsSinceEpoch
|
||||||
|
: _dateFormatter.format(this.{{{name}}}{{#isNullable}}!{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}!{{/defaultValue}}{{/required}}{{/isNullable}}.toUtc());
|
||||||
|
{{/pattern}}
|
||||||
|
{{^pattern}}
|
||||||
|
json[r'{{{baseName}}}'] = _dateFormatter.format(this.{{{name}}}{{#isNullable}}!{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}!{{/defaultValue}}{{/required}}{{/isNullable}}.toUtc());
|
||||||
|
{{/pattern}}
|
||||||
|
{{/isDate}}
|
||||||
|
{{^isDateTime}}
|
||||||
|
{{^isDate}}
|
||||||
|
json[r'{{{baseName}}}'] = this.{{{name}}}{{#isArray}}{{#uniqueItems}}{{#isNullable}}!{{/isNullable}}.toList(growable: false){{/uniqueItems}}{{/isArray}};
|
||||||
|
{{/isDate}}
|
||||||
|
{{/isDateTime}}
|
||||||
|
{{#isNullable}}
|
||||||
|
} else {
|
||||||
|
json[r'{{{baseName}}}'] = null;
|
||||||
|
}
|
||||||
|
{{/isNullable}}
|
||||||
|
{{^isNullable}}
|
||||||
|
{{^required}}
|
||||||
|
{{^defaultValue}}
|
||||||
|
} else {
|
||||||
|
json[r'{{{baseName}}}'] = null;
|
||||||
|
}
|
||||||
|
{{/defaultValue}}
|
||||||
|
{{/required}}
|
||||||
|
{{/isNullable}}
|
||||||
|
{{/vars}}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [{{{classname}}}] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static {{{classname}}}? fromJson(dynamic value) {
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
// Ensure that the map contains the required keys.
|
||||||
|
// Note 1: the values aren't checked for validity beyond being non-null.
|
||||||
|
// Note 2: this code is stripped in release mode!
|
||||||
|
assert(() {
|
||||||
|
requiredKeys.forEach((key) {
|
||||||
|
assert(json.containsKey(key), 'Required key "{{{classname}}}[$key]" is missing from JSON.');
|
||||||
|
assert(json[key] != null, 'Required key "{{{classname}}}[$key]" has a null value in JSON.');
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
|
||||||
|
return {{{classname}}}(
|
||||||
|
{{#vars}}
|
||||||
|
{{#isDateTime}}
|
||||||
|
{{{name}}}: mapDateTime(json, r'{{{baseName}}}', r'{{{pattern}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
|
||||||
|
{{/isDateTime}}
|
||||||
|
{{#isDate}}
|
||||||
|
{{{name}}}: mapDateTime(json, r'{{{baseName}}}', r'{{{pattern}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
|
||||||
|
{{/isDate}}
|
||||||
|
{{^isDateTime}}
|
||||||
|
{{^isDate}}
|
||||||
|
{{#complexType}}
|
||||||
|
{{#isArray}}
|
||||||
|
{{#items.isArray}}
|
||||||
|
{{{name}}}: json[r'{{{baseName}}}'] is List
|
||||||
|
? (json[r'{{{baseName}}}'] as List).map((e) =>
|
||||||
|
{{#items.complexType}}
|
||||||
|
{{items.complexType}}.listFromJson(json[r'{{{baseName}}}']){{#uniqueItems}}.toSet(){{/uniqueItems}}
|
||||||
|
{{/items.complexType}}
|
||||||
|
{{^items.complexType}}
|
||||||
|
e == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.items.dataType}}>[]{{/items.isNullable}} : (e as List).cast<{{items.items.dataType}}>()
|
||||||
|
{{/items.complexType}}
|
||||||
|
).toList()
|
||||||
|
: {{#isNullable}}null{{/isNullable}}{{^isNullable}}const []{{/isNullable}},
|
||||||
|
{{/items.isArray}}
|
||||||
|
{{^items.isArray}}
|
||||||
|
{{{name}}}: {{{complexType}}}.listFromJson(json[r'{{{baseName}}}']){{#uniqueItems}}.toSet(){{/uniqueItems}},
|
||||||
|
{{/items.isArray}}
|
||||||
|
{{/isArray}}
|
||||||
|
{{^isArray}}
|
||||||
|
{{#isMap}}
|
||||||
|
{{#items.isArray}}
|
||||||
|
{{{name}}}: json[r'{{{baseName}}}'] == null
|
||||||
|
? {{#defaultValue}}{{{.}}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}}
|
||||||
|
{{#items.complexType}}
|
||||||
|
: {{items.complexType}}.mapListFromJson(json[r'{{{baseName}}}']),
|
||||||
|
{{/items.complexType}}
|
||||||
|
{{^items.complexType}}
|
||||||
|
: mapCastOfType<String, List>(json, r'{{{baseName}}}'),
|
||||||
|
{{/items.complexType}}
|
||||||
|
{{/items.isArray}}
|
||||||
|
{{^items.isArray}}
|
||||||
|
{{#items.isMap}}
|
||||||
|
{{#items.complexType}}
|
||||||
|
{{{name}}}: {{items.complexType}}.mapFromJson(json[r'{{{baseName}}}']),
|
||||||
|
{{/items.complexType}}
|
||||||
|
{{^items.complexType}}
|
||||||
|
{{{name}}}: mapCastOfType<String, dynamic>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
|
||||||
|
{{/items.complexType}}
|
||||||
|
{{/items.isMap}}
|
||||||
|
{{^items.isMap}}
|
||||||
|
{{#items.complexType}}
|
||||||
|
{{{name}}}: {{{items.complexType}}}.mapFromJson(json[r'{{{baseName}}}']),
|
||||||
|
{{/items.complexType}}
|
||||||
|
{{^items.complexType}}
|
||||||
|
{{{name}}}: mapCastOfType<String, {{items.dataType}}>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
|
||||||
|
{{/items.complexType}}
|
||||||
|
{{/items.isMap}}
|
||||||
|
{{/items.isArray}}
|
||||||
|
{{/isMap}}
|
||||||
|
{{^isMap}}
|
||||||
|
{{#isBinary}}
|
||||||
|
{{{name}}}: null, // No support for decoding binary content from JSON
|
||||||
|
{{/isBinary}}
|
||||||
|
{{^isBinary}}
|
||||||
|
{{{name}}}: {{{complexType}}}.fromJson(json[r'{{{baseName}}}']){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
|
||||||
|
{{/isBinary}}
|
||||||
|
{{/isMap}}
|
||||||
|
{{/isArray}}
|
||||||
|
{{/complexType}}
|
||||||
|
{{^complexType}}
|
||||||
|
{{#isArray}}
|
||||||
|
{{#isEnum}}
|
||||||
|
{{{name}}}: {{{items.datatypeWithEnum}}}.listFromJson(json[r'{{{baseName}}}']){{#uniqueItems}}.toSet(){{/uniqueItems}},
|
||||||
|
{{/isEnum}}
|
||||||
|
{{^isEnum}}
|
||||||
|
{{{name}}}: json[r'{{{baseName}}}'] is Iterable
|
||||||
|
? (json[r'{{{baseName}}}'] as Iterable).cast<{{{items.datatype}}}>().{{#uniqueItems}}toSet(){{/uniqueItems}}{{^uniqueItems}}toList(growable: false){{/uniqueItems}}
|
||||||
|
: {{#defaultValue}}{{{.}}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}},
|
||||||
|
{{/isEnum}}
|
||||||
|
{{/isArray}}
|
||||||
|
{{^isArray}}
|
||||||
|
{{#isMap}}
|
||||||
|
{{{name}}}: mapCastOfType<String, {{{items.datatype}}}>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
|
||||||
|
{{/isMap}}
|
||||||
|
{{^isMap}}
|
||||||
|
{{#isNumber}}
|
||||||
|
{{{name}}}: {{#isNullable}}json[r'{{{baseName}}}'] == null
|
||||||
|
? {{#defaultValue}}{{{.}}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}}
|
||||||
|
: {{/isNullable}}{{{datatypeWithEnum}}}.parse('${json[r'{{{baseName}}}']}'),
|
||||||
|
{{/isNumber}}
|
||||||
|
{{^isNumber}}
|
||||||
|
{{^isEnum}}
|
||||||
|
{{{name}}}: mapValueOfType<{{{datatypeWithEnum}}}>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
|
||||||
|
{{/isEnum}}
|
||||||
|
{{#isEnum}}
|
||||||
|
{{{name}}}: {{{enumName}}}.fromJson(json[r'{{{baseName}}}']){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
|
||||||
|
{{/isEnum}}
|
||||||
|
{{/isNumber}}
|
||||||
|
{{/isMap}}
|
||||||
|
{{/isArray}}
|
||||||
|
{{/complexType}}
|
||||||
|
{{/isDate}}
|
||||||
|
{{/isDateTime}}
|
||||||
|
{{/vars}}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<{{{classname}}}> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <{{{classname}}}>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = {{{classname}}}.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, {{{classname}}}> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, {{{classname}}}>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = {{{classname}}}.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of {{{classname}}}-objects as value to a dart map
|
||||||
|
static Map<String, List<{{{classname}}}>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<{{{classname}}}>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = {{{classname}}}.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
{{#vars}}
|
||||||
|
{{#required}}
|
||||||
|
'{{{baseName}}}',
|
||||||
|
{{/required}}
|
||||||
|
{{/vars}}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{{#vars}}
|
||||||
|
{{^isModel}}
|
||||||
|
{{#isEnum}}
|
||||||
|
{{^isContainer}}
|
||||||
|
|
||||||
|
{{>serialization/native/native_enum_inline}}
|
||||||
|
{{/isContainer}}
|
||||||
|
{{#isContainer}}
|
||||||
|
{{#mostInnerItems}}
|
||||||
|
|
||||||
|
{{>serialization/native/native_enum_inline}}
|
||||||
|
{{/mostInnerItems}}
|
||||||
|
{{/isContainer}}
|
||||||
|
{{/isEnum}}
|
||||||
|
{{/isModel}}
|
||||||
|
{{/vars}}
|
@ -6815,6 +6815,23 @@
|
|||||||
"$ref": "#/components/schemas/AssetOrder"
|
"$ref": "#/components/schemas/AssetOrder"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "page",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"minimum": 1,
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "pageSize",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "personId",
|
"name": "personId",
|
||||||
"required": false,
|
"required": false,
|
||||||
@ -6880,10 +6897,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"items": {
|
"$ref": "#/components/schemas/TimeBucketResponseDto"
|
||||||
"$ref": "#/components/schemas/AssetResponseDto"
|
|
||||||
},
|
|
||||||
"type": "array"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -7017,7 +7031,7 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/components/schemas/TimeBucketResponseDto"
|
"$ref": "#/components/schemas/TimeBucketsResponseDto"
|
||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array"
|
||||||
}
|
}
|
||||||
@ -13393,7 +13407,177 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"TimeBucketAssetResponseDto": {
|
||||||
|
"properties": {
|
||||||
|
"duration": {
|
||||||
|
"default": [],
|
||||||
|
"items": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"default": [],
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"isArchived": {
|
||||||
|
"default": [],
|
||||||
|
"items": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"isFavorite": {
|
||||||
|
"default": [],
|
||||||
|
"items": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"isImage": {
|
||||||
|
"default": [],
|
||||||
|
"items": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"isTrashed": {
|
||||||
|
"default": [],
|
||||||
|
"items": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"isVideo": {
|
||||||
|
"default": [],
|
||||||
|
"items": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"livePhotoVideoId": {
|
||||||
|
"default": [],
|
||||||
|
"items": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"localDateTime": {
|
||||||
|
"default": [],
|
||||||
|
"items": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"ownerId": {
|
||||||
|
"default": [],
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"projectionType": {
|
||||||
|
"default": [],
|
||||||
|
"items": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"ratio": {
|
||||||
|
"default": [],
|
||||||
|
"items": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"stack": {
|
||||||
|
"default": [],
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/TimelineStackResponseDto"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"thumbhash": {
|
||||||
|
"default": [],
|
||||||
|
"items": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"duration",
|
||||||
|
"id",
|
||||||
|
"isArchived",
|
||||||
|
"isFavorite",
|
||||||
|
"isImage",
|
||||||
|
"isTrashed",
|
||||||
|
"isVideo",
|
||||||
|
"livePhotoVideoId",
|
||||||
|
"localDateTime",
|
||||||
|
"ownerId",
|
||||||
|
"projectionType",
|
||||||
|
"ratio",
|
||||||
|
"stack",
|
||||||
|
"thumbhash"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"TimeBucketResponseDto": {
|
"TimeBucketResponseDto": {
|
||||||
|
"properties": {
|
||||||
|
"bucketAssets": {
|
||||||
|
"$ref": "#/components/schemas/TimeBucketAssetResponseDto"
|
||||||
|
},
|
||||||
|
"hasNextPage": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"bucketAssets",
|
||||||
|
"hasNextPage"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"TimeBucketSize": {
|
||||||
|
"enum": [
|
||||||
|
"DAY",
|
||||||
|
"MONTH"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"TimeBucketsResponseDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"count": {
|
"count": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
@ -13408,12 +13592,24 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"TimeBucketSize": {
|
"TimelineStackResponseDto": {
|
||||||
"enum": [
|
"properties": {
|
||||||
"DAY",
|
"assetCount": {
|
||||||
"MONTH"
|
"type": "number"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"primaryAssetId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"assetCount",
|
||||||
|
"id",
|
||||||
|
"primaryAssetId"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"ToneMapping": {
|
"ToneMapping": {
|
||||||
"enum": [
|
"enum": [
|
||||||
|
@ -1376,7 +1376,32 @@ export type TagBulkAssetsResponseDto = {
|
|||||||
export type TagUpdateDto = {
|
export type TagUpdateDto = {
|
||||||
color?: string | null;
|
color?: string | null;
|
||||||
};
|
};
|
||||||
|
export type TimelineStackResponseDto = {
|
||||||
|
assetCount: number;
|
||||||
|
id: string;
|
||||||
|
primaryAssetId: string;
|
||||||
|
};
|
||||||
|
export type TimeBucketAssetResponseDto = {
|
||||||
|
duration: (string | number)[];
|
||||||
|
id: string[];
|
||||||
|
isArchived: number[];
|
||||||
|
isFavorite: number[];
|
||||||
|
isImage: number[];
|
||||||
|
isTrashed: number[];
|
||||||
|
isVideo: number[];
|
||||||
|
livePhotoVideoId: (string | number)[];
|
||||||
|
localDateTime: string[];
|
||||||
|
ownerId: string[];
|
||||||
|
projectionType: (string | number)[];
|
||||||
|
ratio: number[];
|
||||||
|
stack: TimelineStackResponseDto[];
|
||||||
|
thumbhash: (string | number)[];
|
||||||
|
};
|
||||||
export type TimeBucketResponseDto = {
|
export type TimeBucketResponseDto = {
|
||||||
|
bucketAssets: TimeBucketAssetResponseDto;
|
||||||
|
hasNextPage: boolean;
|
||||||
|
};
|
||||||
|
export type TimeBucketsResponseDto = {
|
||||||
count: number;
|
count: number;
|
||||||
timeBucket: string;
|
timeBucket: string;
|
||||||
};
|
};
|
||||||
@ -3197,13 +3222,15 @@ export function tagAssets({ id, bulkIdsDto }: {
|
|||||||
body: bulkIdsDto
|
body: bulkIdsDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, tagId, timeBucket, userId, withPartners, withStacked }: {
|
export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, order, page, pageSize, personId, size, tagId, timeBucket, userId, withPartners, withStacked }: {
|
||||||
albumId?: string;
|
albumId?: string;
|
||||||
isArchived?: boolean;
|
isArchived?: boolean;
|
||||||
isFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
isTrashed?: boolean;
|
isTrashed?: boolean;
|
||||||
key?: string;
|
key?: string;
|
||||||
order?: AssetOrder;
|
order?: AssetOrder;
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
personId?: string;
|
personId?: string;
|
||||||
size: TimeBucketSize;
|
size: TimeBucketSize;
|
||||||
tagId?: string;
|
tagId?: string;
|
||||||
@ -3214,7 +3241,7 @@ export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key,
|
|||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
data: AssetResponseDto[];
|
data: TimeBucketResponseDto;
|
||||||
}>(`/timeline/bucket${QS.query(QS.explode({
|
}>(`/timeline/bucket${QS.query(QS.explode({
|
||||||
albumId,
|
albumId,
|
||||||
isArchived,
|
isArchived,
|
||||||
@ -3222,6 +3249,8 @@ export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key,
|
|||||||
isTrashed,
|
isTrashed,
|
||||||
key,
|
key,
|
||||||
order,
|
order,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
personId,
|
personId,
|
||||||
size,
|
size,
|
||||||
tagId,
|
tagId,
|
||||||
@ -3249,7 +3278,7 @@ export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key
|
|||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
data: TimeBucketResponseDto[];
|
data: TimeBucketsResponseDto[];
|
||||||
}>(`/timeline/buckets${QS.query(QS.explode({
|
}>(`/timeline/buckets${QS.query(QS.explode({
|
||||||
albumId,
|
albumId,
|
||||||
isArchived,
|
isArchived,
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { Controller, Get, Query } from '@nestjs/common';
|
import { Controller, Get, Query } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto';
|
import { TimeBucketAssetDto, TimeBucketDto } from 'src/dtos/time-bucket.dto';
|
||||||
import { Permission } from 'src/enum';
|
import { Permission } from 'src/enum';
|
||||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||||
import { TimelineService } from 'src/services/timeline.service';
|
import { TimelineService } from 'src/services/timeline.service';
|
||||||
@ -14,13 +13,13 @@ export class TimelineController {
|
|||||||
|
|
||||||
@Get('buckets')
|
@Get('buckets')
|
||||||
@Authenticated({ permission: Permission.ASSET_READ, sharedLink: true })
|
@Authenticated({ permission: Permission.ASSET_READ, sharedLink: true })
|
||||||
getTimeBuckets(@Auth() auth: AuthDto, @Query() dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> {
|
getTimeBuckets(@Auth() auth: AuthDto, @Query() dto: TimeBucketDto) {
|
||||||
return this.service.getTimeBuckets(auth, dto);
|
return this.service.getTimeBuckets(auth, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('bucket')
|
@Get('bucket')
|
||||||
@Authenticated({ permission: Permission.ASSET_READ, sharedLink: true })
|
@Authenticated({ permission: Permission.ASSET_READ, sharedLink: true })
|
||||||
getTimeBucket(@Auth() auth: AuthDto, @Query() dto: TimeBucketAssetDto): Promise<AssetResponseDto[]> {
|
getTimeBucket(@Auth() auth: AuthDto, @Query() dto: TimeBucketAssetDto) {
|
||||||
return this.service.getTimeBucket(auth, dto) as Promise<AssetResponseDto[]>;
|
return this.service.getTimeBucket(auth, dto);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2
server/src/db.d.ts
vendored
2
server/src/db.d.ts
vendored
@ -153,7 +153,7 @@ export interface Assets {
|
|||||||
isVisible: Generated<boolean>;
|
isVisible: Generated<boolean>;
|
||||||
libraryId: string | null;
|
libraryId: string | null;
|
||||||
livePhotoVideoId: string | null;
|
livePhotoVideoId: string | null;
|
||||||
localDateTime: Timestamp | null;
|
localDateTime: Timestamp;
|
||||||
originalFileName: string;
|
originalFileName: string;
|
||||||
originalPath: string;
|
originalPath: string;
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
|
@ -13,6 +13,7 @@ import { TagResponseDto, mapTag } from 'src/dtos/tag.dto';
|
|||||||
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { AssetType } from 'src/enum';
|
import { AssetType } from 'src/enum';
|
||||||
|
import { hexOrBufferToBase64 } from 'src/utils/bytes';
|
||||||
import { mimeTypes } from 'src/utils/mime-types';
|
import { mimeTypes } from 'src/utils/mime-types';
|
||||||
|
|
||||||
export class SanitizedAssetResponseDto {
|
export class SanitizedAssetResponseDto {
|
||||||
@ -102,15 +103,6 @@ const mapStack = (entity: AssetEntity) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// if an asset is jsonified in the DB before being returned, its buffer fields will be hex-encoded strings
|
|
||||||
export const hexOrBufferToBase64 = (encoded: string | Buffer) => {
|
|
||||||
if (typeof encoded === 'string') {
|
|
||||||
return Buffer.from(encoded.slice(2), 'hex').toString('base64');
|
|
||||||
}
|
|
||||||
|
|
||||||
return encoded.toString('base64');
|
|
||||||
};
|
|
||||||
|
|
||||||
export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): AssetResponseDto {
|
export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): AssetResponseDto {
|
||||||
const { stripMetadata = false, withStack = false } = options;
|
const { stripMetadata = false, withStack = false } = options;
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
|
||||||
|
import { IsEnum, IsInt, IsNotEmpty, IsString, Min } from 'class-validator';
|
||||||
import { AssetOrder } from 'src/enum';
|
import { AssetOrder } from 'src/enum';
|
||||||
import { TimeBucketSize } from 'src/repositories/asset.repository';
|
import { TimeBucketSize } from 'src/repositories/asset.repository';
|
||||||
|
import { TimeBucketAssets, TimelineStack } from 'src/services/timeline.service.types';
|
||||||
import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
|
import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
|
||||||
|
|
||||||
export class TimeBucketDto {
|
export class TimeBucketDto {
|
||||||
@ -46,12 +48,132 @@ export class TimeBucketDto {
|
|||||||
export class TimeBucketAssetDto extends TimeBucketDto {
|
export class TimeBucketAssetDto extends TimeBucketDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
timeBucket!: string;
|
timeBucket!: string;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
@Optional()
|
||||||
|
page?: number;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@Optional()
|
||||||
|
pageSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TimeBucketResponseDto {
|
export class TimelineStackResponseDto implements TimelineStack {
|
||||||
|
@ApiProperty()
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
primaryAssetId!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
assetCount!: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TimeBucketAssetResponseDto implements TimeBucketAssets {
|
||||||
|
@ApiProperty({ type: [String] })
|
||||||
|
id: string[] = [];
|
||||||
|
|
||||||
|
@ApiProperty({ type: [String] })
|
||||||
|
ownerId: string[] = [];
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
ratio: number[] = [];
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
isFavorite: number[] = [];
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
isArchived: number[] = [];
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
isTrashed: number[] = [];
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
isImage: number[] = [];
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
isVideo: number[] = [];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
oneOf: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
thumbhash: (string | number)[] = [];
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
localDateTime: Date[] = [];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
oneOf: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
duration: (string | number)[] = [];
|
||||||
|
|
||||||
|
@ApiProperty({ type: [TimelineStackResponseDto] })
|
||||||
|
stack: (TimelineStackResponseDto | number)[] = [];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
oneOf: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
projectionType: (string | number)[] = [];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
oneOf: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
livePhotoVideoId: (string | number)[] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TimeBucketsResponseDto {
|
||||||
@ApiProperty({ type: 'string' })
|
@ApiProperty({ type: 'string' })
|
||||||
timeBucket!: string;
|
timeBucket!: string;
|
||||||
|
|
||||||
@ApiProperty({ type: 'integer' })
|
@ApiProperty({ type: 'integer' })
|
||||||
count!: number;
|
count!: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class TimeBucketResponseDto {
|
||||||
|
@ApiProperty({ type: TimeBucketAssetResponseDto })
|
||||||
|
bucketAssets!: TimeBucketAssetResponseDto;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
hasNextPage!: boolean;
|
||||||
|
}
|
||||||
|
@ -1,4 +1,12 @@
|
|||||||
import { DeduplicateJoinsPlugin, ExpressionBuilder, Kysely, SelectQueryBuilder, sql } from 'kysely';
|
import {
|
||||||
|
DeduplicateJoinsPlugin,
|
||||||
|
Expression,
|
||||||
|
expressionBuilder,
|
||||||
|
ExpressionBuilder,
|
||||||
|
Kysely,
|
||||||
|
SelectQueryBuilder,
|
||||||
|
sql,
|
||||||
|
} from 'kysely';
|
||||||
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||||
import { AssetFace, AssetFile, AssetJobStatus, columns, Exif, Stack, Tag, User } from 'src/database';
|
import { AssetFace, AssetFile, AssetJobStatus, columns, Exif, Stack, Tag, User } from 'src/database';
|
||||||
import { DB } from 'src/db';
|
import { DB } from 'src/db';
|
||||||
@ -105,19 +113,30 @@ export function withFacesAndPeople(eb: ExpressionBuilder<DB, 'assets'>, withDele
|
|||||||
).as('faces');
|
).as('faces');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasPeople<O>(qb: SelectQueryBuilder<DB, 'assets', O>, personIds: string[]) {
|
// export function hasPeople<O>(qb: SelectQueryBuilder<DB, 'assets', O>, personIds: string[]) {
|
||||||
return qb.innerJoin(
|
// return qb.innerJoin(
|
||||||
(eb) =>
|
// (eb) =>
|
||||||
eb
|
// eb
|
||||||
.selectFrom('asset_faces')
|
// .selectFrom('asset_faces')
|
||||||
.select('assetId')
|
// .select('assetId')
|
||||||
.where('personId', '=', anyUuid(personIds!))
|
// .where('personId', '=', anyUuid(personIds!))
|
||||||
.where('deletedAt', 'is', null)
|
// .where('deletedAt', 'is', null)
|
||||||
.groupBy('assetId')
|
// .groupBy('assetId')
|
||||||
.having((eb) => eb.fn.count('personId').distinct(), '=', personIds.length)
|
// .having((eb) => eb.fn.count('personId').distinct(), '=', personIds.length)
|
||||||
.as('has_people'),
|
// .as('has_people'),
|
||||||
(join) => join.onRef('has_people.assetId', '=', 'assets.id'),
|
// (join) => join.onRef('has_people.assetId', '=', 'assets.id'),
|
||||||
);
|
// );
|
||||||
|
// }
|
||||||
|
export function hasPeople(personIds: string[]) {
|
||||||
|
const eb = expressionBuilder<DB, never>();
|
||||||
|
return eb
|
||||||
|
.selectFrom('asset_faces')
|
||||||
|
.select('assetId')
|
||||||
|
.where('personId', '=', anyUuid(personIds!))
|
||||||
|
.where('deletedAt', 'is', null)
|
||||||
|
.groupBy('assetId')
|
||||||
|
.having((eb) => eb.fn.count('personId').distinct(), '=', personIds.length)
|
||||||
|
.as('has_people');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasTags<O>(qb: SelectQueryBuilder<DB, 'assets', O>, tagIds: string[]) {
|
export function hasTags<O>(qb: SelectQueryBuilder<DB, 'assets', O>, tagIds: string[]) {
|
||||||
@ -159,15 +178,25 @@ export function truncatedDate<O>(size: TimeBucketSize) {
|
|||||||
return sql<O>`date_trunc(${size}, "localDateTime" at time zone 'UTC') at time zone 'UTC'`;
|
return sql<O>`date_trunc(${size}, "localDateTime" at time zone 'UTC') at time zone 'UTC'`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withTagId<O>(qb: SelectQueryBuilder<DB, 'assets', O>, tagId: string) {
|
// export function withTagId<O>(qb: SelectQueryBuilder<DB, 'assets', O>, tagId: string) {
|
||||||
return qb.where((eb) =>
|
// return qb.where((eb) =>
|
||||||
eb.exists(
|
// eb.exists(
|
||||||
eb
|
// eb
|
||||||
.selectFrom('tags_closure')
|
// .selectFrom('tags_closure')
|
||||||
.innerJoin('tag_asset', 'tag_asset.tagsId', 'tags_closure.id_descendant')
|
// .innerJoin('tag_asset', 'tag_asset.tagsId', 'tags_closure.id_descendant')
|
||||||
.whereRef('tag_asset.assetsId', '=', 'assets.id')
|
// .whereRef('tag_asset.assetsId', '=', 'assets.id')
|
||||||
.where('tags_closure.id_ancestor', '=', tagId),
|
// .where('tags_closure.id_ancestor', '=', tagId),
|
||||||
),
|
// ),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
export function withTagId(tagId: string, assetId: Expression<string>) {
|
||||||
|
const eb = expressionBuilder<DB, never>();
|
||||||
|
return eb.exists(
|
||||||
|
eb
|
||||||
|
.selectFrom('tags_closure')
|
||||||
|
.innerJoin('tag_asset', 'tag_asset.tagsId', 'tags_closure.id_descendant')
|
||||||
|
.whereRef('tag_asset.assetsId', '=', assetId)
|
||||||
|
.where('tags_closure.id_ancestor', '=', tagId),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,94 +206,106 @@ const joinDeduplicationPlugin = new DeduplicateJoinsPlugin();
|
|||||||
export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuilderOptions) {
|
export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuilderOptions) {
|
||||||
options.isArchived ??= options.withArchived ? undefined : false;
|
options.isArchived ??= options.withArchived ? undefined : false;
|
||||||
options.withDeleted ||= !!(options.trashedAfter || options.trashedBefore || options.isOffline);
|
options.withDeleted ||= !!(options.trashedAfter || options.trashedBefore || options.isOffline);
|
||||||
return kysely
|
return (
|
||||||
.withPlugin(joinDeduplicationPlugin)
|
kysely
|
||||||
.selectFrom('assets')
|
.withPlugin(joinDeduplicationPlugin)
|
||||||
.selectAll('assets')
|
.selectFrom('assets')
|
||||||
.$if(!!options.tagIds && options.tagIds.length > 0, (qb) => hasTags(qb, options.tagIds!))
|
.selectAll('assets')
|
||||||
.$if(!!options.personIds && options.personIds.length > 0, (qb) => hasPeople(qb, options.personIds!))
|
.$if(!!options.tagIds && options.tagIds.length > 0, (qb) => hasTags(qb, options.tagIds!))
|
||||||
.$if(!!options.createdBefore, (qb) => qb.where('assets.createdAt', '<=', options.createdBefore!))
|
// .$if(!!options.personIds && options.personIds.length > 0, (qb) => hasPeople(qb, options.personIds!))
|
||||||
.$if(!!options.createdAfter, (qb) => qb.where('assets.createdAt', '>=', options.createdAfter!))
|
.$if(!!options.personIds && options.personIds.length > 0, (qb) =>
|
||||||
.$if(!!options.updatedBefore, (qb) => qb.where('assets.updatedAt', '<=', options.updatedBefore!))
|
qb.innerJoin(
|
||||||
.$if(!!options.updatedAfter, (qb) => qb.where('assets.updatedAt', '>=', options.updatedAfter!))
|
() => hasPeople(options.personIds!),
|
||||||
.$if(!!options.trashedBefore, (qb) => qb.where('assets.deletedAt', '<=', options.trashedBefore!))
|
(join) => join.onRef('has_people.assetId', '=', 'assets.id'),
|
||||||
.$if(!!options.trashedAfter, (qb) => qb.where('assets.deletedAt', '>=', options.trashedAfter!))
|
),
|
||||||
.$if(!!options.takenBefore, (qb) => qb.where('assets.fileCreatedAt', '<=', options.takenBefore!))
|
)
|
||||||
.$if(!!options.takenAfter, (qb) => qb.where('assets.fileCreatedAt', '>=', options.takenAfter!))
|
.$if(!!options.createdBefore, (qb) => qb.where('assets.createdAt', '<=', options.createdBefore!))
|
||||||
.$if(options.city !== undefined, (qb) =>
|
.$if(!!options.createdAfter, (qb) => qb.where('assets.createdAt', '>=', options.createdAfter!))
|
||||||
qb
|
.$if(!!options.updatedBefore, (qb) => qb.where('assets.updatedAt', '<=', options.updatedBefore!))
|
||||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
.$if(!!options.updatedAfter, (qb) => qb.where('assets.updatedAt', '>=', options.updatedAfter!))
|
||||||
.where('exif.city', options.city === null ? 'is' : '=', options.city!),
|
.$if(!!options.trashedBefore, (qb) => qb.where('assets.deletedAt', '<=', options.trashedBefore!))
|
||||||
)
|
.$if(!!options.trashedAfter, (qb) => qb.where('assets.deletedAt', '>=', options.trashedAfter!))
|
||||||
.$if(options.state !== undefined, (qb) =>
|
.$if(!!options.takenBefore, (qb) => qb.where('assets.fileCreatedAt', '<=', options.takenBefore!))
|
||||||
qb
|
.$if(!!options.takenAfter, (qb) => qb.where('assets.fileCreatedAt', '>=', options.takenAfter!))
|
||||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
.$if(options.city !== undefined, (qb) =>
|
||||||
.where('exif.state', options.state === null ? 'is' : '=', options.state!),
|
qb
|
||||||
)
|
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
||||||
.$if(options.country !== undefined, (qb) =>
|
.where('exif.city', options.city === null ? 'is' : '=', options.city!),
|
||||||
qb
|
)
|
||||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
.$if(options.state !== undefined, (qb) =>
|
||||||
.where('exif.country', options.country === null ? 'is' : '=', options.country!),
|
qb
|
||||||
)
|
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
||||||
.$if(options.make !== undefined, (qb) =>
|
.where('exif.state', options.state === null ? 'is' : '=', options.state!),
|
||||||
qb
|
)
|
||||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
.$if(options.country !== undefined, (qb) =>
|
||||||
.where('exif.make', options.make === null ? 'is' : '=', options.make!),
|
qb
|
||||||
)
|
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
||||||
.$if(options.model !== undefined, (qb) =>
|
.where('exif.country', options.country === null ? 'is' : '=', options.country!),
|
||||||
qb
|
)
|
||||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
.$if(options.make !== undefined, (qb) =>
|
||||||
.where('exif.model', options.model === null ? 'is' : '=', options.model!),
|
qb
|
||||||
)
|
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
||||||
.$if(options.lensModel !== undefined, (qb) =>
|
.where('exif.make', options.make === null ? 'is' : '=', options.make!),
|
||||||
qb
|
)
|
||||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
.$if(options.model !== undefined, (qb) =>
|
||||||
.where('exif.lensModel', options.lensModel === null ? 'is' : '=', options.lensModel!),
|
qb
|
||||||
)
|
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
||||||
.$if(options.rating !== undefined, (qb) =>
|
.where('exif.model', options.model === null ? 'is' : '=', options.model!),
|
||||||
qb
|
)
|
||||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
.$if(options.lensModel !== undefined, (qb) =>
|
||||||
.where('exif.rating', options.rating === null ? 'is' : '=', options.rating!),
|
qb
|
||||||
)
|
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
||||||
.$if(!!options.checksum, (qb) => qb.where('assets.checksum', '=', options.checksum!))
|
.where('exif.lensModel', options.lensModel === null ? 'is' : '=', options.lensModel!),
|
||||||
.$if(!!options.deviceAssetId, (qb) => qb.where('assets.deviceAssetId', '=', options.deviceAssetId!))
|
)
|
||||||
.$if(!!options.deviceId, (qb) => qb.where('assets.deviceId', '=', options.deviceId!))
|
.$if(options.rating !== undefined, (qb) =>
|
||||||
.$if(!!options.id, (qb) => qb.where('assets.id', '=', asUuid(options.id!)))
|
qb
|
||||||
.$if(!!options.libraryId, (qb) => qb.where('assets.libraryId', '=', asUuid(options.libraryId!)))
|
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
||||||
.$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!)))
|
.where('exif.rating', options.rating === null ? 'is' : '=', options.rating!),
|
||||||
.$if(!!options.encodedVideoPath, (qb) => qb.where('assets.encodedVideoPath', '=', options.encodedVideoPath!))
|
)
|
||||||
.$if(!!options.originalPath, (qb) =>
|
.$if(!!options.checksum, (qb) => qb.where('assets.checksum', '=', options.checksum!))
|
||||||
qb.where(sql`f_unaccent(assets."originalPath")`, 'ilike', sql`'%' || f_unaccent(${options.originalPath}) || '%'`),
|
.$if(!!options.deviceAssetId, (qb) => qb.where('assets.deviceAssetId', '=', options.deviceAssetId!))
|
||||||
)
|
.$if(!!options.deviceId, (qb) => qb.where('assets.deviceId', '=', options.deviceId!))
|
||||||
.$if(!!options.originalFileName, (qb) =>
|
.$if(!!options.id, (qb) => qb.where('assets.id', '=', asUuid(options.id!)))
|
||||||
qb.where(
|
.$if(!!options.libraryId, (qb) => qb.where('assets.libraryId', '=', asUuid(options.libraryId!)))
|
||||||
sql`f_unaccent(assets."originalFileName")`,
|
.$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!)))
|
||||||
'ilike',
|
.$if(!!options.encodedVideoPath, (qb) => qb.where('assets.encodedVideoPath', '=', options.encodedVideoPath!))
|
||||||
sql`'%' || f_unaccent(${options.originalFileName}) || '%'`,
|
.$if(!!options.originalPath, (qb) =>
|
||||||
),
|
qb.where(
|
||||||
)
|
sql`f_unaccent(assets."originalPath")`,
|
||||||
.$if(!!options.description, (qb) =>
|
'ilike',
|
||||||
qb
|
sql`'%' || f_unaccent(${options.originalPath}) || '%'`,
|
||||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
),
|
||||||
.where(sql`f_unaccent(exif.description)`, 'ilike', sql`'%' || f_unaccent(${options.description}) || '%'`),
|
)
|
||||||
)
|
.$if(!!options.originalFileName, (qb) =>
|
||||||
.$if(!!options.type, (qb) => qb.where('assets.type', '=', options.type!))
|
qb.where(
|
||||||
.$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!))
|
sql`f_unaccent(assets."originalFileName")`,
|
||||||
.$if(options.isOffline !== undefined, (qb) => qb.where('assets.isOffline', '=', options.isOffline!))
|
'ilike',
|
||||||
.$if(options.isVisible !== undefined, (qb) => qb.where('assets.isVisible', '=', options.isVisible!))
|
sql`'%' || f_unaccent(${options.originalFileName}) || '%'`,
|
||||||
.$if(options.isArchived !== undefined, (qb) => qb.where('assets.isArchived', '=', options.isArchived!))
|
),
|
||||||
.$if(options.isEncoded !== undefined, (qb) =>
|
)
|
||||||
qb.where('assets.encodedVideoPath', options.isEncoded ? 'is not' : 'is', null),
|
.$if(!!options.description, (qb) =>
|
||||||
)
|
qb
|
||||||
.$if(options.isMotion !== undefined, (qb) =>
|
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
||||||
qb.where('assets.livePhotoVideoId', options.isMotion ? 'is not' : 'is', null),
|
.where(sql`f_unaccent(exif.description)`, 'ilike', sql`'%' || f_unaccent(${options.description}) || '%'`),
|
||||||
)
|
)
|
||||||
.$if(!!options.isNotInAlbum, (qb) =>
|
.$if(!!options.type, (qb) => qb.where('assets.type', '=', options.type!))
|
||||||
qb.where((eb) =>
|
.$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!))
|
||||||
eb.not(eb.exists((eb) => eb.selectFrom('albums_assets_assets').whereRef('assetsId', '=', 'assets.id'))),
|
.$if(options.isOffline !== undefined, (qb) => qb.where('assets.isOffline', '=', options.isOffline!))
|
||||||
),
|
.$if(options.isVisible !== undefined, (qb) => qb.where('assets.isVisible', '=', options.isVisible!))
|
||||||
)
|
.$if(options.isArchived !== undefined, (qb) => qb.where('assets.isArchived', '=', options.isArchived!))
|
||||||
.$if(!!options.withExif, withExifInner)
|
.$if(options.isEncoded !== undefined, (qb) =>
|
||||||
.$if(!!(options.withFaces || options.withPeople || options.personIds), (qb) => qb.select(withFacesAndPeople))
|
qb.where('assets.encodedVideoPath', options.isEncoded ? 'is not' : 'is', null),
|
||||||
.$if(!options.withDeleted, (qb) => qb.where('assets.deletedAt', 'is', null));
|
)
|
||||||
|
.$if(options.isMotion !== undefined, (qb) =>
|
||||||
|
qb.where('assets.livePhotoVideoId', options.isMotion ? 'is not' : 'is', null),
|
||||||
|
)
|
||||||
|
.$if(!!options.isNotInAlbum, (qb) =>
|
||||||
|
qb.where((eb) =>
|
||||||
|
eb.not(eb.exists((eb) => eb.selectFrom('albums_assets_assets').whereRef('assetsId', '=', 'assets.id'))),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.$if(!!options.withExif, withExifInner)
|
||||||
|
.$if(!!(options.withFaces || options.withPeople || options.personIds), (qb) => qb.select(withFacesAndPeople))
|
||||||
|
.$if(!options.withDeleted, (qb) => qb.where('assets.deletedAt', 'is', null))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Insertable, Kysely, Selectable, UpdateResult, Updateable, sql } from 'kysely';
|
import { Insertable, Kysely, Selectable, UpdateResult, Updateable, sql } from 'kysely';
|
||||||
import { isEmpty, isUndefined, omitBy } from 'lodash';
|
import { isEmpty, isUndefined, omitBy, round } from 'lodash';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { AssetFiles, AssetJobStatus, Assets, DB, Exif } from 'src/db';
|
import { AssetFiles, AssetJobStatus, Assets, DB, Exif } from 'src/db';
|
||||||
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
||||||
@ -25,6 +25,10 @@ import { anyUuid, asUuid, removeUndefinedKeys, unnest } from 'src/utils/database
|
|||||||
import { globToSqlPattern } from 'src/utils/misc';
|
import { globToSqlPattern } from 'src/utils/misc';
|
||||||
import { Paginated, PaginationOptions, paginationHelper } from 'src/utils/pagination';
|
import { Paginated, PaginationOptions, paginationHelper } from 'src/utils/pagination';
|
||||||
|
|
||||||
|
import { TimeBucketAssets } from 'src/services/timeline.service.types';
|
||||||
|
import { isFlipped } from 'src/utils/asset.util';
|
||||||
|
import { hexOrBufferToBase64 } from 'src/utils/bytes';
|
||||||
|
|
||||||
export type AssetStats = Record<AssetType, number>;
|
export type AssetStats = Record<AssetType, number>;
|
||||||
|
|
||||||
export interface AssetStatsOptions {
|
export interface AssetStatsOptions {
|
||||||
@ -710,7 +714,13 @@ export class AssetRepository {
|
|||||||
.innerJoin('albums_assets_assets', 'assets.id', 'albums_assets_assets.assetsId')
|
.innerJoin('albums_assets_assets', 'assets.id', 'albums_assets_assets.assetsId')
|
||||||
.where('albums_assets_assets.albumsId', '=', asUuid(options.albumId!)),
|
.where('albums_assets_assets.albumsId', '=', asUuid(options.albumId!)),
|
||||||
)
|
)
|
||||||
.$if(!!options.personId, (qb) => hasPeople(qb, [options.personId!]))
|
// .$if(!!options.personId, (qb) => hasPeople(qb, [options.personId!]))
|
||||||
|
.$if(!!options.personId, (qb) =>
|
||||||
|
qb.innerJoin(
|
||||||
|
() => hasPeople([options.personId!]),
|
||||||
|
(join) => join.onRef('has_people.assetId', '=', 'assets.id'),
|
||||||
|
),
|
||||||
|
)
|
||||||
.$if(!!options.withStacked, (qb) =>
|
.$if(!!options.withStacked, (qb) =>
|
||||||
qb
|
qb
|
||||||
.leftJoin('asset_stack', (join) =>
|
.leftJoin('asset_stack', (join) =>
|
||||||
@ -727,7 +737,8 @@ export class AssetRepository {
|
|||||||
.$if(options.isDuplicate !== undefined, (qb) =>
|
.$if(options.isDuplicate !== undefined, (qb) =>
|
||||||
qb.where('assets.duplicateId', options.isDuplicate ? 'is not' : 'is', null),
|
qb.where('assets.duplicateId', options.isDuplicate ? 'is not' : 'is', null),
|
||||||
)
|
)
|
||||||
.$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!)),
|
// .$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!)),
|
||||||
|
.$if(!!options.tagId, (qb) => qb.where((eb) => withTagId(options.tagId!, eb.ref('assets.id')))),
|
||||||
)
|
)
|
||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
.select('timeBucket')
|
.select('timeBucket')
|
||||||
@ -744,17 +755,37 @@ export class AssetRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.TIME_BUCKET, { size: TimeBucketSize.MONTH, withStacked: true }] })
|
@GenerateSql({ params: [DummyValue.TIME_BUCKET, { size: TimeBucketSize.MONTH, withStacked: true }] })
|
||||||
async getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]> {
|
async getTimeBucket(timeBucket: string, options: TimeBucketOptions, pagination: PaginationOptions) {
|
||||||
return this.db
|
const paginate = pagination.skip! >= 1 && pagination.take >= 1;
|
||||||
|
const query = this.db
|
||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
.selectAll('assets')
|
.select([
|
||||||
.$call(withExif)
|
'assets.id as id',
|
||||||
|
'assets.ownerId',
|
||||||
|
'assets.status',
|
||||||
|
'type',
|
||||||
|
'duration',
|
||||||
|
'isFavorite',
|
||||||
|
'isArchived',
|
||||||
|
'thumbhash',
|
||||||
|
'localDateTime',
|
||||||
|
'livePhotoVideoId',
|
||||||
|
])
|
||||||
|
.leftJoin('exif', 'assets.id', 'exif.assetId')
|
||||||
|
.select(['exif.exifImageHeight as height', 'exifImageWidth as width', 'exif.orientation', 'exif.projectionType'])
|
||||||
|
.select(sql<boolean>`('assets.deletedAt' IS NOT NULL)`.as('isTrashed'))
|
||||||
.$if(!!options.albumId, (qb) =>
|
.$if(!!options.albumId, (qb) =>
|
||||||
qb
|
qb
|
||||||
.innerJoin('albums_assets_assets', 'albums_assets_assets.assetsId', 'assets.id')
|
.innerJoin('albums_assets_assets', 'albums_assets_assets.assetsId', 'assets.id')
|
||||||
.where('albums_assets_assets.albumsId', '=', options.albumId!),
|
.where('albums_assets_assets.albumsId', '=', options.albumId!),
|
||||||
)
|
)
|
||||||
.$if(!!options.personId, (qb) => hasPeople(qb, [options.personId!]))
|
// .$if(!!options.personId, (qb) => hasPeople(qb, [options.personId!]))
|
||||||
|
.$if(!!options.personId, (qb) =>
|
||||||
|
qb.innerJoin(
|
||||||
|
() => hasPeople([options.personId!]),
|
||||||
|
(join) => join.onRef('has_people.assetId', '=', 'assets.id'),
|
||||||
|
),
|
||||||
|
)
|
||||||
.$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!)))
|
.$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!)))
|
||||||
.$if(options.isArchived !== undefined, (qb) => qb.where('assets.isArchived', '=', options.isArchived!))
|
.$if(options.isArchived !== undefined, (qb) => qb.where('assets.isArchived', '=', options.isArchived!))
|
||||||
.$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!))
|
.$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!))
|
||||||
@ -784,12 +815,79 @@ export class AssetRepository {
|
|||||||
qb.where('assets.duplicateId', options.isDuplicate ? 'is not' : 'is', null),
|
qb.where('assets.duplicateId', options.isDuplicate ? 'is not' : 'is', null),
|
||||||
)
|
)
|
||||||
.$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED))
|
.$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED))
|
||||||
.$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!))
|
// .$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!))
|
||||||
|
.$if(!!options.tagId, (qb) => qb.where((eb) => withTagId(options.tagId!, eb.ref('assets.id'))))
|
||||||
.where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null)
|
.where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null)
|
||||||
.where('assets.isVisible', '=', true)
|
.where('assets.isVisible', '=', true)
|
||||||
.where(truncatedDate(options.size), '=', timeBucket.replace(/^[+-]/, ''))
|
.where(truncatedDate(options.size), '=', timeBucket.replace(/^[+-]/, ''))
|
||||||
.orderBy('assets.localDateTime', options.order ?? 'desc')
|
.orderBy('assets.localDateTime', options.order ?? 'desc')
|
||||||
.execute() as any as Promise<AssetEntity[]>;
|
.$if(paginate, (qb) => qb.offset(pagination.skip!))
|
||||||
|
.$if(paginate, (qb) => qb.limit(pagination.take + 1));
|
||||||
|
|
||||||
|
const items = await query.execute();
|
||||||
|
|
||||||
|
const hasNextPage = paginate && items.length > pagination.take;
|
||||||
|
if (paginate) {
|
||||||
|
items.splice(pagination.take);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bucketAssets: TimeBucketAssets = {
|
||||||
|
id: [],
|
||||||
|
ownerId: [],
|
||||||
|
ratio: [],
|
||||||
|
isFavorite: [],
|
||||||
|
isArchived: [],
|
||||||
|
isTrashed: [],
|
||||||
|
isVideo: [],
|
||||||
|
isImage: [],
|
||||||
|
thumbhash: [],
|
||||||
|
localDateTime: [],
|
||||||
|
stack: [],
|
||||||
|
duration: [],
|
||||||
|
projectionType: [],
|
||||||
|
livePhotoVideoId: [],
|
||||||
|
};
|
||||||
|
for (const item of items) {
|
||||||
|
let width = item.width!;
|
||||||
|
let height = item.height!;
|
||||||
|
if (isFlipped(item.orientation)) {
|
||||||
|
const w = item.width!;
|
||||||
|
const h = item.height!;
|
||||||
|
height = w;
|
||||||
|
width = h;
|
||||||
|
}
|
||||||
|
bucketAssets.id.push(item.id);
|
||||||
|
bucketAssets.ownerId.push(item.ownerId);
|
||||||
|
bucketAssets.ratio.push(round(width / height, 2));
|
||||||
|
bucketAssets.isArchived.push(item.isArchived ? 1 : 0);
|
||||||
|
bucketAssets.isFavorite.push(item.isFavorite ? 1 : 0);
|
||||||
|
bucketAssets.isTrashed.push(item.isTrashed ? 1 : 0);
|
||||||
|
bucketAssets.thumbhash.push(item.thumbhash ? hexOrBufferToBase64(item.thumbhash) : 0);
|
||||||
|
bucketAssets.localDateTime.push(item.localDateTime);
|
||||||
|
bucketAssets.stack.push(this.mapStack(item.stack) || 0);
|
||||||
|
bucketAssets.duration.push(item.duration || 0);
|
||||||
|
bucketAssets.projectionType.push(item.projectionType || 0);
|
||||||
|
bucketAssets.livePhotoVideoId.push(item.livePhotoVideoId || 0);
|
||||||
|
bucketAssets.isImage.push(item.type === AssetType.IMAGE ? 1 : 0);
|
||||||
|
bucketAssets.isVideo.push(item.type === AssetType.VIDEO ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
bucketAssets,
|
||||||
|
hasNextPage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
mapStack(entity?: { id: string | null; primaryAssetId: string | null; assetCount: string | number | bigint | null }) {
|
||||||
|
if (!entity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: entity.id!,
|
||||||
|
primaryAssetId: entity.primaryAssetId!,
|
||||||
|
assetCount: entity.assetCount as number,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
|
@ -4,7 +4,7 @@ import { DateTime } from 'luxon';
|
|||||||
import { Writable } from 'node:stream';
|
import { Writable } from 'node:stream';
|
||||||
import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
|
import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
|
||||||
import { SessionSyncCheckpoints } from 'src/db';
|
import { SessionSyncCheckpoints } from 'src/db';
|
||||||
import { AssetResponseDto, hexOrBufferToBase64, mapAsset } from 'src/dtos/asset-response.dto';
|
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import {
|
import {
|
||||||
AssetDeltaSyncDto,
|
AssetDeltaSyncDto,
|
||||||
@ -18,6 +18,7 @@ import { DatabaseAction, EntityType, Permission, SyncEntityType, SyncRequestType
|
|||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { SyncAck } from 'src/types';
|
import { SyncAck } from 'src/types';
|
||||||
import { getMyPartnerIds } from 'src/utils/asset.util';
|
import { getMyPartnerIds } from 'src/utils/asset.util';
|
||||||
|
import { hexOrBufferToBase64 } from 'src/utils/bytes';
|
||||||
import { setIsEqual } from 'src/utils/set';
|
import { setIsEqual } from 'src/utils/set';
|
||||||
import { fromAck, serialize } from 'src/utils/sync';
|
import { fromAck, serialize } from 'src/utils/sync';
|
||||||
|
|
||||||
|
@ -1,30 +1,36 @@
|
|||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import { AssetResponseDto, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto';
|
import {
|
||||||
|
TimeBucketAssetDto,
|
||||||
|
TimeBucketDto,
|
||||||
|
TimeBucketResponseDto,
|
||||||
|
TimeBucketsResponseDto,
|
||||||
|
} from 'src/dtos/time-bucket.dto';
|
||||||
import { Permission } from 'src/enum';
|
import { Permission } from 'src/enum';
|
||||||
import { TimeBucketOptions } from 'src/repositories/asset.repository';
|
import { TimeBucketOptions, TimeBucketSize } from 'src/repositories/asset.repository';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { getMyPartnerIds } from 'src/utils/asset.util';
|
import { getMyPartnerIds } from 'src/utils/asset.util';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TimelineService extends BaseService {
|
export class TimelineService extends BaseService {
|
||||||
async getTimeBuckets(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> {
|
async getTimeBuckets(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketsResponseDto[]> {
|
||||||
await this.timeBucketChecks(auth, dto);
|
await this.timeBucketChecks(auth, dto);
|
||||||
const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto);
|
const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto);
|
||||||
return this.assetRepository.getTimeBuckets(timeBucketOptions);
|
return this.assetRepository.getTimeBuckets(timeBucketOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTimeBucket(
|
async getTimeBucket(auth: AuthDto, dto: TimeBucketAssetDto): Promise<TimeBucketResponseDto> {
|
||||||
auth: AuthDto,
|
|
||||||
dto: TimeBucketAssetDto,
|
|
||||||
): Promise<AssetResponseDto[] | SanitizedAssetResponseDto[]> {
|
|
||||||
await this.timeBucketChecks(auth, dto);
|
await this.timeBucketChecks(auth, dto);
|
||||||
const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto);
|
const timeBucketOptions = await this.buildTimeBucketOptions(auth, { ...dto, size: TimeBucketSize.MONTH });
|
||||||
const assets = await this.assetRepository.getTimeBucket(dto.timeBucket, timeBucketOptions);
|
|
||||||
return !auth.sharedLink || auth.sharedLink?.showExif
|
const page = dto.page || 1;
|
||||||
? assets.map((asset) => mapAsset(asset, { withStack: true, auth }))
|
const size = dto.pageSize || -1;
|
||||||
: assets.map((asset) => mapAsset(asset, { stripMetadata: true, auth }));
|
if (dto.pageSize === 0) {
|
||||||
|
throw new BadRequestException('pageSize must not be 0');
|
||||||
|
}
|
||||||
|
const a = await this.assetRepository.getTimeBucket(dto.timeBucket, timeBucketOptions, { skip: page, take: size });
|
||||||
|
console.log(a);
|
||||||
|
return a;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async buildTimeBucketOptions(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketOptions> {
|
private async buildTimeBucketOptions(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketOptions> {
|
||||||
|
22
server/src/services/timeline.service.types.ts
Normal file
22
server/src/services/timeline.service.types.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
export type TimelineStack = {
|
||||||
|
id: string;
|
||||||
|
primaryAssetId: string;
|
||||||
|
assetCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TimeBucketAssets = {
|
||||||
|
id: string[];
|
||||||
|
ownerId: string[];
|
||||||
|
ratio: number[];
|
||||||
|
isFavorite: number[];
|
||||||
|
isArchived: number[];
|
||||||
|
isTrashed: number[];
|
||||||
|
isVideo: number[];
|
||||||
|
isImage: number[];
|
||||||
|
thumbhash: (string | number)[];
|
||||||
|
localDateTime: Date[];
|
||||||
|
stack: (TimelineStack | number)[];
|
||||||
|
duration: (string | number)[];
|
||||||
|
projectionType: (string | number)[];
|
||||||
|
livePhotoVideoId: (string | number)[];
|
||||||
|
};
|
@ -200,3 +200,16 @@ export const asRequest = (request: AuthRequest, file: Express.Multer.File) => {
|
|||||||
file: mapToUploadFile(file as ImmichFile),
|
file: mapToUploadFile(file as ImmichFile),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function isRotated90CW(orientation: number) {
|
||||||
|
return orientation === 5 || orientation === 6 || orientation === 90;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRotated270CW(orientation: number) {
|
||||||
|
return orientation === 7 || orientation === 8 || orientation === -90;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isFlipped(orientation?: string | null) {
|
||||||
|
const value = Number(orientation);
|
||||||
|
return value && (isRotated270CW(value) || isRotated90CW(value));
|
||||||
|
}
|
||||||
|
@ -22,3 +22,12 @@ export function asHumanReadable(bytes: number, precision = 1): string {
|
|||||||
|
|
||||||
return `${remainder.toFixed(magnitude == 0 ? 0 : precision)} ${units[magnitude]}`;
|
return `${remainder.toFixed(magnitude == 0 ? 0 : precision)} ${units[magnitude]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if an asset is jsonified in the DB before being returned, its buffer fields will be hex-encoded strings
|
||||||
|
export const hexOrBufferToBase64 = (encoded: string | Buffer) => {
|
||||||
|
if (typeof encoded === 'string') {
|
||||||
|
return Buffer.from(encoded.slice(2), 'hex').toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
return encoded.toString('base64');
|
||||||
|
};
|
||||||
|
@ -13,7 +13,6 @@ import { LoggingRepository } from 'src/repositories/logging.repository';
|
|||||||
import { bootstrapTelemetry } from 'src/repositories/telemetry.repository';
|
import { bootstrapTelemetry } from 'src/repositories/telemetry.repository';
|
||||||
import { ApiService } from 'src/services/api.service';
|
import { ApiService } from 'src/services/api.service';
|
||||||
import { isStartUpError, useSwagger } from 'src/utils/misc';
|
import { isStartUpError, useSwagger } from 'src/utils/misc';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
process.title = 'immich-api';
|
process.title = 'immich-api';
|
||||||
|
|
||||||
|
6
typescript-open-api/typescript-sdk/package-lock.json
generated
Normal file
6
typescript-open-api/typescript-sdk/package-lock.json
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "typescript-sdk",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
@ -1,25 +1,25 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { shortcut } from '$lib/actions/shortcut';
|
||||||
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
|
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
|
||||||
|
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||||
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
|
import { AssetStore, type TimelineAsset } from '$lib/stores/assets-store.svelte';
|
||||||
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
||||||
|
import { handlePromiseError } from '$lib/utils';
|
||||||
|
import { cancelMultiselect, downloadAlbum } from '$lib/utils/asset-utils';
|
||||||
import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
|
import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||||
import type { AlbumResponseDto, SharedLinkResponseDto, UserResponseDto } from '@immich/sdk';
|
import type { AlbumResponseDto, SharedLinkResponseDto, UserResponseDto } from '@immich/sdk';
|
||||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
import { mdiFileImagePlusOutline, mdiFolderDownloadOutline } from '@mdi/js';
|
||||||
import { cancelMultiselect, downloadAlbum } from '$lib/utils/asset-utils';
|
import { onDestroy } from 'svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||||
import DownloadAction from '../photos-page/actions/download-action.svelte';
|
import DownloadAction from '../photos-page/actions/download-action.svelte';
|
||||||
import AssetGrid from '../photos-page/asset-grid.svelte';
|
import AssetGrid from '../photos-page/asset-grid.svelte';
|
||||||
import AssetSelectControlBar from '../photos-page/asset-select-control-bar.svelte';
|
|
||||||
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
||||||
import ImmichLogoSmallLink from '../shared-components/immich-logo-small-link.svelte';
|
import ImmichLogoSmallLink from '../shared-components/immich-logo-small-link.svelte';
|
||||||
import ThemeButton from '../shared-components/theme-button.svelte';
|
import ThemeButton from '../shared-components/theme-button.svelte';
|
||||||
import { shortcut } from '$lib/actions/shortcut';
|
|
||||||
import { mdiFileImagePlusOutline, mdiFolderDownloadOutline } from '@mdi/js';
|
|
||||||
import { handlePromiseError } from '$lib/utils';
|
|
||||||
import AlbumSummary from './album-summary.svelte';
|
import AlbumSummary from './album-summary.svelte';
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
import { onDestroy } from 'svelte';
|
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
sharedLink: SharedLinkResponseDto;
|
sharedLink: SharedLinkResponseDto;
|
||||||
@ -36,7 +36,7 @@
|
|||||||
$effect(() => void assetStore.updateOptions({ albumId: album.id, order: album.order }));
|
$effect(() => void assetStore.updateOptions({ albumId: album.id, order: album.order }));
|
||||||
onDestroy(() => assetStore.destroy());
|
onDestroy(() => assetStore.destroy());
|
||||||
|
|
||||||
const assetInteraction = new AssetInteraction();
|
const assetInteraction = new AssetInteraction<TimelineAsset>();
|
||||||
|
|
||||||
dragAndDropFilesStore.subscribe((value) => {
|
dragAndDropFilesStore.subscribe((value) => {
|
||||||
if (value.isDragging && value.files.length > 0) {
|
if (value.isDragging && value.files.length > 0) {
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
import type { AssetAction } from '$lib/constants';
|
import type { AssetAction } from '$lib/constants';
|
||||||
import type { AlbumResponseDto, AssetResponseDto } from '@immich/sdk';
|
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
|
||||||
|
import type { AlbumResponseDto } from '@immich/sdk';
|
||||||
|
|
||||||
type ActionMap = {
|
type ActionMap = {
|
||||||
[AssetAction.ARCHIVE]: { asset: AssetResponseDto };
|
[AssetAction.ARCHIVE]: { asset: TimelineAsset };
|
||||||
[AssetAction.UNARCHIVE]: { asset: AssetResponseDto };
|
[AssetAction.UNARCHIVE]: { asset: TimelineAsset };
|
||||||
[AssetAction.FAVORITE]: { asset: AssetResponseDto };
|
[AssetAction.FAVORITE]: { asset: TimelineAsset };
|
||||||
[AssetAction.UNFAVORITE]: { asset: AssetResponseDto };
|
[AssetAction.UNFAVORITE]: { asset: TimelineAsset };
|
||||||
[AssetAction.TRASH]: { asset: AssetResponseDto };
|
[AssetAction.TRASH]: { asset: TimelineAsset };
|
||||||
[AssetAction.DELETE]: { asset: AssetResponseDto };
|
[AssetAction.DELETE]: { asset: TimelineAsset };
|
||||||
[AssetAction.RESTORE]: { asset: AssetResponseDto };
|
[AssetAction.RESTORE]: { asset: TimelineAsset };
|
||||||
[AssetAction.ADD]: { asset: AssetResponseDto };
|
[AssetAction.ADD]: { asset: TimelineAsset };
|
||||||
[AssetAction.ADD_TO_ALBUM]: { asset: AssetResponseDto; album: AlbumResponseDto };
|
[AssetAction.ADD_TO_ALBUM]: { asset: TimelineAsset; album: AlbumResponseDto };
|
||||||
[AssetAction.UNSTACK]: { assets: AssetResponseDto[] };
|
[AssetAction.UNSTACK]: { assets: TimelineAsset[] };
|
||||||
[AssetAction.KEEP_THIS_DELETE_OTHERS]: { asset: AssetResponseDto };
|
[AssetAction.KEEP_THIS_DELETE_OTHERS]: { asset: TimelineAsset };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Action = {
|
export type Action = {
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
import Portal from '$lib/components/shared-components/portal/portal.svelte';
|
import Portal from '$lib/components/shared-components/portal/portal.svelte';
|
||||||
import { AssetAction } from '$lib/constants';
|
import { AssetAction } from '$lib/constants';
|
||||||
import { addAssetsToAlbum, addAssetsToNewAlbum } from '$lib/utils/asset-utils';
|
import { addAssetsToAlbum, addAssetsToNewAlbum } from '$lib/utils/asset-utils';
|
||||||
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import type { AlbumResponseDto, AssetResponseDto } from '@immich/sdk';
|
import type { AlbumResponseDto, AssetResponseDto } from '@immich/sdk';
|
||||||
import { mdiImageAlbum, mdiShareVariantOutline } from '@mdi/js';
|
import { mdiImageAlbum, mdiShareVariantOutline } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
@ -24,14 +25,14 @@
|
|||||||
showSelectionModal = false;
|
showSelectionModal = false;
|
||||||
const album = await addAssetsToNewAlbum(albumName, [asset.id]);
|
const album = await addAssetsToNewAlbum(albumName, [asset.id]);
|
||||||
if (album) {
|
if (album) {
|
||||||
onAction({ type: AssetAction.ADD_TO_ALBUM, asset, album });
|
onAction({ type: AssetAction.ADD_TO_ALBUM, asset: toTimelineAsset(asset), album });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddToAlbum = async (album: AlbumResponseDto) => {
|
const handleAddToAlbum = async (album: AlbumResponseDto) => {
|
||||||
showSelectionModal = false;
|
showSelectionModal = false;
|
||||||
await addAssetsToAlbum(album.id, [asset.id]);
|
await addAssetsToAlbum(album.id, [asset.id]);
|
||||||
onAction({ type: AssetAction.ADD_TO_ALBUM, asset, album });
|
onAction({ type: AssetAction.ADD_TO_ALBUM, asset: toTimelineAsset(asset), album });
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||||
import { AssetAction } from '$lib/constants';
|
import { AssetAction } from '$lib/constants';
|
||||||
import { toggleArchive } from '$lib/utils/asset-utils';
|
import { toggleArchive } from '$lib/utils/asset-utils';
|
||||||
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import type { AssetResponseDto } from '@immich/sdk';
|
import type { AssetResponseDto } from '@immich/sdk';
|
||||||
import { mdiArchiveArrowDownOutline, mdiArchiveArrowUpOutline } from '@mdi/js';
|
import { mdiArchiveArrowDownOutline, mdiArchiveArrowUpOutline } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
@ -18,11 +19,11 @@
|
|||||||
|
|
||||||
const onArchive = async () => {
|
const onArchive = async () => {
|
||||||
if (!asset.isArchived) {
|
if (!asset.isArchived) {
|
||||||
preAction({ type: AssetAction.ARCHIVE, asset });
|
preAction({ type: AssetAction.ARCHIVE, asset: toTimelineAsset(asset) });
|
||||||
}
|
}
|
||||||
const updatedAsset = await toggleArchive(asset);
|
const updatedAsset = await toggleArchive(asset);
|
||||||
if (updatedAsset) {
|
if (updatedAsset) {
|
||||||
onAction({ type: asset.isArchived ? AssetAction.ARCHIVE : AssetAction.UNARCHIVE, asset });
|
onAction({ type: asset.isArchived ? AssetAction.ARCHIVE : AssetAction.UNARCHIVE, asset: toTimelineAsset(asset) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
import { showDeleteModal } from '$lib/stores/preferences.store';
|
import { showDeleteModal } from '$lib/stores/preferences.store';
|
||||||
import { featureFlags } from '$lib/stores/server-config.store';
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import { deleteAssets, type AssetResponseDto } from '@immich/sdk';
|
import { deleteAssets, type AssetResponseDto } from '@immich/sdk';
|
||||||
import { mdiDeleteForeverOutline, mdiDeleteOutline } from '@mdi/js';
|
import { mdiDeleteForeverOutline, mdiDeleteOutline } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
@ -42,9 +43,9 @@
|
|||||||
|
|
||||||
const trashAsset = async () => {
|
const trashAsset = async () => {
|
||||||
try {
|
try {
|
||||||
preAction({ type: AssetAction.TRASH, asset });
|
preAction({ type: AssetAction.TRASH, asset: toTimelineAsset(asset) });
|
||||||
await deleteAssets({ assetBulkDeleteDto: { ids: [asset.id] } });
|
await deleteAssets({ assetBulkDeleteDto: { ids: [asset.id] } });
|
||||||
onAction({ type: AssetAction.TRASH, asset });
|
onAction({ type: AssetAction.TRASH, asset: toTimelineAsset(asset) });
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: $t('moved_to_trash'),
|
message: $t('moved_to_trash'),
|
||||||
@ -58,7 +59,7 @@
|
|||||||
const deleteAsset = async () => {
|
const deleteAsset = async () => {
|
||||||
try {
|
try {
|
||||||
await deleteAssets({ assetBulkDeleteDto: { ids: [asset.id], force: true } });
|
await deleteAssets({ assetBulkDeleteDto: { ids: [asset.id], force: true } });
|
||||||
onAction({ type: AssetAction.DELETE, asset });
|
onAction({ type: AssetAction.DELETE, asset: toTimelineAsset(asset) });
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: $t('permanently_deleted_asset'),
|
message: $t('permanently_deleted_asset'),
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
} from '$lib/components/shared-components/notification/notification';
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
import { AssetAction } from '$lib/constants';
|
import { AssetAction } from '$lib/constants';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import { updateAsset, type AssetResponseDto } from '@immich/sdk';
|
import { updateAsset, type AssetResponseDto } from '@immich/sdk';
|
||||||
import { mdiHeart, mdiHeartOutline } from '@mdi/js';
|
import { mdiHeart, mdiHeartOutline } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
@ -30,7 +31,10 @@
|
|||||||
|
|
||||||
asset = { ...asset, isFavorite: data.isFavorite };
|
asset = { ...asset, isFavorite: data.isFavorite };
|
||||||
|
|
||||||
onAction({ type: asset.isFavorite ? AssetAction.FAVORITE : AssetAction.UNFAVORITE, asset });
|
onAction({
|
||||||
|
type: asset.isFavorite ? AssetAction.FAVORITE : AssetAction.UNFAVORITE,
|
||||||
|
asset: toTimelineAsset(asset),
|
||||||
|
});
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||||
|
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||||
import { AssetAction } from '$lib/constants';
|
import { AssetAction } from '$lib/constants';
|
||||||
import { keepThisDeleteOthers } from '$lib/utils/asset-utils';
|
import { keepThisDeleteOthers } from '$lib/utils/asset-utils';
|
||||||
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import type { AssetResponseDto, StackResponseDto } from '@immich/sdk';
|
import type { AssetResponseDto, StackResponseDto } from '@immich/sdk';
|
||||||
import { mdiPinOutline } from '@mdi/js';
|
import { mdiPinOutline } from '@mdi/js';
|
||||||
import type { OnAction } from './action';
|
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
import type { OnAction } from './action';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
stack: StackResponseDto;
|
stack: StackResponseDto;
|
||||||
@ -29,7 +30,7 @@
|
|||||||
|
|
||||||
const keptAsset = await keepThisDeleteOthers(asset, stack);
|
const keptAsset = await keepThisDeleteOthers(asset, stack);
|
||||||
if (keptAsset) {
|
if (keptAsset) {
|
||||||
onAction({ type: AssetAction.UNSTACK, assets: [keptAsset] });
|
onAction({ type: AssetAction.UNSTACK, assets: [toTimelineAsset(keptAsset)] });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
} from '$lib/components/shared-components/notification/notification';
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
import { AssetAction } from '$lib/constants';
|
import { AssetAction } from '$lib/constants';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import { restoreAssets, type AssetResponseDto } from '@immich/sdk';
|
import { restoreAssets, type AssetResponseDto } from '@immich/sdk';
|
||||||
import { mdiHistory } from '@mdi/js';
|
import { mdiHistory } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
@ -23,7 +24,7 @@
|
|||||||
await restoreAssets({ bulkIdsDto: { ids: [asset.id] } });
|
await restoreAssets({ bulkIdsDto: { ids: [asset.id] } });
|
||||||
asset.isTrashed = false;
|
asset.isTrashed = false;
|
||||||
|
|
||||||
onAction({ type: AssetAction.RESTORE, asset });
|
onAction({ type: AssetAction.RESTORE, asset: toTimelineAsset(asset) });
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||||
import { AssetAction } from '$lib/constants';
|
import { AssetAction } from '$lib/constants';
|
||||||
import { deleteStack } from '$lib/utils/asset-utils';
|
import { deleteStack } from '$lib/utils/asset-utils';
|
||||||
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import type { StackResponseDto } from '@immich/sdk';
|
import type { StackResponseDto } from '@immich/sdk';
|
||||||
import { mdiImageMinusOutline } from '@mdi/js';
|
import { mdiImageMinusOutline } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
@ -17,7 +18,7 @@
|
|||||||
const handleUnstack = async () => {
|
const handleUnstack = async () => {
|
||||||
const unstackedAssets = await deleteStack([stack.id]);
|
const unstackedAssets = await deleteStack([stack.id]);
|
||||||
if (unstackedAssets) {
|
if (unstackedAssets) {
|
||||||
onAction({ type: AssetAction.UNSTACK, assets: unstackedAssets });
|
onAction({ type: AssetAction.UNSTACK, assets: unstackedAssets.map((a) => toTimelineAsset(a)) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -13,8 +13,9 @@ describe('AssetViewerNavBar component', () => {
|
|||||||
showDownloadButton: false,
|
showDownloadButton: false,
|
||||||
showMotionPlayButton: false,
|
showMotionPlayButton: false,
|
||||||
showShareButton: false,
|
showShareButton: false,
|
||||||
|
preAction: () => {},
|
||||||
onZoomImage: () => {},
|
onZoomImage: () => {},
|
||||||
onCopyImage: () => {},
|
onCopyImage: async () => {},
|
||||||
onAction: () => {},
|
onAction: () => {},
|
||||||
onRunJob: () => {},
|
onRunJob: () => {},
|
||||||
onPlaySlideshow: () => {},
|
onPlaySlideshow: () => {},
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
import { getAssetJobMessage, getSharedLink, handlePromiseError, isSharedLink } from '$lib/utils';
|
import { getAssetJobMessage, getSharedLink, handlePromiseError, isSharedLink } from '$lib/utils';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { SlideshowHistory } from '$lib/utils/slideshow-history';
|
import { SlideshowHistory } from '$lib/utils/slideshow-history';
|
||||||
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import {
|
import {
|
||||||
AssetJobName,
|
AssetJobName,
|
||||||
AssetTypeEnum,
|
AssetTypeEnum,
|
||||||
@ -52,7 +53,7 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
asset: AssetResponseDto;
|
asset: AssetResponseDto;
|
||||||
preloadAssets?: AssetResponseDto[];
|
preloadAssets?: { id: string }[];
|
||||||
showNavigation?: boolean;
|
showNavigation?: boolean;
|
||||||
withStacked?: boolean;
|
withStacked?: boolean;
|
||||||
isShared?: boolean;
|
isShared?: boolean;
|
||||||
@ -62,7 +63,7 @@
|
|||||||
onAction?: OnAction | undefined;
|
onAction?: OnAction | undefined;
|
||||||
reactions?: ActivityResponseDto[];
|
reactions?: ActivityResponseDto[];
|
||||||
showCloseButton?: boolean;
|
showCloseButton?: boolean;
|
||||||
onClose: (dto: { asset: AssetResponseDto }) => void;
|
onClose: (asset: AssetResponseDto) => void;
|
||||||
onNext: () => Promise<HasAsset>;
|
onNext: () => Promise<HasAsset>;
|
||||||
onPrevious: () => Promise<HasAsset>;
|
onPrevious: () => Promise<HasAsset>;
|
||||||
onRandom: () => Promise<AssetResponseDto | undefined>;
|
onRandom: () => Promise<AssetResponseDto | undefined>;
|
||||||
@ -267,7 +268,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const closeViewer = () => {
|
const closeViewer = () => {
|
||||||
onClose({ asset });
|
onClose(asset);
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeEditor = () => {
|
const closeEditor = () => {
|
||||||
@ -605,8 +606,8 @@
|
|||||||
imageClass={{ 'border-2 border-white': stackedAsset.id === asset.id }}
|
imageClass={{ 'border-2 border-white': stackedAsset.id === asset.id }}
|
||||||
brokenAssetClass="text-xs"
|
brokenAssetClass="text-xs"
|
||||||
dimmed={stackedAsset.id !== asset.id}
|
dimmed={stackedAsset.id !== asset.id}
|
||||||
asset={stackedAsset}
|
asset={toTimelineAsset(stackedAsset)}
|
||||||
onClick={(stackedAsset) => {
|
onClick={() => {
|
||||||
asset = stackedAsset;
|
asset = stackedAsset;
|
||||||
}}
|
}}
|
||||||
onMouseEvent={({ isMouseOver }) => handleStackedAssetMouseEvent(isMouseOver, stackedAsset)}
|
onMouseEvent={({ isMouseOver }) => handleStackedAssetMouseEvent(isMouseOver, stackedAsset)}
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
import { canCopyImageToClipboard, copyImageToClipboard, isWebCompatibleImage } from '$lib/utils/asset-utils';
|
import { canCopyImageToClipboard, copyImageToClipboard, isWebCompatibleImage } from '$lib/utils/asset-utils';
|
||||||
import { getBoundingBox } from '$lib/utils/people-utils';
|
import { getBoundingBox } from '$lib/utils/people-utils';
|
||||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||||
import { AssetMediaSize, AssetTypeEnum, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
|
import { AssetMediaSize, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { type SwipeCustomEvent, swipe } from 'svelte-gestures';
|
import { type SwipeCustomEvent, swipe } from 'svelte-gestures';
|
||||||
@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
asset: AssetResponseDto;
|
asset: AssetResponseDto;
|
||||||
preloadAssets?: AssetResponseDto[] | undefined;
|
preloadAssets?: { id: string }[] | undefined;
|
||||||
element?: HTMLDivElement | undefined;
|
element?: HTMLDivElement | undefined;
|
||||||
haveFadeTransition?: boolean;
|
haveFadeTransition?: boolean;
|
||||||
sharedLink?: SharedLinkResponseDto | undefined;
|
sharedLink?: SharedLinkResponseDto | undefined;
|
||||||
@ -68,12 +68,10 @@
|
|||||||
$boundingBoxesArray = [];
|
$boundingBoxesArray = [];
|
||||||
});
|
});
|
||||||
|
|
||||||
const preload = (targetSize: AssetMediaSize | 'original', preloadAssets?: AssetResponseDto[]) => {
|
const preload = (targetSize: AssetMediaSize | 'original', preloadAssets?: { id: string }[]) => {
|
||||||
for (const preloadAsset of preloadAssets || []) {
|
for (const preloadAsset of preloadAssets || []) {
|
||||||
if (preloadAsset.type === AssetTypeEnum.Image) {
|
let img = new Image();
|
||||||
let img = new Image();
|
img.src = getAssetUrl(preloadAsset.id, targetSize, null);
|
||||||
img.src = getAssetUrl(preloadAsset.id, targetSize, preloadAsset.thumbhash);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,8 +4,8 @@
|
|||||||
import { locale, playVideoThumbnailOnHover } from '$lib/stores/preferences.store';
|
import { locale, playVideoThumbnailOnHover } from '$lib/stores/preferences.store';
|
||||||
import { getAssetPlaybackUrl, getAssetThumbnailUrl, isSharedLink } from '$lib/utils';
|
import { getAssetPlaybackUrl, getAssetThumbnailUrl, isSharedLink } from '$lib/utils';
|
||||||
import { timeToSeconds } from '$lib/utils/date-time';
|
import { timeToSeconds } from '$lib/utils/date-time';
|
||||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
// import { getAltText } from '$lib/utils/thumbnail-util';
|
||||||
import { AssetMediaSize, AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
|
import { AssetMediaSize } from '@immich/sdk';
|
||||||
import {
|
import {
|
||||||
mdiArchiveArrowDownOutline,
|
mdiArchiveArrowDownOutline,
|
||||||
mdiCameraBurst,
|
mdiCameraBurst,
|
||||||
@ -17,22 +17,23 @@
|
|||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
|
|
||||||
import { thumbhash } from '$lib/actions/thumbhash';
|
import { thumbhash } from '$lib/actions/thumbhash';
|
||||||
|
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
|
||||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||||
|
import { getFocusable } from '$lib/utils/focus-util';
|
||||||
import { currentUrlReplaceAssetId } from '$lib/utils/navigation';
|
import { currentUrlReplaceAssetId } from '$lib/utils/navigation';
|
||||||
import { TUNABLES } from '$lib/utils/tunables';
|
import { TUNABLES } from '$lib/utils/tunables';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
import type { ClassValue } from 'svelte/elements';
|
import type { ClassValue } from 'svelte/elements';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import ImageThumbnail from './image-thumbnail.svelte';
|
import ImageThumbnail from './image-thumbnail.svelte';
|
||||||
import VideoThumbnail from './video-thumbnail.svelte';
|
import VideoThumbnail from './video-thumbnail.svelte';
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { getFocusable } from '$lib/utils/focus-util';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
asset: AssetResponseDto;
|
asset: TimelineAsset;
|
||||||
groupIndex?: number;
|
groupIndex?: number;
|
||||||
thumbnailSize?: number | undefined;
|
thumbnailSize?: number;
|
||||||
thumbnailWidth?: number | undefined;
|
thumbnailWidth?: number;
|
||||||
thumbnailHeight?: number | undefined;
|
thumbnailHeight?: number;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
focussed?: boolean;
|
focussed?: boolean;
|
||||||
selectionCandidate?: boolean;
|
selectionCandidate?: boolean;
|
||||||
@ -44,10 +45,10 @@
|
|||||||
imageClass?: ClassValue;
|
imageClass?: ClassValue;
|
||||||
brokenAssetClass?: ClassValue;
|
brokenAssetClass?: ClassValue;
|
||||||
dimmed?: boolean;
|
dimmed?: boolean;
|
||||||
onClick?: ((asset: AssetResponseDto) => void) | undefined;
|
onClick?: (asset: TimelineAsset) => void;
|
||||||
onSelect?: ((asset: AssetResponseDto) => void) | undefined;
|
onSelect?: (asset: TimelineAsset) => void;
|
||||||
onMouseEvent?: ((event: { isMouseOver: boolean; selectedGroupIndex: number }) => void) | undefined;
|
onMouseEvent?: (event: { isMouseOver: boolean; selectedGroupIndex: number }) => void;
|
||||||
handleFocus?: (() => void) | undefined;
|
handleFocus?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@ -331,7 +332,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if asset.type === AssetTypeEnum.Image && asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR}
|
{#if asset.isImage && asset.projectionType === ProjectionType.EQUIRECTANGULAR}
|
||||||
<div class="absolute right-0 top-0 z-10 flex place-items-center gap-1 text-xs font-medium text-white">
|
<div class="absolute right-0 top-0 z-10 flex place-items-center gap-1 text-xs font-medium text-white">
|
||||||
<span class="pr-2 pt-2">
|
<span class="pr-2 pt-2">
|
||||||
<Icon path={mdiRotate360} size="24" />
|
<Icon path={mdiRotate360} size="24" />
|
||||||
@ -344,7 +345,7 @@
|
|||||||
<div
|
<div
|
||||||
class={[
|
class={[
|
||||||
'absolute z-10 flex place-items-center gap-1 text-xs font-medium text-white',
|
'absolute z-10 flex place-items-center gap-1 text-xs font-medium text-white',
|
||||||
asset.type == AssetTypeEnum.Image && !asset.livePhotoVideoId ? 'top-0 right-0' : 'top-7 right-1',
|
asset.isImage && !asset.livePhotoVideoId ? 'top-0 right-0' : 'top-7 right-1',
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<span class="pr-2 pt-2 flex place-items-center gap-1">
|
<span class="pr-2 pt-2 flex place-items-center gap-1">
|
||||||
@ -354,27 +355,28 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
<!-- altText={$getAltText(asset)} -->
|
||||||
<ImageThumbnail
|
<ImageThumbnail
|
||||||
class={imageClass}
|
class={imageClass}
|
||||||
{brokenAssetClass}
|
{brokenAssetClass}
|
||||||
url={getAssetThumbnailUrl({ id: asset.id, size: AssetMediaSize.Thumbnail, cacheKey: asset.thumbhash })}
|
url={getAssetThumbnailUrl({ id: asset.id, size: AssetMediaSize.Thumbnail, cacheKey: asset.thumbhash })}
|
||||||
altText={$getAltText(asset)}
|
altText="todo"
|
||||||
widthStyle="{width}px"
|
widthStyle="{width}px"
|
||||||
heightStyle="{height}px"
|
heightStyle="{height}px"
|
||||||
curve={selected}
|
curve={selected}
|
||||||
onComplete={(errored) => ((loaded = true), (thumbError = errored))}
|
onComplete={(errored) => ((loaded = true), (thumbError = errored))}
|
||||||
/>
|
/>
|
||||||
{#if asset.type === AssetTypeEnum.Video}
|
{#if asset.isVideo}
|
||||||
<div class="absolute top-0 h-full w-full">
|
<div class="absolute top-0 h-full w-full">
|
||||||
<VideoThumbnail
|
<VideoThumbnail
|
||||||
url={getAssetPlaybackUrl({ id: asset.id, cacheKey: asset.thumbhash })}
|
url={getAssetPlaybackUrl({ id: asset.id, cacheKey: asset.thumbhash })}
|
||||||
enablePlayback={mouseOver && $playVideoThumbnailOnHover}
|
enablePlayback={mouseOver && $playVideoThumbnailOnHover}
|
||||||
curve={selected}
|
curve={selected}
|
||||||
durationInSeconds={timeToSeconds(asset.duration)}
|
durationInSeconds={timeToSeconds(asset.duration!)}
|
||||||
playbackOnIconHover={!$playVideoThumbnailOnHover}
|
playbackOnIconHover={!$playVideoThumbnailOnHover}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else if asset.type === AssetTypeEnum.Image && asset.livePhotoVideoId}
|
{:else if asset.isImage && asset.livePhotoVideoId}
|
||||||
<div class="absolute top-0 h-full w-full">
|
<div class="absolute top-0 h-full w-full">
|
||||||
<VideoThumbnail
|
<VideoThumbnail
|
||||||
url={getAssetPlaybackUrl({ id: asset.livePhotoVideoId, cacheKey: asset.thumbhash })}
|
url={getAssetPlaybackUrl({ id: asset.livePhotoVideoId, cacheKey: asset.thumbhash })}
|
||||||
|
@ -73,7 +73,7 @@
|
|||||||
const viewport: Viewport = $state({ width: 0, height: 0 });
|
const viewport: Viewport = $state({ width: 0, height: 0 });
|
||||||
// need to include padding in the viewport for gallery
|
// need to include padding in the viewport for gallery
|
||||||
const galleryViewport: Viewport = $derived({ height: viewport.height, width: viewport.width - 32 });
|
const galleryViewport: Viewport = $derived({ height: viewport.height, width: viewport.width - 32 });
|
||||||
const assetInteraction = new AssetInteraction();
|
const assetInteraction = new AssetInteraction<AssetResponseDto>();
|
||||||
let progressBarController: Tween<number> | undefined = $state(undefined);
|
let progressBarController: Tween<number> | undefined = $state(undefined);
|
||||||
let videoPlayer: HTMLVideoElement | undefined = $state();
|
let videoPlayer: HTMLVideoElement | undefined = $state();
|
||||||
const asHref = (asset: AssetResponseDto) => `?${QueryParameter.ID}=${asset.id}`;
|
const asHref = (asset: AssetResponseDto) => `?${QueryParameter.ID}=${asset.id}`;
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
import type { OnArchive } from '$lib/utils/actions';
|
import type { OnArchive } from '$lib/utils/actions';
|
||||||
|
import { archiveAssets } from '$lib/utils/asset-utils';
|
||||||
import { mdiArchiveArrowDownOutline, mdiArchiveArrowUpOutline, mdiTimerSand } from '@mdi/js';
|
import { mdiArchiveArrowDownOutline, mdiArchiveArrowUpOutline, mdiTimerSand } from '@mdi/js';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
||||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||||
import { archiveAssets } from '$lib/utils/asset-utils';
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onArchive?: OnArchive;
|
onArchive?: OnArchive;
|
||||||
|
@ -6,9 +6,10 @@
|
|||||||
} from '$lib/components/shared-components/notification/notification';
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
import { getAssetJobIcon, getAssetJobMessage, getAssetJobName } from '$lib/utils';
|
import { getAssetJobIcon, getAssetJobMessage, getAssetJobName } from '$lib/utils';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { AssetJobName, AssetTypeEnum, runAssetJobs } from '@immich/sdk';
|
import { isTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
import { AssetJobName, AssetTypeEnum, runAssetJobs, type AssetResponseDto } from '@immich/sdk';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
jobs?: AssetJobName[];
|
jobs?: AssetJobName[];
|
||||||
@ -19,7 +20,11 @@
|
|||||||
|
|
||||||
const { clearSelect, getOwnedAssets } = getAssetControlContext();
|
const { clearSelect, getOwnedAssets } = getAssetControlContext();
|
||||||
|
|
||||||
let isAllVideos = $derived([...getOwnedAssets()].every((asset) => asset.type === AssetTypeEnum.Video));
|
let isAllVideos = $derived(
|
||||||
|
[...getOwnedAssets()].every((asset) =>
|
||||||
|
isTimelineAsset(asset) ? asset.isVideo : (asset as AssetResponseDto).type === AssetTypeEnum.Video,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const handleRunJob = async (name: AssetJobName) => {
|
const handleRunJob = async (name: AssetJobName) => {
|
||||||
try {
|
try {
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
|
||||||
import { downloadArchive, downloadFile } from '$lib/utils/asset-utils';
|
|
||||||
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
|
||||||
import { shortcut } from '$lib/actions/shortcut';
|
import { shortcut } from '$lib/actions/shortcut';
|
||||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
|
import { getKey } from '$lib/utils';
|
||||||
|
import { downloadArchive, downloadFile } from '$lib/utils/asset-utils';
|
||||||
|
import { isTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
|
import { getAssetInfo, type AssetResponseDto } from '@immich/sdk';
|
||||||
import { mdiCloudDownloadOutline, mdiFileDownloadOutline, mdiFolderDownloadOutline } from '@mdi/js';
|
import { mdiCloudDownloadOutline, mdiFileDownloadOutline, mdiFolderDownloadOutline } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
||||||
|
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
filename?: string;
|
filename?: string;
|
||||||
@ -20,7 +23,11 @@
|
|||||||
const assets = [...getAssets()];
|
const assets = [...getAssets()];
|
||||||
if (assets.length === 1) {
|
if (assets.length === 1) {
|
||||||
clearSelect();
|
clearSelect();
|
||||||
await downloadFile(assets[0]);
|
let asset: AssetResponseDto = assets[0] as AssetResponseDto;
|
||||||
|
if (isTimelineAsset(assets[0])) {
|
||||||
|
asset = await getAssetInfo({ id: assets[0].id, key: getKey() });
|
||||||
|
}
|
||||||
|
await downloadFile(asset);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
|
import { getAssetControlContext } from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||||
|
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
|
||||||
import type { OnLink, OnUnlink } from '$lib/utils/actions';
|
import type { OnLink, OnUnlink } from '$lib/utils/actions';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { AssetTypeEnum, getAssetInfo, updateAsset } from '@immich/sdk';
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
|
import { getAssetInfo, updateAsset } from '@immich/sdk';
|
||||||
import { mdiLinkOff, mdiMotionPlayOutline, mdiTimerSand } from '@mdi/js';
|
import { mdiLinkOff, mdiMotionPlayOutline, mdiTimerSand } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
||||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onLink: OnLink;
|
onLink: OnLink;
|
||||||
@ -28,14 +30,14 @@
|
|||||||
|
|
||||||
const handleLink = async () => {
|
const handleLink = async () => {
|
||||||
let [still, motion] = [...getOwnedAssets()];
|
let [still, motion] = [...getOwnedAssets()];
|
||||||
if (still.type === AssetTypeEnum.Video) {
|
if ((still as TimelineAsset).isVideo) {
|
||||||
[still, motion] = [motion, still];
|
[still, motion] = [motion, still];
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading = true;
|
loading = true;
|
||||||
const stillResponse = await updateAsset({ id: still.id, updateAssetDto: { livePhotoVideoId: motion.id } });
|
const stillResponse = await updateAsset({ id: still.id, updateAssetDto: { livePhotoVideoId: motion.id } });
|
||||||
onLink({ still: stillResponse, motion });
|
onLink({ still: toTimelineAsset(stillResponse), motion: motion as TimelineAsset });
|
||||||
clearSelect();
|
clearSelect();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, $t('errors.unable_to_link_motion_video'));
|
handleError(error, $t('errors.unable_to_link_motion_video'));
|
||||||
@ -46,22 +48,22 @@
|
|||||||
|
|
||||||
const handleUnlink = async () => {
|
const handleUnlink = async () => {
|
||||||
const [still] = [...getOwnedAssets()];
|
const [still] = [...getOwnedAssets()];
|
||||||
|
if (still) {
|
||||||
const motionId = still?.livePhotoVideoId;
|
const motionId = (still as TimelineAsset).livePhotoVideoId;
|
||||||
if (!motionId) {
|
if (!motionId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
try {
|
loading = true;
|
||||||
loading = true;
|
const stillResponse = await updateAsset({ id: still.id, updateAssetDto: { livePhotoVideoId: null } });
|
||||||
const stillResponse = await updateAsset({ id: still.id, updateAssetDto: { livePhotoVideoId: null } });
|
const motionResponse = await getAssetInfo({ id: motionId });
|
||||||
const motionResponse = await getAssetInfo({ id: motionId });
|
onUnlink({ still: toTimelineAsset(stillResponse), motion: toTimelineAsset(motionResponse) });
|
||||||
onUnlink({ still: stillResponse, motion: motionResponse });
|
clearSelect();
|
||||||
clearSelect();
|
} catch (error) {
|
||||||
} catch (error) {
|
handleError(error, $t('errors.unable_to_unlink_motion_video'));
|
||||||
handleError(error, $t('errors.unable_to_unlink_motion_video'));
|
} finally {
|
||||||
} finally {
|
loading = false;
|
||||||
loading = false;
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
|
import type { AssetInteraction, BaseInteractionAsset } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { type AssetStore, isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
import { type AssetStore, isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
||||||
|
import { cancelMultiselect, selectAllAssets } from '$lib/utils/asset-utils';
|
||||||
import { mdiSelectAll, mdiSelectRemove } from '@mdi/js';
|
import { mdiSelectAll, mdiSelectRemove } from '@mdi/js';
|
||||||
import { selectAllAssets, cancelMultiselect } from '$lib/utils/asset-utils';
|
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assetStore: AssetStore;
|
assetStore: AssetStore;
|
||||||
assetInteraction: AssetInteraction;
|
assetInteraction: AssetInteraction<BaseInteractionAsset>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { assetStore, assetInteraction }: Props = $props();
|
let { assetStore, assetInteraction }: Props = $props();
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
|
||||||
import { getAssetControlContext } from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
import { getAssetControlContext } from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||||
import { mdiImageMinusOutline, mdiImageMultipleOutline } from '@mdi/js';
|
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||||
import { stackAssets, deleteStack } from '$lib/utils/asset-utils';
|
|
||||||
import type { OnStack, OnUnstack } from '$lib/utils/actions';
|
import type { OnStack, OnUnstack } from '$lib/utils/actions';
|
||||||
|
import { deleteStack, stackAssets } from '$lib/utils/asset-utils';
|
||||||
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
|
import { mdiImageMinusOutline, mdiImageMultipleOutline } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -34,7 +35,7 @@
|
|||||||
}
|
}
|
||||||
const unstackedAssets = await deleteStack([stack.id]);
|
const unstackedAssets = await deleteStack([stack.id]);
|
||||||
if (unstackedAssets) {
|
if (unstackedAssets) {
|
||||||
onUnstack?.(unstackedAssets);
|
onUnstack?.(unstackedAssets.map((a) => toTimelineAsset(a)));
|
||||||
}
|
}
|
||||||
clearSelect();
|
clearSelect();
|
||||||
};
|
};
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
import {
|
import {
|
||||||
type AssetStore,
|
|
||||||
type AssetBucket,
|
type AssetBucket,
|
||||||
assetSnapshot,
|
assetSnapshot,
|
||||||
assetsSnapshot,
|
assetsSnapshot,
|
||||||
|
type AssetStore,
|
||||||
isSelectingAllAssets,
|
isSelectingAllAssets,
|
||||||
|
type TimelineAsset,
|
||||||
} from '$lib/stores/assets-store.svelte';
|
} from '$lib/stores/assets-store.svelte';
|
||||||
import { navigate } from '$lib/utils/navigation';
|
import { navigate } from '$lib/utils/navigation';
|
||||||
import { getDateLocaleString } from '$lib/utils/timeline-util';
|
import { getDateLocaleString } from '$lib/utils/timeline-util';
|
||||||
import type { AssetResponseDto } from '@immich/sdk';
|
|
||||||
|
import type { AssetInteraction, BaseInteractionAsset } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { mdiCheckCircle, mdiCircleOutline } from '@mdi/js';
|
import { mdiCheckCircle, mdiCircleOutline } from '@mdi/js';
|
||||||
import { fly } from 'svelte/transition';
|
import { fly, scale } from 'svelte/transition';
|
||||||
import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
|
import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
|
||||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
|
||||||
import { scale } from 'svelte/transition';
|
|
||||||
|
|
||||||
import { flip } from 'svelte/animate';
|
import { flip } from 'svelte/animate';
|
||||||
|
|
||||||
@ -29,11 +29,11 @@
|
|||||||
showArchiveIcon: boolean;
|
showArchiveIcon: boolean;
|
||||||
bucket: AssetBucket;
|
bucket: AssetBucket;
|
||||||
assetStore: AssetStore;
|
assetStore: AssetStore;
|
||||||
assetInteraction: AssetInteraction;
|
assetInteraction: AssetInteraction<BaseInteractionAsset>;
|
||||||
|
|
||||||
onSelect: ({ title, assets }: { title: string; assets: AssetResponseDto[] }) => void;
|
onSelect: ({ title, assets }: { title: string; assets: TimelineAsset[] }) => void;
|
||||||
onSelectAssets: (asset: AssetResponseDto) => void;
|
onSelectAssets: (asset: TimelineAsset) => void;
|
||||||
onSelectAssetCandidates: (asset: AssetResponseDto | null) => void;
|
onSelectAssetCandidates: (asset: TimelineAsset | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@ -54,7 +54,7 @@
|
|||||||
|
|
||||||
const transitionDuration = $derived.by(() => (bucket.store.suspendTransitions && !$isUploading ? 0 : 150));
|
const transitionDuration = $derived.by(() => (bucket.store.suspendTransitions && !$isUploading ? 0 : 150));
|
||||||
const scaleDuration = $derived(transitionDuration === 0 ? 0 : transitionDuration + 100);
|
const scaleDuration = $derived(transitionDuration === 0 ? 0 : transitionDuration + 100);
|
||||||
const onClick = (assetStore: AssetStore, assets: AssetResponseDto[], groupTitle: string, asset: AssetResponseDto) => {
|
const onClick = (assetStore: AssetStore, assets: TimelineAsset[], groupTitle: string, asset: TimelineAsset) => {
|
||||||
if (isSelectionMode || assetInteraction.selectionActive) {
|
if (isSelectionMode || assetInteraction.selectionActive) {
|
||||||
assetSelectHandler(assetStore, asset, assets, groupTitle);
|
assetSelectHandler(assetStore, asset, assets, groupTitle);
|
||||||
return;
|
return;
|
||||||
@ -62,12 +62,12 @@
|
|||||||
void navigate({ targetRoute: 'current', assetId: asset.id });
|
void navigate({ targetRoute: 'current', assetId: asset.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectGroup = (title: string, assets: AssetResponseDto[]) => onSelect({ title, assets });
|
const handleSelectGroup = (title: string, assets: TimelineAsset[]) => onSelect({ title, assets });
|
||||||
|
|
||||||
const assetSelectHandler = (
|
const assetSelectHandler = (
|
||||||
assetStore: AssetStore,
|
assetStore: AssetStore,
|
||||||
asset: AssetResponseDto,
|
asset: TimelineAsset,
|
||||||
assetsInDateGroup: AssetResponseDto[],
|
assetsInDateGroup: TimelineAsset[],
|
||||||
groupTitle: string,
|
groupTitle: string,
|
||||||
) => {
|
) => {
|
||||||
onSelectAssets(asset);
|
onSelectAssets(asset);
|
||||||
@ -91,7 +91,7 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const assetMouseEventHandler = (groupTitle: string, asset: AssetResponseDto | null) => {
|
const assetMouseEventHandler = (groupTitle: string, asset: TimelineAsset | null) => {
|
||||||
// Show multi select icon on hover on date group
|
// Show multi select icon on hover on date group
|
||||||
hoveredDateGroup = groupTitle;
|
hoveredDateGroup = groupTitle;
|
||||||
|
|
||||||
@ -100,7 +100,7 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const assetOnFocusHandler = (asset: AssetResponseDto) => {
|
const assetOnFocusHandler = (asset: TimelineAsset) => {
|
||||||
assetInteraction.focussedAssetId = asset.id;
|
assetInteraction.focussedAssetId = asset.id;
|
||||||
};
|
};
|
||||||
function filterIntersecting<R extends { intersecting: boolean }>(intersectable: R[]) {
|
function filterIntersecting<R extends { intersecting: boolean }>(intersectable: R[]) {
|
||||||
|
@ -1,10 +1,21 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { afterNavigate, beforeNavigate, goto } from '$app/navigation';
|
import { afterNavigate, beforeNavigate, goto } from '$app/navigation';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { resizeObserver, type OnResizeCallback } from '$lib/actions/resize-observer';
|
||||||
import { shortcuts, type ShortcutOptions } from '$lib/actions/shortcut';
|
import { shortcuts, type ShortcutOptions } from '$lib/actions/shortcut';
|
||||||
import type { Action } from '$lib/components/asset-viewer/actions/action';
|
import type { Action } from '$lib/components/asset-viewer/actions/action';
|
||||||
|
import Skeleton from '$lib/components/photos-page/skeleton.svelte';
|
||||||
import { AppRoute, AssetAction } from '$lib/constants';
|
import { AppRoute, AssetAction } from '$lib/constants';
|
||||||
|
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import { AssetBucket, assetsSnapshot, AssetStore, isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
import {
|
||||||
|
AssetBucket,
|
||||||
|
assetsSnapshot,
|
||||||
|
AssetStore,
|
||||||
|
isSelectingAllAssets,
|
||||||
|
type TimelineAsset,
|
||||||
|
} from '$lib/stores/assets-store.svelte';
|
||||||
|
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||||
import { showDeleteModal } from '$lib/stores/preferences.store';
|
import { showDeleteModal } from '$lib/stores/preferences.store';
|
||||||
import { searchStore } from '$lib/stores/search.svelte';
|
import { searchStore } from '$lib/stores/search.svelte';
|
||||||
import { featureFlags } from '$lib/stores/server-config.store';
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
@ -13,19 +24,14 @@
|
|||||||
import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
|
import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
|
||||||
import { navigate } from '$lib/utils/navigation';
|
import { navigate } from '$lib/utils/navigation';
|
||||||
import { type ScrubberListener } from '$lib/utils/timeline-util';
|
import { type ScrubberListener } from '$lib/utils/timeline-util';
|
||||||
import type { AlbumResponseDto, AssetResponseDto, PersonResponseDto } from '@immich/sdk';
|
import { getAssetInfo, type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
|
||||||
import { onMount, type Snippet } from 'svelte';
|
import { onMount, type Snippet } from 'svelte';
|
||||||
|
import type { UpdatePayload } from 'vite';
|
||||||
import Portal from '../shared-components/portal/portal.svelte';
|
import Portal from '../shared-components/portal/portal.svelte';
|
||||||
import Scrubber from '../shared-components/scrubber/scrubber.svelte';
|
import Scrubber from '../shared-components/scrubber/scrubber.svelte';
|
||||||
import ShowShortcuts from '../shared-components/show-shortcuts.svelte';
|
import ShowShortcuts from '../shared-components/show-shortcuts.svelte';
|
||||||
import AssetDateGroup from './asset-date-group.svelte';
|
import AssetDateGroup from './asset-date-group.svelte';
|
||||||
import DeleteAssetDialog from './delete-asset-dialog.svelte';
|
import DeleteAssetDialog from './delete-asset-dialog.svelte';
|
||||||
import { resizeObserver, type OnResizeCallback } from '$lib/actions/resize-observer';
|
|
||||||
import Skeleton from '$lib/components/photos-page/skeleton.svelte';
|
|
||||||
import { page } from '$app/stores';
|
|
||||||
import type { UpdatePayload } from 'vite';
|
|
||||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
|
||||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isSelectionMode?: boolean;
|
isSelectionMode?: boolean;
|
||||||
@ -35,7 +41,7 @@
|
|||||||
additionally, update the page location/url with the asset as the asset-grid is scrolled */
|
additionally, update the page location/url with the asset as the asset-grid is scrolled */
|
||||||
enableRouting: boolean;
|
enableRouting: boolean;
|
||||||
assetStore: AssetStore;
|
assetStore: AssetStore;
|
||||||
assetInteraction: AssetInteraction;
|
assetInteraction: AssetInteraction<TimelineAsset>;
|
||||||
removeAction?: AssetAction.UNARCHIVE | AssetAction.ARCHIVE | AssetAction.FAVORITE | AssetAction.UNFAVORITE | null;
|
removeAction?: AssetAction.UNARCHIVE | AssetAction.ARCHIVE | AssetAction.FAVORITE | AssetAction.UNFAVORITE | null;
|
||||||
withStacked?: boolean;
|
withStacked?: boolean;
|
||||||
showArchiveIcon?: boolean;
|
showArchiveIcon?: boolean;
|
||||||
@ -43,7 +49,7 @@
|
|||||||
album?: AlbumResponseDto | null;
|
album?: AlbumResponseDto | null;
|
||||||
person?: PersonResponseDto | null;
|
person?: PersonResponseDto | null;
|
||||||
isShowDeleteConfirmation?: boolean;
|
isShowDeleteConfirmation?: boolean;
|
||||||
onSelect?: (asset: AssetResponseDto) => void;
|
onSelect?: (asset: TimelineAsset) => void;
|
||||||
onEscape?: () => void;
|
onEscape?: () => void;
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
empty?: Snippet;
|
empty?: Snippet;
|
||||||
@ -352,7 +358,7 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectAsset = (asset: AssetResponseDto) => {
|
const handleSelectAsset = (asset: TimelineAsset) => {
|
||||||
if (!assetStore.albumAssets.has(asset.id)) {
|
if (!assetStore.albumAssets.has(asset.id)) {
|
||||||
assetInteraction.selectAsset(asset);
|
assetInteraction.selectAsset(asset);
|
||||||
}
|
}
|
||||||
@ -363,7 +369,8 @@
|
|||||||
|
|
||||||
if (previousAsset) {
|
if (previousAsset) {
|
||||||
const preloadAsset = await assetStore.getPreviousAsset(previousAsset);
|
const preloadAsset = await assetStore.getPreviousAsset(previousAsset);
|
||||||
assetViewingStore.setAsset(previousAsset, preloadAsset ? [preloadAsset] : []);
|
const asset = await getAssetInfo({ id: previousAsset.id });
|
||||||
|
assetViewingStore.setAsset(asset, preloadAsset ? [preloadAsset] : []);
|
||||||
await navigate({ targetRoute: 'current', assetId: previousAsset.id });
|
await navigate({ targetRoute: 'current', assetId: previousAsset.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -375,7 +382,8 @@
|
|||||||
|
|
||||||
if (nextAsset) {
|
if (nextAsset) {
|
||||||
const preloadAsset = await assetStore.getNextAsset(nextAsset);
|
const preloadAsset = await assetStore.getNextAsset(nextAsset);
|
||||||
assetViewingStore.setAsset(nextAsset, preloadAsset ? [preloadAsset] : []);
|
const asset = await getAssetInfo({ id: nextAsset.id });
|
||||||
|
assetViewingStore.setAsset(asset, preloadAsset ? [preloadAsset] : []);
|
||||||
await navigate({ targetRoute: 'current', assetId: nextAsset.id });
|
await navigate({ targetRoute: 'current', assetId: nextAsset.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -387,14 +395,14 @@
|
|||||||
|
|
||||||
if (randomAsset) {
|
if (randomAsset) {
|
||||||
const preloadAsset = await assetStore.getNextAsset(randomAsset);
|
const preloadAsset = await assetStore.getNextAsset(randomAsset);
|
||||||
assetViewingStore.setAsset(randomAsset, preloadAsset ? [preloadAsset] : []);
|
const asset = await getAssetInfo({ id: randomAsset.id });
|
||||||
|
assetViewingStore.setAsset(asset, preloadAsset ? [preloadAsset] : []);
|
||||||
await navigate({ targetRoute: 'current', assetId: randomAsset.id });
|
await navigate({ targetRoute: 'current', assetId: randomAsset.id });
|
||||||
|
return asset;
|
||||||
}
|
}
|
||||||
|
|
||||||
return randomAsset;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = async ({ asset }: { asset: AssetResponseDto }) => {
|
const handleClose = async (asset: { id: string }) => {
|
||||||
assetViewingStore.showAssetViewer(false);
|
assetViewingStore.showAssetViewer(false);
|
||||||
showSkeleton = true;
|
showSkeleton = true;
|
||||||
$gridScrollTarget = { at: asset.id };
|
$gridScrollTarget = { at: asset.id };
|
||||||
@ -410,7 +418,7 @@
|
|||||||
case AssetAction.ARCHIVE: {
|
case AssetAction.ARCHIVE: {
|
||||||
// find the next asset to show or close the viewer
|
// find the next asset to show or close the viewer
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||||
(await handleNext()) || (await handlePrevious()) || (await handleClose({ asset: action.asset }));
|
(await handleNext()) || (await handlePrevious()) || (await handleClose(action.asset));
|
||||||
|
|
||||||
// delete after find the next one
|
// delete after find the next one
|
||||||
assetStore.removeAssets([action.asset.id]);
|
assetStore.removeAssets([action.asset.id]);
|
||||||
@ -439,7 +447,7 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let lastAssetMouseEvent: AssetResponseDto | null = $state(null);
|
let lastAssetMouseEvent: TimelineAsset | null = $state(null);
|
||||||
|
|
||||||
let shiftKeyIsDown = $state(false);
|
let shiftKeyIsDown = $state(false);
|
||||||
|
|
||||||
@ -469,14 +477,14 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectAssetCandidates = (asset: AssetResponseDto | null) => {
|
const handleSelectAssetCandidates = (asset: TimelineAsset | null) => {
|
||||||
if (asset) {
|
if (asset) {
|
||||||
selectAssetCandidates(asset);
|
selectAssetCandidates(asset);
|
||||||
}
|
}
|
||||||
lastAssetMouseEvent = asset;
|
lastAssetMouseEvent = asset;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGroupSelect = (assetStore: AssetStore, group: string, assets: AssetResponseDto[]) => {
|
const handleGroupSelect = (assetStore: AssetStore, group: string, assets: TimelineAsset[]) => {
|
||||||
if (assetInteraction.selectedGroup.has(group)) {
|
if (assetInteraction.selectedGroup.has(group)) {
|
||||||
assetInteraction.removeGroupFromMultiselectGroup(group);
|
assetInteraction.removeGroupFromMultiselectGroup(group);
|
||||||
for (const asset of assets) {
|
for (const asset of assets) {
|
||||||
@ -496,7 +504,7 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectAssets = async (asset: AssetResponseDto) => {
|
const handleSelectAssets = async (asset: TimelineAsset) => {
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -579,7 +587,7 @@
|
|||||||
assetInteraction.setAssetSelectionStart(deselect ? null : asset);
|
assetInteraction.setAssetSelectionStart(deselect ? null : asset);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectAssetCandidates = (endAsset: AssetResponseDto) => {
|
const selectAssetCandidates = (endAsset: TimelineAsset) => {
|
||||||
if (!shiftKeyIsDown) {
|
if (!shiftKeyIsDown) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
export interface AssetControlContext {
|
export interface AssetControlContext {
|
||||||
// Wrap assets in a function, because context isn't reactive.
|
// Wrap assets in a function, because context isn't reactive.
|
||||||
getAssets: () => AssetResponseDto[]; // All assets includes partners' assets
|
getAssets: () => BaseInteractionAsset[]; // All assets includes partners' assets
|
||||||
getOwnedAssets: () => AssetResponseDto[]; // Only assets owned by the user
|
getOwnedAssets: () => BaseInteractionAsset[]; // Only assets owned by the user
|
||||||
clearSelect: () => void;
|
clearSelect: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -14,13 +14,13 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { AssetResponseDto } from '@immich/sdk';
|
import type { BaseInteractionAsset } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { mdiClose } from '@mdi/js';
|
import { mdiClose } from '@mdi/js';
|
||||||
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assets: AssetResponseDto[];
|
assets: BaseInteractionAsset[];
|
||||||
clearSelect: () => void;
|
clearSelect: () => void;
|
||||||
ownerId?: string | undefined;
|
ownerId?: string | undefined;
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
import { downloadArchive } from '$lib/utils/asset-utils';
|
import { downloadArchive } from '$lib/utils/asset-utils';
|
||||||
import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
|
import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { addSharedLinkAssets, type SharedLinkResponseDto } from '@immich/sdk';
|
import { addSharedLinkAssets, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
|
||||||
import { mdiArrowLeft, mdiFileImagePlusOutline, mdiFolderDownloadOutline, mdiSelectAll } from '@mdi/js';
|
import { mdiArrowLeft, mdiFileImagePlusOutline, mdiFolderDownloadOutline, mdiSelectAll } from '@mdi/js';
|
||||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||||
import DownloadAction from '../photos-page/actions/download-action.svelte';
|
import DownloadAction from '../photos-page/actions/download-action.svelte';
|
||||||
@ -31,7 +31,7 @@
|
|||||||
let { sharedLink = $bindable(), isOwned }: Props = $props();
|
let { sharedLink = $bindable(), isOwned }: Props = $props();
|
||||||
|
|
||||||
const viewport: Viewport = $state({ width: 0, height: 0 });
|
const viewport: Viewport = $state({ width: 0, height: 0 });
|
||||||
const assetInteraction = new AssetInteraction();
|
const assetInteraction = new AssetInteraction<AssetResponseDto>();
|
||||||
|
|
||||||
let assets = $derived(sharedLink.assets);
|
let assets = $derived(sharedLink.assets);
|
||||||
|
|
||||||
|
@ -1,31 +1,32 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { type ShortcutOptions, shortcuts } from '$lib/actions/shortcut';
|
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
import { shortcuts, type ShortcutOptions } from '$lib/actions/shortcut';
|
||||||
import type { Action } from '$lib/components/asset-viewer/actions/action';
|
import type { Action } from '$lib/components/asset-viewer/actions/action';
|
||||||
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
|
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
|
||||||
import { AppRoute, AssetAction } from '$lib/constants';
|
import { AppRoute, AssetAction } from '$lib/constants';
|
||||||
|
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import type { Viewport } from '$lib/stores/assets-store.svelte';
|
import type { Viewport } from '$lib/stores/assets-store.svelte';
|
||||||
import { showDeleteModal } from '$lib/stores/preferences.store';
|
import { showDeleteModal } from '$lib/stores/preferences.store';
|
||||||
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
|
import { handlePromiseError } from '$lib/utils';
|
||||||
import { deleteAssets } from '$lib/utils/actions';
|
import { deleteAssets } from '$lib/utils/actions';
|
||||||
import { archiveAssets, cancelMultiselect } from '$lib/utils/asset-utils';
|
import { archiveAssets, cancelMultiselect } from '$lib/utils/asset-utils';
|
||||||
import { featureFlags } from '$lib/stores/server-config.store';
|
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
import { getJustifiedLayoutFromAssets, type CommonJustifiedLayout } from '$lib/utils/layout-utils';
|
||||||
import { navigate } from '$lib/utils/navigation';
|
import { navigate } from '$lib/utils/navigation';
|
||||||
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import { type AssetResponseDto } from '@immich/sdk';
|
import { type AssetResponseDto } from '@immich/sdk';
|
||||||
|
import { debounce } from 'lodash-es';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import AssetViewer from '../../asset-viewer/asset-viewer.svelte';
|
import AssetViewer from '../../asset-viewer/asset-viewer.svelte';
|
||||||
import ShowShortcuts from '../show-shortcuts.svelte';
|
|
||||||
import Portal from '../portal/portal.svelte';
|
|
||||||
import { handlePromiseError } from '$lib/utils';
|
|
||||||
import DeleteAssetDialog from '../../photos-page/delete-asset-dialog.svelte';
|
import DeleteAssetDialog from '../../photos-page/delete-asset-dialog.svelte';
|
||||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import Portal from '../portal/portal.svelte';
|
||||||
import { debounce } from 'lodash-es';
|
import ShowShortcuts from '../show-shortcuts.svelte';
|
||||||
import { getJustifiedLayoutFromAssets, type CommonJustifiedLayout } from '$lib/utils/layout-utils';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assets: AssetResponseDto[];
|
assets: AssetResponseDto[];
|
||||||
assetInteraction: AssetInteraction;
|
assetInteraction: AssetInteraction<AssetResponseDto>;
|
||||||
disableAssetSelect?: boolean;
|
disableAssetSelect?: boolean;
|
||||||
showArchiveIcon?: boolean;
|
showArchiveIcon?: boolean;
|
||||||
viewport: Viewport;
|
viewport: Viewport;
|
||||||
@ -481,18 +482,18 @@
|
|||||||
>
|
>
|
||||||
<Thumbnail
|
<Thumbnail
|
||||||
readonly={disableAssetSelect}
|
readonly={disableAssetSelect}
|
||||||
onClick={(asset) => {
|
onClick={() => {
|
||||||
if (assetInteraction.selectionActive) {
|
if (assetInteraction.selectionActive) {
|
||||||
handleSelectAssets(asset);
|
handleSelectAssets(asset);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void viewAssetHandler(asset);
|
void viewAssetHandler(asset);
|
||||||
}}
|
}}
|
||||||
onSelect={(asset) => handleSelectAssets(asset)}
|
onSelect={() => handleSelectAssets(asset)}
|
||||||
onMouseEvent={() => assetMouseEventHandler(asset)}
|
onMouseEvent={() => assetMouseEventHandler(asset)}
|
||||||
handleFocus={() => assetOnFocusHandler(asset)}
|
handleFocus={() => assetOnFocusHandler(asset)}
|
||||||
{showArchiveIcon}
|
{showArchiveIcon}
|
||||||
{asset}
|
asset={toTimelineAsset(asset)}
|
||||||
selected={assetInteraction.hasSelectedAsset(asset.id)}
|
selected={assetInteraction.hasSelectedAsset(asset.id)}
|
||||||
selectionCandidate={assetInteraction.hasSelectionCandidate(asset.id)}
|
selectionCandidate={assetInteraction.hasSelectionCandidate(asset.id)}
|
||||||
focussed={assetInteraction.isFocussedAsset(asset.id)}
|
focussed={assetInteraction.isFocussedAsset(asset.id)}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction, type BaseInteractionAsset } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { resetSavedUser, user } from '$lib/stores/user.store';
|
import { resetSavedUser, user } from '$lib/stores/user.store';
|
||||||
import { assetFactory } from '@test-data/factories/asset-factory';
|
import { assetFactory } from '@test-data/factories/asset-factory';
|
||||||
import { userAdminFactory } from '@test-data/factories/user-factory';
|
import { userAdminFactory } from '@test-data/factories/user-factory';
|
||||||
|
|
||||||
describe('AssetInteraction', () => {
|
describe('AssetInteraction', () => {
|
||||||
let assetInteraction: AssetInteraction;
|
let assetInteraction: AssetInteraction<BaseInteractionAsset>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
assetInteraction = new AssetInteraction();
|
assetInteraction = new AssetInteraction();
|
||||||
|
@ -1,19 +1,27 @@
|
|||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
import type { AssetResponseDto, UserAdminResponseDto } from '@immich/sdk';
|
import type { TimelineStackResponseDto, UserAdminResponseDto } from '@immich/sdk';
|
||||||
import { SvelteSet } from 'svelte/reactivity';
|
import { SvelteSet } from 'svelte/reactivity';
|
||||||
import { fromStore } from 'svelte/store';
|
import { fromStore } from 'svelte/store';
|
||||||
|
|
||||||
export class AssetInteraction {
|
export type BaseInteractionAsset = {
|
||||||
selectedAssets = $state<AssetResponseDto[]>([]);
|
id: string;
|
||||||
|
isTrashed: boolean;
|
||||||
|
isArchived: boolean;
|
||||||
|
isFavorite: boolean;
|
||||||
|
ownerId: string;
|
||||||
|
stack?: TimelineStackResponseDto | null | undefined;
|
||||||
|
};
|
||||||
|
export class AssetInteraction<T extends BaseInteractionAsset> {
|
||||||
|
selectedAssets = $state<T[]>([]);
|
||||||
hasSelectedAsset(assetId: string) {
|
hasSelectedAsset(assetId: string) {
|
||||||
return this.selectedAssets.some((asset) => asset.id === assetId);
|
return this.selectedAssets.some((asset) => asset.id === assetId);
|
||||||
}
|
}
|
||||||
selectedGroup = new SvelteSet<string>();
|
selectedGroup = new SvelteSet<string>();
|
||||||
assetSelectionCandidates = $state<AssetResponseDto[]>([]);
|
assetSelectionCandidates = $state<T[]>([]);
|
||||||
hasSelectionCandidate(assetId: string) {
|
hasSelectionCandidate(assetId: string) {
|
||||||
return this.assetSelectionCandidates.some((asset) => asset.id === assetId);
|
return this.assetSelectionCandidates.some((asset) => asset.id === assetId);
|
||||||
}
|
}
|
||||||
assetSelectionStart = $state<AssetResponseDto | null>(null);
|
assetSelectionStart = $state<T | null>(null);
|
||||||
focussedAssetId = $state<string | null>(null);
|
focussedAssetId = $state<string | null>(null);
|
||||||
selectionActive = $derived(this.selectedAssets.length > 0);
|
selectionActive = $derived(this.selectedAssets.length > 0);
|
||||||
|
|
||||||
@ -25,13 +33,13 @@ export class AssetInteraction {
|
|||||||
isAllFavorite = $derived(this.selectedAssets.every((asset) => asset.isFavorite));
|
isAllFavorite = $derived(this.selectedAssets.every((asset) => asset.isFavorite));
|
||||||
isAllUserOwned = $derived(this.selectedAssets.every((asset) => asset.ownerId === this.userId));
|
isAllUserOwned = $derived(this.selectedAssets.every((asset) => asset.ownerId === this.userId));
|
||||||
|
|
||||||
selectAsset(asset: AssetResponseDto) {
|
selectAsset(asset: T) {
|
||||||
if (!this.hasSelectedAsset(asset.id)) {
|
if (!this.hasSelectedAsset(asset.id)) {
|
||||||
this.selectedAssets.push(asset);
|
this.selectedAssets.push(asset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selectAssets(assets: AssetResponseDto[]) {
|
selectAssets(assets: T[]) {
|
||||||
for (const asset of assets) {
|
for (const asset of assets) {
|
||||||
this.selectAsset(asset);
|
this.selectAsset(asset);
|
||||||
}
|
}
|
||||||
@ -52,11 +60,11 @@ export class AssetInteraction {
|
|||||||
this.selectedGroup.delete(group);
|
this.selectedGroup.delete(group);
|
||||||
}
|
}
|
||||||
|
|
||||||
setAssetSelectionStart(asset: AssetResponseDto | null) {
|
setAssetSelectionStart(asset: T | null) {
|
||||||
this.assetSelectionStart = asset;
|
this.assetSelectionStart = asset;
|
||||||
}
|
}
|
||||||
|
|
||||||
setAssetSelectionCandidates(assets: AssetResponseDto[]) {
|
setAssetSelectionCandidates(assets: T[]) {
|
||||||
this.assetSelectionCandidates = assets;
|
this.assetSelectionCandidates = assets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,11 +5,11 @@ import { readonly, writable } from 'svelte/store';
|
|||||||
|
|
||||||
function createAssetViewingStore() {
|
function createAssetViewingStore() {
|
||||||
const viewingAssetStoreState = writable<AssetResponseDto>();
|
const viewingAssetStoreState = writable<AssetResponseDto>();
|
||||||
const preloadAssets = writable<AssetResponseDto[]>([]);
|
const preloadAssets = writable<{ id: string }[]>([]);
|
||||||
const viewState = writable<boolean>(false);
|
const viewState = writable<boolean>(false);
|
||||||
const gridScrollTarget = writable<AssetGridRouteSearchParams | null | undefined>();
|
const gridScrollTarget = writable<AssetGridRouteSearchParams | null | undefined>();
|
||||||
|
|
||||||
const setAsset = (asset: AssetResponseDto, assetsToPreload: AssetResponseDto[] = []) => {
|
const setAsset = (asset: AssetResponseDto, assetsToPreload: { id: string }[] = []) => {
|
||||||
preloadAssets.set(assetsToPreload);
|
preloadAssets.set(assetsToPreload);
|
||||||
viewingAssetStoreState.set(asset);
|
viewingAssetStoreState.set(asset);
|
||||||
viewState.set(true);
|
viewState.set(true);
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
type CommonLayoutOptions,
|
type CommonLayoutOptions,
|
||||||
type CommonPosition,
|
type CommonPosition,
|
||||||
} from '$lib/utils/layout-utils';
|
} from '$lib/utils/layout-utils';
|
||||||
import { formatDateGroupTitle, fromLocalDateTime } from '$lib/utils/timeline-util';
|
import { formatDateGroupTitle, toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import { TUNABLES } from '$lib/utils/tunables';
|
import { TUNABLES } from '$lib/utils/tunables';
|
||||||
import {
|
import {
|
||||||
AssetOrder,
|
AssetOrder,
|
||||||
@ -15,7 +15,8 @@ import {
|
|||||||
getTimeBucket,
|
getTimeBucket,
|
||||||
getTimeBuckets,
|
getTimeBuckets,
|
||||||
TimeBucketSize,
|
TimeBucketSize,
|
||||||
type AssetResponseDto,
|
type TimeBucketResponseDto,
|
||||||
|
type TimelineStackResponseDto,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { clamp, debounce, isEqual, throttle } from 'lodash-es';
|
import { clamp, debounce, isEqual, throttle } from 'lodash-es';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
@ -62,13 +63,30 @@ function updateObject(target: any, source: any): boolean {
|
|||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function assetSnapshot(asset: AssetResponseDto) {
|
export function assetSnapshot(asset: TimelineAsset): TimelineAsset {
|
||||||
return $state.snapshot(asset);
|
return $state.snapshot(asset) as TimelineAsset;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function assetsSnapshot(assets: AssetResponseDto[]) {
|
export function assetsSnapshot(assets: TimelineAsset[]): TimelineAsset[] {
|
||||||
return assets.map((a) => $state.snapshot(a));
|
return assets.map((a) => $state.snapshot(a)) as TimelineAsset[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TimelineAsset = {
|
||||||
|
id: string;
|
||||||
|
ownerId: string;
|
||||||
|
ratio: number;
|
||||||
|
thumbhash: string | null;
|
||||||
|
localDateTime: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
isFavorite: boolean;
|
||||||
|
isTrashed: boolean;
|
||||||
|
isVideo: boolean;
|
||||||
|
isImage: boolean;
|
||||||
|
stack: TimelineStackResponseDto | null;
|
||||||
|
duration: string | null;
|
||||||
|
projectionType: string | null;
|
||||||
|
livePhotoVideoId: string | null;
|
||||||
|
};
|
||||||
class IntersectingAsset {
|
class IntersectingAsset {
|
||||||
// --- public ---
|
// --- public ---
|
||||||
readonly #group: AssetDateGroup;
|
readonly #group: AssetDateGroup;
|
||||||
@ -92,17 +110,17 @@ class IntersectingAsset {
|
|||||||
});
|
});
|
||||||
|
|
||||||
position: CommonPosition | undefined = $state();
|
position: CommonPosition | undefined = $state();
|
||||||
asset: AssetResponseDto | undefined = $state();
|
asset: TimelineAsset | undefined = $state();
|
||||||
id: string | undefined = $derived(this.asset?.id);
|
id: string | undefined = $derived(this.asset?.id);
|
||||||
|
|
||||||
constructor(group: AssetDateGroup, asset: AssetResponseDto) {
|
constructor(group: AssetDateGroup, asset: TimelineAsset) {
|
||||||
this.#group = group;
|
this.#group = group;
|
||||||
this.asset = asset;
|
this.asset = asset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
type AssetOperation = (asset: AssetResponseDto) => { remove: boolean };
|
type AssetOperation = (asset: TimelineAsset) => { remove: boolean };
|
||||||
|
|
||||||
type MoveAsset = { asset: AssetResponseDto; year: number; month: number };
|
type MoveAsset = { asset: TimelineAsset; year: number; month: number };
|
||||||
export class AssetDateGroup {
|
export class AssetDateGroup {
|
||||||
// --- public
|
// --- public
|
||||||
readonly bucket: AssetBucket;
|
readonly bucket: AssetBucket;
|
||||||
@ -131,8 +149,8 @@ export class AssetDateGroup {
|
|||||||
|
|
||||||
sortAssets(sortOrder: AssetOrder = AssetOrder.Desc) {
|
sortAssets(sortOrder: AssetOrder = AssetOrder.Desc) {
|
||||||
this.intersetingAssets.sort((a, b) => {
|
this.intersetingAssets.sort((a, b) => {
|
||||||
const aDate = DateTime.fromISO(a.asset!.fileCreatedAt).toUTC();
|
const aDate = DateTime.fromISO(a.asset!.localDateTime).toUTC();
|
||||||
const bDate = DateTime.fromISO(b.asset!.fileCreatedAt).toUTC();
|
const bDate = DateTime.fromISO(b.asset!.localDateTime).toUTC();
|
||||||
|
|
||||||
if (sortOrder === AssetOrder.Asc) {
|
if (sortOrder === AssetOrder.Asc) {
|
||||||
return aDate.diff(bDate).milliseconds;
|
return aDate.diff(bDate).milliseconds;
|
||||||
@ -223,6 +241,25 @@ export type ViewportXY = Viewport & {
|
|||||||
y: number;
|
y: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class AddContext {
|
||||||
|
lookupCache: {
|
||||||
|
[dayOfMonth: number]: AssetDateGroup;
|
||||||
|
} = {};
|
||||||
|
unprocessedAssets: TimelineAsset[] = [];
|
||||||
|
changedDateGroups = new Set<AssetDateGroup>();
|
||||||
|
newDateGroups = new Set<AssetDateGroup>();
|
||||||
|
sort(bucket: AssetBucket, sortOrder: AssetOrder = AssetOrder.Desc) {
|
||||||
|
for (const group of this.changedDateGroups) {
|
||||||
|
group.sortAssets(sortOrder);
|
||||||
|
}
|
||||||
|
for (const group of this.newDateGroups) {
|
||||||
|
group.sortAssets(sortOrder);
|
||||||
|
}
|
||||||
|
if (this.newDateGroups.size > 0) {
|
||||||
|
bucket.sortDateGroups();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
export class AssetBucket {
|
export class AssetBucket {
|
||||||
// --- public ---
|
// --- public ---
|
||||||
#intersecting: boolean = $state(false);
|
#intersecting: boolean = $state(false);
|
||||||
@ -314,7 +351,7 @@ export class AssetBucket {
|
|||||||
getAssets() {
|
getAssets() {
|
||||||
// eslint-disable-next-line unicorn/no-array-reduce
|
// eslint-disable-next-line unicorn/no-array-reduce
|
||||||
return this.dateGroups.reduce(
|
return this.dateGroups.reduce(
|
||||||
(accumulator: AssetResponseDto[], g: AssetDateGroup) => accumulator.concat(g.getAssets()),
|
(accumulator: TimelineAsset[], g: AssetDateGroup) => accumulator.concat(g.getAssets()),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -379,55 +416,82 @@ export class AssetBucket {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// note - if the assets are not part of this bucket, they will not be added
|
// note - if the assets are not part of this bucket, they will not be added
|
||||||
addAssets(assets: AssetResponseDto[]) {
|
addAssets(bucketResponse: TimeBucketResponseDto) {
|
||||||
const lookupCache: {
|
const addContext = new AddContext();
|
||||||
[dayOfMonth: number]: AssetDateGroup;
|
const assets = bucketResponse.bucketAssets;
|
||||||
} = {};
|
const size = assets.id.length;
|
||||||
const unprocessedAssets: AssetResponseDto[] = [];
|
for (let i = 0; i < size; i++) {
|
||||||
const changedDateGroups = new Set<AssetDateGroup>();
|
const id = assets.id[i];
|
||||||
const newDateGroups = new Set<AssetDateGroup>();
|
const ownerId = assets.ownerId[i];
|
||||||
for (const asset of assets) {
|
const localDateTime = assets.localDateTime[i];
|
||||||
const date = DateTime.fromISO(asset.localDateTime).toUTC();
|
|
||||||
const month = date.get('month');
|
const ratio = assets.ratio[i];
|
||||||
const year = date.get('year');
|
const isFavorite = !!assets.isFavorite[i];
|
||||||
if (this.month === month && this.year === year) {
|
const isTrashed = !!assets.isTrashed[i];
|
||||||
const day = date.get('day');
|
const isArchived = !!assets.isArchived[i];
|
||||||
let dateGroup: AssetDateGroup | undefined = lookupCache[day];
|
const isVideo = !!assets.isVideo[i];
|
||||||
if (!dateGroup) {
|
const isImage = !!assets.isImage[i];
|
||||||
dateGroup = this.findDateGroupByDay(day);
|
const thumbhash = assets.thumbhash[i];
|
||||||
if (dateGroup) {
|
const stack = assets.stack[i];
|
||||||
lookupCache[day] = dateGroup;
|
const duration = assets.duration[i];
|
||||||
}
|
const projectionType = assets.projectionType[i];
|
||||||
}
|
const livePhotoVideoId = assets.livePhotoVideoId[i];
|
||||||
|
const timelineAsset: TimelineAsset = {
|
||||||
|
id,
|
||||||
|
ownerId,
|
||||||
|
ratio,
|
||||||
|
thumbhash: typeof thumbhash === 'number' ? null : thumbhash,
|
||||||
|
localDateTime,
|
||||||
|
isFavorite,
|
||||||
|
isArchived,
|
||||||
|
isTrashed,
|
||||||
|
isVideo,
|
||||||
|
isImage,
|
||||||
|
stack,
|
||||||
|
duration: typeof duration === 'number' ? null : duration,
|
||||||
|
projectionType: typeof projectionType === 'number' ? null : projectionType,
|
||||||
|
livePhotoVideoId: typeof livePhotoVideoId === 'number' ? null : livePhotoVideoId,
|
||||||
|
};
|
||||||
|
this.addTimelineAsset(timelineAsset, addContext);
|
||||||
|
}
|
||||||
|
addContext.sort(this, this.#sortOrder);
|
||||||
|
return addContext.unprocessedAssets;
|
||||||
|
}
|
||||||
|
|
||||||
|
addTimelineAsset(timelineAsset: TimelineAsset, addContext: AddContext) {
|
||||||
|
const { id, localDateTime } = timelineAsset;
|
||||||
|
const date = DateTime.fromISO(localDateTime).toUTC();
|
||||||
|
|
||||||
|
const month = date.get('month');
|
||||||
|
const year = date.get('year');
|
||||||
|
|
||||||
|
if (this.month === month && this.year === year) {
|
||||||
|
const day = date.get('day');
|
||||||
|
let dateGroup: AssetDateGroup | undefined = addContext.lookupCache[day];
|
||||||
|
if (!dateGroup) {
|
||||||
|
dateGroup = this.findDateGroupByDay(day);
|
||||||
if (dateGroup) {
|
if (dateGroup) {
|
||||||
const intersectingAsset = new IntersectingAsset(dateGroup, asset);
|
addContext.lookupCache[day] = dateGroup;
|
||||||
if (dateGroup.intersetingAssets.some((a) => a.id === asset.id)) {
|
}
|
||||||
console.error(`Ignoring attempt to add duplicate asset ${asset.id} to ${dateGroup.groupTitle}`);
|
}
|
||||||
} else {
|
if (dateGroup) {
|
||||||
dateGroup.intersetingAssets.push(intersectingAsset);
|
const intersectingAsset = new IntersectingAsset(dateGroup, timelineAsset);
|
||||||
changedDateGroups.add(dateGroup);
|
if (dateGroup.intersetingAssets.some((a) => a.id === id)) {
|
||||||
}
|
console.error(`Ignoring attempt to add duplicate asset ${id} to ${dateGroup.groupTitle}`);
|
||||||
} else {
|
} else {
|
||||||
dateGroup = new AssetDateGroup(this, this.dateGroups.length, date, day);
|
dateGroup.intersetingAssets.push(intersectingAsset);
|
||||||
dateGroup.intersetingAssets.push(new IntersectingAsset(dateGroup, asset));
|
addContext.changedDateGroups.add(dateGroup);
|
||||||
this.dateGroups.push(dateGroup);
|
|
||||||
lookupCache[day] = dateGroup;
|
|
||||||
newDateGroups.add(dateGroup);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
unprocessedAssets.push(asset);
|
dateGroup = new AssetDateGroup(this, this.dateGroups.length, date, day);
|
||||||
|
dateGroup.intersetingAssets.push(new IntersectingAsset(dateGroup, timelineAsset));
|
||||||
|
this.dateGroups.push(dateGroup);
|
||||||
|
addContext.lookupCache[day] = dateGroup;
|
||||||
|
addContext.newDateGroups.add(dateGroup);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
addContext.unprocessedAssets.push(timelineAsset);
|
||||||
}
|
}
|
||||||
for (const group of changedDateGroups) {
|
|
||||||
group.sortAssets(this.#sortOrder);
|
|
||||||
}
|
|
||||||
for (const group of newDateGroups) {
|
|
||||||
group.sortAssets(this.#sortOrder);
|
|
||||||
}
|
|
||||||
if (newDateGroups.size > 0) {
|
|
||||||
this.sortDateGroups();
|
|
||||||
}
|
|
||||||
return unprocessedAssets;
|
|
||||||
}
|
}
|
||||||
getRandomDateGroup() {
|
getRandomDateGroup() {
|
||||||
const random = Math.floor(Math.random() * this.dateGroups.length);
|
const random = Math.floor(Math.random() * this.dateGroups.length);
|
||||||
@ -514,12 +578,12 @@ const isMismatched = (option: boolean | undefined, value: boolean): boolean =>
|
|||||||
|
|
||||||
interface AddAsset {
|
interface AddAsset {
|
||||||
type: 'add';
|
type: 'add';
|
||||||
values: AssetResponseDto[];
|
values: TimelineAsset[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateAsset {
|
interface UpdateAsset {
|
||||||
type: 'update';
|
type: 'update';
|
||||||
values: AssetResponseDto[];
|
values: TimelineAsset[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DeleteAsset {
|
interface DeleteAsset {
|
||||||
@ -701,9 +765,13 @@ export class AssetStore {
|
|||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
this.#unsubscribers.push(
|
this.#unsubscribers.push(
|
||||||
websocketEvents.on('on_upload_success', (asset) => this.#addPendingChanges({ type: 'add', values: [asset] })),
|
websocketEvents.on('on_upload_success', (asset) =>
|
||||||
|
this.#addPendingChanges({ type: 'add', values: [toTimelineAsset(asset)] }),
|
||||||
|
),
|
||||||
websocketEvents.on('on_asset_trash', (ids) => this.#addPendingChanges({ type: 'trash', values: ids })),
|
websocketEvents.on('on_asset_trash', (ids) => this.#addPendingChanges({ type: 'trash', values: ids })),
|
||||||
websocketEvents.on('on_asset_update', (asset) => this.#addPendingChanges({ type: 'update', values: [asset] })),
|
websocketEvents.on('on_asset_update', (asset) =>
|
||||||
|
this.#addPendingChanges({ type: 'update', values: [toTimelineAsset(asset)] }),
|
||||||
|
),
|
||||||
websocketEvents.on('on_asset_delete', (id: string) => this.#addPendingChanges({ type: 'delete', values: [id] })),
|
websocketEvents.on('on_asset_delete', (id: string) => this.#addPendingChanges({ type: 'delete', values: [id] })),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -717,8 +785,8 @@ export class AssetStore {
|
|||||||
|
|
||||||
#getPendingChangeBatches() {
|
#getPendingChangeBatches() {
|
||||||
const batch: {
|
const batch: {
|
||||||
add: AssetResponseDto[];
|
add: TimelineAsset[];
|
||||||
update: AssetResponseDto[];
|
update: TimelineAsset[];
|
||||||
remove: string[];
|
remove: string[];
|
||||||
} = {
|
} = {
|
||||||
add: [],
|
add: [],
|
||||||
@ -1042,7 +1110,7 @@ export class AssetStore {
|
|||||||
// so no need to load the bucket, it already has assets
|
// so no need to load the bucket, it already has assets
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const assets = await getTimeBucket(
|
const bucketResponse = await getTimeBucket(
|
||||||
{
|
{
|
||||||
...this.#options,
|
...this.#options,
|
||||||
timeBucket: bucketDate,
|
timeBucket: bucketDate,
|
||||||
@ -1051,9 +1119,9 @@ export class AssetStore {
|
|||||||
},
|
},
|
||||||
{ signal },
|
{ signal },
|
||||||
);
|
);
|
||||||
if (assets) {
|
if (bucketResponse) {
|
||||||
if (this.#options.timelineAlbumId) {
|
if (this.#options.timelineAlbumId) {
|
||||||
const albumAssets = await getTimeBucket(
|
const { bucketAssets: albumAssets } = await getTimeBucket(
|
||||||
{
|
{
|
||||||
albumId: this.#options.timelineAlbumId,
|
albumId: this.#options.timelineAlbumId,
|
||||||
timeBucket: bucketDate,
|
timeBucket: bucketDate,
|
||||||
@ -1062,12 +1130,11 @@ export class AssetStore {
|
|||||||
},
|
},
|
||||||
{ signal },
|
{ signal },
|
||||||
);
|
);
|
||||||
for (const asset of albumAssets) {
|
for (const id of albumAssets.id) {
|
||||||
this.albumAssets.add(asset.id);
|
this.albumAssets.add(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const unprocessed = bucket.addAssets(bucketResponse);
|
||||||
const unprocessed = bucket.addAssets(assets);
|
|
||||||
if (unprocessed.length > 0) {
|
if (unprocessed.length > 0) {
|
||||||
console.error(
|
console.error(
|
||||||
`Warning: getTimeBucket API returning assets not in requested month: ${bucket.bucketDate}, ${JSON.stringify(unprocessed.map((a) => ({ id: a.id, localDateTime: a.localDateTime })))}`,
|
`Warning: getTimeBucket API returning assets not in requested month: ${bucket.bucketDate}, ${JSON.stringify(unprocessed.map((a) => ({ id: a.id, localDateTime: a.localDateTime })))}`,
|
||||||
@ -1081,8 +1148,8 @@ export class AssetStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addAssets(assets: AssetResponseDto[]) {
|
addAssets(assets: TimelineAsset[]) {
|
||||||
const assetsToUpdate: AssetResponseDto[] = [];
|
const assetsToUpdate: TimelineAsset[] = [];
|
||||||
|
|
||||||
for (const asset of assets) {
|
for (const asset of assets) {
|
||||||
if (this.isExcluded(asset)) {
|
if (this.isExcluded(asset)) {
|
||||||
@ -1095,7 +1162,7 @@ export class AssetStore {
|
|||||||
this.#addAssetsToBuckets([...notUpdated]);
|
this.#addAssetsToBuckets([...notUpdated]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#addAssetsToBuckets(assets: AssetResponseDto[]) {
|
#addAssetsToBuckets(assets: TimelineAsset[]) {
|
||||||
if (assets.length === 0) {
|
if (assets.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1112,7 +1179,9 @@ export class AssetStore {
|
|||||||
bucket = new AssetBucket(this, utc, 1, this.#options.order);
|
bucket = new AssetBucket(this, utc, 1, this.#options.order);
|
||||||
this.buckets.push(bucket);
|
this.buckets.push(bucket);
|
||||||
}
|
}
|
||||||
bucket.addAssets([asset]);
|
const addContext = new AddContext();
|
||||||
|
bucket.addTimelineAsset(asset, addContext);
|
||||||
|
addContext.sort(bucket, this.#options.order);
|
||||||
updatedBuckets.add(bucket);
|
updatedBuckets.add(bucket);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1138,7 +1207,7 @@ export class AssetStore {
|
|||||||
await this.initTask.waitUntilCompletion();
|
await this.initTask.waitUntilCompletion();
|
||||||
let bucket = this.#findBucketForAsset(id);
|
let bucket = this.#findBucketForAsset(id);
|
||||||
if (!bucket) {
|
if (!bucket) {
|
||||||
const asset = await getAssetInfo({ id });
|
const asset = toTimelineAsset(await getAssetInfo({ id }));
|
||||||
if (!asset || this.isExcluded(asset)) {
|
if (!asset || this.isExcluded(asset)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1151,7 +1220,7 @@ export class AssetStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async #loadBucketAtTime(localDateTime: string, options?: { cancelable: boolean }) {
|
async #loadBucketAtTime(localDateTime: string, options?: { cancelable: boolean }) {
|
||||||
let date = fromLocalDateTime(localDateTime);
|
let date = DateTime.fromISO(localDateTime).toUTC();
|
||||||
// Only support TimeBucketSize.Month
|
// Only support TimeBucketSize.Month
|
||||||
date = date.set({ day: 1, hour: 0, minute: 0, second: 0, millisecond: 0 });
|
date = date.set({ day: 1, hour: 0, minute: 0, second: 0, millisecond: 0 });
|
||||||
const iso = date.toISO()!;
|
const iso = date.toISO()!;
|
||||||
@ -1161,7 +1230,7 @@ export class AssetStore {
|
|||||||
return this.getBucketByDate(year, month);
|
return this.getBucketByDate(year, month);
|
||||||
}
|
}
|
||||||
|
|
||||||
async #getBucketInfoForAsset(asset: AssetResponseDto, options?: { cancelable: boolean }) {
|
async #getBucketInfoForAsset(asset: { id: string; localDateTime: string }, options?: { cancelable: boolean }) {
|
||||||
const bucketInfo = this.#findBucketForAsset(asset.id);
|
const bucketInfo = this.#findBucketForAsset(asset.id);
|
||||||
if (bucketInfo) {
|
if (bucketInfo) {
|
||||||
return bucketInfo;
|
return bucketInfo;
|
||||||
@ -1195,7 +1264,7 @@ export class AssetStore {
|
|||||||
const changedBuckets = new Set<AssetBucket>();
|
const changedBuckets = new Set<AssetBucket>();
|
||||||
let idsToProcess = new Set(ids);
|
let idsToProcess = new Set(ids);
|
||||||
const idsProcessed = new Set<string>();
|
const idsProcessed = new Set<string>();
|
||||||
const combinedMoveAssets: { asset: AssetResponseDto; year: number; month: number }[][] = [];
|
const combinedMoveAssets: { asset: TimelineAsset; year: number; month: number }[][] = [];
|
||||||
for (const bucket of this.buckets) {
|
for (const bucket of this.buckets) {
|
||||||
if (idsToProcess.size > 0) {
|
if (idsToProcess.size > 0) {
|
||||||
const { moveAssets, processedIds, changedGeometry } = bucket.runAssetOperation(idsToProcess, operation);
|
const { moveAssets, processedIds, changedGeometry } = bucket.runAssetOperation(idsToProcess, operation);
|
||||||
@ -1238,8 +1307,8 @@ export class AssetStore {
|
|||||||
this.#runAssetOperation(new Set(ids), operation);
|
this.#runAssetOperation(new Set(ids), operation);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAssets(assets: AssetResponseDto[]) {
|
updateAssets(assets: TimelineAsset[]) {
|
||||||
const lookup = new Map<string, AssetResponseDto>(assets.map((asset) => [asset.id, asset]));
|
const lookup = new Map<string, TimelineAsset>(assets.map((asset) => [asset.id, asset]));
|
||||||
const { unprocessedIds } = this.#runAssetOperation(new Set(lookup.keys()), (asset) => {
|
const { unprocessedIds } = this.#runAssetOperation(new Set(lookup.keys()), (asset) => {
|
||||||
updateObject(asset, lookup.get(asset.id));
|
updateObject(asset, lookup.get(asset.id));
|
||||||
return { remove: false };
|
return { remove: false };
|
||||||
@ -1261,11 +1330,11 @@ export class AssetStore {
|
|||||||
this.updateIntersections();
|
this.updateIntersections();
|
||||||
}
|
}
|
||||||
|
|
||||||
getFirstAsset(): AssetResponseDto | undefined {
|
getFirstAsset(): TimelineAsset | undefined {
|
||||||
return this.buckets[0]?.getFirstAsset();
|
return this.buckets[0]?.getFirstAsset();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPreviousAsset(asset: AssetResponseDto): Promise<AssetResponseDto | undefined> {
|
async getPreviousAsset(asset: { id: string; localDateTime: string }): Promise<TimelineAsset | undefined> {
|
||||||
let bucket = await this.#getBucketInfoForAsset(asset);
|
let bucket = await this.#getBucketInfoForAsset(asset);
|
||||||
if (!bucket) {
|
if (!bucket) {
|
||||||
return;
|
return;
|
||||||
@ -1308,7 +1377,7 @@ export class AssetStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNextAsset(asset: AssetResponseDto): Promise<AssetResponseDto | undefined> {
|
async getNextAsset(asset: { id: string; localDateTime: string }): Promise<TimelineAsset | undefined> {
|
||||||
let bucket = await this.#getBucketInfoForAsset(asset);
|
let bucket = await this.#getBucketInfoForAsset(asset);
|
||||||
if (!bucket) {
|
if (!bucket) {
|
||||||
return;
|
return;
|
||||||
@ -1347,7 +1416,7 @@ export class AssetStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isExcluded(asset: AssetResponseDto) {
|
isExcluded(asset: TimelineAsset) {
|
||||||
return (
|
return (
|
||||||
isMismatched(this.#options.isArchived, asset.isArchived) ||
|
isMismatched(this.#options.isArchived, asset.isArchived) ||
|
||||||
isMismatched(this.#options.isFavorite, asset.isFavorite) ||
|
isMismatched(this.#options.isFavorite, asset.isFavorite) ||
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
|
import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
|
||||||
import type { AssetStore } from '$lib/stores/assets-store.svelte';
|
import type { AssetStore, TimelineAsset } from '$lib/stores/assets-store.svelte';
|
||||||
import type { StackResponse } from '$lib/utils/asset-utils';
|
import type { StackResponse } from '$lib/utils/asset-utils';
|
||||||
import { deleteAssets as deleteBulk, type AssetResponseDto } from '@immich/sdk';
|
import { deleteAssets as deleteBulk } from '@immich/sdk';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { handleError } from './handle-error';
|
import { handleError } from './handle-error';
|
||||||
|
|
||||||
export type OnDelete = (assetIds: string[]) => void;
|
export type OnDelete = (assetIds: string[]) => void;
|
||||||
export type OnRestore = (ids: string[]) => void;
|
export type OnRestore = (ids: string[]) => void;
|
||||||
export type OnLink = (assets: { still: AssetResponseDto; motion: AssetResponseDto }) => void;
|
export type OnLink = (assets: { still: TimelineAsset; motion: TimelineAsset }) => void;
|
||||||
export type OnUnlink = (assets: { still: AssetResponseDto; motion: AssetResponseDto }) => void;
|
export type OnUnlink = (assets: { still: TimelineAsset; motion: TimelineAsset }) => void;
|
||||||
export type OnAddToAlbum = (ids: string[], albumId: string) => void;
|
export type OnAddToAlbum = (ids: string[], albumId: string) => void;
|
||||||
export type OnArchive = (ids: string[], isArchived: boolean) => void;
|
export type OnArchive = (ids: string[], isArchived: boolean) => void;
|
||||||
export type OnFavorite = (ids: string[], favorite: boolean) => void;
|
export type OnFavorite = (ids: string[], favorite: boolean) => void;
|
||||||
export type OnStack = (result: StackResponse) => void;
|
export type OnStack = (result: StackResponse) => void;
|
||||||
export type OnUnstack = (assets: AssetResponseDto[]) => void;
|
export type OnUnstack = (assets: TimelineAsset[]) => void;
|
||||||
|
|
||||||
export const deleteAssets = async (force: boolean, onAssetDelete: OnDelete, ids: string[]) => {
|
export const deleteAssets = async (force: boolean, onAssetDelete: OnDelete, ids: string[]) => {
|
||||||
const $t = get(t);
|
const $t = get(t);
|
||||||
@ -64,11 +64,11 @@ export function updateStackedAssetInTimeline(assetStore: AssetStore, { stack, to
|
|||||||
* @param assetStore - The asset store to update.
|
* @param assetStore - The asset store to update.
|
||||||
* @param assets - The array of asset response DTOs to update in the asset store.
|
* @param assets - The array of asset response DTOs to update in the asset store.
|
||||||
*/
|
*/
|
||||||
export function updateUnstackedAssetInTimeline(assetStore: AssetStore, assets: AssetResponseDto[]) {
|
export function updateUnstackedAssetInTimeline(assetStore: AssetStore, assets: TimelineAsset[]) {
|
||||||
assetStore.updateAssetOperation(
|
assetStore.updateAssetOperation(
|
||||||
assets.map((asset) => asset.id),
|
assets.map((asset) => asset.id),
|
||||||
(asset) => {
|
(asset) => {
|
||||||
asset.stack = undefined;
|
asset.stack = null;
|
||||||
return { remove: false };
|
return { remove: false };
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -3,7 +3,7 @@ import FormatBoldMessage from '$lib/components/i18n/format-bold-message.svelte';
|
|||||||
import type { InterpolationValues } from '$lib/components/i18n/format-message';
|
import type { InterpolationValues } from '$lib/components/i18n/format-message';
|
||||||
import { NotificationType, notificationController } from '$lib/components/shared-components/notification/notification';
|
import { NotificationType, notificationController } from '$lib/components/shared-components/notification/notification';
|
||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import type { AssetInteraction, BaseInteractionAsset } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetsSnapshot, isSelectingAllAssets, type AssetStore } from '$lib/stores/assets-store.svelte';
|
import { assetsSnapshot, isSelectingAllAssets, type AssetStore } from '$lib/stores/assets-store.svelte';
|
||||||
import { downloadManager } from '$lib/stores/download-store.svelte';
|
import { downloadManager } from '$lib/stores/download-store.svelte';
|
||||||
import { preferences } from '$lib/stores/user.store';
|
import { preferences } from '$lib/stores/user.store';
|
||||||
@ -364,7 +364,7 @@ export const getAssetType = (type: AssetTypeEnum) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSelectedAssets = (assets: AssetResponseDto[], user: UserResponseDto | null): string[] => {
|
export const getSelectedAssets = (assets: BaseInteractionAsset[], user: UserResponseDto | null): string[] => {
|
||||||
const ids = [...assets].filter((a) => user && a.ownerId === user.id).map((a) => a.id);
|
const ids = [...assets].filter((a) => user && a.ownerId === user.id).map((a) => a.id);
|
||||||
|
|
||||||
const numberOfIssues = [...assets].filter((a) => user && a.ownerId !== user.id).length;
|
const numberOfIssues = [...assets].filter((a) => user && a.ownerId !== user.id).length;
|
||||||
@ -383,7 +383,7 @@ export type StackResponse = {
|
|||||||
toDeleteIds: string[];
|
toDeleteIds: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const stackAssets = async (assets: AssetResponseDto[], showNotification = true): Promise<StackResponse> => {
|
export const stackAssets = async (assets: { id: string }[], showNotification = true): Promise<StackResponse> => {
|
||||||
if (assets.length < 2) {
|
if (assets.length < 2) {
|
||||||
return { stack: undefined, toDeleteIds: [] };
|
return { stack: undefined, toDeleteIds: [] };
|
||||||
}
|
}
|
||||||
@ -403,9 +403,9 @@ export const stackAssets = async (assets: AssetResponseDto[], showNotification =
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [index, asset] of assets.entries()) {
|
// for (const [index, asset] of assets.entries()) {
|
||||||
asset.stack = index === 0 ? { id: stack.id, assetCount: stack.assets.length, primaryAssetId: asset.id } : null;
|
// asset.stack = index === 0 ? { id: stack.id, assetCount: stack.assets.length, primaryAssetId: asset.id } : null;
|
||||||
}
|
// }
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stack,
|
stack,
|
||||||
@ -467,7 +467,10 @@ export const keepThisDeleteOthers = async (keepAsset: AssetResponseDto, stack: S
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const selectAllAssets = async (assetStore: AssetStore, assetInteraction: AssetInteraction) => {
|
export const selectAllAssets = async (
|
||||||
|
assetStore: AssetStore,
|
||||||
|
assetInteraction: AssetInteraction<BaseInteractionAsset>,
|
||||||
|
) => {
|
||||||
if (get(isSelectingAllAssets)) {
|
if (get(isSelectingAllAssets)) {
|
||||||
// Selection is already ongoing
|
// Selection is already ongoing
|
||||||
return;
|
return;
|
||||||
@ -495,7 +498,7 @@ export const selectAllAssets = async (assetStore: AssetStore, assetInteraction:
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cancelMultiselect = (assetInteraction: AssetInteraction) => {
|
export const cancelMultiselect = (assetInteraction: AssetInteraction<BaseInteractionAsset>) => {
|
||||||
isSelectingAllAssets.set(false);
|
isSelectingAllAssets.set(false);
|
||||||
assetInteraction.clearMultiselect();
|
assetInteraction.clearMultiselect();
|
||||||
};
|
};
|
||||||
@ -523,7 +526,7 @@ export const toggleArchive = async (asset: AssetResponseDto) => {
|
|||||||
return asset;
|
return asset;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const archiveAssets = async (assets: AssetResponseDto[], archive: boolean) => {
|
export const archiveAssets = async (assets: { id: string }[], archive: boolean) => {
|
||||||
const isArchived = archive;
|
const isArchived = archive;
|
||||||
const ids = assets.map(({ id }) => id);
|
const ids = assets.map(({ id }) => id);
|
||||||
const $t = get(t);
|
const $t = get(t);
|
||||||
@ -533,9 +536,9 @@ export const archiveAssets = async (assets: AssetResponseDto[], archive: boolean
|
|||||||
await updateAssets({ assetBulkUpdateDto: { ids, isArchived } });
|
await updateAssets({ assetBulkUpdateDto: { ids, isArchived } });
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const asset of assets) {
|
// for (const asset of assets) {
|
||||||
asset.isArchived = isArchived;
|
// asset.isArchived = isArchived;
|
||||||
}
|
// }
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: isArchived
|
message: isArchived
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import { getAssetRatio } from '$lib/utils/asset-utils';
|
|
||||||
// import { TUNABLES } from '$lib/utils/tunables';
|
// import { TUNABLES } from '$lib/utils/tunables';
|
||||||
// note: it's important that this is not imported in more than one file due to https://github.com/sveltejs/kit/issues/7805
|
// note: it's important that this is not imported in more than one file due to https://github.com/sveltejs/kit/issues/7805
|
||||||
// import { JustifiedLayout, type LayoutOptions } from '@immich/justified-layout-wasm';
|
// import { JustifiedLayout, type LayoutOptions } from '@immich/justified-layout-wasm';
|
||||||
|
|
||||||
|
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
|
||||||
|
import { getAssetRatio } from '$lib/utils/asset-utils';
|
||||||
|
import { isTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import type { AssetResponseDto } from '@immich/sdk';
|
import type { AssetResponseDto } from '@immich/sdk';
|
||||||
import createJustifiedLayout from 'justified-layout';
|
import createJustifiedLayout from 'justified-layout';
|
||||||
|
|
||||||
@ -26,7 +29,7 @@ export type CommonLayoutOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function getJustifiedLayoutFromAssets(
|
export function getJustifiedLayoutFromAssets(
|
||||||
assets: AssetResponseDto[],
|
assets: (TimelineAsset | AssetResponseDto)[],
|
||||||
options: CommonLayoutOptions,
|
options: CommonLayoutOptions,
|
||||||
): CommonJustifiedLayout {
|
): CommonJustifiedLayout {
|
||||||
// if (useWasm) {
|
// if (useWasm) {
|
||||||
@ -87,7 +90,7 @@ class Adapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function justifiedLayout(assets: AssetResponseDto[], options: CommonLayoutOptions) {
|
export function justifiedLayout(assets: (TimelineAsset | AssetResponseDto)[], options: CommonLayoutOptions) {
|
||||||
const adapter = {
|
const adapter = {
|
||||||
targetRowHeight: options.rowHeight,
|
targetRowHeight: options.rowHeight,
|
||||||
containerWidth: options.rowWidth,
|
containerWidth: options.rowWidth,
|
||||||
@ -96,7 +99,7 @@ export function justifiedLayout(assets: AssetResponseDto[], options: CommonLayou
|
|||||||
};
|
};
|
||||||
|
|
||||||
const result = createJustifiedLayout(
|
const result = createJustifiedLayout(
|
||||||
assets.map((g) => getAssetRatio(g)),
|
assets.map((a) => (isTimelineAsset(a) ? a.ratio : getAssetRatio(a))),
|
||||||
adapter,
|
adapter,
|
||||||
);
|
);
|
||||||
return new Adapter(result);
|
return new Adapter(result);
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import type { AssetBucket } from '$lib/stores/assets-store.svelte';
|
import type { BaseInteractionAsset } from '$lib/stores/asset-interaction.svelte';
|
||||||
|
import type { AssetBucket, TimelineAsset } from '$lib/stores/assets-store.svelte';
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
|
import { getAssetRatio } from '$lib/utils/asset-utils';
|
||||||
import { type CommonJustifiedLayout } from '$lib/utils/layout-utils';
|
import { type CommonJustifiedLayout } from '$lib/utils/layout-utils';
|
||||||
|
|
||||||
import type { AssetResponseDto } from '@immich/sdk';
|
import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
|
||||||
import { memoize } from 'lodash-es';
|
import { memoize } from 'lodash-es';
|
||||||
import { DateTime, type LocaleOptions } from 'luxon';
|
import { DateTime, type LocaleOptions } from 'luxon';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
@ -105,3 +107,30 @@ export const getDateLocaleString = (date: DateTime, opts?: LocaleOptions): strin
|
|||||||
date.toLocaleString(DateTime.DATE_MED_WITH_WEEKDAY, opts);
|
date.toLocaleString(DateTime.DATE_MED_WITH_WEEKDAY, opts);
|
||||||
|
|
||||||
export const formatDateGroupTitle = memoize(formatGroupTitle);
|
export const formatDateGroupTitle = memoize(formatGroupTitle);
|
||||||
|
|
||||||
|
export const toTimelineAsset = (unknownAsset: BaseInteractionAsset): TimelineAsset => {
|
||||||
|
if (isTimelineAsset(unknownAsset)) {
|
||||||
|
return unknownAsset;
|
||||||
|
}
|
||||||
|
const assetResponse = unknownAsset as AssetResponseDto;
|
||||||
|
const { width, height } = getAssetRatio(assetResponse);
|
||||||
|
const ratio = width / height;
|
||||||
|
return {
|
||||||
|
id: assetResponse.id,
|
||||||
|
ownerId: assetResponse.ownerId,
|
||||||
|
ratio,
|
||||||
|
thumbhash: assetResponse.thumbhash,
|
||||||
|
localDateTime: assetResponse.localDateTime,
|
||||||
|
isFavorite: assetResponse.isFavorite,
|
||||||
|
isArchived: assetResponse.isArchived,
|
||||||
|
isTrashed: assetResponse.isTrashed,
|
||||||
|
isVideo: assetResponse.type == AssetTypeEnum.Video,
|
||||||
|
isImage: assetResponse.type == AssetTypeEnum.Image,
|
||||||
|
stack: assetResponse.stack || null,
|
||||||
|
duration: assetResponse.duration || null,
|
||||||
|
projectionType: assetResponse.exifInfo?.projectionType || null,
|
||||||
|
livePhotoVideoId: assetResponse.livePhotoVideoId || null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export const isTimelineAsset = (arg: BaseInteractionAsset): arg is TimelineAsset =>
|
||||||
|
(arg as TimelineAsset).ratio !== undefined;
|
||||||
|
@ -22,9 +22,10 @@
|
|||||||
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
|
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
|
||||||
import RemoveFromAlbum from '$lib/components/photos-page/actions/remove-from-album.svelte';
|
import RemoveFromAlbum from '$lib/components/photos-page/actions/remove-from-album.svelte';
|
||||||
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
|
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
|
||||||
|
import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
|
||||||
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
|
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
|
||||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
|
||||||
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||||
|
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||||
import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
|
import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
|
||||||
@ -33,14 +34,16 @@
|
|||||||
notificationController,
|
notificationController,
|
||||||
} from '$lib/components/shared-components/notification/notification';
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
|
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
|
||||||
import { AppRoute, AlbumPageViewMode } from '$lib/constants';
|
import { AlbumPageViewMode, AppRoute } from '$lib/constants';
|
||||||
import { numberOfComments, setNumberOfComments, updateNumberOfComments } from '$lib/stores/activity.store';
|
import { numberOfComments, setNumberOfComments, updateNumberOfComments } from '$lib/stores/activity.store';
|
||||||
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
import { AssetStore, type TimelineAsset } from '$lib/stores/assets-store.svelte';
|
||||||
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
||||||
import { preferences, user } from '$lib/stores/user.store';
|
import { preferences, user } from '$lib/stores/user.store';
|
||||||
import { handlePromiseError } from '$lib/utils';
|
import { handlePromiseError } from '$lib/utils';
|
||||||
import { downloadAlbum, cancelMultiselect } from '$lib/utils/asset-utils';
|
import { confirmAlbumDelete } from '$lib/utils/album-utils';
|
||||||
|
import { cancelMultiselect, downloadAlbum } from '$lib/utils/asset-utils';
|
||||||
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import {
|
import {
|
||||||
@ -80,13 +83,10 @@
|
|||||||
mdiPresentationPlay,
|
mdiPresentationPlay,
|
||||||
mdiShareVariantOutline,
|
mdiShareVariantOutline,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
|
import { onDestroy } from 'svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
import { onDestroy } from 'svelte';
|
|
||||||
import { confirmAlbumDelete } from '$lib/utils/album-utils';
|
|
||||||
import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
|
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: PageData;
|
data: PageData;
|
||||||
@ -94,7 +94,7 @@
|
|||||||
|
|
||||||
let { data = $bindable() }: Props = $props();
|
let { data = $bindable() }: Props = $props();
|
||||||
|
|
||||||
let { isViewing: showAssetViewer, setAsset, gridScrollTarget } = assetViewingStore;
|
let { isViewing: showAssetViewer, setAssetId, gridScrollTarget } = assetViewingStore;
|
||||||
let { slideshowState, slideshowNavigation } = slideshowStore;
|
let { slideshowState, slideshowNavigation } = slideshowStore;
|
||||||
|
|
||||||
let oldAt: AssetGridRouteSearchParams | null | undefined = $state();
|
let oldAt: AssetGridRouteSearchParams | null | undefined = $state();
|
||||||
@ -107,8 +107,8 @@
|
|||||||
let reactions: ActivityResponseDto[] = $state([]);
|
let reactions: ActivityResponseDto[] = $state([]);
|
||||||
let albumOrder: AssetOrder | undefined = $state(data.album.order);
|
let albumOrder: AssetOrder | undefined = $state(data.album.order);
|
||||||
|
|
||||||
const assetInteraction = new AssetInteraction();
|
const assetInteraction = new AssetInteraction<TimelineAsset>();
|
||||||
const timelineInteraction = new AssetInteraction();
|
const timelineInteraction = new AssetInteraction<TimelineAsset>();
|
||||||
|
|
||||||
afterNavigate(({ from }) => {
|
afterNavigate(({ from }) => {
|
||||||
let url: string | undefined = from?.url?.pathname;
|
let url: string | undefined = from?.url?.pathname;
|
||||||
@ -207,8 +207,7 @@
|
|||||||
? await assetStore.getRandomAsset()
|
? await assetStore.getRandomAsset()
|
||||||
: assetStore.buckets[0]?.dateGroups[0]?.intersetingAssets[0]?.asset;
|
: assetStore.buckets[0]?.dateGroups[0]?.intersetingAssets[0]?.asset;
|
||||||
if (asset) {
|
if (asset) {
|
||||||
setAsset(asset);
|
handlePromiseError(setAssetId(asset.id).then(() => ($slideshowState = SlideshowState.PlaySlideshow)));
|
||||||
$slideshowState = SlideshowState.PlaySlideshow;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -9,16 +9,16 @@
|
|||||||
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
|
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
|
||||||
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
|
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
|
||||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||||
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
|
||||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||||
import { AssetAction } from '$lib/constants';
|
import { AssetAction } from '$lib/constants';
|
||||||
|
|
||||||
import type { PageData } from './$types';
|
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||||
import { mdiPlus, mdiDotsVertical } from '@mdi/js';
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
import { onDestroy } from 'svelte';
|
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
import { AssetStore, type TimelineAsset } from '$lib/stores/assets-store.svelte';
|
||||||
|
import { mdiDotsVertical, mdiPlus } from '@mdi/js';
|
||||||
|
import { onDestroy } from 'svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: PageData;
|
data: PageData;
|
||||||
@ -29,7 +29,7 @@
|
|||||||
void assetStore.updateOptions({ isArchived: true });
|
void assetStore.updateOptions({ isArchived: true });
|
||||||
onDestroy(() => assetStore.destroy());
|
onDestroy(() => assetStore.destroy());
|
||||||
|
|
||||||
const assetInteraction = new AssetInteraction();
|
const assetInteraction = new AssetInteraction<TimelineAsset>();
|
||||||
|
|
||||||
const handleEscape = () => {
|
const handleEscape = () => {
|
||||||
if (assetInteraction.selectionActive) {
|
if (assetInteraction.selectionActive) {
|
||||||
|
@ -9,19 +9,19 @@
|
|||||||
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
||||||
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
|
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
|
||||||
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
|
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
|
||||||
|
import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
|
||||||
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
|
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
|
||||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
|
||||||
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||||
|
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||||
import { AssetAction } from '$lib/constants';
|
import { AssetAction } from '$lib/constants';
|
||||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
|
||||||
import type { PageData } from './$types';
|
|
||||||
import { mdiDotsVertical, mdiPlus } from '@mdi/js';
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
import { onDestroy } from 'svelte';
|
|
||||||
import { preferences } from '$lib/stores/user.store';
|
|
||||||
import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
|
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
|
import { AssetStore, type TimelineAsset } from '$lib/stores/assets-store.svelte';
|
||||||
|
import { preferences } from '$lib/stores/user.store';
|
||||||
|
import { mdiDotsVertical, mdiPlus } from '@mdi/js';
|
||||||
|
import { onDestroy } from 'svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: PageData;
|
data: PageData;
|
||||||
@ -30,10 +30,10 @@
|
|||||||
let { data }: Props = $props();
|
let { data }: Props = $props();
|
||||||
|
|
||||||
const assetStore = new AssetStore();
|
const assetStore = new AssetStore();
|
||||||
void assetStore.updateOptions({ isFavorite: true });
|
void assetStore.updateOptions({ isFavorite: true, withStacked: true });
|
||||||
onDestroy(() => assetStore.destroy());
|
onDestroy(() => assetStore.destroy());
|
||||||
|
|
||||||
const assetInteraction = new AssetInteraction();
|
const assetInteraction = new AssetInteraction<TimelineAsset>();
|
||||||
|
|
||||||
const handleEscape = () => {
|
const handleEscape = () => {
|
||||||
if (assetInteraction.selectionActive) {
|
if (assetInteraction.selectionActive) {
|
||||||
@ -76,6 +76,7 @@
|
|||||||
<UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}>
|
<UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}>
|
||||||
<AssetGrid
|
<AssetGrid
|
||||||
enableRouting={true}
|
enableRouting={true}
|
||||||
|
withStacked={true}
|
||||||
{assetStore}
|
{assetStore}
|
||||||
{assetInteraction}
|
{assetInteraction}
|
||||||
removeAction={AssetAction.UNFAVORITE}
|
removeAction={AssetAction.UNFAVORITE}
|
||||||
|
@ -1,37 +1,38 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { afterNavigate, goto, invalidateAll } from '$app/navigation';
|
import { afterNavigate, goto, invalidateAll } from '$app/navigation';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
|
import SkipLink from '$lib/components/elements/buttons/skip-link.svelte';
|
||||||
import UserPageLayout, { headerId } from '$lib/components/layouts/user-page-layout.svelte';
|
import UserPageLayout, { headerId } from '$lib/components/layouts/user-page-layout.svelte';
|
||||||
|
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
||||||
|
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
|
||||||
|
import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte';
|
||||||
|
import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
|
||||||
|
import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte';
|
||||||
|
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
||||||
|
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
||||||
|
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
||||||
|
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
|
||||||
|
import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
|
||||||
|
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||||
|
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||||
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
|
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
|
||||||
import SideBarSection from '$lib/components/shared-components/side-bar/side-bar-section.svelte';
|
import SideBarSection from '$lib/components/shared-components/side-bar/side-bar-section.svelte';
|
||||||
|
import Breadcrumbs from '$lib/components/shared-components/tree/breadcrumbs.svelte';
|
||||||
import TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.svelte';
|
import TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.svelte';
|
||||||
import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte';
|
import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte';
|
||||||
import { AppRoute, QueryParameter } from '$lib/constants';
|
import { AppRoute, QueryParameter } from '$lib/constants';
|
||||||
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import type { Viewport } from '$lib/stores/assets-store.svelte';
|
import type { Viewport } from '$lib/stores/assets-store.svelte';
|
||||||
import { foldersStore } from '$lib/stores/folders.svelte';
|
import { foldersStore } from '$lib/stores/folders.svelte';
|
||||||
|
import { preferences } from '$lib/stores/user.store';
|
||||||
|
import { cancelMultiselect } from '$lib/utils/asset-utils';
|
||||||
import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils';
|
import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils';
|
||||||
|
import type { AssetResponseDto } from '@immich/sdk';
|
||||||
import { mdiDotsVertical, mdiFolder, mdiFolderHome, mdiFolderOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
|
import { mdiDotsVertical, mdiFolder, mdiFolderHome, mdiFolderOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import Breadcrumbs from '$lib/components/shared-components/tree/breadcrumbs.svelte';
|
|
||||||
import SkipLink from '$lib/components/elements/buttons/skip-link.svelte';
|
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
|
||||||
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
|
||||||
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
|
||||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
|
||||||
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
|
||||||
import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte';
|
|
||||||
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
|
||||||
import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
|
|
||||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
|
||||||
import { preferences } from '$lib/stores/user.store';
|
|
||||||
import { cancelMultiselect } from '$lib/utils/asset-utils';
|
|
||||||
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
|
||||||
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
|
|
||||||
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
|
|
||||||
import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
|
|
||||||
import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: PageData;
|
data: PageData;
|
||||||
@ -46,7 +47,7 @@
|
|||||||
let currentPath = $derived($page.url.searchParams.get(QueryParameter.PATH) || '');
|
let currentPath = $derived($page.url.searchParams.get(QueryParameter.PATH) || '');
|
||||||
let currentTreeItems = $derived(currentPath ? data.currentFolders : Object.keys(tree).sort());
|
let currentTreeItems = $derived(currentPath ? data.currentFolders : Object.keys(tree).sort());
|
||||||
|
|
||||||
const assetInteraction = new AssetInteraction();
|
const assetInteraction = new AssetInteraction<AssetResponseDto>();
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await foldersStore.fetchUniquePaths();
|
await foldersStore.fetchUniquePaths();
|
||||||
|
@ -4,16 +4,16 @@
|
|||||||
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
||||||
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
||||||
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
|
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
|
||||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
|
||||||
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||||
|
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
|
||||||
import { onDestroy } from 'svelte';
|
|
||||||
import type { PageData } from './$types';
|
|
||||||
import { mdiPlus, mdiArrowLeft } from '@mdi/js';
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
|
import { AssetStore, type TimelineAsset } from '$lib/stores/assets-store.svelte';
|
||||||
|
import { mdiArrowLeft, mdiPlus } from '@mdi/js';
|
||||||
|
import { onDestroy } from 'svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: PageData;
|
data: PageData;
|
||||||
@ -24,7 +24,7 @@
|
|||||||
const assetStore = new AssetStore();
|
const assetStore = new AssetStore();
|
||||||
$effect(() => void assetStore.updateOptions({ userId: data.partner.id, isArchived: false, withStacked: true }));
|
$effect(() => void assetStore.updateOptions({ userId: data.partner.id, isArchived: false, withStacked: true }));
|
||||||
onDestroy(() => assetStore.destroy());
|
onDestroy(() => assetStore.destroy());
|
||||||
const assetInteraction = new AssetInteraction();
|
const assetInteraction = new AssetInteraction<TimelineAsset>();
|
||||||
|
|
||||||
const handleEscape = () => {
|
const handleEscape = () => {
|
||||||
if (assetInteraction.selectionActive) {
|
if (assetInteraction.selectionActive) {
|
||||||
|
@ -33,20 +33,14 @@
|
|||||||
import { AppRoute, PersonPageViewMode, QueryParameter, SessionStorageKey } from '$lib/constants';
|
import { AppRoute, PersonPageViewMode, QueryParameter, SessionStorageKey } from '$lib/constants';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
import { AssetStore, type TimelineAsset } from '$lib/stores/assets-store.svelte';
|
||||||
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { preferences } from '$lib/stores/user.store';
|
import { preferences } from '$lib/stores/user.store';
|
||||||
import { websocketEvents } from '$lib/stores/websocket';
|
import { websocketEvents } from '$lib/stores/websocket';
|
||||||
import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
|
import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { isExternalUrl } from '$lib/utils/navigation';
|
import { isExternalUrl } from '$lib/utils/navigation';
|
||||||
import {
|
import { getPersonStatistics, mergePerson, searchPerson, updatePerson, type PersonResponseDto } from '@immich/sdk';
|
||||||
getPersonStatistics,
|
|
||||||
mergePerson,
|
|
||||||
searchPerson,
|
|
||||||
updatePerson,
|
|
||||||
type AssetResponseDto,
|
|
||||||
type PersonResponseDto,
|
|
||||||
} from '@immich/sdk';
|
|
||||||
import {
|
import {
|
||||||
mdiAccountBoxOutline,
|
mdiAccountBoxOutline,
|
||||||
mdiAccountMultipleCheckOutline,
|
mdiAccountMultipleCheckOutline,
|
||||||
@ -59,11 +53,10 @@
|
|||||||
mdiHeartOutline,
|
mdiHeartOutline,
|
||||||
mdiPlus,
|
mdiPlus,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
|
||||||
import { DateTime } from 'luxon';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: PageData;
|
data: PageData;
|
||||||
@ -78,7 +71,7 @@
|
|||||||
$effect(() => void assetStore.updateOptions({ isArchived: false, personId: data.person.id }));
|
$effect(() => void assetStore.updateOptions({ isArchived: false, personId: data.person.id }));
|
||||||
onDestroy(() => assetStore.destroy());
|
onDestroy(() => assetStore.destroy());
|
||||||
|
|
||||||
const assetInteraction = new AssetInteraction();
|
const assetInteraction = new AssetInteraction<TimelineAsset>();
|
||||||
|
|
||||||
let viewMode: PersonPageViewMode = $state(PersonPageViewMode.VIEW_ASSETS);
|
let viewMode: PersonPageViewMode = $state(PersonPageViewMode.VIEW_ASSETS);
|
||||||
let isEditingName = $state(false);
|
let isEditingName = $state(false);
|
||||||
@ -202,7 +195,7 @@
|
|||||||
data = { ...data, person };
|
data = { ...data, person };
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectFeaturePhoto = async (asset: AssetResponseDto) => {
|
const handleSelectFeaturePhoto = async (asset: TimelineAsset) => {
|
||||||
if (viewMode !== PersonPageViewMode.SELECT_PERSON) {
|
if (viewMode !== PersonPageViewMode.SELECT_PERSON) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
import { AssetAction } from '$lib/constants';
|
import { AssetAction } from '$lib/constants';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
import { AssetStore, type TimelineAsset } from '$lib/stores/assets-store.svelte';
|
||||||
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
||||||
import { preferences, user } from '$lib/stores/user.store';
|
import { preferences, user } from '$lib/stores/user.store';
|
||||||
import {
|
import {
|
||||||
@ -32,7 +32,7 @@
|
|||||||
type OnUnlink,
|
type OnUnlink,
|
||||||
} from '$lib/utils/actions';
|
} from '$lib/utils/actions';
|
||||||
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||||
import { AssetTypeEnum } from '@immich/sdk';
|
|
||||||
import { mdiDotsVertical, mdiPlus } from '@mdi/js';
|
import { mdiDotsVertical, mdiPlus } from '@mdi/js';
|
||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
@ -42,7 +42,7 @@
|
|||||||
void assetStore.updateOptions({ isArchived: false, withStacked: true, withPartners: true });
|
void assetStore.updateOptions({ isArchived: false, withStacked: true, withPartners: true });
|
||||||
onDestroy(() => assetStore.destroy());
|
onDestroy(() => assetStore.destroy());
|
||||||
|
|
||||||
const assetInteraction = new AssetInteraction();
|
const assetInteraction = new AssetInteraction<TimelineAsset>();
|
||||||
|
|
||||||
let selectedAssets = $derived(assetInteraction.selectedAssets);
|
let selectedAssets = $derived(assetInteraction.selectedAssets);
|
||||||
let isAssetStackSelected = $derived(selectedAssets.length === 1 && !!selectedAssets[0].stack);
|
let isAssetStackSelected = $derived(selectedAssets.length === 1 && !!selectedAssets[0].stack);
|
||||||
@ -50,8 +50,8 @@
|
|||||||
const isLivePhoto = selectedAssets.length === 1 && !!selectedAssets[0].livePhotoVideoId;
|
const isLivePhoto = selectedAssets.length === 1 && !!selectedAssets[0].livePhotoVideoId;
|
||||||
const isLivePhotoCandidate =
|
const isLivePhotoCandidate =
|
||||||
selectedAssets.length === 2 &&
|
selectedAssets.length === 2 &&
|
||||||
selectedAssets.some((asset) => asset.type === AssetTypeEnum.Image) &&
|
selectedAssets.some((asset) => asset.isImage) &&
|
||||||
selectedAssets.some((asset) => asset.type === AssetTypeEnum.Video);
|
selectedAssets.some((asset) => asset.isVideo);
|
||||||
|
|
||||||
return assetInteraction.isAllUserOwned && (isLivePhoto || isLivePhotoCandidate);
|
return assetInteraction.isAllUserOwned && (isLivePhoto || isLivePhotoCandidate);
|
||||||
});
|
});
|
||||||
|
@ -63,7 +63,7 @@
|
|||||||
let scrollY = $state(0);
|
let scrollY = $state(0);
|
||||||
let scrollYHistory = 0;
|
let scrollYHistory = 0;
|
||||||
|
|
||||||
const assetInteraction = new AssetInteraction();
|
const assetInteraction = new AssetInteraction<AssetResponseDto>();
|
||||||
|
|
||||||
type SearchTerms = MetadataSearchDto & Pick<SmartSearchDto, 'query'>;
|
type SearchTerms = MetadataSearchDto & Pick<SmartSearchDto, 'query'>;
|
||||||
let searchQuery = $derived(page.url.searchParams.get(QueryParameter.QUERY));
|
let searchQuery = $derived(page.url.searchParams.get(QueryParameter.QUERY));
|
||||||
|
@ -17,14 +17,14 @@
|
|||||||
import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte';
|
import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte';
|
||||||
import { AppRoute, AssetAction, QueryParameter, SettingInputFieldType } from '$lib/constants';
|
import { AppRoute, AssetAction, QueryParameter, SettingInputFieldType } from '$lib/constants';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
import { AssetStore, type TimelineAsset } from '$lib/stores/assets-store.svelte';
|
||||||
import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils';
|
import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils';
|
||||||
import { deleteTag, getAllTags, updateTag, upsertTags, type TagResponseDto } from '@immich/sdk';
|
import { deleteTag, getAllTags, updateTag, upsertTags, type TagResponseDto } from '@immich/sdk';
|
||||||
import { Button, HStack, Text } from '@immich/ui';
|
import { Button, HStack, Text } from '@immich/ui';
|
||||||
import { mdiPencil, mdiPlus, mdiTag, mdiTagMultiple, mdiTrashCanOutline } from '@mdi/js';
|
import { mdiPencil, mdiPlus, mdiTag, mdiTagMultiple, mdiTrashCanOutline } from '@mdi/js';
|
||||||
|
import { onDestroy } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import { onDestroy } from 'svelte';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: PageData;
|
data: PageData;
|
||||||
@ -35,7 +35,7 @@
|
|||||||
let pathSegments = $derived(data.path ? data.path.split('/') : []);
|
let pathSegments = $derived(data.path ? data.path.split('/') : []);
|
||||||
let currentPath = $derived($page.url.searchParams.get(QueryParameter.PATH) || '');
|
let currentPath = $derived($page.url.searchParams.get(QueryParameter.PATH) || '');
|
||||||
|
|
||||||
const assetInteraction = new AssetInteraction();
|
const assetInteraction = new AssetInteraction<TimelineAsset>();
|
||||||
|
|
||||||
const buildMap = (tags: TagResponseDto[]) => {
|
const buildMap = (tags: TagResponseDto[]) => {
|
||||||
return Object.fromEntries(tags.map((tag) => [tag.value, tag]));
|
return Object.fromEntries(tags.map((tag) => [tag.value, tag]));
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
} from '$lib/components/shared-components/notification/notification';
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
import { AssetStore, type TimelineAsset } from '$lib/stores/assets-store.svelte';
|
||||||
import { featureFlags, serverConfig } from '$lib/stores/server-config.store';
|
import { featureFlags, serverConfig } from '$lib/stores/server-config.store';
|
||||||
import { handlePromiseError } from '$lib/utils';
|
import { handlePromiseError } from '$lib/utils';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
@ -40,7 +40,7 @@
|
|||||||
void assetStore.updateOptions({ isTrashed: true });
|
void assetStore.updateOptions({ isTrashed: true });
|
||||||
onDestroy(() => assetStore.destroy());
|
onDestroy(() => assetStore.destroy());
|
||||||
|
|
||||||
const assetInteraction = new AssetInteraction();
|
const assetInteraction = new AssetInteraction<TimelineAsset>();
|
||||||
|
|
||||||
const handleEmptyTrash = async () => {
|
const handleEmptyTrash = async () => {
|
||||||
const isConfirmed = await dialogController.show({
|
const isConfirmed = await dialogController.show({
|
||||||
|
Loading…
x
Reference in New Issue
Block a user