Compare commits
54 Commits
sqlite-rem
...
lighter_bu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12381f6b3c | ||
|
|
c9728a107e | ||
|
|
35d91aa6bf | ||
|
|
4174575785 | ||
|
|
606d4b66d0 | ||
|
|
71cc045405 | ||
|
|
21bbf2f5e2 | ||
|
|
3ace02b3e7 | ||
|
|
85359bfc1a | ||
|
|
f7712c332e | ||
|
|
b20440e4d5 | ||
|
|
1d885c1a20 | ||
|
|
ef9245487c | ||
|
|
a3a2ced3a9 | ||
|
|
8837f5b4fb | ||
|
|
97cc9e223e | ||
|
|
07c03b8a79 | ||
|
|
5a3e32fc3c | ||
|
|
5520db10af | ||
|
|
ee08fd012d | ||
|
|
f7fd213260 | ||
|
|
73cd236756 | ||
|
|
bf0be6a655 | ||
|
|
6e8993c6eb | ||
|
|
aea2c9506d | ||
|
|
8011605e6f | ||
|
|
0ed2a2fd2e | ||
|
|
9d527b37f0 | ||
|
|
1d3a546646 | ||
|
|
c16348e3fd | ||
|
|
15d5460afb | ||
|
|
bc5d4b45a6 | ||
|
|
077703adcc | ||
|
|
580a0117c4 | ||
|
|
ffda7364dd | ||
|
|
236973e329 | ||
|
|
cd8806eac0 | ||
|
|
7f934583cf | ||
|
|
6308ae71a1 | ||
|
|
bfefa36f04 | ||
|
|
50cfc461a9 | ||
|
|
77121a0e07 | ||
|
|
89bfa692b1 | ||
|
|
683a10f0fe | ||
|
|
a5eaaddec4 | ||
|
|
d76c50ff22 | ||
|
|
0795f8a761 | ||
|
|
6cb7fffe91 | ||
|
|
9f6120a134 | ||
|
|
f3fe043c22 | ||
|
|
9b7e9bc7b8 | ||
|
|
c1e699ebaf | ||
|
|
3b9490e28d | ||
|
|
5a8f9f3b5c |
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}}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType, TimeBucketSize } from '@immich/sdk';
|
import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { createUserDto } from 'src/fixtures';
|
import { createUserDto } from 'src/fixtures';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
@@ -52,7 +52,7 @@ describe('/timeline', () => {
|
|||||||
|
|
||||||
describe('GET /timeline/buckets', () => {
|
describe('GET /timeline/buckets', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get('/timeline/buckets').query({ size: TimeBucketSize.Month });
|
const { status, body } = await request(app).get('/timeline/buckets').query({});
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
@@ -61,7 +61,7 @@ describe('/timeline', () => {
|
|||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/timeline/buckets')
|
.get('/timeline/buckets')
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||||
.query({ size: TimeBucketSize.Month });
|
.query({});
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
@@ -78,33 +78,17 @@ describe('/timeline', () => {
|
|||||||
assetIds: userAssets.map(({ id }) => id),
|
assetIds: userAssets.map(({ id }) => id),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app).get('/timeline/buckets').query({ key: sharedLink.key });
|
||||||
.get('/timeline/buckets')
|
|
||||||
.query({ key: sharedLink.key, size: TimeBucketSize.Month });
|
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.noPermission);
|
expect(body).toEqual(errorDto.noPermission);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get time buckets by day', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/timeline/buckets')
|
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
|
||||||
.query({ size: TimeBucketSize.Day });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual([
|
|
||||||
{ count: 2, timeBucket: '1970-02-11T00:00:00.000Z' },
|
|
||||||
{ count: 1, timeBucket: '1970-02-10T00:00:00.000Z' },
|
|
||||||
{ count: 1, timeBucket: '1970-01-01T00:00:00.000Z' },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return error if time bucket is requested with partners asset and archived', async () => {
|
it('should return error if time bucket is requested with partners asset and archived', async () => {
|
||||||
const req1 = await request(app)
|
const req1 = await request(app)
|
||||||
.get('/timeline/buckets')
|
.get('/timeline/buckets')
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||||
.query({ size: TimeBucketSize.Month, withPartners: true, isArchived: true });
|
.query({ withPartners: true, isArchived: true });
|
||||||
|
|
||||||
expect(req1.status).toBe(400);
|
expect(req1.status).toBe(400);
|
||||||
expect(req1.body).toEqual(errorDto.badRequest());
|
expect(req1.body).toEqual(errorDto.badRequest());
|
||||||
@@ -112,7 +96,7 @@ describe('/timeline', () => {
|
|||||||
const req2 = await request(app)
|
const req2 = await request(app)
|
||||||
.get('/timeline/buckets')
|
.get('/timeline/buckets')
|
||||||
.set('Authorization', `Bearer ${user.accessToken}`)
|
.set('Authorization', `Bearer ${user.accessToken}`)
|
||||||
.query({ size: TimeBucketSize.Month, withPartners: true, isArchived: undefined });
|
.query({ withPartners: true, isArchived: undefined });
|
||||||
|
|
||||||
expect(req2.status).toBe(400);
|
expect(req2.status).toBe(400);
|
||||||
expect(req2.body).toEqual(errorDto.badRequest());
|
expect(req2.body).toEqual(errorDto.badRequest());
|
||||||
@@ -122,7 +106,7 @@ describe('/timeline', () => {
|
|||||||
const req1 = await request(app)
|
const req1 = await request(app)
|
||||||
.get('/timeline/buckets')
|
.get('/timeline/buckets')
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||||
.query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: true });
|
.query({ withPartners: true, isFavorite: true });
|
||||||
|
|
||||||
expect(req1.status).toBe(400);
|
expect(req1.status).toBe(400);
|
||||||
expect(req1.body).toEqual(errorDto.badRequest());
|
expect(req1.body).toEqual(errorDto.badRequest());
|
||||||
@@ -130,7 +114,7 @@ describe('/timeline', () => {
|
|||||||
const req2 = await request(app)
|
const req2 = await request(app)
|
||||||
.get('/timeline/buckets')
|
.get('/timeline/buckets')
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||||
.query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: false });
|
.query({ withPartners: true, isFavorite: false });
|
||||||
|
|
||||||
expect(req2.status).toBe(400);
|
expect(req2.status).toBe(400);
|
||||||
expect(req2.body).toEqual(errorDto.badRequest());
|
expect(req2.body).toEqual(errorDto.badRequest());
|
||||||
@@ -140,7 +124,7 @@ describe('/timeline', () => {
|
|||||||
const req = await request(app)
|
const req = await request(app)
|
||||||
.get('/timeline/buckets')
|
.get('/timeline/buckets')
|
||||||
.set('Authorization', `Bearer ${user.accessToken}`)
|
.set('Authorization', `Bearer ${user.accessToken}`)
|
||||||
.query({ size: TimeBucketSize.Month, withPartners: true, isTrashed: true });
|
.query({ withPartners: true, isTrashed: true });
|
||||||
|
|
||||||
expect(req.status).toBe(400);
|
expect(req.status).toBe(400);
|
||||||
expect(req.body).toEqual(errorDto.badRequest());
|
expect(req.body).toEqual(errorDto.badRequest());
|
||||||
@@ -150,7 +134,6 @@ describe('/timeline', () => {
|
|||||||
describe('GET /timeline/bucket', () => {
|
describe('GET /timeline/bucket', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get('/timeline/bucket').query({
|
const { status, body } = await request(app).get('/timeline/bucket').query({
|
||||||
size: TimeBucketSize.Month,
|
|
||||||
timeBucket: '1900-01-01',
|
timeBucket: '1900-01-01',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -161,11 +144,27 @@ describe('/timeline', () => {
|
|||||||
it('should handle 5 digit years', async () => {
|
it('should handle 5 digit years', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/timeline/bucket')
|
.get('/timeline/bucket')
|
||||||
.query({ size: TimeBucketSize.Month, timeBucket: '012345-01-01' })
|
.query({ timeBucket: '012345-01-01' })
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`);
|
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual([]);
|
expect(body).toEqual({
|
||||||
|
city: [],
|
||||||
|
country: [],
|
||||||
|
duration: [],
|
||||||
|
id: [],
|
||||||
|
isArchived: [],
|
||||||
|
isFavorite: [],
|
||||||
|
isImage: [],
|
||||||
|
isTrashed: [],
|
||||||
|
livePhotoVideoId: [],
|
||||||
|
localDateTime: [],
|
||||||
|
ownerId: [],
|
||||||
|
projectionType: [],
|
||||||
|
ratio: [],
|
||||||
|
status: [],
|
||||||
|
thumbhash: [],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO enable date string validation while still accepting 5 digit years
|
// TODO enable date string validation while still accepting 5 digit years
|
||||||
@@ -173,7 +172,7 @@ describe('/timeline', () => {
|
|||||||
// const { status, body } = await request(app)
|
// const { status, body } = await request(app)
|
||||||
// .get('/timeline/bucket')
|
// .get('/timeline/bucket')
|
||||||
// .set('Authorization', `Bearer ${user.accessToken}`)
|
// .set('Authorization', `Bearer ${user.accessToken}`)
|
||||||
// .query({ size: TimeBucketSize.Month, timeBucket: 'foo' });
|
// .query({ timeBucket: 'foo' });
|
||||||
|
|
||||||
// expect(status).toBe(400);
|
// expect(status).toBe(400);
|
||||||
// expect(body).toEqual(errorDto.badRequest);
|
// expect(body).toEqual(errorDto.badRequest);
|
||||||
@@ -183,10 +182,26 @@ describe('/timeline', () => {
|
|||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/timeline/bucket')
|
.get('/timeline/bucket')
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||||
.query({ size: TimeBucketSize.Month, timeBucket: '1970-02-10' });
|
.query({ timeBucket: '1970-02-10' });
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual([]);
|
expect(body).toEqual({
|
||||||
|
city: [],
|
||||||
|
country: [],
|
||||||
|
duration: [],
|
||||||
|
id: [],
|
||||||
|
isArchived: [],
|
||||||
|
isFavorite: [],
|
||||||
|
isImage: [],
|
||||||
|
isTrashed: [],
|
||||||
|
livePhotoVideoId: [],
|
||||||
|
localDateTime: [],
|
||||||
|
ownerId: [],
|
||||||
|
projectionType: [],
|
||||||
|
ratio: [],
|
||||||
|
status: [],
|
||||||
|
thumbhash: [],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
4
mobile/openapi/README.md
generated
4
mobile/openapi/README.md
generated
@@ -477,8 +477,8 @@ 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)
|
||||||
- [TimeBucketResponseDto](doc//TimeBucketResponseDto.md)
|
- [TimeBucketAssetResponseDto](doc//TimeBucketAssetResponseDto.md)
|
||||||
- [TimeBucketSize](doc//TimeBucketSize.md)
|
- [TimeBucketsResponseDto](doc//TimeBucketsResponseDto.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
@@ -281,8 +281,8 @@ 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_response_dto.dart';
|
part 'model/time_bucket_asset_response_dto.dart';
|
||||||
part 'model/time_bucket_size.dart';
|
part 'model/time_buckets_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';
|
||||||
|
|||||||
47
mobile/openapi/lib/api/timeline_api.dart
generated
47
mobile/openapi/lib/api/timeline_api.dart
generated
@@ -19,8 +19,6 @@ class TimelineApi {
|
|||||||
/// Performs an HTTP 'GET /timeline/bucket' operation and returns the [Response].
|
/// Performs an HTTP 'GET /timeline/bucket' operation and returns the [Response].
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
/// * [TimeBucketSize] size (required):
|
|
||||||
///
|
|
||||||
/// * [String] timeBucket (required):
|
/// * [String] timeBucket (required):
|
||||||
///
|
///
|
||||||
/// * [String] albumId:
|
/// * [String] albumId:
|
||||||
@@ -35,6 +33,10 @@ class TimelineApi {
|
|||||||
///
|
///
|
||||||
/// * [AssetOrder] order:
|
/// * [AssetOrder] order:
|
||||||
///
|
///
|
||||||
|
/// * [num] page:
|
||||||
|
///
|
||||||
|
/// * [num] pageSize:
|
||||||
|
///
|
||||||
/// * [String] personId:
|
/// * [String] personId:
|
||||||
///
|
///
|
||||||
/// * [String] tagId:
|
/// * [String] tagId:
|
||||||
@@ -44,7 +46,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(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,10 +75,15 @@ 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));
|
||||||
}
|
}
|
||||||
queryParams.addAll(_queryParams('', 'size', size));
|
|
||||||
if (tagId != null) {
|
if (tagId != null) {
|
||||||
queryParams.addAll(_queryParams('', 'tagId', tagId));
|
queryParams.addAll(_queryParams('', 'tagId', tagId));
|
||||||
}
|
}
|
||||||
@@ -107,8 +114,6 @@ class TimelineApi {
|
|||||||
|
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
/// * [TimeBucketSize] size (required):
|
|
||||||
///
|
|
||||||
/// * [String] timeBucket (required):
|
/// * [String] timeBucket (required):
|
||||||
///
|
///
|
||||||
/// * [String] albumId:
|
/// * [String] albumId:
|
||||||
@@ -123,6 +128,10 @@ class TimelineApi {
|
|||||||
///
|
///
|
||||||
/// * [AssetOrder] order:
|
/// * [AssetOrder] order:
|
||||||
///
|
///
|
||||||
|
/// * [num] page:
|
||||||
|
///
|
||||||
|
/// * [num] pageSize:
|
||||||
|
///
|
||||||
/// * [String] personId:
|
/// * [String] personId:
|
||||||
///
|
///
|
||||||
/// * [String] tagId:
|
/// * [String] tagId:
|
||||||
@@ -132,8 +141,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<TimeBucketAssetResponseDto?> getTimeBucket(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(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 +150,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), 'TimeBucketAssetResponseDto',) as TimeBucketAssetResponseDto;
|
||||||
return (await apiClient.deserializeAsync(responseBody, 'List<AssetResponseDto>') as List)
|
|
||||||
.cast<AssetResponseDto>()
|
|
||||||
.toList(growable: false);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -153,8 +159,6 @@ class TimelineApi {
|
|||||||
/// Performs an HTTP 'GET /timeline/buckets' operation and returns the [Response].
|
/// Performs an HTTP 'GET /timeline/buckets' operation and returns the [Response].
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
/// * [TimeBucketSize] size (required):
|
|
||||||
///
|
|
||||||
/// * [String] albumId:
|
/// * [String] albumId:
|
||||||
///
|
///
|
||||||
/// * [bool] isArchived:
|
/// * [bool] isArchived:
|
||||||
@@ -176,7 +180,7 @@ class TimelineApi {
|
|||||||
/// * [bool] withPartners:
|
/// * [bool] withPartners:
|
||||||
///
|
///
|
||||||
/// * [bool] withStacked:
|
/// * [bool] withStacked:
|
||||||
Future<Response> getTimeBucketsWithHttpInfo(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<Response> getTimeBucketsWithHttpInfo({ String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/timeline/buckets';
|
final apiPath = r'/timeline/buckets';
|
||||||
|
|
||||||
@@ -208,7 +212,6 @@ class TimelineApi {
|
|||||||
if (personId != null) {
|
if (personId != null) {
|
||||||
queryParams.addAll(_queryParams('', 'personId', personId));
|
queryParams.addAll(_queryParams('', 'personId', personId));
|
||||||
}
|
}
|
||||||
queryParams.addAll(_queryParams('', 'size', size));
|
|
||||||
if (tagId != null) {
|
if (tagId != null) {
|
||||||
queryParams.addAll(_queryParams('', 'tagId', tagId));
|
queryParams.addAll(_queryParams('', 'tagId', tagId));
|
||||||
}
|
}
|
||||||
@@ -238,8 +241,6 @@ class TimelineApi {
|
|||||||
|
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
/// * [TimeBucketSize] size (required):
|
|
||||||
///
|
|
||||||
/// * [String] albumId:
|
/// * [String] albumId:
|
||||||
///
|
///
|
||||||
/// * [bool] isArchived:
|
/// * [bool] isArchived:
|
||||||
@@ -261,8 +262,8 @@ 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({ 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( 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 +272,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
@@ -618,10 +618,10 @@ class ApiClient {
|
|||||||
return TemplateResponseDto.fromJson(value);
|
return TemplateResponseDto.fromJson(value);
|
||||||
case 'TestEmailResponseDto':
|
case 'TestEmailResponseDto':
|
||||||
return TestEmailResponseDto.fromJson(value);
|
return TestEmailResponseDto.fromJson(value);
|
||||||
case 'TimeBucketResponseDto':
|
case 'TimeBucketAssetResponseDto':
|
||||||
return TimeBucketResponseDto.fromJson(value);
|
return TimeBucketAssetResponseDto.fromJson(value);
|
||||||
case 'TimeBucketSize':
|
case 'TimeBucketsResponseDto':
|
||||||
return TimeBucketSizeTypeTransformer().decode(value);
|
return TimeBucketsResponseDto.fromJson(value);
|
||||||
case 'ToneMapping':
|
case 'ToneMapping':
|
||||||
return ToneMappingTypeTransformer().decode(value);
|
return ToneMappingTypeTransformer().decode(value);
|
||||||
case 'TranscodeHWAccel':
|
case 'TranscodeHWAccel':
|
||||||
|
|||||||
3
mobile/openapi/lib/api_helper.dart
generated
3
mobile/openapi/lib/api_helper.dart
generated
@@ -136,9 +136,6 @@ String parameterToString(dynamic value) {
|
|||||||
if (value is SyncRequestType) {
|
if (value is SyncRequestType) {
|
||||||
return SyncRequestTypeTypeTransformer().encode(value).toString();
|
return SyncRequestTypeTypeTransformer().encode(value).toString();
|
||||||
}
|
}
|
||||||
if (value is TimeBucketSize) {
|
|
||||||
return TimeBucketSizeTypeTransformer().encode(value).toString();
|
|
||||||
}
|
|
||||||
if (value is ToneMapping) {
|
if (value is ToneMapping) {
|
||||||
return ToneMappingTypeTransformer().encode(value).toString();
|
return ToneMappingTypeTransformer().encode(value).toString();
|
||||||
}
|
}
|
||||||
|
|||||||
243
mobile/openapi/lib/model/time_bucket_asset_response_dto.dart
generated
Normal file
243
mobile/openapi/lib/model/time_bucket_asset_response_dto.dart
generated
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
//
|
||||||
|
// 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.city = const [],
|
||||||
|
this.country = const [],
|
||||||
|
this.duration = const [],
|
||||||
|
this.id = const [],
|
||||||
|
this.isArchived = const [],
|
||||||
|
this.isFavorite = const [],
|
||||||
|
this.isImage = const [],
|
||||||
|
this.isTrashed = const [],
|
||||||
|
this.livePhotoVideoId = const [],
|
||||||
|
this.localDateTime = const [],
|
||||||
|
this.ownerId = const [],
|
||||||
|
this.projectionType = const [],
|
||||||
|
this.ratio = const [],
|
||||||
|
this.stack = const [],
|
||||||
|
this.thumbhash = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
List<String?> city;
|
||||||
|
|
||||||
|
List<String?> country;
|
||||||
|
|
||||||
|
List<String?> duration;
|
||||||
|
|
||||||
|
List<String> id;
|
||||||
|
|
||||||
|
List<num> isArchived;
|
||||||
|
|
||||||
|
List<num> isFavorite;
|
||||||
|
|
||||||
|
List<num> isImage;
|
||||||
|
|
||||||
|
List<num> isTrashed;
|
||||||
|
|
||||||
|
List<String?> livePhotoVideoId;
|
||||||
|
|
||||||
|
List<String> localDateTime;
|
||||||
|
|
||||||
|
List<String> ownerId;
|
||||||
|
|
||||||
|
List<String?> projectionType;
|
||||||
|
|
||||||
|
List<num> ratio;
|
||||||
|
|
||||||
|
/// (stack ID, stack asset count) tuple
|
||||||
|
List<List<String>?> stack;
|
||||||
|
|
||||||
|
List<String?> thumbhash;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is TimeBucketAssetResponseDto &&
|
||||||
|
_deepEquality.equals(other.city, city) &&
|
||||||
|
_deepEquality.equals(other.country, country) &&
|
||||||
|
_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.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
|
||||||
|
(city.hashCode) +
|
||||||
|
(country.hashCode) +
|
||||||
|
(duration.hashCode) +
|
||||||
|
(id.hashCode) +
|
||||||
|
(isArchived.hashCode) +
|
||||||
|
(isFavorite.hashCode) +
|
||||||
|
(isImage.hashCode) +
|
||||||
|
(isTrashed.hashCode) +
|
||||||
|
(livePhotoVideoId.hashCode) +
|
||||||
|
(localDateTime.hashCode) +
|
||||||
|
(ownerId.hashCode) +
|
||||||
|
(projectionType.hashCode) +
|
||||||
|
(ratio.hashCode) +
|
||||||
|
(stack.hashCode) +
|
||||||
|
(thumbhash.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'TimeBucketAssetResponseDto[city=$city, country=$country, duration=$duration, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isImage=$isImage, isTrashed=$isTrashed, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, ownerId=$ownerId, projectionType=$projectionType, ratio=$ratio, stack=$stack, thumbhash=$thumbhash]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'city'] = this.city;
|
||||||
|
json[r'country'] = this.country;
|
||||||
|
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'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(
|
||||||
|
city: json[r'city'] is Iterable
|
||||||
|
? (json[r'city'] as Iterable).cast<String>().toList(growable: false)
|
||||||
|
: const [],
|
||||||
|
country: json[r'country'] is Iterable
|
||||||
|
? (json[r'country'] as Iterable).cast<String>().toList(growable: false)
|
||||||
|
: const [],
|
||||||
|
duration: json[r'duration'] is Iterable
|
||||||
|
? (json[r'duration'] as Iterable).cast<String>().toList(growable: false)
|
||||||
|
: const [],
|
||||||
|
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 [],
|
||||||
|
livePhotoVideoId: json[r'livePhotoVideoId'] is Iterable
|
||||||
|
? (json[r'livePhotoVideoId'] as Iterable).cast<String>().toList(growable: false)
|
||||||
|
: const [],
|
||||||
|
localDateTime: json[r'localDateTime'] is Iterable
|
||||||
|
? (json[r'localDateTime'] as Iterable).cast<String>().toList(growable: false)
|
||||||
|
: const [],
|
||||||
|
ownerId: json[r'ownerId'] is Iterable
|
||||||
|
? (json[r'ownerId'] as Iterable).cast<String>().toList(growable: false)
|
||||||
|
: const [],
|
||||||
|
projectionType: json[r'projectionType'] is Iterable
|
||||||
|
? (json[r'projectionType'] as Iterable).cast<String>().toList(growable: false)
|
||||||
|
: const [],
|
||||||
|
ratio: json[r'ratio'] is Iterable
|
||||||
|
? (json[r'ratio'] as Iterable).cast<num>().toList(growable: false)
|
||||||
|
: const [],
|
||||||
|
stack: json[r'stack'] is List
|
||||||
|
? (json[r'stack'] as List).map((e) =>
|
||||||
|
e == null ? null : (e as List).cast<String>()
|
||||||
|
).toList()
|
||||||
|
: const [],
|
||||||
|
thumbhash: json[r'thumbhash'] is Iterable
|
||||||
|
? (json[r'thumbhash'] as Iterable).cast<String>().toList(growable: false)
|
||||||
|
: const [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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>{
|
||||||
|
'city',
|
||||||
|
'country',
|
||||||
|
'duration',
|
||||||
|
'id',
|
||||||
|
'isArchived',
|
||||||
|
'isFavorite',
|
||||||
|
'isImage',
|
||||||
|
'isTrashed',
|
||||||
|
'livePhotoVideoId',
|
||||||
|
'localDateTime',
|
||||||
|
'ownerId',
|
||||||
|
'projectionType',
|
||||||
|
'ratio',
|
||||||
|
'thumbhash',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
85
mobile/openapi/lib/model/time_bucket_size.dart
generated
85
mobile/openapi/lib/model/time_bucket_size.dart
generated
@@ -1,85 +0,0 @@
|
|||||||
//
|
|
||||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
|
||||||
//
|
|
||||||
// @dart=2.18
|
|
||||||
|
|
||||||
// ignore_for_file: unused_element, unused_import
|
|
||||||
// ignore_for_file: always_put_required_named_parameters_first
|
|
||||||
// ignore_for_file: constant_identifier_names
|
|
||||||
// ignore_for_file: lines_longer_than_80_chars
|
|
||||||
|
|
||||||
part of openapi.api;
|
|
||||||
|
|
||||||
|
|
||||||
class TimeBucketSize {
|
|
||||||
/// Instantiate a new enum with the provided [value].
|
|
||||||
const TimeBucketSize._(this.value);
|
|
||||||
|
|
||||||
/// The underlying value of this enum member.
|
|
||||||
final String value;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => value;
|
|
||||||
|
|
||||||
String toJson() => value;
|
|
||||||
|
|
||||||
static const DAY = TimeBucketSize._(r'DAY');
|
|
||||||
static const MONTH = TimeBucketSize._(r'MONTH');
|
|
||||||
|
|
||||||
/// List of all possible values in this [enum][TimeBucketSize].
|
|
||||||
static const values = <TimeBucketSize>[
|
|
||||||
DAY,
|
|
||||||
MONTH,
|
|
||||||
];
|
|
||||||
|
|
||||||
static TimeBucketSize? fromJson(dynamic value) => TimeBucketSizeTypeTransformer().decode(value);
|
|
||||||
|
|
||||||
static List<TimeBucketSize> listFromJson(dynamic json, {bool growable = false,}) {
|
|
||||||
final result = <TimeBucketSize>[];
|
|
||||||
if (json is List && json.isNotEmpty) {
|
|
||||||
for (final row in json) {
|
|
||||||
final value = TimeBucketSize.fromJson(row);
|
|
||||||
if (value != null) {
|
|
||||||
result.add(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result.toList(growable: growable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Transformation class that can [encode] an instance of [TimeBucketSize] to String,
|
|
||||||
/// and [decode] dynamic data back to [TimeBucketSize].
|
|
||||||
class TimeBucketSizeTypeTransformer {
|
|
||||||
factory TimeBucketSizeTypeTransformer() => _instance ??= const TimeBucketSizeTypeTransformer._();
|
|
||||||
|
|
||||||
const TimeBucketSizeTypeTransformer._();
|
|
||||||
|
|
||||||
String encode(TimeBucketSize data) => data.value;
|
|
||||||
|
|
||||||
/// Decodes a [dynamic value][data] to a TimeBucketSize.
|
|
||||||
///
|
|
||||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
|
||||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
|
||||||
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
|
||||||
///
|
|
||||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
|
||||||
/// and users are still using an old app with the old code.
|
|
||||||
TimeBucketSize? decode(dynamic data, {bool allowNull = true}) {
|
|
||||||
if (data != null) {
|
|
||||||
switch (data) {
|
|
||||||
case r'DAY': return TimeBucketSize.DAY;
|
|
||||||
case r'MONTH': return TimeBucketSize.MONTH;
|
|
||||||
default:
|
|
||||||
if (!allowNull) {
|
|
||||||
throw ArgumentError('Unknown enum value to decode: $data');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Singleton [TimeBucketSizeTypeTransformer] instance.
|
|
||||||
static TimeBucketSizeTypeTransformer? _instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -10,9 +10,9 @@
|
|||||||
|
|
||||||
part of openapi.api;
|
part of openapi.api;
|
||||||
|
|
||||||
class TimeBucketResponseDto {
|
class TimeBucketsResponseDto {
|
||||||
/// Returns a new [TimeBucketResponseDto] instance.
|
/// Returns a new [TimeBucketsResponseDto] instance.
|
||||||
TimeBucketResponseDto({
|
TimeBucketsResponseDto({
|
||||||
required this.count,
|
required this.count,
|
||||||
required this.timeBucket,
|
required this.timeBucket,
|
||||||
});
|
});
|
||||||
@@ -22,7 +22,7 @@ class TimeBucketResponseDto {
|
|||||||
String timeBucket;
|
String timeBucket;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is TimeBucketResponseDto &&
|
bool operator ==(Object other) => identical(this, other) || other is TimeBucketsResponseDto &&
|
||||||
other.count == count &&
|
other.count == count &&
|
||||||
other.timeBucket == timeBucket;
|
other.timeBucket == timeBucket;
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ class TimeBucketResponseDto {
|
|||||||
(timeBucket.hashCode);
|
(timeBucket.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'TimeBucketResponseDto[count=$count, timeBucket=$timeBucket]';
|
String toString() => 'TimeBucketsResponseDto[count=$count, timeBucket=$timeBucket]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@@ -42,15 +42,15 @@ class TimeBucketResponseDto {
|
|||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a new [TimeBucketResponseDto] instance and imports its values from
|
/// Returns a new [TimeBucketsResponseDto] instance and imports its values from
|
||||||
/// [value] if it's a [Map], null otherwise.
|
/// [value] if it's a [Map], null otherwise.
|
||||||
// ignore: prefer_constructors_over_static_methods
|
// ignore: prefer_constructors_over_static_methods
|
||||||
static TimeBucketResponseDto? fromJson(dynamic value) {
|
static TimeBucketsResponseDto? fromJson(dynamic value) {
|
||||||
upgradeDto(value, "TimeBucketResponseDto");
|
upgradeDto(value, "TimeBucketsResponseDto");
|
||||||
if (value is Map) {
|
if (value is Map) {
|
||||||
final json = value.cast<String, dynamic>();
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
return TimeBucketResponseDto(
|
return TimeBucketsResponseDto(
|
||||||
count: mapValueOfType<int>(json, r'count')!,
|
count: mapValueOfType<int>(json, r'count')!,
|
||||||
timeBucket: mapValueOfType<String>(json, r'timeBucket')!,
|
timeBucket: mapValueOfType<String>(json, r'timeBucket')!,
|
||||||
);
|
);
|
||||||
@@ -58,11 +58,11 @@ class TimeBucketResponseDto {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<TimeBucketResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
static List<TimeBucketsResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
final result = <TimeBucketResponseDto>[];
|
final result = <TimeBucketsResponseDto>[];
|
||||||
if (json is List && json.isNotEmpty) {
|
if (json is List && json.isNotEmpty) {
|
||||||
for (final row in json) {
|
for (final row in json) {
|
||||||
final value = TimeBucketResponseDto.fromJson(row);
|
final value = TimeBucketsResponseDto.fromJson(row);
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
result.add(value);
|
result.add(value);
|
||||||
}
|
}
|
||||||
@@ -71,12 +71,12 @@ class TimeBucketResponseDto {
|
|||||||
return result.toList(growable: growable);
|
return result.toList(growable: growable);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Map<String, TimeBucketResponseDto> mapFromJson(dynamic json) {
|
static Map<String, TimeBucketsResponseDto> mapFromJson(dynamic json) {
|
||||||
final map = <String, TimeBucketResponseDto>{};
|
final map = <String, TimeBucketsResponseDto>{};
|
||||||
if (json is Map && json.isNotEmpty) {
|
if (json is Map && json.isNotEmpty) {
|
||||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
for (final entry in json.entries) {
|
for (final entry in json.entries) {
|
||||||
final value = TimeBucketResponseDto.fromJson(entry.value);
|
final value = TimeBucketsResponseDto.fromJson(entry.value);
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
map[entry.key] = value;
|
map[entry.key] = value;
|
||||||
}
|
}
|
||||||
@@ -85,14 +85,14 @@ class TimeBucketResponseDto {
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
// maps a json object with a list of TimeBucketResponseDto-objects as value to a dart map
|
// maps a json object with a list of TimeBucketsResponseDto-objects as value to a dart map
|
||||||
static Map<String, List<TimeBucketResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
static Map<String, List<TimeBucketsResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
final map = <String, List<TimeBucketResponseDto>>{};
|
final map = <String, List<TimeBucketsResponseDto>>{};
|
||||||
if (json is Map && json.isNotEmpty) {
|
if (json is Map && json.isNotEmpty) {
|
||||||
// ignore: parameter_assignments
|
// ignore: parameter_assignments
|
||||||
json = json.cast<String, dynamic>();
|
json = json.cast<String, dynamic>();
|
||||||
for (final entry in json.entries) {
|
for (final entry in json.entries) {
|
||||||
map[entry.key] = TimeBucketResponseDto.listFromJson(entry.value, growable: growable,);
|
map[entry.key] = TimeBucketsResponseDto.listFromJson(entry.value, growable: growable,);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
OPENAPI_GENERATOR_VERSION=v7.8.0
|
OPENAPI_GENERATOR_VERSION=v7.12.0
|
||||||
|
|
||||||
# usage: ./bin/generate-open-api.sh
|
# usage: ./bin/generate-open-api.sh
|
||||||
|
|
||||||
@@ -8,6 +8,7 @@ function dart {
|
|||||||
cd ./templates/mobile/serialization/native
|
cd ./templates/mobile/serialization/native
|
||||||
wget -O native_class.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/$OPENAPI_GENERATOR_VERSION/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache
|
wget -O native_class.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/$OPENAPI_GENERATOR_VERSION/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache
|
||||||
patch --no-backup-if-mismatch -u native_class.mustache <native_class.mustache.patch
|
patch --no-backup-if-mismatch -u native_class.mustache <native_class.mustache.patch
|
||||||
|
patch --no-backup-if-mismatch -u native_class.mustache <native_class_nullable_items_in_arrays.patch
|
||||||
|
|
||||||
cd ../../
|
cd ../../
|
||||||
wget -O api.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/$OPENAPI_GENERATOR_VERSION/modules/openapi-generator/src/main/resources/dart2/api.mustache
|
wget -O api.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/$OPENAPI_GENERATOR_VERSION/modules/openapi-generator/src/main/resources/dart2/api.mustache
|
||||||
|
|||||||
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}}
|
||||||
@@ -6949,6 +6949,24 @@
|
|||||||
"$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": {
|
||||||
|
"minimum": 1,
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "personId",
|
"name": "personId",
|
||||||
"required": false,
|
"required": false,
|
||||||
@@ -6958,14 +6976,6 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "size",
|
|
||||||
"required": true,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/TimeBucketSize"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "tagId",
|
"name": "tagId",
|
||||||
"required": false,
|
"required": false,
|
||||||
@@ -7014,10 +7024,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"items": {
|
"$ref": "#/components/schemas/TimeBucketAssetResponseDto"
|
||||||
"$ref": "#/components/schemas/AssetResponseDto"
|
|
||||||
},
|
|
||||||
"type": "array"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -7102,14 +7109,6 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "size",
|
|
||||||
"required": true,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/TimeBucketSize"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "tagId",
|
"name": "tagId",
|
||||||
"required": false,
|
"required": false,
|
||||||
@@ -7151,7 +7150,7 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/components/schemas/TimeBucketResponseDto"
|
"$ref": "#/components/schemas/TimeBucketsResponseDto"
|
||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array"
|
||||||
}
|
}
|
||||||
@@ -13561,7 +13560,131 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"TimeBucketResponseDto": {
|
"TimeBucketAssetResponseDto": {
|
||||||
|
"properties": {
|
||||||
|
"city": {
|
||||||
|
"items": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"country": {
|
||||||
|
"items": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"duration": {
|
||||||
|
"items": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"isArchived": {
|
||||||
|
"items": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"isFavorite": {
|
||||||
|
"items": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"isImage": {
|
||||||
|
"items": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"isTrashed": {
|
||||||
|
"items": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"livePhotoVideoId": {
|
||||||
|
"items": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"localDateTime": {
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"ownerId": {
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"projectionType": {
|
||||||
|
"items": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"ratio": {
|
||||||
|
"items": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"stack": {
|
||||||
|
"description": "(stack ID, stack asset count) tuple",
|
||||||
|
"items": {
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"maxItems": 2,
|
||||||
|
"minItems": 2,
|
||||||
|
"nullable": true,
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"thumbhash": {
|
||||||
|
"items": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"city",
|
||||||
|
"country",
|
||||||
|
"duration",
|
||||||
|
"id",
|
||||||
|
"isArchived",
|
||||||
|
"isFavorite",
|
||||||
|
"isImage",
|
||||||
|
"isTrashed",
|
||||||
|
"livePhotoVideoId",
|
||||||
|
"localDateTime",
|
||||||
|
"ownerId",
|
||||||
|
"projectionType",
|
||||||
|
"ratio",
|
||||||
|
"thumbhash"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"TimeBucketsResponseDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"count": {
|
"count": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
@@ -13576,13 +13699,6 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"TimeBucketSize": {
|
|
||||||
"enum": [
|
|
||||||
"DAY",
|
|
||||||
"MONTH"
|
|
||||||
],
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"ToneMapping": {
|
"ToneMapping": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"hable",
|
"hable",
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class {{{classname}}} {
|
|||||||
{{/required}}
|
{{/required}}
|
||||||
{{/isNullable}}
|
{{/isNullable}}
|
||||||
{{/isEnum}}
|
{{/isEnum}}
|
||||||
{{{datatypeWithEnum}}}{{#isNullable}}?{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}?{{/defaultValue}}{{/required}}{{/isNullable}} {{{name}}};
|
{{#isArray}}{{#uniqueItems}}Set{{/uniqueItems}}{{^uniqueItems}}List{{/uniqueItems}}<{{{items.dataType}}}{{#items.isNullable}}?{{/items.isNullable}}>{{/isArray}}{{^isArray}}{{{datatypeWithEnum}}}{{/isArray}}{{#isNullable}}?{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}?{{/defaultValue}}{{/required}}{{/isNullable}} {{{name}}};
|
||||||
|
|
||||||
{{/vars}}
|
{{/vars}}
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
diff --git a/open-api/templates/mobile/serialization/native/native_class.mustache b/open-api/templates/mobile/serialization/native/native_class.mustache
|
||||||
|
index 9a7b1439b..9f40d5b0b 100644
|
||||||
|
--- a/open-api/templates/mobile/serialization/native/native_class.mustache
|
||||||
|
+++ b/open-api/templates/mobile/serialization/native/native_class.mustache
|
||||||
|
@@ -32,7 +32,7 @@ class {{{classname}}} {
|
||||||
|
{{/required}}
|
||||||
|
{{/isNullable}}
|
||||||
|
{{/isEnum}}
|
||||||
|
- {{{datatypeWithEnum}}}{{#isNullable}}?{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}?{{/defaultValue}}{{/required}}{{/isNullable}} {{{name}}};
|
||||||
|
+ {{#isArray}}{{#uniqueItems}}Set{{/uniqueItems}}{{^uniqueItems}}List{{/uniqueItems}}<{{{items.dataType}}}{{#items.isNullable}}?{{/items.isNullable}}>{{/isArray}}{{^isArray}}{{{datatypeWithEnum}}}{{/isArray}}{{#isNullable}}?{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}?{{/defaultValue}}{{/required}}{{/isNullable}} {{{name}}};
|
||||||
|
|
||||||
|
{{/vars}}
|
||||||
|
@override
|
||||||
@@ -1384,7 +1384,25 @@ export type TagBulkAssetsResponseDto = {
|
|||||||
export type TagUpdateDto = {
|
export type TagUpdateDto = {
|
||||||
color?: string | null;
|
color?: string | null;
|
||||||
};
|
};
|
||||||
export type TimeBucketResponseDto = {
|
export type TimeBucketAssetResponseDto = {
|
||||||
|
city: (string | null)[];
|
||||||
|
country: (string | null)[];
|
||||||
|
duration: (string | null)[];
|
||||||
|
id: string[];
|
||||||
|
isArchived: number[];
|
||||||
|
isFavorite: number[];
|
||||||
|
isImage: number[];
|
||||||
|
isTrashed: number[];
|
||||||
|
livePhotoVideoId: (string | null)[];
|
||||||
|
localDateTime: string[];
|
||||||
|
ownerId: string[];
|
||||||
|
projectionType: (string | null)[];
|
||||||
|
ratio: number[];
|
||||||
|
/** (stack ID, stack asset count) tuple */
|
||||||
|
stack?: (string[] | null)[];
|
||||||
|
thumbhash: (string | null)[];
|
||||||
|
};
|
||||||
|
export type TimeBucketsResponseDto = {
|
||||||
count: number;
|
count: number;
|
||||||
timeBucket: string;
|
timeBucket: string;
|
||||||
};
|
};
|
||||||
@@ -3242,15 +3260,16 @@ 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, 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;
|
|
||||||
tagId?: string;
|
tagId?: string;
|
||||||
timeBucket: string;
|
timeBucket: string;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
@@ -3259,7 +3278,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: TimeBucketAssetResponseDto;
|
||||||
}>(`/timeline/bucket${QS.query(QS.explode({
|
}>(`/timeline/bucket${QS.query(QS.explode({
|
||||||
albumId,
|
albumId,
|
||||||
isArchived,
|
isArchived,
|
||||||
@@ -3267,8 +3286,9 @@ export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key,
|
|||||||
isTrashed,
|
isTrashed,
|
||||||
key,
|
key,
|
||||||
order,
|
order,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
personId,
|
personId,
|
||||||
size,
|
|
||||||
tagId,
|
tagId,
|
||||||
timeBucket,
|
timeBucket,
|
||||||
userId,
|
userId,
|
||||||
@@ -3278,7 +3298,7 @@ export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key,
|
|||||||
...opts
|
...opts
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, tagId, userId, withPartners, withStacked }: {
|
export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, tagId, userId, withPartners, withStacked }: {
|
||||||
albumId?: string;
|
albumId?: string;
|
||||||
isArchived?: boolean;
|
isArchived?: boolean;
|
||||||
isFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
@@ -3286,7 +3306,6 @@ export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key
|
|||||||
key?: string;
|
key?: string;
|
||||||
order?: AssetOrder;
|
order?: AssetOrder;
|
||||||
personId?: string;
|
personId?: string;
|
||||||
size: TimeBucketSize;
|
|
||||||
tagId?: string;
|
tagId?: string;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
withPartners?: boolean;
|
withPartners?: boolean;
|
||||||
@@ -3294,7 +3313,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,
|
||||||
@@ -3303,7 +3322,6 @@ export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key
|
|||||||
key,
|
key,
|
||||||
order,
|
order,
|
||||||
personId,
|
personId,
|
||||||
size,
|
|
||||||
tagId,
|
tagId,
|
||||||
userId,
|
userId,
|
||||||
withPartners,
|
withPartners,
|
||||||
@@ -3782,7 +3800,3 @@ export enum OAuthTokenEndpointAuthMethod {
|
|||||||
ClientSecretPost = "client_secret_post",
|
ClientSecretPost = "client_secret_post",
|
||||||
ClientSecretBasic = "client_secret_basic"
|
ClientSecretBasic = "client_secret_basic"
|
||||||
}
|
}
|
||||||
export enum TimeBucketSize {
|
|
||||||
Day = "DAY",
|
|
||||||
Month = "MONTH"
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -72,7 +72,9 @@ class SqlGenerator {
|
|||||||
await rm(this.options.targetDir, { force: true, recursive: true });
|
await rm(this.options.targetDir, { force: true, recursive: true });
|
||||||
await mkdir(this.options.targetDir);
|
await mkdir(this.options.targetDir);
|
||||||
|
|
||||||
process.env.DB_HOSTNAME = 'localhost';
|
if (!process.env.DB_HOSTNAME) {
|
||||||
|
process.env.DB_HOSTNAME = 'localhost';
|
||||||
|
}
|
||||||
const { database, cls, otel } = new ConfigRepository().getEnv();
|
const { database, cls, otel } = new ConfigRepository().getEnv();
|
||||||
|
|
||||||
const moduleFixture = await Test.createTestingModule({
|
const moduleFixture = await Test.createTestingModule({
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Controller, Get, Query } from '@nestjs/common';
|
import { Controller, Get, Query, Res } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
import { Response } from 'express';
|
||||||
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, TimeBucketAssetResponseDto, 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 +14,19 @@ 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[]> {
|
async getTimeBucket(
|
||||||
return this.service.getTimeBucket(auth, dto) as Promise<AssetResponseDto[]>;
|
@Auth() auth: AuthDto,
|
||||||
|
@Query() dto: TimeBucketAssetDto,
|
||||||
|
@Res({ passthrough: true }) res: Response,
|
||||||
|
): Promise<TimeBucketAssetResponseDto> {
|
||||||
|
res.contentType('application/json');
|
||||||
|
const jsonBucket = await this.service.getTimeBucket(auth, dto);
|
||||||
|
return jsonBucket as unknown as TimeBucketAssetResponseDto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,6 +165,12 @@ export type Stack = {
|
|||||||
assetCount?: number;
|
assetCount?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TimelineStack = {
|
||||||
|
id: string;
|
||||||
|
primaryAssetId: string;
|
||||||
|
assetCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type AuthSharedLink = {
|
export type AuthSharedLink = {
|
||||||
id: string;
|
id: string;
|
||||||
expiresAt: Date | null;
|
expiresAt: Date | null;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import { TagResponseDto, mapTag } from 'src/dtos/tag.dto';
|
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 { AssetStatus, AssetType } from 'src/enum';
|
import { AssetStatus, 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 {
|
||||||
@@ -140,15 +141,6 @@ const mapStack = (entity: { stack?: Stack | null }) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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: MapAsset, options: AssetMapOptions = {}): AssetResponseDto {
|
export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): AssetResponseDto {
|
||||||
const { stripMetadata = false, withStack = false } = options;
|
const { stripMetadata = false, withStack = false } = options;
|
||||||
|
|
||||||
@@ -191,7 +183,7 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
|
|||||||
tags: entity.tags?.map((tag) => mapTag(tag)),
|
tags: entity.tags?.map((tag) => mapTag(tag)),
|
||||||
people: peopleWithFaces(entity.faces),
|
people: peopleWithFaces(entity.faces),
|
||||||
unassignedFaces: entity.faces?.filter((face) => !face.person).map((a) => mapFacesWithoutPerson(a)),
|
unassignedFaces: entity.faces?.filter((face) => !face.person).map((a) => mapFacesWithoutPerson(a)),
|
||||||
checksum: hexOrBufferToBase64(entity.checksum),
|
checksum: hexOrBufferToBase64(entity.checksum)!,
|
||||||
stack: withStack ? mapStack(entity) : undefined,
|
stack: withStack ? mapStack(entity) : undefined,
|
||||||
isOffline: entity.isOffline,
|
isOffline: entity.isOffline,
|
||||||
hasMetadata: true,
|
hasMetadata: true,
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
|
||||||
|
import { IsEnum, IsInt, IsString, Min } from 'class-validator';
|
||||||
import { AssetOrder } from 'src/enum';
|
import { AssetOrder } from 'src/enum';
|
||||||
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 {
|
||||||
@IsNotEmpty()
|
|
||||||
@IsEnum(TimeBucketSize)
|
|
||||||
@ApiProperty({ enum: TimeBucketSize, enumName: 'TimeBucketSize' })
|
|
||||||
size!: TimeBucketSize;
|
|
||||||
|
|
||||||
@ValidateUUID({ optional: true })
|
@ValidateUUID({ optional: true })
|
||||||
userId?: string;
|
userId?: string;
|
||||||
|
|
||||||
@@ -46,9 +42,75 @@ 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()
|
||||||
|
@Min(1)
|
||||||
|
@Optional()
|
||||||
|
pageSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TimeBucketResponseDto {
|
export class TimelineStackResponseDto implements TimelineStack {
|
||||||
|
id!: string;
|
||||||
|
primaryAssetId!: string;
|
||||||
|
assetCount!: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TimeBucketAssetResponseDto implements TimeBucketAssets {
|
||||||
|
id!: string[];
|
||||||
|
|
||||||
|
ownerId!: string[];
|
||||||
|
|
||||||
|
ratio!: number[];
|
||||||
|
|
||||||
|
isFavorite!: number[];
|
||||||
|
|
||||||
|
isArchived!: number[];
|
||||||
|
|
||||||
|
isTrashed!: number[];
|
||||||
|
|
||||||
|
isImage!: number[];
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'array', items: { type: 'string', nullable: true } })
|
||||||
|
thumbhash!: (string | null)[];
|
||||||
|
|
||||||
|
localDateTime!: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'array', items: { type: 'string', nullable: true } })
|
||||||
|
duration!: (string | null)[];
|
||||||
|
|
||||||
|
// id, count
|
||||||
|
@ApiProperty({
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
minItems: 2,
|
||||||
|
maxItems: 2,
|
||||||
|
nullable: true,
|
||||||
|
},
|
||||||
|
description: '(stack ID, stack asset count) tuple',
|
||||||
|
})
|
||||||
|
stack?: ([string, string] | null)[];
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'array', items: { type: 'string', nullable: true } })
|
||||||
|
projectionType!: (string | null)[];
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'array', items: { type: 'string', nullable: true } })
|
||||||
|
livePhotoVideoId!: (string | null)[];
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'array', items: { type: 'string', nullable: true } })
|
||||||
|
city!: (string | null)[];
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'array', items: { type: 'string', nullable: true } })
|
||||||
|
country!: (string | null)[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TimeBucketsResponseDto {
|
||||||
@ApiProperty({ type: 'string' })
|
@ApiProperty({ type: 'string' })
|
||||||
timeBucket!: string;
|
timeBucket!: string;
|
||||||
|
|
||||||
|
|||||||
@@ -236,12 +236,12 @@ limit
|
|||||||
with
|
with
|
||||||
"assets" as (
|
"assets" as (
|
||||||
select
|
select
|
||||||
date_trunc($1, "localDateTime" at time zone 'UTC') at time zone 'UTC' as "timeBucket"
|
date_trunc('MONTH', "localDateTime" at time zone 'UTC') at time zone 'UTC' as "timeBucket"
|
||||||
from
|
from
|
||||||
"assets"
|
"assets"
|
||||||
where
|
where
|
||||||
"assets"."deletedAt" is null
|
"assets"."deletedAt" is null
|
||||||
and "assets"."isVisible" = $2
|
and "assets"."isVisible" = $1
|
||||||
)
|
)
|
||||||
select
|
select
|
||||||
"timeBucket",
|
"timeBucket",
|
||||||
@@ -254,37 +254,95 @@ order by
|
|||||||
"timeBucket" desc
|
"timeBucket" desc
|
||||||
|
|
||||||
-- AssetRepository.getTimeBucket
|
-- AssetRepository.getTimeBucket
|
||||||
select
|
with
|
||||||
"assets".*,
|
"cte" as (
|
||||||
to_json("exif") as "exifInfo",
|
|
||||||
to_json("stacked_assets") as "stack"
|
|
||||||
from
|
|
||||||
"assets"
|
|
||||||
left join "exif" on "assets"."id" = "exif"."assetId"
|
|
||||||
left join "asset_stack" on "asset_stack"."id" = "assets"."stackId"
|
|
||||||
left join lateral (
|
|
||||||
select
|
select
|
||||||
"asset_stack".*,
|
"assets"."duration",
|
||||||
count("stacked") as "assetCount"
|
"assets"."id",
|
||||||
|
assets."isArchived"::int as "isArchived",
|
||||||
|
assets."isFavorite"::int as "isFavorite",
|
||||||
|
(assets.type = 'IMAGE')::int as "isImage",
|
||||||
|
(assets."deletedAt" is null)::int as "isTrashed",
|
||||||
|
(assets.type = 'VIDEO')::int as "isVideo",
|
||||||
|
"assets"."livePhotoVideoId",
|
||||||
|
"assets"."localDateTime",
|
||||||
|
"assets"."ownerId",
|
||||||
|
"assets"."status",
|
||||||
|
encode("assets"."thumbhash", 'base64') as "thumbhash",
|
||||||
|
"exif"."city",
|
||||||
|
"exif"."country",
|
||||||
|
"exif"."projectionType",
|
||||||
|
coalesce(
|
||||||
|
case
|
||||||
|
when exif."exifImageHeight" = 0
|
||||||
|
or exif."exifImageWidth" = 0 then 1
|
||||||
|
when "exif"."orientation" in ('5', '6', '7', '8', '-90', '90') then round(
|
||||||
|
exif."exifImageHeight"::numeric / exif."exifImageWidth"::numeric,
|
||||||
|
3
|
||||||
|
)
|
||||||
|
else round(
|
||||||
|
exif."exifImageWidth"::numeric / exif."exifImageHeight"::numeric,
|
||||||
|
3
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
1
|
||||||
|
) as "ratio",
|
||||||
|
"stack"
|
||||||
from
|
from
|
||||||
"assets" as "stacked"
|
"assets"
|
||||||
|
inner join "exif" on "assets"."id" = "exif"."assetId"
|
||||||
|
left join lateral (
|
||||||
|
select
|
||||||
|
array[stacked."stackId"::text, count('stacked')::text] as "stack"
|
||||||
|
from
|
||||||
|
"assets" as "stacked"
|
||||||
|
where
|
||||||
|
"stacked"."stackId" = "assets"."stackId"
|
||||||
|
and "stacked"."deletedAt" is null
|
||||||
|
and "stacked"."isArchived" = $1
|
||||||
|
group by
|
||||||
|
"stacked"."stackId"
|
||||||
|
) as "stacked_assets" on true
|
||||||
where
|
where
|
||||||
"stacked"."stackId" = "asset_stack"."id"
|
"assets"."deletedAt" is null
|
||||||
and "stacked"."deletedAt" is null
|
and "assets"."isVisible" = $2
|
||||||
and "stacked"."isArchived" = $1
|
and date_trunc('MONTH', "localDateTime" at time zone 'UTC') at time zone 'UTC' = $3
|
||||||
group by
|
and not exists (
|
||||||
"asset_stack"."id"
|
select
|
||||||
) as "stacked_assets" on "asset_stack"."id" is not null
|
from
|
||||||
where
|
"asset_stack"
|
||||||
(
|
where
|
||||||
"asset_stack"."primaryAssetId" = "assets"."id"
|
"asset_stack"."id" = "assets"."stackId"
|
||||||
or "assets"."stackId" is null
|
and "asset_stack"."primaryAssetId" != "assets"."id"
|
||||||
|
)
|
||||||
|
order by
|
||||||
|
"assets"."localDateTime" desc
|
||||||
|
),
|
||||||
|
"agg" as (
|
||||||
|
select
|
||||||
|
coalesce(array_agg("city"), '{}') as "city",
|
||||||
|
coalesce(array_agg("country"), '{}') as "country",
|
||||||
|
coalesce(array_agg("duration"), '{}') as "duration",
|
||||||
|
coalesce(array_agg("id"), '{}') as "id",
|
||||||
|
coalesce(array_agg("isArchived"), '{}') as "isArchived",
|
||||||
|
coalesce(array_agg("isFavorite"), '{}') as "isFavorite",
|
||||||
|
coalesce(array_agg("isImage"), '{}') as "isImage",
|
||||||
|
coalesce(array_agg("isTrashed"), '{}') as "isTrashed",
|
||||||
|
coalesce(array_agg("livePhotoVideoId"), '{}') as "livePhotoVideoId",
|
||||||
|
coalesce(array_agg("localDateTime"), '{}') as "localDateTime",
|
||||||
|
coalesce(array_agg("ownerId"), '{}') as "ownerId",
|
||||||
|
coalesce(array_agg("projectionType"), '{}') as "projectionType",
|
||||||
|
coalesce(array_agg("ratio"), '{}') as "ratio",
|
||||||
|
coalesce(array_agg("status"), '{}') as "status",
|
||||||
|
coalesce(array_agg("thumbhash"), '{}') as "thumbhash",
|
||||||
|
coalesce(json_agg("stack"), '[]') as "stack"
|
||||||
|
from
|
||||||
|
"cte"
|
||||||
)
|
)
|
||||||
and "assets"."deletedAt" is null
|
select
|
||||||
and "assets"."isVisible" = $2
|
to_json(agg)::text as "assets"
|
||||||
and date_trunc($3, "localDateTime" at time zone 'UTC') at time zone 'UTC' = $4
|
from
|
||||||
order by
|
"agg"
|
||||||
"assets"."localDateTime" desc
|
|
||||||
|
|
||||||
-- AssetRepository.getDuplicates
|
-- AssetRepository.getDuplicates
|
||||||
with
|
with
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ export interface AssetBuilderOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface TimeBucketOptions extends AssetBuilderOptions {
|
export interface TimeBucketOptions extends AssetBuilderOptions {
|
||||||
size: TimeBucketSize;
|
|
||||||
order?: AssetOrder;
|
order?: AssetOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,7 +538,7 @@ export class AssetRepository {
|
|||||||
.with('assets', (qb) =>
|
.with('assets', (qb) =>
|
||||||
qb
|
qb
|
||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
.select(truncatedDate<Date>(options.size).as('timeBucket'))
|
.select(truncatedDate<Date>(TimeBucketSize.MONTH).as('timeBucket'))
|
||||||
.$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED))
|
.$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED))
|
||||||
.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)
|
||||||
@@ -581,53 +580,125 @@ export class AssetRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.TIME_BUCKET, { size: TimeBucketSize.MONTH, withStacked: true }] })
|
@GenerateSql({
|
||||||
async getTimeBucket(timeBucket: string, options: TimeBucketOptions) {
|
params: [DummyValue.TIME_BUCKET, { size: TimeBucketSize.MONTH, withStacked: true }, { skip: 0, take: 1000 }],
|
||||||
return this.db
|
})
|
||||||
.selectFrom('assets')
|
getTimeBucket(timeBucket: string, options: TimeBucketOptions) {
|
||||||
.selectAll('assets')
|
const query = this.db
|
||||||
.$call(withExif)
|
.with('cte', (qb) =>
|
||||||
.$if(!!options.albumId, (qb) =>
|
|
||||||
qb
|
qb
|
||||||
.innerJoin('albums_assets_assets', 'albums_assets_assets.assetsId', 'assets.id')
|
.selectFrom('assets')
|
||||||
.where('albums_assets_assets.albumsId', '=', options.albumId!),
|
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
||||||
|
.select((eb) => [
|
||||||
|
'assets.duration',
|
||||||
|
'assets.id',
|
||||||
|
sql`assets."isArchived"::int`.as('isArchived'),
|
||||||
|
sql`assets."isFavorite"::int`.as('isFavorite'),
|
||||||
|
sql`(assets.type = 'IMAGE')::int`.as('isImage'),
|
||||||
|
sql`(assets."deletedAt" is null)::int`.as('isTrashed'),
|
||||||
|
sql`(assets.type = 'VIDEO')::int`.as('isVideo'),
|
||||||
|
'assets.livePhotoVideoId',
|
||||||
|
'assets.localDateTime',
|
||||||
|
'assets.ownerId',
|
||||||
|
'assets.status',
|
||||||
|
eb.fn('encode', ['assets.thumbhash', sql.lit('base64')]).as('thumbhash'),
|
||||||
|
'exif.city',
|
||||||
|
'exif.country',
|
||||||
|
'exif.projectionType',
|
||||||
|
eb.fn
|
||||||
|
.coalesce(
|
||||||
|
eb
|
||||||
|
.case()
|
||||||
|
.when(sql`exif."exifImageHeight" = 0 or exif."exifImageWidth" = 0`)
|
||||||
|
.then(eb.lit(1))
|
||||||
|
.when('exif.orientation', 'in', sql<string>`('5', '6', '7', '8', '-90', '90')`)
|
||||||
|
.then(sql`round(exif."exifImageHeight"::numeric / exif."exifImageWidth"::numeric, 3)`)
|
||||||
|
.else(sql`round(exif."exifImageWidth"::numeric / exif."exifImageHeight"::numeric, 3)`)
|
||||||
|
.end(),
|
||||||
|
eb.lit(1),
|
||||||
|
)
|
||||||
|
.as('ratio'),
|
||||||
|
])
|
||||||
|
.where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null)
|
||||||
|
.where('assets.isVisible', '=', true)
|
||||||
|
.where(truncatedDate(TimeBucketSize.MONTH), '=', timeBucket.replace(/^[+-]/, ''))
|
||||||
|
.$if(!!options.albumId, (qb) =>
|
||||||
|
qb.where((eb) =>
|
||||||
|
eb.exists(
|
||||||
|
eb
|
||||||
|
.selectFrom('albums_assets_assets')
|
||||||
|
.whereRef('albums_assets_assets.assetsId', '=', 'assets.id')
|
||||||
|
.where('albums_assets_assets.albumsId', '=', asUuid(options.albumId!)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.$if(!!options.personId, (qb) => hasPeople(qb, [options.personId!]))
|
||||||
|
.$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!)))
|
||||||
|
.$if(options.isArchived !== undefined, (qb) => qb.where('assets.isArchived', '=', options.isArchived!))
|
||||||
|
.$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!))
|
||||||
|
.$if(!!options.withStacked, (qb) =>
|
||||||
|
qb
|
||||||
|
.where((eb) =>
|
||||||
|
eb.not(
|
||||||
|
eb.exists(
|
||||||
|
eb
|
||||||
|
.selectFrom('asset_stack')
|
||||||
|
.whereRef('asset_stack.id', '=', 'assets.stackId')
|
||||||
|
.whereRef('asset_stack.primaryAssetId', '!=', 'assets.id'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.leftJoinLateral(
|
||||||
|
(eb) =>
|
||||||
|
eb
|
||||||
|
.selectFrom('assets as stacked')
|
||||||
|
.select(sql`array[stacked."stackId"::text, count('stacked')::text]`.as('stack'))
|
||||||
|
.whereRef('stacked.stackId', '=', 'assets.stackId')
|
||||||
|
.where('stacked.deletedAt', 'is', null)
|
||||||
|
.where('stacked.isArchived', '=', false)
|
||||||
|
.groupBy('stacked.stackId')
|
||||||
|
.as('stacked_assets'),
|
||||||
|
(join) => join.onTrue(),
|
||||||
|
)
|
||||||
|
.select('stack'),
|
||||||
|
)
|
||||||
|
.$if(!!options.assetType, (qb) => qb.where('assets.type', '=', options.assetType!))
|
||||||
|
.$if(options.isDuplicate !== undefined, (qb) =>
|
||||||
|
qb.where('assets.duplicateId', options.isDuplicate ? 'is not' : 'is', null),
|
||||||
|
)
|
||||||
|
.$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED))
|
||||||
|
.$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!))
|
||||||
|
.orderBy('assets.localDateTime', options.order ?? 'desc'),
|
||||||
)
|
)
|
||||||
.$if(!!options.personId, (qb) => hasPeople(qb, [options.personId!]))
|
.with('agg', (qb) =>
|
||||||
.$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!)))
|
|
||||||
.$if(options.isArchived !== undefined, (qb) => qb.where('assets.isArchived', '=', options.isArchived!))
|
|
||||||
.$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!))
|
|
||||||
.$if(!!options.withStacked, (qb) =>
|
|
||||||
qb
|
qb
|
||||||
.leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId')
|
.selectFrom('cte')
|
||||||
.where((eb) =>
|
.select((eb) => [
|
||||||
eb.or([eb('asset_stack.primaryAssetId', '=', eb.ref('assets.id')), eb('assets.stackId', 'is', null)]),
|
eb.fn.coalesce(eb.fn('array_agg', ['city']), sql.lit('{}')).as('city'),
|
||||||
)
|
eb.fn.coalesce(eb.fn('array_agg', ['country']), sql.lit('{}')).as('country'),
|
||||||
.leftJoinLateral(
|
eb.fn.coalesce(eb.fn('array_agg', ['duration']), sql.lit('{}')).as('duration'),
|
||||||
(eb) =>
|
eb.fn.coalesce(eb.fn('array_agg', ['id']), sql.lit('{}')).as('id'),
|
||||||
eb
|
eb.fn.coalesce(eb.fn('array_agg', ['isArchived']), sql.lit('{}')).as('isArchived'),
|
||||||
.selectFrom('assets as stacked')
|
eb.fn.coalesce(eb.fn('array_agg', ['isFavorite']), sql.lit('{}')).as('isFavorite'),
|
||||||
.selectAll('asset_stack')
|
eb.fn.coalesce(eb.fn('array_agg', ['isImage']), sql.lit('{}')).as('isImage'),
|
||||||
.select((eb) => eb.fn.count(eb.table('stacked')).as('assetCount'))
|
// TODO: isTrashed is redundant as it will always be all 0s or 1s depending on the options
|
||||||
.whereRef('stacked.stackId', '=', 'asset_stack.id')
|
eb.fn.coalesce(eb.fn('array_agg', ['isTrashed']), sql.lit('{}')).as('isTrashed'),
|
||||||
.where('stacked.deletedAt', 'is', null)
|
eb.fn.coalesce(eb.fn('array_agg', ['livePhotoVideoId']), sql.lit('{}')).as('livePhotoVideoId'),
|
||||||
.where('stacked.isArchived', '=', false)
|
eb.fn.coalesce(eb.fn('array_agg', ['localDateTime']), sql.lit('{}')).as('localDateTime'),
|
||||||
.groupBy('asset_stack.id')
|
eb.fn.coalesce(eb.fn('array_agg', ['ownerId']), sql.lit('{}')).as('ownerId'),
|
||||||
.as('stacked_assets'),
|
eb.fn.coalesce(eb.fn('array_agg', ['projectionType']), sql.lit('{}')).as('projectionType'),
|
||||||
(join) => join.on('asset_stack.id', 'is not', null),
|
eb.fn.coalesce(eb.fn('array_agg', ['ratio']), sql.lit('{}')).as('ratio'),
|
||||||
)
|
eb.fn.coalesce(eb.fn('array_agg', ['status']), sql.lit('{}')).as('status'),
|
||||||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets').$castTo<Stack | null>()).as('stack')),
|
eb.fn.coalesce(eb.fn('array_agg', ['thumbhash']), sql.lit('{}')).as('thumbhash'),
|
||||||
|
])
|
||||||
|
.$if(!!options.withStacked, (qb) =>
|
||||||
|
qb.select((eb) => eb.fn.coalesce(eb.fn('json_agg', ['stack']), sql.lit('[]')).as('stack')),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.$if(!!options.assetType, (qb) => qb.where('assets.type', '=', options.assetType!))
|
.selectFrom('agg')
|
||||||
.$if(options.isDuplicate !== undefined, (qb) =>
|
.select(sql<string>`to_json(agg)::text`.as('assets'));
|
||||||
qb.where('assets.duplicateId', options.isDuplicate ? 'is not' : 'is', null),
|
|
||||||
)
|
return query.executeTakeFirstOrThrow();
|
||||||
.$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED))
|
|
||||||
.$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!))
|
|
||||||
.where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null)
|
|
||||||
.where('assets.isVisible', '=', true)
|
|
||||||
.where(truncatedDate(options.size), '=', timeBucket.replace(/^[+-]/, ''))
|
|
||||||
.orderBy('assets.localDateTime', options.order ?? 'desc')
|
|
||||||
.execute();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@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';
|
||||||
|
|
||||||
@@ -141,7 +142,7 @@ export class SyncService extends BaseService {
|
|||||||
updateId,
|
updateId,
|
||||||
data: {
|
data: {
|
||||||
...data,
|
...data,
|
||||||
checksum: hexOrBufferToBase64(checksum),
|
checksum: hexOrBufferToBase64(checksum)!,
|
||||||
thumbhash: thumbhash ? hexOrBufferToBase64(thumbhash) : null,
|
thumbhash: thumbhash ? hexOrBufferToBase64(thumbhash) : null,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -171,7 +172,7 @@ export class SyncService extends BaseService {
|
|||||||
updateId,
|
updateId,
|
||||||
data: {
|
data: {
|
||||||
...data,
|
...data,
|
||||||
checksum: hexOrBufferToBase64(checksum),
|
checksum: hexOrBufferToBase64(checksum)!,
|
||||||
thumbhash: thumbhash ? hexOrBufferToBase64(thumbhash) : null,
|
thumbhash: thumbhash ? hexOrBufferToBase64(thumbhash) : null,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { BadRequestException } from '@nestjs/common';
|
import { BadRequestException } from '@nestjs/common';
|
||||||
import { TimeBucketSize } from 'src/repositories/asset.repository';
|
|
||||||
import { TimelineService } from 'src/services/timeline.service';
|
import { TimelineService } from 'src/services/timeline.service';
|
||||||
import { assetStub } from 'test/fixtures/asset.stub';
|
|
||||||
import { authStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
import { factory } from 'test/small.factory';
|
|
||||||
import { newTestService, ServiceMocks } from 'test/utils';
|
import { newTestService, ServiceMocks } from 'test/utils';
|
||||||
|
|
||||||
describe(TimelineService.name, () => {
|
describe(TimelineService.name, () => {
|
||||||
@@ -18,13 +15,10 @@ describe(TimelineService.name, () => {
|
|||||||
it("should return buckets if userId and albumId aren't set", async () => {
|
it("should return buckets if userId and albumId aren't set", async () => {
|
||||||
mocks.asset.getTimeBuckets.mockResolvedValue([{ timeBucket: 'bucket', count: 1 }]);
|
mocks.asset.getTimeBuckets.mockResolvedValue([{ timeBucket: 'bucket', count: 1 }]);
|
||||||
|
|
||||||
await expect(
|
await expect(sut.getTimeBuckets(authStub.admin, {})).resolves.toEqual(
|
||||||
sut.getTimeBuckets(authStub.admin, {
|
expect.arrayContaining([{ timeBucket: 'bucket', count: 1 }]),
|
||||||
size: TimeBucketSize.DAY,
|
);
|
||||||
}),
|
|
||||||
).resolves.toEqual(expect.arrayContaining([{ timeBucket: 'bucket', count: 1 }]));
|
|
||||||
expect(mocks.asset.getTimeBuckets).toHaveBeenCalledWith({
|
expect(mocks.asset.getTimeBuckets).toHaveBeenCalledWith({
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
userIds: [authStub.admin.user.id],
|
userIds: [authStub.admin.user.id],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -33,35 +27,34 @@ describe(TimelineService.name, () => {
|
|||||||
describe('getTimeBucket', () => {
|
describe('getTimeBucket', () => {
|
||||||
it('should return the assets for a album time bucket if user has album.read', async () => {
|
it('should return the assets for a album time bucket if user has album.read', async () => {
|
||||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id']));
|
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id']));
|
||||||
mocks.asset.getTimeBucket.mockResolvedValue([assetStub.image]);
|
const json = `[{ id: ['asset-id'] }]`;
|
||||||
|
mocks.asset.getTimeBucket.mockResolvedValue({ assets: json });
|
||||||
|
|
||||||
await expect(
|
await expect(sut.getTimeBucket(authStub.admin, { timeBucket: 'bucket', albumId: 'album-id' })).resolves.toEqual(
|
||||||
sut.getTimeBucket(authStub.admin, { size: TimeBucketSize.DAY, timeBucket: 'bucket', albumId: 'album-id' }),
|
json,
|
||||||
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
|
);
|
||||||
|
|
||||||
expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['album-id']));
|
expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['album-id']));
|
||||||
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', {
|
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', {
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
timeBucket: 'bucket',
|
||||||
albumId: 'album-id',
|
albumId: 'album-id',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the assets for a archive time bucket if user has archive.read', async () => {
|
it('should return the assets for a archive time bucket if user has archive.read', async () => {
|
||||||
mocks.asset.getTimeBucket.mockResolvedValue([assetStub.image]);
|
const json = `[{ id: ['asset-id'] }]`;
|
||||||
|
mocks.asset.getTimeBucket.mockResolvedValue({ assets: json });
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.getTimeBucket(authStub.admin, {
|
sut.getTimeBucket(authStub.admin, {
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
timeBucket: 'bucket',
|
||||||
isArchived: true,
|
isArchived: true,
|
||||||
userId: authStub.admin.user.id,
|
userId: authStub.admin.user.id,
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
|
).resolves.toEqual(json);
|
||||||
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith(
|
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith(
|
||||||
'bucket',
|
'bucket',
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
timeBucket: 'bucket',
|
||||||
isArchived: true,
|
isArchived: true,
|
||||||
userIds: [authStub.admin.user.id],
|
userIds: [authStub.admin.user.id],
|
||||||
@@ -70,20 +63,19 @@ describe(TimelineService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should include partner shared assets', async () => {
|
it('should include partner shared assets', async () => {
|
||||||
mocks.asset.getTimeBucket.mockResolvedValue([assetStub.image]);
|
const json = `[{ id: ['asset-id'] }]`;
|
||||||
|
mocks.asset.getTimeBucket.mockResolvedValue({ assets: json });
|
||||||
mocks.partner.getAll.mockResolvedValue([]);
|
mocks.partner.getAll.mockResolvedValue([]);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.getTimeBucket(authStub.admin, {
|
sut.getTimeBucket(authStub.admin, {
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
timeBucket: 'bucket',
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
userId: authStub.admin.user.id,
|
userId: authStub.admin.user.id,
|
||||||
withPartners: true,
|
withPartners: true,
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
|
).resolves.toEqual(json);
|
||||||
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', {
|
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', {
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
timeBucket: 'bucket',
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
withPartners: true,
|
withPartners: true,
|
||||||
@@ -92,62 +84,37 @@ describe(TimelineService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should check permissions to read tag', async () => {
|
it('should check permissions to read tag', async () => {
|
||||||
mocks.asset.getTimeBucket.mockResolvedValue([assetStub.image]);
|
const json = `[{ id: ['asset-id'] }]`;
|
||||||
|
mocks.asset.getTimeBucket.mockResolvedValue({ assets: json });
|
||||||
mocks.access.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-123']));
|
mocks.access.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-123']));
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.getTimeBucket(authStub.admin, {
|
sut.getTimeBucket(authStub.admin, {
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
timeBucket: 'bucket',
|
||||||
userId: authStub.admin.user.id,
|
userId: authStub.admin.user.id,
|
||||||
tagId: 'tag-123',
|
tagId: 'tag-123',
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
|
).resolves.toEqual(json);
|
||||||
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', {
|
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', {
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
tagId: 'tag-123',
|
tagId: 'tag-123',
|
||||||
timeBucket: 'bucket',
|
timeBucket: 'bucket',
|
||||||
userIds: [authStub.admin.user.id],
|
userIds: [authStub.admin.user.id],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should strip metadata if showExif is disabled', async () => {
|
|
||||||
mocks.access.album.checkSharedLinkAccess.mockResolvedValue(new Set(['album-id']));
|
|
||||||
mocks.asset.getTimeBucket.mockResolvedValue([assetStub.image]);
|
|
||||||
|
|
||||||
const auth = factory.auth({ sharedLink: { showExif: false } });
|
|
||||||
|
|
||||||
const buckets = await sut.getTimeBucket(auth, {
|
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
|
||||||
isArchived: true,
|
|
||||||
albumId: 'album-id',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(buckets).toEqual([expect.objectContaining({ id: 'asset-id' })]);
|
|
||||||
expect(buckets[0]).not.toHaveProperty('exif');
|
|
||||||
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', {
|
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
|
||||||
isArchived: true,
|
|
||||||
albumId: 'album-id',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the assets for a library time bucket if user has library.read', async () => {
|
it('should return the assets for a library time bucket if user has library.read', async () => {
|
||||||
mocks.asset.getTimeBucket.mockResolvedValue([assetStub.image]);
|
const json = `[{ id: ['asset-id'] }]`;
|
||||||
|
mocks.asset.getTimeBucket.mockResolvedValue({ assets: json });
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.getTimeBucket(authStub.admin, {
|
sut.getTimeBucket(authStub.admin, {
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
timeBucket: 'bucket',
|
||||||
userId: authStub.admin.user.id,
|
userId: authStub.admin.user.id,
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
|
).resolves.toEqual(json);
|
||||||
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith(
|
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith(
|
||||||
'bucket',
|
'bucket',
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
timeBucket: 'bucket',
|
||||||
userIds: [authStub.admin.user.id],
|
userIds: [authStub.admin.user.id],
|
||||||
}),
|
}),
|
||||||
@@ -157,7 +124,6 @@ describe(TimelineService.name, () => {
|
|||||||
it('should throw an error if withParners is true and isArchived true or undefined', async () => {
|
it('should throw an error if withParners is true and isArchived true or undefined', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
sut.getTimeBucket(authStub.admin, {
|
sut.getTimeBucket(authStub.admin, {
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
timeBucket: 'bucket',
|
||||||
isArchived: true,
|
isArchived: true,
|
||||||
withPartners: true,
|
withPartners: true,
|
||||||
@@ -167,7 +133,6 @@ describe(TimelineService.name, () => {
|
|||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.getTimeBucket(authStub.admin, {
|
sut.getTimeBucket(authStub.admin, {
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
timeBucket: 'bucket',
|
||||||
isArchived: undefined,
|
isArchived: undefined,
|
||||||
withPartners: true,
|
withPartners: true,
|
||||||
@@ -179,7 +144,6 @@ describe(TimelineService.name, () => {
|
|||||||
it('should throw an error if withParners is true and isFavorite is either true or false', async () => {
|
it('should throw an error if withParners is true and isFavorite is either true or false', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
sut.getTimeBucket(authStub.admin, {
|
sut.getTimeBucket(authStub.admin, {
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
timeBucket: 'bucket',
|
||||||
isFavorite: true,
|
isFavorite: true,
|
||||||
withPartners: true,
|
withPartners: true,
|
||||||
@@ -189,7 +153,6 @@ describe(TimelineService.name, () => {
|
|||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.getTimeBucket(authStub.admin, {
|
sut.getTimeBucket(authStub.admin, {
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
timeBucket: 'bucket',
|
||||||
isFavorite: false,
|
isFavorite: false,
|
||||||
withPartners: true,
|
withPartners: true,
|
||||||
@@ -201,7 +164,6 @@ describe(TimelineService.name, () => {
|
|||||||
it('should throw an error if withParners is true and isTrash is true', async () => {
|
it('should throw an error if withParners is true and isTrash is true', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
sut.getTimeBucket(authStub.admin, {
|
sut.getTimeBucket(authStub.admin, {
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
timeBucket: 'bucket',
|
||||||
isTrashed: true,
|
isTrashed: true,
|
||||||
withPartners: true,
|
withPartners: true,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import { AssetResponseDto, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
import { Stack } from 'src/database';
|
||||||
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, 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 } from 'src/repositories/asset.repository';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
@@ -9,22 +9,32 @@ 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 await this.assetRepository.getTimeBuckets(timeBucketOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTimeBucket(
|
// pre-jsonified response
|
||||||
auth: AuthDto,
|
async getTimeBucket(auth: AuthDto, dto: TimeBucketAssetDto): Promise<string> {
|
||||||
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 });
|
||||||
const assets = await this.assetRepository.getTimeBucket(dto.timeBucket, timeBucketOptions);
|
|
||||||
return !auth.sharedLink || auth.sharedLink?.showExif
|
// TODO: use id cursor for pagination
|
||||||
? assets.map((asset) => mapAsset(asset, { withStack: true, auth }))
|
const bucket = await this.assetRepository.getTimeBucket(dto.timeBucket, timeBucketOptions);
|
||||||
: assets.map((asset) => mapAsset(asset, { stripMetadata: true, auth }));
|
return bucket.assets;
|
||||||
|
}
|
||||||
|
|
||||||
|
mapStack(entity?: Stack | null) {
|
||||||
|
if (!entity) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: entity.id!,
|
||||||
|
primaryAssetId: entity.primaryAssetId!,
|
||||||
|
assetCount: entity.assetCount as number,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async buildTimeBucketOptions(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketOptions> {
|
private async buildTimeBucketOptions(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketOptions> {
|
||||||
|
|||||||
28
server/src/services/timeline.service.types.ts
Normal file
28
server/src/services/timeline.service.types.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
export type TimelineStack = {
|
||||||
|
id: string;
|
||||||
|
primaryAssetId: string;
|
||||||
|
assetCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AssetDescription = {
|
||||||
|
city: string | null;
|
||||||
|
country: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TimeBucketAssets = {
|
||||||
|
id: string[];
|
||||||
|
ownerId: string[];
|
||||||
|
ratio: number[];
|
||||||
|
isFavorite: number[];
|
||||||
|
isArchived: number[];
|
||||||
|
isTrashed: number[];
|
||||||
|
isImage: number[];
|
||||||
|
thumbhash: (string | null)[];
|
||||||
|
localDateTime: string[];
|
||||||
|
stack?: ([string, string] | null)[];
|
||||||
|
duration: (string | null)[];
|
||||||
|
projectionType: (string | null)[];
|
||||||
|
livePhotoVideoId: (string | null)[];
|
||||||
|
city: (string | null)[];
|
||||||
|
country: (string | null)[];
|
||||||
|
};
|
||||||
@@ -197,3 +197,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,15 @@ 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 | null) => {
|
||||||
|
if (!encoded) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (typeof encoded === 'string') {
|
||||||
|
return Buffer.from(encoded.slice(2), 'hex').toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
return encoded.toString('base64');
|
||||||
|
};
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ export function withTags(eb: ExpressionBuilder<DB, 'assets'>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function truncatedDate<O>(size: TimeBucketSize) {
|
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(${sql.lit(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) {
|
||||||
@@ -276,6 +276,7 @@ export function withTagId<O>(qb: SelectQueryBuilder<DB, 'assets', O>, tagId: str
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const joinDeduplicationPlugin = new DeduplicateJoinsPlugin();
|
const joinDeduplicationPlugin = new DeduplicateJoinsPlugin();
|
||||||
/** TODO: This should only be used for search-related queries, not as a general purpose query builder */
|
/** TODO: This should only be used for search-related queries, not as a general purpose query builder */
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,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';
|
||||||
|
|
||||||
|
|||||||
4
server/test/fixtures/asset.stub.ts
vendored
4
server/test/fixtures/asset.stub.ts
vendored
@@ -257,6 +257,10 @@ export const assetStub = {
|
|||||||
duplicateId: null,
|
duplicateId: null,
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
stack: null,
|
stack: null,
|
||||||
|
orientation: '',
|
||||||
|
projectionType: null,
|
||||||
|
height: 3840,
|
||||||
|
width: 2160,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
trashed: Object.freeze({
|
trashed: Object.freeze({
|
||||||
|
|||||||
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 } 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;
|
||||||
|
|||||||
@@ -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,6 +13,7 @@ describe('AssetViewerNavBar component', () => {
|
|||||||
showDownloadButton: false,
|
showDownloadButton: false,
|
||||||
showMotionPlayButton: false,
|
showMotionPlayButton: false,
|
||||||
showShareButton: false,
|
showShareButton: false,
|
||||||
|
preAction: () => {},
|
||||||
onZoomImage: () => {},
|
onZoomImage: () => {},
|
||||||
onCopyImage: () => {},
|
onCopyImage: () => {},
|
||||||
onAction: () => {},
|
onAction: () => {},
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import { closeEditorCofirm } from '$lib/stores/asset-editor.store';
|
import { closeEditorCofirm } from '$lib/stores/asset-editor.store';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
|
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
|
||||||
import { isShowDetail } from '$lib/stores/preferences.store';
|
import { isShowDetail } from '$lib/stores/preferences.store';
|
||||||
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
@@ -16,6 +17,7 @@
|
|||||||
import { getAssetJobMessage, getSharedLink, handlePromiseError } from '$lib/utils';
|
import { getAssetJobMessage, getSharedLink, handlePromiseError } 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,
|
||||||
@@ -47,7 +49,7 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
asset: AssetResponseDto;
|
asset: AssetResponseDto;
|
||||||
preloadAssets?: AssetResponseDto[];
|
preloadAssets?: TimelineAsset[];
|
||||||
showNavigation?: boolean;
|
showNavigation?: boolean;
|
||||||
withStacked?: boolean;
|
withStacked?: boolean;
|
||||||
isShared?: boolean;
|
isShared?: boolean;
|
||||||
@@ -56,10 +58,10 @@
|
|||||||
preAction?: PreAction | undefined;
|
preAction?: PreAction | undefined;
|
||||||
onAction?: OnAction | undefined;
|
onAction?: OnAction | undefined;
|
||||||
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<{ id: string } | undefined>;
|
||||||
copyImage?: () => Promise<void>;
|
copyImage?: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +83,7 @@
|
|||||||
copyImage = $bindable(),
|
copyImage = $bindable(),
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const { setAsset } = assetViewingStore;
|
const { setAssetId } = assetViewingStore;
|
||||||
const {
|
const {
|
||||||
restartProgress: restartSlideshowProgress,
|
restartProgress: restartSlideshowProgress,
|
||||||
stopProgress: stopSlideshowProgress,
|
stopProgress: stopSlideshowProgress,
|
||||||
@@ -121,7 +123,7 @@
|
|||||||
|
|
||||||
untrack(() => {
|
untrack(() => {
|
||||||
if (stack && stack?.assets.length > 1) {
|
if (stack && stack?.assets.length > 1) {
|
||||||
preloadAssets.push(stack.assets[1]);
|
preloadAssets.push(toTimelineAsset(stack.assets[1]));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -161,7 +163,7 @@
|
|||||||
slideshowStateUnsubscribe = slideshowState.subscribe((value) => {
|
slideshowStateUnsubscribe = slideshowState.subscribe((value) => {
|
||||||
if (value === SlideshowState.PlaySlideshow) {
|
if (value === SlideshowState.PlaySlideshow) {
|
||||||
slideshowHistory.reset();
|
slideshowHistory.reset();
|
||||||
slideshowHistory.queue(asset);
|
slideshowHistory.queue(toTimelineAsset(asset));
|
||||||
handlePromiseError(handlePlaySlideshow());
|
handlePromiseError(handlePlaySlideshow());
|
||||||
} else if (value === SlideshowState.StopSlideshow) {
|
} else if (value === SlideshowState.StopSlideshow) {
|
||||||
handlePromiseError(handleStopSlideshow());
|
handlePromiseError(handleStopSlideshow());
|
||||||
@@ -171,7 +173,7 @@
|
|||||||
shuffleSlideshowUnsubscribe = slideshowNavigation.subscribe((value) => {
|
shuffleSlideshowUnsubscribe = slideshowNavigation.subscribe((value) => {
|
||||||
if (value === SlideshowNavigation.Shuffle) {
|
if (value === SlideshowNavigation.Shuffle) {
|
||||||
slideshowHistory.reset();
|
slideshowHistory.reset();
|
||||||
slideshowHistory.queue(asset);
|
slideshowHistory.queue(toTimelineAsset(asset));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -225,7 +227,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const closeViewer = () => {
|
const closeViewer = () => {
|
||||||
onClose({ asset });
|
onClose(asset);
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeEditor = () => {
|
const closeEditor = () => {
|
||||||
@@ -292,8 +294,7 @@
|
|||||||
let assetViewerHtmlElement = $state<HTMLElement>();
|
let assetViewerHtmlElement = $state<HTMLElement>();
|
||||||
|
|
||||||
const slideshowHistory = new SlideshowHistory((asset) => {
|
const slideshowHistory = new SlideshowHistory((asset) => {
|
||||||
setAsset(asset);
|
handlePromiseError(setAssetId(asset.id).then(() => ($restartSlideshowProgress = true)));
|
||||||
$restartSlideshowProgress = true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleVideoStarted = () => {
|
const handleVideoStarted = () => {
|
||||||
@@ -562,8 +563,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)}
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, onDestroy, tick } from 'svelte';
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
import { getAssetOriginalUrl } from '$lib/utils';
|
import { getAssetOriginalUrl } from '$lib/utils';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||||
|
import { onDestroy, onMount, tick } from 'svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
import { imgElement, cropAreaEl, resetCropStore, overlayEl, isResizingOrDragging, cropFrame } from './crop-store';
|
|
||||||
import { draw } from './drawing';
|
|
||||||
import { onImageLoad, resizeCanvas } from './image-loading';
|
|
||||||
import { handleMouseDown, handleMouseMove, handleMouseUp } from './mouse-handlers';
|
|
||||||
import { recalculateCrop, animateCropChange } from './crop-settings';
|
|
||||||
import {
|
import {
|
||||||
changedOriention,
|
changedOriention,
|
||||||
cropAspectRatio,
|
cropAspectRatio,
|
||||||
@@ -17,7 +12,13 @@
|
|||||||
resetGlobalCropStore,
|
resetGlobalCropStore,
|
||||||
rotateDegrees,
|
rotateDegrees,
|
||||||
} from '$lib/stores/asset-editor.store';
|
} from '$lib/stores/asset-editor.store';
|
||||||
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import type { AssetResponseDto } from '@immich/sdk';
|
import type { AssetResponseDto } from '@immich/sdk';
|
||||||
|
import { animateCropChange, recalculateCrop } from './crop-settings';
|
||||||
|
import { cropAreaEl, cropFrame, imgElement, isResizingOrDragging, overlayEl, resetCropStore } from './crop-store';
|
||||||
|
import { draw } from './drawing';
|
||||||
|
import { onImageLoad, resizeCanvas } from './image-loading';
|
||||||
|
import { handleMouseDown, handleMouseMove, handleMouseUp } from './mouse-handlers';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
asset: AssetResponseDto;
|
asset: AssetResponseDto;
|
||||||
@@ -81,7 +82,7 @@
|
|||||||
aria-label="Crop area"
|
aria-label="Crop area"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<img draggable="false" src={img?.src} alt={$getAltText(asset)} />
|
<img draggable="false" src={img?.src} alt={$getAltText(toTimelineAsset(asset))} />
|
||||||
<div class={`${$isResizingOrDragging ? 'resizing' : ''} crop-frame`} bind:this={$cropFrame}>
|
<div class={`${$isResizingOrDragging ? 'resizing' : ''} crop-frame`} bind:this={$cropFrame}>
|
||||||
<div class="grid"></div>
|
<div class="grid"></div>
|
||||||
<div class="corner top-left"></div>
|
<div class="corner top-left"></div>
|
||||||
|
|||||||
@@ -1,31 +1,32 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { shortcuts } from '$lib/actions/shortcut';
|
import { shortcuts } from '$lib/actions/shortcut';
|
||||||
import { zoomImageAction, zoomed } from '$lib/actions/zoom-image';
|
import { zoomImageAction, zoomed } from '$lib/actions/zoom-image';
|
||||||
|
import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.svelte';
|
||||||
import BrokenAsset from '$lib/components/assets/broken-asset.svelte';
|
import BrokenAsset from '$lib/components/assets/broken-asset.svelte';
|
||||||
|
import { photoViewerImgElement, type TimelineAsset } from '$lib/stores/assets-store.svelte';
|
||||||
|
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
||||||
import { boundingBoxesArray } from '$lib/stores/people.store';
|
import { boundingBoxesArray } from '$lib/stores/people.store';
|
||||||
import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store';
|
import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store';
|
||||||
import { SlideshowLook, SlideshowState, slideshowLookCssMapping, slideshowStore } from '$lib/stores/slideshow.store';
|
import { SlideshowLook, SlideshowState, slideshowLookCssMapping, slideshowStore } from '$lib/stores/slideshow.store';
|
||||||
import { photoZoomState } from '$lib/stores/zoom-image.store';
|
import { photoZoomState } from '$lib/stores/zoom-image.store';
|
||||||
import { getAssetOriginalUrl, getAssetThumbnailUrl, handlePromiseError } from '$lib/utils';
|
import { getAssetOriginalUrl, getAssetThumbnailUrl, handlePromiseError } from '$lib/utils';
|
||||||
import { canCopyImageToClipboard, copyImageToClipboard, isWebCompatibleImage } from '$lib/utils/asset-utils';
|
import { canCopyImageToClipboard, copyImageToClipboard, isWebCompatibleImage } from '$lib/utils/asset-utils';
|
||||||
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { getBoundingBox } from '$lib/utils/people-utils';
|
import { getBoundingBox } from '$lib/utils/people-utils';
|
||||||
|
import { cancelImageUrl } from '$lib/utils/sw-messaging';
|
||||||
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 { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
|
import { AssetMediaSize, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
|
import { swipe, type SwipeCustomEvent } from 'svelte-gestures';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { type SwipeCustomEvent, swipe } from 'svelte-gestures';
|
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
|
||||||
import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.svelte';
|
|
||||||
import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
|
|
||||||
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
|
||||||
import { cancelImageUrl, preloadImageUrl } from '$lib/utils/sw-messaging';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
asset: AssetResponseDto;
|
asset: AssetResponseDto;
|
||||||
preloadAssets?: AssetResponseDto[] | undefined;
|
preloadAssets?: TimelineAsset[] | undefined;
|
||||||
element?: HTMLDivElement | undefined;
|
element?: HTMLDivElement | undefined;
|
||||||
haveFadeTransition?: boolean;
|
haveFadeTransition?: boolean;
|
||||||
sharedLink?: SharedLinkResponseDto | undefined;
|
sharedLink?: SharedLinkResponseDto | undefined;
|
||||||
@@ -69,10 +70,11 @@
|
|||||||
$boundingBoxesArray = [];
|
$boundingBoxesArray = [];
|
||||||
});
|
});
|
||||||
|
|
||||||
const preload = (targetSize: AssetMediaSize | 'original', preloadAssets?: AssetResponseDto[]) => {
|
const preload = (targetSize: AssetMediaSize | 'original', preloadAssets?: TimelineAsset[]) => {
|
||||||
for (const preloadAsset of preloadAssets || []) {
|
for (const preloadAsset of preloadAssets || []) {
|
||||||
if (preloadAsset.type === AssetTypeEnum.Image) {
|
if (preloadAsset.isImage) {
|
||||||
preloadImageUrl(getAssetUrl(preloadAsset.id, targetSize, preloadAsset.thumbhash));
|
let img = new Image();
|
||||||
|
img.src = getAssetUrl(preloadAsset.id, targetSize, preloadAsset.thumbhash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -197,7 +199,7 @@
|
|||||||
bind:clientWidth={containerWidth}
|
bind:clientWidth={containerWidth}
|
||||||
bind:clientHeight={containerHeight}
|
bind:clientHeight={containerHeight}
|
||||||
>
|
>
|
||||||
<img style="display:none" src={imageLoaderUrl} alt={$getAltText(asset)} {onload} {onerror} />
|
<img style="display:none" src={imageLoaderUrl} alt="" {onload} {onerror} />
|
||||||
{#if !imageLoaded}
|
{#if !imageLoaded}
|
||||||
<div id="spinner" class="flex h-full items-center justify-center">
|
<div id="spinner" class="flex h-full items-center justify-center">
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
@@ -213,7 +215,7 @@
|
|||||||
{#if $slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.BlurredBackground}
|
{#if $slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.BlurredBackground}
|
||||||
<img
|
<img
|
||||||
src={assetFileUrl}
|
src={assetFileUrl}
|
||||||
alt={$getAltText(asset)}
|
alt=""
|
||||||
class="absolute top-0 start-0 -z-10 object-cover h-full w-full blur-lg"
|
class="absolute top-0 start-0 -z-10 object-cover h-full w-full blur-lg"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
/>
|
/>
|
||||||
@@ -221,7 +223,7 @@
|
|||||||
<img
|
<img
|
||||||
bind:this={$photoViewerImgElement}
|
bind:this={$photoViewerImgElement}
|
||||||
src={assetFileUrl}
|
src={assetFileUrl}
|
||||||
alt={$getAltText(asset)}
|
alt={$getAltText(toTimelineAsset(asset))}
|
||||||
class="h-full w-full {$slideshowState === SlideshowState.None
|
class="h-full w-full {$slideshowState === SlideshowState.None
|
||||||
? 'object-contain'
|
? 'object-contain'
|
||||||
: slideshowLookCssMapping[$slideshowLook]}"
|
: slideshowLookCssMapping[$slideshowLook]}"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import { getAssetPlaybackUrl, getAssetThumbnailUrl } from '$lib/utils';
|
import { getAssetPlaybackUrl, getAssetThumbnailUrl } 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,
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
import { thumbhash } from '$lib/actions/thumbhash';
|
import { thumbhash } from '$lib/actions/thumbhash';
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
|
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 { focusNext } from '$lib/utils/focus-util';
|
import { focusNext } from '$lib/utils/focus-util';
|
||||||
import { currentUrlReplaceAssetId } from '$lib/utils/navigation';
|
import { currentUrlReplaceAssetId } from '$lib/utils/navigation';
|
||||||
@@ -29,11 +30,11 @@
|
|||||||
import VideoThumbnail from './video-thumbnail.svelte';
|
import VideoThumbnail from './video-thumbnail.svelte';
|
||||||
|
|
||||||
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;
|
||||||
selectionCandidate?: boolean;
|
selectionCandidate?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: 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 {
|
||||||
@@ -319,7 +320,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 end-0 top-0 z-10 flex place-items-center gap-1 text-xs font-medium text-white">
|
<div class="absolute end-0 top-0 z-10 flex place-items-center gap-1 text-xs font-medium text-white">
|
||||||
<span class="pe-2 pt-2">
|
<span class="pe-2 pt-2">
|
||||||
<Icon path={mdiRotate360} size="24" />
|
<Icon path={mdiRotate360} size="24" />
|
||||||
@@ -332,7 +333,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 end-0' : 'top-7 end-1',
|
asset.isImage && !asset.livePhotoVideoId ? 'top-0 end-0' : 'top-7 end-1',
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<span class="pe-2 pt-2 flex place-items-center gap-1">
|
<span class="pe-2 pt-2 flex place-items-center gap-1">
|
||||||
@@ -352,17 +353,17 @@
|
|||||||
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 })}
|
||||||
|
|||||||
@@ -24,16 +24,18 @@
|
|||||||
NotificationType,
|
NotificationType,
|
||||||
} from '$lib/components/shared-components/notification/notification';
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
import { AppRoute, QueryParameter } from '$lib/constants';
|
import { AppRoute, QueryParameter } from '$lib/constants';
|
||||||
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
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 { type Viewport } from '$lib/stores/assets-store.svelte';
|
import { type TimelineAsset, type Viewport } from '$lib/stores/assets-store.svelte';
|
||||||
import { type MemoryAsset, memoryStore } from '$lib/stores/memory.store.svelte';
|
import { type MemoryAsset, memoryStore } from '$lib/stores/memory.store.svelte';
|
||||||
import { locale, videoViewerMuted, videoViewerVolume } from '$lib/stores/preferences.store';
|
import { locale, videoViewerMuted, videoViewerVolume } from '$lib/stores/preferences.store';
|
||||||
import { preferences } from '$lib/stores/user.store';
|
import { preferences } from '$lib/stores/user.store';
|
||||||
import { getAssetPlaybackUrl, getAssetThumbnailUrl, handlePromiseError, memoryLaneTitle } from '$lib/utils';
|
import { getAssetPlaybackUrl, getAssetThumbnailUrl, handlePromiseError, memoryLaneTitle } from '$lib/utils';
|
||||||
import { cancelMultiselect } from '$lib/utils/asset-utils';
|
import { cancelMultiselect } from '$lib/utils/asset-utils';
|
||||||
import { fromLocalDateTime } from '$lib/utils/timeline-util';
|
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||||
import { AssetMediaSize, type AssetResponseDto, AssetTypeEnum } from '@immich/sdk';
|
import { fromLocalDateTime, toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
|
import { AssetMediaSize, getAssetInfo } from '@immich/sdk';
|
||||||
import { IconButton } from '@immich/ui';
|
import { IconButton } from '@immich/ui';
|
||||||
import {
|
import {
|
||||||
mdiCardsOutline,
|
mdiCardsOutline,
|
||||||
@@ -66,6 +68,11 @@
|
|||||||
let playerInitialized = $state(false);
|
let playerInitialized = $state(false);
|
||||||
let paused = $state(false);
|
let paused = $state(false);
|
||||||
let current = $state<MemoryAsset | undefined>(undefined);
|
let current = $state<MemoryAsset | undefined>(undefined);
|
||||||
|
let currentMemoryAssetFull = $derived.by(async () =>
|
||||||
|
current?.asset ? await getAssetInfo({ id: current?.asset.id, key: authManager.key }) : undefined,
|
||||||
|
);
|
||||||
|
let currentTimelineAssets = $derived(current?.memory.assets.map((a) => toTimelineAsset(a)) || []);
|
||||||
|
|
||||||
let isSaved = $derived(current?.memory.isSaved);
|
let isSaved = $derived(current?.memory.isSaved);
|
||||||
let viewerHeight = $state(0);
|
let viewerHeight = $state(0);
|
||||||
|
|
||||||
@@ -76,8 +83,8 @@
|
|||||||
const assetInteraction = new AssetInteraction();
|
const assetInteraction = new AssetInteraction();
|
||||||
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: { id: string }) => `?${QueryParameter.ID}=${asset.id}`;
|
||||||
const handleNavigate = async (asset?: AssetResponseDto) => {
|
const handleNavigate = async (asset?: { id: string }) => {
|
||||||
if ($isViewing) {
|
if ($isViewing) {
|
||||||
return asset;
|
return asset;
|
||||||
}
|
}
|
||||||
@@ -88,9 +95,9 @@
|
|||||||
|
|
||||||
await goto(asHref(asset));
|
await goto(asHref(asset));
|
||||||
};
|
};
|
||||||
const setProgressDuration = (asset: AssetResponseDto) => {
|
const setProgressDuration = (asset: TimelineAsset) => {
|
||||||
if (asset.type === AssetTypeEnum.Video) {
|
if (asset.isVideo) {
|
||||||
const timeParts = asset.duration.split(':').map(Number);
|
const timeParts = asset.duration!.split(':').map(Number);
|
||||||
const durationInMilliseconds = (timeParts[0] * 3600 + timeParts[1] * 60 + timeParts[2]) * 1000;
|
const durationInMilliseconds = (timeParts[0] * 3600 + timeParts[1] * 60 + timeParts[2]) * 1000;
|
||||||
progressBarController = new Tween<number>(0, {
|
progressBarController = new Tween<number>(0, {
|
||||||
duration: (from: number, to: number) => (to ? durationInMilliseconds * (to - from) : 0),
|
duration: (from: number, to: number) => (to ? durationInMilliseconds * (to - from) : 0),
|
||||||
@@ -106,7 +113,8 @@
|
|||||||
const handleNextMemory = () => handleNavigate(current?.nextMemory?.assets[0]);
|
const handleNextMemory = () => handleNavigate(current?.nextMemory?.assets[0]);
|
||||||
const handlePreviousMemory = () => handleNavigate(current?.previousMemory?.assets[0]);
|
const handlePreviousMemory = () => handleNavigate(current?.previousMemory?.assets[0]);
|
||||||
const handleEscape = async () => goto(AppRoute.PHOTOS);
|
const handleEscape = async () => goto(AppRoute.PHOTOS);
|
||||||
const handleSelectAll = () => assetInteraction.selectAssets(current?.memory.assets || []);
|
const handleSelectAll = () =>
|
||||||
|
assetInteraction.selectAssets(current?.memory.assets.map((a) => toTimelineAsset(a)) || []);
|
||||||
const handleAction = async (callingContext: string, action: 'reset' | 'pause' | 'play') => {
|
const handleAction = async (callingContext: string, action: 'reset' | 'pause' | 'play') => {
|
||||||
// leaving these log statements here as comments. Very useful to figure out what's going on during dev!
|
// leaving these log statements here as comments. Very useful to figure out what's going on during dev!
|
||||||
// console.log(`handleAction[${callingContext}] called with: ${action}`);
|
// console.log(`handleAction[${callingContext}] called with: ${action}`);
|
||||||
@@ -239,7 +247,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initPlayer = () => {
|
const initPlayer = () => {
|
||||||
const isVideoAssetButPlayerHasNotLoadedYet = current && current.asset.type === AssetTypeEnum.Video && !videoPlayer;
|
const isVideoAssetButPlayerHasNotLoadedYet = current && current.asset.isVideo && !videoPlayer;
|
||||||
if (playerInitialized || isVideoAssetButPlayerHasNotLoadedYet) {
|
if (playerInitialized || isVideoAssetButPlayerHasNotLoadedYet) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -439,7 +447,7 @@
|
|||||||
<div class="relative h-full w-full rounded-2xl bg-black">
|
<div class="relative h-full w-full rounded-2xl bg-black">
|
||||||
{#key current.asset.id}
|
{#key current.asset.id}
|
||||||
<div transition:fade class="h-full w-full">
|
<div transition:fade class="h-full w-full">
|
||||||
{#if current.asset.type === AssetTypeEnum.Video}
|
{#if current.asset.isVideo}
|
||||||
<video
|
<video
|
||||||
bind:this={videoPlayer}
|
bind:this={videoPlayer}
|
||||||
autoplay
|
autoplay
|
||||||
@@ -456,7 +464,7 @@
|
|||||||
<img
|
<img
|
||||||
class="h-full w-full rounded-2xl object-contain transition-all"
|
class="h-full w-full rounded-2xl object-contain transition-all"
|
||||||
src={getAssetThumbnailUrl({ id: current.asset.id, size: AssetMediaSize.Preview })}
|
src={getAssetThumbnailUrl({ id: current.asset.id, size: AssetMediaSize.Preview })}
|
||||||
alt={current.asset.exifInfo?.description}
|
alt={$getAltText(current.asset)}
|
||||||
draggable="false"
|
draggable="false"
|
||||||
transition:fade
|
transition:fade
|
||||||
/>
|
/>
|
||||||
@@ -549,8 +557,10 @@
|
|||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{current.asset.exifInfo?.city || ''}
|
{#await currentMemoryAssetFull then asset}
|
||||||
{current.asset.exifInfo?.country || ''}
|
{asset?.exifInfo?.city || ''}
|
||||||
|
{asset?.exifInfo?.country || ''}
|
||||||
|
{/await}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -621,7 +631,7 @@
|
|||||||
<GalleryViewer
|
<GalleryViewer
|
||||||
onNext={handleNextAsset}
|
onNext={handleNextAsset}
|
||||||
onPrevious={handlePreviousAsset}
|
onPrevious={handlePreviousAsset}
|
||||||
assets={current.memory.assets}
|
assets={currentTimelineAssets}
|
||||||
viewport={galleryViewport}
|
viewport={galleryViewport}
|
||||||
{assetInteraction}
|
{assetInteraction}
|
||||||
slidingWindowOffset={viewerHeight}
|
slidingWindowOffset={viewerHeight}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -4,11 +4,11 @@
|
|||||||
import { getSelectedAssets } from '$lib/utils/asset-utils';
|
import { getSelectedAssets } from '$lib/utils/asset-utils';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { updateAssets } from '@immich/sdk';
|
import { updateAssets } from '@immich/sdk';
|
||||||
|
import { mdiCalendarEditOutline } from '@mdi/js';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
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 { mdiCalendarEditOutline } from '@mdi/js';
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
interface Props {
|
interface Props {
|
||||||
menuItem?: boolean;
|
menuItem?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
|
import { downloadArchive, downloadFile } from '$lib/utils/asset-utils';
|
||||||
|
import { getAssetInfo } 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,8 @@
|
|||||||
const assets = [...getAssets()];
|
const assets = [...getAssets()];
|
||||||
if (assets.length === 1) {
|
if (assets.length === 1) {
|
||||||
clearSelect();
|
clearSelect();
|
||||||
await downloadFile(assets[0]);
|
let asset = await getAssetInfo({ id: assets[0].id, key: authManager.key });
|
||||||
|
await downloadFile(asset);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
<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 { authManager } from '$lib/managers/auth-manager.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 +32,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 +50,22 @@
|
|||||||
|
|
||||||
const handleUnlink = async () => {
|
const handleUnlink = async () => {
|
||||||
const [still] = [...getOwnedAssets()];
|
const [still] = [...getOwnedAssets()];
|
||||||
|
if (still) {
|
||||||
const motionId = still?.livePhotoVideoId;
|
const motionId = still.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, key: authManager.key });
|
||||||
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,10 +1,10 @@
|
|||||||
<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 AssetStore, isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
|
||||||
import { mdiSelectAll, mdiSelectRemove } from '@mdi/js';
|
|
||||||
import { selectAllAssets, cancelMultiselect } from '$lib/utils/asset-utils';
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import type { AssetInteraction } from '$lib/stores/asset-interaction.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 { t } from 'svelte-i18n';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assetStore: AssetStore;
|
assetStore: AssetStore;
|
||||||
|
|||||||
@@ -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 { mdiCheckCircle, mdiCircleOutline } from '@mdi/js';
|
|
||||||
import { fly } from 'svelte/transition';
|
|
||||||
import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
|
|
||||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { scale } from 'svelte/transition';
|
import { mdiCheckCircle, mdiCircleOutline } from '@mdi/js';
|
||||||
|
import { fly, scale } from 'svelte/transition';
|
||||||
|
import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
|
||||||
|
|
||||||
import { flip } from 'svelte/animate';
|
import { flip } from 'svelte/animate';
|
||||||
|
|
||||||
@@ -31,9 +31,9 @@
|
|||||||
assetStore: AssetStore;
|
assetStore: AssetStore;
|
||||||
assetInteraction: AssetInteraction;
|
assetInteraction: AssetInteraction;
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,39 @@
|
|||||||
<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 { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
|
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';
|
||||||
import { handlePromiseError } from '$lib/utils';
|
import { handlePromiseError } from '$lib/utils';
|
||||||
import { deleteAssets, updateStackedAssetInTimeline, updateUnstackedAssetInTimeline } from '$lib/utils/actions';
|
import { deleteAssets, updateStackedAssetInTimeline, updateUnstackedAssetInTimeline } from '$lib/utils/actions';
|
||||||
import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
|
import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
|
||||||
|
import { focusNext } from '$lib/utils/focus-util';
|
||||||
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';
|
|
||||||
import { focusNext } from '$lib/utils/focus-util';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isSelectionMode?: boolean;
|
isSelectionMode?: boolean;
|
||||||
@@ -44,7 +51,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;
|
||||||
@@ -362,7 +369,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);
|
||||||
}
|
}
|
||||||
@@ -373,7 +380,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, key: authManager.key });
|
||||||
|
assetViewingStore.setAsset(asset, preloadAsset ? [preloadAsset] : []);
|
||||||
await navigate({ targetRoute: 'current', assetId: previousAsset.id });
|
await navigate({ targetRoute: 'current', assetId: previousAsset.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,7 +393,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, key: authManager.key });
|
||||||
|
assetViewingStore.setAsset(asset, preloadAsset ? [preloadAsset] : []);
|
||||||
await navigate({ targetRoute: 'current', assetId: nextAsset.id });
|
await navigate({ targetRoute: 'current', assetId: nextAsset.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,14 +406,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, key: authManager.key });
|
||||||
|
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 };
|
||||||
@@ -420,7 +429,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]);
|
||||||
@@ -449,7 +458,7 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let lastAssetMouseEvent: AssetResponseDto | null = $state(null);
|
let lastAssetMouseEvent: TimelineAsset | null = $state(null);
|
||||||
|
|
||||||
let shiftKeyIsDown = $state(false);
|
let shiftKeyIsDown = $state(false);
|
||||||
|
|
||||||
@@ -479,14 +488,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) {
|
||||||
@@ -506,7 +515,7 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectAssets = async (asset: AssetResponseDto) => {
|
const handleSelectAssets = async (asset: TimelineAsset) => {
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -589,7 +598,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: () => TimelineAsset[]; // All assets includes partners' assets
|
||||||
getOwnedAssets: () => AssetResponseDto[]; // Only assets owned by the user
|
getOwnedAssets: () => TimelineAsset[]; // 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 { TimelineAsset } from '$lib/stores/assets-store.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: TimelineAsset[];
|
||||||
clearSelect: () => void;
|
clearSelect: () => void;
|
||||||
ownerId?: string | undefined;
|
ownerId?: string | undefined;
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import { memoryStore } from '$lib/stores/memory.store.svelte';
|
import { memoryStore } from '$lib/stores/memory.store.svelte';
|
||||||
import { getAssetThumbnailUrl, memoryLaneTitle } from '$lib/utils';
|
import { getAssetThumbnailUrl, memoryLaneTitle } from '$lib/utils';
|
||||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||||
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import { mdiChevronLeft, mdiChevronRight } from '@mdi/js';
|
import { mdiChevronLeft, mdiChevronRight } from '@mdi/js';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
@@ -82,7 +83,7 @@
|
|||||||
<img
|
<img
|
||||||
class="h-full w-full rounded-xl object-cover"
|
class="h-full w-full rounded-xl object-cover"
|
||||||
src={getAssetThumbnailUrl(memory.assets[0].id)}
|
src={getAssetThumbnailUrl(memory.assets[0].id)}
|
||||||
alt={$t('memory_lane_title', { values: { title: $getAltText(memory.assets[0]) } })}
|
alt={$t('memory_lane_title', { values: { title: $getAltText(toTimelineAsset(memory.assets[0])) } })}
|
||||||
draggable="false"
|
draggable="false"
|
||||||
/>
|
/>
|
||||||
<p class="absolute bottom-2 start-4 z-10 text-lg text-white max-md:text-sm">
|
<p class="absolute bottom-2 start-4 z-10 text-lg text-white max-md:text-sm">
|
||||||
|
|||||||
@@ -11,7 +11,8 @@
|
|||||||
import { cancelMultiselect, downloadArchive } from '$lib/utils/asset-utils';
|
import { cancelMultiselect, 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 { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
|
import { addSharedLinkAssets, getAssetInfo, type SharedLinkResponseDto } from '@immich/sdk';
|
||||||
import { mdiArrowLeft, mdiFileImagePlusOutline, mdiFolderDownloadOutline, mdiSelectAll } from '@mdi/js';
|
import { mdiArrowLeft, mdiFileImagePlusOutline, mdiFolderDownloadOutline, mdiSelectAll } from '@mdi/js';
|
||||||
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';
|
||||||
@@ -33,7 +34,8 @@
|
|||||||
const viewport: Viewport = $state({ width: 0, height: 0 });
|
const viewport: Viewport = $state({ width: 0, height: 0 });
|
||||||
const assetInteraction = new AssetInteraction();
|
const assetInteraction = new AssetInteraction();
|
||||||
|
|
||||||
let assets = $derived(sharedLink.assets);
|
let assets = $derived(sharedLink.assets.map((a) => toTimelineAsset(a)));
|
||||||
|
let fullAsset = $derived(assets[0] ? getAssetInfo({ id: assets[0]?.id, key: authManager.key }) : null);
|
||||||
|
|
||||||
dragAndDropFilesStore.subscribe((value) => {
|
dragAndDropFilesStore.subscribe((value) => {
|
||||||
if (value.isDragging && value.files.length > 0) {
|
if (value.isDragging && value.files.length > 0) {
|
||||||
@@ -127,14 +129,16 @@
|
|||||||
<GalleryViewer {assets} {assetInteraction} {viewport} />
|
<GalleryViewer {assets} {assetInteraction} {viewport} />
|
||||||
</section>
|
</section>
|
||||||
{:else}
|
{:else}
|
||||||
<AssetViewer
|
{#await fullAsset then asset}
|
||||||
asset={assets[0]}
|
<AssetViewer
|
||||||
showCloseButton={false}
|
asset={asset!}
|
||||||
onAction={handleAction}
|
showCloseButton={false}
|
||||||
onPrevious={() => Promise.resolve(false)}
|
onAction={handleAction}
|
||||||
onNext={() => Promise.resolve(false)}
|
onPrevious={() => Promise.resolve(false)}
|
||||||
onRandom={() => Promise.resolve(undefined)}
|
onNext={() => Promise.resolve(false)}
|
||||||
onClose={() => {}}
|
onRandom={() => Promise.resolve(undefined)}
|
||||||
/>
|
onClose={() => {}}
|
||||||
|
/>
|
||||||
|
{/await}
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -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 { TimelineAsset, 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 { focusNext } from '$lib/utils/focus-util';
|
||||||
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';
|
|
||||||
import { focusNext } from '$lib/utils/focus-util';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assets: AssetResponseDto[];
|
assets: (TimelineAsset | AssetResponseDto)[];
|
||||||
assetInteraction: AssetInteraction;
|
assetInteraction: AssetInteraction;
|
||||||
disableAssetSelect?: boolean;
|
disableAssetSelect?: boolean;
|
||||||
showArchiveIcon?: boolean;
|
showArchiveIcon?: boolean;
|
||||||
@@ -33,9 +34,9 @@
|
|||||||
onIntersected?: (() => void) | undefined;
|
onIntersected?: (() => void) | undefined;
|
||||||
showAssetName?: boolean;
|
showAssetName?: boolean;
|
||||||
isShowDeleteConfirmation?: boolean;
|
isShowDeleteConfirmation?: boolean;
|
||||||
onPrevious?: (() => Promise<AssetResponseDto | undefined>) | undefined;
|
onPrevious?: (() => Promise<{ id: string } | undefined>) | undefined;
|
||||||
onNext?: (() => Promise<AssetResponseDto | undefined>) | undefined;
|
onNext?: (() => Promise<{ id: string } | undefined>) | undefined;
|
||||||
onRandom?: (() => Promise<AssetResponseDto | undefined>) | undefined;
|
onRandom?: (() => Promise<{ id: string } | undefined>) | undefined;
|
||||||
pageHeaderOffset?: number;
|
pageHeaderOffset?: number;
|
||||||
slidingWindowOffset?: number;
|
slidingWindowOffset?: number;
|
||||||
}
|
}
|
||||||
@@ -56,7 +57,7 @@
|
|||||||
pageHeaderOffset = 0,
|
pageHeaderOffset = 0,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let { isViewing: isViewerOpen, asset: viewingAsset, setAsset } = assetViewingStore;
|
let { isViewing: isViewerOpen, asset: viewingAsset, setAssetId } = assetViewingStore;
|
||||||
|
|
||||||
let geometry: CommonJustifiedLayout | undefined = $state();
|
let geometry: CommonJustifiedLayout | undefined = $state();
|
||||||
|
|
||||||
@@ -83,19 +84,26 @@
|
|||||||
containerHeight = geometry.containerHeight;
|
containerHeight = geometry.containerHeight;
|
||||||
containerWidth = geometry.containerWidth;
|
containerWidth = geometry.containerWidth;
|
||||||
for (const [i, asset] of assets.entries()) {
|
for (const [i, asset] of assets.entries()) {
|
||||||
const layout = {
|
const top = geometry.getTop(i);
|
||||||
asset,
|
const left = geometry.getLeft(i);
|
||||||
top: geometry.getTop(i),
|
const width = geometry.getWidth(i);
|
||||||
left: geometry.getLeft(i),
|
const height = geometry.getHeight(i);
|
||||||
width: geometry.getWidth(i),
|
|
||||||
height: geometry.getHeight(i),
|
const layoutTopWithOffset = top + pageHeaderOffset;
|
||||||
};
|
const layoutBottom = layoutTopWithOffset + height;
|
||||||
// 54 is the content height of the asset-selection-app-bar
|
|
||||||
const layoutTopWithOffset = layout.top + pageHeaderOffset;
|
|
||||||
const layoutBottom = layoutTopWithOffset + layout.height;
|
|
||||||
|
|
||||||
const display = layoutTopWithOffset < slidingWindow.bottom && layoutBottom > slidingWindow.top;
|
const display = layoutTopWithOffset < slidingWindow.bottom && layoutBottom > slidingWindow.top;
|
||||||
assetLayout.push({ ...layout, display });
|
|
||||||
|
const layout = {
|
||||||
|
asset,
|
||||||
|
top,
|
||||||
|
left,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
display,
|
||||||
|
};
|
||||||
|
|
||||||
|
assetLayout.push(layout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +117,7 @@
|
|||||||
let showShortcuts = $state(false);
|
let showShortcuts = $state(false);
|
||||||
let currentViewAssetIndex = 0;
|
let currentViewAssetIndex = 0;
|
||||||
let shiftKeyIsDown = $state(false);
|
let shiftKeyIsDown = $state(false);
|
||||||
let lastAssetMouseEvent: AssetResponseDto | null = $state(null);
|
let lastAssetMouseEvent: TimelineAsset | null = $state(null);
|
||||||
let slidingWindow = $state({ top: 0, bottom: 0 });
|
let slidingWindow = $state({ top: 0, bottom: 0 });
|
||||||
|
|
||||||
const updateSlidingWindow = () => {
|
const updateSlidingWindow = () => {
|
||||||
@@ -139,14 +147,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const viewAssetHandler = async (asset: AssetResponseDto) => {
|
const viewAssetHandler = async (asset: TimelineAsset) => {
|
||||||
currentViewAssetIndex = assets.findIndex((a) => a.id == asset.id);
|
currentViewAssetIndex = assets.findIndex((a) => a.id == asset.id);
|
||||||
setAsset(assets[currentViewAssetIndex]);
|
await setAssetId(assets[currentViewAssetIndex].id);
|
||||||
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
|
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectAllAssets = () => {
|
const selectAllAssets = () => {
|
||||||
assetInteraction.selectAssets(assets);
|
assetInteraction.selectAssets(assets.map((a) => toTimelineAsset(a)));
|
||||||
};
|
};
|
||||||
|
|
||||||
const deselectAllAssets = () => {
|
const deselectAllAssets = () => {
|
||||||
@@ -168,7 +176,7 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectAssets = (asset: AssetResponseDto) => {
|
const handleSelectAssets = (asset: TimelineAsset) => {
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -191,14 +199,14 @@
|
|||||||
assetInteraction.setAssetSelectionStart(deselect ? null : asset);
|
assetInteraction.setAssetSelectionStart(deselect ? null : asset);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectAssetCandidates = (asset: AssetResponseDto | null) => {
|
const handleSelectAssetCandidates = (asset: TimelineAsset | null) => {
|
||||||
if (asset) {
|
if (asset) {
|
||||||
selectAssetCandidates(asset);
|
selectAssetCandidates(asset);
|
||||||
}
|
}
|
||||||
lastAssetMouseEvent = asset;
|
lastAssetMouseEvent = asset;
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectAssetCandidates = (endAsset: AssetResponseDto) => {
|
const selectAssetCandidates = (endAsset: TimelineAsset) => {
|
||||||
if (!shiftKeyIsDown) {
|
if (!shiftKeyIsDown) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -215,7 +223,7 @@
|
|||||||
[start, end] = [end, start];
|
[start, end] = [end, start];
|
||||||
}
|
}
|
||||||
|
|
||||||
assetInteraction.setAssetSelectionCandidates(assets.slice(start, end + 1));
|
assetInteraction.setAssetSelectionCandidates(assets.slice(start, end + 1).map((a) => toTimelineAsset(a)));
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSelectStart = (e: Event) => {
|
const onSelectStart = (e: Event) => {
|
||||||
@@ -293,7 +301,7 @@
|
|||||||
|
|
||||||
const handleNext = async (): Promise<boolean> => {
|
const handleNext = async (): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
let asset: AssetResponseDto | undefined;
|
let asset: { id: string } | undefined;
|
||||||
if (onNext) {
|
if (onNext) {
|
||||||
asset = await onNext();
|
asset = await onNext();
|
||||||
} else {
|
} else {
|
||||||
@@ -317,9 +325,9 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRandom = async (): Promise<AssetResponseDto | undefined> => {
|
const handleRandom = async (): Promise<{ id: string } | undefined> => {
|
||||||
try {
|
try {
|
||||||
let asset: AssetResponseDto | undefined;
|
let asset: { id: string } | undefined;
|
||||||
if (onRandom) {
|
if (onRandom) {
|
||||||
asset = await onRandom();
|
asset = await onRandom();
|
||||||
} else {
|
} else {
|
||||||
@@ -343,7 +351,7 @@
|
|||||||
|
|
||||||
const handlePrevious = async (): Promise<boolean> => {
|
const handlePrevious = async (): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
let asset: AssetResponseDto | undefined;
|
let asset: { id: string } | undefined;
|
||||||
if (onPrevious) {
|
if (onPrevious) {
|
||||||
asset = await onPrevious();
|
asset = await onPrevious();
|
||||||
} else {
|
} else {
|
||||||
@@ -367,9 +375,9 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const navigateToAsset = async (asset?: AssetResponseDto) => {
|
const navigateToAsset = async (asset?: { id: string }) => {
|
||||||
if (asset && asset.id !== $viewingAsset.id) {
|
if (asset && asset.id !== $viewingAsset.id) {
|
||||||
setAsset(asset);
|
await setAssetId(asset.id);
|
||||||
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
|
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -388,14 +396,14 @@
|
|||||||
} else if (currentViewAssetIndex === assets.length) {
|
} else if (currentViewAssetIndex === assets.length) {
|
||||||
await handlePrevious();
|
await handlePrevious();
|
||||||
} else {
|
} else {
|
||||||
setAsset(assets[currentViewAssetIndex]);
|
await setAssetId(assets[currentViewAssetIndex].id);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const assetMouseEventHandler = (asset: AssetResponseDto | null) => {
|
const assetMouseEventHandler = (asset: TimelineAsset | null) => {
|
||||||
if (assetInteraction.selectionActive) {
|
if (assetInteraction.selectionActive) {
|
||||||
handleSelectAssetCandidates(asset);
|
handleSelectAssetCandidates(asset);
|
||||||
}
|
}
|
||||||
@@ -457,31 +465,31 @@
|
|||||||
class="absolute"
|
class="absolute"
|
||||||
style:overflow="clip"
|
style:overflow="clip"
|
||||||
style="width: {layout.width}px; height: {layout.height}px; top: {layout.top}px; left: {layout.left}px"
|
style="width: {layout.width}px; height: {layout.height}px; top: {layout.top}px; left: {layout.left}px"
|
||||||
title={showAssetName ? asset.originalFileName : ''}
|
|
||||||
>
|
>
|
||||||
<Thumbnail
|
<Thumbnail
|
||||||
readonly={disableAssetSelect}
|
readonly={disableAssetSelect}
|
||||||
onClick={(asset) => {
|
onClick={() => {
|
||||||
if (assetInteraction.selectionActive) {
|
if (assetInteraction.selectionActive) {
|
||||||
handleSelectAssets(asset);
|
handleSelectAssets(toTimelineAsset(asset));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void viewAssetHandler(asset);
|
void viewAssetHandler(toTimelineAsset(asset));
|
||||||
}}
|
}}
|
||||||
onSelect={(asset) => handleSelectAssets(asset)}
|
onSelect={() => handleSelectAssets(toTimelineAsset(asset))}
|
||||||
onMouseEvent={() => assetMouseEventHandler(asset)}
|
onMouseEvent={() => assetMouseEventHandler(toTimelineAsset(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)}
|
||||||
thumbnailWidth={layout.width}
|
thumbnailWidth={layout.width}
|
||||||
thumbnailHeight={layout.height}
|
thumbnailHeight={layout.height}
|
||||||
/>
|
/>
|
||||||
|
<!-- note: if using showAssetName then the 'assets' prop must be AssetResponseDto (only used by folders) -->
|
||||||
{#if showAssetName}
|
{#if showAssetName}
|
||||||
<div
|
<div
|
||||||
class="absolute text-center p-1 text-xs font-mono font-semibold w-full bottom-0 bg-gradient-to-t bg-slate-50/75 overflow-clip text-ellipsis whitespace-pre-wrap"
|
class="absolute text-center p-1 text-xs font-mono font-semibold w-full bottom-0 bg-gradient-to-t bg-slate-50/75 overflow-clip text-ellipsis whitespace-pre-wrap"
|
||||||
>
|
>
|
||||||
{asset.originalFileName}
|
{(asset as AssetResponseDto).originalFileName}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { getAssetThumbnailUrl } from '$lib/utils';
|
import { getAssetThumbnailUrl } from '$lib/utils';
|
||||||
import { getAssetResolution, getFileSize } from '$lib/utils/asset-utils';
|
import { getAssetResolution, getFileSize } from '$lib/utils/asset-utils';
|
||||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||||
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import { type AssetResponseDto, getAllAlbums } from '@immich/sdk';
|
import { type AssetResponseDto, getAllAlbums } from '@immich/sdk';
|
||||||
import { mdiHeart, mdiImageMultipleOutline, mdiMagnifyPlus } from '@mdi/js';
|
import { mdiHeart, mdiImageMultipleOutline, mdiMagnifyPlus } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
@@ -36,7 +37,7 @@
|
|||||||
<!-- THUMBNAIL-->
|
<!-- THUMBNAIL-->
|
||||||
<img
|
<img
|
||||||
src={getAssetThumbnailUrl(asset.id)}
|
src={getAssetThumbnailUrl(asset.id)}
|
||||||
alt={$getAltText(asset)}
|
alt={$getAltText(toTimelineAsset(asset))}
|
||||||
title={assetData}
|
title={assetData}
|
||||||
class="h-60 object-cover rounded-t-xl w-full"
|
class="h-60 object-cover rounded-t-xl w-full"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } 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 { timelineAssetFactory } 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', () => {
|
||||||
@@ -11,8 +11,8 @@ describe('AssetInteraction', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('calculates derived values from selection', () => {
|
it('calculates derived values from selection', () => {
|
||||||
assetInteraction.selectAsset(assetFactory.build({ isFavorite: true, isArchived: true, isTrashed: true }));
|
assetInteraction.selectAsset(timelineAssetFactory.build({ isFavorite: true, isArchived: true, isTrashed: true }));
|
||||||
assetInteraction.selectAsset(assetFactory.build({ isFavorite: true, isArchived: false, isTrashed: false }));
|
assetInteraction.selectAsset(timelineAssetFactory.build({ isFavorite: true, isArchived: false, isTrashed: false }));
|
||||||
|
|
||||||
expect(assetInteraction.selectionActive).toBe(true);
|
expect(assetInteraction.selectionActive).toBe(true);
|
||||||
expect(assetInteraction.isAllTrashed).toBe(false);
|
expect(assetInteraction.isAllTrashed).toBe(false);
|
||||||
@@ -22,7 +22,7 @@ describe('AssetInteraction', () => {
|
|||||||
|
|
||||||
it('updates isAllUserOwned when the active user changes', () => {
|
it('updates isAllUserOwned when the active user changes', () => {
|
||||||
const [user1, user2] = userAdminFactory.buildList(2);
|
const [user1, user2] = userAdminFactory.buildList(2);
|
||||||
assetInteraction.selectAsset(assetFactory.build({ ownerId: user1.id }));
|
assetInteraction.selectAsset(timelineAssetFactory.build({ ownerId: user1.id }));
|
||||||
|
|
||||||
const cleanup = $effect.root(() => {
|
const cleanup = $effect.root(() => {
|
||||||
expect(assetInteraction.isAllUserOwned).toBe(false);
|
expect(assetInteraction.isAllUserOwned).toBe(false);
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
|
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
|
||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
import type { AssetResponseDto, UserAdminResponseDto } from '@immich/sdk';
|
import type { 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 class AssetInteraction {
|
||||||
selectedAssets = $state<AssetResponseDto[]>([]);
|
selectedAssets = $state<TimelineAsset[]>([]);
|
||||||
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<TimelineAsset[]>([]);
|
||||||
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<TimelineAsset | null>(null);
|
||||||
selectionActive = $derived(this.selectedAssets.length > 0);
|
selectionActive = $derived(this.selectedAssets.length > 0);
|
||||||
|
|
||||||
private user = fromStore<UserAdminResponseDto | undefined>(user);
|
private user = fromStore<UserAdminResponseDto | undefined>(user);
|
||||||
@@ -24,13 +25,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: TimelineAsset) {
|
||||||
if (!this.hasSelectedAsset(asset.id)) {
|
if (!this.hasSelectedAsset(asset.id)) {
|
||||||
this.selectedAssets.push(asset);
|
this.selectedAssets.push(asset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selectAssets(assets: AssetResponseDto[]) {
|
selectAssets(assets: TimelineAsset[]) {
|
||||||
for (const asset of assets) {
|
for (const asset of assets) {
|
||||||
this.selectAsset(asset);
|
this.selectAsset(asset);
|
||||||
}
|
}
|
||||||
@@ -51,11 +52,11 @@ export class AssetInteraction {
|
|||||||
this.selectedGroup.delete(group);
|
this.selectedGroup.delete(group);
|
||||||
}
|
}
|
||||||
|
|
||||||
setAssetSelectionStart(asset: AssetResponseDto | null) {
|
setAssetSelectionStart(asset: TimelineAsset | null) {
|
||||||
this.assetSelectionStart = asset;
|
this.assetSelectionStart = asset;
|
||||||
}
|
}
|
||||||
|
|
||||||
setAssetSelectionCandidates(assets: AssetResponseDto[]) {
|
setAssetSelectionCandidates(assets: TimelineAsset[]) {
|
||||||
this.assetSelectionCandidates = assets;
|
this.assetSelectionCandidates = assets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
|
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
|
||||||
import { type AssetGridRouteSearchParams } from '$lib/utils/navigation';
|
import { type AssetGridRouteSearchParams } from '$lib/utils/navigation';
|
||||||
import { getAssetInfo, type AssetResponseDto } from '@immich/sdk';
|
import { getAssetInfo, type AssetResponseDto } from '@immich/sdk';
|
||||||
import { readonly, writable } from 'svelte/store';
|
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<TimelineAsset[]>([]);
|
||||||
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: TimelineAsset[] = []) => {
|
||||||
preloadAssets.set(assetsToPreload);
|
preloadAssets.set(assetsToPreload);
|
||||||
viewingAssetStoreState.set(asset);
|
viewingAssetStoreState.set(asset);
|
||||||
viewState.set(true);
|
viewState.set(true);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { sdkMock } from '$lib/__mocks__/sdk.mock';
|
import { sdkMock } from '$lib/__mocks__/sdk.mock';
|
||||||
import { AbortError } from '$lib/utils';
|
import { AbortError } from '$lib/utils';
|
||||||
import { TimeBucketSize, type AssetResponseDto } from '@immich/sdk';
|
import { type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk';
|
||||||
import { assetFactory } from '@test-data/factories/asset-factory';
|
import { timelineAssetFactory, toResponseDto } from '@test-data/factories/asset-factory';
|
||||||
import { AssetStore } from './assets-store.svelte';
|
import { AssetStore, type TimelineAsset } from './assets-store.svelte';
|
||||||
|
|
||||||
describe('AssetStore', () => {
|
describe('AssetStore', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -11,18 +11,22 @@ describe('AssetStore', () => {
|
|||||||
|
|
||||||
describe('init', () => {
|
describe('init', () => {
|
||||||
let assetStore: AssetStore;
|
let assetStore: AssetStore;
|
||||||
const bucketAssets: Record<string, AssetResponseDto[]> = {
|
const bucketAssets: Record<string, TimelineAsset[]> = {
|
||||||
'2024-03-01T00:00:00.000Z': assetFactory
|
'2024-03-01T00:00:00.000Z': timelineAssetFactory
|
||||||
.buildList(1)
|
.buildList(1)
|
||||||
.map((asset) => ({ ...asset, localDateTime: '2024-03-01T00:00:00.000Z' })),
|
.map((asset) => ({ ...asset, localDateTime: '2024-03-01T00:00:00.000Z' })),
|
||||||
'2024-02-01T00:00:00.000Z': assetFactory
|
'2024-02-01T00:00:00.000Z': timelineAssetFactory
|
||||||
.buildList(100)
|
.buildList(100)
|
||||||
.map((asset) => ({ ...asset, localDateTime: '2024-02-01T00:00:00.000Z' })),
|
.map((asset) => ({ ...asset, localDateTime: '2024-02-01T00:00:00.000Z' })),
|
||||||
'2024-01-01T00:00:00.000Z': assetFactory
|
'2024-01-01T00:00:00.000Z': timelineAssetFactory
|
||||||
.buildList(3)
|
.buildList(3)
|
||||||
.map((asset) => ({ ...asset, localDateTime: '2024-01-01T00:00:00.000Z' })),
|
.map((asset) => ({ ...asset, localDateTime: '2024-01-01T00:00:00.000Z' })),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const bucketAssetsResponse: Record<string, TimeBucketAssetResponseDto> = Object.fromEntries(
|
||||||
|
Object.entries(bucketAssets).map(([key, assets]) => [key, toResponseDto(...assets)]),
|
||||||
|
);
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
assetStore = new AssetStore();
|
assetStore = new AssetStore();
|
||||||
sdkMock.getTimeBuckets.mockResolvedValue([
|
sdkMock.getTimeBuckets.mockResolvedValue([
|
||||||
@@ -30,13 +34,14 @@ describe('AssetStore', () => {
|
|||||||
{ count: 100, timeBucket: '2024-02-01T00:00:00.000Z' },
|
{ count: 100, timeBucket: '2024-02-01T00:00:00.000Z' },
|
||||||
{ count: 3, timeBucket: '2024-01-01T00:00:00.000Z' },
|
{ count: 3, timeBucket: '2024-01-01T00:00:00.000Z' },
|
||||||
]);
|
]);
|
||||||
sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssets[timeBucket]));
|
|
||||||
|
sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssetsResponse[timeBucket]));
|
||||||
await assetStore.updateViewport({ width: 1588, height: 1000 });
|
await assetStore.updateViewport({ width: 1588, height: 1000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should load buckets in viewport', () => {
|
it('should load buckets in viewport', () => {
|
||||||
expect(sdkMock.getTimeBuckets).toBeCalledTimes(1);
|
expect(sdkMock.getTimeBuckets).toBeCalledTimes(1);
|
||||||
expect(sdkMock.getTimeBuckets).toBeCalledWith({ size: TimeBucketSize.Month });
|
|
||||||
expect(sdkMock.getTimeBucket).toHaveBeenCalledTimes(2);
|
expect(sdkMock.getTimeBucket).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -48,29 +53,31 @@ describe('AssetStore', () => {
|
|||||||
|
|
||||||
expect(plainBuckets).toEqual(
|
expect(plainBuckets).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({ bucketDate: '2024-03-01T00:00:00.000Z', bucketHeight: 303 }),
|
expect.objectContaining({ bucketDate: '2024-03-01T00:00:00.000Z', bucketHeight: 185.5 }),
|
||||||
expect.objectContaining({ bucketDate: '2024-02-01T00:00:00.000Z', bucketHeight: 4514.333_333_333_333 }),
|
expect.objectContaining({ bucketDate: '2024-02-01T00:00:00.000Z', bucketHeight: 12_016 }),
|
||||||
expect.objectContaining({ bucketDate: '2024-01-01T00:00:00.000Z', bucketHeight: 286 }),
|
expect.objectContaining({ bucketDate: '2024-01-01T00:00:00.000Z', bucketHeight: 286 }),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calculates timeline height', () => {
|
it('calculates timeline height', () => {
|
||||||
expect(assetStore.timelineHeight).toBe(5103.333_333_333_333);
|
expect(assetStore.timelineHeight).toBe(12_487.5);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('loadBucket', () => {
|
describe('loadBucket', () => {
|
||||||
let assetStore: AssetStore;
|
let assetStore: AssetStore;
|
||||||
const bucketAssets: Record<string, AssetResponseDto[]> = {
|
const bucketAssets: Record<string, TimelineAsset[]> = {
|
||||||
'2024-01-03T00:00:00.000Z': assetFactory
|
'2024-01-03T00:00:00.000Z': timelineAssetFactory
|
||||||
.buildList(1)
|
.buildList(1)
|
||||||
.map((asset) => ({ ...asset, localDateTime: '2024-03-01T00:00:00.000Z' })),
|
.map((asset) => ({ ...asset, localDateTime: '2024-03-01T00:00:00.000Z' })),
|
||||||
'2024-01-01T00:00:00.000Z': assetFactory
|
'2024-01-01T00:00:00.000Z': timelineAssetFactory
|
||||||
.buildList(3)
|
.buildList(3)
|
||||||
.map((asset) => ({ ...asset, localDateTime: '2024-01-01T00:00:00.000Z' })),
|
.map((asset) => ({ ...asset, localDateTime: '2024-01-01T00:00:00.000Z' })),
|
||||||
};
|
};
|
||||||
|
const bucketAssetsResponse: Record<string, TimeBucketAssetResponseDto> = Object.fromEntries(
|
||||||
|
Object.entries(bucketAssets).map(([key, assets]) => [key, toResponseDto(...assets)]),
|
||||||
|
);
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
assetStore = new AssetStore();
|
assetStore = new AssetStore();
|
||||||
sdkMock.getTimeBuckets.mockResolvedValue([
|
sdkMock.getTimeBuckets.mockResolvedValue([
|
||||||
@@ -82,7 +89,7 @@ describe('AssetStore', () => {
|
|||||||
if (signal?.aborted) {
|
if (signal?.aborted) {
|
||||||
throw new AbortError();
|
throw new AbortError();
|
||||||
}
|
}
|
||||||
return bucketAssets[timeBucket];
|
return bucketAssetsResponse[timeBucket];
|
||||||
});
|
});
|
||||||
await assetStore.updateViewport({ width: 1588, height: 0 });
|
await assetStore.updateViewport({ width: 1588, height: 0 });
|
||||||
});
|
});
|
||||||
@@ -149,9 +156,8 @@ describe('AssetStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('adds assets to new bucket', () => {
|
it('adds assets to new bucket', () => {
|
||||||
const asset = assetFactory.build({
|
const asset = timelineAssetFactory.build({
|
||||||
localDateTime: '2024-01-20T12:00:00.000Z',
|
localDateTime: '2024-01-20T12:00:00.000Z',
|
||||||
fileCreatedAt: '2024-01-20T12:00:00.000Z',
|
|
||||||
});
|
});
|
||||||
assetStore.addAssets([asset]);
|
assetStore.addAssets([asset]);
|
||||||
|
|
||||||
@@ -163,9 +169,8 @@ describe('AssetStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('adds assets to existing bucket', () => {
|
it('adds assets to existing bucket', () => {
|
||||||
const [assetOne, assetTwo] = assetFactory.buildList(2, {
|
const [assetOne, assetTwo] = timelineAssetFactory.buildList(2, {
|
||||||
localDateTime: '2024-01-20T12:00:00.000Z',
|
localDateTime: '2024-01-20T12:00:00.000Z',
|
||||||
fileCreatedAt: '2024-01-20T12:00:00.000Z',
|
|
||||||
});
|
});
|
||||||
assetStore.addAssets([assetOne]);
|
assetStore.addAssets([assetOne]);
|
||||||
assetStore.addAssets([assetTwo]);
|
assetStore.addAssets([assetTwo]);
|
||||||
@@ -177,16 +182,13 @@ describe('AssetStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('orders assets in buckets by descending date', () => {
|
it('orders assets in buckets by descending date', () => {
|
||||||
const assetOne = assetFactory.build({
|
const assetOne = timelineAssetFactory.build({
|
||||||
fileCreatedAt: '2024-01-20T12:00:00.000Z',
|
|
||||||
localDateTime: '2024-01-20T12:00:00.000Z',
|
localDateTime: '2024-01-20T12:00:00.000Z',
|
||||||
});
|
});
|
||||||
const assetTwo = assetFactory.build({
|
const assetTwo = timelineAssetFactory.build({
|
||||||
fileCreatedAt: '2024-01-15T12:00:00.000Z',
|
|
||||||
localDateTime: '2024-01-15T12:00:00.000Z',
|
localDateTime: '2024-01-15T12:00:00.000Z',
|
||||||
});
|
});
|
||||||
const assetThree = assetFactory.build({
|
const assetThree = timelineAssetFactory.build({
|
||||||
fileCreatedAt: '2024-01-16T12:00:00.000Z',
|
|
||||||
localDateTime: '2024-01-16T12:00:00.000Z',
|
localDateTime: '2024-01-16T12:00:00.000Z',
|
||||||
});
|
});
|
||||||
assetStore.addAssets([assetOne, assetTwo, assetThree]);
|
assetStore.addAssets([assetOne, assetTwo, assetThree]);
|
||||||
@@ -200,9 +202,9 @@ describe('AssetStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('orders buckets by descending date', () => {
|
it('orders buckets by descending date', () => {
|
||||||
const assetOne = assetFactory.build({ localDateTime: '2024-01-20T12:00:00.000Z' });
|
const assetOne = timelineAssetFactory.build({ localDateTime: '2024-01-20T12:00:00.000Z' });
|
||||||
const assetTwo = assetFactory.build({ localDateTime: '2024-04-20T12:00:00.000Z' });
|
const assetTwo = timelineAssetFactory.build({ localDateTime: '2024-04-20T12:00:00.000Z' });
|
||||||
const assetThree = assetFactory.build({ localDateTime: '2023-01-20T12:00:00.000Z' });
|
const assetThree = timelineAssetFactory.build({ localDateTime: '2023-01-20T12:00:00.000Z' });
|
||||||
assetStore.addAssets([assetOne, assetTwo, assetThree]);
|
assetStore.addAssets([assetOne, assetTwo, assetThree]);
|
||||||
|
|
||||||
expect(assetStore.buckets.length).toEqual(3);
|
expect(assetStore.buckets.length).toEqual(3);
|
||||||
@@ -213,7 +215,7 @@ describe('AssetStore', () => {
|
|||||||
|
|
||||||
it('updates existing asset', () => {
|
it('updates existing asset', () => {
|
||||||
const updateAssetsSpy = vi.spyOn(assetStore, 'updateAssets');
|
const updateAssetsSpy = vi.spyOn(assetStore, 'updateAssets');
|
||||||
const asset = assetFactory.build();
|
const asset = timelineAssetFactory.build();
|
||||||
assetStore.addAssets([asset]);
|
assetStore.addAssets([asset]);
|
||||||
|
|
||||||
assetStore.addAssets([asset]);
|
assetStore.addAssets([asset]);
|
||||||
@@ -223,8 +225,8 @@ describe('AssetStore', () => {
|
|||||||
|
|
||||||
// disabled due to the wasm Justified Layout import
|
// disabled due to the wasm Justified Layout import
|
||||||
it('ignores trashed assets when isTrashed is true', async () => {
|
it('ignores trashed assets when isTrashed is true', async () => {
|
||||||
const asset = assetFactory.build({ isTrashed: false });
|
const asset = timelineAssetFactory.build({ isTrashed: false });
|
||||||
const trashedAsset = assetFactory.build({ isTrashed: true });
|
const trashedAsset = timelineAssetFactory.build({ isTrashed: true });
|
||||||
|
|
||||||
const assetStore = new AssetStore();
|
const assetStore = new AssetStore();
|
||||||
await assetStore.updateOptions({ isTrashed: true });
|
await assetStore.updateOptions({ isTrashed: true });
|
||||||
@@ -244,14 +246,14 @@ describe('AssetStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('ignores non-existing assets', () => {
|
it('ignores non-existing assets', () => {
|
||||||
assetStore.updateAssets([assetFactory.build()]);
|
assetStore.updateAssets([timelineAssetFactory.build()]);
|
||||||
|
|
||||||
expect(assetStore.buckets.length).toEqual(0);
|
expect(assetStore.buckets.length).toEqual(0);
|
||||||
expect(assetStore.getAssets().length).toEqual(0);
|
expect(assetStore.getAssets().length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates an asset', () => {
|
it('updates an asset', () => {
|
||||||
const asset = assetFactory.build({ isFavorite: false });
|
const asset = timelineAssetFactory.build({ isFavorite: false });
|
||||||
const updatedAsset = { ...asset, isFavorite: true };
|
const updatedAsset = { ...asset, isFavorite: true };
|
||||||
|
|
||||||
assetStore.addAssets([asset]);
|
assetStore.addAssets([asset]);
|
||||||
@@ -264,7 +266,7 @@ describe('AssetStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('asset moves buckets when asset date changes', () => {
|
it('asset moves buckets when asset date changes', () => {
|
||||||
const asset = assetFactory.build({ localDateTime: '2024-01-20T12:00:00.000Z' });
|
const asset = timelineAssetFactory.build({ localDateTime: '2024-01-20T12:00:00.000Z' });
|
||||||
const updatedAsset = { ...asset, localDateTime: '2024-03-20T12:00:00.000Z' };
|
const updatedAsset = { ...asset, localDateTime: '2024-03-20T12:00:00.000Z' };
|
||||||
|
|
||||||
assetStore.addAssets([asset]);
|
assetStore.addAssets([asset]);
|
||||||
@@ -292,7 +294,7 @@ describe('AssetStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('ignores invalid IDs', () => {
|
it('ignores invalid IDs', () => {
|
||||||
assetStore.addAssets(assetFactory.buildList(2, { localDateTime: '2024-01-20T12:00:00.000Z' }));
|
assetStore.addAssets(timelineAssetFactory.buildList(2, { localDateTime: '2024-01-20T12:00:00.000Z' }));
|
||||||
assetStore.removeAssets(['', 'invalid', '4c7d9acc']);
|
assetStore.removeAssets(['', 'invalid', '4c7d9acc']);
|
||||||
|
|
||||||
expect(assetStore.getAssets().length).toEqual(2);
|
expect(assetStore.getAssets().length).toEqual(2);
|
||||||
@@ -301,7 +303,9 @@ describe('AssetStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('removes asset from bucket', () => {
|
it('removes asset from bucket', () => {
|
||||||
const [assetOne, assetTwo] = assetFactory.buildList(2, { localDateTime: '2024-01-20T12:00:00.000Z' });
|
const [assetOne, assetTwo] = timelineAssetFactory.buildList(2, {
|
||||||
|
localDateTime: '2024-01-20T12:00:00.000Z',
|
||||||
|
});
|
||||||
assetStore.addAssets([assetOne, assetTwo]);
|
assetStore.addAssets([assetOne, assetTwo]);
|
||||||
assetStore.removeAssets([assetOne.id]);
|
assetStore.removeAssets([assetOne.id]);
|
||||||
|
|
||||||
@@ -311,7 +315,7 @@ describe('AssetStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('does not remove bucket when empty', () => {
|
it('does not remove bucket when empty', () => {
|
||||||
const assets = assetFactory.buildList(2, { localDateTime: '2024-01-20T12:00:00.000Z' });
|
const assets = timelineAssetFactory.buildList(2, { localDateTime: '2024-01-20T12:00:00.000Z' });
|
||||||
assetStore.addAssets(assets);
|
assetStore.addAssets(assets);
|
||||||
assetStore.removeAssets(assets.map((asset) => asset.id));
|
assetStore.removeAssets(assets.map((asset) => asset.id));
|
||||||
|
|
||||||
@@ -334,12 +338,10 @@ describe('AssetStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('populated store returns first asset', () => {
|
it('populated store returns first asset', () => {
|
||||||
const assetOne = assetFactory.build({
|
const assetOne = timelineAssetFactory.build({
|
||||||
fileCreatedAt: '2024-01-20T12:00:00.000Z',
|
|
||||||
localDateTime: '2024-01-20T12:00:00.000Z',
|
localDateTime: '2024-01-20T12:00:00.000Z',
|
||||||
});
|
});
|
||||||
const assetTwo = assetFactory.build({
|
const assetTwo = timelineAssetFactory.build({
|
||||||
fileCreatedAt: '2024-01-15T12:00:00.000Z',
|
|
||||||
localDateTime: '2024-01-15T12:00:00.000Z',
|
localDateTime: '2024-01-15T12:00:00.000Z',
|
||||||
});
|
});
|
||||||
assetStore.addAssets([assetOne, assetTwo]);
|
assetStore.addAssets([assetOne, assetTwo]);
|
||||||
@@ -349,17 +351,20 @@ describe('AssetStore', () => {
|
|||||||
|
|
||||||
describe('getPreviousAsset', () => {
|
describe('getPreviousAsset', () => {
|
||||||
let assetStore: AssetStore;
|
let assetStore: AssetStore;
|
||||||
const bucketAssets: Record<string, AssetResponseDto[]> = {
|
const bucketAssets: Record<string, TimelineAsset[]> = {
|
||||||
'2024-03-01T00:00:00.000Z': assetFactory
|
'2024-03-01T00:00:00.000Z': timelineAssetFactory
|
||||||
.buildList(1)
|
.buildList(1)
|
||||||
.map((asset) => ({ ...asset, localDateTime: '2024-03-01T00:00:00.000Z' })),
|
.map((asset) => ({ ...asset, localDateTime: '2024-03-01T00:00:00.000Z' })),
|
||||||
'2024-02-01T00:00:00.000Z': assetFactory
|
'2024-02-01T00:00:00.000Z': timelineAssetFactory
|
||||||
.buildList(6)
|
.buildList(6)
|
||||||
.map((asset) => ({ ...asset, localDateTime: '2024-02-01T00:00:00.000Z' })),
|
.map((asset) => ({ ...asset, localDateTime: '2024-02-01T00:00:00.000Z' })),
|
||||||
'2024-01-01T00:00:00.000Z': assetFactory
|
'2024-01-01T00:00:00.000Z': timelineAssetFactory
|
||||||
.buildList(3)
|
.buildList(3)
|
||||||
.map((asset) => ({ ...asset, localDateTime: '2024-01-01T00:00:00.000Z' })),
|
.map((asset) => ({ ...asset, localDateTime: '2024-01-01T00:00:00.000Z' })),
|
||||||
};
|
};
|
||||||
|
const bucketAssetsResponse: Record<string, TimeBucketAssetResponseDto> = Object.fromEntries(
|
||||||
|
Object.entries(bucketAssets).map(([key, assets]) => [key, toResponseDto(...assets)]),
|
||||||
|
);
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
assetStore = new AssetStore();
|
assetStore = new AssetStore();
|
||||||
@@ -368,8 +373,7 @@ describe('AssetStore', () => {
|
|||||||
{ count: 6, timeBucket: '2024-02-01T00:00:00.000Z' },
|
{ count: 6, timeBucket: '2024-02-01T00:00:00.000Z' },
|
||||||
{ count: 3, timeBucket: '2024-01-01T00:00:00.000Z' },
|
{ count: 3, timeBucket: '2024-01-01T00:00:00.000Z' },
|
||||||
]);
|
]);
|
||||||
sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssets[timeBucket]));
|
sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssetsResponse[timeBucket]));
|
||||||
|
|
||||||
await assetStore.updateViewport({ width: 1588, height: 1000 });
|
await assetStore.updateViewport({ width: 1588, height: 1000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -445,8 +449,8 @@ describe('AssetStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns the bucket index', () => {
|
it('returns the bucket index', () => {
|
||||||
const assetOne = assetFactory.build({ localDateTime: '2024-01-20T12:00:00.000Z' });
|
const assetOne = timelineAssetFactory.build({ localDateTime: '2024-01-20T12:00:00.000Z' });
|
||||||
const assetTwo = assetFactory.build({ localDateTime: '2024-02-15T12:00:00.000Z' });
|
const assetTwo = timelineAssetFactory.build({ localDateTime: '2024-02-15T12:00:00.000Z' });
|
||||||
assetStore.addAssets([assetOne, assetTwo]);
|
assetStore.addAssets([assetOne, assetTwo]);
|
||||||
|
|
||||||
expect(assetStore.getBucketIndexByAssetId(assetTwo.id)?.bucketDate).toEqual('2024-02-01T00:00:00.000Z');
|
expect(assetStore.getBucketIndexByAssetId(assetTwo.id)?.bucketDate).toEqual('2024-02-01T00:00:00.000Z');
|
||||||
@@ -454,8 +458,8 @@ describe('AssetStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('ignores removed buckets', () => {
|
it('ignores removed buckets', () => {
|
||||||
const assetOne = assetFactory.build({ localDateTime: '2024-01-20T12:00:00.000Z' });
|
const assetOne = timelineAssetFactory.build({ localDateTime: '2024-01-20T12:00:00.000Z' });
|
||||||
const assetTwo = assetFactory.build({ localDateTime: '2024-02-15T12:00:00.000Z' });
|
const assetTwo = timelineAssetFactory.build({ localDateTime: '2024-02-15T12:00:00.000Z' });
|
||||||
assetStore.addAssets([assetOne, assetTwo]);
|
assetStore.addAssets([assetOne, assetTwo]);
|
||||||
|
|
||||||
assetStore.removeAssets([assetTwo.id]);
|
assetStore.removeAssets([assetTwo.id]);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { CancellableTask } from '$lib/utils/cancellable-task';
|
import { CancellableTask } from '$lib/utils/cancellable-task';
|
||||||
import {
|
import {
|
||||||
@@ -6,26 +7,22 @@ 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 { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import { TUNABLES } from '$lib/utils/tunables';
|
import { TUNABLES } from '$lib/utils/tunables';
|
||||||
import {
|
import {
|
||||||
AssetOrder,
|
AssetOrder,
|
||||||
getAssetInfo,
|
getAssetInfo,
|
||||||
getTimeBucket,
|
getTimeBucket,
|
||||||
getTimeBuckets,
|
getTimeBuckets,
|
||||||
TimeBucketSize,
|
type AssetStackResponseDto,
|
||||||
type AssetResponseDto,
|
type TimeBucketAssetResponseDto,
|
||||||
} 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 { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
|
||||||
import { SvelteSet } from 'svelte/reactivity';
|
import { SvelteSet } from 'svelte/reactivity';
|
||||||
import { get, writable, type Unsubscriber } from 'svelte/store';
|
import { get, writable, type Unsubscriber } from 'svelte/store';
|
||||||
import { handleError } from '../utils/handle-error';
|
import { handleError } from '../utils/handle-error';
|
||||||
import { websocketEvents } from './websocket';
|
import { websocketEvents } from './websocket';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
TIMELINE: { INTERSECTION_EXPAND_TOP, INTERSECTION_EXPAND_BOTTOM },
|
TIMELINE: { INTERSECTION_EXPAND_TOP, INTERSECTION_EXPAND_BOTTOM },
|
||||||
} = TUNABLES;
|
} = TUNABLES;
|
||||||
@@ -60,13 +57,33 @@ 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: Date;
|
||||||
|
isArchived: boolean;
|
||||||
|
isFavorite: boolean;
|
||||||
|
isTrashed: boolean;
|
||||||
|
isVideo: boolean;
|
||||||
|
isImage: boolean;
|
||||||
|
stack: AssetStackResponseDto | null;
|
||||||
|
duration: string | null;
|
||||||
|
projectionType: string | null;
|
||||||
|
livePhotoVideoId: string | null;
|
||||||
|
city: string | null;
|
||||||
|
country: string | null;
|
||||||
|
people: string[];
|
||||||
|
};
|
||||||
class IntersectingAsset {
|
class IntersectingAsset {
|
||||||
// --- public ---
|
// --- public ---
|
||||||
readonly #group: AssetDateGroup;
|
readonly #group: AssetDateGroup;
|
||||||
@@ -90,22 +107,23 @@ 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;
|
||||||
readonly index: number;
|
readonly index: number;
|
||||||
readonly date: DateTime;
|
readonly date: Date;
|
||||||
|
readonly groupTitle: string;
|
||||||
readonly dayOfMonth: number;
|
readonly dayOfMonth: number;
|
||||||
intersetingAssets: IntersectingAsset[] = $state([]);
|
intersetingAssets: IntersectingAsset[] = $state([]);
|
||||||
|
|
||||||
@@ -120,24 +138,23 @@ export class AssetDateGroup {
|
|||||||
col = $state(0);
|
col = $state(0);
|
||||||
deferredLayout = false;
|
deferredLayout = false;
|
||||||
|
|
||||||
constructor(bucket: AssetBucket, index: number, date: DateTime, dayOfMonth: number) {
|
constructor(bucket: AssetBucket, index: number, date: Date, dayOfMonth: number) {
|
||||||
this.index = index;
|
this.index = index;
|
||||||
this.bucket = bucket;
|
this.bucket = bucket;
|
||||||
this.date = date;
|
this.date = date;
|
||||||
this.dayOfMonth = dayOfMonth;
|
this.dayOfMonth = dayOfMonth;
|
||||||
|
this.groupTitle = new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()).toLocaleString(
|
||||||
|
get(locale),
|
||||||
|
{ timeZone: 'UTC', weekday: 'short', year: 'numeric', month: 'long', day: 'numeric' },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
sortAssets(sortOrder: AssetOrder = AssetOrder.Desc) {
|
sortAssets(sortOrder: AssetOrder = AssetOrder.Desc) {
|
||||||
this.intersetingAssets.sort((a, b) => {
|
if (sortOrder === AssetOrder.Asc) {
|
||||||
const aDate = DateTime.fromISO(a.asset!.fileCreatedAt).toUTC();
|
this.intersetingAssets.sort((a, b) => a.asset!.localDateTime.valueOf() - b.asset!.localDateTime.valueOf());
|
||||||
const bDate = DateTime.fromISO(b.asset!.fileCreatedAt).toUTC();
|
} else {
|
||||||
|
this.intersetingAssets.sort((a, b) => b.asset!.localDateTime.valueOf() - a.asset!.localDateTime.valueOf());
|
||||||
if (sortOrder === AssetOrder.Asc) {
|
}
|
||||||
return aDate.diff(bDate).milliseconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
return bDate.diff(aDate).milliseconds;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getFirstAsset() {
|
getFirstAsset() {
|
||||||
@@ -167,27 +184,28 @@ export class AssetDateGroup {
|
|||||||
let changedGeometry = false;
|
let changedGeometry = false;
|
||||||
for (const assetId of unprocessedIds) {
|
for (const assetId of unprocessedIds) {
|
||||||
const index = this.intersetingAssets.findIndex((ia) => ia.id == assetId);
|
const index = this.intersetingAssets.findIndex((ia) => ia.id == assetId);
|
||||||
if (index !== -1) {
|
if (index === -1) {
|
||||||
const asset = this.intersetingAssets[index].asset!;
|
continue;
|
||||||
const oldTime = asset.localDateTime;
|
}
|
||||||
let { remove } = operation(asset);
|
|
||||||
const newTime = asset.localDateTime;
|
const asset = this.intersetingAssets[index].asset!;
|
||||||
if (oldTime !== newTime) {
|
const oldTime = asset.localDateTime;
|
||||||
const utc = DateTime.fromISO(asset.localDateTime).toUTC().startOf('month');
|
let { remove } = operation(asset);
|
||||||
const year = utc.get('year');
|
const newTime = asset.localDateTime;
|
||||||
const month = utc.get('month');
|
if (oldTime.valueOf() !== newTime.valueOf()) {
|
||||||
if (this.bucket.year !== year || this.bucket.month !== month) {
|
const year = newTime.getUTCFullYear();
|
||||||
remove = true;
|
const month = newTime.getUTCMonth();
|
||||||
moveAssets.push({ asset, year, month });
|
if (this.bucket.year !== year || this.bucket.month !== month) {
|
||||||
}
|
remove = true;
|
||||||
}
|
moveAssets.push({ asset, year, month });
|
||||||
unprocessedIds.delete(assetId);
|
|
||||||
processedIds.add(assetId);
|
|
||||||
if (remove || this.bucket.store.isExcluded(asset)) {
|
|
||||||
this.intersetingAssets.splice(index, 1);
|
|
||||||
changedGeometry = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
unprocessedIds.delete(assetId);
|
||||||
|
processedIds.add(assetId);
|
||||||
|
if (remove || this.bucket.store.isExcluded(asset)) {
|
||||||
|
this.intersetingAssets.splice(index, 1);
|
||||||
|
changedGeometry = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { moveAssets, processedIds, unprocessedIds, changedGeometry };
|
return { moveAssets, processedIds, unprocessedIds, changedGeometry };
|
||||||
}
|
}
|
||||||
@@ -210,10 +228,6 @@ export class AssetDateGroup {
|
|||||||
get absoluteDateGroupTop() {
|
get absoluteDateGroupTop() {
|
||||||
return this.bucket.top + this.top;
|
return this.bucket.top + this.top;
|
||||||
}
|
}
|
||||||
|
|
||||||
get groupTitle() {
|
|
||||||
return formatDateGroupTitle(this.date);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Viewport {
|
export interface Viewport {
|
||||||
@@ -225,6 +239,26 @@ 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);
|
||||||
@@ -261,22 +295,20 @@ export class AssetBucket {
|
|||||||
readonly month: number;
|
readonly month: number;
|
||||||
readonly year: number;
|
readonly year: number;
|
||||||
|
|
||||||
constructor(store: AssetStore, utcDate: DateTime, initialCount: number, order: AssetOrder = AssetOrder.Desc) {
|
constructor(store: AssetStore, date: Date, initialCount: number, order: AssetOrder = AssetOrder.Desc) {
|
||||||
this.store = store;
|
this.store = store;
|
||||||
this.#initialCount = initialCount;
|
this.#initialCount = initialCount;
|
||||||
this.#sortOrder = order;
|
this.#sortOrder = order;
|
||||||
|
|
||||||
const year = utcDate.get('year');
|
const bucketDateFormatted = date.toLocaleString(get(locale), {
|
||||||
const month = utcDate.get('month');
|
|
||||||
const bucketDateFormatted = utcDate.toJSDate().toLocaleString(get(locale), {
|
|
||||||
month: 'short',
|
month: 'short',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
timeZone: 'UTC',
|
timeZone: 'UTC',
|
||||||
});
|
});
|
||||||
this.bucketDate = utcDate.toISO()!.toString();
|
this.bucketDate = date.toISOString();
|
||||||
this.bucketDateFormatted = bucketDateFormatted;
|
this.bucketDateFormatted = bucketDateFormatted;
|
||||||
this.month = month;
|
this.month = date.getUTCMonth();
|
||||||
this.year = year;
|
this.year = date.getUTCFullYear();
|
||||||
|
|
||||||
this.loader = new CancellableTask(
|
this.loader = new CancellableTask(
|
||||||
() => {
|
() => {
|
||||||
@@ -316,7 +348,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()),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -333,10 +365,10 @@ export class AssetBucket {
|
|||||||
|
|
||||||
sortDateGroups() {
|
sortDateGroups() {
|
||||||
if (this.#sortOrder === AssetOrder.Asc) {
|
if (this.#sortOrder === AssetOrder.Asc) {
|
||||||
return this.dateGroups.sort((a, b) => a.date.diff(b.date).milliseconds);
|
return this.dateGroups.sort((a, b) => a.date.valueOf() - b.date.valueOf());
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.dateGroups.sort((a, b) => b.date.diff(a.date).milliseconds);
|
return this.dateGroups.sort((a, b) => b.date.valueOf() - a.date.valueOf());
|
||||||
}
|
}
|
||||||
|
|
||||||
runAssetOperation(ids: Set<string>, operation: AssetOperation) {
|
runAssetOperation(ids: Set<string>, operation: AssetOperation) {
|
||||||
@@ -381,56 +413,78 @@ 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(bucketAssets: TimeBucketAssetResponseDto) {
|
||||||
const lookupCache: {
|
const time1 = performance.now();
|
||||||
[dayOfMonth: number]: AssetDateGroup;
|
const addContext = new AddContext();
|
||||||
} = {};
|
const people: string[] = [];
|
||||||
const unprocessedAssets: AssetResponseDto[] = [];
|
for (let i = 0; i < bucketAssets.id.length; i++) {
|
||||||
const changedDateGroups = new Set<AssetDateGroup>();
|
const timelineAsset: TimelineAsset = {
|
||||||
const newDateGroups = new Set<AssetDateGroup>();
|
city: bucketAssets.city[i],
|
||||||
for (const asset of assets) {
|
country: bucketAssets.country[i],
|
||||||
const date = DateTime.fromISO(asset.localDateTime).toUTC();
|
duration: bucketAssets.duration[i],
|
||||||
const month = date.get('month');
|
id: bucketAssets.id[i],
|
||||||
const year = date.get('year');
|
isArchived: Boolean(bucketAssets.isArchived[i]),
|
||||||
if (this.month === month && this.year === year) {
|
isFavorite: Boolean(bucketAssets.isFavorite[i]),
|
||||||
const day = date.get('day');
|
isImage: Boolean(bucketAssets.isImage[i]),
|
||||||
let dateGroup: AssetDateGroup | undefined = lookupCache[day];
|
isTrashed: Boolean(bucketAssets.isTrashed[i]),
|
||||||
if (!dateGroup) {
|
isVideo: !bucketAssets.isImage[i],
|
||||||
dateGroup = this.findDateGroupByDay(day);
|
livePhotoVideoId: bucketAssets.livePhotoVideoId[i],
|
||||||
if (dateGroup) {
|
localDateTime: new Date(bucketAssets.localDateTime[i]),
|
||||||
lookupCache[day] = dateGroup;
|
ownerId: bucketAssets.ownerId[i],
|
||||||
}
|
people,
|
||||||
}
|
projectionType: bucketAssets.projectionType[i],
|
||||||
if (dateGroup) {
|
ratio: bucketAssets.ratio[i],
|
||||||
const intersectingAsset = new IntersectingAsset(dateGroup, asset);
|
stack: bucketAssets.stack?.[i]
|
||||||
if (dateGroup.intersetingAssets.some((a) => a.id === asset.id)) {
|
? {
|
||||||
console.error(`Ignoring attempt to add duplicate asset ${asset.id} to ${dateGroup.groupTitle}`);
|
id: bucketAssets.stack[i]![0],
|
||||||
} else {
|
primaryAssetId: bucketAssets.id[i],
|
||||||
dateGroup.intersetingAssets.push(intersectingAsset);
|
assetCount: Number.parseInt(bucketAssets.stack[i]![1]),
|
||||||
changedDateGroups.add(dateGroup);
|
}
|
||||||
}
|
: null,
|
||||||
} else {
|
thumbhash: bucketAssets.thumbhash[i],
|
||||||
dateGroup = new AssetDateGroup(this, this.dateGroups.length, date, day);
|
};
|
||||||
dateGroup.intersetingAssets.push(new IntersectingAsset(dateGroup, asset));
|
this.addTimelineAsset(timelineAsset, addContext);
|
||||||
this.dateGroups.push(dateGroup);
|
|
||||||
lookupCache[day] = dateGroup;
|
|
||||||
newDateGroups.add(dateGroup);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
unprocessedAssets.push(asset);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for (const group of changedDateGroups) {
|
|
||||||
group.sortAssets(this.#sortOrder);
|
addContext.sort(this, this.#sortOrder);
|
||||||
}
|
const time2 = performance.now();
|
||||||
for (const group of newDateGroups) {
|
const time = time2 - time1;
|
||||||
group.sortAssets(this.#sortOrder);
|
console.log(`AssetBucket.addAssets took ${time}ms`);
|
||||||
}
|
return addContext.unprocessedAssets;
|
||||||
if (newDateGroups.size > 0) {
|
|
||||||
this.sortDateGroups();
|
|
||||||
}
|
|
||||||
return unprocessedAssets;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addTimelineAsset(timelineAsset: TimelineAsset, addContext: AddContext) {
|
||||||
|
const month = timelineAsset.localDateTime.getUTCMonth();
|
||||||
|
const year = timelineAsset.localDateTime.getUTCFullYear();
|
||||||
|
if (this.month === month && this.year === year) {
|
||||||
|
const day = timelineAsset.localDateTime.getUTCDay();
|
||||||
|
let dateGroup: AssetDateGroup | undefined = addContext.lookupCache[day];
|
||||||
|
if (!dateGroup) {
|
||||||
|
dateGroup = this.findDateGroupByDay(day);
|
||||||
|
if (dateGroup) {
|
||||||
|
addContext.lookupCache[day] = dateGroup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dateGroup) {
|
||||||
|
const intersectingAsset = new IntersectingAsset(dateGroup, timelineAsset);
|
||||||
|
dateGroup.intersetingAssets.push(intersectingAsset);
|
||||||
|
addContext.changedDateGroups.add(dateGroup);
|
||||||
|
} else {
|
||||||
|
dateGroup = new AssetDateGroup(this, this.dateGroups.length, timelineAsset.localDateTime, day);
|
||||||
|
dateGroup.intersetingAssets.push(new IntersectingAsset(dateGroup, timelineAsset));
|
||||||
|
this.dateGroups.push(dateGroup);
|
||||||
|
addContext.lookupCache[day] = dateGroup;
|
||||||
|
addContext.newDateGroups.add(dateGroup);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
`Year ${year} and month ${month} do not match bucket year ${this.year} and month ${this.month} (${this.bucketDate} vs ${timelineAsset.localDateTime.toISOString()})`,
|
||||||
|
);
|
||||||
|
addContext.unprocessedAssets.push(timelineAsset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getRandomDateGroup() {
|
getRandomDateGroup() {
|
||||||
const random = Math.floor(Math.random() * this.dateGroups.length);
|
const random = Math.floor(Math.random() * this.dateGroups.length);
|
||||||
return this.dateGroups[random];
|
return this.dateGroups[random];
|
||||||
@@ -479,6 +533,7 @@ export class AssetBucket {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get bucketHeight() {
|
get bucketHeight() {
|
||||||
return this.#bucketHeight;
|
return this.#bucketHeight;
|
||||||
}
|
}
|
||||||
@@ -516,12 +571,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 {
|
||||||
@@ -718,9 +773,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] })),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -734,8 +793,8 @@ export class AssetStore {
|
|||||||
|
|
||||||
#getPendingChangeBatches() {
|
#getPendingChangeBatches() {
|
||||||
const batch: {
|
const batch: {
|
||||||
add: AssetResponseDto[];
|
add: TimelineAsset[];
|
||||||
update: AssetResponseDto[];
|
update: TimelineAsset[];
|
||||||
remove: string[];
|
remove: string[];
|
||||||
} = {
|
} = {
|
||||||
add: [],
|
add: [],
|
||||||
@@ -864,13 +923,11 @@ export class AssetStore {
|
|||||||
async #initialiazeTimeBuckets() {
|
async #initialiazeTimeBuckets() {
|
||||||
const timebuckets = await getTimeBuckets({
|
const timebuckets = await getTimeBuckets({
|
||||||
...this.#options,
|
...this.#options,
|
||||||
size: TimeBucketSize.Month,
|
|
||||||
key: authManager.key,
|
key: authManager.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.buckets = timebuckets.map((bucket) => {
|
this.buckets = timebuckets.map((bucket) => {
|
||||||
const utcDate = DateTime.fromISO(bucket.timeBucket).toUTC();
|
return new AssetBucket(this, new Date(bucket.timeBucket), bucket.count, this.#options.order);
|
||||||
return new AssetBucket(this, utcDate, bucket.count, this.#options.order);
|
|
||||||
});
|
});
|
||||||
this.albumAssets.clear();
|
this.albumAssets.clear();
|
||||||
this.#updateViewportGeometry(false);
|
this.#updateViewportGeometry(false);
|
||||||
@@ -904,6 +961,7 @@ export class AssetStore {
|
|||||||
await this.#initialiazeTimeBuckets();
|
await this.#initialiazeTimeBuckets();
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy() {
|
public destroy() {
|
||||||
this.disconnect();
|
this.disconnect();
|
||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
@@ -971,6 +1029,7 @@ export class AssetStore {
|
|||||||
rowWidth: Math.floor(viewportWidth),
|
rowWidth: Math.floor(viewportWidth),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#updateGeometry(bucket: AssetBucket, invalidateHeight: boolean) {
|
#updateGeometry(bucket: AssetBucket, invalidateHeight: boolean) {
|
||||||
if (invalidateHeight) {
|
if (invalidateHeight) {
|
||||||
bucket.isBucketHeightActual = false;
|
bucket.isBucketHeightActual = false;
|
||||||
@@ -1050,9 +1109,9 @@ export class AssetStore {
|
|||||||
cancelable = options.cancelable;
|
cancelable = options.cancelable;
|
||||||
}
|
}
|
||||||
|
|
||||||
const date = DateTime.fromISO(bucketDate).toUTC();
|
const date = new Date(bucketDate);
|
||||||
const year = date.get('year');
|
const year = date.getUTCFullYear();
|
||||||
const month = date.get('month');
|
const month = date.getUTCMonth();
|
||||||
const bucket = this.getBucketByDate(year, month);
|
const bucket = this.getBucketByDate(year, month);
|
||||||
if (!bucket) {
|
if (!bucket) {
|
||||||
return;
|
return;
|
||||||
@@ -1068,32 +1127,30 @@ 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,
|
||||||
size: TimeBucketSize.Month,
|
|
||||||
key: authManager.key,
|
key: authManager.key,
|
||||||
},
|
},
|
||||||
{ signal },
|
{ signal },
|
||||||
);
|
);
|
||||||
if (assets) {
|
if (bucketResponse) {
|
||||||
if (this.#options.timelineAlbumId) {
|
if (this.#options.timelineAlbumId) {
|
||||||
const albumAssets = await getTimeBucket(
|
const albumAssets = await getTimeBucket(
|
||||||
{
|
{
|
||||||
albumId: this.#options.timelineAlbumId,
|
albumId: this.#options.timelineAlbumId,
|
||||||
timeBucket: bucketDate,
|
timeBucket: bucketDate,
|
||||||
size: TimeBucketSize.Month,
|
|
||||||
key: authManager.key,
|
key: authManager.key,
|
||||||
},
|
},
|
||||||
{ 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 })))}`,
|
||||||
@@ -1107,8 +1164,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)) {
|
||||||
@@ -1121,7 +1178,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;
|
||||||
}
|
}
|
||||||
@@ -1129,16 +1186,17 @@ export class AssetStore {
|
|||||||
const updatedDateGroups = new Set<AssetDateGroup>();
|
const updatedDateGroups = new Set<AssetDateGroup>();
|
||||||
|
|
||||||
for (const asset of assets) {
|
for (const asset of assets) {
|
||||||
const utc = DateTime.fromISO(asset.localDateTime).toUTC().startOf('month');
|
const year = asset.localDateTime.getUTCFullYear();
|
||||||
const year = utc.get('year');
|
const month = asset.localDateTime.getUTCMonth();
|
||||||
const month = utc.get('month');
|
|
||||||
let bucket = this.getBucketByDate(year, month);
|
let bucket = this.getBucketByDate(year, month);
|
||||||
|
|
||||||
if (!bucket) {
|
if (!bucket) {
|
||||||
bucket = new AssetBucket(this, utc, 1, this.#options.order);
|
bucket = new AssetBucket(this, asset.localDateTime, 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1164,7 +1222,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, key: authManager.key }));
|
||||||
if (!asset || this.isExcluded(asset)) {
|
if (!asset || this.isExcluded(asset)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1176,23 +1234,21 @@ export class AssetStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async #loadBucketAtTime(localDateTime: string, options?: { cancelable: boolean }) {
|
async #loadBucketAtTime(localDateTime: Date, options?: { cancelable: boolean }) {
|
||||||
let date = fromLocalDateTime(localDateTime);
|
|
||||||
// Only support TimeBucketSize.Month
|
// Only support TimeBucketSize.Month
|
||||||
date = date.set({ day: 1, hour: 0, minute: 0, second: 0, millisecond: 0 });
|
const year = localDateTime.getUTCFullYear();
|
||||||
const iso = date.toISO()!;
|
const month = localDateTime.getUTCMonth();
|
||||||
const year = date.get('year');
|
localDateTime = new Date(year, month);
|
||||||
const month = date.get('month');
|
await this.loadBucket(localDateTime.toISOString(), options);
|
||||||
await this.loadBucket(iso, options);
|
|
||||||
return this.getBucketByDate(year, month);
|
return this.getBucketByDate(year, month);
|
||||||
}
|
}
|
||||||
|
|
||||||
async #getBucketInfoForAsset(asset: AssetResponseDto, options?: { cancelable: boolean }) {
|
async #getBucketInfoForAsset(asset: { id: string; localDateTime: Date | string }, options?: { cancelable: boolean }) {
|
||||||
const bucketInfo = this.#findBucketForAsset(asset.id);
|
const bucketInfo = this.#findBucketForAsset(asset.id);
|
||||||
if (bucketInfo) {
|
if (bucketInfo) {
|
||||||
return bucketInfo;
|
return bucketInfo;
|
||||||
}
|
}
|
||||||
await this.#loadBucketAtTime(asset.localDateTime, options);
|
await this.#loadBucketAtTime(new Date(asset.localDateTime), options);
|
||||||
return this.#findBucketForAsset(asset.id);
|
return this.#findBucketForAsset(asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1221,7 +1277,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);
|
||||||
@@ -1264,8 +1320,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 };
|
||||||
@@ -1287,11 +1343,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: Date | string }): Promise<TimelineAsset | undefined> {
|
||||||
let bucket = await this.#getBucketInfoForAsset(asset);
|
let bucket = await this.#getBucketInfoForAsset(asset);
|
||||||
if (!bucket) {
|
if (!bucket) {
|
||||||
return;
|
return;
|
||||||
@@ -1334,7 +1390,7 @@ export class AssetStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNextAsset(asset: AssetResponseDto): Promise<AssetResponseDto | undefined> {
|
async getNextAsset(asset: { id: string; localDateTime: Date | string }): Promise<TimelineAsset | undefined> {
|
||||||
let bucket = await this.#getBucketInfoForAsset(asset);
|
let bucket = await this.#getBucketInfoForAsset(asset);
|
||||||
if (!bucket) {
|
if (!bucket) {
|
||||||
return;
|
return;
|
||||||
@@ -1373,7 +1429,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,13 +1,8 @@
|
|||||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||||
|
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
|
||||||
import { asLocalTimeISO } from '$lib/utils/date-time';
|
import { asLocalTimeISO } from '$lib/utils/date-time';
|
||||||
import {
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
type AssetResponseDto,
|
import { deleteMemory, type MemoryResponseDto, removeMemoryAssets, searchMemories, updateMemory } from '@immich/sdk';
|
||||||
deleteMemory,
|
|
||||||
type MemoryResponseDto,
|
|
||||||
removeMemoryAssets,
|
|
||||||
searchMemories,
|
|
||||||
updateMemory,
|
|
||||||
} from '@immich/sdk';
|
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
type MemoryIndex = {
|
type MemoryIndex = {
|
||||||
@@ -17,7 +12,7 @@ type MemoryIndex = {
|
|||||||
|
|
||||||
export type MemoryAsset = MemoryIndex & {
|
export type MemoryAsset = MemoryIndex & {
|
||||||
memory: MemoryResponseDto;
|
memory: MemoryResponseDto;
|
||||||
asset: AssetResponseDto;
|
asset: TimelineAsset;
|
||||||
previousMemory?: MemoryResponseDto;
|
previousMemory?: MemoryResponseDto;
|
||||||
previous?: MemoryAsset;
|
previous?: MemoryAsset;
|
||||||
next?: MemoryAsset;
|
next?: MemoryAsset;
|
||||||
@@ -41,7 +36,7 @@ class MemoryStoreSvelte {
|
|||||||
memoryIndex,
|
memoryIndex,
|
||||||
previousMemory: this.memories[memoryIndex - 1],
|
previousMemory: this.memories[memoryIndex - 1],
|
||||||
nextMemory: this.memories[memoryIndex + 1],
|
nextMemory: this.memories[memoryIndex + 1],
|
||||||
asset,
|
asset: toTimelineAsset(asset),
|
||||||
assetIndex,
|
assetIndex,
|
||||||
previous,
|
previous,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 };
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ import { AppRoute } from '$lib/constants';
|
|||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import { downloadManager } from '$lib/managers/download-manager.svelte';
|
import { downloadManager } from '$lib/managers/download-manager.svelte';
|
||||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetsSnapshot, isSelectingAllAssets, type AssetStore } from '$lib/stores/assets-store.svelte';
|
import {
|
||||||
|
assetsSnapshot,
|
||||||
|
isSelectingAllAssets,
|
||||||
|
type AssetStore,
|
||||||
|
type TimelineAsset,
|
||||||
|
} from '$lib/stores/assets-store.svelte';
|
||||||
import { preferences } from '$lib/stores/user.store';
|
import { preferences } from '$lib/stores/user.store';
|
||||||
import { downloadRequest, withError } from '$lib/utils';
|
import { downloadRequest, withError } from '$lib/utils';
|
||||||
import { createAlbum } from '$lib/utils/album-utils';
|
import { createAlbum } from '$lib/utils/album-utils';
|
||||||
@@ -365,7 +370,7 @@ export const getAssetType = (type: AssetTypeEnum) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSelectedAssets = (assets: AssetResponseDto[], user: UserResponseDto | null): string[] => {
|
export const getSelectedAssets = (assets: TimelineAsset[], 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;
|
||||||
@@ -384,7 +389,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: [] };
|
||||||
}
|
}
|
||||||
@@ -404,9 +409,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,
|
||||||
@@ -524,7 +529,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);
|
||||||
@@ -534,9 +539,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,17 +1,15 @@
|
|||||||
import type { AssetResponseDto } from '@immich/sdk';
|
|
||||||
|
|
||||||
export class SlideshowHistory {
|
export class SlideshowHistory {
|
||||||
private history: AssetResponseDto[] = [];
|
private history: { id: string }[] = [];
|
||||||
private index = 0;
|
private index = 0;
|
||||||
|
|
||||||
constructor(private onChange: (asset: AssetResponseDto) => void) {}
|
constructor(private onChange: (asset: { id: string }) => void) {}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.history = [];
|
this.history = [];
|
||||||
this.index = 0;
|
this.index = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
queue(asset: AssetResponseDto) {
|
queue(asset: { id: string }) {
|
||||||
this.history.push(asset);
|
this.history.push(asset);
|
||||||
|
|
||||||
// If we were at the end of the slideshow history, move the index to the new end
|
// If we were at the end of the slideshow history, move the index to the new end
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
|
||||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||||
import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
|
|
||||||
import { init, register, waitLocale } from 'svelte-i18n';
|
import { init, register, waitLocale } from 'svelte-i18n';
|
||||||
|
|
||||||
const onePerson = [{ name: 'person' }];
|
const onePerson = [{ name: 'person' }];
|
||||||
@@ -40,25 +40,17 @@ describe('getAltText', () => {
|
|||||||
'generates correctly formatted alt text when isVideo=$isVideo, city=$city, country=$country, people=$people.length',
|
'generates correctly formatted alt text when isVideo=$isVideo, city=$city, country=$country, people=$people.length',
|
||||||
({ isVideo, city, country, people, expected }) => {
|
({ isVideo, city, country, people, expected }) => {
|
||||||
const asset = {
|
const asset = {
|
||||||
exifInfo: { city, country },
|
city,
|
||||||
|
country,
|
||||||
|
people: (people || [])?.map((p: { name: string }) => p.name),
|
||||||
localDateTime: '2024-01-01T12:00:00.000Z',
|
localDateTime: '2024-01-01T12:00:00.000Z',
|
||||||
people,
|
isVideo,
|
||||||
type: isVideo ? AssetTypeEnum.Video : AssetTypeEnum.Image,
|
isImage: !isVideo,
|
||||||
} as AssetResponseDto;
|
} as unknown as TimelineAsset;
|
||||||
|
|
||||||
getAltText.subscribe((fn) => {
|
getAltText.subscribe((fn) => {
|
||||||
expect(fn(asset)).toEqual(expected);
|
expect(fn(asset)).toEqual(expected);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
it('defaults to the description, if available', () => {
|
|
||||||
const asset = {
|
|
||||||
exifInfo: { description: 'description' },
|
|
||||||
} as AssetResponseDto;
|
|
||||||
|
|
||||||
getAltText.subscribe((fn) => {
|
|
||||||
expect(fn(asset)).toEqual('description');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
|
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
|
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { derived, get } from 'svelte/store';
|
import { derived, get } from 'svelte/store';
|
||||||
import { fromLocalDateTime } from './timeline-util';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate thumbnail size based on number of assets and viewport width
|
* Calculate thumbnail size based on number of assets and viewport width
|
||||||
@@ -39,24 +38,20 @@ export function getThumbnailSize(assetCount: number, viewWidth: number): number
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getAltText = derived(t, ($t) => {
|
export const getAltText = derived(t, ($t) => {
|
||||||
return (asset: AssetResponseDto) => {
|
return (asset: TimelineAsset) => {
|
||||||
if (asset.exifInfo?.description) {
|
const date = asset.localDateTime.toLocaleString(get(locale), { dateStyle: 'long', timeZone: 'UTC' });
|
||||||
return asset.exifInfo.description;
|
const hasPlace = asset.city && asset.country;
|
||||||
}
|
|
||||||
|
|
||||||
const date = fromLocalDateTime(asset.localDateTime).toLocaleString({ dateStyle: 'long' }, { locale: get(locale) });
|
const peopleCount = asset.people.length;
|
||||||
const hasPlace = !!asset.exifInfo?.city && !!asset.exifInfo?.country;
|
const isVideo = asset.isVideo;
|
||||||
const names = asset.people?.filter((p) => p.name).map((p) => p.name) ?? [];
|
|
||||||
const peopleCount = names.length;
|
|
||||||
const isVideo = asset.type === AssetTypeEnum.Video;
|
|
||||||
|
|
||||||
const values = {
|
const values = {
|
||||||
date,
|
date,
|
||||||
city: asset.exifInfo?.city,
|
city: asset.city,
|
||||||
country: asset.exifInfo?.country,
|
country: asset.country,
|
||||||
person1: names[0],
|
person1: peopleCount > 0 ? asset.people[0] : undefined,
|
||||||
person2: names[1],
|
person2: peopleCount > 1 ? asset.people[1] : undefined,
|
||||||
person3: names[2],
|
person3: peopleCount > 2 ? asset.people[2] : undefined,
|
||||||
isVideo,
|
isVideo,
|
||||||
additionalCount: peopleCount > 3 ? peopleCount - 2 : 0,
|
additionalCount: peopleCount > 3 ? peopleCount - 2 : 0,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
|
import type { 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 { 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';
|
||||||
@@ -15,6 +19,8 @@ export const fromLocalDateTime = (localDateTime: string) =>
|
|||||||
export const fromDateTimeOriginal = (dateTimeOriginal: string, timeZone: string) =>
|
export const fromDateTimeOriginal = (dateTimeOriginal: string, timeZone: string) =>
|
||||||
DateTime.fromISO(dateTimeOriginal, { zone: timeZone });
|
DateTime.fromISO(dateTimeOriginal, { zone: timeZone });
|
||||||
|
|
||||||
|
export const startOfMonth = (date: Date) => new Date(date.getFullYear(), date.getMonth());
|
||||||
|
|
||||||
export function formatGroupTitle(_date: DateTime): string {
|
export function formatGroupTitle(_date: DateTime): string {
|
||||||
if (!_date.isValid) {
|
if (!_date.isValid) {
|
||||||
return _date.toString();
|
return _date.toString();
|
||||||
@@ -56,3 +62,37 @@ 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: AssetResponseDto | TimelineAsset): TimelineAsset => {
|
||||||
|
if (isTimelineAsset(unknownAsset)) {
|
||||||
|
return unknownAsset;
|
||||||
|
}
|
||||||
|
const assetResponse = unknownAsset as AssetResponseDto;
|
||||||
|
const { width, height } = getAssetRatio(assetResponse);
|
||||||
|
const ratio = width / height;
|
||||||
|
const city = assetResponse.exifInfo?.city;
|
||||||
|
const country = assetResponse.exifInfo?.country;
|
||||||
|
const people = assetResponse.people?.map((person) => person.name) || [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: assetResponse.id,
|
||||||
|
ownerId: assetResponse.ownerId,
|
||||||
|
ratio,
|
||||||
|
thumbhash: assetResponse.thumbhash,
|
||||||
|
localDateTime: new Date(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,
|
||||||
|
city: city || null,
|
||||||
|
country: country || null,
|
||||||
|
people,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export const isTimelineAsset = (arg: AssetResponseDto | TimelineAsset): arg is TimelineAsset =>
|
||||||
|
(arg as TimelineAsset).ratio !== undefined;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
function getBoolean(string: string | null, fallback: boolean) {
|
function getBoolean(string: string | null, fallback: boolean) {
|
||||||
if (string === null) {
|
if (string === null) {
|
||||||
return fallback;
|
return fallback;
|
||||||
@@ -10,18 +12,23 @@ function getNumber(string: string | null, fallback: number) {
|
|||||||
}
|
}
|
||||||
return Number.parseInt(string);
|
return Number.parseInt(string);
|
||||||
}
|
}
|
||||||
|
const storage = browser
|
||||||
|
? localStorage
|
||||||
|
: {
|
||||||
|
getItem: () => null,
|
||||||
|
};
|
||||||
export const TUNABLES = {
|
export const TUNABLES = {
|
||||||
LAYOUT: {
|
LAYOUT: {
|
||||||
WASM: getBoolean(localStorage.getItem('LAYOUT.WASM'), false),
|
WASM: getBoolean(storage.getItem('LAYOUT.WASM'), false),
|
||||||
},
|
},
|
||||||
TIMELINE: {
|
TIMELINE: {
|
||||||
INTERSECTION_EXPAND_TOP: getNumber(localStorage.getItem('TIMELINE_INTERSECTION_EXPAND_TOP'), 500),
|
INTERSECTION_EXPAND_TOP: getNumber(storage.getItem('TIMELINE_INTERSECTION_EXPAND_TOP'), 500),
|
||||||
INTERSECTION_EXPAND_BOTTOM: getNumber(localStorage.getItem('TIMELINE_INTERSECTION_EXPAND_BOTTOM'), 500),
|
INTERSECTION_EXPAND_BOTTOM: getNumber(storage.getItem('TIMELINE_INTERSECTION_EXPAND_BOTTOM'), 500),
|
||||||
},
|
},
|
||||||
ASSET_GRID: {
|
ASSET_GRID: {
|
||||||
NAVIGATE_ON_ASSET_IN_VIEW: getBoolean(localStorage.getItem('ASSET_GRID.NAVIGATE_ON_ASSET_IN_VIEW'), false),
|
NAVIGATE_ON_ASSET_IN_VIEW: getBoolean(storage.getItem('ASSET_GRID.NAVIGATE_ON_ASSET_IN_VIEW'), false),
|
||||||
},
|
},
|
||||||
IMAGE_THUMBNAIL: {
|
IMAGE_THUMBNAIL: {
|
||||||
THUMBHASH_FADE_DURATION: getNumber(localStorage.getItem('THUMBHASH_FADE_DURATION'), 150),
|
THUMBHASH_FADE_DURATION: getNumber(storage.getItem('THUMBHASH_FADE_DURATION'), 150),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -87,7 +87,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();
|
||||||
@@ -169,8 +169,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 } 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;
|
||||||
|
|||||||
@@ -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 } 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,7 +30,7 @@
|
|||||||
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();
|
||||||
@@ -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 { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils';
|
import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils';
|
||||||
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;
|
||||||
@@ -82,7 +83,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
assetInteraction.selectAssets(data.pathAssets);
|
assetInteraction.selectAssets(data.pathAssets.map((a) => toTimelineAsset(a)));
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -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 } 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;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,28 +1,41 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { afterNavigate, goto } from '$app/navigation';
|
import { afterNavigate, goto } from '$app/navigation';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
|
import { shortcut } from '$lib/actions/shortcut';
|
||||||
|
import AlbumCardGroup from '$lib/components/album-page/album-card-group.svelte';
|
||||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
||||||
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
|
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
|
||||||
|
import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte';
|
||||||
import ChangeDate from '$lib/components/photos-page/actions/change-date-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';
|
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 CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
||||||
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
||||||
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
||||||
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
|
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
|
||||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.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 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 GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
|
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
|
||||||
import { cancelMultiselect } from '$lib/utils/asset-utils';
|
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||||
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
|
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
|
||||||
import { AppRoute, QueryParameter } from '$lib/constants';
|
import { AppRoute, QueryParameter } from '$lib/constants';
|
||||||
|
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 { shortcut } from '$lib/actions/shortcut';
|
import type { TimelineAsset, Viewport } from '$lib/stores/assets-store.svelte';
|
||||||
|
import { lang, locale } from '$lib/stores/preferences.store';
|
||||||
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
|
import { preferences } from '$lib/stores/user.store';
|
||||||
|
import { handlePromiseError } from '$lib/utils';
|
||||||
|
import { cancelMultiselect } from '$lib/utils/asset-utils';
|
||||||
|
import { parseUtcDate } from '$lib/utils/date-time';
|
||||||
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
import { isAlbumsRoute, isPeopleRoute } from '$lib/utils/navigation';
|
||||||
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import {
|
import {
|
||||||
type AlbumResponseDto,
|
type AlbumResponseDto,
|
||||||
type AssetResponseDto,
|
|
||||||
getPerson,
|
getPerson,
|
||||||
getTagById,
|
getTagById,
|
||||||
type MetadataSearchDto,
|
type MetadataSearchDto,
|
||||||
@@ -31,21 +44,8 @@
|
|||||||
type SmartSearchDto,
|
type SmartSearchDto,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
|
import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
|
||||||
import type { Viewport } from '$lib/stores/assets-store.svelte';
|
|
||||||
import { lang, locale } from '$lib/stores/preferences.store';
|
|
||||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
|
||||||
import { handlePromiseError } from '$lib/utils';
|
|
||||||
import { parseUtcDate } from '$lib/utils/date-time';
|
|
||||||
import { featureFlags } from '$lib/stores/server-config.store';
|
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
|
||||||
import AlbumCardGroup from '$lib/components/album-page/album-card-group.svelte';
|
|
||||||
import { isAlbumsRoute, isPeopleRoute } from '$lib/utils/navigation';
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
import { tick } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte';
|
import { t } from 'svelte-i18n';
|
||||||
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';
|
|
||||||
|
|
||||||
const MAX_ASSET_COUNT = 5000;
|
const MAX_ASSET_COUNT = 5000;
|
||||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
|
|
||||||
let nextPage = $state(1);
|
let nextPage = $state(1);
|
||||||
let searchResultAlbums: AlbumResponseDto[] = $state([]);
|
let searchResultAlbums: AlbumResponseDto[] = $state([]);
|
||||||
let searchResultAssets: AssetResponseDto[] = $state([]);
|
let searchResultAssets: TimelineAsset[] = $state([]);
|
||||||
let isLoading = $state(true);
|
let isLoading = $state(true);
|
||||||
let scrollY = $state(0);
|
let scrollY = $state(0);
|
||||||
let scrollYHistory = 0;
|
let scrollYHistory = 0;
|
||||||
@@ -122,7 +122,7 @@
|
|||||||
|
|
||||||
const onAssetDelete = (assetIds: string[]) => {
|
const onAssetDelete = (assetIds: string[]) => {
|
||||||
const assetIdSet = new Set(assetIds);
|
const assetIdSet = new Set(assetIds);
|
||||||
searchResultAssets = searchResultAssets.filter((a: AssetResponseDto) => !assetIdSet.has(a.id));
|
searchResultAssets = searchResultAssets.filter((a: TimelineAsset) => !assetIdSet.has(a.id));
|
||||||
};
|
};
|
||||||
const handleSelectAll = () => {
|
const handleSelectAll = () => {
|
||||||
assetInteraction.selectAssets(searchResultAssets);
|
assetInteraction.selectAssets(searchResultAssets);
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
: await searchAssets({ metadataSearchDto: searchDto });
|
: await searchAssets({ metadataSearchDto: searchDto });
|
||||||
|
|
||||||
searchResultAlbums.push(...albums.items);
|
searchResultAlbums.push(...albums.items);
|
||||||
searchResultAssets.push(...assets.items);
|
searchResultAssets.push(...assets.items.map((a) => toTimelineAsset(a)));
|
||||||
|
|
||||||
nextPage = Number(assets.nextPage) || 0;
|
nextPage = Number(assets.nextPage) || 0;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -238,7 +238,7 @@
|
|||||||
|
|
||||||
if (terms.isNotInAlbum.toString() == 'true') {
|
if (terms.isNotInAlbum.toString() == 'true') {
|
||||||
const assetIdSet = new Set(assetIds);
|
const assetIdSet = new Set(assetIds);
|
||||||
searchResultAssets = searchResultAssets.filter((a: AssetResponseDto) => !assetIdSet.has(a.id));
|
searchResultAssets = searchResultAssets.filter((a) => !assetIdSet.has(a.id));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -22,9 +22,9 @@
|
|||||||
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;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
|
import { AssetTypeEnum, type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk';
|
||||||
import { Sync } from 'factory.ts';
|
import { Sync } from 'factory.ts';
|
||||||
|
|
||||||
export const assetFactory = Sync.makeFactory<AssetResponseDto>({
|
export const assetFactory = Sync.makeFactory<AssetResponseDto>({
|
||||||
@@ -25,3 +26,62 @@ export const assetFactory = Sync.makeFactory<AssetResponseDto>({
|
|||||||
isOffline: Sync.each(() => faker.datatype.boolean()),
|
isOffline: Sync.each(() => faker.datatype.boolean()),
|
||||||
hasMetadata: Sync.each(() => faker.datatype.boolean()),
|
hasMetadata: Sync.each(() => faker.datatype.boolean()),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const timelineAssetFactory = Sync.makeFactory<TimelineAsset>({
|
||||||
|
id: Sync.each(() => faker.string.uuid()),
|
||||||
|
ratio: Sync.each(() => faker.number.int()),
|
||||||
|
ownerId: Sync.each(() => faker.string.uuid()),
|
||||||
|
thumbhash: Sync.each(() => faker.string.alphanumeric(28)),
|
||||||
|
localDateTime: Sync.each(() => faker.date.past()),
|
||||||
|
isFavorite: Sync.each(() => faker.datatype.boolean()),
|
||||||
|
isArchived: false,
|
||||||
|
isTrashed: false,
|
||||||
|
isImage: true,
|
||||||
|
isVideo: false,
|
||||||
|
duration: '0:00:00.00000',
|
||||||
|
stack: null,
|
||||||
|
projectionType: null,
|
||||||
|
livePhotoVideoId: Sync.each(() => faker.string.uuid()),
|
||||||
|
city: faker.location.city(),
|
||||||
|
country: faker.location.country(),
|
||||||
|
people: [faker.person.fullName()],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const toResponseDto = (...timelineAsset: TimelineAsset[]) => {
|
||||||
|
const bucketAssets: TimeBucketAssetResponseDto = {
|
||||||
|
city: [],
|
||||||
|
country: [],
|
||||||
|
duration: [],
|
||||||
|
id: [],
|
||||||
|
isArchived: [],
|
||||||
|
isFavorite: [],
|
||||||
|
isImage: [],
|
||||||
|
isTrashed: [],
|
||||||
|
livePhotoVideoId: [],
|
||||||
|
localDateTime: [],
|
||||||
|
ownerId: [],
|
||||||
|
projectionType: [],
|
||||||
|
ratio: [],
|
||||||
|
stack: [],
|
||||||
|
thumbhash: [],
|
||||||
|
};
|
||||||
|
for (const asset of timelineAsset) {
|
||||||
|
bucketAssets.city.push(asset.city);
|
||||||
|
bucketAssets.country.push(asset.country);
|
||||||
|
bucketAssets.duration.push(asset.duration!);
|
||||||
|
bucketAssets.id.push(asset.id);
|
||||||
|
bucketAssets.isArchived.push(asset.isArchived ? 1 : 0);
|
||||||
|
bucketAssets.isFavorite.push(asset.isFavorite ? 1 : 0);
|
||||||
|
bucketAssets.isImage.push(asset.isImage ? 1 : 0);
|
||||||
|
bucketAssets.isTrashed.push(asset.isTrashed ? 1 : 0);
|
||||||
|
bucketAssets.livePhotoVideoId.push(asset.livePhotoVideoId!);
|
||||||
|
bucketAssets.localDateTime.push(asset.localDateTime.toISOString());
|
||||||
|
bucketAssets.ownerId.push(asset.ownerId);
|
||||||
|
bucketAssets.projectionType.push(asset.projectionType!);
|
||||||
|
bucketAssets.ratio.push(asset.ratio);
|
||||||
|
bucketAssets.stack?.push(asset.stack ? [asset.stack.id, asset.stack.assetCount.toString()] : null);
|
||||||
|
bucketAssets.thumbhash.push(asset.thumbhash!);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bucketAssets;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user