mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c8d0597359 | |||
| 2015f95ff5 | |||
| d4f29ab6ac | |||
| 3decc864b5 | |||
| eca0e60db8 | |||
| 8cff5883b5 | |||
| 3d320d9751 | |||
| b9e0e65bdb | |||
| 88e5e8d6ea | |||
| ee107c98d5 | |||
| affe0ac5ee | |||
| f1d8ab8aae | |||
| c0898b96ca | |||
| 5e9bda7fab | |||
| b60e9c6771 | |||
| b554664791 | |||
| 97c62136b7 | |||
| c1051c7ed2 | |||
| 65bd0a9320 | |||
| bf32864644 | |||
| 7ef7ecec5b | |||
| bc4abd18e4 | |||
| b74cfd4424 | |||
| 7dc84f56c0 | |||
| 92634f923b | |||
| 96b6165bd3 | |||
| 2624f3884f | |||
| f9b7ce9407 | |||
| 013ea37a0d | |||
| b2b4385271 | |||
| 081c75bb21 | |||
| da337578fb | |||
| acf4109171 | |||
| 66601a1fdc | |||
| 02ff077367 | |||
| 94bb6c1a5e | |||
| fe9e5afcf4 | |||
| 5e89efba64 | |||
| 5a457d72c9 | |||
| 45ccdb37fb |
@@ -392,6 +392,8 @@ jobs:
|
|||||||
node-version-file: './server/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
- name: Setup Mise
|
||||||
|
uses: immich-app/devtools/actions/use-mise@035e80a7d4355d5f087ffb95db9e4a0944c04e56 # use-mise-action-v1.1.3
|
||||||
- name: Run pnpm install
|
- name: Run pnpm install
|
||||||
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile
|
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile
|
||||||
- name: Run medium tests
|
- name: Run medium tests
|
||||||
|
|||||||
Vendored
+2
-1
@@ -26,7 +26,8 @@
|
|||||||
},
|
},
|
||||||
"[svelte]": {
|
"[svelte]": {
|
||||||
"editor.defaultFormatter": "svelte.svelte-vscode",
|
"editor.defaultFormatter": "svelte.svelte-vscode",
|
||||||
"editor.formatOnSave": true
|
"editor.formatOnSave": true,
|
||||||
|
"tailwindCSS.lint.suggestCanonicalClasses": "ignore"
|
||||||
},
|
},
|
||||||
"[typescript]": {
|
"[typescript]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[tools]
|
[tools]
|
||||||
terragrunt = "1.0.1"
|
terragrunt = "1.0.2"
|
||||||
opentofu = "1.11.6"
|
opentofu = "1.11.6"
|
||||||
|
|
||||||
[tasks."tg:fmt"]
|
[tasks."tg:fmt"]
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ services:
|
|||||||
container_name: immich_prometheus
|
container_name: immich_prometheus
|
||||||
ports:
|
ports:
|
||||||
- 9090:9090
|
- 9090:9090
|
||||||
image: prom/prometheus@sha256:5550dc63da361dc30f6fe02ac0e4dfc736ededfef3c8d12a634db04a67824d78
|
image: prom/prometheus@sha256:e4254400b85610324913f0dc4acf92603d9984e7519414c5a12811aa6146acc3
|
||||||
volumes:
|
volumes:
|
||||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||||
- prometheus-data:/prometheus
|
- prometheus-data:/prometheus
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ VectorChord is the successor extension to pgvecto.rs, allowing for higher perfor
|
|||||||
|
|
||||||
### Migrating from pgvecto.rs
|
### Migrating from pgvecto.rs
|
||||||
|
|
||||||
Support for pgvecto.rs will be dropped in a later release, hence we recommend all users currently using pgvecto.rs to migrate to VectorChord at their convenience. There are two primary approaches to do so.
|
Support for pgvecto.rs has been dropped as of 3.0, hence all users currently using pgvecto.rs should migrate to VectorChord. There are two primary approaches to do so.
|
||||||
|
|
||||||
The easiest option is to have both extensions installed during the migration:
|
The easiest option is to have both extensions installed during the migration:
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ You can learn how to set up Tailscale together with Immich with the [tutorial vi
|
|||||||
### Cons
|
### Cons
|
||||||
|
|
||||||
- The Tailscale client usually needs to run as root on your devices and it increases the attack surface slightly compared to a minimal Wireguard server. e.g., an [RCE vulnerability](https://github.com/tailscale/tailscale/security/advisories/GHSA-vqp6-rc3h-83cp) was discovered in the Windows Tailscale client in November 2022.
|
- The Tailscale client usually needs to run as root on your devices and it increases the attack surface slightly compared to a minimal Wireguard server. e.g., an [RCE vulnerability](https://github.com/tailscale/tailscale/security/advisories/GHSA-vqp6-rc3h-83cp) was discovered in the Windows Tailscale client in November 2022.
|
||||||
- Tailscale is a paid service. However, there is a generous [free tier](https://tailscale.com/pricing/) that permits up to 3 users and up to 100 devices.
|
- Tailscale is a paid service. However, there is a generous [free tier](https://tailscale.com/pricing/) suitable for personal use.
|
||||||
- Tailscale needs to be installed and running on both server-side and client-side.
|
- Tailscale needs to be installed and running on both server-side and client-side.
|
||||||
|
|
||||||
## Option 3: Reverse Proxy
|
## Option 3: Reverse Proxy
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ Information on the current workers can be found [here](/administration/jobs-work
|
|||||||
| `DB_PASSWORD` | Database password | `postgres` | server, database<sup>\*1</sup> |
|
| `DB_PASSWORD` | Database password | `postgres` | server, database<sup>\*1</sup> |
|
||||||
| `DB_DATABASE_NAME` | Database name | `immich` | server, database<sup>\*1</sup> |
|
| `DB_DATABASE_NAME` | Database name | `immich` | server, database<sup>\*1</sup> |
|
||||||
| `DB_SSL_MODE` | Database SSL mode | | server |
|
| `DB_SSL_MODE` | Database SSL mode | | server |
|
||||||
| `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database vector extension (one of [`vectorchord`, `pgvector`, `pgvecto.rs`]) | | server |
|
| `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database vector extension (one of [`vectorchord`, `pgvector`]) | | server |
|
||||||
| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server |
|
| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server |
|
||||||
| `DB_STORAGE_TYPE` | Optimize concurrent IO on SSDs or sequential IO on HDDs ([`SSD`, `HDD`])<sup>\*3</sup> | `SSD` | database |
|
| `DB_STORAGE_TYPE` | Optimize concurrent IO on SSDs or sequential IO on HDDs ([`SSD`, `HDD`])<sup>\*3</sup> | `SSD` | database |
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,3 @@ These storage mediums have different performance characteristics. As a result, t
|
|||||||
#### Can I use the new database image as a general PostgreSQL image outside of Immich?
|
#### Can I use the new database image as a general PostgreSQL image outside of Immich?
|
||||||
|
|
||||||
It’s a standard PostgreSQL container image that additionally contains the VectorChord, pgvector, and (optionally) pgvecto.rs extensions. If you were using the previous pgvecto.rs image for other purposes, you can similarly do so with this image.
|
It’s a standard PostgreSQL container image that additionally contains the VectorChord, pgvector, and (optionally) pgvecto.rs extensions. If you were using the previous pgvecto.rs image for other purposes, you can similarly do so with this image.
|
||||||
|
|
||||||
#### If pgvecto.rs and pgvector still work, why should I switch to VectorChord?
|
|
||||||
|
|
||||||
VectorChord is faster, more stable, uses less RAM, and (with the settings Immich uses) offers higher-quality results than pgvector and pgvecto.rs. This translates to better search and facial recognition experiences. In addition, pgvecto.rs support will be dropped in the future, so changing it sooner will avoid disruption.
|
|
||||||
|
|||||||
+4
-42
@@ -2,82 +2,44 @@ import { expect } from 'vitest';
|
|||||||
|
|
||||||
export const errorDto = {
|
export const errorDto = {
|
||||||
unauthorized: {
|
unauthorized: {
|
||||||
error: 'Unauthorized',
|
|
||||||
statusCode: 401,
|
|
||||||
message: 'Authentication required',
|
message: 'Authentication required',
|
||||||
correlationId: expect.any(String),
|
|
||||||
},
|
},
|
||||||
unauthorizedWithMessage: (message: string) => ({
|
unauthorizedWithMessage: (message: string) => ({
|
||||||
error: 'Unauthorized',
|
|
||||||
statusCode: 401,
|
|
||||||
message,
|
message,
|
||||||
correlationId: expect.any(String),
|
|
||||||
}),
|
}),
|
||||||
forbidden: {
|
forbidden: {
|
||||||
error: 'Forbidden',
|
|
||||||
statusCode: 403,
|
|
||||||
message: expect.any(String),
|
message: expect.any(String),
|
||||||
correlationId: expect.any(String),
|
|
||||||
},
|
},
|
||||||
missingPermission: (permission: string) => ({
|
missingPermission: (permission: string) => ({
|
||||||
error: 'Forbidden',
|
|
||||||
statusCode: 403,
|
|
||||||
message: `Missing required permission: ${permission}`,
|
message: `Missing required permission: ${permission}`,
|
||||||
correlationId: expect.any(String),
|
|
||||||
}),
|
}),
|
||||||
wrongPassword: {
|
wrongPassword: {
|
||||||
error: 'Bad Request',
|
|
||||||
statusCode: 400,
|
|
||||||
message: 'Wrong password',
|
message: 'Wrong password',
|
||||||
correlationId: expect.any(String),
|
|
||||||
},
|
},
|
||||||
invalidToken: {
|
invalidToken: {
|
||||||
error: 'Unauthorized',
|
|
||||||
statusCode: 401,
|
|
||||||
message: 'Invalid user token',
|
message: 'Invalid user token',
|
||||||
correlationId: expect.any(String),
|
|
||||||
},
|
},
|
||||||
invalidShareKey: {
|
invalidShareKey: {
|
||||||
error: 'Unauthorized',
|
|
||||||
statusCode: 401,
|
|
||||||
message: 'Invalid share key',
|
message: 'Invalid share key',
|
||||||
correlationId: expect.any(String),
|
|
||||||
},
|
},
|
||||||
passwordRequired: {
|
passwordRequired: {
|
||||||
error: 'Unauthorized',
|
|
||||||
statusCode: 401,
|
|
||||||
message: 'Password required',
|
message: 'Password required',
|
||||||
correlationId: expect.any(String),
|
|
||||||
},
|
},
|
||||||
badRequest: (message: any = null) => ({
|
badRequest: (message: any = null) => ({
|
||||||
error: 'Bad Request',
|
|
||||||
statusCode: 400,
|
|
||||||
message: message ?? expect.anything(),
|
message: message ?? expect.anything(),
|
||||||
correlationId: expect.any(String),
|
}),
|
||||||
|
validationError: (errors?: ReadonlyArray<{ path: ReadonlyArray<string | number>; message: string }>) => ({
|
||||||
|
message: 'Validation failed',
|
||||||
|
errors: errors ? expect.arrayContaining(errors.map((e) => expect.objectContaining(e))) : expect.any(Array),
|
||||||
}),
|
}),
|
||||||
noPermission: {
|
noPermission: {
|
||||||
error: 'Bad Request',
|
|
||||||
statusCode: 400,
|
|
||||||
message: expect.stringContaining('Not found or no'),
|
message: expect.stringContaining('Not found or no'),
|
||||||
correlationId: expect.any(String),
|
|
||||||
},
|
},
|
||||||
incorrectLogin: {
|
incorrectLogin: {
|
||||||
error: 'Unauthorized',
|
|
||||||
statusCode: 401,
|
|
||||||
message: 'Incorrect email or password',
|
message: 'Incorrect email or password',
|
||||||
correlationId: expect.any(String),
|
|
||||||
},
|
},
|
||||||
alreadyHasAdmin: {
|
alreadyHasAdmin: {
|
||||||
error: 'Bad Request',
|
|
||||||
statusCode: 400,
|
|
||||||
message: 'The server already has an admin',
|
message: 'The server already has an admin',
|
||||||
correlationId: expect.any(String),
|
|
||||||
},
|
|
||||||
invalidEmail: {
|
|
||||||
error: 'Bad Request',
|
|
||||||
statusCode: 400,
|
|
||||||
message: ['email must be an email'],
|
|
||||||
correlationId: expect.any(String),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,9 @@ describe('/libraries', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[importPaths] Array must have unique items']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['importPaths'], message: 'Array must have unique items' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not create an external library with duplicate exclusion patterns', async () => {
|
it('should not create an external library with duplicate exclusion patterns', async () => {
|
||||||
@@ -125,7 +127,9 @@ describe('/libraries', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[exclusionPatterns] Array must have unique items']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['exclusionPatterns'], message: 'Array must have unique items' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -157,7 +161,9 @@ describe('/libraries', () => {
|
|||||||
.send({ name: '' });
|
.send({ name: '' });
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[name] Too small: expected string to have >=1 characters']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['name'], message: 'Too small: expected string to have >=1 characters' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should change the import paths', async () => {
|
it('should change the import paths', async () => {
|
||||||
@@ -181,7 +187,9 @@ describe('/libraries', () => {
|
|||||||
.send({ importPaths: [''] });
|
.send({ importPaths: [''] });
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[importPaths] Array items must not be empty']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['importPaths'], message: 'Array items must not be empty' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject duplicate import paths', async () => {
|
it('should reject duplicate import paths', async () => {
|
||||||
@@ -191,7 +199,9 @@ describe('/libraries', () => {
|
|||||||
.send({ importPaths: ['/path', '/path'] });
|
.send({ importPaths: ['/path', '/path'] });
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[importPaths] Array must have unique items']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['importPaths'], message: 'Array must have unique items' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should change the exclusion pattern', async () => {
|
it('should change the exclusion pattern', async () => {
|
||||||
@@ -215,7 +225,9 @@ describe('/libraries', () => {
|
|||||||
.send({ exclusionPatterns: ['**/*.jpg', '**/*.jpg'] });
|
.send({ exclusionPatterns: ['**/*.jpg', '**/*.jpg'] });
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[exclusionPatterns] Array must have unique items']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['exclusionPatterns'], message: 'Array must have unique items' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject an empty exclusion pattern', async () => {
|
it('should reject an empty exclusion pattern', async () => {
|
||||||
@@ -225,7 +237,9 @@ describe('/libraries', () => {
|
|||||||
.send({ exclusionPatterns: [''] });
|
.send({ exclusionPatterns: [''] });
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[exclusionPatterns] Array items must not be empty']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['exclusionPatterns'], message: 'Array items must not be empty' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -109,7 +109,9 @@ describe('/map', () => {
|
|||||||
.get('/map/reverse-geocode?lon=123')
|
.get('/map/reverse-geocode?lon=123')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[lat] Invalid input: expected number, received NaN']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['lat'], message: 'Invalid input: expected number, received NaN' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if a lat is not a number', async () => {
|
it('should throw an error if a lat is not a number', async () => {
|
||||||
@@ -117,7 +119,9 @@ describe('/map', () => {
|
|||||||
.get('/map/reverse-geocode?lat=abc&lon=123.456')
|
.get('/map/reverse-geocode?lat=abc&lon=123.456')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[lat] Invalid input: expected number, received NaN']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['lat'], message: 'Invalid input: expected number, received NaN' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if a lat is out of range', async () => {
|
it('should throw an error if a lat is out of range', async () => {
|
||||||
@@ -125,7 +129,9 @@ describe('/map', () => {
|
|||||||
.get('/map/reverse-geocode?lat=91&lon=123.456')
|
.get('/map/reverse-geocode?lat=91&lon=123.456')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[lat] Too big: expected number to be <=90']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['lat'], message: 'Too big: expected number to be <=90' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if a lon is not provided', async () => {
|
it('should throw an error if a lon is not provided', async () => {
|
||||||
@@ -133,7 +139,9 @@ describe('/map', () => {
|
|||||||
.get('/map/reverse-geocode?lat=75')
|
.get('/map/reverse-geocode?lat=75')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[lon] Invalid input: expected number, received NaN']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['lon'], message: 'Invalid input: expected number, received NaN' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const reverseGeocodeTestCases = [
|
const reverseGeocodeTestCases = [
|
||||||
|
|||||||
@@ -105,7 +105,11 @@ describe(`/oauth`, () => {
|
|||||||
it(`should throw an error if a redirect uri is not provided`, async () => {
|
it(`should throw an error if a redirect uri is not provided`, async () => {
|
||||||
const { status, body } = await request(app).post('/oauth/authorize').send({});
|
const { status, body } = await request(app).post('/oauth/authorize').send({});
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[redirectUri] Invalid input: expected string, received undefined']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([
|
||||||
|
{ path: ['redirectUri'], message: 'Invalid input: expected string, received undefined' },
|
||||||
|
]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return a redirect uri', async () => {
|
it('should return a redirect uri', async () => {
|
||||||
@@ -164,13 +168,17 @@ describe(`/oauth`, () => {
|
|||||||
it(`should throw an error if a url is not provided`, async () => {
|
it(`should throw an error if a url is not provided`, async () => {
|
||||||
const { status, body } = await request(app).post('/oauth/callback').send({});
|
const { status, body } = await request(app).post('/oauth/callback').send({});
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[url] Invalid input: expected string, received undefined']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['url'], message: 'Invalid input: expected string, received undefined' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should throw an error if the url is empty`, async () => {
|
it(`should throw an error if the url is empty`, async () => {
|
||||||
const { status, body } = await request(app).post('/oauth/callback').send({ url: '' });
|
const { status, body } = await request(app).post('/oauth/callback').send({ url: '' });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[url] Too small: expected string to have >=1 characters']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['url'], message: 'Too small: expected string to have >=1 characters' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should throw an error if the state is not provided`, async () => {
|
it(`should throw an error if the state is not provided`, async () => {
|
||||||
@@ -332,9 +340,7 @@ describe(`/oauth`, () => {
|
|||||||
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
|
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
|
||||||
expect(status).toBe(500);
|
expect(status).toBe(500);
|
||||||
expect(body).toMatchObject({
|
expect(body).toMatchObject({
|
||||||
error: 'Internal Server Error',
|
|
||||||
message: 'Failed to finish oauth',
|
message: 'Failed to finish oauth',
|
||||||
statusCode: 500,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -353,7 +359,7 @@ describe(`/oauth`, () => {
|
|||||||
const callbackParams = await loginWithOAuth('oauth-no-auto-register');
|
const callbackParams = await loginWithOAuth('oauth-no-auto-register');
|
||||||
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
|
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest('User does not exist and auto registering is disabled.'));
|
expect(body).toEqual(errorDto.badRequest('OAuth authentication failed'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should link to an existing user by email', async () => {
|
it('should link to an existing user by email', async () => {
|
||||||
@@ -377,7 +383,11 @@ describe(`/oauth`, () => {
|
|||||||
it(`should throw an error if the logout_token is not provided`, async () => {
|
it(`should throw an error if the logout_token is not provided`, async () => {
|
||||||
const { status, body } = await request(app).post('/oauth/backchannel-logout').send({});
|
const { status, body } = await request(app).post('/oauth/backchannel-logout').send({});
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[logout_token] Invalid input: expected string, received undefined']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([
|
||||||
|
{ path: ['logout_token'], message: 'Invalid input: expected string, received undefined' },
|
||||||
|
]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should throw an error if an invalid logout token is provided`, async () => {
|
it(`should throw an error if an invalid logout token is provided`, async () => {
|
||||||
@@ -495,11 +505,10 @@ describe(`/oauth`, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should reject OAuth discovery over HTTP', async () => {
|
it('should reject OAuth discovery over HTTP', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status } = await request(app)
|
||||||
.post('/oauth/authorize')
|
.post('/oauth/authorize')
|
||||||
.send({ redirectUri: 'http://127.0.0.1:2285/auth/login' });
|
.send({ redirectUri: 'http://127.0.0.1:2285/auth/login' });
|
||||||
expect(status).toBe(500);
|
expect(status).toBe(500);
|
||||||
expect(body).toMatchObject({ statusCode: 500 });
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -341,7 +341,9 @@ describe('/shared-links', () => {
|
|||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest());
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: [], message: 'Invalid input: expected object, received undefined' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require an asset/album id', async () => {
|
it('should require an asset/album id', async () => {
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ describe('/stacks', () => {
|
|||||||
.send({ assetIds: [asset.id] });
|
.send({ assetIds: [asset.id] });
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest());
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['assetIds'], message: 'Too small: expected array to have >=2 items' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require a valid id', async () => {
|
it('should require a valid id', async () => {
|
||||||
@@ -51,7 +53,12 @@ describe('/stacks', () => {
|
|||||||
.send({ assetIds: [uuidDto.invalid, uuidDto.invalid] });
|
.send({ assetIds: [uuidDto.invalid, uuidDto.invalid] });
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest());
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([
|
||||||
|
{ path: ['assetIds', 0], message: 'Invalid UUID' },
|
||||||
|
{ path: ['assetIds', 1], message: 'Invalid UUID' },
|
||||||
|
]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require access', async () => {
|
it('should require access', async () => {
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ describe('/tags', () => {
|
|||||||
.get(`/tags/${uuidDto.invalid}`)
|
.get(`/tags/${uuidDto.invalid}`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID']));
|
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get tag details', async () => {
|
it('should get tag details', async () => {
|
||||||
@@ -427,7 +427,7 @@ describe('/tags', () => {
|
|||||||
.delete(`/tags/${uuidDto.invalid}`)
|
.delete(`/tags/${uuidDto.invalid}`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID']));
|
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delete a tag', async () => {
|
it('should delete a tag', async () => {
|
||||||
|
|||||||
@@ -108,14 +108,20 @@ describe('/admin/users', () => {
|
|||||||
expect(body).toEqual(errorDto.forbidden);
|
expect(body).toEqual(errorDto.forbidden);
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const key of ['password', 'email', 'name', 'quotaSizeInBytes', 'shouldChangePassword', 'notify']) {
|
for (const [key, message] of [
|
||||||
|
['password', 'Invalid input: expected string, received null'],
|
||||||
|
['email', 'Invalid input: expected email, received object'],
|
||||||
|
['name', 'Invalid input: expected string, received null'],
|
||||||
|
['shouldChangePassword', 'Invalid input: expected boolean, received null'],
|
||||||
|
['notify', 'Invalid input: expected boolean, received null'],
|
||||||
|
] as const) {
|
||||||
it(`should not allow null ${key}`, async () => {
|
it(`should not allow null ${key}`, async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.post(`/admin/users`)
|
.post(`/admin/users`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ ...createUserDto.user1, [key]: null });
|
.send({ ...createUserDto.user1, [key]: null });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest());
|
expect(body).toEqual(errorDto.validationError([{ path: [key], message }]));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,14 +159,19 @@ describe('/admin/users', () => {
|
|||||||
expect(body).toEqual(errorDto.forbidden);
|
expect(body).toEqual(errorDto.forbidden);
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const key of ['password', 'email', 'name', 'shouldChangePassword']) {
|
for (const [key, message] of [
|
||||||
|
['password', 'Invalid input: expected string, received null'],
|
||||||
|
['email', 'Invalid input: expected email, received object'],
|
||||||
|
['name', 'Invalid input: expected string, received null'],
|
||||||
|
['shouldChangePassword', 'Invalid input: expected boolean, received null'],
|
||||||
|
] as const) {
|
||||||
it(`should not allow null ${key}`, async () => {
|
it(`should not allow null ${key}`, async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/admin/users/${uuidDto.notFound}`)
|
.put(`/admin/users/${uuidDto.notFound}`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ [key]: null });
|
.send({ [key]: null });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest());
|
expect(body).toEqual(errorDto.validationError([{ path: [key], message }]));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ describe('/users', () => {
|
|||||||
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toMatchObject(errorDto.badRequest('Email already in use by another account'));
|
expect(body).toMatchObject(errorDto.badRequest('Email is not available'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update my email', async () => {
|
it('should update my email', async () => {
|
||||||
@@ -179,7 +179,9 @@ describe('/users', () => {
|
|||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
errorDto.badRequest(['[download.archiveSize] Invalid input: expected int, received number']),
|
errorDto.validationError([
|
||||||
|
{ path: ['download', 'archiveSize'], message: 'Invalid input: expected int, received number' },
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -207,7 +209,9 @@ describe('/users', () => {
|
|||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
errorDto.badRequest(['[download.includeEmbeddedVideos] Invalid input: expected boolean, received number']),
|
errorDto.validationError([
|
||||||
|
{ path: ['download', 'includeEmbeddedVideos'], message: 'Invalid input: expected boolean, received number' },
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -32,8 +32,12 @@ export function generateThumbhash(rng: SeededRandom): string {
|
|||||||
return Array.from({ length: 10 }, () => rng.nextInt(0, 256).toString(16).padStart(2, '0')).join('');
|
return Array.from({ length: 10 }, () => rng.nextInt(0, 256).toString(16).padStart(2, '0')).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateDuration(rng: SeededRandom): string {
|
export function generateDuration(rng: SeededRandom): number {
|
||||||
return `${rng.nextInt(GENERATION_CONSTANTS.MIN_VIDEO_DURATION_SECONDS, GENERATION_CONSTANTS.MAX_VIDEO_DURATION_SECONDS)}.${rng.nextInt(0, 1000).toString().padStart(3, '0')}`;
|
return (
|
||||||
|
rng.nextInt(GENERATION_CONSTANTS.MIN_VIDEO_DURATION_SECONDS, GENERATION_CONSTANTS.MAX_VIDEO_DURATION_SECONDS) *
|
||||||
|
1000 +
|
||||||
|
rng.nextInt(0, 1000)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateUUID(): string {
|
export function generateUUID(): string {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export type MockTimelineAsset = {
|
|||||||
isTrashed: boolean;
|
isTrashed: boolean;
|
||||||
isVideo: boolean;
|
isVideo: boolean;
|
||||||
isImage: boolean;
|
isImage: boolean;
|
||||||
duration: string | null;
|
duration: number | null;
|
||||||
projectionType: string | null;
|
projectionType: string | null;
|
||||||
livePhotoVideoId: string | null;
|
livePhotoVideoId: string | null;
|
||||||
city: string | null;
|
city: string | null;
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ test.describe('Timeline', () => {
|
|||||||
await page.keyboard.down('Shift');
|
await page.keyboard.down('Shift');
|
||||||
await thumbnailUtils.withAssetId(page, assets[2].id).hover();
|
await thumbnailUtils.withAssetId(page, assets[2].id).hover();
|
||||||
await expect(
|
await expect(
|
||||||
thumbnailUtils.locator(page).locator('.absolute.top-0.h-full.w-full.bg-immich-primary.opacity-40'),
|
thumbnailUtils.locator(page).locator('.absolute.top-0.size-full.bg-immich-primary.opacity-40'),
|
||||||
).toHaveCount(3);
|
).toHaveCount(3);
|
||||||
await thumbnailUtils.selectButton(page, assets[2].id).click();
|
await thumbnailUtils.selectButton(page, assets[2].id).click();
|
||||||
await page.keyboard.up('Shift');
|
await page.keyboard.up('Shift');
|
||||||
|
|||||||
+1
-1
Submodule e2e/test-assets updated: 0eac5a3738...6742055402
@@ -1761,6 +1761,7 @@
|
|||||||
"play_original_video": "Play original video",
|
"play_original_video": "Play original video",
|
||||||
"play_original_video_setting_description": "Prefer playback of original videos rather than transcoded videos. If original asset is not compatible it may not playback correctly.",
|
"play_original_video_setting_description": "Prefer playback of original videos rather than transcoded videos. If original asset is not compatible it may not playback correctly.",
|
||||||
"play_transcoded_video": "Play transcoded video",
|
"play_transcoded_video": "Play transcoded video",
|
||||||
|
"playback_speed": "Playback speed",
|
||||||
"please_auth_to_access": "Please authenticate to access",
|
"please_auth_to_access": "Please authenticate to access",
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
"preferences_settings_subtitle": "Manage the app's preferences",
|
"preferences_settings_subtitle": "Manage the app's preferences",
|
||||||
@@ -2436,6 +2437,7 @@
|
|||||||
"workflows": "Workflows",
|
"workflows": "Workflows",
|
||||||
"workflows_help_text": "Workflows automate actions on your assets based on triggers and filters",
|
"workflows_help_text": "Workflows automate actions on your assets based on triggers and filters",
|
||||||
"wrong_pin_code": "Wrong PIN code",
|
"wrong_pin_code": "Wrong PIN code",
|
||||||
|
"x_of_total": "{x}/{total}",
|
||||||
"year": "Year",
|
"year": "Year",
|
||||||
"years_ago": "{years, plural, one {# year} other {# years}} ago",
|
"years_ago": "{years, plural, one {# year} other {# years}} ago",
|
||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 \
|
|||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
# Pascal support was dropped in 9.11
|
# Pascal support was dropped in 9.11
|
||||||
apt-get install --no-install-recommends -yqq libcudnn9-cuda-12=9.10.2.21-1 && \
|
apt-get install --no-install-recommends -yqq libcudnn9-cuda-12=9.10.2.21-1 tzdata && \
|
||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ ARG RKNN_TOOLKIT_VERSION="v2.3.0"
|
|||||||
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 \
|
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 \
|
||||||
MACHINE_LEARNING_MODEL_ARENA=false
|
MACHINE_LEARNING_MODEL_ARENA=false
|
||||||
|
|
||||||
ADD --checksum=sha256:73993ed4b440460825f21611731564503cc1d5a0c123746477da6cd574f34885 "https://github.com/airockchip/rknn-toolkit2/raw/refs/tags/${RKNN_TOOLKIT_VERSION}/rknpu2/runtime/Linux/librknn_api/aarch64/librknnrt.so" /usr/lib/
|
ADD --chmod=644 --checksum=sha256:73993ed4b440460825f21611731564503cc1d5a0c123746477da6cd574f34885 "https://github.com/airockchip/rknn-toolkit2/raw/refs/tags/${RKNN_TOOLKIT_VERSION}/rknpu2/runtime/Linux/librknn_api/aarch64/librknnrt.so" /usr/lib/
|
||||||
|
|
||||||
FROM prod-${DEVICE} AS prod
|
FROM prod-${DEVICE} AS prod
|
||||||
|
|
||||||
|
|||||||
@@ -183,7 +183,10 @@ async def predict(
|
|||||||
text: str | None = Form(default=None),
|
text: str | None = Form(default=None),
|
||||||
) -> Any:
|
) -> Any:
|
||||||
if image is not None:
|
if image is not None:
|
||||||
inputs: Image | str = await run(lambda: decode_pil(image))
|
decoded = await run(lambda: decode_pil(image))
|
||||||
|
if decoded.width == 0 or decoded.height == 0:
|
||||||
|
raise HTTPException(400, "Image has zero width or height")
|
||||||
|
inputs: Image | str = decoded
|
||||||
elif text is not None:
|
elif text is not None:
|
||||||
inputs = text
|
inputs = text
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1198,6 +1198,19 @@ class TestLoad:
|
|||||||
mock_model.model_format = ModelFormat.ONNX
|
mock_model.model_format = ModelFormat.ONNX
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("size", [(0, 100), (100, 0), (0, 0)])
|
||||||
|
def test_predict_rejects_empty_image(size: tuple[int, int], deployed_app: TestClient) -> None:
|
||||||
|
with mock.patch("immich_ml.main.decode_pil", return_value=Image.new("RGB", size)):
|
||||||
|
response = deployed_app.post(
|
||||||
|
"http://localhost:3003/predict",
|
||||||
|
data={"entries": json.dumps({"clip": {"visual": {"modelName": "ViT-B-32__openai"}}})},
|
||||||
|
files={"image": b"fake image bytes"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert "zero" in response.json()["detail"].lower()
|
||||||
|
|
||||||
|
|
||||||
def test_root_endpoint(deployed_app: TestClient) -> None:
|
def test_root_endpoint(deployed_app: TestClient) -> None:
|
||||||
response = deployed_app.get("http://localhost:3003")
|
response = deployed_app.get("http://localhost:3003")
|
||||||
|
|
||||||
|
|||||||
@@ -15,17 +15,26 @@ config_roots = [
|
|||||||
|
|
||||||
[tools]
|
[tools]
|
||||||
node = "24.15.0"
|
node = "24.15.0"
|
||||||
flutter = "3.41.6"
|
flutter = "3.41.7"
|
||||||
pnpm = "10.33.0"
|
pnpm = "10.33.1"
|
||||||
terragrunt = "1.0.1"
|
terragrunt = "1.0.2"
|
||||||
opentofu = "1.11.6"
|
opentofu = "1.11.6"
|
||||||
java = "21.0.2"
|
java = "21.0.2"
|
||||||
|
|
||||||
[tools."github:CQLabs/homebrew-dcm"]
|
[tools."github:CQLabs/homebrew-dcm"]
|
||||||
version = "1.35.1"
|
version = "1.37.0"
|
||||||
bin = "dcm"
|
bin = "dcm"
|
||||||
postinstall = "chmod +x $MISE_TOOL_INSTALL_PATH/dcm"
|
postinstall = "chmod +x $MISE_TOOL_INSTALL_PATH/dcm"
|
||||||
|
|
||||||
|
[tools."github:jellyfin/jellyfin-ffmpeg"]
|
||||||
|
version = "7.1.3-6"
|
||||||
|
|
||||||
|
[tools."github:jellyfin/jellyfin-ffmpeg".platforms]
|
||||||
|
linux-x64 = { asset_pattern = "jellyfin-ffmpeg_*_portable_linux64-gpl.tar.xz" }
|
||||||
|
linux-arm64 = { asset_pattern = "jellyfin-ffmpeg_*_portable_linuxarm64-gpl.tar.xz" }
|
||||||
|
macos-x64 = { asset_pattern = "jellyfin-ffmpeg_*_portable_mac64-gpl.tar.xz" }
|
||||||
|
macos-arm64 = { asset_pattern = "jellyfin-ffmpeg_*_portable_macarm64-gpl.tar.xz" }
|
||||||
|
|
||||||
[settings]
|
[settings]
|
||||||
experimental = true
|
experimental = true
|
||||||
pin = true
|
pin = true
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"dart.flutterSdkPath": ".fvm/versions/3.41.7",
|
"dart.flutterSdkPath": ".fvm/versions/3.41.9",
|
||||||
"dart.lineLength": 120,
|
"dart.lineLength": 120,
|
||||||
"[dart]": {
|
"[dart]": {
|
||||||
"editor.rulers": [
|
"editor.rulers": [
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
version: '>=1.29.0 <=1.36.0'
|
version: '>=1.29.0 <=1.37.0'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
app_identifier "app.alextran.immich" # The bundle identifier of your app
|
app_identifier "app.alextran.immich" # The bundle identifier of your app
|
||||||
apple_id "alex.tran1502@gmail.com" # Your Apple email address
|
apple_id "altran@futo.org" # Your Apple email address
|
||||||
|
|
||||||
|
|
||||||
# For more information about the Appfile, see:
|
# For more information about the Appfile, see:
|
||||||
|
|||||||
@@ -17,9 +17,10 @@ default_platform(:ios)
|
|||||||
|
|
||||||
platform :ios do
|
platform :ios do
|
||||||
# Constants
|
# Constants
|
||||||
TEAM_ID = "2F67MQ8R79"
|
TEAM_ID = "2W7AC6T8T5"
|
||||||
CODE_SIGN_IDENTITY = "Apple Distribution: Hau Tran (#{TEAM_ID})"
|
CODE_SIGN_IDENTITY = "Apple Distribution: FUTO Holdings, Inc. (#{TEAM_ID})"
|
||||||
BASE_BUNDLE_ID = "app.alextran.immich"
|
BASE_BUNDLE_ID = "app.alextran.immich"
|
||||||
|
DEV_BUNDLE_ID = "tech.futo.immich.testflight"
|
||||||
|
|
||||||
# Helper method to get App Store Connect API key
|
# Helper method to get App Store Connect API key
|
||||||
def get_api_key
|
def get_api_key
|
||||||
@@ -44,16 +45,14 @@ def get_version_from_pubspec
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Helper method to configure code signing for all targets
|
# Helper method to configure code signing for all targets
|
||||||
def configure_code_signing(bundle_id_suffix: "", profile_name_main:, profile_name_share:, profile_name_widget:)
|
def configure_code_signing(base_bundle_id:, profile_name_main:, profile_name_share:, profile_name_widget:)
|
||||||
bundle_suffix = bundle_id_suffix.empty? ? "" : ".#{bundle_id_suffix}"
|
|
||||||
|
|
||||||
# Runner (main app)
|
# Runner (main app)
|
||||||
update_code_signing_settings(
|
update_code_signing_settings(
|
||||||
use_automatic_signing: false,
|
use_automatic_signing: false,
|
||||||
path: "./Runner.xcodeproj",
|
path: "./Runner.xcodeproj",
|
||||||
team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID,
|
team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID,
|
||||||
code_sign_identity: CODE_SIGN_IDENTITY,
|
code_sign_identity: CODE_SIGN_IDENTITY,
|
||||||
bundle_identifier: "#{BASE_BUNDLE_ID}#{bundle_suffix}",
|
bundle_identifier: base_bundle_id,
|
||||||
profile_name: profile_name_main,
|
profile_name: profile_name_main,
|
||||||
targets: ["Runner"]
|
targets: ["Runner"]
|
||||||
)
|
)
|
||||||
@@ -64,7 +63,7 @@ end
|
|||||||
path: "./Runner.xcodeproj",
|
path: "./Runner.xcodeproj",
|
||||||
team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID,
|
team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID,
|
||||||
code_sign_identity: CODE_SIGN_IDENTITY,
|
code_sign_identity: CODE_SIGN_IDENTITY,
|
||||||
bundle_identifier: "#{BASE_BUNDLE_ID}#{bundle_suffix}.ShareExtension",
|
bundle_identifier: "#{base_bundle_id}.ShareExtension",
|
||||||
profile_name: profile_name_share,
|
profile_name: profile_name_share,
|
||||||
targets: ["ShareExtension"]
|
targets: ["ShareExtension"]
|
||||||
)
|
)
|
||||||
@@ -75,7 +74,7 @@ end
|
|||||||
path: "./Runner.xcodeproj",
|
path: "./Runner.xcodeproj",
|
||||||
team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID,
|
team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID,
|
||||||
code_sign_identity: CODE_SIGN_IDENTITY,
|
code_sign_identity: CODE_SIGN_IDENTITY,
|
||||||
bundle_identifier: "#{BASE_BUNDLE_ID}#{bundle_suffix}.Widget",
|
bundle_identifier: "#{base_bundle_id}.Widget",
|
||||||
profile_name: profile_name_widget,
|
profile_name: profile_name_widget,
|
||||||
targets: ["WidgetExtension"]
|
targets: ["WidgetExtension"]
|
||||||
)
|
)
|
||||||
@@ -84,7 +83,7 @@ end
|
|||||||
# Helper method to build and upload to TestFlight
|
# Helper method to build and upload to TestFlight
|
||||||
def build_and_upload(
|
def build_and_upload(
|
||||||
api_key:,
|
api_key:,
|
||||||
bundle_id_suffix: "",
|
base_bundle_id:,
|
||||||
configuration: "Release",
|
configuration: "Release",
|
||||||
distribute_external: true,
|
distribute_external: true,
|
||||||
version_number: nil,
|
version_number: nil,
|
||||||
@@ -92,8 +91,7 @@ end
|
|||||||
profile_name_share:,
|
profile_name_share:,
|
||||||
profile_name_widget:
|
profile_name_widget:
|
||||||
)
|
)
|
||||||
bundle_suffix = bundle_id_suffix.empty? ? "" : ".#{bundle_id_suffix}"
|
app_identifier = base_bundle_id
|
||||||
app_identifier = "#{BASE_BUNDLE_ID}#{bundle_suffix}"
|
|
||||||
|
|
||||||
# Set version number if provided
|
# Set version number if provided
|
||||||
if version_number
|
if version_number
|
||||||
@@ -142,18 +140,18 @@ end
|
|||||||
# Download and install provisioning profiles from App Store Connect
|
# Download and install provisioning profiles from App Store Connect
|
||||||
# Certificate is imported by GHA workflow into build.keychain
|
# Certificate is imported by GHA workflow into build.keychain
|
||||||
# Capture profile names after each sigh call
|
# Capture profile names after each sigh call
|
||||||
sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development", force: true)
|
sigh(api_key: api_key, app_identifier: DEV_BUNDLE_ID, force: true)
|
||||||
main_profile_name = lane_context[SharedValues::SIGH_NAME]
|
main_profile_name = lane_context[SharedValues::SIGH_NAME]
|
||||||
|
|
||||||
sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development.ShareExtension", force: true)
|
sigh(api_key: api_key, app_identifier: "#{DEV_BUNDLE_ID}.ShareExtension", force: true)
|
||||||
share_profile_name = lane_context[SharedValues::SIGH_NAME]
|
share_profile_name = lane_context[SharedValues::SIGH_NAME]
|
||||||
|
|
||||||
sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development.Widget", force: true)
|
sigh(api_key: api_key, app_identifier: "#{DEV_BUNDLE_ID}.Widget", force: true)
|
||||||
widget_profile_name = lane_context[SharedValues::SIGH_NAME]
|
widget_profile_name = lane_context[SharedValues::SIGH_NAME]
|
||||||
|
|
||||||
# Configure code signing for dev bundle IDs using the downloaded profile names
|
# Configure code signing for dev bundle IDs using the downloaded profile names
|
||||||
configure_code_signing(
|
configure_code_signing(
|
||||||
bundle_id_suffix: "development",
|
base_bundle_id: DEV_BUNDLE_ID,
|
||||||
profile_name_main: main_profile_name,
|
profile_name_main: main_profile_name,
|
||||||
profile_name_share: share_profile_name,
|
profile_name_share: share_profile_name,
|
||||||
profile_name_widget: widget_profile_name
|
profile_name_widget: widget_profile_name
|
||||||
@@ -162,7 +160,7 @@ end
|
|||||||
# Build and upload
|
# Build and upload
|
||||||
build_and_upload(
|
build_and_upload(
|
||||||
api_key: api_key,
|
api_key: api_key,
|
||||||
bundle_id_suffix: "development",
|
base_bundle_id: DEV_BUNDLE_ID,
|
||||||
configuration: "Profile",
|
configuration: "Profile",
|
||||||
distribute_external: false,
|
distribute_external: false,
|
||||||
profile_name_main: main_profile_name,
|
profile_name_main: main_profile_name,
|
||||||
@@ -189,6 +187,7 @@ end
|
|||||||
|
|
||||||
# Configure code signing for production bundle IDs
|
# Configure code signing for production bundle IDs
|
||||||
configure_code_signing(
|
configure_code_signing(
|
||||||
|
base_bundle_id: BASE_BUNDLE_ID,
|
||||||
profile_name_main: main_profile_name,
|
profile_name_main: main_profile_name,
|
||||||
profile_name_share: share_profile_name,
|
profile_name_share: share_profile_name,
|
||||||
profile_name_widget: widget_profile_name
|
profile_name_widget: widget_profile_name
|
||||||
@@ -197,6 +196,7 @@ end
|
|||||||
# Build and upload with version number
|
# Build and upload with version number
|
||||||
build_and_upload(
|
build_and_upload(
|
||||||
api_key: api_key,
|
api_key: api_key,
|
||||||
|
base_bundle_id: BASE_BUNDLE_ID,
|
||||||
version_number: get_version_from_pubspec,
|
version_number: get_version_from_pubspec,
|
||||||
distribute_external: false,
|
distribute_external: false,
|
||||||
profile_name_main: main_profile_name,
|
profile_name_main: main_profile_name,
|
||||||
@@ -243,25 +243,25 @@ end
|
|||||||
|
|
||||||
desc "iOS Build Only (no TestFlight upload)"
|
desc "iOS Build Only (no TestFlight upload)"
|
||||||
lane :gha_build_only do
|
lane :gha_build_only do
|
||||||
# Use the same build process as production, just skip the upload
|
# Use the same build process as the dev TestFlight lane, just skip the upload
|
||||||
# This ensures PR builds validate the same way as production builds
|
# This ensures PR builds validate the same way as dev TestFlight builds
|
||||||
|
|
||||||
api_key = get_api_key
|
api_key = get_api_key
|
||||||
|
|
||||||
# Download and install provisioning profiles from App Store Connect
|
# Download and install provisioning profiles from App Store Connect
|
||||||
# Certificate is imported by GHA workflow into build.keychain
|
# Certificate is imported by GHA workflow into build.keychain
|
||||||
sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development", force: true)
|
sigh(api_key: api_key, app_identifier: DEV_BUNDLE_ID, force: true)
|
||||||
main_profile_name = lane_context[SharedValues::SIGH_NAME]
|
main_profile_name = lane_context[SharedValues::SIGH_NAME]
|
||||||
|
|
||||||
sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development.ShareExtension", force: true)
|
sigh(api_key: api_key, app_identifier: "#{DEV_BUNDLE_ID}.ShareExtension", force: true)
|
||||||
share_profile_name = lane_context[SharedValues::SIGH_NAME]
|
share_profile_name = lane_context[SharedValues::SIGH_NAME]
|
||||||
|
|
||||||
sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development.Widget", force: true)
|
sigh(api_key: api_key, app_identifier: "#{DEV_BUNDLE_ID}.Widget", force: true)
|
||||||
widget_profile_name = lane_context[SharedValues::SIGH_NAME]
|
widget_profile_name = lane_context[SharedValues::SIGH_NAME]
|
||||||
|
|
||||||
# Configure code signing for dev bundle IDs
|
# Configure code signing for dev bundle IDs
|
||||||
configure_code_signing(
|
configure_code_signing(
|
||||||
bundle_id_suffix: "development",
|
base_bundle_id: DEV_BUNDLE_ID,
|
||||||
profile_name_main: main_profile_name,
|
profile_name_main: main_profile_name,
|
||||||
profile_name_share: share_profile_name,
|
profile_name_share: share_profile_name,
|
||||||
profile_name_widget: widget_profile_name
|
profile_name_widget: widget_profile_name
|
||||||
@@ -277,9 +277,9 @@ end
|
|||||||
xcargs: "-skipMacroValidation CODE_SIGN_IDENTITY='#{CODE_SIGN_IDENTITY}' CODE_SIGN_STYLE=Manual",
|
xcargs: "-skipMacroValidation CODE_SIGN_IDENTITY='#{CODE_SIGN_IDENTITY}' CODE_SIGN_STYLE=Manual",
|
||||||
export_options: {
|
export_options: {
|
||||||
provisioningProfiles: {
|
provisioningProfiles: {
|
||||||
"#{BASE_BUNDLE_ID}.development" => main_profile_name,
|
DEV_BUNDLE_ID => main_profile_name,
|
||||||
"#{BASE_BUNDLE_ID}.development.ShareExtension" => share_profile_name,
|
"#{DEV_BUNDLE_ID}.ShareExtension" => share_profile_name,
|
||||||
"#{BASE_BUNDLE_ID}.development.Widget" => widget_profile_name
|
"#{DEV_BUNDLE_ID}.Widget" => widget_profile_name
|
||||||
},
|
},
|
||||||
signingStyle: "manual",
|
signingStyle: "manual",
|
||||||
signingCertificate: CODE_SIGN_IDENTITY
|
signingCertificate: CODE_SIGN_IDENTITY
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/sync_event.model.dart';
|
import 'package:immich_mobile/domain/models/sync_event.model.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
@@ -192,17 +193,22 @@ class SyncStreamService {
|
|||||||
final remoteSyncAssets = data.cast<SyncAssetV1>();
|
final remoteSyncAssets = data.cast<SyncAssetV1>();
|
||||||
await _syncStreamRepository.updateAssetsV1(remoteSyncAssets);
|
await _syncStreamRepository.updateAssetsV1(remoteSyncAssets);
|
||||||
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
|
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
|
||||||
final hasPermission = await _localFilesManager.hasManageMediaPermission();
|
await _syncAssetTrashStatus(remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.id).toList());
|
||||||
if (hasPermission) {
|
}
|
||||||
await _handleRemoteTrashed(remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.checksum));
|
return;
|
||||||
await _applyRemoteRestoreToLocal();
|
case SyncEntityType.assetV2:
|
||||||
} else {
|
final remoteSyncAssets = data.cast<SyncAssetV2>();
|
||||||
_logger.warning("sync Trashed Assets cannot proceed because MANAGE_MEDIA permission is missing");
|
await _syncStreamRepository.updateAssetsV2(remoteSyncAssets);
|
||||||
}
|
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
|
||||||
|
await _syncAssetTrashStatus(remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.id).toList());
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case SyncEntityType.assetDeleteV1:
|
case SyncEntityType.assetDeleteV1:
|
||||||
return _syncStreamRepository.deleteAssetsV1(data.cast());
|
final remoteSyncAssets = data.cast<SyncAssetDeleteV1>();
|
||||||
|
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
|
||||||
|
await _syncAssetDeletion(remoteSyncAssets.map((e) => e.assetId).toList());
|
||||||
|
}
|
||||||
|
return _syncStreamRepository.deleteAssetsV1(remoteSyncAssets);
|
||||||
case SyncEntityType.assetExifV1:
|
case SyncEntityType.assetExifV1:
|
||||||
return _syncStreamRepository.updateAssetsExifV1(data.cast());
|
return _syncStreamRepository.updateAssetsExifV1(data.cast());
|
||||||
case SyncEntityType.assetEditV1:
|
case SyncEntityType.assetEditV1:
|
||||||
@@ -215,8 +221,12 @@ class SyncStreamService {
|
|||||||
return _syncStreamRepository.deleteAssetsMetadataV1(data.cast());
|
return _syncStreamRepository.deleteAssetsMetadataV1(data.cast());
|
||||||
case SyncEntityType.partnerAssetV1:
|
case SyncEntityType.partnerAssetV1:
|
||||||
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'partner');
|
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'partner');
|
||||||
|
case SyncEntityType.partnerAssetV2:
|
||||||
|
return _syncStreamRepository.updateAssetsV2(data.cast(), debugLabel: 'partner');
|
||||||
case SyncEntityType.partnerAssetBackfillV1:
|
case SyncEntityType.partnerAssetBackfillV1:
|
||||||
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'partner backfill');
|
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'partner backfill');
|
||||||
|
case SyncEntityType.partnerAssetBackfillV2:
|
||||||
|
return _syncStreamRepository.updateAssetsV2(data.cast(), debugLabel: 'partner backfill');
|
||||||
case SyncEntityType.partnerAssetDeleteV1:
|
case SyncEntityType.partnerAssetDeleteV1:
|
||||||
return _syncStreamRepository.deleteAssetsV1(data.cast(), debugLabel: "partner");
|
return _syncStreamRepository.deleteAssetsV1(data.cast(), debugLabel: "partner");
|
||||||
case SyncEntityType.partnerAssetExifV1:
|
case SyncEntityType.partnerAssetExifV1:
|
||||||
@@ -237,10 +247,16 @@ class SyncStreamService {
|
|||||||
return _syncStreamRepository.deleteAlbumUsersV1(data.cast());
|
return _syncStreamRepository.deleteAlbumUsersV1(data.cast());
|
||||||
case SyncEntityType.albumAssetCreateV1:
|
case SyncEntityType.albumAssetCreateV1:
|
||||||
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'album asset create');
|
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'album asset create');
|
||||||
|
case SyncEntityType.albumAssetCreateV2:
|
||||||
|
return _syncStreamRepository.updateAssetsV2(data.cast(), debugLabel: 'album asset create');
|
||||||
case SyncEntityType.albumAssetUpdateV1:
|
case SyncEntityType.albumAssetUpdateV1:
|
||||||
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'album asset update');
|
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'album asset update');
|
||||||
|
case SyncEntityType.albumAssetUpdateV2:
|
||||||
|
return _syncStreamRepository.updateAssetsV2(data.cast(), debugLabel: 'album asset update');
|
||||||
case SyncEntityType.albumAssetBackfillV1:
|
case SyncEntityType.albumAssetBackfillV1:
|
||||||
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'album asset backfill');
|
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'album asset backfill');
|
||||||
|
case SyncEntityType.albumAssetBackfillV2:
|
||||||
|
return _syncStreamRepository.updateAssetsV2(data.cast(), debugLabel: 'album asset backfill');
|
||||||
case SyncEntityType.albumAssetExifCreateV1:
|
case SyncEntityType.albumAssetExifCreateV1:
|
||||||
return _syncStreamRepository.updateAssetsExifV1(data.cast(), debugLabel: 'album asset exif create');
|
return _syncStreamRepository.updateAssetsExifV1(data.cast(), debugLabel: 'album asset exif create');
|
||||||
case SyncEntityType.albumAssetExifUpdateV1:
|
case SyncEntityType.albumAssetExifUpdateV1:
|
||||||
@@ -342,6 +358,47 @@ class SyncStreamService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> handleWsAssetUploadReadyV2Batch(List<dynamic> batchData) async {
|
||||||
|
if (batchData.isEmpty) return;
|
||||||
|
|
||||||
|
_logger.info('Processing batch of ${batchData.length} AssetUploadReadyV2 events');
|
||||||
|
|
||||||
|
final List<SyncAssetV2> assets = [];
|
||||||
|
final List<SyncAssetExifV1> exifs = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (final data in batchData) {
|
||||||
|
if (data is! Map<String, dynamic>) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final payload = data;
|
||||||
|
final assetData = payload['asset'];
|
||||||
|
final exifData = payload['exif'];
|
||||||
|
|
||||||
|
if (assetData == null || exifData == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final asset = SyncAssetV2.fromJson(assetData);
|
||||||
|
final exif = SyncAssetExifV1.fromJson(exifData);
|
||||||
|
|
||||||
|
if (asset != null && exif != null) {
|
||||||
|
assets.add(asset);
|
||||||
|
exifs.add(exif);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assets.isNotEmpty && exifs.isNotEmpty) {
|
||||||
|
await _syncStreamRepository.updateAssetsV2(assets, debugLabel: 'websocket-batch');
|
||||||
|
await _syncStreamRepository.updateAssetsExifV1(exifs, debugLabel: 'websocket-batch');
|
||||||
|
_logger.info('Successfully processed ${assets.length} assets in batch');
|
||||||
|
}
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
_logger.severe("Error processing AssetUploadReadyV2 websocket batch events", error, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> handleWsAssetEditReadyV1(dynamic data) async {
|
Future<void> handleWsAssetEditReadyV1(dynamic data) async {
|
||||||
_logger.info('Processing AssetEditReadyV1 event');
|
_logger.info('Processing AssetEditReadyV1 event');
|
||||||
|
|
||||||
@@ -382,28 +439,67 @@ class SyncStreamService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleRemoteTrashed(Iterable<String> checksums) async {
|
Future<void> handleWsAssetEditReadyV2(dynamic data) async {
|
||||||
if (checksums.isEmpty) {
|
_logger.info('Processing AssetEditReadyV2 event');
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (data is! Map<String, dynamic>) {
|
||||||
|
throw ArgumentError("Invalid data format for AssetEditReadyV2 event");
|
||||||
|
}
|
||||||
|
|
||||||
|
final payload = data;
|
||||||
|
|
||||||
|
if (payload['asset'] == null) {
|
||||||
|
throw ArgumentError("Missing 'asset' field in AssetEditReadyV2 event data");
|
||||||
|
}
|
||||||
|
|
||||||
|
final asset = SyncAssetV2.fromJson(payload['asset']);
|
||||||
|
if (asset == null) {
|
||||||
|
throw ArgumentError("Failed to parse 'asset' field in AssetEditReadyV2 event data");
|
||||||
|
}
|
||||||
|
|
||||||
|
final assetEdits = (payload['edit'] as List<dynamic>)
|
||||||
|
.map((e) => SyncAssetEditV1.fromJson(e))
|
||||||
|
.whereType<SyncAssetEditV1>()
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
await _syncStreamRepository.updateAssetsV2([asset], debugLabel: 'websocket-edit');
|
||||||
|
await _syncStreamRepository.replaceAssetEditsV1(asset.id, assetEdits, debugLabel: 'websocket-edit');
|
||||||
|
|
||||||
|
_logger.info(
|
||||||
|
'Successfully processed AssetEditReadyV2 event for asset ${asset.id} with ${assetEdits.length} edits',
|
||||||
|
);
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
_logger.severe("Error processing AssetEditReadyV2 websocket event", error, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleRemoteDeleted(Iterable<String> remoteIds) async {
|
||||||
|
if (remoteIds.isEmpty) {
|
||||||
return Future.value();
|
return Future.value();
|
||||||
} else {
|
} else {
|
||||||
final localAssetsToTrash = await _localAssetRepository.getAssetsFromBackupAlbums(checksums);
|
final localAssetsToTrash = await _localAssetRepository.getAssetsFromBackupAlbums(remoteIds);
|
||||||
if (localAssetsToTrash.isNotEmpty) {
|
if (localAssetsToTrash.isNotEmpty) {
|
||||||
final mediaUrls = await Future.wait(
|
await _trashLocalAssets(localAssetsToTrash);
|
||||||
localAssetsToTrash.values
|
|
||||||
.expand((e) => e)
|
|
||||||
.map((localAsset) => _storageRepository.getAssetEntityForAsset(localAsset).then((e) => e?.getMediaUrl())),
|
|
||||||
);
|
|
||||||
_logger.info("Moving to trash ${mediaUrls.join(", ")} assets");
|
|
||||||
final result = await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList());
|
|
||||||
if (result) {
|
|
||||||
await _trashedLocalAssetRepository.trashLocalAsset(localAssetsToTrash);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
_logger.info("No assets found in backup-enabled albums for assets: $checksums");
|
_logger.info("No assets found in backup-enabled albums for remote assets: $remoteIds");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _trashLocalAssets(Map<String, List<LocalAsset>> localAssetsToTrash) async {
|
||||||
|
final mediaUrls = await Future.wait(
|
||||||
|
localAssetsToTrash.values
|
||||||
|
.expand((e) => e)
|
||||||
|
.map((localAsset) => _storageRepository.getAssetEntityForAsset(localAsset).then((e) => e?.getMediaUrl())),
|
||||||
|
);
|
||||||
|
_logger.info("Moving to trash ${mediaUrls.join(", ")} assets");
|
||||||
|
final result = await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList());
|
||||||
|
if (result) {
|
||||||
|
await _trashedLocalAssetRepository.trashLocalAsset(localAssetsToTrash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _applyRemoteRestoreToLocal() async {
|
Future<void> _applyRemoteRestoreToLocal() async {
|
||||||
final assetsToRestore = await _trashedLocalAssetRepository.getToRestore();
|
final assetsToRestore = await _trashedLocalAssetRepository.getToRestore();
|
||||||
if (assetsToRestore.isNotEmpty) {
|
if (assetsToRestore.isNotEmpty) {
|
||||||
@@ -413,4 +509,23 @@ class SyncStreamService {
|
|||||||
_logger.info("No remote assets found for restoration");
|
_logger.info("No remote assets found for restoration");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _syncAssetTrashStatus(List<String> remoteIds) async {
|
||||||
|
if (!(await _localFilesManager.hasManageMediaPermission())) {
|
||||||
|
_logger.warning("Syncing asset trash status cannot proceed because MANAGE_MEDIA permission is missing");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _handleRemoteDeleted(remoteIds);
|
||||||
|
await _applyRemoteRestoreToLocal();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _syncAssetDeletion(List<String> remoteIds) async {
|
||||||
|
if (!(await _localFilesManager.hasManageMediaPermission())) {
|
||||||
|
_logger.warning("Syncing asset deletion cannot proceed because MANAGE_MEDIA permission is missing");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _handleRemoteDeleted(remoteIds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ class BackgroundSyncManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> syncWebsocketBatch(List<dynamic> batchData) {
|
Future<void> syncWebsocketBatchV1(List<dynamic> batchData) {
|
||||||
if (_syncWebsocketTask != null) {
|
if (_syncWebsocketTask != null) {
|
||||||
return _syncWebsocketTask!.future;
|
return _syncWebsocketTask!.future;
|
||||||
}
|
}
|
||||||
@@ -196,7 +196,17 @@ class BackgroundSyncManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> syncWebsocketEdit(dynamic data) {
|
Future<void> syncWebsocketBatchV2(List<dynamic> batchData) {
|
||||||
|
if (_syncWebsocketTask != null) {
|
||||||
|
return _syncWebsocketTask!.future;
|
||||||
|
}
|
||||||
|
_syncWebsocketTask = _handleWsAssetUploadReadyV2Batch(batchData);
|
||||||
|
return _syncWebsocketTask!.whenComplete(() {
|
||||||
|
_syncWebsocketTask = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> syncWebsocketEditV1(dynamic data) {
|
||||||
if (_syncWebsocketTask != null) {
|
if (_syncWebsocketTask != null) {
|
||||||
return _syncWebsocketTask!.future;
|
return _syncWebsocketTask!.future;
|
||||||
}
|
}
|
||||||
@@ -206,6 +216,16 @@ class BackgroundSyncManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> syncWebsocketEditV2(dynamic data) {
|
||||||
|
if (_syncWebsocketTask != null) {
|
||||||
|
return _syncWebsocketTask!.future;
|
||||||
|
}
|
||||||
|
_syncWebsocketTask = _handleWsAssetEditReadyV2(data);
|
||||||
|
return _syncWebsocketTask!.whenComplete(() {
|
||||||
|
_syncWebsocketTask = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> syncLinkedAlbum() {
|
Future<void> syncLinkedAlbum() {
|
||||||
if (_linkedAlbumSyncTask != null) {
|
if (_linkedAlbumSyncTask != null) {
|
||||||
return _linkedAlbumSyncTask!.future;
|
return _linkedAlbumSyncTask!.future;
|
||||||
@@ -242,7 +262,17 @@ Cancelable<void> _handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) => ru
|
|||||||
debugLabel: 'websocket-batch',
|
debugLabel: 'websocket-batch',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Cancelable<void> _handleWsAssetUploadReadyV2Batch(List<dynamic> batchData) => runInIsolateGentle(
|
||||||
|
computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetUploadReadyV2Batch(batchData),
|
||||||
|
debugLabel: 'websocket-batch',
|
||||||
|
);
|
||||||
|
|
||||||
Cancelable<void> _handleWsAssetEditReadyV1(dynamic data) => runInIsolateGentle(
|
Cancelable<void> _handleWsAssetEditReadyV1(dynamic data) => runInIsolateGentle(
|
||||||
computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetEditReadyV1(data),
|
computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetEditReadyV1(data),
|
||||||
debugLabel: 'websocket-edit',
|
debugLabel: 'websocket-edit',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Cancelable<void> _handleWsAssetEditReadyV2(dynamic data) => runInIsolateGentle(
|
||||||
|
computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetEditReadyV2(data),
|
||||||
|
debugLabel: 'websocket-edit',
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||||
import 'package:immich_mobile/extensions/string_extensions.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
|
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
|
||||||
import 'package:openapi/api.dart' as api;
|
import 'package:openapi/api.dart' as api;
|
||||||
|
|
||||||
@@ -14,7 +13,7 @@ extension DTOToAsset on api.AssetResponseDto {
|
|||||||
updatedAt: updatedAt,
|
updatedAt: updatedAt,
|
||||||
ownerId: ownerId,
|
ownerId: ownerId,
|
||||||
visibility: visibility.toAssetVisibility(),
|
visibility: visibility.toAssetVisibility(),
|
||||||
durationMs: duration?.toDuration()?.inMilliseconds ?? 0,
|
durationMs: duration,
|
||||||
height: height?.toInt(),
|
height: height?.toInt(),
|
||||||
width: width?.toInt(),
|
width: width?.toInt(),
|
||||||
isFavorite: isFavorite,
|
isFavorite: isFavorite,
|
||||||
@@ -36,7 +35,7 @@ extension DTOToAsset on api.AssetResponseDto {
|
|||||||
updatedAt: updatedAt,
|
updatedAt: updatedAt,
|
||||||
ownerId: ownerId,
|
ownerId: ownerId,
|
||||||
visibility: visibility.toAssetVisibility(),
|
visibility: visibility.toAssetVisibility(),
|
||||||
durationMs: duration?.toDuration()?.inMilliseconds ?? 0,
|
durationMs: duration,
|
||||||
height: height?.toInt(),
|
height: height?.toInt(),
|
||||||
width: width?.toInt(),
|
width: width?.toInt(),
|
||||||
isFavorite: isFavorite,
|
isFavorite: isFavorite,
|
||||||
|
|||||||
@@ -109,31 +109,40 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
|||||||
return query.map((localAlbum) => localAlbum.toDto()).get();
|
return query.map((localAlbum) => localAlbum.toDto()).get();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, List<LocalAsset>>> getAssetsFromBackupAlbums(Iterable<String> checksums) async {
|
Future<Map<String, List<LocalAsset>>> getAssetsFromBackupAlbums(Iterable<String> remoteIds) async {
|
||||||
if (checksums.isEmpty) {
|
if (remoteIds.isEmpty) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
final result = <String, List<LocalAsset>>{};
|
final result = <String, List<LocalAsset>>{};
|
||||||
|
|
||||||
for (final slice in checksums.toSet().slices(kDriftMaxChunk)) {
|
for (final slice in remoteIds.toSet().slices(kDriftMaxChunk)) {
|
||||||
final rows =
|
final rows =
|
||||||
await (_db.select(_db.localAlbumAssetEntity).join([
|
await (_db.select(_db.localAlbumAssetEntity).join([
|
||||||
innerJoin(_db.localAlbumEntity, _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id)),
|
innerJoin(
|
||||||
|
_db.localAlbumEntity,
|
||||||
|
_db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
innerJoin(_db.localAssetEntity, _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id)),
|
innerJoin(_db.localAssetEntity, _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id)),
|
||||||
|
innerJoin(
|
||||||
|
_db.remoteAssetEntity,
|
||||||
|
_db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
])..where(
|
])..where(
|
||||||
_db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected) &
|
_db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected) &
|
||||||
_db.localAssetEntity.checksum.isIn(slice),
|
_db.remoteAssetEntity.id.isIn(slice),
|
||||||
))
|
))
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
for (final row in rows) {
|
for (final row in rows) {
|
||||||
final albumId = row.readTable(_db.localAlbumAssetEntity).albumId;
|
final albumId = row.readTable(_db.localAlbumAssetEntity).albumId;
|
||||||
final assetData = row.readTable(_db.localAssetEntity);
|
final asset = row.readTable(_db.localAssetEntity).toDto();
|
||||||
final asset = assetData.toDto();
|
|
||||||
(result[albumId] ??= <LocalAsset>[]).add(asset);
|
(result[albumId] ??= <LocalAsset>[]).add(asset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,19 +46,25 @@ class SyncApiRepository {
|
|||||||
types: [
|
types: [
|
||||||
SyncRequestType.authUsersV1,
|
SyncRequestType.authUsersV1,
|
||||||
SyncRequestType.usersV1,
|
SyncRequestType.usersV1,
|
||||||
SyncRequestType.assetsV1,
|
serverVersion >= const SemVer(major: 3, minor: 0, patch: 0)
|
||||||
|
? SyncRequestType.assetsV2
|
||||||
|
: SyncRequestType.assetsV1,
|
||||||
SyncRequestType.assetExifsV1,
|
SyncRequestType.assetExifsV1,
|
||||||
if (serverVersion >= const SemVer(major: 2, minor: 6, patch: 0)) SyncRequestType.assetEditsV1,
|
if (serverVersion >= const SemVer(major: 2, minor: 6, patch: 0)) SyncRequestType.assetEditsV1,
|
||||||
SyncRequestType.assetMetadataV1,
|
SyncRequestType.assetMetadataV1,
|
||||||
SyncRequestType.partnersV1,
|
SyncRequestType.partnersV1,
|
||||||
SyncRequestType.partnerAssetsV1,
|
serverVersion >= const SemVer(major: 3, minor: 0, patch: 0)
|
||||||
|
? SyncRequestType.partnerAssetsV2
|
||||||
|
: SyncRequestType.partnerAssetsV1,
|
||||||
SyncRequestType.partnerAssetExifsV1,
|
SyncRequestType.partnerAssetExifsV1,
|
||||||
if (serverVersion < const SemVer(major: 3, minor: 0, patch: 0))
|
if (serverVersion < const SemVer(major: 3, minor: 0, patch: 0))
|
||||||
SyncRequestType.albumsV1
|
SyncRequestType.albumsV1
|
||||||
else
|
else
|
||||||
SyncRequestType.albumsV2,
|
SyncRequestType.albumsV2,
|
||||||
SyncRequestType.albumUsersV1,
|
SyncRequestType.albumUsersV1,
|
||||||
SyncRequestType.albumAssetsV1,
|
serverVersion >= const SemVer(major: 3, minor: 0, patch: 0)
|
||||||
|
? SyncRequestType.albumAssetsV2
|
||||||
|
: SyncRequestType.albumAssetsV1,
|
||||||
SyncRequestType.albumAssetExifsV1,
|
SyncRequestType.albumAssetExifsV1,
|
||||||
SyncRequestType.albumToAssetsV1,
|
SyncRequestType.albumToAssetsV1,
|
||||||
SyncRequestType.memoriesV1,
|
SyncRequestType.memoriesV1,
|
||||||
@@ -67,8 +73,9 @@ class SyncApiRepository {
|
|||||||
SyncRequestType.partnerStacksV1,
|
SyncRequestType.partnerStacksV1,
|
||||||
SyncRequestType.userMetadataV1,
|
SyncRequestType.userMetadataV1,
|
||||||
SyncRequestType.peopleV1,
|
SyncRequestType.peopleV1,
|
||||||
if (serverVersion < const SemVer(major: 2, minor: 6, patch: 0)) SyncRequestType.assetFacesV1,
|
serverVersion >= const SemVer(major: 2, minor: 6, patch: 0)
|
||||||
if (serverVersion >= const SemVer(major: 2, minor: 6, patch: 0)) SyncRequestType.assetFacesV2,
|
? SyncRequestType.assetFacesV2
|
||||||
|
: SyncRequestType.assetFacesV1,
|
||||||
],
|
],
|
||||||
reset: shouldReset,
|
reset: shouldReset,
|
||||||
).toJson(),
|
).toJson(),
|
||||||
@@ -153,6 +160,7 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
|
|||||||
SyncEntityType.partnerV1: SyncPartnerV1.fromJson,
|
SyncEntityType.partnerV1: SyncPartnerV1.fromJson,
|
||||||
SyncEntityType.partnerDeleteV1: SyncPartnerDeleteV1.fromJson,
|
SyncEntityType.partnerDeleteV1: SyncPartnerDeleteV1.fromJson,
|
||||||
SyncEntityType.assetV1: SyncAssetV1.fromJson,
|
SyncEntityType.assetV1: SyncAssetV1.fromJson,
|
||||||
|
SyncEntityType.assetV2: SyncAssetV2.fromJson,
|
||||||
SyncEntityType.assetDeleteV1: SyncAssetDeleteV1.fromJson,
|
SyncEntityType.assetDeleteV1: SyncAssetDeleteV1.fromJson,
|
||||||
SyncEntityType.assetExifV1: SyncAssetExifV1.fromJson,
|
SyncEntityType.assetExifV1: SyncAssetExifV1.fromJson,
|
||||||
SyncEntityType.assetEditV1: SyncAssetEditV1.fromJson,
|
SyncEntityType.assetEditV1: SyncAssetEditV1.fromJson,
|
||||||
@@ -160,7 +168,9 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
|
|||||||
SyncEntityType.assetMetadataV1: SyncAssetMetadataV1.fromJson,
|
SyncEntityType.assetMetadataV1: SyncAssetMetadataV1.fromJson,
|
||||||
SyncEntityType.assetMetadataDeleteV1: SyncAssetMetadataDeleteV1.fromJson,
|
SyncEntityType.assetMetadataDeleteV1: SyncAssetMetadataDeleteV1.fromJson,
|
||||||
SyncEntityType.partnerAssetV1: SyncAssetV1.fromJson,
|
SyncEntityType.partnerAssetV1: SyncAssetV1.fromJson,
|
||||||
|
SyncEntityType.partnerAssetV2: SyncAssetV2.fromJson,
|
||||||
SyncEntityType.partnerAssetBackfillV1: SyncAssetV1.fromJson,
|
SyncEntityType.partnerAssetBackfillV1: SyncAssetV1.fromJson,
|
||||||
|
SyncEntityType.partnerAssetBackfillV2: SyncAssetV2.fromJson,
|
||||||
SyncEntityType.partnerAssetDeleteV1: SyncAssetDeleteV1.fromJson,
|
SyncEntityType.partnerAssetDeleteV1: SyncAssetDeleteV1.fromJson,
|
||||||
SyncEntityType.partnerAssetExifV1: SyncAssetExifV1.fromJson,
|
SyncEntityType.partnerAssetExifV1: SyncAssetExifV1.fromJson,
|
||||||
SyncEntityType.partnerAssetExifBackfillV1: SyncAssetExifV1.fromJson,
|
SyncEntityType.partnerAssetExifBackfillV1: SyncAssetExifV1.fromJson,
|
||||||
@@ -171,8 +181,11 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
|
|||||||
SyncEntityType.albumUserBackfillV1: SyncAlbumUserV1.fromJson,
|
SyncEntityType.albumUserBackfillV1: SyncAlbumUserV1.fromJson,
|
||||||
SyncEntityType.albumUserDeleteV1: SyncAlbumUserDeleteV1.fromJson,
|
SyncEntityType.albumUserDeleteV1: SyncAlbumUserDeleteV1.fromJson,
|
||||||
SyncEntityType.albumAssetCreateV1: SyncAssetV1.fromJson,
|
SyncEntityType.albumAssetCreateV1: SyncAssetV1.fromJson,
|
||||||
|
SyncEntityType.albumAssetCreateV2: SyncAssetV2.fromJson,
|
||||||
SyncEntityType.albumAssetUpdateV1: SyncAssetV1.fromJson,
|
SyncEntityType.albumAssetUpdateV1: SyncAssetV1.fromJson,
|
||||||
|
SyncEntityType.albumAssetUpdateV2: SyncAssetV2.fromJson,
|
||||||
SyncEntityType.albumAssetBackfillV1: SyncAssetV1.fromJson,
|
SyncEntityType.albumAssetBackfillV1: SyncAssetV1.fromJson,
|
||||||
|
SyncEntityType.albumAssetBackfillV2: SyncAssetV2.fromJson,
|
||||||
SyncEntityType.albumAssetExifCreateV1: SyncAssetExifV1.fromJson,
|
SyncEntityType.albumAssetExifCreateV1: SyncAssetExifV1.fromJson,
|
||||||
SyncEntityType.albumAssetExifUpdateV1: SyncAssetExifV1.fromJson,
|
SyncEntityType.albumAssetExifUpdateV1: SyncAssetExifV1.fromJson,
|
||||||
SyncEntityType.albumAssetExifBackfillV1: SyncAssetExifV1.fromJson,
|
SyncEntityType.albumAssetExifBackfillV1: SyncAssetExifV1.fromJson,
|
||||||
|
|||||||
@@ -220,6 +220,44 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> updateAssetsV2(Iterable<SyncAssetV2> data, {String debugLabel = 'user'}) async {
|
||||||
|
try {
|
||||||
|
await _db.batch((batch) {
|
||||||
|
for (final asset in data) {
|
||||||
|
final companion = RemoteAssetEntityCompanion(
|
||||||
|
name: Value(asset.originalFileName),
|
||||||
|
type: Value(asset.type.toAssetType()),
|
||||||
|
createdAt: Value.absentIfNull(asset.fileCreatedAt),
|
||||||
|
updatedAt: Value.absentIfNull(asset.fileModifiedAt),
|
||||||
|
durationMs: Value(asset.duration),
|
||||||
|
checksum: Value(asset.checksum),
|
||||||
|
isFavorite: Value(asset.isFavorite),
|
||||||
|
ownerId: Value(asset.ownerId),
|
||||||
|
localDateTime: Value(asset.localDateTime),
|
||||||
|
thumbHash: Value(asset.thumbhash),
|
||||||
|
deletedAt: Value(asset.deletedAt),
|
||||||
|
visibility: Value(asset.visibility.toAssetVisibility()),
|
||||||
|
livePhotoVideoId: Value(asset.livePhotoVideoId),
|
||||||
|
stackId: Value(asset.stackId),
|
||||||
|
libraryId: Value(asset.libraryId),
|
||||||
|
width: Value(asset.width),
|
||||||
|
height: Value(asset.height),
|
||||||
|
isEdited: Value(asset.isEdited),
|
||||||
|
);
|
||||||
|
|
||||||
|
batch.insert(
|
||||||
|
_db.remoteAssetEntity,
|
||||||
|
companion.copyWith(id: Value(asset.id)),
|
||||||
|
onConflict: DoUpdate((_) => companion),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe('Error: updateAssetsV2 - $debugLabel', error, stack);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> updateAssetsExifV1(Iterable<SyncAssetExifV1> data, {String debugLabel = 'user'}) async {
|
Future<void> updateAssetsExifV1(Iterable<SyncAssetExifV1> data, {String debugLabel = 'user'}) async {
|
||||||
try {
|
try {
|
||||||
await _db.batch((batch) {
|
await _db.batch((batch) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
import 'package:immich_mobile/domain/models/events.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/scroll_extensions.dart';
|
import 'package:immich_mobile/extensions/scroll_extensions.dart';
|
||||||
@@ -363,7 +364,8 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
BaseAsset displayAsset = asset;
|
BaseAsset displayAsset = asset;
|
||||||
final stackChildren = ref.watch(stackChildrenNotifier(asset)).valueOrNull;
|
final showAssetStack = ref.watch(timelineServiceProvider.select((s) => s.origin != TimelineOrigin.trash));
|
||||||
|
final stackChildren = showAssetStack ? ref.watch(stackChildrenNotifier(asset)).valueOrNull : null;
|
||||||
if (stackChildren != null && stackChildren.isNotEmpty) {
|
if (stackChildren != null && stackChildren.isNotEmpty) {
|
||||||
displayAsset = stackChildren.elementAt(stackIndex);
|
displayAsset = stackChildren.elementAt(stackIndex);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
|
||||||
class AssetStackRow extends ConsumerWidget {
|
class AssetStackRow extends ConsumerWidget {
|
||||||
final List<RemoteAsset> stack;
|
final List<RemoteAsset> stack;
|
||||||
@@ -15,6 +17,11 @@ class AssetStackRow extends ConsumerWidget {
|
|||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final hideAssetStack = ref.read(timelineServiceProvider).origin == TimelineOrigin.trash;
|
||||||
|
if (hideAssetStack) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
final showingControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
|
final showingControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
|
||||||
double opacity = ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)) * (showingControls ? 1 : 0);
|
double opacity = ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)) * (showingControls ? 1 : 0);
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class ThumbnailTile extends ConsumerStatefulWidget {
|
|||||||
this.showStorageIndicator = false,
|
this.showStorageIndicator = false,
|
||||||
this.lockSelection = false,
|
this.lockSelection = false,
|
||||||
this.heroOffset,
|
this.heroOffset,
|
||||||
|
this.showStackIndicator = false,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ class ThumbnailTile extends ConsumerStatefulWidget {
|
|||||||
final bool showStorageIndicator;
|
final bool showStorageIndicator;
|
||||||
final bool lockSelection;
|
final bool lockSelection;
|
||||||
final int? heroOffset;
|
final int? heroOffset;
|
||||||
|
final bool showStackIndicator;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<ThumbnailTile> createState() => _ThumbnailTileState();
|
ConsumerState<ThumbnailTile> createState() => _ThumbnailTileState();
|
||||||
@@ -139,7 +141,14 @@ class _ThumbnailTileState extends ConsumerState<ThumbnailTile> {
|
|||||||
duration: Durations.short4,
|
duration: Durations.short4,
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.topRight,
|
alignment: Alignment.topRight,
|
||||||
child: _AssetTypeIcons(asset: asset),
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
_AssetTypeIcons(asset: asset),
|
||||||
|
if (widget.showStackIndicator) _StackIndicator(asset: asset),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (storageIndicator && asset != null)
|
if (storageIndicator && asset != null)
|
||||||
@@ -286,8 +295,8 @@ class _AssetTypeIcons extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final hasStack = asset is RemoteAsset && (asset as RemoteAsset).stackId != null;
|
final remoteAsset = asset is RemoteAsset ? asset as RemoteAsset : null;
|
||||||
final isLivePhoto = asset is RemoteAsset && asset.livePhotoVideoId != null;
|
final isLivePhoto = remoteAsset?.livePhotoVideoId != null;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -295,11 +304,6 @@ class _AssetTypeIcons extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
if (asset.isVideo)
|
if (asset.isVideo)
|
||||||
Padding(padding: const EdgeInsets.only(right: 10.0, top: 6.0), child: _VideoIndicator(asset.duration)),
|
Padding(padding: const EdgeInsets.only(right: 10.0, top: 6.0), child: _VideoIndicator(asset.duration)),
|
||||||
if (hasStack)
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.only(right: 10.0, top: 6.0),
|
|
||||||
child: _TileOverlayIcon(Icons.burst_mode_rounded),
|
|
||||||
),
|
|
||||||
if (isLivePhoto)
|
if (isLivePhoto)
|
||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.only(right: 10.0, top: 6.0),
|
padding: EdgeInsets.only(right: 10.0, top: 6.0),
|
||||||
@@ -312,6 +316,24 @@ class _AssetTypeIcons extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _StackIndicator extends StatelessWidget {
|
||||||
|
final BaseAsset asset;
|
||||||
|
|
||||||
|
const _StackIndicator({required this.asset});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (asset is! RemoteAsset || (asset as RemoteAsset).stackId == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return const Padding(
|
||||||
|
padding: EdgeInsets.only(right: 10.0, top: 6.0),
|
||||||
|
child: _TileOverlayIcon(Icons.burst_mode_rounded),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _UploadProgressOverlay extends StatelessWidget {
|
class _UploadProgressOverlay extends StatelessWidget {
|
||||||
final double progress;
|
final double progress;
|
||||||
|
|
||||||
|
|||||||
@@ -244,6 +244,7 @@ class _AssetTileWidget extends ConsumerWidget {
|
|||||||
final lockSelection = _getLockSelectionStatus(ref);
|
final lockSelection = _getLockSelectionStatus(ref);
|
||||||
final showStorageIndicator = ref.watch(timelineArgsProvider.select((args) => args.showStorageIndicator));
|
final showStorageIndicator = ref.watch(timelineArgsProvider.select((args) => args.showStorageIndicator));
|
||||||
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
||||||
|
final showStackIndicator = ref.read(timelineServiceProvider).origin != TimelineOrigin.trash;
|
||||||
|
|
||||||
return RepaintBoundary(
|
return RepaintBoundary(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
@@ -253,6 +254,7 @@ class _AssetTileWidget extends ConsumerWidget {
|
|||||||
asset,
|
asset,
|
||||||
lockSelection: lockSelection,
|
lockSelection: lockSelection,
|
||||||
showStorageIndicator: showStorageIndicator,
|
showStorageIndicator: showStorageIndicator,
|
||||||
|
showStackIndicator: showStackIndicator,
|
||||||
heroOffset: heroOffset,
|
heroOffset: heroOffset,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -94,8 +94,10 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
|||||||
state = const WebsocketState(isConnected: false, socket: null);
|
state = const WebsocketState(isConnected: false, socket: null);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReady);
|
socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReadyV1);
|
||||||
socket.on('AssetEditReadyV1', _handleSyncAssetEditReady);
|
socket.on('AssetUploadReadyV2', _handleSyncAssetUploadReadyV2);
|
||||||
|
socket.on('AssetEditReadyV1', _handleSyncAssetEditReadyV1);
|
||||||
|
socket.on('AssetEditReadyV2', _handleSyncAssetEditReadyV2);
|
||||||
socket.on('on_config_update', _handleOnConfigUpdate);
|
socket.on('on_config_update', _handleOnConfigUpdate);
|
||||||
socket.on('on_new_release', _handleReleaseUpdates);
|
socket.on('on_new_release', _handleReleaseUpdates);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -163,16 +165,25 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
|||||||
_ref.read(serverInfoProvider.notifier).handleReleaseInfo(serverVersion, releaseVersion);
|
_ref.read(serverInfoProvider.notifier).handleReleaseInfo(serverVersion, releaseVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleSyncAssetUploadReady(dynamic data) {
|
void _handleSyncAssetUploadReadyV1(dynamic data) {
|
||||||
_batchedAssetUploadReady.add(data);
|
_batchedAssetUploadReady.add(data);
|
||||||
_batchDebouncer.run(_processBatchedAssetUploadReady);
|
_batchDebouncer.run(_processBatchedAssetUploadReadyV1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleSyncAssetEditReady(dynamic data) {
|
void _handleSyncAssetUploadReadyV2(dynamic data) {
|
||||||
unawaited(_ref.read(backgroundSyncProvider).syncWebsocketEdit(data));
|
_batchedAssetUploadReady.add(data);
|
||||||
|
_batchDebouncer.run(_processBatchedAssetUploadReadyV2);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _processBatchedAssetUploadReady() {
|
void _handleSyncAssetEditReadyV1(dynamic data) {
|
||||||
|
unawaited(_ref.read(backgroundSyncProvider).syncWebsocketEditV1(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSyncAssetEditReadyV2(dynamic data) {
|
||||||
|
unawaited(_ref.read(backgroundSyncProvider).syncWebsocketEditV2(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _processBatchedAssetUploadReadyV1() {
|
||||||
if (_batchedAssetUploadReady.isEmpty) {
|
if (_batchedAssetUploadReady.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -180,7 +191,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
|||||||
final isSyncAlbumEnabled = Store.get(StoreKey.syncAlbums, false);
|
final isSyncAlbumEnabled = Store.get(StoreKey.syncAlbums, false);
|
||||||
try {
|
try {
|
||||||
unawaited(
|
unawaited(
|
||||||
_ref.read(backgroundSyncProvider).syncWebsocketBatch(_batchedAssetUploadReady.toList()).then((_) {
|
_ref.read(backgroundSyncProvider).syncWebsocketBatchV1(_batchedAssetUploadReady.toList()).then((_) {
|
||||||
if (isSyncAlbumEnabled) {
|
if (isSyncAlbumEnabled) {
|
||||||
_ref.read(backgroundSyncProvider).syncLinkedAlbum();
|
_ref.read(backgroundSyncProvider).syncLinkedAlbum();
|
||||||
}
|
}
|
||||||
@@ -192,6 +203,27 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
|||||||
|
|
||||||
_batchedAssetUploadReady.clear();
|
_batchedAssetUploadReady.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _processBatchedAssetUploadReadyV2() {
|
||||||
|
if (_batchedAssetUploadReady.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final isSyncAlbumEnabled = Store.get(StoreKey.syncAlbums, false);
|
||||||
|
try {
|
||||||
|
unawaited(
|
||||||
|
_ref.read(backgroundSyncProvider).syncWebsocketBatchV2(_batchedAssetUploadReady.toList()).then((_) {
|
||||||
|
if (isSyncAlbumEnabled) {
|
||||||
|
_ref.read(backgroundSyncProvider).syncLinkedAlbum();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
_log.severe("Error processing batched AssetUploadReadyV2 events: $error");
|
||||||
|
}
|
||||||
|
|
||||||
|
_batchedAssetUploadReady.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final websocketProvider = StateNotifierProvider<WebsocketNotifier, WebsocketState>((ref) {
|
final websocketProvider = StateNotifierProvider<WebsocketNotifier, WebsocketState>((ref) {
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ enum ActionButtonType {
|
|||||||
context.selectedCount == 1,
|
context.selectedCount == 1,
|
||||||
ActionButtonType.unstack =>
|
ActionButtonType.unstack =>
|
||||||
context.isOwner && //
|
context.isOwner && //
|
||||||
|
context.timelineOrigin != TimelineOrigin.trash &&
|
||||||
!context.isInLockedView && //
|
!context.isInLockedView && //
|
||||||
context.isStacked,
|
context.isStacked,
|
||||||
ActionButtonType.openInBrowser => context.asset.hasRemote && !context.isInLockedView,
|
ActionButtonType.openInBrowser => context.asset.hasRemote && !context.isInLockedView,
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
[tools]
|
[tools]
|
||||||
flutter = "3.41.7"
|
flutter = "3.41.9"
|
||||||
|
|
||||||
[tools."github:CQLabs/homebrew-dcm"]
|
[tools."github:CQLabs/homebrew-dcm"]
|
||||||
version = "1.30.0"
|
version = "1.30.0"
|
||||||
|
|||||||
Generated
+2
-1
@@ -146,7 +146,7 @@ Class | Method | HTTP request | Description
|
|||||||
*DeprecatedApi* | [**runQueueCommandLegacy**](doc//DeprecatedApi.md#runqueuecommandlegacy) | **PUT** /jobs/{name} | Run jobs
|
*DeprecatedApi* | [**runQueueCommandLegacy**](doc//DeprecatedApi.md#runqueuecommandlegacy) | **PUT** /jobs/{name} | Run jobs
|
||||||
*DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | Download asset archive
|
*DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | Download asset archive
|
||||||
*DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | Retrieve download information
|
*DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | Retrieve download information
|
||||||
*DuplicatesApi* | [**deleteDuplicate**](doc//DuplicatesApi.md#deleteduplicate) | **DELETE** /duplicates/{id} | Delete a duplicate
|
*DuplicatesApi* | [**deleteDuplicate**](doc//DuplicatesApi.md#deleteduplicate) | **DELETE** /duplicates/{id} | Dismiss a duplicate group
|
||||||
*DuplicatesApi* | [**deleteDuplicates**](doc//DuplicatesApi.md#deleteduplicates) | **DELETE** /duplicates | Delete duplicates
|
*DuplicatesApi* | [**deleteDuplicates**](doc//DuplicatesApi.md#deleteduplicates) | **DELETE** /duplicates | Delete duplicates
|
||||||
*DuplicatesApi* | [**getAssetDuplicates**](doc//DuplicatesApi.md#getassetduplicates) | **GET** /duplicates | Retrieve duplicates
|
*DuplicatesApi* | [**getAssetDuplicates**](doc//DuplicatesApi.md#getassetduplicates) | **GET** /duplicates | Retrieve duplicates
|
||||||
*DuplicatesApi* | [**resolveDuplicates**](doc//DuplicatesApi.md#resolveduplicates) | **POST** /duplicates/resolve | Resolve duplicate groups
|
*DuplicatesApi* | [**resolveDuplicates**](doc//DuplicatesApi.md#resolveduplicates) | **POST** /duplicates/resolve | Resolve duplicate groups
|
||||||
@@ -579,6 +579,7 @@ Class | Method | HTTP request | Description
|
|||||||
- [SyncAssetMetadataDeleteV1](doc//SyncAssetMetadataDeleteV1.md)
|
- [SyncAssetMetadataDeleteV1](doc//SyncAssetMetadataDeleteV1.md)
|
||||||
- [SyncAssetMetadataV1](doc//SyncAssetMetadataV1.md)
|
- [SyncAssetMetadataV1](doc//SyncAssetMetadataV1.md)
|
||||||
- [SyncAssetV1](doc//SyncAssetV1.md)
|
- [SyncAssetV1](doc//SyncAssetV1.md)
|
||||||
|
- [SyncAssetV2](doc//SyncAssetV2.md)
|
||||||
- [SyncAuthUserV1](doc//SyncAuthUserV1.md)
|
- [SyncAuthUserV1](doc//SyncAuthUserV1.md)
|
||||||
- [SyncEntityType](doc//SyncEntityType.md)
|
- [SyncEntityType](doc//SyncEntityType.md)
|
||||||
- [SyncMemoryAssetDeleteV1](doc//SyncMemoryAssetDeleteV1.md)
|
- [SyncMemoryAssetDeleteV1](doc//SyncMemoryAssetDeleteV1.md)
|
||||||
|
|||||||
Generated
+1
@@ -327,6 +327,7 @@ part 'model/sync_asset_face_v2.dart';
|
|||||||
part 'model/sync_asset_metadata_delete_v1.dart';
|
part 'model/sync_asset_metadata_delete_v1.dart';
|
||||||
part 'model/sync_asset_metadata_v1.dart';
|
part 'model/sync_asset_metadata_v1.dart';
|
||||||
part 'model/sync_asset_v1.dart';
|
part 'model/sync_asset_v1.dart';
|
||||||
|
part 'model/sync_asset_v2.dart';
|
||||||
part 'model/sync_auth_user_v1.dart';
|
part 'model/sync_auth_user_v1.dart';
|
||||||
part 'model/sync_entity_type.dart';
|
part 'model/sync_entity_type.dart';
|
||||||
part 'model/sync_memory_asset_delete_v1.dart';
|
part 'model/sync_memory_asset_delete_v1.dart';
|
||||||
|
|||||||
Generated
+3
-12
@@ -508,12 +508,9 @@ class AlbumsApi {
|
|||||||
/// * [String] assetId:
|
/// * [String] assetId:
|
||||||
/// Filter albums containing this asset ID (ignores shared parameter)
|
/// Filter albums containing this asset ID (ignores shared parameter)
|
||||||
///
|
///
|
||||||
/// * [bool] favorite:
|
|
||||||
/// Filter to only albums favorited by the authenticated user
|
|
||||||
///
|
|
||||||
/// * [bool] shared:
|
/// * [bool] shared:
|
||||||
/// Filter by shared status: true = only shared, false = not shared, undefined = all owned albums
|
/// Filter by shared status: true = only shared, false = not shared, undefined = all owned albums
|
||||||
Future<Response> getAllAlbumsWithHttpInfo({ String? assetId, bool? favorite, bool? shared, }) async {
|
Future<Response> getAllAlbumsWithHttpInfo({ String? assetId, bool? shared, }) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/albums';
|
final apiPath = r'/albums';
|
||||||
|
|
||||||
@@ -527,9 +524,6 @@ class AlbumsApi {
|
|||||||
if (assetId != null) {
|
if (assetId != null) {
|
||||||
queryParams.addAll(_queryParams('', 'assetId', assetId));
|
queryParams.addAll(_queryParams('', 'assetId', assetId));
|
||||||
}
|
}
|
||||||
if (favorite != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'favorite', favorite));
|
|
||||||
}
|
|
||||||
if (shared != null) {
|
if (shared != null) {
|
||||||
queryParams.addAll(_queryParams('', 'shared', shared));
|
queryParams.addAll(_queryParams('', 'shared', shared));
|
||||||
}
|
}
|
||||||
@@ -557,13 +551,10 @@ class AlbumsApi {
|
|||||||
/// * [String] assetId:
|
/// * [String] assetId:
|
||||||
/// Filter albums containing this asset ID (ignores shared parameter)
|
/// Filter albums containing this asset ID (ignores shared parameter)
|
||||||
///
|
///
|
||||||
/// * [bool] favorite:
|
|
||||||
/// Filter to only albums favorited by the authenticated user
|
|
||||||
///
|
|
||||||
/// * [bool] shared:
|
/// * [bool] shared:
|
||||||
/// Filter by shared status: true = only shared, false = not shared, undefined = all owned albums
|
/// Filter by shared status: true = only shared, false = not shared, undefined = all owned albums
|
||||||
Future<List<AlbumResponseDto>?> getAllAlbums({ String? assetId, bool? favorite, bool? shared, }) async {
|
Future<List<AlbumResponseDto>?> getAllAlbums({ String? assetId, bool? shared, }) async {
|
||||||
final response = await getAllAlbumsWithHttpInfo( assetId: assetId, favorite: favorite, shared: shared, );
|
final response = await getAllAlbumsWithHttpInfo( assetId: assetId, shared: shared, );
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+6
-6
@@ -1234,8 +1234,8 @@ class AssetsApi {
|
|||||||
/// * [String] xImmichChecksum:
|
/// * [String] xImmichChecksum:
|
||||||
/// sha1 checksum that can be used for duplicate detection before the file is uploaded
|
/// sha1 checksum that can be used for duplicate detection before the file is uploaded
|
||||||
///
|
///
|
||||||
/// * [String] duration:
|
/// * [int] duration:
|
||||||
/// Duration (for videos)
|
/// Duration in milliseconds (for videos)
|
||||||
///
|
///
|
||||||
/// * [String] filename:
|
/// * [String] filename:
|
||||||
/// Filename
|
/// Filename
|
||||||
@@ -1253,7 +1253,7 @@ class AssetsApi {
|
|||||||
/// Sidecar file data
|
/// Sidecar file data
|
||||||
///
|
///
|
||||||
/// * [AssetVisibility] visibility:
|
/// * [AssetVisibility] visibility:
|
||||||
Future<Response> uploadAssetWithHttpInfo(MultipartFile assetData, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List<AssetMetadataUpsertItemDto>? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
|
Future<Response> uploadAssetWithHttpInfo(MultipartFile assetData, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, int? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List<AssetMetadataUpsertItemDto>? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/assets';
|
final apiPath = r'/assets';
|
||||||
|
|
||||||
@@ -1358,8 +1358,8 @@ class AssetsApi {
|
|||||||
/// * [String] xImmichChecksum:
|
/// * [String] xImmichChecksum:
|
||||||
/// sha1 checksum that can be used for duplicate detection before the file is uploaded
|
/// sha1 checksum that can be used for duplicate detection before the file is uploaded
|
||||||
///
|
///
|
||||||
/// * [String] duration:
|
/// * [int] duration:
|
||||||
/// Duration (for videos)
|
/// Duration in milliseconds (for videos)
|
||||||
///
|
///
|
||||||
/// * [String] filename:
|
/// * [String] filename:
|
||||||
/// Filename
|
/// Filename
|
||||||
@@ -1377,7 +1377,7 @@ class AssetsApi {
|
|||||||
/// Sidecar file data
|
/// Sidecar file data
|
||||||
///
|
///
|
||||||
/// * [AssetVisibility] visibility:
|
/// * [AssetVisibility] visibility:
|
||||||
Future<AssetMediaResponseDto?> uploadAsset(MultipartFile assetData, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List<AssetMetadataUpsertItemDto>? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
|
Future<AssetMediaResponseDto?> uploadAsset(MultipartFile assetData, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, int? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List<AssetMetadataUpsertItemDto>? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
|
||||||
final response = await uploadAssetWithHttpInfo(assetData, fileCreatedAt, fileModifiedAt, key: key, slug: slug, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, metadata: metadata, sidecarData: sidecarData, visibility: visibility, );
|
final response = await uploadAssetWithHttpInfo(assetData, fileCreatedAt, fileModifiedAt, key: key, slug: slug, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, metadata: metadata, sidecarData: sidecarData, visibility: visibility, );
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
|||||||
+4
-4
@@ -16,9 +16,9 @@ class DuplicatesApi {
|
|||||||
|
|
||||||
final ApiClient apiClient;
|
final ApiClient apiClient;
|
||||||
|
|
||||||
/// Delete a duplicate
|
/// Dismiss a duplicate group
|
||||||
///
|
///
|
||||||
/// Delete a single duplicate asset specified by its ID.
|
/// Dismiss a duplicate group by its ID, unlinking all assets in the group without deleting them.
|
||||||
///
|
///
|
||||||
/// Note: This method returns the HTTP [Response].
|
/// Note: This method returns the HTTP [Response].
|
||||||
///
|
///
|
||||||
@@ -51,9 +51,9 @@ class DuplicatesApi {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete a duplicate
|
/// Dismiss a duplicate group
|
||||||
///
|
///
|
||||||
/// Delete a single duplicate asset specified by its ID.
|
/// Dismiss a duplicate group by its ID, unlinking all assets in the group without deleting them.
|
||||||
///
|
///
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
|
|||||||
Generated
+6
-6
@@ -183,15 +183,15 @@ class PeopleApi {
|
|||||||
/// * [String] closestPersonId:
|
/// * [String] closestPersonId:
|
||||||
/// Closest person ID for similarity search
|
/// Closest person ID for similarity search
|
||||||
///
|
///
|
||||||
/// * [num] page:
|
/// * [int] page:
|
||||||
/// Page number for pagination
|
/// Page number for pagination
|
||||||
///
|
///
|
||||||
/// * [num] size:
|
/// * [int] size:
|
||||||
/// Number of items per page
|
/// Number of items per page
|
||||||
///
|
///
|
||||||
/// * [bool] withHidden:
|
/// * [bool] withHidden:
|
||||||
/// Include hidden people
|
/// Include hidden people
|
||||||
Future<Response> getAllPeopleWithHttpInfo({ String? closestAssetId, String? closestPersonId, num? page, num? size, bool? withHidden, }) async {
|
Future<Response> getAllPeopleWithHttpInfo({ String? closestAssetId, String? closestPersonId, int? page, int? size, bool? withHidden, }) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/people';
|
final apiPath = r'/people';
|
||||||
|
|
||||||
@@ -244,15 +244,15 @@ class PeopleApi {
|
|||||||
/// * [String] closestPersonId:
|
/// * [String] closestPersonId:
|
||||||
/// Closest person ID for similarity search
|
/// Closest person ID for similarity search
|
||||||
///
|
///
|
||||||
/// * [num] page:
|
/// * [int] page:
|
||||||
/// Page number for pagination
|
/// Page number for pagination
|
||||||
///
|
///
|
||||||
/// * [num] size:
|
/// * [int] size:
|
||||||
/// Number of items per page
|
/// Number of items per page
|
||||||
///
|
///
|
||||||
/// * [bool] withHidden:
|
/// * [bool] withHidden:
|
||||||
/// Include hidden people
|
/// Include hidden people
|
||||||
Future<PeopleResponseDto?> getAllPeople({ String? closestAssetId, String? closestPersonId, num? page, num? size, bool? withHidden, }) async {
|
Future<PeopleResponseDto?> getAllPeople({ String? closestAssetId, String? closestPersonId, int? page, int? size, bool? withHidden, }) async {
|
||||||
final response = await getAllPeopleWithHttpInfo( closestAssetId: closestAssetId, closestPersonId: closestPersonId, page: page, size: size, withHidden: withHidden, );
|
final response = await getAllPeopleWithHttpInfo( closestAssetId: closestAssetId, closestPersonId: closestPersonId, page: page, size: size, withHidden: withHidden, );
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
|||||||
Generated
+6
-6
@@ -404,10 +404,10 @@ class SearchApi {
|
|||||||
/// * [List<String>] personIds:
|
/// * [List<String>] personIds:
|
||||||
/// Filter by person IDs
|
/// Filter by person IDs
|
||||||
///
|
///
|
||||||
/// * [num] rating:
|
/// * [int] rating:
|
||||||
/// Filter by rating [1-5], or null for unrated
|
/// Filter by rating [1-5], or null for unrated
|
||||||
///
|
///
|
||||||
/// * [num] size:
|
/// * [int] size:
|
||||||
/// Number of results to return
|
/// Number of results to return
|
||||||
///
|
///
|
||||||
/// * [String] state:
|
/// * [String] state:
|
||||||
@@ -443,7 +443,7 @@ class SearchApi {
|
|||||||
///
|
///
|
||||||
/// * [bool] withExif:
|
/// * [bool] withExif:
|
||||||
/// Include EXIF data in response
|
/// Include EXIF data in response
|
||||||
Future<Response> searchLargeAssetsWithHttpInfo({ List<String>? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List<String>? personIds, num? rating, num? size, String? state, List<String>? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async {
|
Future<Response> searchLargeAssetsWithHttpInfo({ List<String>? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List<String>? personIds, int? rating, int? size, String? state, List<String>? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/search/large-assets';
|
final apiPath = r'/search/large-assets';
|
||||||
|
|
||||||
@@ -619,10 +619,10 @@ class SearchApi {
|
|||||||
/// * [List<String>] personIds:
|
/// * [List<String>] personIds:
|
||||||
/// Filter by person IDs
|
/// Filter by person IDs
|
||||||
///
|
///
|
||||||
/// * [num] rating:
|
/// * [int] rating:
|
||||||
/// Filter by rating [1-5], or null for unrated
|
/// Filter by rating [1-5], or null for unrated
|
||||||
///
|
///
|
||||||
/// * [num] size:
|
/// * [int] size:
|
||||||
/// Number of results to return
|
/// Number of results to return
|
||||||
///
|
///
|
||||||
/// * [String] state:
|
/// * [String] state:
|
||||||
@@ -658,7 +658,7 @@ class SearchApi {
|
|||||||
///
|
///
|
||||||
/// * [bool] withExif:
|
/// * [bool] withExif:
|
||||||
/// Include EXIF data in response
|
/// Include EXIF data in response
|
||||||
Future<List<AssetResponseDto>?> searchLargeAssets({ List<String>? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List<String>? personIds, num? rating, num? size, String? state, List<String>? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async {
|
Future<List<AssetResponseDto>?> searchLargeAssets({ List<String>? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List<String>? personIds, int? rating, int? size, String? state, List<String>? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async {
|
||||||
final response = await searchLargeAssetsWithHttpInfo( albumIds: albumIds, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, isEncoded: isEncoded, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, lensModel: lensModel, libraryId: libraryId, make: make, minFileSize: minFileSize, model: model, ocr: ocr, personIds: personIds, rating: rating, size: size, state: state, tagIds: tagIds, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, visibility: visibility, withDeleted: withDeleted, withExif: withExif, );
|
final response = await searchLargeAssetsWithHttpInfo( albumIds: albumIds, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, isEncoded: isEncoded, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, lensModel: lensModel, libraryId: libraryId, make: make, minFileSize: minFileSize, model: model, ocr: ocr, personIds: personIds, rating: rating, size: size, state: state, tagIds: tagIds, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, visibility: visibility, withDeleted: withDeleted, withExif: withExif, );
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
|||||||
Generated
+2
@@ -700,6 +700,8 @@ class ApiClient {
|
|||||||
return SyncAssetMetadataV1.fromJson(value);
|
return SyncAssetMetadataV1.fromJson(value);
|
||||||
case 'SyncAssetV1':
|
case 'SyncAssetV1':
|
||||||
return SyncAssetV1.fromJson(value);
|
return SyncAssetV1.fromJson(value);
|
||||||
|
case 'SyncAssetV2':
|
||||||
|
return SyncAssetV2.fromJson(value);
|
||||||
case 'SyncAuthUserV1':
|
case 'SyncAuthUserV1':
|
||||||
return SyncAuthUserV1.fromJson(value);
|
return SyncAuthUserV1.fromJson(value);
|
||||||
case 'SyncEntityType':
|
case 'SyncEntityType':
|
||||||
|
|||||||
+1
-10
@@ -24,7 +24,6 @@ class AlbumResponseDto {
|
|||||||
required this.hasSharedLink,
|
required this.hasSharedLink,
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.isActivityEnabled,
|
required this.isActivityEnabled,
|
||||||
required this.isFavorite,
|
|
||||||
this.lastModifiedAssetTimestamp,
|
this.lastModifiedAssetTimestamp,
|
||||||
this.order,
|
this.order,
|
||||||
required this.shared,
|
required this.shared,
|
||||||
@@ -73,9 +72,6 @@ class AlbumResponseDto {
|
|||||||
/// Activity feed enabled
|
/// Activity feed enabled
|
||||||
bool isActivityEnabled;
|
bool isActivityEnabled;
|
||||||
|
|
||||||
/// Whether the authenticated user has favorited this album
|
|
||||||
bool isFavorite;
|
|
||||||
|
|
||||||
/// Last modified asset timestamp
|
/// Last modified asset timestamp
|
||||||
///
|
///
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
@@ -121,7 +117,6 @@ class AlbumResponseDto {
|
|||||||
other.hasSharedLink == hasSharedLink &&
|
other.hasSharedLink == hasSharedLink &&
|
||||||
other.id == id &&
|
other.id == id &&
|
||||||
other.isActivityEnabled == isActivityEnabled &&
|
other.isActivityEnabled == isActivityEnabled &&
|
||||||
other.isFavorite == isFavorite &&
|
|
||||||
other.lastModifiedAssetTimestamp == lastModifiedAssetTimestamp &&
|
other.lastModifiedAssetTimestamp == lastModifiedAssetTimestamp &&
|
||||||
other.order == order &&
|
other.order == order &&
|
||||||
other.shared == shared &&
|
other.shared == shared &&
|
||||||
@@ -142,7 +137,6 @@ class AlbumResponseDto {
|
|||||||
(hasSharedLink.hashCode) +
|
(hasSharedLink.hashCode) +
|
||||||
(id.hashCode) +
|
(id.hashCode) +
|
||||||
(isActivityEnabled.hashCode) +
|
(isActivityEnabled.hashCode) +
|
||||||
(isFavorite.hashCode) +
|
|
||||||
(lastModifiedAssetTimestamp == null ? 0 : lastModifiedAssetTimestamp!.hashCode) +
|
(lastModifiedAssetTimestamp == null ? 0 : lastModifiedAssetTimestamp!.hashCode) +
|
||||||
(order == null ? 0 : order!.hashCode) +
|
(order == null ? 0 : order!.hashCode) +
|
||||||
(shared.hashCode) +
|
(shared.hashCode) +
|
||||||
@@ -150,7 +144,7 @@ class AlbumResponseDto {
|
|||||||
(updatedAt.hashCode);
|
(updatedAt.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'AlbumResponseDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, albumUsers=$albumUsers, assetCount=$assetCount, contributorCounts=$contributorCounts, createdAt=$createdAt, description=$description, endDate=$endDate, hasSharedLink=$hasSharedLink, id=$id, isActivityEnabled=$isActivityEnabled, isFavorite=$isFavorite, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp, order=$order, shared=$shared, startDate=$startDate, updatedAt=$updatedAt]';
|
String toString() => 'AlbumResponseDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, albumUsers=$albumUsers, assetCount=$assetCount, contributorCounts=$contributorCounts, createdAt=$createdAt, description=$description, endDate=$endDate, hasSharedLink=$hasSharedLink, id=$id, isActivityEnabled=$isActivityEnabled, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp, order=$order, shared=$shared, startDate=$startDate, updatedAt=$updatedAt]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@@ -173,7 +167,6 @@ class AlbumResponseDto {
|
|||||||
json[r'hasSharedLink'] = this.hasSharedLink;
|
json[r'hasSharedLink'] = this.hasSharedLink;
|
||||||
json[r'id'] = this.id;
|
json[r'id'] = this.id;
|
||||||
json[r'isActivityEnabled'] = this.isActivityEnabled;
|
json[r'isActivityEnabled'] = this.isActivityEnabled;
|
||||||
json[r'isFavorite'] = this.isFavorite;
|
|
||||||
if (this.lastModifiedAssetTimestamp != null) {
|
if (this.lastModifiedAssetTimestamp != null) {
|
||||||
json[r'lastModifiedAssetTimestamp'] = this.lastModifiedAssetTimestamp!.toUtc().toIso8601String();
|
json[r'lastModifiedAssetTimestamp'] = this.lastModifiedAssetTimestamp!.toUtc().toIso8601String();
|
||||||
} else {
|
} else {
|
||||||
@@ -214,7 +207,6 @@ class AlbumResponseDto {
|
|||||||
hasSharedLink: mapValueOfType<bool>(json, r'hasSharedLink')!,
|
hasSharedLink: mapValueOfType<bool>(json, r'hasSharedLink')!,
|
||||||
id: mapValueOfType<String>(json, r'id')!,
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
isActivityEnabled: mapValueOfType<bool>(json, r'isActivityEnabled')!,
|
isActivityEnabled: mapValueOfType<bool>(json, r'isActivityEnabled')!,
|
||||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
|
|
||||||
lastModifiedAssetTimestamp: mapDateTime(json, r'lastModifiedAssetTimestamp', r''),
|
lastModifiedAssetTimestamp: mapDateTime(json, r'lastModifiedAssetTimestamp', r''),
|
||||||
order: AssetOrder.fromJson(json[r'order']),
|
order: AssetOrder.fromJson(json[r'order']),
|
||||||
shared: mapValueOfType<bool>(json, r'shared')!,
|
shared: mapValueOfType<bool>(json, r'shared')!,
|
||||||
@@ -276,7 +268,6 @@ class AlbumResponseDto {
|
|||||||
'hasSharedLink',
|
'hasSharedLink',
|
||||||
'id',
|
'id',
|
||||||
'isActivityEnabled',
|
'isActivityEnabled',
|
||||||
'isFavorite',
|
|
||||||
'shared',
|
'shared',
|
||||||
'updatedAt',
|
'updatedAt',
|
||||||
};
|
};
|
||||||
|
|||||||
+5
-2
@@ -37,12 +37,15 @@ class AssetBulkUpdateDto {
|
|||||||
|
|
||||||
/// Relative time offset in seconds
|
/// Relative time offset in seconds
|
||||||
///
|
///
|
||||||
|
/// Minimum value: -9007199254740991
|
||||||
|
/// Maximum value: 9007199254740991
|
||||||
|
///
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
/// does not include a default value (using the "default:" property), however, the generated
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
/// source code must fall back to having a nullable type.
|
/// source code must fall back to having a nullable type.
|
||||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
///
|
///
|
||||||
num? dateTimeRelative;
|
int? dateTimeRelative;
|
||||||
|
|
||||||
/// Asset description
|
/// Asset description
|
||||||
///
|
///
|
||||||
@@ -213,7 +216,7 @@ class AssetBulkUpdateDto {
|
|||||||
|
|
||||||
return AssetBulkUpdateDto(
|
return AssetBulkUpdateDto(
|
||||||
dateTimeOriginal: mapValueOfType<String>(json, r'dateTimeOriginal'),
|
dateTimeOriginal: mapValueOfType<String>(json, r'dateTimeOriginal'),
|
||||||
dateTimeRelative: num.parse('${json[r'dateTimeRelative']}'),
|
dateTimeRelative: mapValueOfType<int>(json, r'dateTimeRelative'),
|
||||||
description: mapValueOfType<String>(json, r'description'),
|
description: mapValueOfType<String>(json, r'description'),
|
||||||
duplicateId: mapValueOfType<String>(json, r'duplicateId'),
|
duplicateId: mapValueOfType<String>(json, r'duplicateId'),
|
||||||
ids: json[r'ids'] is Iterable
|
ids: json[r'ids'] is Iterable
|
||||||
|
|||||||
@@ -24,22 +24,26 @@ class AssetEditActionItemDtoParameters {
|
|||||||
/// Height of the crop
|
/// Height of the crop
|
||||||
///
|
///
|
||||||
/// Minimum value: 1
|
/// Minimum value: 1
|
||||||
num height;
|
/// Maximum value: 9007199254740991
|
||||||
|
int height;
|
||||||
|
|
||||||
/// Width of the crop
|
/// Width of the crop
|
||||||
///
|
///
|
||||||
/// Minimum value: 1
|
/// Minimum value: 1
|
||||||
num width;
|
/// Maximum value: 9007199254740991
|
||||||
|
int width;
|
||||||
|
|
||||||
/// Top-Left X coordinate of crop
|
/// Top-Left X coordinate of crop
|
||||||
///
|
///
|
||||||
/// Minimum value: 0
|
/// Minimum value: 0
|
||||||
num x;
|
/// Maximum value: 9007199254740991
|
||||||
|
int x;
|
||||||
|
|
||||||
/// Top-Left Y coordinate of crop
|
/// Top-Left Y coordinate of crop
|
||||||
///
|
///
|
||||||
/// Minimum value: 0
|
/// Minimum value: 0
|
||||||
num y;
|
/// Maximum value: 9007199254740991
|
||||||
|
int y;
|
||||||
|
|
||||||
/// Rotation angle in degrees
|
/// Rotation angle in degrees
|
||||||
num angle;
|
num angle;
|
||||||
@@ -88,10 +92,10 @@ class AssetEditActionItemDtoParameters {
|
|||||||
final json = value.cast<String, dynamic>();
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
return AssetEditActionItemDtoParameters(
|
return AssetEditActionItemDtoParameters(
|
||||||
height: num.parse('${json[r'height']}'),
|
height: mapValueOfType<int>(json, r'height')!,
|
||||||
width: num.parse('${json[r'width']}'),
|
width: mapValueOfType<int>(json, r'width')!,
|
||||||
x: num.parse('${json[r'x']}'),
|
x: mapValueOfType<int>(json, r'x')!,
|
||||||
y: num.parse('${json[r'y']}'),
|
y: mapValueOfType<int>(json, r'y')!,
|
||||||
angle: num.parse('${json[r'angle']}'),
|
angle: num.parse('${json[r'angle']}'),
|
||||||
axis: MirrorAxis.fromJson(json[r'axis'])!,
|
axis: MirrorAxis.fromJson(json[r'axis'])!,
|
||||||
);
|
);
|
||||||
|
|||||||
+12
-11
@@ -57,8 +57,11 @@ class AssetResponseDto {
|
|||||||
/// Duplicate group ID
|
/// Duplicate group ID
|
||||||
String? duplicateId;
|
String? duplicateId;
|
||||||
|
|
||||||
/// Video/gif duration in hh:mm:ss.SSS format (null for static images)
|
/// Video/gif duration in milliseconds (null for static images)
|
||||||
String? duration;
|
///
|
||||||
|
/// Minimum value: 0
|
||||||
|
/// Maximum value: 2147483647
|
||||||
|
int? duration;
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
@@ -80,7 +83,8 @@ class AssetResponseDto {
|
|||||||
/// Asset height
|
/// Asset height
|
||||||
///
|
///
|
||||||
/// Minimum value: 0
|
/// Minimum value: 0
|
||||||
num? height;
|
/// Maximum value: 9007199254740991
|
||||||
|
int? height;
|
||||||
|
|
||||||
/// Asset ID
|
/// Asset ID
|
||||||
String id;
|
String id;
|
||||||
@@ -165,7 +169,8 @@ class AssetResponseDto {
|
|||||||
/// Asset width
|
/// Asset width
|
||||||
///
|
///
|
||||||
/// Minimum value: 0
|
/// Minimum value: 0
|
||||||
num? width;
|
/// Maximum value: 9007199254740991
|
||||||
|
int? width;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto &&
|
bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto &&
|
||||||
@@ -341,14 +346,12 @@ class AssetResponseDto {
|
|||||||
checksum: mapValueOfType<String>(json, r'checksum')!,
|
checksum: mapValueOfType<String>(json, r'checksum')!,
|
||||||
createdAt: mapDateTime(json, r'createdAt', r'')!,
|
createdAt: mapDateTime(json, r'createdAt', r'')!,
|
||||||
duplicateId: mapValueOfType<String>(json, r'duplicateId'),
|
duplicateId: mapValueOfType<String>(json, r'duplicateId'),
|
||||||
duration: mapValueOfType<String>(json, r'duration'),
|
duration: mapValueOfType<int>(json, r'duration'),
|
||||||
exifInfo: ExifResponseDto.fromJson(json[r'exifInfo']),
|
exifInfo: ExifResponseDto.fromJson(json[r'exifInfo']),
|
||||||
fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r'')!,
|
fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r'')!,
|
||||||
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r'')!,
|
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r'')!,
|
||||||
hasMetadata: mapValueOfType<bool>(json, r'hasMetadata')!,
|
hasMetadata: mapValueOfType<bool>(json, r'hasMetadata')!,
|
||||||
height: json[r'height'] == null
|
height: mapValueOfType<int>(json, r'height'),
|
||||||
? null
|
|
||||||
: num.parse('${json[r'height']}'),
|
|
||||||
id: mapValueOfType<String>(json, r'id')!,
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
isArchived: mapValueOfType<bool>(json, r'isArchived')!,
|
isArchived: mapValueOfType<bool>(json, r'isArchived')!,
|
||||||
isEdited: mapValueOfType<bool>(json, r'isEdited')!,
|
isEdited: mapValueOfType<bool>(json, r'isEdited')!,
|
||||||
@@ -372,9 +375,7 @@ class AssetResponseDto {
|
|||||||
unassignedFaces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'unassignedFaces']),
|
unassignedFaces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'unassignedFaces']),
|
||||||
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
|
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
|
||||||
visibility: AssetVisibility.fromJson(json[r'visibility'])!,
|
visibility: AssetVisibility.fromJson(json[r'visibility'])!,
|
||||||
width: json[r'width'] == null
|
width: mapValueOfType<int>(json, r'width'),
|
||||||
? null
|
|
||||||
: num.parse('${json[r'width']}'),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+12
-8
@@ -22,22 +22,26 @@ class CropParameters {
|
|||||||
/// Height of the crop
|
/// Height of the crop
|
||||||
///
|
///
|
||||||
/// Minimum value: 1
|
/// Minimum value: 1
|
||||||
num height;
|
/// Maximum value: 9007199254740991
|
||||||
|
int height;
|
||||||
|
|
||||||
/// Width of the crop
|
/// Width of the crop
|
||||||
///
|
///
|
||||||
/// Minimum value: 1
|
/// Minimum value: 1
|
||||||
num width;
|
/// Maximum value: 9007199254740991
|
||||||
|
int width;
|
||||||
|
|
||||||
/// Top-Left X coordinate of crop
|
/// Top-Left X coordinate of crop
|
||||||
///
|
///
|
||||||
/// Minimum value: 0
|
/// Minimum value: 0
|
||||||
num x;
|
/// Maximum value: 9007199254740991
|
||||||
|
int x;
|
||||||
|
|
||||||
/// Top-Left Y coordinate of crop
|
/// Top-Left Y coordinate of crop
|
||||||
///
|
///
|
||||||
/// Minimum value: 0
|
/// Minimum value: 0
|
||||||
num y;
|
/// Maximum value: 9007199254740991
|
||||||
|
int y;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is CropParameters &&
|
bool operator ==(Object other) => identical(this, other) || other is CropParameters &&
|
||||||
@@ -75,10 +79,10 @@ class CropParameters {
|
|||||||
final json = value.cast<String, dynamic>();
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
return CropParameters(
|
return CropParameters(
|
||||||
height: num.parse('${json[r'height']}'),
|
height: mapValueOfType<int>(json, r'height')!,
|
||||||
width: num.parse('${json[r'width']}'),
|
width: mapValueOfType<int>(json, r'width')!,
|
||||||
x: num.parse('${json[r'x']}'),
|
x: mapValueOfType<int>(json, r'x')!,
|
||||||
y: num.parse('${json[r'y']}'),
|
y: mapValueOfType<int>(json, r'y')!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+3
-2
@@ -27,7 +27,8 @@ class DatabaseBackupConfig {
|
|||||||
/// Keep last amount
|
/// Keep last amount
|
||||||
///
|
///
|
||||||
/// Minimum value: 1
|
/// Minimum value: 1
|
||||||
num keepLastAmount;
|
/// Maximum value: 9007199254740991
|
||||||
|
int keepLastAmount;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is DatabaseBackupConfig &&
|
bool operator ==(Object other) => identical(this, other) || other is DatabaseBackupConfig &&
|
||||||
@@ -64,7 +65,7 @@ class DatabaseBackupConfig {
|
|||||||
return DatabaseBackupConfig(
|
return DatabaseBackupConfig(
|
||||||
cronExpression: mapValueOfType<String>(json, r'cronExpression')!,
|
cronExpression: mapValueOfType<String>(json, r'cronExpression')!,
|
||||||
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||||
keepLastAmount: num.parse('${json[r'keepLastAmount']}'),
|
keepLastAmount: mapValueOfType<int>(json, r'keepLastAmount')!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+5
-2
@@ -22,7 +22,10 @@ class DatabaseBackupDto {
|
|||||||
String filename;
|
String filename;
|
||||||
|
|
||||||
/// Backup file size
|
/// Backup file size
|
||||||
num filesize;
|
///
|
||||||
|
/// Minimum value: -9007199254740991
|
||||||
|
/// Maximum value: 9007199254740991
|
||||||
|
int filesize;
|
||||||
|
|
||||||
/// Backup timezone
|
/// Backup timezone
|
||||||
String timezone;
|
String timezone;
|
||||||
@@ -61,7 +64,7 @@ class DatabaseBackupDto {
|
|||||||
|
|
||||||
return DatabaseBackupDto(
|
return DatabaseBackupDto(
|
||||||
filename: mapValueOfType<String>(json, r'filename')!,
|
filename: mapValueOfType<String>(json, r'filename')!,
|
||||||
filesize: num.parse('${json[r'filesize']}'),
|
filesize: mapValueOfType<int>(json, r'filesize')!,
|
||||||
timezone: mapValueOfType<String>(json, r'timezone')!,
|
timezone: mapValueOfType<String>(json, r'timezone')!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-16
@@ -52,12 +52,14 @@ class ExifResponseDto {
|
|||||||
/// Image height in pixels
|
/// Image height in pixels
|
||||||
///
|
///
|
||||||
/// Minimum value: 0
|
/// Minimum value: 0
|
||||||
num? exifImageHeight;
|
/// Maximum value: 9007199254740991
|
||||||
|
int? exifImageHeight;
|
||||||
|
|
||||||
/// Image width in pixels
|
/// Image width in pixels
|
||||||
///
|
///
|
||||||
/// Minimum value: 0
|
/// Minimum value: 0
|
||||||
num? exifImageWidth;
|
/// Maximum value: 9007199254740991
|
||||||
|
int? exifImageWidth;
|
||||||
|
|
||||||
/// Exposure time
|
/// Exposure time
|
||||||
String? exposureTime;
|
String? exposureTime;
|
||||||
@@ -75,7 +77,10 @@ class ExifResponseDto {
|
|||||||
num? focalLength;
|
num? focalLength;
|
||||||
|
|
||||||
/// ISO sensitivity
|
/// ISO sensitivity
|
||||||
num? iso;
|
///
|
||||||
|
/// Minimum value: -9007199254740991
|
||||||
|
/// Maximum value: 9007199254740991
|
||||||
|
int? iso;
|
||||||
|
|
||||||
/// GPS latitude
|
/// GPS latitude
|
||||||
num? latitude;
|
num? latitude;
|
||||||
@@ -102,7 +107,10 @@ class ExifResponseDto {
|
|||||||
String? projectionType;
|
String? projectionType;
|
||||||
|
|
||||||
/// Rating
|
/// Rating
|
||||||
num? rating;
|
///
|
||||||
|
/// Minimum value: -9007199254740991
|
||||||
|
/// Maximum value: 9007199254740991
|
||||||
|
int? rating;
|
||||||
|
|
||||||
/// State/province name
|
/// State/province name
|
||||||
String? state;
|
String? state;
|
||||||
@@ -292,12 +300,8 @@ class ExifResponseDto {
|
|||||||
country: mapValueOfType<String>(json, r'country'),
|
country: mapValueOfType<String>(json, r'country'),
|
||||||
dateTimeOriginal: mapDateTime(json, r'dateTimeOriginal', r''),
|
dateTimeOriginal: mapDateTime(json, r'dateTimeOriginal', r''),
|
||||||
description: mapValueOfType<String>(json, r'description'),
|
description: mapValueOfType<String>(json, r'description'),
|
||||||
exifImageHeight: json[r'exifImageHeight'] == null
|
exifImageHeight: mapValueOfType<int>(json, r'exifImageHeight'),
|
||||||
? null
|
exifImageWidth: mapValueOfType<int>(json, r'exifImageWidth'),
|
||||||
: num.parse('${json[r'exifImageHeight']}'),
|
|
||||||
exifImageWidth: json[r'exifImageWidth'] == null
|
|
||||||
? null
|
|
||||||
: num.parse('${json[r'exifImageWidth']}'),
|
|
||||||
exposureTime: mapValueOfType<String>(json, r'exposureTime'),
|
exposureTime: mapValueOfType<String>(json, r'exposureTime'),
|
||||||
fNumber: json[r'fNumber'] == null
|
fNumber: json[r'fNumber'] == null
|
||||||
? null
|
? null
|
||||||
@@ -306,9 +310,7 @@ class ExifResponseDto {
|
|||||||
focalLength: json[r'focalLength'] == null
|
focalLength: json[r'focalLength'] == null
|
||||||
? null
|
? null
|
||||||
: num.parse('${json[r'focalLength']}'),
|
: num.parse('${json[r'focalLength']}'),
|
||||||
iso: json[r'iso'] == null
|
iso: mapValueOfType<int>(json, r'iso'),
|
||||||
? null
|
|
||||||
: num.parse('${json[r'iso']}'),
|
|
||||||
latitude: json[r'latitude'] == null
|
latitude: json[r'latitude'] == null
|
||||||
? null
|
? null
|
||||||
: num.parse('${json[r'latitude']}'),
|
: num.parse('${json[r'latitude']}'),
|
||||||
@@ -321,9 +323,7 @@ class ExifResponseDto {
|
|||||||
modifyDate: mapDateTime(json, r'modifyDate', r''),
|
modifyDate: mapDateTime(json, r'modifyDate', r''),
|
||||||
orientation: mapValueOfType<String>(json, r'orientation'),
|
orientation: mapValueOfType<String>(json, r'orientation'),
|
||||||
projectionType: mapValueOfType<String>(json, r'projectionType'),
|
projectionType: mapValueOfType<String>(json, r'projectionType'),
|
||||||
rating: json[r'rating'] == null
|
rating: mapValueOfType<int>(json, r'rating'),
|
||||||
? null
|
|
||||||
: num.parse('${json[r'rating']}'),
|
|
||||||
state: mapValueOfType<String>(json, r'state'),
|
state: mapValueOfType<String>(json, r'state'),
|
||||||
timeZone: mapValueOfType<String>(json, r'timeZone'),
|
timeZone: mapValueOfType<String>(json, r'timeZone'),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,9 +21,13 @@ class MachineLearningAvailabilityChecksDto {
|
|||||||
/// Enabled
|
/// Enabled
|
||||||
bool enabled;
|
bool enabled;
|
||||||
|
|
||||||
num interval;
|
/// Minimum value: -9007199254740991
|
||||||
|
/// Maximum value: 9007199254740991
|
||||||
|
int interval;
|
||||||
|
|
||||||
num timeout;
|
/// Minimum value: -9007199254740991
|
||||||
|
/// Maximum value: 9007199254740991
|
||||||
|
int timeout;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is MachineLearningAvailabilityChecksDto &&
|
bool operator ==(Object other) => identical(this, other) || other is MachineLearningAvailabilityChecksDto &&
|
||||||
@@ -59,8 +63,8 @@ class MachineLearningAvailabilityChecksDto {
|
|||||||
|
|
||||||
return MachineLearningAvailabilityChecksDto(
|
return MachineLearningAvailabilityChecksDto(
|
||||||
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||||
interval: num.parse('${json[r'interval']}'),
|
interval: mapValueOfType<int>(json, r'interval')!,
|
||||||
timeout: num.parse('${json[r'timeout']}'),
|
timeout: mapValueOfType<int>(json, r'timeout')!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+5
-2
@@ -20,7 +20,10 @@ class MaintenanceDetectInstallStorageFolderDto {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/// Number of files in the folder
|
/// Number of files in the folder
|
||||||
num files;
|
///
|
||||||
|
/// Minimum value: -9007199254740991
|
||||||
|
/// Maximum value: 9007199254740991
|
||||||
|
int files;
|
||||||
|
|
||||||
StorageFolder folder;
|
StorageFolder folder;
|
||||||
|
|
||||||
@@ -66,7 +69,7 @@ class MaintenanceDetectInstallStorageFolderDto {
|
|||||||
final json = value.cast<String, dynamic>();
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
return MaintenanceDetectInstallStorageFolderDto(
|
return MaintenanceDetectInstallStorageFolderDto(
|
||||||
files: num.parse('${json[r'files']}'),
|
files: mapValueOfType<int>(json, r'files')!,
|
||||||
folder: StorageFolder.fromJson(json[r'folder'])!,
|
folder: StorageFolder.fromJson(json[r'folder'])!,
|
||||||
readable: mapValueOfType<bool>(json, r'readable')!,
|
readable: mapValueOfType<bool>(json, r'readable')!,
|
||||||
writable: mapValueOfType<bool>(json, r'writable')!,
|
writable: mapValueOfType<bool>(json, r'writable')!,
|
||||||
|
|||||||
@@ -32,13 +32,15 @@ class MaintenanceStatusResponseDto {
|
|||||||
///
|
///
|
||||||
String? error;
|
String? error;
|
||||||
|
|
||||||
|
/// Minimum value: -9007199254740991
|
||||||
|
/// Maximum value: 9007199254740991
|
||||||
///
|
///
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
/// does not include a default value (using the "default:" property), however, the generated
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
/// source code must fall back to having a nullable type.
|
/// source code must fall back to having a nullable type.
|
||||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
///
|
///
|
||||||
num? progress;
|
int? progress;
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
@@ -102,7 +104,7 @@ class MaintenanceStatusResponseDto {
|
|||||||
action: MaintenanceAction.fromJson(json[r'action'])!,
|
action: MaintenanceAction.fromJson(json[r'action'])!,
|
||||||
active: mapValueOfType<bool>(json, r'active')!,
|
active: mapValueOfType<bool>(json, r'active')!,
|
||||||
error: mapValueOfType<String>(json, r'error'),
|
error: mapValueOfType<String>(json, r'error'),
|
||||||
progress: num.parse('${json[r'progress']}'),
|
progress: mapValueOfType<int>(json, r'progress'),
|
||||||
task: mapValueOfType<String>(json, r'task'),
|
task: mapValueOfType<String>(json, r'task'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-8
@@ -215,13 +215,14 @@ class MetadataSearchDto {
|
|||||||
/// Page number
|
/// Page number
|
||||||
///
|
///
|
||||||
/// Minimum value: 1
|
/// Minimum value: 1
|
||||||
|
/// Maximum value: 9007199254740991
|
||||||
///
|
///
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
/// does not include a default value (using the "default:" property), however, the generated
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
/// source code must fall back to having a nullable type.
|
/// source code must fall back to having a nullable type.
|
||||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
///
|
///
|
||||||
num? page;
|
int? page;
|
||||||
|
|
||||||
/// Filter by person IDs
|
/// Filter by person IDs
|
||||||
List<String> personIds;
|
List<String> personIds;
|
||||||
@@ -239,7 +240,7 @@ class MetadataSearchDto {
|
|||||||
///
|
///
|
||||||
/// Minimum value: -1
|
/// Minimum value: -1
|
||||||
/// Maximum value: 5
|
/// Maximum value: 5
|
||||||
num? rating;
|
int? rating;
|
||||||
|
|
||||||
/// Number of results to return
|
/// Number of results to return
|
||||||
///
|
///
|
||||||
@@ -251,7 +252,7 @@ class MetadataSearchDto {
|
|||||||
/// source code must fall back to having a nullable type.
|
/// source code must fall back to having a nullable type.
|
||||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
///
|
///
|
||||||
num? size;
|
int? size;
|
||||||
|
|
||||||
/// Filter by state/province name
|
/// Filter by state/province name
|
||||||
String? state;
|
String? state;
|
||||||
@@ -724,15 +725,13 @@ class MetadataSearchDto {
|
|||||||
order: AssetOrder.fromJson(json[r'order']),
|
order: AssetOrder.fromJson(json[r'order']),
|
||||||
originalFileName: mapValueOfType<String>(json, r'originalFileName'),
|
originalFileName: mapValueOfType<String>(json, r'originalFileName'),
|
||||||
originalPath: mapValueOfType<String>(json, r'originalPath'),
|
originalPath: mapValueOfType<String>(json, r'originalPath'),
|
||||||
page: num.parse('${json[r'page']}'),
|
page: mapValueOfType<int>(json, r'page'),
|
||||||
personIds: json[r'personIds'] is Iterable
|
personIds: json[r'personIds'] is Iterable
|
||||||
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
|
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
|
||||||
: const [],
|
: const [],
|
||||||
previewPath: mapValueOfType<String>(json, r'previewPath'),
|
previewPath: mapValueOfType<String>(json, r'previewPath'),
|
||||||
rating: json[r'rating'] == null
|
rating: mapValueOfType<int>(json, r'rating'),
|
||||||
? null
|
size: mapValueOfType<int>(json, r'size'),
|
||||||
: num.parse('${json[r'rating']}'),
|
|
||||||
size: num.parse('${json[r'size']}'),
|
|
||||||
state: mapValueOfType<String>(json, r'state'),
|
state: mapValueOfType<String>(json, r'state'),
|
||||||
tagIds: json[r'tagIds'] is Iterable
|
tagIds: json[r'tagIds'] is Iterable
|
||||||
? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false)
|
? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false)
|
||||||
|
|||||||
+4
-6
@@ -147,7 +147,7 @@ class RandomSearchDto {
|
|||||||
///
|
///
|
||||||
/// Minimum value: -1
|
/// Minimum value: -1
|
||||||
/// Maximum value: 5
|
/// Maximum value: 5
|
||||||
num? rating;
|
int? rating;
|
||||||
|
|
||||||
/// Number of results to return
|
/// Number of results to return
|
||||||
///
|
///
|
||||||
@@ -159,7 +159,7 @@ class RandomSearchDto {
|
|||||||
/// source code must fall back to having a nullable type.
|
/// source code must fall back to having a nullable type.
|
||||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
///
|
///
|
||||||
num? size;
|
int? size;
|
||||||
|
|
||||||
/// Filter by state/province name
|
/// Filter by state/province name
|
||||||
String? state;
|
String? state;
|
||||||
@@ -549,10 +549,8 @@ class RandomSearchDto {
|
|||||||
personIds: json[r'personIds'] is Iterable
|
personIds: json[r'personIds'] is Iterable
|
||||||
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
|
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
|
||||||
: const [],
|
: const [],
|
||||||
rating: json[r'rating'] == null
|
rating: mapValueOfType<int>(json, r'rating'),
|
||||||
? null
|
size: mapValueOfType<int>(json, r'size'),
|
||||||
: num.parse('${json[r'rating']}'),
|
|
||||||
size: num.parse('${json[r'size']}'),
|
|
||||||
state: mapValueOfType<String>(json, r'state'),
|
state: mapValueOfType<String>(json, r'state'),
|
||||||
tagIds: json[r'tagIds'] is Iterable
|
tagIds: json[r'tagIds'] is Iterable
|
||||||
? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false)
|
? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false)
|
||||||
|
|||||||
+3
-2
@@ -39,13 +39,14 @@ class SessionCreateDto {
|
|||||||
/// Session duration in seconds
|
/// Session duration in seconds
|
||||||
///
|
///
|
||||||
/// Minimum value: 1
|
/// Minimum value: 1
|
||||||
|
/// Maximum value: 9007199254740991
|
||||||
///
|
///
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
/// does not include a default value (using the "default:" property), however, the generated
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
/// source code must fall back to having a nullable type.
|
/// source code must fall back to having a nullable type.
|
||||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
///
|
///
|
||||||
num? duration;
|
int? duration;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is SessionCreateDto &&
|
bool operator ==(Object other) => identical(this, other) || other is SessionCreateDto &&
|
||||||
@@ -94,7 +95,7 @@ class SessionCreateDto {
|
|||||||
return SessionCreateDto(
|
return SessionCreateDto(
|
||||||
deviceOS: mapValueOfType<String>(json, r'deviceOS'),
|
deviceOS: mapValueOfType<String>(json, r'deviceOS'),
|
||||||
deviceType: mapValueOfType<String>(json, r'deviceType'),
|
deviceType: mapValueOfType<String>(json, r'deviceType'),
|
||||||
duration: num.parse('${json[r'duration']}'),
|
duration: mapValueOfType<int>(json, r'duration'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+7
-8
@@ -154,13 +154,14 @@ class SmartSearchDto {
|
|||||||
/// Page number
|
/// Page number
|
||||||
///
|
///
|
||||||
/// Minimum value: 1
|
/// Minimum value: 1
|
||||||
|
/// Maximum value: 9007199254740991
|
||||||
///
|
///
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
/// does not include a default value (using the "default:" property), however, the generated
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
/// source code must fall back to having a nullable type.
|
/// source code must fall back to having a nullable type.
|
||||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
///
|
///
|
||||||
num? page;
|
int? page;
|
||||||
|
|
||||||
/// Filter by person IDs
|
/// Filter by person IDs
|
||||||
List<String> personIds;
|
List<String> personIds;
|
||||||
@@ -187,7 +188,7 @@ class SmartSearchDto {
|
|||||||
///
|
///
|
||||||
/// Minimum value: -1
|
/// Minimum value: -1
|
||||||
/// Maximum value: 5
|
/// Maximum value: 5
|
||||||
num? rating;
|
int? rating;
|
||||||
|
|
||||||
/// Number of results to return
|
/// Number of results to return
|
||||||
///
|
///
|
||||||
@@ -199,7 +200,7 @@ class SmartSearchDto {
|
|||||||
/// source code must fall back to having a nullable type.
|
/// source code must fall back to having a nullable type.
|
||||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
///
|
///
|
||||||
num? size;
|
int? size;
|
||||||
|
|
||||||
/// Filter by state/province name
|
/// Filter by state/province name
|
||||||
String? state;
|
String? state;
|
||||||
@@ -583,16 +584,14 @@ class SmartSearchDto {
|
|||||||
make: mapValueOfType<String>(json, r'make'),
|
make: mapValueOfType<String>(json, r'make'),
|
||||||
model: mapValueOfType<String>(json, r'model'),
|
model: mapValueOfType<String>(json, r'model'),
|
||||||
ocr: mapValueOfType<String>(json, r'ocr'),
|
ocr: mapValueOfType<String>(json, r'ocr'),
|
||||||
page: num.parse('${json[r'page']}'),
|
page: mapValueOfType<int>(json, r'page'),
|
||||||
personIds: json[r'personIds'] is Iterable
|
personIds: json[r'personIds'] is Iterable
|
||||||
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
|
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
|
||||||
: const [],
|
: const [],
|
||||||
query: mapValueOfType<String>(json, r'query'),
|
query: mapValueOfType<String>(json, r'query'),
|
||||||
queryAssetId: mapValueOfType<String>(json, r'queryAssetId'),
|
queryAssetId: mapValueOfType<String>(json, r'queryAssetId'),
|
||||||
rating: json[r'rating'] == null
|
rating: mapValueOfType<int>(json, r'rating'),
|
||||||
? null
|
size: mapValueOfType<int>(json, r'size'),
|
||||||
: num.parse('${json[r'rating']}'),
|
|
||||||
size: num.parse('${json[r'size']}'),
|
|
||||||
state: mapValueOfType<String>(json, r'state'),
|
state: mapValueOfType<String>(json, r'state'),
|
||||||
tagIds: json[r'tagIds'] is Iterable
|
tagIds: json[r'tagIds'] is Iterable
|
||||||
? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false)
|
? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false)
|
||||||
|
|||||||
+2
-4
@@ -152,7 +152,7 @@ class StatisticsSearchDto {
|
|||||||
///
|
///
|
||||||
/// Minimum value: -1
|
/// Minimum value: -1
|
||||||
/// Maximum value: 5
|
/// Maximum value: 5
|
||||||
num? rating;
|
int? rating;
|
||||||
|
|
||||||
/// Filter by state/province name
|
/// Filter by state/province name
|
||||||
String? state;
|
String? state;
|
||||||
@@ -479,9 +479,7 @@ class StatisticsSearchDto {
|
|||||||
personIds: json[r'personIds'] is Iterable
|
personIds: json[r'personIds'] is Iterable
|
||||||
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
|
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
|
||||||
: const [],
|
: const [],
|
||||||
rating: json[r'rating'] == null
|
rating: mapValueOfType<int>(json, r'rating'),
|
||||||
? null
|
|
||||||
: num.parse('${json[r'rating']}'),
|
|
||||||
state: mapValueOfType<String>(json, r'state'),
|
state: mapValueOfType<String>(json, r'state'),
|
||||||
tagIds: json[r'tagIds'] is Iterable
|
tagIds: json[r'tagIds'] is Iterable
|
||||||
? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false)
|
? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false)
|
||||||
|
|||||||
+1
-10
@@ -14,7 +14,6 @@ class SyncAlbumUserV1 {
|
|||||||
/// Returns a new [SyncAlbumUserV1] instance.
|
/// Returns a new [SyncAlbumUserV1] instance.
|
||||||
SyncAlbumUserV1({
|
SyncAlbumUserV1({
|
||||||
required this.albumId,
|
required this.albumId,
|
||||||
required this.isFavorite,
|
|
||||||
required this.role,
|
required this.role,
|
||||||
required this.userId,
|
required this.userId,
|
||||||
});
|
});
|
||||||
@@ -22,9 +21,6 @@ class SyncAlbumUserV1 {
|
|||||||
/// Album ID
|
/// Album ID
|
||||||
String albumId;
|
String albumId;
|
||||||
|
|
||||||
/// Favorite flag
|
|
||||||
bool isFavorite;
|
|
||||||
|
|
||||||
AlbumUserRole role;
|
AlbumUserRole role;
|
||||||
|
|
||||||
/// User ID
|
/// User ID
|
||||||
@@ -33,7 +29,6 @@ class SyncAlbumUserV1 {
|
|||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is SyncAlbumUserV1 &&
|
bool operator ==(Object other) => identical(this, other) || other is SyncAlbumUserV1 &&
|
||||||
other.albumId == albumId &&
|
other.albumId == albumId &&
|
||||||
other.isFavorite == isFavorite &&
|
|
||||||
other.role == role &&
|
other.role == role &&
|
||||||
other.userId == userId;
|
other.userId == userId;
|
||||||
|
|
||||||
@@ -41,17 +36,15 @@ class SyncAlbumUserV1 {
|
|||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(albumId.hashCode) +
|
(albumId.hashCode) +
|
||||||
(isFavorite.hashCode) +
|
|
||||||
(role.hashCode) +
|
(role.hashCode) +
|
||||||
(userId.hashCode);
|
(userId.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SyncAlbumUserV1[albumId=$albumId, isFavorite=$isFavorite, role=$role, userId=$userId]';
|
String toString() => 'SyncAlbumUserV1[albumId=$albumId, role=$role, userId=$userId]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
json[r'albumId'] = this.albumId;
|
json[r'albumId'] = this.albumId;
|
||||||
json[r'isFavorite'] = this.isFavorite;
|
|
||||||
json[r'role'] = this.role;
|
json[r'role'] = this.role;
|
||||||
json[r'userId'] = this.userId;
|
json[r'userId'] = this.userId;
|
||||||
return json;
|
return json;
|
||||||
@@ -67,7 +60,6 @@ class SyncAlbumUserV1 {
|
|||||||
|
|
||||||
return SyncAlbumUserV1(
|
return SyncAlbumUserV1(
|
||||||
albumId: mapValueOfType<String>(json, r'albumId')!,
|
albumId: mapValueOfType<String>(json, r'albumId')!,
|
||||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
|
|
||||||
role: AlbumUserRole.fromJson(json[r'role'])!,
|
role: AlbumUserRole.fromJson(json[r'role'])!,
|
||||||
userId: mapValueOfType<String>(json, r'userId')!,
|
userId: mapValueOfType<String>(json, r'userId')!,
|
||||||
);
|
);
|
||||||
@@ -118,7 +110,6 @@ class SyncAlbumUserV1 {
|
|||||||
/// The list of required keys that must be present in a JSON.
|
/// The list of required keys that must be present in a JSON.
|
||||||
static const requiredKeys = <String>{
|
static const requiredKeys = <String>{
|
||||||
'albumId',
|
'albumId',
|
||||||
'isFavorite',
|
|
||||||
'role',
|
'role',
|
||||||
'userId',
|
'userId',
|
||||||
};
|
};
|
||||||
|
|||||||
+321
@@ -0,0 +1,321 @@
|
|||||||
|
//
|
||||||
|
// 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 SyncAssetV2 {
|
||||||
|
/// Returns a new [SyncAssetV2] instance.
|
||||||
|
SyncAssetV2({
|
||||||
|
required this.checksum,
|
||||||
|
required this.deletedAt,
|
||||||
|
required this.duration,
|
||||||
|
required this.fileCreatedAt,
|
||||||
|
required this.fileModifiedAt,
|
||||||
|
required this.height,
|
||||||
|
required this.id,
|
||||||
|
required this.isEdited,
|
||||||
|
required this.isFavorite,
|
||||||
|
required this.libraryId,
|
||||||
|
required this.livePhotoVideoId,
|
||||||
|
required this.localDateTime,
|
||||||
|
required this.originalFileName,
|
||||||
|
required this.ownerId,
|
||||||
|
required this.stackId,
|
||||||
|
required this.thumbhash,
|
||||||
|
required this.type,
|
||||||
|
required this.visibility,
|
||||||
|
required this.width,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Checksum
|
||||||
|
String checksum;
|
||||||
|
|
||||||
|
/// Deleted at
|
||||||
|
DateTime? deletedAt;
|
||||||
|
|
||||||
|
/// Duration
|
||||||
|
///
|
||||||
|
/// Minimum value: 0
|
||||||
|
/// Maximum value: 2147483647
|
||||||
|
int? duration;
|
||||||
|
|
||||||
|
/// File created at
|
||||||
|
DateTime? fileCreatedAt;
|
||||||
|
|
||||||
|
/// File modified at
|
||||||
|
DateTime? fileModifiedAt;
|
||||||
|
|
||||||
|
/// Asset height
|
||||||
|
///
|
||||||
|
/// Minimum value: -9007199254740991
|
||||||
|
/// Maximum value: 9007199254740991
|
||||||
|
int? height;
|
||||||
|
|
||||||
|
/// Asset ID
|
||||||
|
String id;
|
||||||
|
|
||||||
|
/// Is edited
|
||||||
|
bool isEdited;
|
||||||
|
|
||||||
|
/// Is favorite
|
||||||
|
bool isFavorite;
|
||||||
|
|
||||||
|
/// Library ID
|
||||||
|
String? libraryId;
|
||||||
|
|
||||||
|
/// Live photo video ID
|
||||||
|
String? livePhotoVideoId;
|
||||||
|
|
||||||
|
/// Local date time
|
||||||
|
DateTime? localDateTime;
|
||||||
|
|
||||||
|
/// Original file name
|
||||||
|
String originalFileName;
|
||||||
|
|
||||||
|
/// Owner ID
|
||||||
|
String ownerId;
|
||||||
|
|
||||||
|
/// Stack ID
|
||||||
|
String? stackId;
|
||||||
|
|
||||||
|
/// Thumbhash
|
||||||
|
String? thumbhash;
|
||||||
|
|
||||||
|
AssetTypeEnum type;
|
||||||
|
|
||||||
|
AssetVisibility visibility;
|
||||||
|
|
||||||
|
/// Asset width
|
||||||
|
///
|
||||||
|
/// Minimum value: -9007199254740991
|
||||||
|
/// Maximum value: 9007199254740991
|
||||||
|
int? width;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is SyncAssetV2 &&
|
||||||
|
other.checksum == checksum &&
|
||||||
|
other.deletedAt == deletedAt &&
|
||||||
|
other.duration == duration &&
|
||||||
|
other.fileCreatedAt == fileCreatedAt &&
|
||||||
|
other.fileModifiedAt == fileModifiedAt &&
|
||||||
|
other.height == height &&
|
||||||
|
other.id == id &&
|
||||||
|
other.isEdited == isEdited &&
|
||||||
|
other.isFavorite == isFavorite &&
|
||||||
|
other.libraryId == libraryId &&
|
||||||
|
other.livePhotoVideoId == livePhotoVideoId &&
|
||||||
|
other.localDateTime == localDateTime &&
|
||||||
|
other.originalFileName == originalFileName &&
|
||||||
|
other.ownerId == ownerId &&
|
||||||
|
other.stackId == stackId &&
|
||||||
|
other.thumbhash == thumbhash &&
|
||||||
|
other.type == type &&
|
||||||
|
other.visibility == visibility &&
|
||||||
|
other.width == width;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(checksum.hashCode) +
|
||||||
|
(deletedAt == null ? 0 : deletedAt!.hashCode) +
|
||||||
|
(duration == null ? 0 : duration!.hashCode) +
|
||||||
|
(fileCreatedAt == null ? 0 : fileCreatedAt!.hashCode) +
|
||||||
|
(fileModifiedAt == null ? 0 : fileModifiedAt!.hashCode) +
|
||||||
|
(height == null ? 0 : height!.hashCode) +
|
||||||
|
(id.hashCode) +
|
||||||
|
(isEdited.hashCode) +
|
||||||
|
(isFavorite.hashCode) +
|
||||||
|
(libraryId == null ? 0 : libraryId!.hashCode) +
|
||||||
|
(livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
|
||||||
|
(localDateTime == null ? 0 : localDateTime!.hashCode) +
|
||||||
|
(originalFileName.hashCode) +
|
||||||
|
(ownerId.hashCode) +
|
||||||
|
(stackId == null ? 0 : stackId!.hashCode) +
|
||||||
|
(thumbhash == null ? 0 : thumbhash!.hashCode) +
|
||||||
|
(type.hashCode) +
|
||||||
|
(visibility.hashCode) +
|
||||||
|
(width == null ? 0 : width!.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'SyncAssetV2[checksum=$checksum, deletedAt=$deletedAt, duration=$duration, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, height=$height, id=$id, isEdited=$isEdited, isFavorite=$isFavorite, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, ownerId=$ownerId, stackId=$stackId, thumbhash=$thumbhash, type=$type, visibility=$visibility, width=$width]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'checksum'] = this.checksum;
|
||||||
|
if (this.deletedAt != null) {
|
||||||
|
json[r'deletedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
|
||||||
|
? this.deletedAt!.millisecondsSinceEpoch
|
||||||
|
: this.deletedAt!.toUtc().toIso8601String();
|
||||||
|
} else {
|
||||||
|
// json[r'deletedAt'] = null;
|
||||||
|
}
|
||||||
|
if (this.duration != null) {
|
||||||
|
json[r'duration'] = this.duration;
|
||||||
|
} else {
|
||||||
|
// json[r'duration'] = null;
|
||||||
|
}
|
||||||
|
if (this.fileCreatedAt != null) {
|
||||||
|
json[r'fileCreatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
|
||||||
|
? this.fileCreatedAt!.millisecondsSinceEpoch
|
||||||
|
: this.fileCreatedAt!.toUtc().toIso8601String();
|
||||||
|
} else {
|
||||||
|
// json[r'fileCreatedAt'] = null;
|
||||||
|
}
|
||||||
|
if (this.fileModifiedAt != null) {
|
||||||
|
json[r'fileModifiedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
|
||||||
|
? this.fileModifiedAt!.millisecondsSinceEpoch
|
||||||
|
: this.fileModifiedAt!.toUtc().toIso8601String();
|
||||||
|
} else {
|
||||||
|
// json[r'fileModifiedAt'] = null;
|
||||||
|
}
|
||||||
|
if (this.height != null) {
|
||||||
|
json[r'height'] = this.height;
|
||||||
|
} else {
|
||||||
|
// json[r'height'] = null;
|
||||||
|
}
|
||||||
|
json[r'id'] = this.id;
|
||||||
|
json[r'isEdited'] = this.isEdited;
|
||||||
|
json[r'isFavorite'] = this.isFavorite;
|
||||||
|
if (this.libraryId != null) {
|
||||||
|
json[r'libraryId'] = this.libraryId;
|
||||||
|
} else {
|
||||||
|
// json[r'libraryId'] = null;
|
||||||
|
}
|
||||||
|
if (this.livePhotoVideoId != null) {
|
||||||
|
json[r'livePhotoVideoId'] = this.livePhotoVideoId;
|
||||||
|
} else {
|
||||||
|
// json[r'livePhotoVideoId'] = null;
|
||||||
|
}
|
||||||
|
if (this.localDateTime != null) {
|
||||||
|
json[r'localDateTime'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
|
||||||
|
? this.localDateTime!.millisecondsSinceEpoch
|
||||||
|
: this.localDateTime!.toUtc().toIso8601String();
|
||||||
|
} else {
|
||||||
|
// json[r'localDateTime'] = null;
|
||||||
|
}
|
||||||
|
json[r'originalFileName'] = this.originalFileName;
|
||||||
|
json[r'ownerId'] = this.ownerId;
|
||||||
|
if (this.stackId != null) {
|
||||||
|
json[r'stackId'] = this.stackId;
|
||||||
|
} else {
|
||||||
|
// json[r'stackId'] = null;
|
||||||
|
}
|
||||||
|
if (this.thumbhash != null) {
|
||||||
|
json[r'thumbhash'] = this.thumbhash;
|
||||||
|
} else {
|
||||||
|
// json[r'thumbhash'] = null;
|
||||||
|
}
|
||||||
|
json[r'type'] = this.type;
|
||||||
|
json[r'visibility'] = this.visibility;
|
||||||
|
if (this.width != null) {
|
||||||
|
json[r'width'] = this.width;
|
||||||
|
} else {
|
||||||
|
// json[r'width'] = null;
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [SyncAssetV2] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static SyncAssetV2? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "SyncAssetV2");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return SyncAssetV2(
|
||||||
|
checksum: mapValueOfType<String>(json, r'checksum')!,
|
||||||
|
deletedAt: mapDateTime(json, r'deletedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
|
||||||
|
duration: mapValueOfType<int>(json, r'duration'),
|
||||||
|
fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
|
||||||
|
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
|
||||||
|
height: mapValueOfType<int>(json, r'height'),
|
||||||
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
|
isEdited: mapValueOfType<bool>(json, r'isEdited')!,
|
||||||
|
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
|
||||||
|
libraryId: mapValueOfType<String>(json, r'libraryId'),
|
||||||
|
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
|
||||||
|
localDateTime: mapDateTime(json, r'localDateTime', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
|
||||||
|
originalFileName: mapValueOfType<String>(json, r'originalFileName')!,
|
||||||
|
ownerId: mapValueOfType<String>(json, r'ownerId')!,
|
||||||
|
stackId: mapValueOfType<String>(json, r'stackId'),
|
||||||
|
thumbhash: mapValueOfType<String>(json, r'thumbhash'),
|
||||||
|
type: AssetTypeEnum.fromJson(json[r'type'])!,
|
||||||
|
visibility: AssetVisibility.fromJson(json[r'visibility'])!,
|
||||||
|
width: mapValueOfType<int>(json, r'width'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<SyncAssetV2> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <SyncAssetV2>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = SyncAssetV2.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, SyncAssetV2> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, SyncAssetV2>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = SyncAssetV2.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of SyncAssetV2-objects as value to a dart map
|
||||||
|
static Map<String, List<SyncAssetV2>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<SyncAssetV2>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = SyncAssetV2.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'checksum',
|
||||||
|
'deletedAt',
|
||||||
|
'duration',
|
||||||
|
'fileCreatedAt',
|
||||||
|
'fileModifiedAt',
|
||||||
|
'height',
|
||||||
|
'id',
|
||||||
|
'isEdited',
|
||||||
|
'isFavorite',
|
||||||
|
'libraryId',
|
||||||
|
'livePhotoVideoId',
|
||||||
|
'localDateTime',
|
||||||
|
'originalFileName',
|
||||||
|
'ownerId',
|
||||||
|
'stackId',
|
||||||
|
'thumbhash',
|
||||||
|
'type',
|
||||||
|
'visibility',
|
||||||
|
'width',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
+18
@@ -27,6 +27,7 @@ class SyncEntityType {
|
|||||||
static const userV1 = SyncEntityType._(r'UserV1');
|
static const userV1 = SyncEntityType._(r'UserV1');
|
||||||
static const userDeleteV1 = SyncEntityType._(r'UserDeleteV1');
|
static const userDeleteV1 = SyncEntityType._(r'UserDeleteV1');
|
||||||
static const assetV1 = SyncEntityType._(r'AssetV1');
|
static const assetV1 = SyncEntityType._(r'AssetV1');
|
||||||
|
static const assetV2 = SyncEntityType._(r'AssetV2');
|
||||||
static const assetDeleteV1 = SyncEntityType._(r'AssetDeleteV1');
|
static const assetDeleteV1 = SyncEntityType._(r'AssetDeleteV1');
|
||||||
static const assetExifV1 = SyncEntityType._(r'AssetExifV1');
|
static const assetExifV1 = SyncEntityType._(r'AssetExifV1');
|
||||||
static const assetEditV1 = SyncEntityType._(r'AssetEditV1');
|
static const assetEditV1 = SyncEntityType._(r'AssetEditV1');
|
||||||
@@ -36,7 +37,9 @@ class SyncEntityType {
|
|||||||
static const partnerV1 = SyncEntityType._(r'PartnerV1');
|
static const partnerV1 = SyncEntityType._(r'PartnerV1');
|
||||||
static const partnerDeleteV1 = SyncEntityType._(r'PartnerDeleteV1');
|
static const partnerDeleteV1 = SyncEntityType._(r'PartnerDeleteV1');
|
||||||
static const partnerAssetV1 = SyncEntityType._(r'PartnerAssetV1');
|
static const partnerAssetV1 = SyncEntityType._(r'PartnerAssetV1');
|
||||||
|
static const partnerAssetV2 = SyncEntityType._(r'PartnerAssetV2');
|
||||||
static const partnerAssetBackfillV1 = SyncEntityType._(r'PartnerAssetBackfillV1');
|
static const partnerAssetBackfillV1 = SyncEntityType._(r'PartnerAssetBackfillV1');
|
||||||
|
static const partnerAssetBackfillV2 = SyncEntityType._(r'PartnerAssetBackfillV2');
|
||||||
static const partnerAssetDeleteV1 = SyncEntityType._(r'PartnerAssetDeleteV1');
|
static const partnerAssetDeleteV1 = SyncEntityType._(r'PartnerAssetDeleteV1');
|
||||||
static const partnerAssetExifV1 = SyncEntityType._(r'PartnerAssetExifV1');
|
static const partnerAssetExifV1 = SyncEntityType._(r'PartnerAssetExifV1');
|
||||||
static const partnerAssetExifBackfillV1 = SyncEntityType._(r'PartnerAssetExifBackfillV1');
|
static const partnerAssetExifBackfillV1 = SyncEntityType._(r'PartnerAssetExifBackfillV1');
|
||||||
@@ -50,8 +53,11 @@ class SyncEntityType {
|
|||||||
static const albumUserBackfillV1 = SyncEntityType._(r'AlbumUserBackfillV1');
|
static const albumUserBackfillV1 = SyncEntityType._(r'AlbumUserBackfillV1');
|
||||||
static const albumUserDeleteV1 = SyncEntityType._(r'AlbumUserDeleteV1');
|
static const albumUserDeleteV1 = SyncEntityType._(r'AlbumUserDeleteV1');
|
||||||
static const albumAssetCreateV1 = SyncEntityType._(r'AlbumAssetCreateV1');
|
static const albumAssetCreateV1 = SyncEntityType._(r'AlbumAssetCreateV1');
|
||||||
|
static const albumAssetCreateV2 = SyncEntityType._(r'AlbumAssetCreateV2');
|
||||||
static const albumAssetUpdateV1 = SyncEntityType._(r'AlbumAssetUpdateV1');
|
static const albumAssetUpdateV1 = SyncEntityType._(r'AlbumAssetUpdateV1');
|
||||||
|
static const albumAssetUpdateV2 = SyncEntityType._(r'AlbumAssetUpdateV2');
|
||||||
static const albumAssetBackfillV1 = SyncEntityType._(r'AlbumAssetBackfillV1');
|
static const albumAssetBackfillV1 = SyncEntityType._(r'AlbumAssetBackfillV1');
|
||||||
|
static const albumAssetBackfillV2 = SyncEntityType._(r'AlbumAssetBackfillV2');
|
||||||
static const albumAssetExifCreateV1 = SyncEntityType._(r'AlbumAssetExifCreateV1');
|
static const albumAssetExifCreateV1 = SyncEntityType._(r'AlbumAssetExifCreateV1');
|
||||||
static const albumAssetExifUpdateV1 = SyncEntityType._(r'AlbumAssetExifUpdateV1');
|
static const albumAssetExifUpdateV1 = SyncEntityType._(r'AlbumAssetExifUpdateV1');
|
||||||
static const albumAssetExifBackfillV1 = SyncEntityType._(r'AlbumAssetExifBackfillV1');
|
static const albumAssetExifBackfillV1 = SyncEntityType._(r'AlbumAssetExifBackfillV1');
|
||||||
@@ -81,6 +87,7 @@ class SyncEntityType {
|
|||||||
userV1,
|
userV1,
|
||||||
userDeleteV1,
|
userDeleteV1,
|
||||||
assetV1,
|
assetV1,
|
||||||
|
assetV2,
|
||||||
assetDeleteV1,
|
assetDeleteV1,
|
||||||
assetExifV1,
|
assetExifV1,
|
||||||
assetEditV1,
|
assetEditV1,
|
||||||
@@ -90,7 +97,9 @@ class SyncEntityType {
|
|||||||
partnerV1,
|
partnerV1,
|
||||||
partnerDeleteV1,
|
partnerDeleteV1,
|
||||||
partnerAssetV1,
|
partnerAssetV1,
|
||||||
|
partnerAssetV2,
|
||||||
partnerAssetBackfillV1,
|
partnerAssetBackfillV1,
|
||||||
|
partnerAssetBackfillV2,
|
||||||
partnerAssetDeleteV1,
|
partnerAssetDeleteV1,
|
||||||
partnerAssetExifV1,
|
partnerAssetExifV1,
|
||||||
partnerAssetExifBackfillV1,
|
partnerAssetExifBackfillV1,
|
||||||
@@ -104,8 +113,11 @@ class SyncEntityType {
|
|||||||
albumUserBackfillV1,
|
albumUserBackfillV1,
|
||||||
albumUserDeleteV1,
|
albumUserDeleteV1,
|
||||||
albumAssetCreateV1,
|
albumAssetCreateV1,
|
||||||
|
albumAssetCreateV2,
|
||||||
albumAssetUpdateV1,
|
albumAssetUpdateV1,
|
||||||
|
albumAssetUpdateV2,
|
||||||
albumAssetBackfillV1,
|
albumAssetBackfillV1,
|
||||||
|
albumAssetBackfillV2,
|
||||||
albumAssetExifCreateV1,
|
albumAssetExifCreateV1,
|
||||||
albumAssetExifUpdateV1,
|
albumAssetExifUpdateV1,
|
||||||
albumAssetExifBackfillV1,
|
albumAssetExifBackfillV1,
|
||||||
@@ -170,6 +182,7 @@ class SyncEntityTypeTypeTransformer {
|
|||||||
case r'UserV1': return SyncEntityType.userV1;
|
case r'UserV1': return SyncEntityType.userV1;
|
||||||
case r'UserDeleteV1': return SyncEntityType.userDeleteV1;
|
case r'UserDeleteV1': return SyncEntityType.userDeleteV1;
|
||||||
case r'AssetV1': return SyncEntityType.assetV1;
|
case r'AssetV1': return SyncEntityType.assetV1;
|
||||||
|
case r'AssetV2': return SyncEntityType.assetV2;
|
||||||
case r'AssetDeleteV1': return SyncEntityType.assetDeleteV1;
|
case r'AssetDeleteV1': return SyncEntityType.assetDeleteV1;
|
||||||
case r'AssetExifV1': return SyncEntityType.assetExifV1;
|
case r'AssetExifV1': return SyncEntityType.assetExifV1;
|
||||||
case r'AssetEditV1': return SyncEntityType.assetEditV1;
|
case r'AssetEditV1': return SyncEntityType.assetEditV1;
|
||||||
@@ -179,7 +192,9 @@ class SyncEntityTypeTypeTransformer {
|
|||||||
case r'PartnerV1': return SyncEntityType.partnerV1;
|
case r'PartnerV1': return SyncEntityType.partnerV1;
|
||||||
case r'PartnerDeleteV1': return SyncEntityType.partnerDeleteV1;
|
case r'PartnerDeleteV1': return SyncEntityType.partnerDeleteV1;
|
||||||
case r'PartnerAssetV1': return SyncEntityType.partnerAssetV1;
|
case r'PartnerAssetV1': return SyncEntityType.partnerAssetV1;
|
||||||
|
case r'PartnerAssetV2': return SyncEntityType.partnerAssetV2;
|
||||||
case r'PartnerAssetBackfillV1': return SyncEntityType.partnerAssetBackfillV1;
|
case r'PartnerAssetBackfillV1': return SyncEntityType.partnerAssetBackfillV1;
|
||||||
|
case r'PartnerAssetBackfillV2': return SyncEntityType.partnerAssetBackfillV2;
|
||||||
case r'PartnerAssetDeleteV1': return SyncEntityType.partnerAssetDeleteV1;
|
case r'PartnerAssetDeleteV1': return SyncEntityType.partnerAssetDeleteV1;
|
||||||
case r'PartnerAssetExifV1': return SyncEntityType.partnerAssetExifV1;
|
case r'PartnerAssetExifV1': return SyncEntityType.partnerAssetExifV1;
|
||||||
case r'PartnerAssetExifBackfillV1': return SyncEntityType.partnerAssetExifBackfillV1;
|
case r'PartnerAssetExifBackfillV1': return SyncEntityType.partnerAssetExifBackfillV1;
|
||||||
@@ -193,8 +208,11 @@ class SyncEntityTypeTypeTransformer {
|
|||||||
case r'AlbumUserBackfillV1': return SyncEntityType.albumUserBackfillV1;
|
case r'AlbumUserBackfillV1': return SyncEntityType.albumUserBackfillV1;
|
||||||
case r'AlbumUserDeleteV1': return SyncEntityType.albumUserDeleteV1;
|
case r'AlbumUserDeleteV1': return SyncEntityType.albumUserDeleteV1;
|
||||||
case r'AlbumAssetCreateV1': return SyncEntityType.albumAssetCreateV1;
|
case r'AlbumAssetCreateV1': return SyncEntityType.albumAssetCreateV1;
|
||||||
|
case r'AlbumAssetCreateV2': return SyncEntityType.albumAssetCreateV2;
|
||||||
case r'AlbumAssetUpdateV1': return SyncEntityType.albumAssetUpdateV1;
|
case r'AlbumAssetUpdateV1': return SyncEntityType.albumAssetUpdateV1;
|
||||||
|
case r'AlbumAssetUpdateV2': return SyncEntityType.albumAssetUpdateV2;
|
||||||
case r'AlbumAssetBackfillV1': return SyncEntityType.albumAssetBackfillV1;
|
case r'AlbumAssetBackfillV1': return SyncEntityType.albumAssetBackfillV1;
|
||||||
|
case r'AlbumAssetBackfillV2': return SyncEntityType.albumAssetBackfillV2;
|
||||||
case r'AlbumAssetExifCreateV1': return SyncEntityType.albumAssetExifCreateV1;
|
case r'AlbumAssetExifCreateV1': return SyncEntityType.albumAssetExifCreateV1;
|
||||||
case r'AlbumAssetExifUpdateV1': return SyncEntityType.albumAssetExifUpdateV1;
|
case r'AlbumAssetExifUpdateV1': return SyncEntityType.albumAssetExifUpdateV1;
|
||||||
case r'AlbumAssetExifBackfillV1': return SyncEntityType.albumAssetExifBackfillV1;
|
case r'AlbumAssetExifBackfillV1': return SyncEntityType.albumAssetExifBackfillV1;
|
||||||
|
|||||||
+9
@@ -28,8 +28,10 @@ class SyncRequestType {
|
|||||||
static const albumUsersV1 = SyncRequestType._(r'AlbumUsersV1');
|
static const albumUsersV1 = SyncRequestType._(r'AlbumUsersV1');
|
||||||
static const albumToAssetsV1 = SyncRequestType._(r'AlbumToAssetsV1');
|
static const albumToAssetsV1 = SyncRequestType._(r'AlbumToAssetsV1');
|
||||||
static const albumAssetsV1 = SyncRequestType._(r'AlbumAssetsV1');
|
static const albumAssetsV1 = SyncRequestType._(r'AlbumAssetsV1');
|
||||||
|
static const albumAssetsV2 = SyncRequestType._(r'AlbumAssetsV2');
|
||||||
static const albumAssetExifsV1 = SyncRequestType._(r'AlbumAssetExifsV1');
|
static const albumAssetExifsV1 = SyncRequestType._(r'AlbumAssetExifsV1');
|
||||||
static const assetsV1 = SyncRequestType._(r'AssetsV1');
|
static const assetsV1 = SyncRequestType._(r'AssetsV1');
|
||||||
|
static const assetsV2 = SyncRequestType._(r'AssetsV2');
|
||||||
static const assetExifsV1 = SyncRequestType._(r'AssetExifsV1');
|
static const assetExifsV1 = SyncRequestType._(r'AssetExifsV1');
|
||||||
static const assetEditsV1 = SyncRequestType._(r'AssetEditsV1');
|
static const assetEditsV1 = SyncRequestType._(r'AssetEditsV1');
|
||||||
static const assetMetadataV1 = SyncRequestType._(r'AssetMetadataV1');
|
static const assetMetadataV1 = SyncRequestType._(r'AssetMetadataV1');
|
||||||
@@ -38,6 +40,7 @@ class SyncRequestType {
|
|||||||
static const memoryToAssetsV1 = SyncRequestType._(r'MemoryToAssetsV1');
|
static const memoryToAssetsV1 = SyncRequestType._(r'MemoryToAssetsV1');
|
||||||
static const partnersV1 = SyncRequestType._(r'PartnersV1');
|
static const partnersV1 = SyncRequestType._(r'PartnersV1');
|
||||||
static const partnerAssetsV1 = SyncRequestType._(r'PartnerAssetsV1');
|
static const partnerAssetsV1 = SyncRequestType._(r'PartnerAssetsV1');
|
||||||
|
static const partnerAssetsV2 = SyncRequestType._(r'PartnerAssetsV2');
|
||||||
static const partnerAssetExifsV1 = SyncRequestType._(r'PartnerAssetExifsV1');
|
static const partnerAssetExifsV1 = SyncRequestType._(r'PartnerAssetExifsV1');
|
||||||
static const partnerStacksV1 = SyncRequestType._(r'PartnerStacksV1');
|
static const partnerStacksV1 = SyncRequestType._(r'PartnerStacksV1');
|
||||||
static const stacksV1 = SyncRequestType._(r'StacksV1');
|
static const stacksV1 = SyncRequestType._(r'StacksV1');
|
||||||
@@ -54,8 +57,10 @@ class SyncRequestType {
|
|||||||
albumUsersV1,
|
albumUsersV1,
|
||||||
albumToAssetsV1,
|
albumToAssetsV1,
|
||||||
albumAssetsV1,
|
albumAssetsV1,
|
||||||
|
albumAssetsV2,
|
||||||
albumAssetExifsV1,
|
albumAssetExifsV1,
|
||||||
assetsV1,
|
assetsV1,
|
||||||
|
assetsV2,
|
||||||
assetExifsV1,
|
assetExifsV1,
|
||||||
assetEditsV1,
|
assetEditsV1,
|
||||||
assetMetadataV1,
|
assetMetadataV1,
|
||||||
@@ -64,6 +69,7 @@ class SyncRequestType {
|
|||||||
memoryToAssetsV1,
|
memoryToAssetsV1,
|
||||||
partnersV1,
|
partnersV1,
|
||||||
partnerAssetsV1,
|
partnerAssetsV1,
|
||||||
|
partnerAssetsV2,
|
||||||
partnerAssetExifsV1,
|
partnerAssetExifsV1,
|
||||||
partnerStacksV1,
|
partnerStacksV1,
|
||||||
stacksV1,
|
stacksV1,
|
||||||
@@ -115,8 +121,10 @@ class SyncRequestTypeTypeTransformer {
|
|||||||
case r'AlbumUsersV1': return SyncRequestType.albumUsersV1;
|
case r'AlbumUsersV1': return SyncRequestType.albumUsersV1;
|
||||||
case r'AlbumToAssetsV1': return SyncRequestType.albumToAssetsV1;
|
case r'AlbumToAssetsV1': return SyncRequestType.albumToAssetsV1;
|
||||||
case r'AlbumAssetsV1': return SyncRequestType.albumAssetsV1;
|
case r'AlbumAssetsV1': return SyncRequestType.albumAssetsV1;
|
||||||
|
case r'AlbumAssetsV2': return SyncRequestType.albumAssetsV2;
|
||||||
case r'AlbumAssetExifsV1': return SyncRequestType.albumAssetExifsV1;
|
case r'AlbumAssetExifsV1': return SyncRequestType.albumAssetExifsV1;
|
||||||
case r'AssetsV1': return SyncRequestType.assetsV1;
|
case r'AssetsV1': return SyncRequestType.assetsV1;
|
||||||
|
case r'AssetsV2': return SyncRequestType.assetsV2;
|
||||||
case r'AssetExifsV1': return SyncRequestType.assetExifsV1;
|
case r'AssetExifsV1': return SyncRequestType.assetExifsV1;
|
||||||
case r'AssetEditsV1': return SyncRequestType.assetEditsV1;
|
case r'AssetEditsV1': return SyncRequestType.assetEditsV1;
|
||||||
case r'AssetMetadataV1': return SyncRequestType.assetMetadataV1;
|
case r'AssetMetadataV1': return SyncRequestType.assetMetadataV1;
|
||||||
@@ -125,6 +133,7 @@ class SyncRequestTypeTypeTransformer {
|
|||||||
case r'MemoryToAssetsV1': return SyncRequestType.memoryToAssetsV1;
|
case r'MemoryToAssetsV1': return SyncRequestType.memoryToAssetsV1;
|
||||||
case r'PartnersV1': return SyncRequestType.partnersV1;
|
case r'PartnersV1': return SyncRequestType.partnersV1;
|
||||||
case r'PartnerAssetsV1': return SyncRequestType.partnerAssetsV1;
|
case r'PartnerAssetsV1': return SyncRequestType.partnerAssetsV1;
|
||||||
|
case r'PartnerAssetsV2': return SyncRequestType.partnerAssetsV2;
|
||||||
case r'PartnerAssetExifsV1': return SyncRequestType.partnerAssetExifsV1;
|
case r'PartnerAssetExifsV1': return SyncRequestType.partnerAssetExifsV1;
|
||||||
case r'PartnerStacksV1': return SyncRequestType.partnerStacksV1;
|
case r'PartnerStacksV1': return SyncRequestType.partnerStacksV1;
|
||||||
case r'StacksV1': return SyncRequestType.stacksV1;
|
case r'StacksV1': return SyncRequestType.stacksV1;
|
||||||
|
|||||||
+3
-4
@@ -57,7 +57,8 @@ class SystemConfigOAuthDto {
|
|||||||
/// Default storage quota
|
/// Default storage quota
|
||||||
///
|
///
|
||||||
/// Minimum value: 0
|
/// Minimum value: 0
|
||||||
num? defaultStorageQuota;
|
/// Maximum value: 9007199254740991
|
||||||
|
int? defaultStorageQuota;
|
||||||
|
|
||||||
/// Enabled
|
/// Enabled
|
||||||
bool enabled;
|
bool enabled;
|
||||||
@@ -200,9 +201,7 @@ class SystemConfigOAuthDto {
|
|||||||
buttonText: mapValueOfType<String>(json, r'buttonText')!,
|
buttonText: mapValueOfType<String>(json, r'buttonText')!,
|
||||||
clientId: mapValueOfType<String>(json, r'clientId')!,
|
clientId: mapValueOfType<String>(json, r'clientId')!,
|
||||||
clientSecret: mapValueOfType<String>(json, r'clientSecret')!,
|
clientSecret: mapValueOfType<String>(json, r'clientSecret')!,
|
||||||
defaultStorageQuota: json[r'defaultStorageQuota'] == null
|
defaultStorageQuota: mapValueOfType<int>(json, r'defaultStorageQuota'),
|
||||||
? null
|
|
||||||
: num.parse('${json[r'defaultStorageQuota']}'),
|
|
||||||
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||||
endSessionEndpoint: mapValueOfType<String>(json, r'endSessionEndpoint')!,
|
endSessionEndpoint: mapValueOfType<String>(json, r'endSessionEndpoint')!,
|
||||||
issuerUrl: mapValueOfType<String>(json, r'issuerUrl')!,
|
issuerUrl: mapValueOfType<String>(json, r'issuerUrl')!,
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class SystemConfigSmtpTransportDto {
|
|||||||
///
|
///
|
||||||
/// Minimum value: 0
|
/// Minimum value: 0
|
||||||
/// Maximum value: 65535
|
/// Maximum value: 65535
|
||||||
num port;
|
int port;
|
||||||
|
|
||||||
/// Whether to use secure connection (TLS/SSL)
|
/// Whether to use secure connection (TLS/SSL)
|
||||||
bool secure;
|
bool secure;
|
||||||
@@ -87,7 +87,7 @@ class SystemConfigSmtpTransportDto {
|
|||||||
host: mapValueOfType<String>(json, r'host')!,
|
host: mapValueOfType<String>(json, r'host')!,
|
||||||
ignoreCert: mapValueOfType<bool>(json, r'ignoreCert')!,
|
ignoreCert: mapValueOfType<bool>(json, r'ignoreCert')!,
|
||||||
password: mapValueOfType<String>(json, r'password')!,
|
password: mapValueOfType<String>(json, r'password')!,
|
||||||
port: num.parse('${json[r'port']}'),
|
port: mapValueOfType<int>(json, r'port')!,
|
||||||
secure: mapValueOfType<bool>(json, r'secure')!,
|
secure: mapValueOfType<bool>(json, r'secure')!,
|
||||||
username: mapValueOfType<String>(json, r'username')!,
|
username: mapValueOfType<String>(json, r'username')!,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ class TimeBucketAssetResponseDto {
|
|||||||
/// Array of country names extracted from EXIF GPS data
|
/// Array of country names extracted from EXIF GPS data
|
||||||
List<String?> country;
|
List<String?> country;
|
||||||
|
|
||||||
/// Array of video/gif durations in hh:mm:ss.SSS format (null for static images)
|
/// Array of video/gif durations in milliseconds (null for static images)
|
||||||
List<String?> duration;
|
List<int?> duration;
|
||||||
|
|
||||||
/// Array of file creation timestamps in UTC
|
/// Array of file creation timestamps in UTC
|
||||||
List<String> fileCreatedAt;
|
List<String> fileCreatedAt;
|
||||||
@@ -172,7 +172,7 @@ class TimeBucketAssetResponseDto {
|
|||||||
? (json[r'country'] as Iterable).cast<String>().toList(growable: false)
|
? (json[r'country'] as Iterable).cast<String>().toList(growable: false)
|
||||||
: const [],
|
: const [],
|
||||||
duration: json[r'duration'] is Iterable
|
duration: json[r'duration'] is Iterable
|
||||||
? (json[r'duration'] as Iterable).cast<String>().toList(growable: false)
|
? (json[r'duration'] as Iterable).cast<int>().toList(growable: false)
|
||||||
: const [],
|
: const [],
|
||||||
fileCreatedAt: json[r'fileCreatedAt'] is Iterable
|
fileCreatedAt: json[r'fileCreatedAt'] is Iterable
|
||||||
? (json[r'fileCreatedAt'] as Iterable).cast<String>().toList(growable: false)
|
? (json[r'fileCreatedAt'] as Iterable).cast<String>().toList(growable: false)
|
||||||
|
|||||||
+6
-33
@@ -13,53 +13,26 @@ part of openapi.api;
|
|||||||
class UpdateAlbumUserDto {
|
class UpdateAlbumUserDto {
|
||||||
/// Returns a new [UpdateAlbumUserDto] instance.
|
/// Returns a new [UpdateAlbumUserDto] instance.
|
||||||
UpdateAlbumUserDto({
|
UpdateAlbumUserDto({
|
||||||
this.isFavorite,
|
required this.role,
|
||||||
this.role,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Mark album as favorite for the user (only the user themselves can update)
|
AlbumUserRole role;
|
||||||
///
|
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
|
||||||
/// does not include a default value (using the "default:" property), however, the generated
|
|
||||||
/// source code must fall back to having a nullable type.
|
|
||||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
|
||||||
///
|
|
||||||
bool? isFavorite;
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
|
||||||
/// does not include a default value (using the "default:" property), however, the generated
|
|
||||||
/// source code must fall back to having a nullable type.
|
|
||||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
|
||||||
///
|
|
||||||
AlbumUserRole? role;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is UpdateAlbumUserDto &&
|
bool operator ==(Object other) => identical(this, other) || other is UpdateAlbumUserDto &&
|
||||||
other.isFavorite == isFavorite &&
|
|
||||||
other.role == role;
|
other.role == role;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(isFavorite == null ? 0 : isFavorite!.hashCode) +
|
(role.hashCode);
|
||||||
(role == null ? 0 : role!.hashCode);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'UpdateAlbumUserDto[isFavorite=$isFavorite, role=$role]';
|
String toString() => 'UpdateAlbumUserDto[role=$role]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
if (this.isFavorite != null) {
|
|
||||||
json[r'isFavorite'] = this.isFavorite;
|
|
||||||
} else {
|
|
||||||
// json[r'isFavorite'] = null;
|
|
||||||
}
|
|
||||||
if (this.role != null) {
|
|
||||||
json[r'role'] = this.role;
|
json[r'role'] = this.role;
|
||||||
} else {
|
|
||||||
// json[r'role'] = null;
|
|
||||||
}
|
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,8 +45,7 @@ class UpdateAlbumUserDto {
|
|||||||
final json = value.cast<String, dynamic>();
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
return UpdateAlbumUserDto(
|
return UpdateAlbumUserDto(
|
||||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
role: AlbumUserRole.fromJson(json[r'role'])!,
|
||||||
role: AlbumUserRole.fromJson(json[r'role']),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -121,6 +93,7 @@ class UpdateAlbumUserDto {
|
|||||||
|
|
||||||
/// The list of required keys that must be present in a JSON.
|
/// The list of required keys that must be present in a JSON.
|
||||||
static const requiredKeys = <String>{
|
static const requiredKeys = <String>{
|
||||||
|
'role',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -26,7 +26,10 @@ class WorkflowActionResponseDto {
|
|||||||
String id;
|
String id;
|
||||||
|
|
||||||
/// Action order
|
/// Action order
|
||||||
num order;
|
///
|
||||||
|
/// Minimum value: -9007199254740991
|
||||||
|
/// Maximum value: 9007199254740991
|
||||||
|
int order;
|
||||||
|
|
||||||
/// Plugin action ID
|
/// Plugin action ID
|
||||||
String pluginActionId;
|
String pluginActionId;
|
||||||
@@ -79,7 +82,7 @@ class WorkflowActionResponseDto {
|
|||||||
return WorkflowActionResponseDto(
|
return WorkflowActionResponseDto(
|
||||||
actionConfig: mapCastOfType<String, Object>(json, r'actionConfig'),
|
actionConfig: mapCastOfType<String, Object>(json, r'actionConfig'),
|
||||||
id: mapValueOfType<String>(json, r'id')!,
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
order: num.parse('${json[r'order']}'),
|
order: mapValueOfType<int>(json, r'order')!,
|
||||||
pluginActionId: mapValueOfType<String>(json, r'pluginActionId')!,
|
pluginActionId: mapValueOfType<String>(json, r'pluginActionId')!,
|
||||||
workflowId: mapValueOfType<String>(json, r'workflowId')!,
|
workflowId: mapValueOfType<String>(json, r'workflowId')!,
|
||||||
);
|
);
|
||||||
|
|||||||
+5
-2
@@ -26,7 +26,10 @@ class WorkflowFilterResponseDto {
|
|||||||
String id;
|
String id;
|
||||||
|
|
||||||
/// Filter order
|
/// Filter order
|
||||||
num order;
|
///
|
||||||
|
/// Minimum value: -9007199254740991
|
||||||
|
/// Maximum value: 9007199254740991
|
||||||
|
int order;
|
||||||
|
|
||||||
/// Plugin filter ID
|
/// Plugin filter ID
|
||||||
String pluginFilterId;
|
String pluginFilterId;
|
||||||
@@ -79,7 +82,7 @@ class WorkflowFilterResponseDto {
|
|||||||
return WorkflowFilterResponseDto(
|
return WorkflowFilterResponseDto(
|
||||||
filterConfig: mapCastOfType<String, Object>(json, r'filterConfig'),
|
filterConfig: mapCastOfType<String, Object>(json, r'filterConfig'),
|
||||||
id: mapValueOfType<String>(json, r'id')!,
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
order: num.parse('${json[r'order']}'),
|
order: mapValueOfType<int>(json, r'order')!,
|
||||||
pluginFilterId: mapValueOfType<String>(json, r'pluginFilterId')!,
|
pluginFilterId: mapValueOfType<String>(json, r'pluginFilterId')!,
|
||||||
workflowId: mapValueOfType<String>(json, r'workflowId')!,
|
workflowId: mapValueOfType<String>(json, r'workflowId')!,
|
||||||
);
|
);
|
||||||
|
|||||||
+1
-1
@@ -1989,4 +1989,4 @@ packages:
|
|||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.11.0 <4.0.0"
|
dart: ">=3.11.0 <4.0.0"
|
||||||
flutter: "3.41.7"
|
flutter: "3.41.9"
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ version: 2.7.5+3046
|
|||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.11.0 <4.0.0'
|
sdk: '>=3.11.0 <4.0.0'
|
||||||
flutter: 3.41.7
|
flutter: 3.41.9
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
async: ^2.13.1
|
async: ^2.13.1
|
||||||
|
|||||||
@@ -419,8 +419,8 @@ void main() {
|
|||||||
'album-b': [mergedAsset],
|
'album-b': [mergedAsset],
|
||||||
};
|
};
|
||||||
when(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any())).thenAnswer((invocation) async {
|
when(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any())).thenAnswer((invocation) async {
|
||||||
final Iterable<String> requestedChecksums = invocation.positionalArguments.first as Iterable<String>;
|
final Iterable<String> requestedRemoteIds = invocation.positionalArguments.first as Iterable<String>;
|
||||||
expect(requestedChecksums.toSet(), equals({'checksum-local', 'checksum-merged', 'checksum-remote-only'}));
|
expect(requestedRemoteIds.toSet(), equals({'remote-1', 'remote-2', 'remote-3'}));
|
||||||
return assetsByAlbum;
|
return assetsByAlbum;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -482,12 +482,18 @@ void main() {
|
|||||||
verifyNever(() => mockTrashedLocalAssetRepo.trashLocalAsset(any()));
|
verifyNever(() => mockTrashedLocalAssetRepo.trashLocalAsset(any()));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("does not request local deletions for permanent remote delete events", () async {
|
test("requests local deletions lookup by remote ids for permanent remote delete events", () async {
|
||||||
|
when(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any())).thenAnswer((invocation) async {
|
||||||
|
final Iterable<String> requestedRemoteIds = invocation.positionalArguments.first as Iterable<String>;
|
||||||
|
expect(requestedRemoteIds.toSet(), equals({'remote-asset'}));
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
|
||||||
final events = [SyncStreamStub.assetDeleteV1];
|
final events = [SyncStreamStub.assetDeleteV1];
|
||||||
|
|
||||||
await simulateEvents(events);
|
await simulateEvents(events);
|
||||||
|
|
||||||
verifyNever(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any()));
|
verify(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any())).called(1);
|
||||||
verifyNever(() => mockLocalFilesManagerRepo.moveToTrash(any()));
|
verifyNever(() => mockLocalFilesManagerRepo.moveToTrash(any()));
|
||||||
verify(() => mockSyncStreamRepo.deleteAssetsV1(any())).called(1);
|
verify(() => mockSyncStreamRepo.deleteAssetsV1(any())).called(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1648,15 +1648,6 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "favorite",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"description": "Filter to only albums favorited by the authenticated user",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "shared",
|
"name": "shared",
|
||||||
"required": false,
|
"required": false,
|
||||||
@@ -5181,7 +5172,7 @@
|
|||||||
},
|
},
|
||||||
"/duplicates/{id}": {
|
"/duplicates/{id}": {
|
||||||
"delete": {
|
"delete": {
|
||||||
"description": "Delete a single duplicate asset specified by its ID.",
|
"description": "Dismiss a duplicate group by its ID, unlinking all assets in the group without deleting them.",
|
||||||
"operationId": "deleteDuplicate",
|
"operationId": "deleteDuplicate",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
@@ -5211,7 +5202,7 @@
|
|||||||
"api_key": []
|
"api_key": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"summary": "Delete a duplicate",
|
"summary": "Dismiss a duplicate group",
|
||||||
"tags": [
|
"tags": [
|
||||||
"Duplicates"
|
"Duplicates"
|
||||||
],
|
],
|
||||||
@@ -7973,8 +7964,9 @@
|
|||||||
"description": "Page number for pagination",
|
"description": "Page number for pagination",
|
||||||
"schema": {
|
"schema": {
|
||||||
"minimum": 1,
|
"minimum": 1,
|
||||||
|
"maximum": 9007199254740991,
|
||||||
"default": 1,
|
"default": 1,
|
||||||
"type": "number"
|
"type": "integer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -7986,7 +7978,7 @@
|
|||||||
"minimum": 1,
|
"minimum": 1,
|
||||||
"maximum": 1000,
|
"maximum": 1000,
|
||||||
"default": 500,
|
"default": 500,
|
||||||
"type": "number"
|
"type": "integer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -9381,7 +9373,7 @@
|
|||||||
],
|
],
|
||||||
"x-immich-state": "Stable",
|
"x-immich-state": "Stable",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "number",
|
"type": "integer",
|
||||||
"minimum": -1,
|
"minimum": -1,
|
||||||
"maximum": 5,
|
"maximum": 5,
|
||||||
"nullable": true
|
"nullable": true
|
||||||
@@ -9395,7 +9387,7 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"minimum": 1,
|
"minimum": 1,
|
||||||
"maximum": 1000,
|
"maximum": 1000,
|
||||||
"type": "number"
|
"type": "integer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -15333,10 +15325,6 @@
|
|||||||
"description": "Activity feed enabled",
|
"description": "Activity feed enabled",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"isFavorite": {
|
|
||||||
"description": "Whether the authenticated user has favorited this album",
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"lastModifiedAssetTimestamp": {
|
"lastModifiedAssetTimestamp": {
|
||||||
"description": "Last modified asset timestamp",
|
"description": "Last modified asset timestamp",
|
||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
@@ -15370,7 +15358,6 @@
|
|||||||
"hasSharedLink",
|
"hasSharedLink",
|
||||||
"id",
|
"id",
|
||||||
"isActivityEnabled",
|
"isActivityEnabled",
|
||||||
"isFavorite",
|
|
||||||
"shared",
|
"shared",
|
||||||
"updatedAt"
|
"updatedAt"
|
||||||
],
|
],
|
||||||
@@ -15650,7 +15637,9 @@
|
|||||||
},
|
},
|
||||||
"dateTimeRelative": {
|
"dateTimeRelative": {
|
||||||
"description": "Relative time offset in seconds",
|
"description": "Relative time offset in seconds",
|
||||||
"type": "number"
|
"maximum": 9007199254740991,
|
||||||
|
"minimum": -9007199254740991,
|
||||||
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"description": "Asset description",
|
"description": "Asset description",
|
||||||
@@ -16272,8 +16261,10 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"duration": {
|
"duration": {
|
||||||
"description": "Duration (for videos)",
|
"description": "Duration in milliseconds (for videos)",
|
||||||
"type": "string"
|
"maximum": 2147483647,
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"fileCreatedAt": {
|
"fileCreatedAt": {
|
||||||
"description": "File creation date",
|
"description": "File creation date",
|
||||||
@@ -16641,9 +16632,11 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"duration": {
|
"duration": {
|
||||||
"description": "Video/gif duration in hh:mm:ss.SSS format (null for static images)",
|
"description": "Video/gif duration in milliseconds (null for static images)",
|
||||||
|
"maximum": 2147483647,
|
||||||
|
"minimum": 0,
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "string"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"exifInfo": {
|
"exifInfo": {
|
||||||
"$ref": "#/components/schemas/ExifResponseDto"
|
"$ref": "#/components/schemas/ExifResponseDto"
|
||||||
@@ -16664,9 +16657,10 @@
|
|||||||
},
|
},
|
||||||
"height": {
|
"height": {
|
||||||
"description": "Asset height",
|
"description": "Asset height",
|
||||||
|
"maximum": 9007199254740991,
|
||||||
"minimum": 0,
|
"minimum": 0,
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "number"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"description": "Asset ID",
|
"description": "Asset ID",
|
||||||
@@ -16809,9 +16803,10 @@
|
|||||||
},
|
},
|
||||||
"width": {
|
"width": {
|
||||||
"description": "Asset width",
|
"description": "Asset width",
|
||||||
|
"maximum": 9007199254740991,
|
||||||
"minimum": 0,
|
"minimum": 0,
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "number"
|
"type": "integer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -17228,23 +17223,27 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"height": {
|
"height": {
|
||||||
"description": "Height of the crop",
|
"description": "Height of the crop",
|
||||||
|
"maximum": 9007199254740991,
|
||||||
"minimum": 1,
|
"minimum": 1,
|
||||||
"type": "number"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"width": {
|
"width": {
|
||||||
"description": "Width of the crop",
|
"description": "Width of the crop",
|
||||||
|
"maximum": 9007199254740991,
|
||||||
"minimum": 1,
|
"minimum": 1,
|
||||||
"type": "number"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"x": {
|
"x": {
|
||||||
"description": "Top-Left X coordinate of crop",
|
"description": "Top-Left X coordinate of crop",
|
||||||
|
"maximum": 9007199254740991,
|
||||||
"minimum": 0,
|
"minimum": 0,
|
||||||
"type": "number"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"y": {
|
"y": {
|
||||||
"description": "Top-Left Y coordinate of crop",
|
"description": "Top-Left Y coordinate of crop",
|
||||||
|
"maximum": 9007199254740991,
|
||||||
"minimum": 0,
|
"minimum": 0,
|
||||||
"type": "number"
|
"type": "integer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -17268,8 +17267,9 @@
|
|||||||
},
|
},
|
||||||
"keepLastAmount": {
|
"keepLastAmount": {
|
||||||
"description": "Keep last amount",
|
"description": "Keep last amount",
|
||||||
|
"maximum": 9007199254740991,
|
||||||
"minimum": 1,
|
"minimum": 1,
|
||||||
"type": "number"
|
"type": "integer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -17302,7 +17302,9 @@
|
|||||||
},
|
},
|
||||||
"filesize": {
|
"filesize": {
|
||||||
"description": "Backup file size",
|
"description": "Backup file size",
|
||||||
"type": "number"
|
"maximum": 9007199254740991,
|
||||||
|
"minimum": -9007199254740991,
|
||||||
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"timezone": {
|
"timezone": {
|
||||||
"description": "Backup timezone",
|
"description": "Backup timezone",
|
||||||
@@ -17641,16 +17643,18 @@
|
|||||||
"exifImageHeight": {
|
"exifImageHeight": {
|
||||||
"default": null,
|
"default": null,
|
||||||
"description": "Image height in pixels",
|
"description": "Image height in pixels",
|
||||||
|
"maximum": 9007199254740991,
|
||||||
"minimum": 0,
|
"minimum": 0,
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "number"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"exifImageWidth": {
|
"exifImageWidth": {
|
||||||
"default": null,
|
"default": null,
|
||||||
"description": "Image width in pixels",
|
"description": "Image width in pixels",
|
||||||
|
"maximum": 9007199254740991,
|
||||||
"minimum": 0,
|
"minimum": 0,
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "number"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"exposureTime": {
|
"exposureTime": {
|
||||||
"default": null,
|
"default": null,
|
||||||
@@ -17681,8 +17685,10 @@
|
|||||||
"iso": {
|
"iso": {
|
||||||
"default": null,
|
"default": null,
|
||||||
"description": "ISO sensitivity",
|
"description": "ISO sensitivity",
|
||||||
|
"maximum": 9007199254740991,
|
||||||
|
"minimum": -9007199254740991,
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "number"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"latitude": {
|
"latitude": {
|
||||||
"default": null,
|
"default": null,
|
||||||
@@ -17736,8 +17742,10 @@
|
|||||||
"rating": {
|
"rating": {
|
||||||
"default": null,
|
"default": null,
|
||||||
"description": "Rating",
|
"description": "Rating",
|
||||||
|
"maximum": 9007199254740991,
|
||||||
|
"minimum": -9007199254740991,
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "number"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"state": {
|
"state": {
|
||||||
"default": null,
|
"default": null,
|
||||||
@@ -18164,10 +18172,14 @@
|
|||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"interval": {
|
"interval": {
|
||||||
"type": "number"
|
"maximum": 9007199254740991,
|
||||||
|
"minimum": -9007199254740991,
|
||||||
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"timeout": {
|
"timeout": {
|
||||||
"type": "number"
|
"maximum": 9007199254740991,
|
||||||
|
"minimum": -9007199254740991,
|
||||||
|
"type": "integer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -18217,7 +18229,9 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"files": {
|
"files": {
|
||||||
"description": "Number of files in the folder",
|
"description": "Number of files in the folder",
|
||||||
"type": "number"
|
"maximum": 9007199254740991,
|
||||||
|
"minimum": -9007199254740991,
|
||||||
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"folder": {
|
"folder": {
|
||||||
"$ref": "#/components/schemas/StorageFolder"
|
"$ref": "#/components/schemas/StorageFolder"
|
||||||
@@ -18260,7 +18274,9 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"type": "number"
|
"maximum": 9007199254740991,
|
||||||
|
"minimum": -9007199254740991,
|
||||||
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"task": {
|
"task": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -18737,8 +18753,9 @@
|
|||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
"description": "Page number",
|
"description": "Page number",
|
||||||
|
"maximum": 9007199254740991,
|
||||||
"minimum": 1,
|
"minimum": 1,
|
||||||
"type": "number"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"personIds": {
|
"personIds": {
|
||||||
"description": "Filter by person IDs",
|
"description": "Filter by person IDs",
|
||||||
@@ -18758,7 +18775,7 @@
|
|||||||
"maximum": 5,
|
"maximum": 5,
|
||||||
"minimum": -1,
|
"minimum": -1,
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "number",
|
"type": "integer",
|
||||||
"x-immich-history": [
|
"x-immich-history": [
|
||||||
{
|
{
|
||||||
"version": "v1",
|
"version": "v1",
|
||||||
@@ -18780,7 +18797,7 @@
|
|||||||
"description": "Number of results to return",
|
"description": "Number of results to return",
|
||||||
"maximum": 1000,
|
"maximum": 1000,
|
||||||
"minimum": 1,
|
"minimum": 1,
|
||||||
"type": "number"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"state": {
|
"state": {
|
||||||
"description": "Filter by state/province name",
|
"description": "Filter by state/province name",
|
||||||
@@ -20611,7 +20628,7 @@
|
|||||||
"maximum": 5,
|
"maximum": 5,
|
||||||
"minimum": -1,
|
"minimum": -1,
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "number",
|
"type": "integer",
|
||||||
"x-immich-history": [
|
"x-immich-history": [
|
||||||
{
|
{
|
||||||
"version": "v1",
|
"version": "v1",
|
||||||
@@ -20633,7 +20650,7 @@
|
|||||||
"description": "Number of results to return",
|
"description": "Number of results to return",
|
||||||
"maximum": 1000,
|
"maximum": 1000,
|
||||||
"minimum": 1,
|
"minimum": 1,
|
||||||
"type": "number"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"state": {
|
"state": {
|
||||||
"description": "Filter by state/province name",
|
"description": "Filter by state/province name",
|
||||||
@@ -21451,8 +21468,9 @@
|
|||||||
},
|
},
|
||||||
"duration": {
|
"duration": {
|
||||||
"description": "Session duration in seconds",
|
"description": "Session duration in seconds",
|
||||||
|
"maximum": 9007199254740991,
|
||||||
"minimum": 1,
|
"minimum": 1,
|
||||||
"type": "number"
|
"type": "integer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
@@ -21966,8 +21984,9 @@
|
|||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
"description": "Page number",
|
"description": "Page number",
|
||||||
|
"maximum": 9007199254740991,
|
||||||
"minimum": 1,
|
"minimum": 1,
|
||||||
"type": "number"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"personIds": {
|
"personIds": {
|
||||||
"description": "Filter by person IDs",
|
"description": "Filter by person IDs",
|
||||||
@@ -21993,7 +22012,7 @@
|
|||||||
"maximum": 5,
|
"maximum": 5,
|
||||||
"minimum": -1,
|
"minimum": -1,
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "number",
|
"type": "integer",
|
||||||
"x-immich-history": [
|
"x-immich-history": [
|
||||||
{
|
{
|
||||||
"version": "v1",
|
"version": "v1",
|
||||||
@@ -22015,7 +22034,7 @@
|
|||||||
"description": "Number of results to return",
|
"description": "Number of results to return",
|
||||||
"maximum": 1000,
|
"maximum": 1000,
|
||||||
"minimum": 1,
|
"minimum": 1,
|
||||||
"type": "number"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"state": {
|
"state": {
|
||||||
"description": "Filter by state/province name",
|
"description": "Filter by state/province name",
|
||||||
@@ -22253,7 +22272,7 @@
|
|||||||
"maximum": 5,
|
"maximum": 5,
|
||||||
"minimum": -1,
|
"minimum": -1,
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "number",
|
"type": "integer",
|
||||||
"x-immich-history": [
|
"x-immich-history": [
|
||||||
{
|
{
|
||||||
"version": "v1",
|
"version": "v1",
|
||||||
@@ -22466,10 +22485,6 @@
|
|||||||
"description": "Album ID",
|
"description": "Album ID",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"isFavorite": {
|
|
||||||
"description": "Favorite flag",
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"role": {
|
"role": {
|
||||||
"$ref": "#/components/schemas/AlbumUserRole"
|
"$ref": "#/components/schemas/AlbumUserRole"
|
||||||
},
|
},
|
||||||
@@ -22480,7 +22495,6 @@
|
|||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"albumId",
|
"albumId",
|
||||||
"isFavorite",
|
|
||||||
"role",
|
"role",
|
||||||
"userId"
|
"userId"
|
||||||
],
|
],
|
||||||
@@ -23165,6 +23179,135 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"SyncAssetV2": {
|
||||||
|
"properties": {
|
||||||
|
"checksum": {
|
||||||
|
"description": "Checksum",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"deletedAt": {
|
||||||
|
"description": "Deleted at",
|
||||||
|
"example": "2024-01-01T00:00:00.000Z",
|
||||||
|
"format": "date-time",
|
||||||
|
"nullable": true,
|
||||||
|
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"duration": {
|
||||||
|
"description": "Duration",
|
||||||
|
"maximum": 2147483647,
|
||||||
|
"minimum": 0,
|
||||||
|
"nullable": true,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"fileCreatedAt": {
|
||||||
|
"description": "File created at",
|
||||||
|
"example": "2024-01-01T00:00:00.000Z",
|
||||||
|
"format": "date-time",
|
||||||
|
"nullable": true,
|
||||||
|
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"fileModifiedAt": {
|
||||||
|
"description": "File modified at",
|
||||||
|
"example": "2024-01-01T00:00:00.000Z",
|
||||||
|
"format": "date-time",
|
||||||
|
"nullable": true,
|
||||||
|
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"height": {
|
||||||
|
"description": "Asset height",
|
||||||
|
"maximum": 9007199254740991,
|
||||||
|
"minimum": -9007199254740991,
|
||||||
|
"nullable": true,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"description": "Asset ID",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"isEdited": {
|
||||||
|
"description": "Is edited",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"isFavorite": {
|
||||||
|
"description": "Is favorite",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"libraryId": {
|
||||||
|
"description": "Library ID",
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"livePhotoVideoId": {
|
||||||
|
"description": "Live photo video ID",
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"localDateTime": {
|
||||||
|
"description": "Local date time",
|
||||||
|
"example": "2024-01-01T00:00:00.000Z",
|
||||||
|
"format": "date-time",
|
||||||
|
"nullable": true,
|
||||||
|
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"originalFileName": {
|
||||||
|
"description": "Original file name",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"ownerId": {
|
||||||
|
"description": "Owner ID",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"stackId": {
|
||||||
|
"description": "Stack ID",
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"thumbhash": {
|
||||||
|
"description": "Thumbhash",
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"$ref": "#/components/schemas/AssetTypeEnum"
|
||||||
|
},
|
||||||
|
"visibility": {
|
||||||
|
"$ref": "#/components/schemas/AssetVisibility"
|
||||||
|
},
|
||||||
|
"width": {
|
||||||
|
"description": "Asset width",
|
||||||
|
"maximum": 9007199254740991,
|
||||||
|
"minimum": -9007199254740991,
|
||||||
|
"nullable": true,
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"checksum",
|
||||||
|
"deletedAt",
|
||||||
|
"duration",
|
||||||
|
"fileCreatedAt",
|
||||||
|
"fileModifiedAt",
|
||||||
|
"height",
|
||||||
|
"id",
|
||||||
|
"isEdited",
|
||||||
|
"isFavorite",
|
||||||
|
"libraryId",
|
||||||
|
"livePhotoVideoId",
|
||||||
|
"localDateTime",
|
||||||
|
"originalFileName",
|
||||||
|
"ownerId",
|
||||||
|
"stackId",
|
||||||
|
"thumbhash",
|
||||||
|
"type",
|
||||||
|
"visibility",
|
||||||
|
"width"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"SyncAuthUserV1": {
|
"SyncAuthUserV1": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"avatarColor": {
|
"avatarColor": {
|
||||||
@@ -23265,6 +23408,7 @@
|
|||||||
"UserV1",
|
"UserV1",
|
||||||
"UserDeleteV1",
|
"UserDeleteV1",
|
||||||
"AssetV1",
|
"AssetV1",
|
||||||
|
"AssetV2",
|
||||||
"AssetDeleteV1",
|
"AssetDeleteV1",
|
||||||
"AssetExifV1",
|
"AssetExifV1",
|
||||||
"AssetEditV1",
|
"AssetEditV1",
|
||||||
@@ -23274,7 +23418,9 @@
|
|||||||
"PartnerV1",
|
"PartnerV1",
|
||||||
"PartnerDeleteV1",
|
"PartnerDeleteV1",
|
||||||
"PartnerAssetV1",
|
"PartnerAssetV1",
|
||||||
|
"PartnerAssetV2",
|
||||||
"PartnerAssetBackfillV1",
|
"PartnerAssetBackfillV1",
|
||||||
|
"PartnerAssetBackfillV2",
|
||||||
"PartnerAssetDeleteV1",
|
"PartnerAssetDeleteV1",
|
||||||
"PartnerAssetExifV1",
|
"PartnerAssetExifV1",
|
||||||
"PartnerAssetExifBackfillV1",
|
"PartnerAssetExifBackfillV1",
|
||||||
@@ -23288,8 +23434,11 @@
|
|||||||
"AlbumUserBackfillV1",
|
"AlbumUserBackfillV1",
|
||||||
"AlbumUserDeleteV1",
|
"AlbumUserDeleteV1",
|
||||||
"AlbumAssetCreateV1",
|
"AlbumAssetCreateV1",
|
||||||
|
"AlbumAssetCreateV2",
|
||||||
"AlbumAssetUpdateV1",
|
"AlbumAssetUpdateV1",
|
||||||
|
"AlbumAssetUpdateV2",
|
||||||
"AlbumAssetBackfillV1",
|
"AlbumAssetBackfillV1",
|
||||||
|
"AlbumAssetBackfillV2",
|
||||||
"AlbumAssetExifCreateV1",
|
"AlbumAssetExifCreateV1",
|
||||||
"AlbumAssetExifUpdateV1",
|
"AlbumAssetExifUpdateV1",
|
||||||
"AlbumAssetExifBackfillV1",
|
"AlbumAssetExifBackfillV1",
|
||||||
@@ -23581,8 +23730,10 @@
|
|||||||
"AlbumUsersV1",
|
"AlbumUsersV1",
|
||||||
"AlbumToAssetsV1",
|
"AlbumToAssetsV1",
|
||||||
"AlbumAssetsV1",
|
"AlbumAssetsV1",
|
||||||
|
"AlbumAssetsV2",
|
||||||
"AlbumAssetExifsV1",
|
"AlbumAssetExifsV1",
|
||||||
"AssetsV1",
|
"AssetsV1",
|
||||||
|
"AssetsV2",
|
||||||
"AssetExifsV1",
|
"AssetExifsV1",
|
||||||
"AssetEditsV1",
|
"AssetEditsV1",
|
||||||
"AssetMetadataV1",
|
"AssetMetadataV1",
|
||||||
@@ -23591,6 +23742,7 @@
|
|||||||
"MemoryToAssetsV1",
|
"MemoryToAssetsV1",
|
||||||
"PartnersV1",
|
"PartnersV1",
|
||||||
"PartnerAssetsV1",
|
"PartnerAssetsV1",
|
||||||
|
"PartnerAssetsV2",
|
||||||
"PartnerAssetExifsV1",
|
"PartnerAssetExifsV1",
|
||||||
"PartnerStacksV1",
|
"PartnerStacksV1",
|
||||||
"StacksV1",
|
"StacksV1",
|
||||||
@@ -24390,9 +24542,10 @@
|
|||||||
},
|
},
|
||||||
"defaultStorageQuota": {
|
"defaultStorageQuota": {
|
||||||
"description": "Default storage quota",
|
"description": "Default storage quota",
|
||||||
|
"maximum": 9007199254740991,
|
||||||
"minimum": 0,
|
"minimum": 0,
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "number"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"description": "Enabled",
|
"description": "Enabled",
|
||||||
@@ -24567,7 +24720,7 @@
|
|||||||
"description": "SMTP server port",
|
"description": "SMTP server port",
|
||||||
"maximum": 65535,
|
"maximum": 65535,
|
||||||
"minimum": 0,
|
"minimum": 0,
|
||||||
"type": "number"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"secure": {
|
"secure": {
|
||||||
"description": "Whether to use secure connection (TLS/SSL)",
|
"description": "Whether to use secure connection (TLS/SSL)",
|
||||||
@@ -24983,10 +25136,12 @@
|
|||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
"duration": {
|
"duration": {
|
||||||
"description": "Array of video/gif durations in hh:mm:ss.SSS format (null for static images)",
|
"description": "Array of video/gif durations in milliseconds (null for static images)",
|
||||||
"items": {
|
"items": {
|
||||||
|
"maximum": 2147483647,
|
||||||
|
"minimum": 0,
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "string"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
@@ -25221,14 +25376,13 @@
|
|||||||
},
|
},
|
||||||
"UpdateAlbumUserDto": {
|
"UpdateAlbumUserDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"isFavorite": {
|
|
||||||
"description": "Mark album as favorite for the user (only the user themselves can update)",
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"role": {
|
"role": {
|
||||||
"$ref": "#/components/schemas/AlbumUserRole"
|
"$ref": "#/components/schemas/AlbumUserRole"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"required": [
|
||||||
|
"role"
|
||||||
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"UpdateAssetDto": {
|
"UpdateAssetDto": {
|
||||||
@@ -25986,7 +26140,9 @@
|
|||||||
},
|
},
|
||||||
"order": {
|
"order": {
|
||||||
"description": "Action order",
|
"description": "Action order",
|
||||||
"type": "number"
|
"maximum": 9007199254740991,
|
||||||
|
"minimum": -9007199254740991,
|
||||||
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"pluginActionId": {
|
"pluginActionId": {
|
||||||
"description": "Plugin action ID",
|
"description": "Plugin action ID",
|
||||||
@@ -26085,7 +26241,9 @@
|
|||||||
},
|
},
|
||||||
"order": {
|
"order": {
|
||||||
"description": "Filter order",
|
"description": "Filter order",
|
||||||
"type": "number"
|
"maximum": 9007199254740991,
|
||||||
|
"minimum": -9007199254740991,
|
||||||
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"pluginFilterId": {
|
"pluginFilterId": {
|
||||||
"description": "Plugin filter ID",
|
"description": "Plugin filter ID",
|
||||||
|
|||||||
@@ -474,8 +474,6 @@ export type AlbumResponseDto = {
|
|||||||
id: string;
|
id: string;
|
||||||
/** Activity feed enabled */
|
/** Activity feed enabled */
|
||||||
isActivityEnabled: boolean;
|
isActivityEnabled: boolean;
|
||||||
/** Whether the authenticated user has favorited this album */
|
|
||||||
isFavorite: boolean;
|
|
||||||
/** Last modified asset timestamp */
|
/** Last modified asset timestamp */
|
||||||
lastModifiedAssetTimestamp?: string;
|
lastModifiedAssetTimestamp?: string;
|
||||||
order?: AssetOrder;
|
order?: AssetOrder;
|
||||||
@@ -558,9 +556,7 @@ export type MapMarkerResponseDto = {
|
|||||||
state: string | null;
|
state: string | null;
|
||||||
};
|
};
|
||||||
export type UpdateAlbumUserDto = {
|
export type UpdateAlbumUserDto = {
|
||||||
/** Mark album as favorite for the user (only the user themselves can update) */
|
role: AlbumUserRole;
|
||||||
isFavorite?: boolean;
|
|
||||||
role?: AlbumUserRole;
|
|
||||||
};
|
};
|
||||||
export type AlbumUserAddDto = {
|
export type AlbumUserAddDto = {
|
||||||
/** Album user role */
|
/** Album user role */
|
||||||
@@ -618,8 +614,8 @@ export type AssetMetadataUpsertItemDto = {
|
|||||||
export type AssetMediaCreateDto = {
|
export type AssetMediaCreateDto = {
|
||||||
/** Asset file data */
|
/** Asset file data */
|
||||||
assetData: Blob;
|
assetData: Blob;
|
||||||
/** Duration (for videos) */
|
/** Duration in milliseconds (for videos) */
|
||||||
duration?: string;
|
duration?: number;
|
||||||
/** File creation date */
|
/** File creation date */
|
||||||
fileCreatedAt: string;
|
fileCreatedAt: string;
|
||||||
/** File modification date */
|
/** File modification date */
|
||||||
@@ -858,8 +854,8 @@ export type AssetResponseDto = {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
/** Duplicate group ID */
|
/** Duplicate group ID */
|
||||||
duplicateId?: string | null;
|
duplicateId?: string | null;
|
||||||
/** Video/gif duration in hh:mm:ss.SSS format (null for static images) */
|
/** Video/gif duration in milliseconds (null for static images) */
|
||||||
duration: string | null;
|
duration: number | null;
|
||||||
exifInfo?: ExifResponseDto;
|
exifInfo?: ExifResponseDto;
|
||||||
/** The actual UTC timestamp when the file was created/captured, preserving timezone information. This is the authoritative timestamp for chronological sorting within timeline groups. Combined with timezone data, this can be used to determine the exact moment the photo was taken. */
|
/** The actual UTC timestamp when the file was created/captured, preserving timezone information. This is the authoritative timestamp for chronological sorting within timeline groups. Combined with timezone data, this can be used to determine the exact moment the photo was taken. */
|
||||||
fileCreatedAt: string;
|
fileCreatedAt: string;
|
||||||
@@ -2677,8 +2673,8 @@ export type TimeBucketAssetResponseDto = {
|
|||||||
city: (string | null)[];
|
city: (string | null)[];
|
||||||
/** Array of country names extracted from EXIF GPS data */
|
/** Array of country names extracted from EXIF GPS data */
|
||||||
country: (string | null)[];
|
country: (string | null)[];
|
||||||
/** Array of video/gif durations in hh:mm:ss.SSS format (null for static images) */
|
/** Array of video/gif durations in milliseconds (null for static images) */
|
||||||
duration: (string | null)[];
|
duration: (number | null)[];
|
||||||
/** Array of file creation timestamps in UTC */
|
/** Array of file creation timestamps in UTC */
|
||||||
fileCreatedAt: string[];
|
fileCreatedAt: string[];
|
||||||
/** Array of asset IDs in the time bucket */
|
/** Array of asset IDs in the time bucket */
|
||||||
@@ -2860,8 +2856,6 @@ export type SyncAlbumUserDeleteV1 = {
|
|||||||
export type SyncAlbumUserV1 = {
|
export type SyncAlbumUserV1 = {
|
||||||
/** Album ID */
|
/** Album ID */
|
||||||
albumId: string;
|
albumId: string;
|
||||||
/** Favorite flag */
|
|
||||||
isFavorite: boolean;
|
|
||||||
role: AlbumUserRole;
|
role: AlbumUserRole;
|
||||||
/** User ID */
|
/** User ID */
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -3081,6 +3075,44 @@ export type SyncAssetV1 = {
|
|||||||
/** Asset width */
|
/** Asset width */
|
||||||
width: number | null;
|
width: number | null;
|
||||||
};
|
};
|
||||||
|
export type SyncAssetV2 = {
|
||||||
|
/** Checksum */
|
||||||
|
checksum: string;
|
||||||
|
/** Deleted at */
|
||||||
|
deletedAt: string | null;
|
||||||
|
/** Duration */
|
||||||
|
duration: number | null;
|
||||||
|
/** File created at */
|
||||||
|
fileCreatedAt: string | null;
|
||||||
|
/** File modified at */
|
||||||
|
fileModifiedAt: string | null;
|
||||||
|
/** Asset height */
|
||||||
|
height: number | null;
|
||||||
|
/** Asset ID */
|
||||||
|
id: string;
|
||||||
|
/** Is edited */
|
||||||
|
isEdited: boolean;
|
||||||
|
/** Is favorite */
|
||||||
|
isFavorite: boolean;
|
||||||
|
/** Library ID */
|
||||||
|
libraryId: string | null;
|
||||||
|
/** Live photo video ID */
|
||||||
|
livePhotoVideoId: string | null;
|
||||||
|
/** Local date time */
|
||||||
|
localDateTime: string | null;
|
||||||
|
/** Original file name */
|
||||||
|
originalFileName: string;
|
||||||
|
/** Owner ID */
|
||||||
|
ownerId: string;
|
||||||
|
/** Stack ID */
|
||||||
|
stackId: string | null;
|
||||||
|
/** Thumbhash */
|
||||||
|
thumbhash: string | null;
|
||||||
|
"type": AssetTypeEnum;
|
||||||
|
visibility: AssetVisibility;
|
||||||
|
/** Asset width */
|
||||||
|
width: number | null;
|
||||||
|
};
|
||||||
export type SyncAuthUserV1 = {
|
export type SyncAuthUserV1 = {
|
||||||
avatarColor?: (UserAvatarColor) | null;
|
avatarColor?: (UserAvatarColor) | null;
|
||||||
/** User deleted at */
|
/** User deleted at */
|
||||||
@@ -3625,9 +3657,8 @@ export function getUserStatisticsAdmin({ id, isFavorite, isTrashed, visibility }
|
|||||||
/**
|
/**
|
||||||
* List all albums
|
* List all albums
|
||||||
*/
|
*/
|
||||||
export function getAllAlbums({ assetId, favorite, shared }: {
|
export function getAllAlbums({ assetId, shared }: {
|
||||||
assetId?: string;
|
assetId?: string;
|
||||||
favorite?: boolean;
|
|
||||||
shared?: boolean;
|
shared?: boolean;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
@@ -3635,7 +3666,6 @@ export function getAllAlbums({ assetId, favorite, shared }: {
|
|||||||
data: AlbumResponseDto[];
|
data: AlbumResponseDto[];
|
||||||
}>(`/albums${QS.query(QS.explode({
|
}>(`/albums${QS.query(QS.explode({
|
||||||
assetId,
|
assetId,
|
||||||
favorite,
|
|
||||||
shared
|
shared
|
||||||
}))}`, {
|
}))}`, {
|
||||||
...opts
|
...opts
|
||||||
@@ -4450,7 +4480,7 @@ export function resolveDuplicates({ duplicateResolveDto }: {
|
|||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Delete a duplicate
|
* Dismiss a duplicate group
|
||||||
*/
|
*/
|
||||||
export function deleteDuplicate({ id }: {
|
export function deleteDuplicate({ id }: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -7117,6 +7147,7 @@ export enum SyncEntityType {
|
|||||||
UserV1 = "UserV1",
|
UserV1 = "UserV1",
|
||||||
UserDeleteV1 = "UserDeleteV1",
|
UserDeleteV1 = "UserDeleteV1",
|
||||||
AssetV1 = "AssetV1",
|
AssetV1 = "AssetV1",
|
||||||
|
AssetV2 = "AssetV2",
|
||||||
AssetDeleteV1 = "AssetDeleteV1",
|
AssetDeleteV1 = "AssetDeleteV1",
|
||||||
AssetExifV1 = "AssetExifV1",
|
AssetExifV1 = "AssetExifV1",
|
||||||
AssetEditV1 = "AssetEditV1",
|
AssetEditV1 = "AssetEditV1",
|
||||||
@@ -7126,7 +7157,9 @@ export enum SyncEntityType {
|
|||||||
PartnerV1 = "PartnerV1",
|
PartnerV1 = "PartnerV1",
|
||||||
PartnerDeleteV1 = "PartnerDeleteV1",
|
PartnerDeleteV1 = "PartnerDeleteV1",
|
||||||
PartnerAssetV1 = "PartnerAssetV1",
|
PartnerAssetV1 = "PartnerAssetV1",
|
||||||
|
PartnerAssetV2 = "PartnerAssetV2",
|
||||||
PartnerAssetBackfillV1 = "PartnerAssetBackfillV1",
|
PartnerAssetBackfillV1 = "PartnerAssetBackfillV1",
|
||||||
|
PartnerAssetBackfillV2 = "PartnerAssetBackfillV2",
|
||||||
PartnerAssetDeleteV1 = "PartnerAssetDeleteV1",
|
PartnerAssetDeleteV1 = "PartnerAssetDeleteV1",
|
||||||
PartnerAssetExifV1 = "PartnerAssetExifV1",
|
PartnerAssetExifV1 = "PartnerAssetExifV1",
|
||||||
PartnerAssetExifBackfillV1 = "PartnerAssetExifBackfillV1",
|
PartnerAssetExifBackfillV1 = "PartnerAssetExifBackfillV1",
|
||||||
@@ -7140,8 +7173,11 @@ export enum SyncEntityType {
|
|||||||
AlbumUserBackfillV1 = "AlbumUserBackfillV1",
|
AlbumUserBackfillV1 = "AlbumUserBackfillV1",
|
||||||
AlbumUserDeleteV1 = "AlbumUserDeleteV1",
|
AlbumUserDeleteV1 = "AlbumUserDeleteV1",
|
||||||
AlbumAssetCreateV1 = "AlbumAssetCreateV1",
|
AlbumAssetCreateV1 = "AlbumAssetCreateV1",
|
||||||
|
AlbumAssetCreateV2 = "AlbumAssetCreateV2",
|
||||||
AlbumAssetUpdateV1 = "AlbumAssetUpdateV1",
|
AlbumAssetUpdateV1 = "AlbumAssetUpdateV1",
|
||||||
|
AlbumAssetUpdateV2 = "AlbumAssetUpdateV2",
|
||||||
AlbumAssetBackfillV1 = "AlbumAssetBackfillV1",
|
AlbumAssetBackfillV1 = "AlbumAssetBackfillV1",
|
||||||
|
AlbumAssetBackfillV2 = "AlbumAssetBackfillV2",
|
||||||
AlbumAssetExifCreateV1 = "AlbumAssetExifCreateV1",
|
AlbumAssetExifCreateV1 = "AlbumAssetExifCreateV1",
|
||||||
AlbumAssetExifUpdateV1 = "AlbumAssetExifUpdateV1",
|
AlbumAssetExifUpdateV1 = "AlbumAssetExifUpdateV1",
|
||||||
AlbumAssetExifBackfillV1 = "AlbumAssetExifBackfillV1",
|
AlbumAssetExifBackfillV1 = "AlbumAssetExifBackfillV1",
|
||||||
@@ -7171,8 +7207,10 @@ export enum SyncRequestType {
|
|||||||
AlbumUsersV1 = "AlbumUsersV1",
|
AlbumUsersV1 = "AlbumUsersV1",
|
||||||
AlbumToAssetsV1 = "AlbumToAssetsV1",
|
AlbumToAssetsV1 = "AlbumToAssetsV1",
|
||||||
AlbumAssetsV1 = "AlbumAssetsV1",
|
AlbumAssetsV1 = "AlbumAssetsV1",
|
||||||
|
AlbumAssetsV2 = "AlbumAssetsV2",
|
||||||
AlbumAssetExifsV1 = "AlbumAssetExifsV1",
|
AlbumAssetExifsV1 = "AlbumAssetExifsV1",
|
||||||
AssetsV1 = "AssetsV1",
|
AssetsV1 = "AssetsV1",
|
||||||
|
AssetsV2 = "AssetsV2",
|
||||||
AssetExifsV1 = "AssetExifsV1",
|
AssetExifsV1 = "AssetExifsV1",
|
||||||
AssetEditsV1 = "AssetEditsV1",
|
AssetEditsV1 = "AssetEditsV1",
|
||||||
AssetMetadataV1 = "AssetMetadataV1",
|
AssetMetadataV1 = "AssetMetadataV1",
|
||||||
@@ -7181,6 +7219,7 @@ export enum SyncRequestType {
|
|||||||
MemoryToAssetsV1 = "MemoryToAssetsV1",
|
MemoryToAssetsV1 = "MemoryToAssetsV1",
|
||||||
PartnersV1 = "PartnersV1",
|
PartnersV1 = "PartnersV1",
|
||||||
PartnerAssetsV1 = "PartnerAssetsV1",
|
PartnerAssetsV1 = "PartnerAssetsV1",
|
||||||
|
PartnerAssetsV2 = "PartnerAssetsV2",
|
||||||
PartnerAssetExifsV1 = "PartnerAssetExifsV1",
|
PartnerAssetExifsV1 = "PartnerAssetExifsV1",
|
||||||
PartnerStacksV1 = "PartnerStacksV1",
|
PartnerStacksV1 = "PartnerStacksV1",
|
||||||
StacksV1 = "StacksV1",
|
StacksV1 = "StacksV1",
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import { HttpError } from '@oazapfts/runtime';
|
import { HttpError } from '@oazapfts/runtime';
|
||||||
|
|
||||||
|
export interface ApiValidationError {
|
||||||
|
code: string;
|
||||||
|
path: (string | number)[];
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ApiExceptionResponse {
|
export interface ApiExceptionResponse {
|
||||||
message: string;
|
message: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
statusCode: number;
|
statusCode: number;
|
||||||
|
errors?: ApiValidationError[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiHttpError extends HttpError {
|
export interface ApiHttpError extends HttpError {
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
"version": "2.7.5",
|
"version": "2.7.5",
|
||||||
"description": "Monorepo for Immich",
|
"description": "Monorepo for Immich",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319",
|
"packageManager": "pnpm@10.33.1+sha512.05ba3c1d5d1c18f68df06470d74055e62d41fc110a0c660db1b2dfb2785327f04cf0f68345d4609bc52089e7fa0343c31593b2f9594e2c5d5da426230acc9820",
|
||||||
"engines": {
|
"engines": {
|
||||||
"pnpm": ">=10.0.0"
|
"pnpm": ">=10.0.0"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+1062
-1000
File diff suppressed because it is too large
Load Diff
@@ -48,6 +48,7 @@ export default typescriptEslint.config([
|
|||||||
'unicorn/import-style': 'off',
|
'unicorn/import-style': 'off',
|
||||||
'unicorn/prefer-structured-clone': 'off',
|
'unicorn/prefer-structured-clone': 'off',
|
||||||
'unicorn/no-for-loop': 'off',
|
'unicorn/no-for-loop': 'off',
|
||||||
|
'unicorn/no-array-sort': 'off',
|
||||||
'@typescript-eslint/await-thenable': 'error',
|
'@typescript-eslint/await-thenable': 'error',
|
||||||
'@typescript-eslint/no-misused-promises': 'error',
|
'@typescript-eslint/no-misused-promises': 'error',
|
||||||
'require-await': 'off',
|
'require-await': 'off',
|
||||||
|
|||||||
+4
-4
@@ -46,15 +46,15 @@
|
|||||||
"@nestjs/platform-express": "^11.0.4",
|
"@nestjs/platform-express": "^11.0.4",
|
||||||
"@nestjs/platform-socket.io": "^11.0.4",
|
"@nestjs/platform-socket.io": "^11.0.4",
|
||||||
"@nestjs/schedule": "^6.0.0",
|
"@nestjs/schedule": "^6.0.0",
|
||||||
"@nestjs/swagger": "11.2.6",
|
"@nestjs/swagger": "^11.4.2",
|
||||||
"@nestjs/websockets": "^11.0.4",
|
"@nestjs/websockets": "^11.0.4",
|
||||||
"@opentelemetry/api": "^1.9.0",
|
"@opentelemetry/api": "^1.9.0",
|
||||||
"@opentelemetry/context-async-hooks": "^2.0.0",
|
"@opentelemetry/context-async-hooks": "^2.0.0",
|
||||||
"@opentelemetry/exporter-prometheus": "^0.215.0",
|
"@opentelemetry/exporter-prometheus": "^0.215.0",
|
||||||
"@opentelemetry/instrumentation-http": "^0.215.0",
|
"@opentelemetry/instrumentation-http": "^0.215.0",
|
||||||
"@opentelemetry/instrumentation-ioredis": "^0.62.0",
|
"@opentelemetry/instrumentation-ioredis": "^0.63.0",
|
||||||
"@opentelemetry/instrumentation-nestjs-core": "^0.60.0",
|
"@opentelemetry/instrumentation-nestjs-core": "^0.61.0",
|
||||||
"@opentelemetry/instrumentation-pg": "^0.66.0",
|
"@opentelemetry/instrumentation-pg": "^0.67.0",
|
||||||
"@opentelemetry/resources": "^2.0.1",
|
"@opentelemetry/resources": "^2.0.1",
|
||||||
"@opentelemetry/sdk-metrics": "^2.0.1",
|
"@opentelemetry/sdk-metrics": "^2.0.1",
|
||||||
"@opentelemetry/sdk-node": "^0.215.0",
|
"@opentelemetry/sdk-node": "^0.215.0",
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ export const ErrorMessages = {
|
|||||||
|
|
||||||
export const POSTGRES_VERSION_RANGE = '>=14.0.0';
|
export const POSTGRES_VERSION_RANGE = '>=14.0.0';
|
||||||
export const VECTORCHORD_VERSION_RANGE = '>=0.3 <2';
|
export const VECTORCHORD_VERSION_RANGE = '>=0.3 <2';
|
||||||
export const VECTORS_VERSION_RANGE = '>=0.2 <0.4';
|
|
||||||
export const VECTOR_VERSION_RANGE = '>=0.5 <1';
|
export const VECTOR_VERSION_RANGE = '>=0.5 <1';
|
||||||
|
|
||||||
export const JOBS_ASSET_PAGINATION_SIZE = 1000;
|
export const JOBS_ASSET_PAGINATION_SIZE = 1000;
|
||||||
@@ -24,15 +23,10 @@ export const EXTENSION_NAMES: Record<DatabaseExtension, string> = {
|
|||||||
cube: 'cube',
|
cube: 'cube',
|
||||||
earthdistance: 'earthdistance',
|
earthdistance: 'earthdistance',
|
||||||
vector: 'pgvector',
|
vector: 'pgvector',
|
||||||
vectors: 'pgvecto.rs',
|
|
||||||
vchord: 'VectorChord',
|
vchord: 'VectorChord',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const VECTOR_EXTENSIONS = [
|
export const VECTOR_EXTENSIONS = [DatabaseExtension.VectorChord, DatabaseExtension.Vector] as const;
|
||||||
DatabaseExtension.VectorChord,
|
|
||||||
DatabaseExtension.Vectors,
|
|
||||||
DatabaseExtension.Vector,
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export const VECTOR_INDEX_TABLES = {
|
export const VECTOR_INDEX_TABLES = {
|
||||||
[VectorIndex.Clip]: 'smart_search',
|
[VectorIndex.Clip]: 'smart_search',
|
||||||
@@ -42,6 +36,8 @@ export const VECTOR_INDEX_TABLES = {
|
|||||||
export const VECTORCHORD_LIST_SLACK_FACTOR = 1.2;
|
export const VECTORCHORD_LIST_SLACK_FACTOR = 1.2;
|
||||||
|
|
||||||
export const SALT_ROUNDS = 10;
|
export const SALT_ROUNDS = 10;
|
||||||
|
// Syntactically valid bcrypt hash used in login() preventing timing-based user enumeration.
|
||||||
|
export const LOGIN_DUMMY_HASH = '$2b$10$abcdefghijklmnopqrstuuABCDEFGHIJKLMNOPQRSTUVWXYZabcde';
|
||||||
|
|
||||||
export const IWorker = 'IWorker';
|
export const IWorker = 'IWorker';
|
||||||
|
|
||||||
|
|||||||
@@ -28,14 +28,16 @@ describe(ActivityController.name, () => {
|
|||||||
const { status, body } = await request(ctx.getHttpServer()).get('/activities');
|
const { status, body } = await request(ctx.getHttpServer()).get('/activities');
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
factory.responses.badRequest(['[albumId] Invalid input: expected string, received undefined']),
|
factory.responses.validationError([
|
||||||
|
{ path: ['albumId'], message: 'Invalid input: expected string, received undefined' },
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject an invalid albumId', async () => {
|
it('should reject an invalid albumId', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer()).get('/activities').query({ albumId: '123' });
|
const { status, body } = await request(ctx.getHttpServer()).get('/activities').query({ albumId: '123' });
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest(['[albumId] Invalid UUID']));
|
expect(body).toEqual(factory.responses.validationError([{ path: ['albumId'], message: 'Invalid UUID' }]));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject an invalid assetId', async () => {
|
it('should reject an invalid assetId', async () => {
|
||||||
@@ -43,7 +45,7 @@ describe(ActivityController.name, () => {
|
|||||||
.get('/activities')
|
.get('/activities')
|
||||||
.query({ albumId: factory.uuid(), assetId: '123' });
|
.query({ albumId: factory.uuid(), assetId: '123' });
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest(['[assetId] Invalid UUID']));
|
expect(body).toEqual(factory.responses.validationError([{ path: ['assetId'], message: 'Invalid UUID' }]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,7 +60,7 @@ describe(ActivityController.name, () => {
|
|||||||
.post('/activities')
|
.post('/activities')
|
||||||
.send({ albumId: '123', type: 'like' });
|
.send({ albumId: '123', type: 'like' });
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest(['[albumId] Invalid UUID']));
|
expect(body).toEqual(factory.responses.validationError([{ path: ['albumId'], message: 'Invalid UUID' }]));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require a comment when type is comment', async () => {
|
it('should require a comment when type is comment', async () => {
|
||||||
@@ -66,7 +68,11 @@ describe(ActivityController.name, () => {
|
|||||||
.post('/activities')
|
.post('/activities')
|
||||||
.send({ albumId: factory.uuid(), type: 'comment', comment: null });
|
.send({ albumId: factory.uuid(), type: 'comment', comment: null });
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest(['[comment] Invalid input: expected string, received null']));
|
expect(body).toEqual(
|
||||||
|
factory.responses.validationError([
|
||||||
|
{ path: ['comment'], message: 'Invalid input: expected string, received null' },
|
||||||
|
]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -79,7 +85,7 @@ describe(ActivityController.name, () => {
|
|||||||
it('should require a valid uuid', async () => {
|
it('should require a valid uuid', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer()).delete(`/activities/123`);
|
const { status, body } = await request(ctx.getHttpServer()).delete(`/activities/123`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID']));
|
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,13 +27,17 @@ describe(AlbumController.name, () => {
|
|||||||
it('should reject an invalid shared param', async () => {
|
it('should reject an invalid shared param', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer()).get('/albums?shared=invalid');
|
const { status, body } = await request(ctx.getHttpServer()).get('/albums?shared=invalid');
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest(['[shared] Invalid option: expected one of "true"|"false"']));
|
expect(body).toEqual(
|
||||||
|
factory.responses.validationError([
|
||||||
|
{ path: ['shared'], message: 'Invalid option: expected one of "true"|"false"' },
|
||||||
|
]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject an invalid assetId param', async () => {
|
it('should reject an invalid assetId param', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer()).get('/albums?assetId=invalid');
|
const { status, body } = await request(ctx.getHttpServer()).get('/albums?assetId=invalid');
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest(['[assetId] Invalid UUID']));
|
expect(body).toEqual(factory.responses.validationError([{ path: ['assetId'], message: 'Invalid UUID' }]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ describe(ApiKeyController.name, () => {
|
|||||||
it('should require a valid uuid', async () => {
|
it('should require a valid uuid', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer()).get(`/api-keys/123`);
|
const { status, body } = await request(ctx.getHttpServer()).get(`/api-keys/123`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID']));
|
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ describe(ApiKeyController.name, () => {
|
|||||||
.put(`/api-keys/123`)
|
.put(`/api-keys/123`)
|
||||||
.send({ name: 'new name', permissions: [Permission.All] });
|
.send({ name: 'new name', permissions: [Permission.All] });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID']));
|
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow updating just the name', async () => {
|
it('should allow updating just the name', async () => {
|
||||||
@@ -84,7 +84,7 @@ describe(ApiKeyController.name, () => {
|
|||||||
it('should require a valid uuid', async () => {
|
it('should require a valid uuid', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer()).delete(`/api-keys/123`);
|
const { status, body } = await request(ctx.getHttpServer()).delete(`/api-keys/123`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID']));
|
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -80,7 +80,9 @@ describe(AssetMediaController.name, () => {
|
|||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
factory.responses.badRequest(['[metadata] Invalid input: expected JSON string, received string']),
|
factory.responses.validationError([
|
||||||
|
{ path: ['metadata'], message: 'Invalid input: expected JSON string, received string' },
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -91,8 +93,8 @@ describe(AssetMediaController.name, () => {
|
|||||||
.field({ ...makeUploadDto({ omit: 'fileCreatedAt' }) });
|
.field({ ...makeUploadDto({ omit: 'fileCreatedAt' }) });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
factory.responses.badRequest([
|
factory.responses.validationError([
|
||||||
'[fileCreatedAt] Invalid input: expected ISO 8601 datetime string, received undefined',
|
{ path: ['fileCreatedAt'], message: 'Invalid input: expected ISO 8601 datetime string, received undefined' },
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -104,8 +106,8 @@ describe(AssetMediaController.name, () => {
|
|||||||
.field(makeUploadDto({ omit: 'fileModifiedAt' }));
|
.field(makeUploadDto({ omit: 'fileModifiedAt' }));
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
factory.responses.badRequest([
|
factory.responses.validationError([
|
||||||
'[fileModifiedAt] Invalid input: expected ISO 8601 datetime string, received undefined',
|
{ path: ['fileModifiedAt'], message: 'Invalid input: expected ISO 8601 datetime string, received undefined' },
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -117,7 +119,9 @@ describe(AssetMediaController.name, () => {
|
|||||||
.field({ ...makeUploadDto(), isFavorite: 'not-a-boolean' });
|
.field({ ...makeUploadDto(), isFavorite: 'not-a-boolean' });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
factory.responses.badRequest(['[isFavorite] Invalid option: expected one of "true"|"false"']),
|
factory.responses.validationError([
|
||||||
|
{ path: ['isFavorite'], message: 'Invalid option: expected one of "true"|"false"' },
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -128,7 +132,9 @@ describe(AssetMediaController.name, () => {
|
|||||||
.field({ ...makeUploadDto(), visibility: 'not-an-option' });
|
.field({ ...makeUploadDto(), visibility: 'not-an-option' });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
factory.responses.badRequest([expect.stringContaining('[visibility] Invalid option: expected one of')]),
|
factory.responses.validationError([
|
||||||
|
{ path: ['visibility'], message: expect.stringContaining('Invalid option: expected one of') },
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ describe(AssetController.name, () => {
|
|||||||
.send({ ids: ['123'] });
|
.send({ ids: ['123'] });
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest(['[ids.0] Invalid UUID']));
|
expect(body).toEqual(factory.responses.validationError([{ path: ['ids', 0], message: 'Invalid UUID' }]));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require duplicateId to be a string', async () => {
|
it('should require duplicateId to be a string', async () => {
|
||||||
@@ -42,7 +42,9 @@ describe(AssetController.name, () => {
|
|||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
factory.responses.badRequest(['[duplicateId] Invalid input: expected string, received boolean']),
|
factory.responses.validationError([
|
||||||
|
{ path: ['duplicateId'], message: 'Invalid input: expected string, received boolean' },
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -70,7 +72,7 @@ describe(AssetController.name, () => {
|
|||||||
.send({ ids: ['123'] });
|
.send({ ids: ['123'] });
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest(['[ids.0] Invalid UUID']));
|
expect(body).toEqual(factory.responses.validationError([{ path: ['ids', 0], message: 'Invalid UUID' }]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,7 +85,7 @@ describe(AssetController.name, () => {
|
|||||||
it('should require a valid id', async () => {
|
it('should require a valid id', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer()).get(`/assets/123`);
|
const { status, body } = await request(ctx.getHttpServer()).get(`/assets/123`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID']));
|
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -97,12 +99,10 @@ describe(AssetController.name, () => {
|
|||||||
const { status, body } = await request(ctx.getHttpServer()).put('/assets/copy').send({});
|
const { status, body } = await request(ctx.getHttpServer()).put('/assets/copy').send({});
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
factory.responses.badRequest(
|
factory.responses.validationError([
|
||||||
expect.arrayContaining([
|
{ path: ['sourceId'], message: 'Invalid input: expected string, received undefined' },
|
||||||
'[sourceId] Invalid input: expected string, received undefined',
|
{ path: ['targetId'], message: 'Invalid input: expected string, received undefined' },
|
||||||
'[targetId] Invalid input: expected string, received undefined',
|
]),
|
||||||
]),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -125,7 +125,9 @@ describe(AssetController.name, () => {
|
|||||||
.put('/assets/metadata')
|
.put('/assets/metadata')
|
||||||
.send({ items: [{ assetId: '123', key: 'test', value: {} }] });
|
.send({ items: [{ assetId: '123', key: 'test', value: {} }] });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['[items.0.assetId] Invalid UUID'])));
|
expect(body).toEqual(
|
||||||
|
factory.responses.validationError([{ path: ['items', 0, 'assetId'], message: 'Invalid UUID' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require a key', async () => {
|
it('should require a key', async () => {
|
||||||
@@ -134,9 +136,9 @@ describe(AssetController.name, () => {
|
|||||||
.send({ items: [{ assetId: factory.uuid(), value: {} }] });
|
.send({ items: [{ assetId: factory.uuid(), value: {} }] });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
factory.responses.badRequest(
|
factory.responses.validationError([
|
||||||
expect.arrayContaining(['[items.0.key] Invalid input: expected string, received undefined']),
|
{ path: ['items', 0, 'key'], message: 'Invalid input: expected string, received undefined' },
|
||||||
),
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -159,7 +161,9 @@ describe(AssetController.name, () => {
|
|||||||
.delete('/assets/metadata')
|
.delete('/assets/metadata')
|
||||||
.send({ items: [{ assetId: '123', key: 'test' }] });
|
.send({ items: [{ assetId: '123', key: 'test' }] });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['[items.0.assetId] Invalid UUID'])));
|
expect(body).toEqual(
|
||||||
|
factory.responses.validationError([{ path: ['items', 0, 'assetId'], message: 'Invalid UUID' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require a key', async () => {
|
it('should require a key', async () => {
|
||||||
@@ -168,9 +172,9 @@ describe(AssetController.name, () => {
|
|||||||
.send({ items: [{ assetId: factory.uuid() }] });
|
.send({ items: [{ assetId: factory.uuid() }] });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
factory.responses.badRequest(
|
factory.responses.validationError([
|
||||||
expect.arrayContaining(['[items.0.key] Invalid input: expected string, received undefined']),
|
{ path: ['items', 0, 'key'], message: 'Invalid input: expected string, received undefined' },
|
||||||
),
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -191,33 +195,56 @@ describe(AssetController.name, () => {
|
|||||||
it('should require a valid id', async () => {
|
it('should require a valid id', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/123`);
|
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/123`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest(['Invalid input: expected object, received undefined']));
|
expect(body).toEqual(
|
||||||
|
factory.responses.validationError([
|
||||||
|
{ path: [], message: 'Invalid input: expected object, received undefined' },
|
||||||
|
]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject invalid gps coordinates', async () => {
|
it('should reject invalid gps coordinates', async () => {
|
||||||
for (const test of [
|
for (const [test, errors] of [
|
||||||
{ latitude: 12 },
|
[{ latitude: 12 }, [{ path: [], message: 'Latitude and longitude must be provided together' }]],
|
||||||
{ longitude: 12 },
|
[{ longitude: 12 }, [{ path: [], message: 'Latitude and longitude must be provided together' }]],
|
||||||
{ latitude: 12, longitude: 'abc' },
|
[
|
||||||
{ latitude: 'abc', longitude: 12 },
|
{ latitude: 12, longitude: 'abc' },
|
||||||
{ latitude: null, longitude: 12 },
|
[{ path: ['longitude'], message: 'Invalid input: expected number, received string' }],
|
||||||
{ latitude: 12, longitude: null },
|
],
|
||||||
{ latitude: 91, longitude: 12 },
|
[
|
||||||
{ latitude: -91, longitude: 12 },
|
{ latitude: 'abc', longitude: 12 },
|
||||||
{ latitude: 12, longitude: -181 },
|
[{ path: ['latitude'], message: 'Invalid input: expected number, received string' }],
|
||||||
{ latitude: 12, longitude: 181 },
|
],
|
||||||
]) {
|
[
|
||||||
|
{ latitude: null, longitude: 12 },
|
||||||
|
[{ path: ['latitude'], message: 'Invalid input: expected number, received null' }],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ latitude: 12, longitude: null },
|
||||||
|
[{ path: ['longitude'], message: 'Invalid input: expected number, received null' }],
|
||||||
|
],
|
||||||
|
[{ latitude: 91, longitude: 12 }, [{ path: ['latitude'], message: 'Too big: expected number to be <=90' }]],
|
||||||
|
[{ latitude: -91, longitude: 12 }, [{ path: ['latitude'], message: 'Too small: expected number to be >=-90' }]],
|
||||||
|
[
|
||||||
|
{ latitude: 12, longitude: -181 },
|
||||||
|
[{ path: ['longitude'], message: 'Too small: expected number to be >=-180' }],
|
||||||
|
],
|
||||||
|
[{ latitude: 12, longitude: 181 }, [{ path: ['longitude'], message: 'Too big: expected number to be <=180' }]],
|
||||||
|
] as const) {
|
||||||
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}`).send(test);
|
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}`).send(test);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest());
|
expect(body).toEqual(factory.responses.validationError(errors));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject invalid rating', async () => {
|
it('should reject invalid rating', async () => {
|
||||||
for (const test of [{ rating: 7 }, { rating: 3.5 }, { rating: -2 }]) {
|
for (const [test, errors] of [
|
||||||
|
[{ rating: 7 }, [{ path: ['rating'], message: 'Too big: expected number to be <=5' }]],
|
||||||
|
[{ rating: 3.5 }, [{ path: ['rating'], message: 'Invalid input: expected int, received number' }]],
|
||||||
|
[{ rating: -2 }, [{ path: ['rating'], message: 'Too small: expected number to be >=-1' }]],
|
||||||
|
] as const) {
|
||||||
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}`).send(test);
|
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}`).send(test);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest());
|
expect(body).toEqual(factory.responses.validationError(errors));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -261,13 +288,17 @@ describe(AssetController.name, () => {
|
|||||||
it('should require a valid id', async () => {
|
it('should require a valid id', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/123/metadata`).send({ items: [] });
|
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/123/metadata`).send({ items: [] });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['[id] Invalid UUID'])));
|
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require items to be an array', async () => {
|
it('should require items to be an array', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}/metadata`).send({});
|
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}/metadata`).send({});
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest(['[items] Invalid input: expected array, received undefined']));
|
expect(body).toEqual(
|
||||||
|
factory.responses.validationError([
|
||||||
|
{ path: ['items'], message: 'Invalid input: expected array, received undefined' },
|
||||||
|
]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require each item to have a valid key', async () => {
|
it('should require each item to have a valid key', async () => {
|
||||||
@@ -276,7 +307,9 @@ describe(AssetController.name, () => {
|
|||||||
.send({ items: [{ value: { some: 'value' } }] });
|
.send({ items: [{ value: { some: 'value' } }] });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
factory.responses.badRequest(['[items.0.key] Invalid input: expected string, received undefined']),
|
factory.responses.validationError([
|
||||||
|
{ path: ['items', 0, 'key'], message: 'Invalid input: expected string, received undefined' },
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -286,9 +319,9 @@ describe(AssetController.name, () => {
|
|||||||
.send({ items: [{ key: 'mobile-app', value: null }] });
|
.send({ items: [{ key: 'mobile-app', value: null }] });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
factory.responses.badRequest(
|
factory.responses.validationError([
|
||||||
expect.arrayContaining(['[items.0.value] Invalid input: expected record, received null']),
|
{ path: ['items', 0, 'value'], message: 'Invalid input: expected record, received null' },
|
||||||
),
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -326,7 +359,7 @@ describe(AssetController.name, () => {
|
|||||||
it('should require a valid id', async () => {
|
it('should require a valid id', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer()).get(`/assets/123/metadata/mobile-app`);
|
const { status, body } = await request(ctx.getHttpServer()).get(`/assets/123/metadata/mobile-app`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['[id] Invalid UUID'])));
|
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -376,7 +409,7 @@ describe(AssetController.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['[id] Invalid UUID'])));
|
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should check the action and parameters discriminator', async () => {
|
it('should check the action and parameters discriminator', async () => {
|
||||||
@@ -398,13 +431,12 @@ describe(AssetController.name, () => {
|
|||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
factory.responses.badRequest(
|
factory.responses.validationError([
|
||||||
expect.arrayContaining([
|
{
|
||||||
expect.stringContaining(
|
path: ['edits', 0, 'parameters'],
|
||||||
"[edits.0.parameters] Invalid parameters for action 'rotate', expecting keys: angle",
|
message: expect.stringContaining("Invalid parameters for action 'rotate', expecting keys: angle"),
|
||||||
),
|
},
|
||||||
]),
|
]),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -413,7 +445,11 @@ describe(AssetController.name, () => {
|
|||||||
.put(`/assets/${factory.uuid()}/edits`)
|
.put(`/assets/${factory.uuid()}/edits`)
|
||||||
.send({ edits: [] });
|
.send({ edits: [] });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest(['[edits] Too small: expected array to have >=1 items']));
|
expect(body).toEqual(
|
||||||
|
factory.responses.validationError([
|
||||||
|
{ path: ['edits'], message: 'Too small: expected array to have >=1 items' },
|
||||||
|
]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -426,7 +462,7 @@ describe(AssetController.name, () => {
|
|||||||
it('should require a valid id', async () => {
|
it('should require a valid id', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer()).delete(`/assets/123/metadata/mobile-app`);
|
const { status, body } = await request(ctx.getHttpServer()).delete(`/assets/123/metadata/mobile-app`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID']));
|
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,19 +28,27 @@ describe(AuthController.name, () => {
|
|||||||
it('should require an email address', async () => {
|
it('should require an email address', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer()).post('/auth/admin-sign-up').send({ name, password });
|
const { status, body } = await request(ctx.getHttpServer()).post('/auth/admin-sign-up').send({ name, password });
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(errorDto.badRequest());
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['email'], message: 'Invalid input: expected email, received undefined' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require a password', async () => {
|
it('should require a password', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer()).post('/auth/admin-sign-up').send({ name, email });
|
const { status, body } = await request(ctx.getHttpServer()).post('/auth/admin-sign-up').send({ name, email });
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(errorDto.badRequest());
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([
|
||||||
|
{ path: ['password'], message: 'Invalid input: expected string, received undefined' },
|
||||||
|
]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require a name', async () => {
|
it('should require a name', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer()).post('/auth/admin-sign-up').send({ email, password });
|
const { status, body } = await request(ctx.getHttpServer()).post('/auth/admin-sign-up').send({ email, password });
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(errorDto.badRequest());
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['name'], message: 'Invalid input: expected string, received undefined' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require a valid email', async () => {
|
it('should require a valid email', async () => {
|
||||||
@@ -48,7 +56,9 @@ describe(AuthController.name, () => {
|
|||||||
.post('/auth/admin-sign-up')
|
.post('/auth/admin-sign-up')
|
||||||
.send({ name, email: 'immich', password });
|
.send({ name, email: 'immich', password });
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(errorDto.badRequest());
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['email'], message: 'Invalid input: expected email, received string' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should transform email to lower case', async () => {
|
it('should transform email to lower case', async () => {
|
||||||
@@ -73,9 +83,9 @@ describe(AuthController.name, () => {
|
|||||||
const { status, body } = await request(ctx.getHttpServer()).post('/auth/login').send({ name: 'admin' });
|
const { status, body } = await request(ctx.getHttpServer()).post('/auth/login').send({ name: 'admin' });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
errorDto.badRequest([
|
errorDto.validationError([
|
||||||
'[email] Invalid input: expected email, received undefined',
|
{ path: ['email'], message: 'Invalid input: expected email, received undefined' },
|
||||||
'[password] Invalid input: expected string, received undefined',
|
{ path: ['password'], message: 'Invalid input: expected string, received undefined' },
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -85,7 +95,9 @@ describe(AuthController.name, () => {
|
|||||||
.post('/auth/login')
|
.post('/auth/login')
|
||||||
.send({ name: 'admin', email: null, password: 'password' });
|
.send({ name: 'admin', email: null, password: 'password' });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[email] Invalid input: expected email, received object']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['email'], message: 'Invalid input: expected email, received object' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should not allow null password`, async () => {
|
it(`should not allow null password`, async () => {
|
||||||
@@ -93,7 +105,9 @@ describe(AuthController.name, () => {
|
|||||||
.post('/auth/login')
|
.post('/auth/login')
|
||||||
.send({ name: 'admin', email: 'admin@immich.cloud', password: null });
|
.send({ name: 'admin', email: 'admin@immich.cloud', password: null });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[password] Invalid input: expected string, received null']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['password'], message: 'Invalid input: expected string, received null' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject an invalid email', async () => {
|
it('should reject an invalid email', async () => {
|
||||||
@@ -104,7 +118,9 @@ describe(AuthController.name, () => {
|
|||||||
.send({ name: 'admin', email: [], password: 'password' });
|
.send({ name: 'admin', email: [], password: 'password' });
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[email] Invalid input: expected email, received object']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['email'], message: 'Invalid input: expected email, received object' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should transform the email to all lowercase', async () => {
|
it('should transform the email to all lowercase', async () => {
|
||||||
@@ -195,19 +211,31 @@ describe(AuthController.name, () => {
|
|||||||
it('should reject 5 digits', async () => {
|
it('should reject 5 digits', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: '12345' });
|
const { status, body } = await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: '12345' });
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(errorDto.badRequest([String.raw`[pinCode] Invalid string: must match pattern /^\d{6}$/`]));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([
|
||||||
|
{ path: ['pinCode'], message: String.raw`Invalid string: must match pattern /^\d{6}$/` },
|
||||||
|
]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject 7 digits', async () => {
|
it('should reject 7 digits', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: '1234567' });
|
const { status, body } = await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: '1234567' });
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(errorDto.badRequest([String.raw`[pinCode] Invalid string: must match pattern /^\d{6}$/`]));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([
|
||||||
|
{ path: ['pinCode'], message: String.raw`Invalid string: must match pattern /^\d{6}$/` },
|
||||||
|
]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject non-numbers', async () => {
|
it('should reject non-numbers', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: 'A12345' });
|
const { status, body } = await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: 'A12345' });
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(errorDto.badRequest([String.raw`[pinCode] Invalid string: must match pattern /^\d{6}$/`]));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([
|
||||||
|
{ path: ['pinCode'], message: String.raw`Invalid string: must match pattern /^\d{6}$/` },
|
||||||
|
]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ describe(DuplicateController.name, () => {
|
|||||||
it('should require a valid uuid', async () => {
|
it('should require a valid uuid', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer()).delete(`/duplicates/123`);
|
const { status, body } = await request(ctx.getHttpServer()).delete(`/duplicates/123`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID']));
|
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ export class DuplicateController {
|
|||||||
@Authenticated({ permission: Permission.DuplicateDelete })
|
@Authenticated({ permission: Permission.DuplicateDelete })
|
||||||
@HttpCode(HttpStatus.NO_CONTENT)
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
@Endpoint({
|
@Endpoint({
|
||||||
summary: 'Delete a duplicate',
|
summary: 'Dismiss a duplicate group',
|
||||||
description: 'Delete a single duplicate asset specified by its ID.',
|
description: 'Dismiss a duplicate group by its ID, unlinking all assets in the group without deleting them.',
|
||||||
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
|
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
|
||||||
})
|
})
|
||||||
deleteDuplicate(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
deleteDuplicate(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ describe(MaintenanceController.name, () => {
|
|||||||
});
|
});
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
errorDto.badRequest(['[restoreBackupFilename] Backup filename is required when action is restore_database']),
|
errorDto.validationError([
|
||||||
|
{ path: ['restoreBackupFilename'], message: 'Backup filename is required when action is restore_database' },
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
expect(ctx.authenticate).toHaveBeenCalled();
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,7 +47,11 @@ describe(MemoryController.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[data.year] Invalid input: expected number, received undefined']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([
|
||||||
|
{ path: ['data', 'year'], message: 'Invalid input: expected number, received undefined' },
|
||||||
|
]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should accept showAt and hideAt', async () => {
|
it('should accept showAt and hideAt', async () => {
|
||||||
@@ -81,7 +85,7 @@ describe(MemoryController.name, () => {
|
|||||||
it('should require a valid id', async () => {
|
it('should require a valid id', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer()).get(`/memories/invalid`);
|
const { status, body } = await request(ctx.getHttpServer()).get(`/memories/invalid`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID']));
|
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -94,13 +98,15 @@ describe(MemoryController.name, () => {
|
|||||||
it('should require a valid id', async () => {
|
it('should require a valid id', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer()).put(`/memories/invalid`);
|
const { status, body } = await request(ctx.getHttpServer()).put(`/memories/invalid`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['Invalid input: expected object, received undefined']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: [], message: 'Invalid input: expected object, received undefined' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require at least one field', async () => {
|
it('should require at least one field', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer()).put(`/memories/${factory.uuid()}`).send({});
|
const { status, body } = await request(ctx.getHttpServer()).put(`/memories/${factory.uuid()}`).send({});
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['At least one field must be provided']));
|
expect(body).toEqual(errorDto.validationError([{ path: [], message: 'At least one field must be provided' }]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -120,7 +126,7 @@ describe(MemoryController.name, () => {
|
|||||||
it('should require a valid id', async () => {
|
it('should require a valid id', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer()).put(`/memories/invalid/assets`).send({ ids: [] });
|
const { status, body } = await request(ctx.getHttpServer()).put(`/memories/invalid/assets`).send({ ids: [] });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID']));
|
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require a valid asset id', async () => {
|
it('should require a valid asset id', async () => {
|
||||||
@@ -128,7 +134,7 @@ describe(MemoryController.name, () => {
|
|||||||
.put(`/memories/${factory.uuid()}/assets`)
|
.put(`/memories/${factory.uuid()}/assets`)
|
||||||
.send({ ids: ['invalid'] });
|
.send({ ids: ['invalid'] });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[ids.0] Invalid UUID']));
|
expect(body).toEqual(errorDto.validationError([{ path: ['ids', 0], message: 'Invalid UUID' }]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -141,7 +147,7 @@ describe(MemoryController.name, () => {
|
|||||||
it('should require a valid id', async () => {
|
it('should require a valid id', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer()).delete(`/memories/invalid/assets`);
|
const { status, body } = await request(ctx.getHttpServer()).delete(`/memories/invalid/assets`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID']));
|
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require a valid asset id', async () => {
|
it('should require a valid asset id', async () => {
|
||||||
@@ -149,7 +155,7 @@ describe(MemoryController.name, () => {
|
|||||||
.delete(`/memories/${factory.uuid()}/assets`)
|
.delete(`/memories/${factory.uuid()}/assets`)
|
||||||
.send({ ids: ['invalid'] });
|
.send({ ids: ['invalid'] });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[ids.0] Invalid UUID']));
|
expect(body).toEqual(errorDto.validationError([{ path: ['ids', 0], message: 'Invalid UUID' }]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,7 +31,11 @@ describe(NotificationController.name, () => {
|
|||||||
.query({ level: 'invalid' })
|
.query({ level: 'invalid' })
|
||||||
.set('Authorization', `Bearer token`);
|
.set('Authorization', `Bearer token`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest([expect.stringContaining('[level] Invalid option: expected one of')]));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([
|
||||||
|
{ path: ['level'], message: expect.stringContaining('Invalid option: expected one of') },
|
||||||
|
]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -45,7 +49,9 @@ describe(NotificationController.name, () => {
|
|||||||
it('should require a list', async () => {
|
it('should require a list', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer()).put(`/notifications`).send({ ids: true });
|
const { status, body } = await request(ctx.getHttpServer()).put(`/notifications`).send({ ids: true });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[ids] Invalid input: expected array, received boolean']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['ids'], message: 'Invalid input: expected array, received boolean' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require uuids', async () => {
|
it('should require uuids', async () => {
|
||||||
@@ -53,7 +59,9 @@ describe(NotificationController.name, () => {
|
|||||||
.put(`/notifications`)
|
.put(`/notifications`)
|
||||||
.send({ ids: [true] });
|
.send({ ids: [true] });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[ids.0] Invalid input: expected string, received boolean']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['ids', 0], message: 'Invalid input: expected string, received boolean' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should accept valid uuids', async () => {
|
it('should accept valid uuids', async () => {
|
||||||
@@ -75,7 +83,7 @@ describe(NotificationController.name, () => {
|
|||||||
it('should require a valid uuid', async () => {
|
it('should require a valid uuid', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer()).get(`/notifications/123`);
|
const { status, body } = await request(ctx.getHttpServer()).get(`/notifications/123`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID']));
|
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ describe(PartnerController.name, () => {
|
|||||||
const { status, body } = await request(ctx.getHttpServer()).get(`/partners`).set('Authorization', `Bearer token`);
|
const { status, body } = await request(ctx.getHttpServer()).get(`/partners`).set('Authorization', `Bearer token`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
errorDto.badRequest([expect.stringContaining('[direction] Invalid option: expected one of')]),
|
errorDto.validationError([
|
||||||
|
{ path: ['direction'], message: expect.stringContaining('Invalid option: expected one of') },
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -44,7 +46,9 @@ describe(PartnerController.name, () => {
|
|||||||
.set('Authorization', `Bearer token`);
|
.set('Authorization', `Bearer token`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
errorDto.badRequest([expect.stringContaining('[direction] Invalid option: expected one of')]),
|
errorDto.validationError([
|
||||||
|
{ path: ['direction'], message: expect.stringContaining('Invalid option: expected one of') },
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -61,7 +65,7 @@ describe(PartnerController.name, () => {
|
|||||||
.send({ sharedWithId: 'invalid' })
|
.send({ sharedWithId: 'invalid' })
|
||||||
.set('Authorization', `Bearer token`);
|
.set('Authorization', `Bearer token`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[sharedWithId] Invalid UUID']));
|
expect(body).toEqual(errorDto.validationError([{ path: ['sharedWithId'], message: 'Invalid UUID' }]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -77,7 +81,7 @@ describe(PartnerController.name, () => {
|
|||||||
.send({ inTimeline: true })
|
.send({ inTimeline: true })
|
||||||
.set('Authorization', `Bearer token`);
|
.set('Authorization', `Bearer token`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID']));
|
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -92,7 +96,7 @@ describe(PartnerController.name, () => {
|
|||||||
.delete(`/partners/invalid`)
|
.delete(`/partners/invalid`)
|
||||||
.set('Authorization', `Bearer token`);
|
.set('Authorization', `Bearer token`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID']));
|
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user