diff --git a/cli/src/commands/asset.spec.ts b/cli/src/commands/asset.spec.ts index 21700ef963..f179b350c9 100644 --- a/cli/src/commands/asset.spec.ts +++ b/cli/src/commands/asset.spec.ts @@ -4,7 +4,7 @@ import path from 'node:path'; import { setTimeout as sleep } from 'node:timers/promises'; import { describe, expect, it, MockedFunction, vi } from 'vitest'; -import { Action, checkBulkUpload, defaults, getSupportedMediaTypes, Reason } from '@immich/sdk'; +import { AssetRejectReason, AssetUploadAction, checkBulkUpload, defaults, getSupportedMediaTypes } from '@immich/sdk'; import createFetchMock from 'vitest-fetch-mock'; import { @@ -120,7 +120,7 @@ describe('checkForDuplicates', () => { vi.mocked(checkBulkUpload).mockResolvedValue({ results: [ { - action: Action.Accept, + action: AssetUploadAction.Accept, id: testFilePath, }, ], @@ -144,10 +144,10 @@ describe('checkForDuplicates', () => { vi.mocked(checkBulkUpload).mockResolvedValue({ results: [ { - action: Action.Reject, + action: AssetUploadAction.Reject, id: testFilePath, assetId: 'fc5621b1-86f6-44a1-9905-403e607df9f5', - reason: Reason.Duplicate, + reason: AssetRejectReason.Duplicate, }, ], }); @@ -167,7 +167,7 @@ describe('checkForDuplicates', () => { vi.mocked(checkBulkUpload).mockResolvedValue({ results: [ { - action: Action.Accept, + action: AssetUploadAction.Accept, id: testFilePath, }, ], @@ -187,7 +187,7 @@ describe('checkForDuplicates', () => { mocked.mockResolvedValue({ results: [ { - action: Action.Accept, + action: AssetUploadAction.Accept, id: testFilePath, }, ], diff --git a/cli/src/commands/asset.ts b/cli/src/commands/asset.ts index 7d4b09b69d..2c6430c83a 100644 --- a/cli/src/commands/asset.ts +++ b/cli/src/commands/asset.ts @@ -1,9 +1,9 @@ import { - Action, AssetBulkUploadCheckItem, AssetBulkUploadCheckResult, AssetMediaResponseDto, AssetMediaStatus, + AssetUploadAction, Permission, addAssetsToAlbum, checkBulkUpload, @@ -234,7 +234,7 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas const results = response.results as AssetBulkUploadCheckResults; for (const { id: filepath, assetId, action } of results) { - if (action === Action.Accept) { + if (action === AssetUploadAction.Accept) { newFiles.push(filepath); } else { // rejects are always duplicates @@ -404,8 +404,6 @@ const uploadFile = async (input: string, stats: Stats): Promise Settings -> Storage Template`. Immich provides a set of variables that you can use in constructing the template, along with additional custom text. If the template produces [multiple files with the same filename, they won't be overwritten](https://github.com/immich-app/immich/discussions/3324) as a sequence number is appended to the filename. +Date and time variables in storage templates are rendered in the server's local timezone. + ```bash title="Default template" Year/Year-Month-Day/Filename.Extension ``` diff --git a/e2e/src/specs/server/api/album.e2e-spec.ts b/e2e/src/specs/server/api/album.e2e-spec.ts index a9e90940ab..3725de8d26 100644 --- a/e2e/src/specs/server/api/album.e2e-spec.ts +++ b/e2e/src/specs/server/api/album.e2e-spec.ts @@ -130,12 +130,11 @@ describe('/albums', () => { describe('GET /albums', () => { it("should not show other users' favorites", async () => { const { status, body } = await request(app) - .get(`/albums/${user1Albums[0].id}?withoutAssets=false`) + .get(`/albums/${user1Albums[0].id}`) .set('Authorization', `Bearer ${user2.accessToken}`); expect(status).toEqual(200); expect(body).toEqual({ ...user1Albums[0], - assets: [expect.objectContaining({ isFavorite: false })], contributorCounts: [{ userId: user1.userId, assetCount: 1 }], lastModifiedAssetTimestamp: expect.any(String), startDate: expect.any(String), @@ -304,13 +303,12 @@ describe('/albums', () => { describe('GET /albums/:id', () => { it('should return album info for own album', async () => { const { status, body } = await request(app) - .get(`/albums/${user1Albums[0].id}?withoutAssets=false`) + .get(`/albums/${user1Albums[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); expect(body).toEqual({ ...user1Albums[0], - assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })], contributorCounts: [{ userId: user1.userId, assetCount: 1 }], lastModifiedAssetTimestamp: expect.any(String), startDate: expect.any(String), @@ -322,7 +320,7 @@ describe('/albums', () => { it('should return album info for shared album (editor)', async () => { const { status, body } = await request(app) - .get(`/albums/${user2Albums[0].id}?withoutAssets=false`) + .get(`/albums/${user2Albums[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); @@ -331,14 +329,14 @@ describe('/albums', () => { it('should return album info for shared album (viewer)', async () => { const { status, body } = await request(app) - .get(`/albums/${user1Albums[3].id}?withoutAssets=false`) + .get(`/albums/${user1Albums[3].id}`) .set('Authorization', `Bearer ${user2.accessToken}`); expect(status).toBe(200); expect(body).toMatchObject({ id: user1Albums[3].id }); }); - it('should return album info with assets when withoutAssets is undefined', async () => { + it('should return album info', async () => { const { status, body } = await request(app) .get(`/albums/${user1Albums[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`); @@ -346,25 +344,6 @@ describe('/albums', () => { expect(status).toBe(200); expect(body).toEqual({ ...user1Albums[0], - assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })], - contributorCounts: [{ userId: user1.userId, assetCount: 1 }], - lastModifiedAssetTimestamp: expect.any(String), - startDate: expect.any(String), - endDate: expect.any(String), - albumUsers: expect.any(Array), - shared: true, - }); - }); - - it('should return album info without assets when withoutAssets is true', async () => { - const { status, body } = await request(app) - .get(`/albums/${user1Albums[0].id}?withoutAssets=true`) - .set('Authorization', `Bearer ${user1.accessToken}`); - - expect(status).toBe(200); - expect(body).toEqual({ - ...user1Albums[0], - assets: [], contributorCounts: [{ userId: user1.userId, assetCount: 1 }], assetCount: 1, lastModifiedAssetTimestamp: expect.any(String), @@ -379,13 +358,12 @@ describe('/albums', () => { await utils.deleteAssets(user1.accessToken, [user1Asset2.id]); const { status, body } = await request(app) - .get(`/albums/${user2Albums[0].id}?withoutAssets=true`) + .get(`/albums/${user2Albums[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); expect(body).toEqual({ ...user2Albums[0], - assets: [], contributorCounts: [{ userId: user1.userId, assetCount: 1 }], assetCount: 1, lastModifiedAssetTimestamp: expect.any(String), @@ -426,7 +404,6 @@ describe('/albums', () => { shared: false, albumUsers: [], hasSharedLink: false, - assets: [], assetCount: 0, owner: expect.objectContaining({ email: user1.userEmail }), isActivityEnabled: true, diff --git a/e2e/src/specs/server/api/asset.e2e-spec.ts b/e2e/src/specs/server/api/asset.e2e-spec.ts index 11e825a7cd..3fbacd5bf6 100644 --- a/e2e/src/specs/server/api/asset.e2e-spec.ts +++ b/e2e/src/specs/server/api/asset.e2e-spec.ts @@ -1,7 +1,6 @@ import { AssetMediaResponseDto, AssetMediaStatus, - AssetResponseDto, AssetTypeEnum, AssetVisibility, getAssetInfo, @@ -19,7 +18,7 @@ import { Socket } from 'socket.io-client'; import { createUserDto, uuidDto } from 'src/fixtures'; import { makeRandomImage } from 'src/generators'; import { errorDto } from 'src/responses'; -import { app, asBearerAuth, tempDir, TEN_TIMES, testAssetDir, utils } from 'src/utils'; +import { app, asBearerAuth, tempDir, testAssetDir, utils } from 'src/utils'; import request from 'supertest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; @@ -95,8 +94,8 @@ describe('/asset', () => { utils.createAsset(user1.accessToken), utils.createAsset(user1.accessToken, { isFavorite: true, - fileCreatedAt: yesterday.toISO(), - fileModifiedAt: yesterday.toISO(), + fileCreatedAt: yesterday.toUTC().toISO(), + fileModifiedAt: yesterday.toUTC().toISO(), assetData: { filename: 'example.mp4' }, }), utils.createAsset(user1.accessToken), @@ -380,62 +379,12 @@ describe('/asset', () => { }); }); - describe('GET /assets/random', () => { - beforeAll(async () => { - await Promise.all([ - utils.createAsset(user1.accessToken), - utils.createAsset(user1.accessToken), - utils.createAsset(user1.accessToken), - utils.createAsset(user1.accessToken), - utils.createAsset(user1.accessToken), - utils.createAsset(user1.accessToken), - ]); - - await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration'); - }); - - it.each(TEN_TIMES)('should return 1 random assets', async () => { - const { status, body } = await request(app) - .get('/assets/random') - .set('Authorization', `Bearer ${user1.accessToken}`); - - expect(status).toBe(200); - - const assets: AssetResponseDto[] = body; - expect(assets.length).toBe(1); - expect(assets[0].ownerId).toBe(user1.userId); - }); - - it.each(TEN_TIMES)('should return 2 random assets', async () => { - const { status, body } = await request(app) - .get('/assets/random?count=2') - .set('Authorization', `Bearer ${user1.accessToken}`); - - expect(status).toBe(200); - - const assets: AssetResponseDto[] = body; - expect(assets.length).toBe(2); - - for (const asset of assets) { - expect(asset.ownerId).toBe(user1.userId); - } - }); - - it.skip('should return 1 asset if there are 10 assets in the database but user 2 only has 1', async () => { - const { status, body } = await request(app) - .get('/assets/random') - .set('Authorization', `Bearer ${user2.accessToken}`); - - expect(status).toBe(200); - expect(body).toEqual([expect.objectContaining({ id: user2Assets[0].id })]); - }); - }); - describe('PUT /assets/:id', () => { it('should require access', async () => { const { status, body } = await request(app) .put(`/assets/${user2Assets[0].id}`) - .set('Authorization', `Bearer ${user1.accessToken}`); + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({}); expect(status).toBe(400); expect(body).toEqual(errorDto.noPermission); }); @@ -1142,8 +1091,6 @@ describe('/asset', () => { const { body, status } = await request(app) .post('/assets') .set('Authorization', `Bearer ${quotaUser.accessToken}`) - .field('deviceAssetId', 'example-image') - .field('deviceId', 'e2e') .field('fileCreatedAt', new Date().toISOString()) .field('fileModifiedAt', new Date().toISOString()) .attach('assetData', makeRandomImage(), 'example.jpg'); @@ -1160,8 +1107,6 @@ describe('/asset', () => { const { body, status } = await request(app) .post('/assets') .set('Authorization', `Bearer ${quotaUser.accessToken}`) - .field('deviceAssetId', 'example-image') - .field('deviceId', 'e2e') .field('fileCreatedAt', new Date().toISOString()) .field('fileModifiedAt', new Date().toISOString()) .attach('assetData', randomBytes(2014), 'example.jpg'); @@ -1215,29 +1160,4 @@ describe('/asset', () => { expect(video.checksum).toStrictEqual(checksum); }); }); - - describe('POST /assets/exist', () => { - it('ignores invalid deviceAssetIds', async () => { - const response = await utils.checkExistingAssets(user1.accessToken, { - deviceId: 'test-assets-exist', - deviceAssetIds: ['invalid', 'INVALID'], - }); - - expect(response.existingIds).toHaveLength(0); - }); - - it('returns the IDs of existing assets', async () => { - await utils.createAsset(user1.accessToken, { - deviceId: 'test-assets-exist', - deviceAssetId: 'test-asset-0', - }); - - const response = await utils.checkExistingAssets(user1.accessToken, { - deviceId: 'test-assets-exist', - deviceAssetIds: ['test-asset-0'], - }); - - expect(response.existingIds).toEqual(['test-asset-0']); - }); - }); }); diff --git a/e2e/src/specs/server/api/library.e2e-spec.ts b/e2e/src/specs/server/api/library.e2e-spec.ts index 4d67a84647..719436a66d 100644 --- a/e2e/src/specs/server/api/library.e2e-spec.ts +++ b/e2e/src/specs/server/api/library.e2e-spec.ts @@ -110,7 +110,7 @@ describe('/libraries', () => { }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(["All importPaths's elements must be unique"])); + expect(body).toEqual(errorDto.badRequest(['[importPaths] Array must have unique items'])); }); it('should not create an external library with duplicate exclusion patterns', async () => { @@ -125,7 +125,7 @@ describe('/libraries', () => { }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(["All exclusionPatterns's elements must be unique"])); + expect(body).toEqual(errorDto.badRequest(['[exclusionPatterns] Array must have unique items'])); }); }); @@ -157,7 +157,7 @@ describe('/libraries', () => { .send({ name: '' }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['name should not be empty'])); + expect(body).toEqual(errorDto.badRequest(['[name] Too small: expected string to have >=1 characters'])); }); it('should change the import paths', async () => { @@ -181,7 +181,7 @@ describe('/libraries', () => { .send({ importPaths: [''] }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['each value in importPaths should not be empty'])); + expect(body).toEqual(errorDto.badRequest(['[importPaths] Array items must not be empty'])); }); it('should reject duplicate import paths', async () => { @@ -191,7 +191,7 @@ describe('/libraries', () => { .send({ importPaths: ['/path', '/path'] }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(["All importPaths's elements must be unique"])); + expect(body).toEqual(errorDto.badRequest(['[importPaths] Array must have unique items'])); }); it('should change the exclusion pattern', async () => { @@ -215,7 +215,7 @@ describe('/libraries', () => { .send({ exclusionPatterns: ['**/*.jpg', '**/*.jpg'] }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(["All exclusionPatterns's elements must be unique"])); + expect(body).toEqual(errorDto.badRequest(['[exclusionPatterns] Array must have unique items'])); }); it('should reject an empty exclusion pattern', async () => { @@ -225,7 +225,7 @@ describe('/libraries', () => { .send({ exclusionPatterns: [''] }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['each value in exclusionPatterns should not be empty'])); + expect(body).toEqual(errorDto.badRequest(['[exclusionPatterns] Array items must not be empty'])); }); }); diff --git a/e2e/src/specs/server/api/map.e2e-spec.ts b/e2e/src/specs/server/api/map.e2e-spec.ts index 977638aa24..c280deb134 100644 --- a/e2e/src/specs/server/api/map.e2e-spec.ts +++ b/e2e/src/specs/server/api/map.e2e-spec.ts @@ -109,7 +109,7 @@ describe('/map', () => { .get('/map/reverse-geocode?lon=123') .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['lat must be a number between -90 and 90'])); + expect(body).toEqual(errorDto.badRequest(['[lat] Invalid input: expected number, received NaN'])); }); it('should throw an error if a lat is not a number', async () => { @@ -117,7 +117,7 @@ describe('/map', () => { .get('/map/reverse-geocode?lat=abc&lon=123.456') .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['lat must be a number between -90 and 90'])); + expect(body).toEqual(errorDto.badRequest(['[lat] Invalid input: expected number, received NaN'])); }); it('should throw an error if a lat is out of range', async () => { @@ -125,7 +125,7 @@ describe('/map', () => { .get('/map/reverse-geocode?lat=91&lon=123.456') .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['lat must be a number between -90 and 90'])); + expect(body).toEqual(errorDto.badRequest(['[lat] Too big: expected number to be <=90'])); }); it('should throw an error if a lon is not provided', async () => { @@ -133,7 +133,7 @@ describe('/map', () => { .get('/map/reverse-geocode?lat=75') .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['lon must be a number between -180 and 180'])); + expect(body).toEqual(errorDto.badRequest(['[lon] Invalid input: expected number, received NaN'])); }); const reverseGeocodeTestCases = [ diff --git a/e2e/src/specs/server/api/oauth.e2e-spec.ts b/e2e/src/specs/server/api/oauth.e2e-spec.ts index ae9064375f..a0ae1dc819 100644 --- a/e2e/src/specs/server/api/oauth.e2e-spec.ts +++ b/e2e/src/specs/server/api/oauth.e2e-spec.ts @@ -101,7 +101,7 @@ describe(`/oauth`, () => { it(`should throw an error if a redirect uri is not provided`, async () => { const { status, body } = await request(app).post('/oauth/authorize').send({}); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['redirectUri must be a string', 'redirectUri should not be empty'])); + expect(body).toEqual(errorDto.badRequest(['[redirectUri] Invalid input: expected string, received undefined'])); }); it('should return a redirect uri', async () => { @@ -123,13 +123,13 @@ describe(`/oauth`, () => { it(`should throw an error if a url is not provided`, async () => { const { status, body } = await request(app).post('/oauth/callback').send({}); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['url must be a string', 'url should not be empty'])); + expect(body).toEqual(errorDto.badRequest(['[url] Invalid input: expected string, received undefined'])); }); it(`should throw an error if the url is empty`, async () => { const { status, body } = await request(app).post('/oauth/callback').send({ url: '' }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['url should not be empty'])); + expect(body).toEqual(errorDto.badRequest(['[url] Too small: expected string to have >=1 characters'])); }); it(`should throw an error if the state is not provided`, async () => { diff --git a/e2e/src/specs/server/api/search.e2e-spec.ts b/e2e/src/specs/server/api/search.e2e-spec.ts index 2f6ea75f77..4ee021b1e4 100644 --- a/e2e/src/specs/server/api/search.e2e-spec.ts +++ b/e2e/src/specs/server/api/search.e2e-spec.ts @@ -74,7 +74,6 @@ describe('/search', () => { const bytes = await readFile(join(testAssetDir, filename)); assets.push( await utils.createAsset(admin.accessToken, { - deviceAssetId: `test-${filename}`, assetData: { bytes, filename }, ...dto, }), diff --git a/e2e/src/specs/server/api/shared-link.e2e-spec.ts b/e2e/src/specs/server/api/shared-link.e2e-spec.ts index 00c455d6cb..1d069d0f54 100644 --- a/e2e/src/specs/server/api/shared-link.e2e-spec.ts +++ b/e2e/src/specs/server/api/shared-link.e2e-spec.ts @@ -243,9 +243,21 @@ describe('/shared-links', () => { }); it('should get data for correct password protected link', async () => { + const response = await request(app) + .post('/shared-links/login') + .send({ password: 'foo' }) + .query({ key: linkWithPassword.key }); + + expect(response.status).toBe(201); + + const cookies = response.get('Set-Cookie') ?? []; + expect(cookies).toHaveLength(1); + expect(cookies[0]).toContain('immich_shared_link_token'); + const { status, body } = await request(app) .get('/shared-links/me') - .query({ key: linkWithPassword.key, password: 'foo' }); + .query({ key: linkWithPassword.key }) + .set('Cookie', cookies); expect(status).toBe(200); expect(body).toEqual( diff --git a/e2e/src/specs/server/api/tag.e2e-spec.ts b/e2e/src/specs/server/api/tag.e2e-spec.ts index d69536f3a3..7b5a2f16de 100644 --- a/e2e/src/specs/server/api/tag.e2e-spec.ts +++ b/e2e/src/specs/server/api/tag.e2e-spec.ts @@ -309,7 +309,7 @@ describe('/tags', () => { .get(`/tags/${uuidDto.invalid}`) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID'])); }); it('should get tag details', async () => { @@ -427,7 +427,7 @@ describe('/tags', () => { .delete(`/tags/${uuidDto.invalid}`) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID'])); }); it('should delete a tag', async () => { diff --git a/e2e/src/specs/server/api/user-admin.e2e-spec.ts b/e2e/src/specs/server/api/user-admin.e2e-spec.ts index 793c508a36..6751b21e84 100644 --- a/e2e/src/specs/server/api/user-admin.e2e-spec.ts +++ b/e2e/src/specs/server/api/user-admin.e2e-spec.ts @@ -287,7 +287,8 @@ describe('/admin/users', () => { it('should delete user', async () => { const { status, body } = await request(app) .delete(`/admin/users/${userToDelete.userId}`) - .set('Authorization', `Bearer ${admin.accessToken}`); + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({}); expect(status).toBe(200); expect(body).toMatchObject({ diff --git a/e2e/src/specs/server/api/user.e2e-spec.ts b/e2e/src/specs/server/api/user.e2e-spec.ts index 3f280dddf5..ee13a29c1b 100644 --- a/e2e/src/specs/server/api/user.e2e-spec.ts +++ b/e2e/src/specs/server/api/user.e2e-spec.ts @@ -178,7 +178,9 @@ describe('/users', () => { .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['download.archiveSize must be an integer number'])); + expect(body).toEqual( + errorDto.badRequest(['[download.archiveSize] Invalid input: expected int, received number']), + ); }); it('should update download archive size', async () => { @@ -204,7 +206,9 @@ describe('/users', () => { .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['download.includeEmbeddedVideos must be a boolean value'])); + expect(body).toEqual( + errorDto.badRequest(['[download.includeEmbeddedVideos] Invalid input: expected boolean, received number']), + ); }); it('should update download include embedded videos', async () => { diff --git a/e2e/src/specs/web/duplicates.e2e-spec.ts b/e2e/src/specs/web/duplicates.e2e-spec.ts index 34f11cdf78..c39e9019d3 100644 --- a/e2e/src/specs/web/duplicates.e2e-spec.ts +++ b/e2e/src/specs/web/duplicates.e2e-spec.ts @@ -16,8 +16,8 @@ test.describe('Duplicates Utility', () => { test.beforeEach(async ({ context }) => { [firstAsset, secondAsset] = await Promise.all([ - utils.createAsset(admin.accessToken, { deviceAssetId: 'duplicate-a' }), - utils.createAsset(admin.accessToken, { deviceAssetId: 'duplicate-b' }), + utils.createAsset(admin.accessToken, {}), + utils.createAsset(admin.accessToken, {}), ]); await updateAssets( diff --git a/e2e/src/specs/web/photo-viewer.e2e-spec.ts b/e2e/src/specs/web/photo-viewer.e2e-spec.ts index 76d9d61ed6..71f2145be8 100644 --- a/e2e/src/specs/web/photo-viewer.e2e-spec.ts +++ b/e2e/src/specs/web/photo-viewer.e2e-spec.ts @@ -77,18 +77,4 @@ test.describe('Photo Viewer', () => { }); expect(tagAtCenter).toBe('IMG'); }); - - test('reloads photo when checksum changes', async ({ page }) => { - await page.goto(`/photos/${asset.id}`); - - const preview = page.getByTestId('preview').filter({ visible: true }); - await expect(preview).toHaveAttribute('src', /.+/); - const initialSrc = await preview.getAttribute('src'); - - const websocketEvent = utils.waitForWebsocketEvent({ event: 'assetUpdate', id: asset.id }); - await utils.replaceAsset(admin.accessToken, asset.id); - await websocketEvent; - - await expect(preview).not.toHaveAttribute('src', initialSrc!); - }); }); diff --git a/e2e/src/ui/generators/timeline/rest-response.ts b/e2e/src/ui/generators/timeline/rest-response.ts index 0c4bd06dc3..3114e3676d 100644 --- a/e2e/src/ui/generators/timeline/rest-response.ts +++ b/e2e/src/ui/generators/timeline/rest-response.ts @@ -315,11 +315,9 @@ export function toAssetResponseDto(asset: MockTimelineAsset, owner?: UserRespons return { id: asset.id, - deviceAssetId: `device-${asset.id}`, ownerId: asset.ownerId, owner: owner || defaultOwner, libraryId: `library-${asset.ownerId}`, - deviceId: `device-${asset.ownerId}`, type: asset.isVideo ? AssetTypeEnum.Video : AssetTypeEnum.Image, originalPath: `/original/${asset.id}.${asset.isVideo ? 'mp4' : 'jpg'}`, originalFileName: `${asset.id}.${asset.isVideo ? 'mp4' : 'jpg'}`, @@ -429,7 +427,6 @@ export function getAlbum( hasSharedLink: false, isActivityEnabled: true, assetCount: albumAssets.length, - assets: albumAssets, startDate: albumAssets.length > 0 ? albumAssets.at(-1)?.fileCreatedAt : undefined, endDate: albumAssets.length > 0 ? albumAssets[0].fileCreatedAt : undefined, lastModifiedAssetTimestamp: albumAssets.length > 0 ? albumAssets[0].fileCreatedAt : undefined, diff --git a/e2e/src/ui/mock-network/broken-asset-network.ts b/e2e/src/ui/mock-network/broken-asset-network.ts index 1494b40531..75d579e1ef 100644 --- a/e2e/src/ui/mock-network/broken-asset-network.ts +++ b/e2e/src/ui/mock-network/broken-asset-network.ts @@ -16,7 +16,6 @@ export const createMockStackAsset = (ownerId: string): AssetResponseDto => { const now = new Date().toISOString(); return { id: assetId, - deviceAssetId: `device-${assetId}`, ownerId, owner: { id: ownerId, @@ -27,7 +26,6 @@ export const createMockStackAsset = (ownerId: string): AssetResponseDto => { avatarColor: 'blue' as never, }, libraryId: `library-${ownerId}`, - deviceId: `device-${ownerId}`, type: AssetTypeEnum.Image, originalPath: `/original/${assetId}.jpg`, originalFileName: `${assetId}.jpg`, @@ -69,7 +67,7 @@ export const createMockStackAsset = (ownerId: string): AssetResponseDto => { tags: [], people: [], unassignedFaces: [], - stack: null, + stack: undefined, isOffline: false, hasMetadata: true, duplicateId: null, diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 4d44d99e2f..aa4c3b8499 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -3,7 +3,6 @@ import { AssetMediaResponseDto, AssetResponseDto, AssetVisibility, - CheckExistingAssetsDto, CreateAlbumDto, CreateLibraryDto, JobCreateDto, @@ -20,7 +19,6 @@ import { UserAdminCreateDto, UserPreferencesUpdateDto, ValidateLibraryDto, - checkExistingAssets, createAlbum, createApiKey, createJob, @@ -343,8 +341,6 @@ export const utils = { }, ) => { const _dto = { - deviceAssetId: 'test-1', - deviceId: 'test', fileCreatedAt: new Date().toISOString(), fileModifiedAt: new Date().toISOString(), ...dto, @@ -375,40 +371,6 @@ export const utils = { return body as AssetMediaResponseDto; }, - replaceAsset: async ( - accessToken: string, - assetId: string, - dto?: Partial> & { assetData?: FileData }, - ) => { - const _dto = { - deviceAssetId: 'test-1', - deviceId: 'test', - fileCreatedAt: new Date().toISOString(), - fileModifiedAt: new Date().toISOString(), - ...dto, - }; - - const assetData = dto?.assetData?.bytes || makeRandomImage(); - const filename = dto?.assetData?.filename || 'example.png'; - - if (dto?.assetData?.bytes) { - console.log(`Uploading ${filename}`); - } - - const builder = request(app) - .put(`/assets/${assetId}/original`) - .attach('assetData', assetData, filename) - .set('Authorization', `Bearer ${accessToken}`); - - for (const [key, value] of Object.entries(_dto)) { - void builder.field(key, String(value)); - } - - const { body } = await builder; - - return body as AssetMediaResponseDto; - }, - createImageFile: (path: string) => { if (!existsSync(dirname(path))) { mkdirSync(dirname(path), { recursive: true }); @@ -450,9 +412,6 @@ export const utils = { getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }), - checkExistingAssets: (accessToken: string, checkExistingAssetsDto: CheckExistingAssetsDto) => - checkExistingAssets({ checkExistingAssetsDto }, { headers: asBearerAuth(accessToken) }), - searchAssets: async (accessToken: string, dto: MetadataSearchDto) => { return searchAssets({ metadataSearchDto: dto }, { headers: asBearerAuth(accessToken) }); }, diff --git a/i18n/en.json b/i18n/en.json index 4f2922f35d..351b97f00c 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1392,6 +1392,7 @@ "light_theme": "Switch to light theme", "like": "Like", "like_deleted": "Like deleted", + "link": "Link", "link_motion_video": "Link motion video", "link_to_docs": "For more information, refer to the documentation.", "link_to_oauth": "Link to OAuth", @@ -1562,6 +1563,8 @@ "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", "mute_memories": "Mute Memories", "my_albums": "My albums", + "my_immich_description": "Copy current page as a My Immich link", + "my_immich_title": "My Immich link", "name": "Name", "name_or_nickname": "Name or nickname", "name_required": "Name is required", @@ -1926,6 +1929,8 @@ "scan_settings": "Scan Settings", "scanning": "Scanning", "scanning_for_album": "Scanning for album...", + "screencast_mode_description": "Show keyboard and mouse event indicators on the screen", + "screencast_mode_title": "Toggle screencast mode", "search": "Search", "search_albums": "Search albums", "search_by_context": "Search by context", @@ -2214,6 +2219,8 @@ "sync_status": "Sync Status", "sync_status_subtitle": "View and manage the sync system", "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich", + "system_theme": "System theme", + "system_theme_command_description": "Use the system theme ({value})", "tag": "Tag", "tag_assets": "Tag assets", "tag_created": "Created tag: {tag}", diff --git a/machine-learning/uv.lock b/machine-learning/uv.lock index 099a22b118..67cff1fdd7 100644 --- a/machine-learning/uv.lock +++ b/machine-learning/uv.lock @@ -2271,11 +2271,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.22" +version = "0.0.26" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, + { url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" }, ] [[package]] diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/BackgroundServicePlugin.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/BackgroundServicePlugin.kt deleted file mode 100644 index f62f25558d..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/BackgroundServicePlugin.kt +++ /dev/null @@ -1,389 +0,0 @@ -package app.alextran.immich - -import android.app.Activity -import android.content.ContentResolver -import android.content.ContentUris -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.provider.MediaStore -import android.provider.Settings -import android.util.Log -import androidx.annotation.RequiresApi -import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.embedding.engine.plugins.activity.ActivityAware -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.Result -import io.flutter.plugin.common.PluginRegistry -import java.security.MessageDigest -import java.io.FileInputStream -import kotlinx.coroutines.* -import androidx.core.net.toUri - -/** - * Android plugin for Dart `BackgroundService` and file trash operations - */ -class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware, PluginRegistry.ActivityResultListener { - - private var methodChannel: MethodChannel? = null - private var fileTrashChannel: MethodChannel? = null - private var context: Context? = null - private var pendingResult: Result? = null - private val permissionRequestCode = 1001 - private val trashRequestCode = 1002 - private var activityBinding: ActivityPluginBinding? = null - - override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { - onAttachedToEngine(binding.applicationContext, binding.binaryMessenger) - } - - private fun onAttachedToEngine(ctx: Context, messenger: BinaryMessenger) { - context = ctx - methodChannel = MethodChannel(messenger, "immich/foregroundChannel") - methodChannel?.setMethodCallHandler(this) - - // Add file trash channel - fileTrashChannel = MethodChannel(messenger, "file_trash") - fileTrashChannel?.setMethodCallHandler(this) - } - - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - onDetachedFromEngine() - } - - private fun onDetachedFromEngine() { - methodChannel?.setMethodCallHandler(null) - methodChannel = null - fileTrashChannel?.setMethodCallHandler(null) - fileTrashChannel = null - } - - override fun onMethodCall(call: MethodCall, result: Result) { - val ctx = context!! - when (call.method) { - // Existing BackgroundService methods - "enable" -> { - val args = call.arguments>()!! - ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - .edit() - .putBoolean(ContentObserverWorker.SHARED_PREF_SERVICE_ENABLED, true) - .putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args[0] as Long) - .putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args[1] as String) - .apply() - ContentObserverWorker.enable(ctx, immediate = args[2] as Boolean) - result.success(true) - } - - "configure" -> { - val args = call.arguments>()!! - val requireUnmeteredNetwork = args[0] as Boolean - val requireCharging = args[1] as Boolean - val triggerUpdateDelay = (args[2] as Number).toLong() - val triggerMaxDelay = (args[3] as Number).toLong() - ContentObserverWorker.configureWork( - ctx, - requireUnmeteredNetwork, - requireCharging, - triggerUpdateDelay, - triggerMaxDelay - ) - result.success(true) - } - - "disable" -> { - ContentObserverWorker.disable(ctx) - BackupWorker.stopWork(ctx) - result.success(true) - } - - "isEnabled" -> { - result.success(ContentObserverWorker.isEnabled(ctx)) - } - - "isIgnoringBatteryOptimizations" -> { - result.success(BackupWorker.isIgnoringBatteryOptimizations(ctx)) - } - - "digestFiles" -> { - val args = call.arguments>()!! - GlobalScope.launch(Dispatchers.IO) { - val buf = ByteArray(BUFFER_SIZE) - val digest: MessageDigest = MessageDigest.getInstance("SHA-1") - val hashes = arrayOfNulls(args.size) - for (i in args.indices) { - val path = args[i] - var len = 0 - try { - val file = FileInputStream(path) - file.use { assetFile -> - while (true) { - len = assetFile.read(buf) - if (len != BUFFER_SIZE) break - digest.update(buf) - } - } - digest.update(buf, 0, len) - hashes[i] = digest.digest() - } catch (e: Exception) { - // skip this file - Log.w(TAG, "Failed to hash file ${args[i]}: $e") - } - } - result.success(hashes.asList()) - } - } - - // File Trash methods moved from MainActivity - "moveToTrash" -> { - val mediaUrls = call.argument>("mediaUrls") - if (mediaUrls != null) { - if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) && hasManageMediaPermission()) { - moveToTrash(mediaUrls, result) - } else { - result.error("PERMISSION_DENIED", "Media permission required", null) - } - } else { - result.error("INVALID_NAME", "The mediaUrls is not specified.", null) - } - } - - "restoreFromTrash" -> { - val fileName = call.argument("fileName") - val type = call.argument("type") - val mediaId = call.argument("mediaId") - if (fileName != null && type != null) { - if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) && hasManageMediaPermission()) { - restoreFromTrash(fileName, type, result) - } else { - result.error("PERMISSION_DENIED", "Media permission required", null) - } - } else - if (mediaId != null && type != null) { - if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) && hasManageMediaPermission()) { - restoreFromTrashById(mediaId, type, result) - } else { - result.error("PERMISSION_DENIED", "Media permission required", null) - } - } else { - result.error("INVALID_PARAMS", "Required params are not specified.", null) - } - } - - "requestManageMediaPermission" -> { - if (!hasManageMediaPermission()) { - requestManageMediaPermission(result) - } else { - Log.e("Manage storage permission", "Permission already granted") - result.success(true) - } - } - - "hasManageMediaPermission" -> { - if (hasManageMediaPermission()) { - Log.i("Manage storage permission", "Permission already granted") - result.success(true) - } else { - result.success(false) - } - } - - "manageMediaPermission" -> requestManageMediaPermission(result) - - else -> result.notImplemented() - } - } - - private fun hasManageMediaPermission(): Boolean { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - MediaStore.canManageMedia(context!!); - } else { - false - } - } - - private fun requestManageMediaPermission(result: Result) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - pendingResult = result // Store the result callback - val activity = activityBinding?.activity ?: return - - val intent = Intent(Settings.ACTION_REQUEST_MANAGE_MEDIA) - intent.data = "package:${activity.packageName}".toUri() - activity.startActivityForResult(intent, permissionRequestCode) - } else { - result.success(false) - } - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun moveToTrash(mediaUrls: List, result: Result) { - val urisToTrash = mediaUrls.map { it.toUri() } - if (urisToTrash.isEmpty()) { - result.error("INVALID_ARGS", "No valid URIs provided", null) - return - } - - toggleTrash(urisToTrash, true, result); - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun restoreFromTrash(name: String, type: Int, result: Result) { - val uri = getTrashedFileUri(name, type) - if (uri == null) { - Log.e("TrashError", "Asset Uri cannot be found obtained") - result.error("TrashError", "Asset Uri cannot be found obtained", null) - return - } - Log.e("FILE_URI", uri.toString()) - uri.let { toggleTrash(listOf(it), false, result) } - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun restoreFromTrashById(mediaId: String, type: Int, result: Result) { - val id = mediaId.toLongOrNull() - if (id == null) { - result.error("INVALID_ID", "The file id is not a valid number: $mediaId", null) - return - } - if (!isInTrash(id)) { - result.error("TrashNotFound", "Item with id=$id not found in trash", null) - return - } - - val uri = ContentUris.withAppendedId(contentUriForType(type), id) - - try { - Log.i(TAG, "restoreFromTrashById: uri=$uri (type=$type,id=$id)") - restoreUris(listOf(uri), result) - } catch (e: Exception) { - Log.w(TAG, "restoreFromTrashById failed", e) - } - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun toggleTrash(contentUris: List, isTrashed: Boolean, result: Result) { - val activity = activityBinding?.activity - val contentResolver = context?.contentResolver - if (activity == null || contentResolver == null) { - result.error("TrashError", "Activity or ContentResolver not available", null) - return - } - - try { - val pendingIntent = MediaStore.createTrashRequest(contentResolver, contentUris, isTrashed) - pendingResult = result // Store for onActivityResult - activity.startIntentSenderForResult( - pendingIntent.intentSender, - trashRequestCode, - null, 0, 0, 0 - ) - } catch (e: Exception) { - Log.e("TrashError", "Error creating or starting trash request", e) - result.error("TrashError", "Error creating or starting trash request", null) - } - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun getTrashedFileUri(fileName: String, type: Int): Uri? { - val contentResolver = context?.contentResolver ?: return null - val queryUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL) - val projection = arrayOf(MediaStore.Files.FileColumns._ID) - - val queryArgs = Bundle().apply { - putString( - ContentResolver.QUERY_ARG_SQL_SELECTION, - "${MediaStore.Files.FileColumns.DISPLAY_NAME} = ?" - ) - putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(fileName)) - putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY) - } - - contentResolver.query(queryUri, projection, queryArgs, null)?.use { cursor -> - if (cursor.moveToFirst()) { - val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)) - return ContentUris.withAppendedId(contentUriForType(type), id) - } - } - return null - } - - // ActivityAware implementation - override fun onAttachedToActivity(binding: ActivityPluginBinding) { - activityBinding = binding - binding.addActivityResultListener(this) - } - - override fun onDetachedFromActivityForConfigChanges() { - activityBinding?.removeActivityResultListener(this) - activityBinding = null - } - - override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { - activityBinding = binding - binding.addActivityResultListener(this) - } - - override fun onDetachedFromActivity() { - activityBinding?.removeActivityResultListener(this) - activityBinding = null - } - - // ActivityResultListener implementation - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { - if (requestCode == permissionRequestCode) { - val granted = hasManageMediaPermission() - pendingResult?.success(granted) - pendingResult = null - return true - } - - if (requestCode == trashRequestCode) { - val approved = resultCode == Activity.RESULT_OK - pendingResult?.success(approved) - pendingResult = null - return true - } - return false - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun isInTrash(id: Long): Boolean { - val contentResolver = context?.contentResolver ?: return false - val filesUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL) - val args = Bundle().apply { - putString(ContentResolver.QUERY_ARG_SQL_SELECTION, "${MediaStore.Files.FileColumns._ID}=?") - putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(id.toString())) - putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY) - putInt(ContentResolver.QUERY_ARG_LIMIT, 1) - } - return contentResolver.query(filesUri, arrayOf(MediaStore.Files.FileColumns._ID), args, null) - ?.use { it.moveToFirst() } == true - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun restoreUris(uris: List, result: Result) { - if (uris.isEmpty()) { - result.error("TrashError", "No URIs to restore", null) - return - } - Log.i(TAG, "restoreUris: count=${uris.size}, first=${uris.first()}") - toggleTrash(uris, false, result) - } - - @RequiresApi(Build.VERSION_CODES.Q) - private fun contentUriForType(type: Int): Uri = - when (type) { - // same order as AssetType from dart - 1 -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI - 2 -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI - 3 -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI - else -> MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL) - } -} - -private const val TAG = "BackgroundServicePlugin" -private const val BUFFER_SIZE = 2 * 1024 * 1024 diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/BackupWorker.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/BackupWorker.kt deleted file mode 100644 index 9c90528dc9..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/BackupWorker.kt +++ /dev/null @@ -1,394 +0,0 @@ -package app.alextran.immich - -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.content.Context -import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE -import android.os.Build -import android.os.Handler -import android.os.Looper -import android.os.PowerManager -import android.os.SystemClock -import android.util.Log -import androidx.annotation.RequiresApi -import androidx.core.app.NotificationCompat -import androidx.concurrent.futures.ResolvableFuture -import androidx.work.BackoffPolicy -import androidx.work.Constraints -import androidx.work.ForegroundInfo -import androidx.work.ListenableWorker -import androidx.work.NetworkType -import androidx.work.WorkerParameters -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequest -import androidx.work.WorkManager -import androidx.work.WorkInfo -import com.google.common.util.concurrent.ListenableFuture -import io.flutter.embedding.engine.FlutterEngine -import io.flutter.embedding.engine.dart.DartExecutor -import io.flutter.embedding.engine.loader.FlutterLoader -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.view.FlutterCallbackInformation -import java.util.concurrent.TimeUnit - -/** - * Worker executed by Android WorkManager to perform backup in background - * - * Starts the Dart runtime/engine and calls `_nativeEntry` function in - * `background.service.dart` to run the actual backup logic. - * Called by Android WorkManager when all constraints for the work are met, - * i.e. battery is not low and optionally Wifi and charging are active. - */ -class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ctx, params), - MethodChannel.MethodCallHandler { - - private val resolvableFuture = ResolvableFuture.create() - private var engine: FlutterEngine? = null - private lateinit var backgroundChannel: MethodChannel - private val notificationManager = - ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - private val isIgnoringBatteryOptimizations = isIgnoringBatteryOptimizations(applicationContext) - private var timeBackupStarted: Long = 0L - private var notificationBuilder: NotificationCompat.Builder? = null - private var notificationDetailBuilder: NotificationCompat.Builder? = null - private var fgFuture: ListenableFuture? = null - - override fun startWork(): ListenableFuture { - - Log.d(TAG, "startWork") - - val ctx = applicationContext - - if (!flutterLoader.initialized()) { - flutterLoader.startInitialization(ctx) - } - - // Create a Notification channel - createChannel() - - Log.d(TAG, "isIgnoringBatteryOptimizations $isIgnoringBatteryOptimizations") - if (isIgnoringBatteryOptimizations) { - // normal background services can only up to 10 minutes - // foreground services are allowed to run indefinitely - // requires battery optimizations to be disabled (either manually by the user - // or by the system learning that immich is important to the user) - val title = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) - .getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!! - showInfo(getInfoBuilder(title, indeterminate = true).build()) - } - - engine = FlutterEngine(ctx) - - flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) { - runDart() - - } - - return resolvableFuture - } - - /** - * Starts the Dart runtime/engine and calls `_nativeEntry` function in - * `background.service.dart` to run the actual backup logic. - */ - private fun runDart() { - val callbackDispatcherHandle = applicationContext.getSharedPreferences( - SHARED_PREF_NAME, Context.MODE_PRIVATE - ).getLong(SHARED_PREF_CALLBACK_KEY, 0L) - val callbackInformation = - FlutterCallbackInformation.lookupCallbackInformation(callbackDispatcherHandle) - val appBundlePath = flutterLoader.findAppBundlePath() - - engine?.let { engine -> - backgroundChannel = MethodChannel(engine.dartExecutor, "immich/backgroundChannel") - backgroundChannel.setMethodCallHandler(this@BackupWorker) - engine.dartExecutor.executeDartCallback( - DartExecutor.DartCallback( - applicationContext.assets, - appBundlePath, - callbackInformation - ) - ) - } - } - - override fun onStopped() { - Log.d(TAG, "onStopped") - // called when the system has to stop this worker because constraints are - // no longer met or the system needs resources for more important tasks - Handler(Looper.getMainLooper()).postAtFrontOfQueue { - if (::backgroundChannel.isInitialized) { - backgroundChannel.invokeMethod("systemStop", null) - } - } - waitOnSetForegroundAsync() - // cannot await/get(block) on resolvableFuture as its already cancelled (would throw CancellationException) - // instead, wait for 5 seconds until forcefully stopping backup work - Handler(Looper.getMainLooper()).postDelayed({ - stopEngine(null) - }, 5000) - } - - private fun waitOnSetForegroundAsync() { - val fgFuture = this.fgFuture - if (fgFuture != null && !fgFuture.isCancelled && !fgFuture.isDone) { - try { - fgFuture.get(500, TimeUnit.MILLISECONDS) - } catch (e: Exception) { - // ignored, there is nothing to be done - } - } - } - - private fun stopEngine(result: Result?) { - clearBackgroundNotification() - engine?.destroy() - engine = null - if (result != null) { - Log.d(TAG, "stopEngine result=${result}") - resolvableFuture.set(result) - } - waitOnSetForegroundAsync() - } - - @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) - override fun onMethodCall(call: MethodCall, r: MethodChannel.Result) { - when (call.method) { - "initialized" -> { - timeBackupStarted = SystemClock.uptimeMillis() - backgroundChannel.invokeMethod( - "onAssetsChanged", - null, - object : MethodChannel.Result { - override fun notImplemented() { - stopEngine(Result.failure()) - } - - override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { - stopEngine(Result.failure()) - } - - override fun success(receivedResult: Any?) { - val success = receivedResult as Boolean - stopEngine(if (success) Result.success() else Result.retry()) - } - } - ) - } - - "updateNotification" -> { - val args = call.arguments>()!! - val title = args[0] as String? - val content = args[1] as String? - val progress = args[2] as Int - val max = args[3] as Int - val indeterminate = args[4] as Boolean - val isDetail = args[5] as Boolean - val onlyIfFG = args[6] as Boolean - if (!onlyIfFG || isIgnoringBatteryOptimizations) { - showInfo( - getInfoBuilder(title, content, isDetail, progress, max, indeterminate).build(), - isDetail - ) - } - } - - "showError" -> { - val args = call.arguments>()!! - val title = args[0] as String - val content = args[1] as String? - val individualTag = args[2] as String? - showError(title, content, individualTag) - } - - "clearErrorNotifications" -> clearErrorNotifications() - "hasContentChanged" -> { - val lastChange = applicationContext - .getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) - .getLong(SHARED_PREF_LAST_CHANGE, timeBackupStarted) - val hasContentChanged = lastChange > timeBackupStarted; - timeBackupStarted = SystemClock.uptimeMillis() - r.success(hasContentChanged) - } - - else -> r.notImplemented() - } - } - - private fun showError(title: String, content: String?, individualTag: String?) { - val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ERROR_ID) - .setContentTitle(title) - .setTicker(title) - .setContentText(content) - .setSmallIcon(R.drawable.notification_icon) - .build() - notificationManager.notify(individualTag, NOTIFICATION_ERROR_ID, notification) - } - - private fun clearErrorNotifications() { - notificationManager.cancel(NOTIFICATION_ERROR_ID) - } - - private fun clearBackgroundNotification() { - notificationManager.cancel(NOTIFICATION_ID) - notificationManager.cancel(NOTIFICATION_DETAIL_ID) - } - - private fun showInfo(notification: Notification, isDetail: Boolean = false) { - val id = if (isDetail) NOTIFICATION_DETAIL_ID else NOTIFICATION_ID - - if (isIgnoringBatteryOptimizations && !isDetail) { - fgFuture = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - setForegroundAsync(ForegroundInfo(id, notification, FOREGROUND_SERVICE_TYPE_SHORT_SERVICE)) - } else { - setForegroundAsync(ForegroundInfo(id, notification)) - } - } else { - notificationManager.notify(id, notification) - } - } - - private fun getInfoBuilder( - title: String? = null, - content: String? = null, - isDetail: Boolean = false, - progress: Int = 0, - max: Int = 0, - indeterminate: Boolean = false, - ): NotificationCompat.Builder { - var builder = if (isDetail) notificationDetailBuilder else notificationBuilder - if (builder == null) { - builder = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID) - .setSmallIcon(R.drawable.notification_icon) - .setOnlyAlertOnce(true) - .setOngoing(true) - if (isDetail) { - notificationDetailBuilder = builder - } else { - notificationBuilder = builder - } - } - if (title != null) { - builder.setTicker(title).setContentTitle(title) - } - if (content != null) { - builder.setContentText(content) - } - return builder.setProgress(max, progress, indeterminate) - } - - private fun createChannel() { - val foreground = NotificationChannel( - NOTIFICATION_CHANNEL_ID, - NOTIFICATION_CHANNEL_ID, - NotificationManager.IMPORTANCE_LOW - ) - notificationManager.createNotificationChannel(foreground) - val error = NotificationChannel( - NOTIFICATION_CHANNEL_ERROR_ID, - NOTIFICATION_CHANNEL_ERROR_ID, - NotificationManager.IMPORTANCE_HIGH - ) - notificationManager.createNotificationChannel(error) - } - - companion object { - const val SHARED_PREF_NAME = "immichBackgroundService" - const val SHARED_PREF_CALLBACK_KEY = "callbackDispatcherHandle" - const val SHARED_PREF_NOTIFICATION_TITLE = "notificationTitle" - const val SHARED_PREF_LAST_CHANGE = "lastChange" - - private const val TASK_NAME_BACKUP = "immich/BackupWorker" - private const val NOTIFICATION_CHANNEL_ID = "immich/backgroundService" - private const val NOTIFICATION_CHANNEL_ERROR_ID = "immich/backgroundServiceError" - private const val NOTIFICATION_DEFAULT_TITLE = "Immich" - private const val NOTIFICATION_ID = 1 - private const val NOTIFICATION_ERROR_ID = 2 - private const val NOTIFICATION_DETAIL_ID = 3 - private const val ONE_MINUTE = 60000L - - /** - * Enqueues the BackupWorker to run once the constraints are met - */ - fun enqueueBackupWorker( - context: Context, - requireWifi: Boolean = false, - requireCharging: Boolean = false, - delayMilliseconds: Long = 0L - ) { - val workRequest = buildWorkRequest(requireWifi, requireCharging, delayMilliseconds) - WorkManager.getInstance(context) - .enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.KEEP, workRequest) - Log.d(TAG, "enqueueBackupWorker: BackupWorker enqueued") - } - - /** - * Updates the constraints of an already enqueued BackupWorker - */ - fun updateBackupWorker( - context: Context, - requireWifi: Boolean = false, - requireCharging: Boolean = false - ) { - try { - val wm = WorkManager.getInstance(context) - val workInfoFuture = wm.getWorkInfosForUniqueWork(TASK_NAME_BACKUP) - val workInfoList = workInfoFuture.get(1000, TimeUnit.MILLISECONDS) - if (workInfoList != null) { - for (workInfo in workInfoList) { - if (workInfo.state == WorkInfo.State.ENQUEUED) { - val workRequest = buildWorkRequest(requireWifi, requireCharging) - wm.enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.REPLACE, workRequest) - Log.d(TAG, "updateBackupWorker updated BackupWorker constraints") - return - } - } - } - Log.d(TAG, "updateBackupWorker: BackupWorker not enqueued") - } catch (e: Exception) { - Log.d(TAG, "updateBackupWorker failed: $e") - } - } - - /** - * Stops the currently running worker (if any) and removes it from the work queue - */ - fun stopWork(context: Context) { - WorkManager.getInstance(context).cancelUniqueWork(TASK_NAME_BACKUP) - Log.d(TAG, "stopWork: BackupWorker cancelled") - } - - /** - * Returns `true` if the app is ignoring battery optimizations - */ - fun isIgnoringBatteryOptimizations(ctx: Context): Boolean { - val powerManager = ctx.getSystemService(Context.POWER_SERVICE) as PowerManager - return powerManager.isIgnoringBatteryOptimizations(ctx.packageName) - } - - private fun buildWorkRequest( - requireWifi: Boolean = false, - requireCharging: Boolean = false, - delayMilliseconds: Long = 0L - ): OneTimeWorkRequest { - val constraints = Constraints.Builder() - .setRequiredNetworkType(if (requireWifi) NetworkType.UNMETERED else NetworkType.CONNECTED) - .setRequiresBatteryNotLow(true) - .setRequiresCharging(requireCharging) - .build(); - - val work = OneTimeWorkRequest.Builder(BackupWorker::class.java) - .setConstraints(constraints) - .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, ONE_MINUTE, TimeUnit.MILLISECONDS) - .setInitialDelay(delayMilliseconds, TimeUnit.MILLISECONDS) - .build() - return work - } - - private val flutterLoader = FlutterLoader() - } -} - -private const val TAG = "BackupWorker" diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/ContentObserverWorker.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/ContentObserverWorker.kt deleted file mode 100644 index 9cb2ec7779..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/ContentObserverWorker.kt +++ /dev/null @@ -1,144 +0,0 @@ -package app.alextran.immich - -import android.content.Context -import android.os.SystemClock -import android.provider.MediaStore -import android.util.Log -import androidx.work.Constraints -import androidx.work.Worker -import androidx.work.WorkerParameters -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequest -import androidx.work.WorkManager -import androidx.work.Operation -import java.util.concurrent.TimeUnit - -/** - * Worker executed by Android WorkManager observing content changes (new photos/videos) - * - * Immediately enqueues the BackupWorker when running. - * As this work is not triggered periodically, but on content change, the - * worker enqueues itself again after each run. - */ -class ContentObserverWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) { - - override fun doWork(): Result { - if (!isEnabled(applicationContext)) { - return Result.failure() - } - if (triggeredContentUris.size > 0) { - startBackupWorker(applicationContext, delayMilliseconds = 0) - } - enqueueObserverWorker(applicationContext, ExistingWorkPolicy.REPLACE) - return Result.success() - } - - companion object { - const val SHARED_PREF_SERVICE_ENABLED = "serviceEnabled" - private const val SHARED_PREF_REQUIRE_WIFI = "requireWifi" - private const val SHARED_PREF_REQUIRE_CHARGING = "requireCharging" - private const val SHARED_PREF_TRIGGER_UPDATE_DELAY = "triggerUpdateDelay" - private const val SHARED_PREF_TRIGGER_MAX_DELAY = "triggerMaxDelay" - - private const val TASK_NAME_OBSERVER = "immich/ContentObserver" - - /** - * Enqueues the `ContentObserverWorker`. - * - * @param context Android Context - */ - fun enable(context: Context, immediate: Boolean = false) { - enqueueObserverWorker(context, ExistingWorkPolicy.KEEP) - Log.d(TAG, "enabled ContentObserverWorker") - if (immediate) { - startBackupWorker(context, delayMilliseconds = 5000) - } - } - - /** - * Configures the `BackupWorker` to run when all constraints are met. - * - * @param context Android Context - * @param requireWifi if true, task only runs if connected to wifi - * @param requireCharging if true, task only runs if device is charging - */ - fun configureWork(context: Context, - requireWifi: Boolean = false, - requireCharging: Boolean = false, - triggerUpdateDelay: Long = 5000, - triggerMaxDelay: Long = 50000) { - context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - .edit() - .putBoolean(SHARED_PREF_SERVICE_ENABLED, true) - .putBoolean(SHARED_PREF_REQUIRE_WIFI, requireWifi) - .putBoolean(SHARED_PREF_REQUIRE_CHARGING, requireCharging) - .putLong(SHARED_PREF_TRIGGER_UPDATE_DELAY, triggerUpdateDelay) - .putLong(SHARED_PREF_TRIGGER_MAX_DELAY, triggerMaxDelay) - .apply() - BackupWorker.updateBackupWorker(context, requireWifi, requireCharging) - } - - /** - * Stops the currently running worker (if any) and removes it from the work queue - */ - fun disable(context: Context) { - context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - .edit().putBoolean(SHARED_PREF_SERVICE_ENABLED, false).apply() - WorkManager.getInstance(context).cancelUniqueWork(TASK_NAME_OBSERVER) - Log.d(TAG, "disabled ContentObserverWorker") - } - - /** - * Return true if the user has enabled the background backup service - */ - fun isEnabled(ctx: Context): Boolean { - return ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - .getBoolean(SHARED_PREF_SERVICE_ENABLED, false) - } - - /** - * Enqueue and replace the worker without the content trigger but with a short delay - */ - fun workManagerAppClearedWorkaround(context: Context) { - val work = OneTimeWorkRequest.Builder(ContentObserverWorker::class.java) - .setInitialDelay(500, TimeUnit.MILLISECONDS) - .build() - WorkManager - .getInstance(context) - .enqueueUniqueWork(TASK_NAME_OBSERVER, ExistingWorkPolicy.REPLACE, work) - .result - .get() - Log.d(TAG, "workManagerAppClearedWorkaround") - } - - private fun enqueueObserverWorker(context: Context, policy: ExistingWorkPolicy) { - val sp = context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - val constraints = Constraints.Builder() - .addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true) - .addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true) - .addContentUriTrigger(MediaStore.Video.Media.INTERNAL_CONTENT_URI, true) - .addContentUriTrigger(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true) - .setTriggerContentUpdateDelay(sp.getLong(SHARED_PREF_TRIGGER_UPDATE_DELAY, 5000), TimeUnit.MILLISECONDS) - .setTriggerContentMaxDelay(sp.getLong(SHARED_PREF_TRIGGER_MAX_DELAY, 50000), TimeUnit.MILLISECONDS) - .build() - - val work = OneTimeWorkRequest.Builder(ContentObserverWorker::class.java) - .setConstraints(constraints) - .build() - WorkManager.getInstance(context).enqueueUniqueWork(TASK_NAME_OBSERVER, policy, work) - } - - fun startBackupWorker(context: Context, delayMilliseconds: Long) { - val sp = context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - if (!sp.getBoolean(SHARED_PREF_SERVICE_ENABLED, false)) - return - val requireWifi = sp.getBoolean(SHARED_PREF_REQUIRE_WIFI, true) - val requireCharging = sp.getBoolean(SHARED_PREF_REQUIRE_CHARGING, false) - BackupWorker.enqueueBackupWorker(context, requireWifi, requireCharging, delayMilliseconds) - sp.edit().putLong(BackupWorker.SHARED_PREF_LAST_CHANGE, SystemClock.uptimeMillis()).apply() - } - - } -} - -private const val TAG = "ContentObserverWorker" diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/ImmichApp.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/ImmichApp.kt index 4474c63e09..37a325e896 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/ImmichApp.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/ImmichApp.kt @@ -18,8 +18,6 @@ class ImmichApp : Application() { // Thus, the BackupWorker is not started. If the system kills the process after each initialization // (because of low memory etc.), the backup is never performed. // As a workaround, we also run a backup check when initializing the application - - ContentObserverWorker.startBackupWorker(context = this, delayMilliseconds = 0) Handler(Looper.getMainLooper()).postDelayed({ // We can only check the engine count and not the status of the lock here, // as the previous start might have been killed without unlocking. diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt index 06649de8f0..2c80b8d2bd 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt @@ -51,7 +51,6 @@ class MainActivity : FlutterFragmentActivity() { BackgroundWorkerFgHostApi.setUp(messenger, BackgroundWorkerApiImpl(ctx)) ConnectivityApi.setUp(messenger, ConnectivityApiImpl(ctx)) - flutterEngine.plugins.add(BackgroundServicePlugin()) flutterEngine.plugins.add(backgroundEngineLockImpl) flutterEngine.plugins.add(nativeSyncApiImpl) } diff --git a/mobile/integration_test/test_utils/general_helper.dart b/mobile/integration_test/test_utils/general_helper.dart index d6065170ef..66955364f3 100644 --- a/mobile/integration_test/test_utils/general_helper.dart +++ b/mobile/integration_test/test_utils/general_helper.dart @@ -5,7 +5,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/main.dart' as app; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; import 'package:integration_test/integration_test.dart'; @@ -39,20 +38,11 @@ class ImmichTestHelper { static Future loadApp(WidgetTester tester) async { await EasyLocalization.ensureInitialized(); // Clear all data from Isar (reuse existing instance if available) - final (isar, drift, logDb) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDb); + final (drift, _) = await Bootstrap.initDomain(); await Store.clear(); - await isar.writeTxn(() => isar.clear()); // Load main Widget await tester.pumpWidget( - ProviderScope( - overrides: [ - dbProvider.overrideWithValue(isar), - isarProvider.overrideWithValue(isar), - driftProvider.overrideWith(driftOverride(drift)), - ], - child: const app.MainWidget(), - ), + ProviderScope(overrides: [driftProvider.overrideWith(driftOverride(drift))], child: const app.MainWidget()), ); // Post run tasks await EasyLocalization.ensureInitialized(); diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index e1ec4aff07..c0d7e2c35a 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -33,8 +33,6 @@ PODS: - Flutter - integration_test (0.0.1): - Flutter - - isar_community_flutter_libs (1.0.0): - - Flutter - local_auth_darwin (0.0.1): - Flutter - FlutterMacOS @@ -75,16 +73,16 @@ PODS: - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS - - sqlite3 (3.49.1): - - sqlite3/common (= 3.49.1) - - sqlite3/common (3.49.1) - - sqlite3/dbstatvtab (3.49.1): + - sqlite3 (3.49.2): + - sqlite3/common (= 3.49.2) + - sqlite3/common (3.49.2) + - sqlite3/dbstatvtab (3.49.2): - sqlite3/common - - sqlite3/fts5 (3.49.1): + - sqlite3/fts5 (3.49.2): - sqlite3/common - - sqlite3/perf-threadsafe (3.49.1): + - sqlite3/perf-threadsafe (3.49.2): - sqlite3/common - - sqlite3/rtree (3.49.1): + - sqlite3/rtree (3.49.2): - sqlite3/common - sqlite3_flutter_libs (0.0.1): - Flutter @@ -116,7 +114,6 @@ DEPENDENCIES: - home_widget (from `.symlinks/plugins/home_widget/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - - isar_community_flutter_libs (from `.symlinks/plugins/isar_community_flutter_libs/ios`) - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) - maplibre_gl (from `.symlinks/plugins/maplibre_gl/ios`) - native_video_player (from `.symlinks/plugins/native_video_player/ios`) @@ -174,8 +171,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/image_picker_ios/ios" integration_test: :path: ".symlinks/plugins/integration_test/ios" - isar_community_flutter_libs: - :path: ".symlinks/plugins/isar_community_flutter_libs/ios" local_auth_darwin: :path: ".symlinks/plugins/local_auth_darwin/darwin" maplibre_gl: @@ -228,7 +223,6 @@ SPEC CHECKSUMS: home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e - isar_community_flutter_libs: bede843185a61a05ff364a05c9b23209523f7e0d local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391 MapLibre: 69e572367f4ef6287e18246cfafc39c80cdcabcd maplibre_gl: 3c924e44725147b03dda33430ad216005b40555f @@ -245,7 +239,7 @@ SPEC CHECKSUMS: share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 - sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983 + sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1 sqlite3_flutter_libs: f8fc13346870e73fe35ebf6dbb997fbcd156b241 url_launcher_ios: 694010445543906933d732453a59da0a173ae33d wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 178454f381..f88d624b89 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -10,8 +10,6 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B6A31FED0FC846D6BD69BBC /* Pods_ShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */; }; - 65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */; }; - 65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -90,8 +88,6 @@ 357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 571EAA93D77181C7C98C2EA6 /* Pods-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.release.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.release.xcconfig"; sourceTree = ""; }; - 65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundServicePlugin.swift; sourceTree = ""; }; - 65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundSyncWorker.swift; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -151,11 +147,15 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ B231F52D2E93A44A00BC45D1 /* Core */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = Core; sourceTree = ""; }; B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = Sync; sourceTree = ""; }; @@ -177,6 +177,8 @@ }; FEE084F22EC172080045228E /* Schemas */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = Schemas; sourceTree = ""; }; @@ -238,15 +240,6 @@ name = Frameworks; sourceTree = ""; }; - 65DD438629917FAD0047FFA8 /* BackgroundSync */ = { - isa = PBXGroup; - children = ( - 65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */, - 65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */, - ); - path = BackgroundSync; - sourceTree = ""; - }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -291,7 +284,6 @@ B21E34A62E5AF9760031FDB9 /* Background */, B2CF7F8C2DDE4EBB00744BF6 /* Sync */, FA9973382CF6DF4B000EF859 /* Runner.entitlements */, - 65DD438629917FAD0047FFA8 /* BackgroundSync */, FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, @@ -571,14 +563,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; @@ -607,14 +595,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; @@ -627,7 +611,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, A01DD69B2F7F43B40049AB63 /* ImageRequest.swift in Sources */, B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */, @@ -642,7 +625,6 @@ B21E34AA2E5AFD2B0031FDB9 /* BackgroundWorkerApiImpl.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */, - 65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1261,7 +1243,7 @@ repositoryURL = "https://github.com/pointfreeco/sqlite-data"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.3.0; + minimumVersion = 1.6.1; }; }; FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */ = { @@ -1269,7 +1251,7 @@ repositoryURL = "https://github.com/apple/swift-http-structured-headers.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.5.0; + minimumVersion = 1.6.0; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4962230c22..800ff8ac52 100644 --- a/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -24,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/sqlite-data", "state" : { - "revision" : "05704b563ecb7f0bd7e49b6f360a6383a3e53e7d", - "version" : "1.5.1" + "revision" : "da3a94ed49c7a30d82853de551c07a93196e8cab", + "version" : "1.6.1" } }, { @@ -78,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-http-structured-headers.git", "state" : { - "revision" : "a9f3c352f4d46afd155e00b3c6e85decae6bcbeb", - "version" : "1.5.0" + "revision" : "76d7627bd88b47bf5a0f8497dd244885960dde0b", + "version" : "1.6.0" } }, { @@ -123,8 +123,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { - "revision" : "d8163b3a98f3c8434c4361e85126db449d84bc66", - "version" : "0.30.0" + "revision" : "8da8818fccd9959bd683934ddc62cf45bb65b3c8", + "version" : "0.31.1" } }, { diff --git a/mobile/ios/Runner/AppDelegate.swift b/mobile/ios/Runner/AppDelegate.swift index 81af41ab08..2d41fd541e 100644 --- a/mobile/ios/Runner/AppDelegate.swift +++ b/mobile/ios/Runner/AppDelegate.swift @@ -24,33 +24,8 @@ import UIKit GeneratedPluginRegistrant.register(with: self) let controller: FlutterViewController = window?.rootViewController as! FlutterViewController AppDelegate.registerPlugins(with: controller.engine, controller: controller) - BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!) - - BackgroundServicePlugin.registerBackgroundProcessing() BackgroundWorkerApiImpl.registerBackgroundWorkers() - BackgroundServicePlugin.setPluginRegistrantCallback { registry in - if !registry.hasPlugin("org.cocoapods.path-provider-foundation") { - PathProviderPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.path-provider-foundation")!) - } - - if !registry.hasPlugin("org.cocoapods.photo-manager") { - PhotoManagerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.photo-manager")!) - } - - if !registry.hasPlugin("org.cocoapods.shared-preferences-foundation") { - SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.shared-preferences-foundation")!) - } - - if !registry.hasPlugin("org.cocoapods.permission-handler-apple") { - PermissionHandlerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.permission-handler-apple")!) - } - - if !registry.hasPlugin("org.cocoapods.network-info-plus") { - FPPNetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.network-info-plus")!) - } - } - return super.application(application, didFinishLaunchingWithOptions: launchOptions) } diff --git a/mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift b/mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift deleted file mode 100644 index cac9faab01..0000000000 --- a/mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift +++ /dev/null @@ -1,408 +0,0 @@ -// -// BackgroundServicePlugin.swift -// Runner -// -// Created by Marty Fuhry on 2/14/23. -// - -import Flutter -import BackgroundTasks -import path_provider_foundation -import CryptoKit -import Network - -class BackgroundServicePlugin: NSObject, FlutterPlugin { - - public static var flutterPluginRegistrantCallback: FlutterPluginRegistrantCallback? - - public static func setPluginRegistrantCallback(_ callback: FlutterPluginRegistrantCallback) { - flutterPluginRegistrantCallback = callback - } - - // Pause the application in XCode, then enter - // e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.backgroundFetch"] - // or - // e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.backgroundProcessing"] - // Then resume the application see the background code run - // Tested on a physical device, not a simulator - // This will submit either the Fetch or Processing command to the BGTaskScheduler for immediate processing. - // In my tests, I can only get app.alextran.immich.backgroundProcessing simulated by running the above command - - // This is the task ID in Info.plist to register as our background task ID - public static let backgroundFetchTaskID = "app.alextran.immich.backgroundFetch" - public static let backgroundProcessingTaskID = "app.alextran.immich.backgroundProcessing" - - // Establish communication with the main isolate and set up the channel call - // to this BackgroundServicePlugion() - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel( - name: "immich/foregroundChannel", - binaryMessenger: registrar.messenger() - ) - - let instance = BackgroundServicePlugin() - registrar.addMethodCallDelegate(instance, channel: channel) - registrar.addApplicationDelegate(instance) - } - - // Registers the Flutter engine with the plugins, used by the other Background Flutter engine - public static func register(engine: FlutterEngine) { - GeneratedPluginRegistrant.register(with: engine) - } - - // Registers the task IDs from the system so that we can process them here in this class - public static func registerBackgroundProcessing() { - - let processingRegisterd = BGTaskScheduler.shared.register( - forTaskWithIdentifier: backgroundProcessingTaskID, - using: nil) { task in - if task is BGProcessingTask { - handleBackgroundProcessing(task: task as! BGProcessingTask) - } - } - - let fetchRegisterd = BGTaskScheduler.shared.register( - forTaskWithIdentifier: backgroundFetchTaskID, - using: nil) { task in - if task is BGAppRefreshTask { - handleBackgroundFetch(task: task as! BGAppRefreshTask) - } - } - } - - // Handles the channel methods from Flutter - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "enable": - handleBackgroundEnable(call: call, result: result) - break - case "configure": - handleConfigure(call: call, result: result) - break - case "disable": - handleDisable(call: call, result: result) - break - case "isEnabled": - handleIsEnabled(call: call, result: result) - break - case "isIgnoringBatteryOptimizations": - result(FlutterMethodNotImplemented) - break - case "lastBackgroundFetchTime": - let defaults = UserDefaults.standard - let lastRunTime = defaults.value(forKey: "last_background_fetch_run_time") - result(lastRunTime) - break - case "lastBackgroundProcessingTime": - let defaults = UserDefaults.standard - let lastRunTime = defaults.value(forKey: "last_background_processing_run_time") - result(lastRunTime) - break - case "numberOfBackgroundProcesses": - handleNumberOfProcesses(call: call, result: result) - break - case "backgroundAppRefreshEnabled": - handleBackgroundRefreshStatus(call: call, result: result) - break - case "digestFiles": - handleDigestFiles(call: call, result: result) - break - default: - result(FlutterMethodNotImplemented) - break - } - } - - // Calculates the SHA-1 hash of each file from the list of paths provided - func handleDigestFiles(call: FlutterMethodCall, result: @escaping FlutterResult) { - - let bufsize = 2 * 1024 * 1024 - // Private error to throw if file cannot be read - enum DigestError: String, LocalizedError { - case NoFileHandle = "Cannot Open File Handle" - - public var errorDescription: String? { self.rawValue } - } - - // Parse the arguments or else fail - guard let args = call.arguments as? Array else { - print("Cannot parse args as array: \(String(describing: call.arguments))") - result(FlutterError(code: "Malformed", - message: "Received args is not an Array", - details: nil)) - return - } - - // Compute hash in background thread - DispatchQueue.global(qos: .background).async { - var hashes: [FlutterStandardTypedData?] = Array(repeating: nil, count: args.count) - for i in (0 ..< args.count) { - do { - guard let file = FileHandle(forReadingAtPath: args[i]) else { throw DigestError.NoFileHandle } - var hasher = Insecure.SHA1.init(); - while autoreleasepool(invoking: { - let chunk = file.readData(ofLength: bufsize) - guard !chunk.isEmpty else { return false } // EOF - hasher.update(data: chunk) - return true // continue - }) { } - let digest = hasher.finalize() - hashes[i] = FlutterStandardTypedData(bytes: Data(Array(digest.makeIterator()))) - } catch { - print("Cannot calculate the digest of the file \(args[i]) due to \(error.localizedDescription)") - } - } - - // Return result in main thread - DispatchQueue.main.async { - result(Array(hashes)) - } - } - } - - // Called by the flutter code when enabled so that we can turn on the background services - // and save the callback information to communicate on this method channel - public func handleBackgroundEnable(call: FlutterMethodCall, result: FlutterResult) { - - // Needs to parse the arguments from the method call - guard let args = call.arguments as? Array else { - print("Cannot parse args as array: \(call.arguments)") - result(FlutterMethodNotImplemented) - return - } - - // Requires 3 arguments in the array - guard args.count == 3 else { - print("Requires 3 arguments and received \(args.count)") - result(FlutterMethodNotImplemented) - return - } - - // Parses the arguments - let callbackHandle = args[0] as? Int64 - let notificationTitle = args[1] as? String - let instant = args[2] as? Bool - - // Write enabled to settings - let defaults = UserDefaults.standard - - // We are now enabled, so store this - defaults.set(true, forKey: "background_service_enabled") - - // The callback handle is an int64 address to communicate with the main isolate's - // entry function - defaults.set(callbackHandle, forKey: "callback_handle") - - // This is not used yet and will need to be implemented - defaults.set(notificationTitle, forKey: "notification_title") - - // Schedule the background services - BackgroundServicePlugin.scheduleBackgroundSync() - BackgroundServicePlugin.scheduleBackgroundFetch() - - result(true) - } - - // Called by the flutter code at launch to see if the background service is enabled or not - func handleIsEnabled(call: FlutterMethodCall, result: FlutterResult) { - let defaults = UserDefaults.standard - let enabled = defaults.value(forKey: "background_service_enabled") as? Bool - - // False by default - result(enabled ?? false) - } - - // Called by the Flutter code whenever a change in configuration is set - func handleConfigure(call: FlutterMethodCall, result: FlutterResult) { - - // Needs to be able to parse the arguments or else fail - guard let args = call.arguments as? Array else { - print("Cannot parse args as array: \(call.arguments)") - result(FlutterError()) - return - } - - // Needs to have 4 arguments in the call or else fail - guard args.count == 4 else { - print("Not enough arguments, 4 required: \(args.count) given") - result(FlutterError()) - return - } - - // Parse the arguments from the method call - let requireUnmeteredNetwork = args[0] as? Bool - let requireCharging = args[1] as? Bool - let triggerUpdateDelay = args[2] as? Int - let triggerMaxDelay = args[3] as? Int - - // Store the values from the call in the defaults - let defaults = UserDefaults.standard - defaults.set(requireUnmeteredNetwork, forKey: "require_unmetered_network") - defaults.set(requireCharging, forKey: "require_charging") - defaults.set(triggerUpdateDelay, forKey: "trigger_update_delay") - defaults.set(triggerMaxDelay, forKey: "trigger_max_delay") - - // Cancel the background services and reschedule them - BGTaskScheduler.shared.cancelAllTaskRequests() - BackgroundServicePlugin.scheduleBackgroundSync() - BackgroundServicePlugin.scheduleBackgroundFetch() - result(true) - } - - // Returns the number of currently scheduled background processes to Flutter, strictly - // for debugging - func handleNumberOfProcesses(call: FlutterMethodCall, result: @escaping FlutterResult) { - BGTaskScheduler.shared.getPendingTaskRequests { requests in - result(requests.count) - } - } - - // Disables the service, cancels all the task requests - func handleDisable(call: FlutterMethodCall, result: FlutterResult) { - let defaults = UserDefaults.standard - defaults.set(false, forKey: "background_service_enabled") - - BGTaskScheduler.shared.cancelAllTaskRequests() - result(true) - } - - // Checks the status of the Background App Refresh from the system - // Returns true if the service is enabled for Immich, and false otherwise - func handleBackgroundRefreshStatus(call: FlutterMethodCall, result: FlutterResult) { - switch UIApplication.shared.backgroundRefreshStatus { - case .available: - result(true) - break - case .denied: - result(false) - break - case .restricted: - result(false) - break - default: - result(false) - break - } - } - - - // Schedules a short-running background sync to sync only a few photos - static func scheduleBackgroundFetch() { - // We will schedule this task to run no matter the charging or wifi requirents from the end user - // 1. They can set Background App Refresh to Off / Wi-Fi / Wi-Fi & Cellular Data from Settings - // 2. We will check the battery connectivity when we begin running the background activity - let backgroundFetch = BGAppRefreshTaskRequest(identifier: BackgroundServicePlugin.backgroundFetchTaskID) - - // Use 5 minutes from now as earliest begin date - backgroundFetch.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60) - - do { - try BGTaskScheduler.shared.submit(backgroundFetch) - } catch { - print("Could not schedule the background task \(error.localizedDescription)") - } - } - - // Schedules a long-running background sync for syncing all of the photos - static func scheduleBackgroundSync() { - let backgroundProcessing = BGProcessingTaskRequest(identifier: BackgroundServicePlugin.backgroundProcessingTaskID) - - // We need the values for requiring charging - let defaults = UserDefaults.standard - let requireCharging = defaults.value(forKey: "require_charging") as? Bool - - // Always require network connectivity, and set the require charging from the above - backgroundProcessing.requiresNetworkConnectivity = true - backgroundProcessing.requiresExternalPower = requireCharging ?? true - - // Use 15 minutes from now as earliest begin date - backgroundProcessing.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) - - do { - // Submit the task to the scheduler - try BGTaskScheduler.shared.submit(backgroundProcessing) - } catch { - print("Could not schedule the background task \(error.localizedDescription)") - } - } - - // This function runs when the system kicks off the BGAppRefreshTask from the Background Task Scheduler - static func handleBackgroundFetch(task: BGAppRefreshTask) { - // Schedule the next sync task so we can run this again later - scheduleBackgroundFetch() - - // Log the time of last background processing to now - let defaults = UserDefaults.standard - defaults.set(Date().timeIntervalSince1970, forKey: "last_background_fetch_run_time") - - // If we have required charging, we should check the charging status - let requireCharging = defaults.value(forKey: "require_charging") as? Bool ?? false - if (requireCharging) { - UIDevice.current.isBatteryMonitoringEnabled = true - if (UIDevice.current.batteryState == .unplugged) { - // The device is unplugged and we have required charging - // Therefore, we will simply complete the task without - // running it. - task.setTaskCompleted(success: true) - return - } - } - - // If we have required Wi-Fi, we can check the isExpensive property - let requireWifi = defaults.value(forKey: "require_wifi") as? Bool ?? false - if (requireWifi) { - let wifiMonitor = NWPathMonitor(requiredInterfaceType: .wifi) - let isExpensive = wifiMonitor.currentPath.isExpensive - if (isExpensive) { - // The network is expensive and we have required Wi-Fi - // Therefore, we will simply complete the task without - // running it - task.setTaskCompleted(success: true) - return - } - } - - // Schedule the next sync task so we can run this again later - scheduleBackgroundFetch() - - // The background sync task should only run for 20 seconds at most - BackgroundServicePlugin.runBackgroundSync(task, maxSeconds: 20) - } - - // This function runs when the system kicks off the BGProcessingTask from the Background Task Scheduler - static func handleBackgroundProcessing(task: BGProcessingTask) { - // Schedule the next sync task so we run this again later - scheduleBackgroundSync() - - // Log the time of last background processing to now - let defaults = UserDefaults.standard - defaults.set(Date().timeIntervalSince1970, forKey: "last_background_processing_run_time") - - // We won't specify a max time for the background sync service, so this can run for longer - BackgroundServicePlugin.runBackgroundSync(task, maxSeconds: nil) - } - - // This is a synchronous function which uses a semaphore to run the background sync worker's run - // function, which will create a background Isolate and communicate with the Flutter code to back - // up the assets. When it completes, we signal the semaphore and complete the execution allowing the - // control to pass back to the caller synchronously - static func runBackgroundSync(_ task: BGTask, maxSeconds: Int?) { - - let semaphore = DispatchSemaphore(value: 0) - DispatchQueue.main.async { - let backgroundWorker = BackgroundSyncWorker { _ in - semaphore.signal() - } - task.expirationHandler = { - backgroundWorker.cancel() - task.setTaskCompleted(success: true) - } - - backgroundWorker.run(maxSeconds: maxSeconds) - task.setTaskCompleted(success: true) - } - semaphore.wait() - } - - -} diff --git a/mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift b/mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift deleted file mode 100644 index 88d9368308..0000000000 --- a/mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift +++ /dev/null @@ -1,271 +0,0 @@ -// -// BackgroundSyncProcessing.swift -// Runner -// -// Created by Marty Fuhry on 2/6/23. -// -// Credit to https://github.com/fluttercommunity/flutter_workmanager/blob/main/ios/Classes/BackgroundWorker.swift - -import Foundation -import Flutter -import BackgroundTasks - -// The background worker which creates a new Flutter VM, communicates with it -// to run the backup job, and then finishes execution and calls back to its callback -// handler -class BackgroundSyncWorker { - - // The Flutter engine we create for background execution. - // This is not the main Flutter engine which shows the UI, - // this is a brand new isolate created and managed in this code - // here. It does not share memory with the main - // Flutter engine which shows the UI. - // It needs to be started up, registered, and torn down here - let engine: FlutterEngine? = FlutterEngine( - name: "BackgroundImmich" - ) - - let notificationId = "com.alextran.immich/backgroundNotifications" - // The background message passing channel - var channel: FlutterMethodChannel? - - var completionHandler: (UIBackgroundFetchResult) -> Void - let taskSessionStart = Date() - - // We need the completion handler to tell the system when we are done running - init(_ completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - - // This is the background message passing channel to be used with the background engine - // created here in this platform code - self.channel = FlutterMethodChannel( - name: "immich/backgroundChannel", - binaryMessenger: engine!.binaryMessenger - ) - self.completionHandler = completionHandler - } - - // Handles all of the messages from the Flutter VM called into this platform code - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "initialized": - // Initialize tells us that we can now call into the Flutter VM to tell it to begin the update - self.channel?.invokeMethod( - "backgroundProcessing", - arguments: nil, - result: { flutterResult in - - // This is the result we send back to the BGTaskScheduler to let it know whether we'll need more time later or - // if this execution failed - let result: UIBackgroundFetchResult = (flutterResult as? Bool ?? false) ? .newData : .failed - - // Show the task duration - let taskSessionCompleter = Date() - let taskDuration = taskSessionCompleter.timeIntervalSince(self.taskSessionStart) - print("[\(String(describing: self))] \(#function) -> performBackgroundRequest.\(result) (finished in \(taskDuration) seconds)") - - // Complete the execution - self.complete(result) - }) - break - case "updateNotification": - let handled = self.handleNotification(call) - result(handled) - break - case "showError": - let handled = self.handleError(call) - result(handled) - break - case "clearErrorNotifications": - self.handleClearErrorNotifications() - result(true) - break - case "hasContentChanged": - // This is only called for Android, but we provide an implementation here - // telling Flutter that we don't have any information about whether the gallery - // contents have changed or not, so we can just say "no, they've not changed" - result(false) - break - default: - result(FlutterError()) - self.complete(UIBackgroundFetchResult.failed) - } - } - - // Runs the background sync by starting up a new isolate and handling the calls - // until it completes - public func run(maxSeconds: Int?) { - // We need the callback handle to start up the Flutter VM from the entry point - let defaults = UserDefaults.standard - guard let callbackHandle = defaults.value(forKey: "callback_handle") as? Int64 else { - // Can't find the callback handle, this is fatal - complete(UIBackgroundFetchResult.failed) - return - - } - - // Use the provided callbackHandle to get the callback function - guard let callback = FlutterCallbackCache.lookupCallbackInformation(callbackHandle) else { - // We need this callback or else this is fatal - complete(UIBackgroundFetchResult.failed) - return - } - - // Sanity check for the engine existing - if engine == nil { - complete(UIBackgroundFetchResult.failed) - return - } - - // Run the engine - let isRunning = engine!.run( - withEntrypoint: callback.callbackName, - libraryURI: callback.callbackLibraryPath - ) - - // If this engine isn't running, this is fatal - if !isRunning { - complete(UIBackgroundFetchResult.failed) - return - } - - // If we have a timer, we need to start the timer to cancel ourselves - // so that we don't run longer than the provided maxSeconds - // After maxSeconds has elapsed, we will invoke "systemStop" - if maxSeconds != nil { - // Schedule a non-repeating timer to run after maxSeconds - let timer = Timer.scheduledTimer(withTimeInterval: TimeInterval(maxSeconds!), - repeats: false) { timer in - // The callback invalidates the timer and stops execution - timer.invalidate() - - // If the channel is already deallocated, we don't need to do anything - if self.channel == nil { - return - } - - // Tell the Flutter VM to stop backing up now - self.channel?.invokeMethod( - "systemStop", - arguments: nil, - result: nil) - - // Complete the execution - self.complete(UIBackgroundFetchResult.newData) - } - } - - // Set the handle function to the channel message handler - self.channel?.setMethodCallHandler(handle) - - // Register this to get access to the plugins on the platform channel - BackgroundServicePlugin.flutterPluginRegistrantCallback?(engine!) - } - - // Cancels execution of this task, used by the system's task expiration handler - // which is called shortly before execution is about to expire - public func cancel() { - // If the channel is already deallocated, we don't need to do anything - if self.channel == nil { - return - } - - // Tell the Flutter VM to stop backing up now - self.channel?.invokeMethod( - "systemStop", - arguments: nil, - result: nil) - - // Complete the execution - self.complete(UIBackgroundFetchResult.newData) - } - - // Completes the execution, destroys the engine, and sends a completion to our callback completionHandler - private func complete(_ fetchResult: UIBackgroundFetchResult) { - engine?.destroyContext() - channel = nil - completionHandler(fetchResult) - } - - private func handleNotification(_ call: FlutterMethodCall) -> Bool { - - // Parse the arguments as an array list - guard let args = call.arguments as? Array else { - print("Failed to parse \(call.arguments) as array") - return false; - } - - // Requires 7 arguments passed or else fail - guard args.count == 7 else { - print("Needs 7 arguments, but was only passed \(args.count)") - return false - } - - // Parse the arguments to send the notification update - let title = args[0] as? String - let content = args[1] as? String - let progress = args[2] as? Int - let maximum = args[3] as? Int - let indeterminate = args[4] as? Bool - let isDetail = args[5] as? Bool - let onlyIfForeground = args[6] as? Bool - - // Build the notification - let notificationContent = UNMutableNotificationContent() - notificationContent.body = content ?? "Uploading..." - notificationContent.title = title ?? "Immich" - - // Add it to the Notification center - let notification = UNNotificationRequest( - identifier: notificationId, - content: notificationContent, - trigger: nil - ) - let center = UNUserNotificationCenter.current() - center.add(notification) { (error: Error?) in - if let theError = error { - print("Error showing notifications: \(theError)") - } - } - - return true - } - - private func handleError(_ call: FlutterMethodCall) -> Bool { - // Parse the arguments as an array list - guard let args = call.arguments as? Array else { - return false; - } - - // Requires 7 arguments passed or else fail - guard args.count == 3 else { - return false - } - - let title = args[0] as? String - let content = args[1] as? String - let individualTag = args[2] as? String - - // Build the notification - let notificationContent = UNMutableNotificationContent() - notificationContent.body = content ?? "Error running the backup job." - notificationContent.title = title ?? "Immich" - - // Add it to the Notification center - let notification = UNNotificationRequest( - identifier: notificationId, - content: notificationContent, - trigger: nil - ) - let center = UNUserNotificationCenter.current() - center.add(notification) - - return true - } - - private func handleClearErrorNotifications() { - let center = UNUserNotificationCenter.current() - center.removeDeliveredNotifications(withIdentifiers: [notificationId]) - center.removePendingNotificationRequests(withIdentifiers: [notificationId]) - } -} - diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index b5d7d780a6..9d194ad665 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -8,8 +8,6 @@ app.alextran.immich.background.refreshUpload app.alextran.immich.background.processingUpload - app.alextran.immich.backgroundFetch - app.alextran.immich.backgroundProcessing CADisableMinimumFrameDurationOnPhone diff --git a/mobile/lib/constants/aspect_ratios.dart b/mobile/lib/constants/aspect_ratios.dart new file mode 100644 index 0000000000..9159db4ef1 --- /dev/null +++ b/mobile/lib/constants/aspect_ratios.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +enum AspectRatioPreset { + free(ratio: null, label: 'Free', icon: Icons.crop_free_rounded), + square(ratio: 1.0, label: '1:1', icon: Icons.crop_square_rounded), + ratio16x9(ratio: 16 / 9, label: '16:9', icon: Icons.crop_16_9_rounded), + ratio3x2(ratio: 3 / 2, label: '3:2', icon: Icons.crop_3_2_rounded), + ratio7x5(ratio: 7 / 5, label: '7:5', icon: Icons.crop_7_5_rounded), + ratio9x16(ratio: 9 / 16, label: '9:16', icon: Icons.crop_16_9_rounded, iconRotated: true), + ratio2x3(ratio: 2 / 3, label: '2:3', icon: Icons.crop_3_2_rounded, iconRotated: true), + ratio5x7(ratio: 5 / 7, label: '5:7', icon: Icons.crop_7_5_rounded, iconRotated: true); + + final double? ratio; + final String label; + final IconData icon; + final bool iconRotated; + + const AspectRatioPreset({required this.ratio, required this.label, required this.icon, this.iconRotated = false}); +} diff --git a/mobile/lib/constants/constants.dart b/mobile/lib/constants/constants.dart index 9d28941b8f..1748a2a57d 100644 --- a/mobile/lib/constants/constants.dart +++ b/mobile/lib/constants/constants.dart @@ -1,9 +1,5 @@ import 'dart:io'; -const int noDbId = -9223372036854775808; // from Isar -const double downloadCompleted = -1; -const double downloadFailed = -2; - const String kMobileMetadataKey = "mobile-app"; // Number of log entries to retain on app start @@ -47,9 +43,6 @@ const List<(String, String)> kWidgetNames = [ ('com.immich.widget.memory', 'app.alextran.immich.widget.MemoryReceiver'), ]; -const double kUploadStatusFailed = -1.0; -const double kUploadStatusCanceled = -2.0; - const int kMinMonthsToEnableScrubberSnap = 12; const String kImmichAppStoreLink = "https://apps.apple.com/app/immich/id1613945652"; diff --git a/mobile/lib/constants/enums.dart b/mobile/lib/constants/enums.dart index 32ef9bbbed..877145c322 100644 --- a/mobile/lib/constants/enums.dart +++ b/mobile/lib/constants/enums.dart @@ -11,8 +11,6 @@ enum TextSearchType { context, filename, description, ocr } enum AssetVisibilityEnum { timeline, hidden, archive, locked } -enum SortUserBy { id } - enum ActionSource { timeline, viewer } enum CleanupStep { selectDate, scan, delete } diff --git a/mobile/lib/domain/interfaces/db.interface.dart b/mobile/lib/domain/interfaces/db.interface.dart deleted file mode 100644 index 5645d15c47..0000000000 --- a/mobile/lib/domain/interfaces/db.interface.dart +++ /dev/null @@ -1,3 +0,0 @@ -abstract interface class IDatabaseRepository { - Future transaction(Future Function() callback); -} diff --git a/mobile/lib/domain/models/asset/base_asset.model.dart b/mobile/lib/domain/models/asset/base_asset.model.dart index 9ba8cd06f8..85c42fd24f 100644 --- a/mobile/lib/domain/models/asset/base_asset.model.dart +++ b/mobile/lib/domain/models/asset/base_asset.model.dart @@ -71,6 +71,8 @@ sealed class BaseAsset { bool get isLocalOnly => storage == AssetState.local; bool get isRemoteOnly => storage == AssetState.remote; + bool get isEditable => false; + // Overridden in subclasses AssetState get storage; String? get localId; diff --git a/mobile/lib/domain/models/asset/remote_asset.model.dart b/mobile/lib/domain/models/asset/remote_asset.model.dart index b9a0e64d6a..745e8f46ff 100644 --- a/mobile/lib/domain/models/asset/remote_asset.model.dart +++ b/mobile/lib/domain/models/asset/remote_asset.model.dart @@ -43,6 +43,9 @@ class RemoteAsset extends BaseAsset { @override String get heroTag => '${localId ?? checksum}_$id'; + @override + bool get isEditable => isImage && !isMotionPhoto && !isAnimatedImage; + @override String toString() { return '''Asset { diff --git a/mobile/lib/domain/models/asset_edit.model.dart b/mobile/lib/domain/models/asset_edit.model.dart index b3266dba46..9809b9c606 100644 --- a/mobile/lib/domain/models/asset_edit.model.dart +++ b/mobile/lib/domain/models/asset_edit.model.dart @@ -1,21 +1,25 @@ -import "package:openapi/api.dart" as api show AssetEditAction; +import "package:openapi/api.dart" show CropParameters, RotateParameters, MirrorParameters; enum AssetEditAction { rotate, crop, mirror, other } -extension AssetEditActionExtension on AssetEditAction { - api.AssetEditAction? toDto() { - return switch (this) { - AssetEditAction.rotate => api.AssetEditAction.rotate, - AssetEditAction.crop => api.AssetEditAction.crop, - AssetEditAction.mirror => api.AssetEditAction.mirror, - AssetEditAction.other => null, - }; - } +sealed class AssetEdit { + const AssetEdit(); } -class AssetEdit { - final AssetEditAction action; - final Map parameters; +class CropEdit extends AssetEdit { + final CropParameters parameters; - const AssetEdit({required this.action, required this.parameters}); + const CropEdit(this.parameters); +} + +class RotateEdit extends AssetEdit { + final RotateParameters parameters; + + const RotateEdit(this.parameters); +} + +class MirrorEdit extends AssetEdit { + final MirrorParameters parameters; + + const MirrorEdit(this.parameters); } diff --git a/mobile/lib/domain/models/device_asset.model.dart b/mobile/lib/domain/models/device_asset.model.dart deleted file mode 100644 index a404f5a9e2..0000000000 --- a/mobile/lib/domain/models/device_asset.model.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'dart:typed_data'; - -class DeviceAsset { - final String assetId; - final Uint8List hash; - final DateTime modifiedTime; - - const DeviceAsset({required this.assetId, required this.hash, required this.modifiedTime}); - - @override - bool operator ==(covariant DeviceAsset other) { - if (identical(this, other)) return true; - - return other.assetId == assetId && other.hash == hash && other.modifiedTime == modifiedTime; - } - - @override - int get hashCode { - return assetId.hashCode ^ hash.hashCode ^ modifiedTime.hashCode; - } - - @override - String toString() { - return 'DeviceAsset(assetId: $assetId, hash: $hash, modifiedTime: $modifiedTime)'; - } - - DeviceAsset copyWith({String? assetId, Uint8List? hash, DateTime? modifiedTime}) { - return DeviceAsset( - assetId: assetId ?? this.assetId, - hash: hash ?? this.hash, - modifiedTime: modifiedTime ?? this.modifiedTime, - ); - } -} diff --git a/mobile/lib/domain/models/exif.model.dart b/mobile/lib/domain/models/exif.model.dart index d0f78b59de..45b787d586 100644 --- a/mobile/lib/domain/models/exif.model.dart +++ b/mobile/lib/domain/models/exif.model.dart @@ -7,6 +7,8 @@ class ExifInfo { final String? timeZone; final DateTime? dateTimeOriginal; final int? rating; + final int? width; + final int? height; // GPS final double? latitude; @@ -48,6 +50,8 @@ class ExifInfo { this.timeZone, this.dateTimeOriginal, this.rating, + this.width, + this.height, this.isFlipped = false, this.latitude, this.longitude, @@ -74,6 +78,8 @@ class ExifInfo { other.timeZone == timeZone && other.dateTimeOriginal == dateTimeOriginal && other.rating == rating && + other.width == width && + other.height == height && other.latitude == latitude && other.longitude == longitude && other.city == city && @@ -98,6 +104,8 @@ class ExifInfo { timeZone.hashCode ^ dateTimeOriginal.hashCode ^ rating.hashCode ^ + width.hashCode ^ + height.hashCode ^ latitude.hashCode ^ longitude.hashCode ^ city.hashCode ^ @@ -123,6 +131,8 @@ isFlipped: $isFlipped, timeZone: ${timeZone ?? 'NA'}, dateTimeOriginal: ${dateTimeOriginal ?? 'NA'}, rating: ${rating ?? 'NA'}, +width: ${width ?? 'NA'}, +height: ${height ?? 'NA'}, latitude: ${latitude ?? 'NA'}, longitude: ${longitude ?? 'NA'}, city: ${city ?? 'NA'}, @@ -146,6 +156,8 @@ exposureSeconds: ${exposureSeconds ?? 'NA'}, String? timeZone, DateTime? dateTimeOriginal, int? rating, + int? width, + int? height, double? latitude, double? longitude, String? city, @@ -168,6 +180,8 @@ exposureSeconds: ${exposureSeconds ?? 'NA'}, timeZone: timeZone ?? this.timeZone, dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal, rating: rating ?? this.rating, + width: width ?? this.width, + height: height ?? this.height, isFlipped: isFlipped ?? this.isFlipped, latitude: latitude ?? this.latitude, longitude: longitude ?? this.longitude, diff --git a/mobile/lib/domain/services/asset.service.dart b/mobile/lib/domain/services/asset.service.dart index 198733b3c8..7fa8c13fd8 100644 --- a/mobile/lib/domain/services/asset.service.dart +++ b/mobile/lib/domain/services/asset.service.dart @@ -1,12 +1,9 @@ import 'package:immich_mobile/domain/models/album/local_album.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/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; -typedef _AssetVideoDimension = ({double? width, double? height, bool isFlipped}); - class AssetService { final RemoteAssetRepository _remoteAssetRepository; final DriftLocalAssetRepository _localAssetRepository; @@ -58,49 +55,6 @@ class AssetService { return _remoteAssetRepository.getExif(id); } - Future getAspectRatio(BaseAsset asset) async { - final dimension = asset is LocalAsset - ? await _getLocalAssetDimensions(asset) - : await _getRemoteAssetDimensions(asset as RemoteAsset); - - if (dimension.width == null || dimension.height == null || dimension.height == 0) { - return 1.0; - } - - return dimension.isFlipped ? dimension.height! / dimension.width! : dimension.width! / dimension.height!; - } - - Future<_AssetVideoDimension> _getLocalAssetDimensions(LocalAsset asset) async { - double? width = asset.width?.toDouble(); - double? height = asset.height?.toDouble(); - int orientation = asset.orientation; - - if (width == null || height == null) { - final fetched = await _localAssetRepository.get(asset.id); - width = fetched?.width?.toDouble(); - height = fetched?.height?.toDouble(); - orientation = fetched?.orientation ?? 0; - } - - // On Android, local assets need orientation correction for 90°/270° rotations - // On iOS, the Photos framework pre-corrects dimensions - final isFlipped = CurrentPlatform.isAndroid && (orientation == 90 || orientation == 270); - return (width: width, height: height, isFlipped: isFlipped); - } - - Future<_AssetVideoDimension> _getRemoteAssetDimensions(RemoteAsset asset) async { - double? width = asset.width?.toDouble(); - double? height = asset.height?.toDouble(); - - if (width == null || height == null) { - final fetched = await _remoteAssetRepository.get(asset.id); - width = fetched?.width?.toDouble(); - height = fetched?.height?.toDouble(); - } - - return (width: width, height: height, isFlipped: false); - } - Future> getPlaces(String userId) { return _remoteAssetRepository.getPlaces(userId); } diff --git a/mobile/lib/domain/services/background_worker.service.dart b/mobile/lib/domain/services/background_worker.service.dart index 93a2a14127..d4da3e31a4 100644 --- a/mobile/lib/domain/services/background_worker.service.dart +++ b/mobile/lib/domain/services/background_worker.service.dart @@ -16,19 +16,16 @@ import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart' show nativeSyncApiProvider; import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/auth.service.dart'; -import 'package:immich_mobile/services/localization.service.dart'; import 'package:immich_mobile/services/foreground_upload.service.dart'; +import 'package:immich_mobile/services/localization.service.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/wm_executor.dart'; -import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; class BackgroundWorkerFgService { @@ -58,7 +55,6 @@ class BackgroundWorkerFgService { class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { ProviderContainer? _ref; - final Isar _isar; final Drift _drift; final DriftLogger _driftLogger; final BackgroundWorkerBgHostApi _backgroundHostApi; @@ -67,18 +63,11 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { bool _isCleanedUp = false; - BackgroundWorkerBgService({required Isar isar, required Drift drift, required DriftLogger driftLogger}) - : _isar = isar, - _drift = drift, + BackgroundWorkerBgService({required Drift drift, required DriftLogger driftLogger}) + : _drift = drift, _driftLogger = driftLogger, _backgroundHostApi = BackgroundWorkerBgHostApi() { - _ref = ProviderContainer( - overrides: [ - dbProvider.overrideWithValue(isar), - isarProvider.overrideWithValue(isar), - driftProvider.overrideWith(driftOverride(drift)), - ], - ); + _ref = ProviderContainer(overrides: [driftProvider.overrideWith(driftOverride(drift))]); BackgroundWorkerFlutterApi.setUp(this); } @@ -102,7 +91,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { ), FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false), FileDownloader().trackTasks(), - _ref?.read(fileMediaRepositoryProvider).enableBackgroundAccess(), ].nonNulls, ); @@ -209,9 +197,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { backgroundSyncManager?.cancel(), ]; - if (_isar.isOpen) { - cleanupFutures.add(_isar.close()); - } await Future.wait(cleanupFutures.nonNulls); _logger.info("Background worker resources cleaned up"); } catch (error, stack) { @@ -301,7 +286,6 @@ Future backgroundSyncNativeEntrypoint() async { WidgetsFlutterBinding.ensureInitialized(); DartPluginRegistrant.ensureInitialized(); - final (isar, drift, logDB) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDB, shouldBufferLogs: false, listenStoreUpdates: false); - await BackgroundWorkerBgService(isar: isar, drift: drift, driftLogger: logDB).init(); + final (drift, logDB) = await Bootstrap.initDomain(shouldBufferLogs: false, listenStoreUpdates: false); + await BackgroundWorkerBgService(drift: drift, driftLogger: logDB).init(); } diff --git a/mobile/lib/domain/services/log.service.dart b/mobile/lib/domain/services/log.service.dart index 64010b9220..b58ee89535 100644 --- a/mobile/lib/domain/services/log.service.dart +++ b/mobile/lib/domain/services/log.service.dart @@ -15,7 +15,7 @@ import 'package:logging/logging.dart'; /// via [IStoreRepository] class LogService { final LogRepository _logRepository; - final IStoreRepository _storeRepository; + final DriftStoreRepository _storeRepository; final List _msgBuffer = []; @@ -38,7 +38,7 @@ class LogService { static Future init({ required LogRepository logRepository, - required IStoreRepository storeRepository, + required DriftStoreRepository storeRepository, bool shouldBuffer = true, }) async { _instance ??= await create( @@ -51,7 +51,7 @@ class LogService { static Future create({ required LogRepository logRepository, - required IStoreRepository storeRepository, + required DriftStoreRepository storeRepository, bool shouldBuffer = true, }) async { final instance = LogService._(logRepository, storeRepository, shouldBuffer); diff --git a/mobile/lib/domain/services/store.service.dart b/mobile/lib/domain/services/store.service.dart index 0098c3d262..b325ffd631 100644 --- a/mobile/lib/domain/services/store.service.dart +++ b/mobile/lib/domain/services/store.service.dart @@ -6,13 +6,13 @@ import 'package:immich_mobile/infrastructure/repositories/store.repository.dart' /// Provides access to a persistent key-value store with an in-memory cache. /// Listens for repository changes to keep the cache updated. class StoreService { - final IStoreRepository _storeRepository; + final DriftStoreRepository _storeRepository; /// In-memory cache. Keys are [StoreKey.id] final Map _cache = {}; StreamSubscription>? _storeUpdateSubscription; - StoreService._({required IStoreRepository isarStoreRepository}) : _storeRepository = isarStoreRepository; + StoreService._({required DriftStoreRepository isarStoreRepository}) : _storeRepository = isarStoreRepository; // TODO: Temporary typedef to make minimal changes. Remove this and make the presentation layer access store through a provider static StoreService? _instance; @@ -24,12 +24,12 @@ class StoreService { } // TODO: Replace the implementation with the one from create after removing the typedef - static Future init({required IStoreRepository storeRepository, bool listenUpdates = true}) async { + static Future init({required DriftStoreRepository storeRepository, bool listenUpdates = true}) async { _instance ??= await create(storeRepository: storeRepository, listenUpdates: listenUpdates); return _instance!; } - static Future create({required IStoreRepository storeRepository, bool listenUpdates = true}) async { + static Future create({required DriftStoreRepository storeRepository, bool listenUpdates = true}) async { final instance = StoreService._(isarStoreRepository: storeRepository); await instance.populateCache(); if (listenUpdates) { @@ -91,8 +91,6 @@ class StoreService { await _storeRepository.deleteAll(); _cache.clear(); } - - bool get isBetaTimelineEnabled => tryGet(StoreKey.betaTimeline) ?? true; } class StoreKeyNotFoundException implements Exception { diff --git a/mobile/lib/domain/services/user.service.dart b/mobile/lib/domain/services/user.service.dart index d347d8aa4f..1f9c015ad7 100644 --- a/mobile/lib/domain/services/user.service.dart +++ b/mobile/lib/domain/services/user.service.dart @@ -4,23 +4,17 @@ import 'dart:typed_data'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; import 'package:logging/logging.dart'; class UserService { final Logger _log = Logger("UserService"); - final IsarUserRepository _isarUserRepository; final UserApiRepository _userApiRepository; final StoreService _storeService; - UserService({ - required IsarUserRepository isarUserRepository, - required UserApiRepository userApiRepository, - required StoreService storeService, - }) : _isarUserRepository = isarUserRepository, - _userApiRepository = userApiRepository, - _storeService = storeService; + UserService({required UserApiRepository userApiRepository, required StoreService storeService}) + : _userApiRepository = userApiRepository, + _storeService = storeService; UserDto getMyUser() { return _storeService.get(StoreKey.currentUser); @@ -38,7 +32,6 @@ class UserService { final user = await _userApiRepository.getMyUser(); if (user == null) return null; await _storeService.put(StoreKey.currentUser, user); - await _isarUserRepository.update(user); return user; } @@ -47,19 +40,10 @@ class UserService { final path = await _userApiRepository.createProfileImage(name: name, data: image); final updatedUser = getMyUser(); await _storeService.put(StoreKey.currentUser, updatedUser); - await _isarUserRepository.update(updatedUser); return path; } catch (e) { _log.warning("Failed to upload profile image", e); return null; } } - - Future> getAll() async { - return await _isarUserRepository.getAll(); - } - - Future deleteAll() { - return _isarUserRepository.deleteAll(); - } } diff --git a/mobile/lib/domain/utils/migrate_cloud_ids.dart b/mobile/lib/domain/utils/migrate_cloud_ids.dart index 33a8eca94d..32188b4838 100644 --- a/mobile/lib/domain/utils/migrate_cloud_ids.dart +++ b/mobile/lib/domain/utils/migrate_cloud_ids.dart @@ -80,12 +80,14 @@ Future _processCloudIdMappingsInBatches( AssetMetadataBulkUpsertItemDto( assetId: mapping.remoteAssetId, key: kMobileMetadataKey, - value: RemoteAssetMobileAppMetadata( - cloudId: mapping.localAsset.cloudId, - createdAt: mapping.localAsset.createdAt.toIso8601String(), - adjustmentTime: mapping.localAsset.adjustmentTime?.toIso8601String(), - latitude: mapping.localAsset.latitude?.toString(), - longitude: mapping.localAsset.longitude?.toString(), + value: Map.from( + RemoteAssetMobileAppMetadata( + cloudId: mapping.localAsset.cloudId, + createdAt: mapping.localAsset.createdAt.toIso8601String(), + adjustmentTime: mapping.localAsset.adjustmentTime?.toIso8601String(), + latitude: mapping.localAsset.latitude?.toString(), + longitude: mapping.localAsset.longitude?.toString(), + ).toJson(), ), ), ); diff --git a/mobile/lib/entities/README.md b/mobile/lib/entities/README.md deleted file mode 100644 index c2ad4876e3..0000000000 --- a/mobile/lib/entities/README.md +++ /dev/null @@ -1 +0,0 @@ -This directory contains entity that is stored in the local storage. \ No newline at end of file diff --git a/mobile/lib/entities/album.entity.dart b/mobile/lib/entities/album.entity.dart deleted file mode 100644 index 2ca0d50dcc..0000000000 --- a/mobile/lib/entities/album.entity.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/utils/datetime_comparison.dart'; -import 'package:isar/isar.dart'; -// ignore: implementation_imports -import 'package:isar/src/common/isar_links_common.dart'; -import 'package:openapi/api.dart'; - -part 'album.entity.g.dart'; - -@Collection(inheritance: false) -class Album { - @protected - Album({ - this.remoteId, - this.localId, - required this.name, - required this.createdAt, - required this.modifiedAt, - this.description, - this.startDate, - this.endDate, - this.lastModifiedAssetTimestamp, - required this.shared, - required this.activityEnabled, - this.sortOrder = SortOrder.desc, - }); - - // fields stored in DB - Id id = Isar.autoIncrement; - @Index(unique: false, replace: false, type: IndexType.hash) - String? remoteId; - @Index(unique: false, replace: false, type: IndexType.hash) - String? localId; - String name; - String? description; - DateTime createdAt; - DateTime modifiedAt; - DateTime? startDate; - DateTime? endDate; - DateTime? lastModifiedAssetTimestamp; - bool shared; - bool activityEnabled; - @enumerated - SortOrder sortOrder; - final IsarLink owner = IsarLink(); - final IsarLink thumbnail = IsarLink(); - final IsarLinks sharedUsers = IsarLinks(); - final IsarLinks assets = IsarLinks(); - - // transient fields - @ignore - bool isAll = false; - - @ignore - String? remoteThumbnailAssetId; - - @ignore - int remoteAssetCount = 0; - - // getters - @ignore - bool get isRemote => remoteId != null; - - @ignore - bool get isLocal => localId != null; - - @ignore - int get assetCount => assets.length; - - @ignore - String? get ownerId => owner.value?.id; - - @ignore - String? get ownerName { - // Guard null owner - if (owner.value == null) { - return null; - } - - final name = []; - if (owner.value?.name != null) { - name.add(owner.value!.name); - } - - return name.join(' '); - } - - @ignore - String get eTagKeyAssetCount => "device-album-$localId-asset-count"; - - // the following getter are needed because Isar links do not make data - // accessible in an object freshly created (not loaded from DB) - - @ignore - Iterable get remoteUsers => - sharedUsers.isEmpty ? (sharedUsers as IsarLinksCommon).addedObjects : sharedUsers; - - @ignore - Iterable get remoteAssets => assets.isEmpty ? (assets as IsarLinksCommon).addedObjects : assets; - - @override - bool operator ==(other) { - if (other is! Album) return false; - return id == other.id && - remoteId == other.remoteId && - localId == other.localId && - name == other.name && - description == other.description && - createdAt.isAtSameMomentAs(other.createdAt) && - modifiedAt.isAtSameMomentAs(other.modifiedAt) && - isAtSameMomentAs(startDate, other.startDate) && - isAtSameMomentAs(endDate, other.endDate) && - isAtSameMomentAs(lastModifiedAssetTimestamp, other.lastModifiedAssetTimestamp) && - shared == other.shared && - activityEnabled == other.activityEnabled && - owner.value == other.owner.value && - thumbnail.value == other.thumbnail.value && - sharedUsers.length == other.sharedUsers.length && - assets.length == other.assets.length; - } - - @override - @ignore - int get hashCode => - id.hashCode ^ - remoteId.hashCode ^ - localId.hashCode ^ - name.hashCode ^ - createdAt.hashCode ^ - modifiedAt.hashCode ^ - startDate.hashCode ^ - endDate.hashCode ^ - description.hashCode ^ - lastModifiedAssetTimestamp.hashCode ^ - shared.hashCode ^ - activityEnabled.hashCode ^ - owner.value.hashCode ^ - thumbnail.value.hashCode ^ - sharedUsers.length.hashCode ^ - assets.length.hashCode; - - static Future remote(AlbumResponseDto dto) async { - final Isar db = Isar.getInstance()!; - final Album a = Album( - remoteId: dto.id, - name: dto.albumName, - createdAt: dto.createdAt, - modifiedAt: dto.updatedAt, - description: dto.description, - lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp, - shared: dto.shared, - startDate: dto.startDate, - endDate: dto.endDate, - activityEnabled: dto.isActivityEnabled, - ); - a.remoteAssetCount = dto.assetCount; - a.owner.value = await db.users.getById(dto.ownerId); - if (dto.order != null) { - a.sortOrder = dto.order == AssetOrder.asc ? SortOrder.asc : SortOrder.desc; - } - - if (dto.albumThumbnailAssetId != null) { - a.thumbnail.value = await db.assets.where().remoteIdEqualTo(dto.albumThumbnailAssetId).findFirst(); - } - if (dto.albumUsers.isNotEmpty) { - final users = await db.users.getAllById(dto.albumUsers.map((e) => e.user.id).toList(growable: false)); - a.sharedUsers.addAll(users.cast()); - } - if (dto.assets.isNotEmpty) { - final assets = await db.assets.getAllByRemoteId(dto.assets.map((e) => e.id)); - a.assets.addAll(assets); - } - return a; - } - - @override - String toString() => 'remoteId: $remoteId name: $name description: $description'; -} - -extension AssetsHelper on IsarCollection { - Future store(Album a) async { - await put(a); - await a.owner.save(); - await a.thumbnail.save(); - await a.sharedUsers.save(); - await a.assets.save(); - return a; - } -} diff --git a/mobile/lib/entities/album.entity.g.dart b/mobile/lib/entities/album.entity.g.dart deleted file mode 100644 index ecbbab48c2..0000000000 --- a/mobile/lib/entities/album.entity.g.dart +++ /dev/null @@ -1,2240 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'album.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetAlbumCollection on Isar { - IsarCollection get albums => this.collection(); -} - -const AlbumSchema = CollectionSchema( - name: r'Album', - id: -1355968412107120937, - properties: { - r'activityEnabled': PropertySchema( - id: 0, - name: r'activityEnabled', - type: IsarType.bool, - ), - r'createdAt': PropertySchema( - id: 1, - name: r'createdAt', - type: IsarType.dateTime, - ), - r'description': PropertySchema( - id: 2, - name: r'description', - type: IsarType.string, - ), - r'endDate': PropertySchema( - id: 3, - name: r'endDate', - type: IsarType.dateTime, - ), - r'lastModifiedAssetTimestamp': PropertySchema( - id: 4, - name: r'lastModifiedAssetTimestamp', - type: IsarType.dateTime, - ), - r'localId': PropertySchema(id: 5, name: r'localId', type: IsarType.string), - r'modifiedAt': PropertySchema( - id: 6, - name: r'modifiedAt', - type: IsarType.dateTime, - ), - r'name': PropertySchema(id: 7, name: r'name', type: IsarType.string), - r'remoteId': PropertySchema( - id: 8, - name: r'remoteId', - type: IsarType.string, - ), - r'shared': PropertySchema(id: 9, name: r'shared', type: IsarType.bool), - r'sortOrder': PropertySchema( - id: 10, - name: r'sortOrder', - type: IsarType.byte, - enumMap: _AlbumsortOrderEnumValueMap, - ), - r'startDate': PropertySchema( - id: 11, - name: r'startDate', - type: IsarType.dateTime, - ), - }, - - estimateSize: _albumEstimateSize, - serialize: _albumSerialize, - deserialize: _albumDeserialize, - deserializeProp: _albumDeserializeProp, - idName: r'id', - indexes: { - r'remoteId': IndexSchema( - id: 6301175856541681032, - name: r'remoteId', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'remoteId', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - r'localId': IndexSchema( - id: 1199848425898359622, - name: r'localId', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'localId', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - }, - links: { - r'owner': LinkSchema( - id: 8272576585804958029, - name: r'owner', - target: r'User', - single: true, - ), - r'thumbnail': LinkSchema( - id: 4055421409629988258, - name: r'thumbnail', - target: r'Asset', - single: true, - ), - r'sharedUsers': LinkSchema( - id: 8972835302564625434, - name: r'sharedUsers', - target: r'User', - single: false, - ), - r'assets': LinkSchema( - id: 1059358332698388152, - name: r'assets', - target: r'Asset', - single: false, - ), - }, - embeddedSchemas: {}, - - getId: _albumGetId, - getLinks: _albumGetLinks, - attach: _albumAttach, - version: '3.3.0-dev.3', -); - -int _albumEstimateSize( - Album object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - { - final value = object.description; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.localId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - bytesCount += 3 + object.name.length * 3; - { - final value = object.remoteId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - return bytesCount; -} - -void _albumSerialize( - Album object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeBool(offsets[0], object.activityEnabled); - writer.writeDateTime(offsets[1], object.createdAt); - writer.writeString(offsets[2], object.description); - writer.writeDateTime(offsets[3], object.endDate); - writer.writeDateTime(offsets[4], object.lastModifiedAssetTimestamp); - writer.writeString(offsets[5], object.localId); - writer.writeDateTime(offsets[6], object.modifiedAt); - writer.writeString(offsets[7], object.name); - writer.writeString(offsets[8], object.remoteId); - writer.writeBool(offsets[9], object.shared); - writer.writeByte(offsets[10], object.sortOrder.index); - writer.writeDateTime(offsets[11], object.startDate); -} - -Album _albumDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = Album( - activityEnabled: reader.readBool(offsets[0]), - createdAt: reader.readDateTime(offsets[1]), - description: reader.readStringOrNull(offsets[2]), - endDate: reader.readDateTimeOrNull(offsets[3]), - lastModifiedAssetTimestamp: reader.readDateTimeOrNull(offsets[4]), - localId: reader.readStringOrNull(offsets[5]), - modifiedAt: reader.readDateTime(offsets[6]), - name: reader.readString(offsets[7]), - remoteId: reader.readStringOrNull(offsets[8]), - shared: reader.readBool(offsets[9]), - sortOrder: - _AlbumsortOrderValueEnumMap[reader.readByteOrNull(offsets[10])] ?? - SortOrder.desc, - startDate: reader.readDateTimeOrNull(offsets[11]), - ); - object.id = id; - return object; -} - -P _albumDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readBool(offset)) as P; - case 1: - return (reader.readDateTime(offset)) as P; - case 2: - return (reader.readStringOrNull(offset)) as P; - case 3: - return (reader.readDateTimeOrNull(offset)) as P; - case 4: - return (reader.readDateTimeOrNull(offset)) as P; - case 5: - return (reader.readStringOrNull(offset)) as P; - case 6: - return (reader.readDateTime(offset)) as P; - case 7: - return (reader.readString(offset)) as P; - case 8: - return (reader.readStringOrNull(offset)) as P; - case 9: - return (reader.readBool(offset)) as P; - case 10: - return (_AlbumsortOrderValueEnumMap[reader.readByteOrNull(offset)] ?? - SortOrder.desc) - as P; - case 11: - return (reader.readDateTimeOrNull(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -const _AlbumsortOrderEnumValueMap = {'asc': 0, 'desc': 1}; -const _AlbumsortOrderValueEnumMap = {0: SortOrder.asc, 1: SortOrder.desc}; - -Id _albumGetId(Album object) { - return object.id; -} - -List> _albumGetLinks(Album object) { - return [object.owner, object.thumbnail, object.sharedUsers, object.assets]; -} - -void _albumAttach(IsarCollection col, Id id, Album object) { - object.id = id; - object.owner.attach(col, col.isar.collection(), r'owner', id); - object.thumbnail.attach(col, col.isar.collection(), r'thumbnail', id); - object.sharedUsers.attach( - col, - col.isar.collection(), - r'sharedUsers', - id, - ); - object.assets.attach(col, col.isar.collection(), r'assets', id); -} - -extension AlbumQueryWhereSort on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension AlbumQueryWhere on QueryBuilder { - QueryBuilder idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder idGreaterThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder idLessThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder remoteIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'remoteId', value: [null]), - ); - }); - } - - QueryBuilder remoteIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [null], - includeLower: false, - upper: [], - ), - ); - }); - } - - QueryBuilder remoteIdEqualTo( - String? remoteId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'remoteId', value: [remoteId]), - ); - }); - } - - QueryBuilder remoteIdNotEqualTo( - String? remoteId, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [], - upper: [remoteId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [remoteId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [remoteId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [], - upper: [remoteId], - includeUpper: false, - ), - ); - } - }); - } - - QueryBuilder localIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'localId', value: [null]), - ); - }); - } - - QueryBuilder localIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [null], - includeLower: false, - upper: [], - ), - ); - }); - } - - QueryBuilder localIdEqualTo( - String? localId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'localId', value: [localId]), - ); - }); - } - - QueryBuilder localIdNotEqualTo( - String? localId, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [], - upper: [localId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [localId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [localId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [], - upper: [localId], - includeUpper: false, - ), - ); - } - }); - } -} - -extension AlbumQueryFilter on QueryBuilder { - QueryBuilder activityEnabledEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'activityEnabled', value: value), - ); - }); - } - - QueryBuilder createdAtEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'createdAt', value: value), - ); - }); - } - - QueryBuilder createdAtGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'createdAt', - value: value, - ), - ); - }); - } - - QueryBuilder createdAtLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'createdAt', - value: value, - ), - ); - }); - } - - QueryBuilder createdAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'createdAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder descriptionIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'description'), - ); - }); - } - - QueryBuilder descriptionIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'description'), - ); - }); - } - - QueryBuilder descriptionEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'description', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'description', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'description', value: ''), - ); - }); - } - - QueryBuilder descriptionIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'description', value: ''), - ); - }); - } - - QueryBuilder endDateIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'endDate'), - ); - }); - } - - QueryBuilder endDateIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'endDate'), - ); - }); - } - - QueryBuilder endDateEqualTo( - DateTime? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'endDate', value: value), - ); - }); - } - - QueryBuilder endDateGreaterThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'endDate', - value: value, - ), - ); - }); - } - - QueryBuilder endDateLessThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'endDate', - value: value, - ), - ); - }); - } - - QueryBuilder endDateBetween( - DateTime? lower, - DateTime? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'endDate', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder idGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - lastModifiedAssetTimestampIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'lastModifiedAssetTimestamp'), - ); - }); - } - - QueryBuilder - lastModifiedAssetTimestampIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull( - property: r'lastModifiedAssetTimestamp', - ), - ); - }); - } - - QueryBuilder - lastModifiedAssetTimestampEqualTo(DateTime? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'lastModifiedAssetTimestamp', - value: value, - ), - ); - }); - } - - QueryBuilder - lastModifiedAssetTimestampGreaterThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'lastModifiedAssetTimestamp', - value: value, - ), - ); - }); - } - - QueryBuilder - lastModifiedAssetTimestampLessThan(DateTime? value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'lastModifiedAssetTimestamp', - value: value, - ), - ); - }); - } - - QueryBuilder - lastModifiedAssetTimestampBetween( - DateTime? lower, - DateTime? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'lastModifiedAssetTimestamp', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder localIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'localId'), - ); - }); - } - - QueryBuilder localIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'localId'), - ); - }); - } - - QueryBuilder localIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'localId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'localId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'localId', value: ''), - ); - }); - } - - QueryBuilder localIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'localId', value: ''), - ); - }); - } - - QueryBuilder modifiedAtEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'modifiedAt', value: value), - ); - }); - } - - QueryBuilder modifiedAtGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'modifiedAt', - value: value, - ), - ); - }); - } - - QueryBuilder modifiedAtLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'modifiedAt', - value: value, - ), - ); - }); - } - - QueryBuilder modifiedAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'modifiedAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder nameEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'name', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'name', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'name', value: ''), - ); - }); - } - - QueryBuilder nameIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'name', value: ''), - ); - }); - } - - QueryBuilder remoteIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'remoteId'), - ); - }); - } - - QueryBuilder remoteIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'remoteId'), - ); - }); - } - - QueryBuilder remoteIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'remoteId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'remoteId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'remoteId', value: ''), - ); - }); - } - - QueryBuilder remoteIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'remoteId', value: ''), - ); - }); - } - - QueryBuilder sharedEqualTo(bool value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'shared', value: value), - ); - }); - } - - QueryBuilder sortOrderEqualTo( - SortOrder value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'sortOrder', value: value), - ); - }); - } - - QueryBuilder sortOrderGreaterThan( - SortOrder value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'sortOrder', - value: value, - ), - ); - }); - } - - QueryBuilder sortOrderLessThan( - SortOrder value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'sortOrder', - value: value, - ), - ); - }); - } - - QueryBuilder sortOrderBetween( - SortOrder lower, - SortOrder upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'sortOrder', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder startDateIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'startDate'), - ); - }); - } - - QueryBuilder startDateIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'startDate'), - ); - }); - } - - QueryBuilder startDateEqualTo( - DateTime? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'startDate', value: value), - ); - }); - } - - QueryBuilder startDateGreaterThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'startDate', - value: value, - ), - ); - }); - } - - QueryBuilder startDateLessThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'startDate', - value: value, - ), - ); - }); - } - - QueryBuilder startDateBetween( - DateTime? lower, - DateTime? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'startDate', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension AlbumQueryObject on QueryBuilder {} - -extension AlbumQueryLinks on QueryBuilder { - QueryBuilder owner(FilterQuery q) { - return QueryBuilder.apply(this, (query) { - return query.link(q, r'owner'); - }); - } - - QueryBuilder ownerIsNull() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'owner', 0, true, 0, true); - }); - } - - QueryBuilder thumbnail( - FilterQuery q, - ) { - return QueryBuilder.apply(this, (query) { - return query.link(q, r'thumbnail'); - }); - } - - QueryBuilder thumbnailIsNull() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'thumbnail', 0, true, 0, true); - }); - } - - QueryBuilder sharedUsers( - FilterQuery q, - ) { - return QueryBuilder.apply(this, (query) { - return query.link(q, r'sharedUsers'); - }); - } - - QueryBuilder sharedUsersLengthEqualTo( - int length, - ) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'sharedUsers', length, true, length, true); - }); - } - - QueryBuilder sharedUsersIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'sharedUsers', 0, true, 0, true); - }); - } - - QueryBuilder sharedUsersIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'sharedUsers', 0, false, 999999, true); - }); - } - - QueryBuilder sharedUsersLengthLessThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'sharedUsers', 0, true, length, include); - }); - } - - QueryBuilder - sharedUsersLengthGreaterThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'sharedUsers', length, include, 999999, true); - }); - } - - QueryBuilder sharedUsersLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength( - r'sharedUsers', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } - - QueryBuilder assets( - FilterQuery q, - ) { - return QueryBuilder.apply(this, (query) { - return query.link(q, r'assets'); - }); - } - - QueryBuilder assetsLengthEqualTo( - int length, - ) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'assets', length, true, length, true); - }); - } - - QueryBuilder assetsIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'assets', 0, true, 0, true); - }); - } - - QueryBuilder assetsIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'assets', 0, false, 999999, true); - }); - } - - QueryBuilder assetsLengthLessThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'assets', 0, true, length, include); - }); - } - - QueryBuilder assetsLengthGreaterThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'assets', length, include, 999999, true); - }); - } - - QueryBuilder assetsLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength( - r'assets', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } -} - -extension AlbumQuerySortBy on QueryBuilder { - QueryBuilder sortByActivityEnabled() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'activityEnabled', Sort.asc); - }); - } - - QueryBuilder sortByActivityEnabledDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'activityEnabled', Sort.desc); - }); - } - - QueryBuilder sortByCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'createdAt', Sort.asc); - }); - } - - QueryBuilder sortByCreatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'createdAt', Sort.desc); - }); - } - - QueryBuilder sortByDescription() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.asc); - }); - } - - QueryBuilder sortByDescriptionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.desc); - }); - } - - QueryBuilder sortByEndDate() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'endDate', Sort.asc); - }); - } - - QueryBuilder sortByEndDateDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'endDate', Sort.desc); - }); - } - - QueryBuilder sortByLastModifiedAssetTimestamp() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.asc); - }); - } - - QueryBuilder - sortByLastModifiedAssetTimestampDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.desc); - }); - } - - QueryBuilder sortByLocalId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.asc); - }); - } - - QueryBuilder sortByLocalIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.desc); - }); - } - - QueryBuilder sortByModifiedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedAt', Sort.asc); - }); - } - - QueryBuilder sortByModifiedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedAt', Sort.desc); - }); - } - - QueryBuilder sortByName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.asc); - }); - } - - QueryBuilder sortByNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.desc); - }); - } - - QueryBuilder sortByRemoteId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.asc); - }); - } - - QueryBuilder sortByRemoteIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.desc); - }); - } - - QueryBuilder sortByShared() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shared', Sort.asc); - }); - } - - QueryBuilder sortBySharedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shared', Sort.desc); - }); - } - - QueryBuilder sortBySortOrder() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'sortOrder', Sort.asc); - }); - } - - QueryBuilder sortBySortOrderDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'sortOrder', Sort.desc); - }); - } - - QueryBuilder sortByStartDate() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'startDate', Sort.asc); - }); - } - - QueryBuilder sortByStartDateDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'startDate', Sort.desc); - }); - } -} - -extension AlbumQuerySortThenBy on QueryBuilder { - QueryBuilder thenByActivityEnabled() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'activityEnabled', Sort.asc); - }); - } - - QueryBuilder thenByActivityEnabledDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'activityEnabled', Sort.desc); - }); - } - - QueryBuilder thenByCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'createdAt', Sort.asc); - }); - } - - QueryBuilder thenByCreatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'createdAt', Sort.desc); - }); - } - - QueryBuilder thenByDescription() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.asc); - }); - } - - QueryBuilder thenByDescriptionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.desc); - }); - } - - QueryBuilder thenByEndDate() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'endDate', Sort.asc); - }); - } - - QueryBuilder thenByEndDateDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'endDate', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByLastModifiedAssetTimestamp() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.asc); - }); - } - - QueryBuilder - thenByLastModifiedAssetTimestampDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.desc); - }); - } - - QueryBuilder thenByLocalId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.asc); - }); - } - - QueryBuilder thenByLocalIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.desc); - }); - } - - QueryBuilder thenByModifiedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedAt', Sort.asc); - }); - } - - QueryBuilder thenByModifiedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedAt', Sort.desc); - }); - } - - QueryBuilder thenByName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.asc); - }); - } - - QueryBuilder thenByNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.desc); - }); - } - - QueryBuilder thenByRemoteId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.asc); - }); - } - - QueryBuilder thenByRemoteIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.desc); - }); - } - - QueryBuilder thenByShared() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shared', Sort.asc); - }); - } - - QueryBuilder thenBySharedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shared', Sort.desc); - }); - } - - QueryBuilder thenBySortOrder() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'sortOrder', Sort.asc); - }); - } - - QueryBuilder thenBySortOrderDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'sortOrder', Sort.desc); - }); - } - - QueryBuilder thenByStartDate() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'startDate', Sort.asc); - }); - } - - QueryBuilder thenByStartDateDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'startDate', Sort.desc); - }); - } -} - -extension AlbumQueryWhereDistinct on QueryBuilder { - QueryBuilder distinctByActivityEnabled() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'activityEnabled'); - }); - } - - QueryBuilder distinctByCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'createdAt'); - }); - } - - QueryBuilder distinctByDescription({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'description', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByEndDate() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'endDate'); - }); - } - - QueryBuilder distinctByLastModifiedAssetTimestamp() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'lastModifiedAssetTimestamp'); - }); - } - - QueryBuilder distinctByLocalId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'localId', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByModifiedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'modifiedAt'); - }); - } - - QueryBuilder distinctByName({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'name', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByRemoteId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'remoteId', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByShared() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'shared'); - }); - } - - QueryBuilder distinctBySortOrder() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'sortOrder'); - }); - } - - QueryBuilder distinctByStartDate() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'startDate'); - }); - } -} - -extension AlbumQueryProperty on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder activityEnabledProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'activityEnabled'); - }); - } - - QueryBuilder createdAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'createdAt'); - }); - } - - QueryBuilder descriptionProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'description'); - }); - } - - QueryBuilder endDateProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'endDate'); - }); - } - - QueryBuilder - lastModifiedAssetTimestampProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'lastModifiedAssetTimestamp'); - }); - } - - QueryBuilder localIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'localId'); - }); - } - - QueryBuilder modifiedAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'modifiedAt'); - }); - } - - QueryBuilder nameProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'name'); - }); - } - - QueryBuilder remoteIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'remoteId'); - }); - } - - QueryBuilder sharedProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'shared'); - }); - } - - QueryBuilder sortOrderProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'sortOrder'); - }); - } - - QueryBuilder startDateProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'startDate'); - }); - } -} diff --git a/mobile/lib/entities/android_device_asset.entity.dart b/mobile/lib/entities/android_device_asset.entity.dart deleted file mode 100644 index 792de346b9..0000000000 --- a/mobile/lib/entities/android_device_asset.entity.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:immich_mobile/entities/device_asset.entity.dart'; -import 'package:isar/isar.dart'; - -part 'android_device_asset.entity.g.dart'; - -@Collection() -class AndroidDeviceAsset extends DeviceAsset { - AndroidDeviceAsset({required this.id, required super.hash}); - Id id; -} diff --git a/mobile/lib/entities/android_device_asset.entity.g.dart b/mobile/lib/entities/android_device_asset.entity.g.dart deleted file mode 100644 index f8b1e32c72..0000000000 --- a/mobile/lib/entities/android_device_asset.entity.g.dart +++ /dev/null @@ -1,463 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'android_device_asset.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetAndroidDeviceAssetCollection on Isar { - IsarCollection get androidDeviceAssets => - this.collection(); -} - -const AndroidDeviceAssetSchema = CollectionSchema( - name: r'AndroidDeviceAsset', - id: -6758387181232899335, - properties: { - r'hash': PropertySchema(id: 0, name: r'hash', type: IsarType.byteList), - }, - - estimateSize: _androidDeviceAssetEstimateSize, - serialize: _androidDeviceAssetSerialize, - deserialize: _androidDeviceAssetDeserialize, - deserializeProp: _androidDeviceAssetDeserializeProp, - idName: r'id', - indexes: { - r'hash': IndexSchema( - id: -7973251393006690288, - name: r'hash', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'hash', - type: IndexType.hash, - caseSensitive: false, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {}, - - getId: _androidDeviceAssetGetId, - getLinks: _androidDeviceAssetGetLinks, - attach: _androidDeviceAssetAttach, - version: '3.3.0-dev.3', -); - -int _androidDeviceAssetEstimateSize( - AndroidDeviceAsset object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.hash.length; - return bytesCount; -} - -void _androidDeviceAssetSerialize( - AndroidDeviceAsset object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeByteList(offsets[0], object.hash); -} - -AndroidDeviceAsset _androidDeviceAssetDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = AndroidDeviceAsset( - hash: reader.readByteList(offsets[0]) ?? [], - id: id, - ); - return object; -} - -P _androidDeviceAssetDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readByteList(offset) ?? []) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _androidDeviceAssetGetId(AndroidDeviceAsset object) { - return object.id; -} - -List> _androidDeviceAssetGetLinks( - AndroidDeviceAsset object, -) { - return []; -} - -void _androidDeviceAssetAttach( - IsarCollection col, - Id id, - AndroidDeviceAsset object, -) { - object.id = id; -} - -extension AndroidDeviceAssetQueryWhereSort - on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension AndroidDeviceAssetQueryWhere - on QueryBuilder { - QueryBuilder - idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder - idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder - idGreaterThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder - idLessThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder - idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - hashEqualTo(List hash) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'hash', value: [hash]), - ); - }); - } - - QueryBuilder - hashNotEqualTo(List hash) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [], - upper: [hash], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [hash], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [hash], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [], - upper: [hash], - includeUpper: false, - ), - ); - } - }); - } -} - -extension AndroidDeviceAssetQueryFilter - on QueryBuilder { - QueryBuilder - hashElementEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'hash', value: value), - ); - }); - } - - QueryBuilder - hashElementGreaterThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'hash', - value: value, - ), - ); - }); - } - - QueryBuilder - hashElementLessThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'hash', - value: value, - ), - ); - }); - } - - QueryBuilder - hashElementBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'hash', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - hashLengthEqualTo(int length) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', length, true, length, true); - }); - } - - QueryBuilder - hashIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, true, 0, true); - }); - } - - QueryBuilder - hashIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, false, 999999, true); - }); - } - - QueryBuilder - hashLengthLessThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, true, length, include); - }); - } - - QueryBuilder - hashLengthGreaterThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', length, include, 999999, true); - }); - } - - QueryBuilder - hashLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'hash', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } - - QueryBuilder - idEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder - idGreaterThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder - idLessThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder - idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension AndroidDeviceAssetQueryObject - on QueryBuilder {} - -extension AndroidDeviceAssetQueryLinks - on QueryBuilder {} - -extension AndroidDeviceAssetQuerySortBy - on QueryBuilder {} - -extension AndroidDeviceAssetQuerySortThenBy - on QueryBuilder { - QueryBuilder - thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder - thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } -} - -extension AndroidDeviceAssetQueryWhereDistinct - on QueryBuilder { - QueryBuilder - distinctByHash() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'hash'); - }); - } -} - -extension AndroidDeviceAssetQueryProperty - on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder, QQueryOperations> hashProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'hash'); - }); - } -} diff --git a/mobile/lib/entities/asset.entity.dart b/mobile/lib/entities/asset.entity.dart deleted file mode 100644 index 0d549457a1..0000000000 --- a/mobile/lib/entities/asset.entity.dart +++ /dev/null @@ -1,575 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/extensions/string_extensions.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart' as entity; -import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; -import 'package:immich_mobile/utils/diff.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; -import 'package:openapi/api.dart'; -import 'package:path/path.dart' as p; -import 'package:photo_manager/photo_manager.dart' show AssetEntity; - -part 'asset.entity.g.dart'; - -/// Asset (online or local) -@Collection(inheritance: false) -class Asset { - Asset.remote(AssetResponseDto remote) - : remoteId = remote.id, - checksum = remote.checksum, - fileCreatedAt = remote.fileCreatedAt, - fileModifiedAt = remote.fileModifiedAt, - updatedAt = remote.updatedAt, - durationInSeconds = remote.duration.toDuration()?.inSeconds ?? 0, - type = remote.type.toAssetType(), - fileName = remote.originalFileName, - height = remote.exifInfo?.exifImageHeight?.toInt(), - width = remote.exifInfo?.exifImageWidth?.toInt(), - livePhotoVideoId = remote.livePhotoVideoId, - ownerId = fastHash(remote.ownerId), - exifInfo = remote.exifInfo == null ? null : ExifDtoConverter.fromDto(remote.exifInfo!), - isFavorite = remote.isFavorite, - isArchived = remote.isArchived, - isTrashed = remote.isTrashed, - isOffline = remote.isOffline, - // workaround to nullify stackPrimaryAssetId for the parent asset until we refactor the mobile app - // stack handling to properly handle it - stackPrimaryAssetId = remote.stack?.primaryAssetId == remote.id ? null : remote.stack?.primaryAssetId, - stackCount = remote.stack?.assetCount ?? 0, - stackId = remote.stack?.id, - thumbhash = remote.thumbhash, - visibility = getVisibility(remote.visibility); - - Asset({ - this.id = Isar.autoIncrement, - required this.checksum, - this.remoteId, - required this.localId, - required this.ownerId, - required this.fileCreatedAt, - required this.fileModifiedAt, - required this.updatedAt, - required this.durationInSeconds, - required this.type, - this.width, - this.height, - required this.fileName, - this.livePhotoVideoId, - this.exifInfo, - this.isFavorite = false, - this.isArchived = false, - this.isTrashed = false, - this.stackId, - this.stackPrimaryAssetId, - this.stackCount = 0, - this.isOffline = false, - this.thumbhash, - this.visibility = AssetVisibilityEnum.timeline, - }); - - @ignore - AssetEntity? _local; - - @ignore - AssetEntity? get local { - if (isLocal && _local == null) { - _local = AssetEntity( - id: localId!, - typeInt: isImage ? 1 : 2, - width: width ?? 0, - height: height ?? 0, - duration: durationInSeconds, - createDateSecond: fileCreatedAt.millisecondsSinceEpoch ~/ 1000, - modifiedDateSecond: fileModifiedAt.millisecondsSinceEpoch ~/ 1000, - title: fileName, - ); - } - return _local; - } - - set local(AssetEntity? assetEntity) => _local = assetEntity; - - @ignore - bool _didUpdateLocal = false; - - @ignore - Future get localAsync async { - final local = this.local; - if (local == null) { - throw Exception('Asset $fileName has no local data'); - } - - final updatedLocal = _didUpdateLocal ? local : await local.obtainForNewProperties(); - if (updatedLocal == null) { - throw Exception('Could not fetch local data for $fileName'); - } - - this.local = updatedLocal; - _didUpdateLocal = true; - return updatedLocal; - } - - Id id = Isar.autoIncrement; - - /// stores the raw SHA1 bytes as a base64 String - /// because Isar cannot sort lists of byte arrays - String checksum; - - String? thumbhash; - - @Index(unique: false, replace: false, type: IndexType.hash) - String? remoteId; - - @Index(unique: false, replace: false, type: IndexType.hash) - String? localId; - - @Index(unique: true, replace: false, composite: [CompositeIndex("checksum", type: IndexType.hash)]) - int ownerId; - - DateTime fileCreatedAt; - - DateTime fileModifiedAt; - - DateTime updatedAt; - - int durationInSeconds; - - @Enumerated(EnumType.ordinal) - AssetType type; - - short? width; - - short? height; - - String fileName; - - String? livePhotoVideoId; - - bool isFavorite; - - bool isArchived; - - bool isTrashed; - - bool isOffline; - - @ignore - ExifInfo? exifInfo; - - String? stackId; - - String? stackPrimaryAssetId; - - int stackCount; - - @Enumerated(EnumType.ordinal) - AssetVisibilityEnum visibility; - - /// Returns null if the asset has no sync access to the exif info - @ignore - double? get aspectRatio { - final orientatedWidth = this.orientatedWidth; - final orientatedHeight = this.orientatedHeight; - - if (orientatedWidth != null && orientatedHeight != null && orientatedWidth > 0 && orientatedHeight > 0) { - return orientatedWidth.toDouble() / orientatedHeight.toDouble(); - } - - return null; - } - - /// `true` if this [Asset] is present on the device - @ignore - bool get isLocal => localId != null; - - @ignore - bool get isInDb => id != Isar.autoIncrement; - - @ignore - String get name => p.withoutExtension(fileName); - - /// `true` if this [Asset] is present on the server - @ignore - bool get isRemote => remoteId != null; - - @ignore - bool get isImage => type == AssetType.image; - - @ignore - bool get isVideo => type == AssetType.video; - - @ignore - bool get isMotionPhoto => livePhotoVideoId != null; - - @ignore - AssetState get storage { - if (isRemote && isLocal) { - return AssetState.merged; - } else if (isRemote) { - return AssetState.remote; - } else if (isLocal) { - return AssetState.local; - } else { - throw Exception("Asset has illegal state: $this"); - } - } - - @ignore - Duration get duration => Duration(seconds: durationInSeconds); - - // ignore: invalid_annotation_target - @ignore - set byteHash(List hash) => checksum = base64.encode(hash); - - /// Returns null if the asset has no sync access to the exif info - @ignore - @pragma('vm:prefer-inline') - bool? get isFlipped { - final exifInfo = this.exifInfo; - if (exifInfo != null) { - return exifInfo.isFlipped; - } - - if (_didUpdateLocal && Platform.isAndroid) { - final local = this.local; - if (local == null) { - throw Exception('Asset $fileName has no local data'); - } - return local.orientation == 90 || local.orientation == 270; - } - - return null; - } - - /// Returns null if the asset has no sync access to the exif info - @ignore - @pragma('vm:prefer-inline') - int? get orientatedHeight { - final isFlipped = this.isFlipped; - if (isFlipped == null) { - return null; - } - - return isFlipped ? width : height; - } - - /// Returns null if the asset has no sync access to the exif info - @ignore - @pragma('vm:prefer-inline') - int? get orientatedWidth { - final isFlipped = this.isFlipped; - if (isFlipped == null) { - return null; - } - - return isFlipped ? height : width; - } - - @override - bool operator ==(other) { - if (other is! Asset) return false; - if (identical(this, other)) return true; - return id == other.id && - checksum == other.checksum && - remoteId == other.remoteId && - localId == other.localId && - ownerId == other.ownerId && - fileCreatedAt.isAtSameMomentAs(other.fileCreatedAt) && - fileModifiedAt.isAtSameMomentAs(other.fileModifiedAt) && - updatedAt.isAtSameMomentAs(other.updatedAt) && - durationInSeconds == other.durationInSeconds && - type == other.type && - width == other.width && - height == other.height && - fileName == other.fileName && - livePhotoVideoId == other.livePhotoVideoId && - isFavorite == other.isFavorite && - isLocal == other.isLocal && - isArchived == other.isArchived && - isTrashed == other.isTrashed && - stackCount == other.stackCount && - stackPrimaryAssetId == other.stackPrimaryAssetId && - stackId == other.stackId; - } - - @override - @ignore - int get hashCode => - id.hashCode ^ - checksum.hashCode ^ - remoteId.hashCode ^ - localId.hashCode ^ - ownerId.hashCode ^ - fileCreatedAt.hashCode ^ - fileModifiedAt.hashCode ^ - updatedAt.hashCode ^ - durationInSeconds.hashCode ^ - type.hashCode ^ - width.hashCode ^ - height.hashCode ^ - fileName.hashCode ^ - livePhotoVideoId.hashCode ^ - isFavorite.hashCode ^ - isLocal.hashCode ^ - isArchived.hashCode ^ - isTrashed.hashCode ^ - stackCount.hashCode ^ - stackPrimaryAssetId.hashCode ^ - stackId.hashCode; - - /// Returns `true` if this [Asset] can updated with values from parameter [a] - bool canUpdate(Asset a) { - assert(isInDb); - assert(checksum == a.checksum); - assert(a.storage != AssetState.merged); - return a.updatedAt.isAfter(updatedAt) || - a.isRemote && !isRemote || - a.isLocal && !isLocal || - width == null && a.width != null || - height == null && a.height != null || - livePhotoVideoId == null && a.livePhotoVideoId != null || - isFavorite != a.isFavorite || - isArchived != a.isArchived || - isTrashed != a.isTrashed || - isOffline != a.isOffline || - a.exifInfo?.latitude != exifInfo?.latitude || - a.exifInfo?.longitude != exifInfo?.longitude || - // no local stack count or different count from remote - a.thumbhash != thumbhash || - stackId != a.stackId || - stackCount != a.stackCount || - stackPrimaryAssetId == null && a.stackPrimaryAssetId != null || - visibility != a.visibility; - } - - /// Returns a new [Asset] with values from this and merged & updated with [a] - Asset updatedCopy(Asset a) { - assert(canUpdate(a)); - if (a.updatedAt.isAfter(updatedAt)) { - // take most values from newer asset - // keep vales that can never be set by the asset not in DB - if (a.isRemote) { - return a.copyWith( - id: id, - localId: localId, - width: a.width ?? width, - height: a.height ?? height, - exifInfo: a.exifInfo?.copyWith(assetId: id) ?? exifInfo, - ); - } else if (isRemote) { - return copyWith( - localId: localId ?? a.localId, - width: width ?? a.width, - height: height ?? a.height, - exifInfo: exifInfo ?? a.exifInfo?.copyWith(assetId: id), - ); - } else { - // TODO: Revisit this and remove all bool field assignments - return a.copyWith( - id: id, - remoteId: remoteId, - livePhotoVideoId: livePhotoVideoId, - // workaround to nullify stackPrimaryAssetId for the parent asset until we refactor the mobile app - // stack handling to properly handle it - stackId: stackId, - stackPrimaryAssetId: stackPrimaryAssetId == remoteId ? null : stackPrimaryAssetId, - stackCount: stackCount, - isFavorite: isFavorite, - isArchived: isArchived, - isTrashed: isTrashed, - isOffline: isOffline, - ); - } - } else { - // fill in potentially missing values, i.e. merge assets - if (a.isRemote) { - // values from remote take precedence - return copyWith( - remoteId: a.remoteId, - width: a.width, - height: a.height, - livePhotoVideoId: a.livePhotoVideoId, - // workaround to nullify stackPrimaryAssetId for the parent asset until we refactor the mobile app - // stack handling to properly handle it - stackId: a.stackId, - stackPrimaryAssetId: a.stackPrimaryAssetId == a.remoteId ? null : a.stackPrimaryAssetId, - stackCount: a.stackCount, - // isFavorite + isArchived are not set by device-only assets - isFavorite: a.isFavorite, - isArchived: a.isArchived, - isTrashed: a.isTrashed, - isOffline: a.isOffline, - exifInfo: a.exifInfo?.copyWith(assetId: id) ?? exifInfo, - thumbhash: a.thumbhash, - ); - } else { - // add only missing values (and set isLocal to true) - return copyWith( - localId: localId ?? a.localId, - width: width ?? a.width, - height: height ?? a.height, - exifInfo: exifInfo ?? a.exifInfo?.copyWith(assetId: id), // updated to use assetId - ); - } - } - } - - Asset copyWith({ - Id? id, - String? checksum, - String? remoteId, - String? localId, - int? ownerId, - DateTime? fileCreatedAt, - DateTime? fileModifiedAt, - DateTime? updatedAt, - int? durationInSeconds, - AssetType? type, - short? width, - short? height, - String? fileName, - String? livePhotoVideoId, - bool? isFavorite, - bool? isArchived, - bool? isTrashed, - bool? isOffline, - ExifInfo? exifInfo, - String? stackId, - String? stackPrimaryAssetId, - int? stackCount, - String? thumbhash, - AssetVisibilityEnum? visibility, - }) => Asset( - id: id ?? this.id, - checksum: checksum ?? this.checksum, - remoteId: remoteId ?? this.remoteId, - localId: localId ?? this.localId, - ownerId: ownerId ?? this.ownerId, - fileCreatedAt: fileCreatedAt ?? this.fileCreatedAt, - fileModifiedAt: fileModifiedAt ?? this.fileModifiedAt, - updatedAt: updatedAt ?? this.updatedAt, - durationInSeconds: durationInSeconds ?? this.durationInSeconds, - type: type ?? this.type, - width: width ?? this.width, - height: height ?? this.height, - fileName: fileName ?? this.fileName, - livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, - isFavorite: isFavorite ?? this.isFavorite, - isArchived: isArchived ?? this.isArchived, - isTrashed: isTrashed ?? this.isTrashed, - isOffline: isOffline ?? this.isOffline, - exifInfo: exifInfo ?? this.exifInfo, - stackId: stackId ?? this.stackId, - stackPrimaryAssetId: stackPrimaryAssetId ?? this.stackPrimaryAssetId, - stackCount: stackCount ?? this.stackCount, - thumbhash: thumbhash ?? this.thumbhash, - visibility: visibility ?? this.visibility, - ); - - Future put(Isar db) async { - await db.assets.put(this); - if (exifInfo != null) { - await db.exifInfos.put(entity.ExifInfo.fromDto(exifInfo!.copyWith(assetId: id))); - } - } - - static int compareById(Asset a, Asset b) => a.id.compareTo(b.id); - - static int compareByLocalId(Asset a, Asset b) => compareToNullable(a.localId, b.localId); - - static int compareByChecksum(Asset a, Asset b) => a.checksum.compareTo(b.checksum); - - static int compareByOwnerChecksum(Asset a, Asset b) { - final int ownerIdOrder = a.ownerId.compareTo(b.ownerId); - if (ownerIdOrder != 0) return ownerIdOrder; - return compareByChecksum(a, b); - } - - static int compareByOwnerChecksumCreatedModified(Asset a, Asset b) { - final int ownerIdOrder = a.ownerId.compareTo(b.ownerId); - if (ownerIdOrder != 0) return ownerIdOrder; - final int checksumOrder = compareByChecksum(a, b); - if (checksumOrder != 0) return checksumOrder; - final int createdOrder = a.fileCreatedAt.compareTo(b.fileCreatedAt); - if (createdOrder != 0) return createdOrder; - return a.fileModifiedAt.compareTo(b.fileModifiedAt); - } - - @override - String toString() { - return """ -{ - "id": ${id == Isar.autoIncrement ? '"N/A"' : id}, - "remoteId": "${remoteId ?? "N/A"}", - "localId": "${localId ?? "N/A"}", - "checksum": "$checksum", - "ownerId": $ownerId, - "livePhotoVideoId": "${livePhotoVideoId ?? "N/A"}", - "stackId": "${stackId ?? "N/A"}", - "stackPrimaryAssetId": "${stackPrimaryAssetId ?? "N/A"}", - "stackCount": "$stackCount", - "fileCreatedAt": "$fileCreatedAt", - "fileModifiedAt": "$fileModifiedAt", - "updatedAt": "$updatedAt", - "durationInSeconds": $durationInSeconds, - "type": "$type", - "fileName": "$fileName", - "isFavorite": $isFavorite, - "isRemote": $isRemote, - "storage": "$storage", - "width": ${width ?? "N/A"}, - "height": ${height ?? "N/A"}, - "isArchived": $isArchived, - "isTrashed": $isTrashed, - "isOffline": $isOffline, - "visibility": "$visibility", -}"""; - } - - static getVisibility(AssetVisibility visibility) => switch (visibility) { - AssetVisibility.archive => AssetVisibilityEnum.archive, - AssetVisibility.hidden => AssetVisibilityEnum.hidden, - AssetVisibility.locked => AssetVisibilityEnum.locked, - AssetVisibility.timeline || _ => AssetVisibilityEnum.timeline, - }; -} - -enum AssetType { - // do not change this order! - other, - image, - video, - audio, -} - -extension AssetTypeEnumHelper on AssetTypeEnum { - AssetType toAssetType() => switch (this) { - AssetTypeEnum.IMAGE => AssetType.image, - AssetTypeEnum.VIDEO => AssetType.video, - AssetTypeEnum.AUDIO => AssetType.audio, - AssetTypeEnum.OTHER => AssetType.other, - _ => throw Exception(), - }; -} - -/// Describes where the information of this asset came from: -/// only from the local device, only from the remote server or merged from both -enum AssetState { local, remote, merged } - -extension AssetsHelper on IsarCollection { - Future deleteAllByRemoteId(Iterable ids) => ids.isEmpty ? Future.value(0) : remote(ids).deleteAll(); - Future deleteAllByLocalId(Iterable ids) => ids.isEmpty ? Future.value(0) : local(ids).deleteAll(); - Future> getAllByRemoteId(Iterable ids) => ids.isEmpty ? Future.value([]) : remote(ids).findAll(); - Future> getAllByLocalId(Iterable ids) => ids.isEmpty ? Future.value([]) : local(ids).findAll(); - Future getByRemoteId(String id) => where().remoteIdEqualTo(id).findFirst(); - - QueryBuilder remote(Iterable ids) => - where().anyOf(ids, (q, String e) => q.remoteIdEqualTo(e)); - QueryBuilder local(Iterable ids) { - return where().anyOf(ids, (q, String e) => q.localIdEqualTo(e)); - } -} diff --git a/mobile/lib/entities/asset.entity.g.dart b/mobile/lib/entities/asset.entity.g.dart deleted file mode 100644 index db6bc72331..0000000000 --- a/mobile/lib/entities/asset.entity.g.dart +++ /dev/null @@ -1,3711 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'asset.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetAssetCollection on Isar { - IsarCollection get assets => this.collection(); -} - -const AssetSchema = CollectionSchema( - name: r'Asset', - id: -2933289051367723566, - properties: { - r'checksum': PropertySchema( - id: 0, - name: r'checksum', - type: IsarType.string, - ), - r'durationInSeconds': PropertySchema( - id: 1, - name: r'durationInSeconds', - type: IsarType.long, - ), - r'fileCreatedAt': PropertySchema( - id: 2, - name: r'fileCreatedAt', - type: IsarType.dateTime, - ), - r'fileModifiedAt': PropertySchema( - id: 3, - name: r'fileModifiedAt', - type: IsarType.dateTime, - ), - r'fileName': PropertySchema( - id: 4, - name: r'fileName', - type: IsarType.string, - ), - r'height': PropertySchema(id: 5, name: r'height', type: IsarType.int), - r'isArchived': PropertySchema( - id: 6, - name: r'isArchived', - type: IsarType.bool, - ), - r'isFavorite': PropertySchema( - id: 7, - name: r'isFavorite', - type: IsarType.bool, - ), - r'isOffline': PropertySchema( - id: 8, - name: r'isOffline', - type: IsarType.bool, - ), - r'isTrashed': PropertySchema( - id: 9, - name: r'isTrashed', - type: IsarType.bool, - ), - r'livePhotoVideoId': PropertySchema( - id: 10, - name: r'livePhotoVideoId', - type: IsarType.string, - ), - r'localId': PropertySchema(id: 11, name: r'localId', type: IsarType.string), - r'ownerId': PropertySchema(id: 12, name: r'ownerId', type: IsarType.long), - r'remoteId': PropertySchema( - id: 13, - name: r'remoteId', - type: IsarType.string, - ), - r'stackCount': PropertySchema( - id: 14, - name: r'stackCount', - type: IsarType.long, - ), - r'stackId': PropertySchema(id: 15, name: r'stackId', type: IsarType.string), - r'stackPrimaryAssetId': PropertySchema( - id: 16, - name: r'stackPrimaryAssetId', - type: IsarType.string, - ), - r'thumbhash': PropertySchema( - id: 17, - name: r'thumbhash', - type: IsarType.string, - ), - r'type': PropertySchema( - id: 18, - name: r'type', - type: IsarType.byte, - enumMap: _AssettypeEnumValueMap, - ), - r'updatedAt': PropertySchema( - id: 19, - name: r'updatedAt', - type: IsarType.dateTime, - ), - r'visibility': PropertySchema( - id: 20, - name: r'visibility', - type: IsarType.byte, - enumMap: _AssetvisibilityEnumValueMap, - ), - r'width': PropertySchema(id: 21, name: r'width', type: IsarType.int), - }, - - estimateSize: _assetEstimateSize, - serialize: _assetSerialize, - deserialize: _assetDeserialize, - deserializeProp: _assetDeserializeProp, - idName: r'id', - indexes: { - r'remoteId': IndexSchema( - id: 6301175856541681032, - name: r'remoteId', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'remoteId', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - r'localId': IndexSchema( - id: 1199848425898359622, - name: r'localId', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'localId', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - r'ownerId_checksum': IndexSchema( - id: -3295822444433175883, - name: r'ownerId_checksum', - unique: true, - replace: false, - properties: [ - IndexPropertySchema( - name: r'ownerId', - type: IndexType.value, - caseSensitive: false, - ), - IndexPropertySchema( - name: r'checksum', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {}, - - getId: _assetGetId, - getLinks: _assetGetLinks, - attach: _assetAttach, - version: '3.3.0-dev.3', -); - -int _assetEstimateSize( - Asset object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.checksum.length * 3; - bytesCount += 3 + object.fileName.length * 3; - { - final value = object.livePhotoVideoId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.localId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.remoteId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.stackId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.stackPrimaryAssetId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.thumbhash; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - return bytesCount; -} - -void _assetSerialize( - Asset object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeString(offsets[0], object.checksum); - writer.writeLong(offsets[1], object.durationInSeconds); - writer.writeDateTime(offsets[2], object.fileCreatedAt); - writer.writeDateTime(offsets[3], object.fileModifiedAt); - writer.writeString(offsets[4], object.fileName); - writer.writeInt(offsets[5], object.height); - writer.writeBool(offsets[6], object.isArchived); - writer.writeBool(offsets[7], object.isFavorite); - writer.writeBool(offsets[8], object.isOffline); - writer.writeBool(offsets[9], object.isTrashed); - writer.writeString(offsets[10], object.livePhotoVideoId); - writer.writeString(offsets[11], object.localId); - writer.writeLong(offsets[12], object.ownerId); - writer.writeString(offsets[13], object.remoteId); - writer.writeLong(offsets[14], object.stackCount); - writer.writeString(offsets[15], object.stackId); - writer.writeString(offsets[16], object.stackPrimaryAssetId); - writer.writeString(offsets[17], object.thumbhash); - writer.writeByte(offsets[18], object.type.index); - writer.writeDateTime(offsets[19], object.updatedAt); - writer.writeByte(offsets[20], object.visibility.index); - writer.writeInt(offsets[21], object.width); -} - -Asset _assetDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = Asset( - checksum: reader.readString(offsets[0]), - durationInSeconds: reader.readLong(offsets[1]), - fileCreatedAt: reader.readDateTime(offsets[2]), - fileModifiedAt: reader.readDateTime(offsets[3]), - fileName: reader.readString(offsets[4]), - height: reader.readIntOrNull(offsets[5]), - id: id, - isArchived: reader.readBoolOrNull(offsets[6]) ?? false, - isFavorite: reader.readBoolOrNull(offsets[7]) ?? false, - isOffline: reader.readBoolOrNull(offsets[8]) ?? false, - isTrashed: reader.readBoolOrNull(offsets[9]) ?? false, - livePhotoVideoId: reader.readStringOrNull(offsets[10]), - localId: reader.readStringOrNull(offsets[11]), - ownerId: reader.readLong(offsets[12]), - remoteId: reader.readStringOrNull(offsets[13]), - stackCount: reader.readLongOrNull(offsets[14]) ?? 0, - stackId: reader.readStringOrNull(offsets[15]), - stackPrimaryAssetId: reader.readStringOrNull(offsets[16]), - thumbhash: reader.readStringOrNull(offsets[17]), - type: - _AssettypeValueEnumMap[reader.readByteOrNull(offsets[18])] ?? - AssetType.other, - updatedAt: reader.readDateTime(offsets[19]), - visibility: - _AssetvisibilityValueEnumMap[reader.readByteOrNull(offsets[20])] ?? - AssetVisibilityEnum.timeline, - width: reader.readIntOrNull(offsets[21]), - ); - return object; -} - -P _assetDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readString(offset)) as P; - case 1: - return (reader.readLong(offset)) as P; - case 2: - return (reader.readDateTime(offset)) as P; - case 3: - return (reader.readDateTime(offset)) as P; - case 4: - return (reader.readString(offset)) as P; - case 5: - return (reader.readIntOrNull(offset)) as P; - case 6: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 7: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 8: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 9: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 10: - return (reader.readStringOrNull(offset)) as P; - case 11: - return (reader.readStringOrNull(offset)) as P; - case 12: - return (reader.readLong(offset)) as P; - case 13: - return (reader.readStringOrNull(offset)) as P; - case 14: - return (reader.readLongOrNull(offset) ?? 0) as P; - case 15: - return (reader.readStringOrNull(offset)) as P; - case 16: - return (reader.readStringOrNull(offset)) as P; - case 17: - return (reader.readStringOrNull(offset)) as P; - case 18: - return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ?? - AssetType.other) - as P; - case 19: - return (reader.readDateTime(offset)) as P; - case 20: - return (_AssetvisibilityValueEnumMap[reader.readByteOrNull(offset)] ?? - AssetVisibilityEnum.timeline) - as P; - case 21: - return (reader.readIntOrNull(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -const _AssettypeEnumValueMap = {'other': 0, 'image': 1, 'video': 2, 'audio': 3}; -const _AssettypeValueEnumMap = { - 0: AssetType.other, - 1: AssetType.image, - 2: AssetType.video, - 3: AssetType.audio, -}; -const _AssetvisibilityEnumValueMap = { - 'timeline': 0, - 'hidden': 1, - 'archive': 2, - 'locked': 3, -}; -const _AssetvisibilityValueEnumMap = { - 0: AssetVisibilityEnum.timeline, - 1: AssetVisibilityEnum.hidden, - 2: AssetVisibilityEnum.archive, - 3: AssetVisibilityEnum.locked, -}; - -Id _assetGetId(Asset object) { - return object.id; -} - -List> _assetGetLinks(Asset object) { - return []; -} - -void _assetAttach(IsarCollection col, Id id, Asset object) { - object.id = id; -} - -extension AssetByIndex on IsarCollection { - Future getByOwnerIdChecksum(int ownerId, String checksum) { - return getByIndex(r'ownerId_checksum', [ownerId, checksum]); - } - - Asset? getByOwnerIdChecksumSync(int ownerId, String checksum) { - return getByIndexSync(r'ownerId_checksum', [ownerId, checksum]); - } - - Future deleteByOwnerIdChecksum(int ownerId, String checksum) { - return deleteByIndex(r'ownerId_checksum', [ownerId, checksum]); - } - - bool deleteByOwnerIdChecksumSync(int ownerId, String checksum) { - return deleteByIndexSync(r'ownerId_checksum', [ownerId, checksum]); - } - - Future> getAllByOwnerIdChecksum( - List ownerIdValues, - List checksumValues, - ) { - final len = ownerIdValues.length; - assert( - checksumValues.length == len, - 'All index values must have the same length', - ); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([ownerIdValues[i], checksumValues[i]]); - } - - return getAllByIndex(r'ownerId_checksum', values); - } - - List getAllByOwnerIdChecksumSync( - List ownerIdValues, - List checksumValues, - ) { - final len = ownerIdValues.length; - assert( - checksumValues.length == len, - 'All index values must have the same length', - ); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([ownerIdValues[i], checksumValues[i]]); - } - - return getAllByIndexSync(r'ownerId_checksum', values); - } - - Future deleteAllByOwnerIdChecksum( - List ownerIdValues, - List checksumValues, - ) { - final len = ownerIdValues.length; - assert( - checksumValues.length == len, - 'All index values must have the same length', - ); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([ownerIdValues[i], checksumValues[i]]); - } - - return deleteAllByIndex(r'ownerId_checksum', values); - } - - int deleteAllByOwnerIdChecksumSync( - List ownerIdValues, - List checksumValues, - ) { - final len = ownerIdValues.length; - assert( - checksumValues.length == len, - 'All index values must have the same length', - ); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([ownerIdValues[i], checksumValues[i]]); - } - - return deleteAllByIndexSync(r'ownerId_checksum', values); - } - - Future putByOwnerIdChecksum(Asset object) { - return putByIndex(r'ownerId_checksum', object); - } - - Id putByOwnerIdChecksumSync(Asset object, {bool saveLinks = true}) { - return putByIndexSync(r'ownerId_checksum', object, saveLinks: saveLinks); - } - - Future> putAllByOwnerIdChecksum(List objects) { - return putAllByIndex(r'ownerId_checksum', objects); - } - - List putAllByOwnerIdChecksumSync( - List objects, { - bool saveLinks = true, - }) { - return putAllByIndexSync( - r'ownerId_checksum', - objects, - saveLinks: saveLinks, - ); - } -} - -extension AssetQueryWhereSort on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension AssetQueryWhere on QueryBuilder { - QueryBuilder idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder idGreaterThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder idLessThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder remoteIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'remoteId', value: [null]), - ); - }); - } - - QueryBuilder remoteIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [null], - includeLower: false, - upper: [], - ), - ); - }); - } - - QueryBuilder remoteIdEqualTo( - String? remoteId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'remoteId', value: [remoteId]), - ); - }); - } - - QueryBuilder remoteIdNotEqualTo( - String? remoteId, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [], - upper: [remoteId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [remoteId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [remoteId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [], - upper: [remoteId], - includeUpper: false, - ), - ); - } - }); - } - - QueryBuilder localIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'localId', value: [null]), - ); - }); - } - - QueryBuilder localIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [null], - includeLower: false, - upper: [], - ), - ); - }); - } - - QueryBuilder localIdEqualTo( - String? localId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'localId', value: [localId]), - ); - }); - } - - QueryBuilder localIdNotEqualTo( - String? localId, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [], - upper: [localId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [localId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [localId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [], - upper: [localId], - includeUpper: false, - ), - ); - } - }); - } - - QueryBuilder ownerIdEqualToAnyChecksum( - int ownerId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo( - indexName: r'ownerId_checksum', - value: [ownerId], - ), - ); - }); - } - - QueryBuilder ownerIdNotEqualToAnyChecksum( - int ownerId, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [], - upper: [ownerId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [], - upper: [ownerId], - includeUpper: false, - ), - ); - } - }); - } - - QueryBuilder ownerIdGreaterThanAnyChecksum( - int ownerId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId], - includeLower: include, - upper: [], - ), - ); - }); - } - - QueryBuilder ownerIdLessThanAnyChecksum( - int ownerId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [], - upper: [ownerId], - includeUpper: include, - ), - ); - }); - } - - QueryBuilder ownerIdBetweenAnyChecksum( - int lowerOwnerId, - int upperOwnerId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [lowerOwnerId], - includeLower: includeLower, - upper: [upperOwnerId], - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder ownerIdChecksumEqualTo( - int ownerId, - String checksum, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo( - indexName: r'ownerId_checksum', - value: [ownerId, checksum], - ), - ); - }); - } - - QueryBuilder - ownerIdEqualToChecksumNotEqualTo(int ownerId, String checksum) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId], - upper: [ownerId, checksum], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId, checksum], - includeLower: false, - upper: [ownerId], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId, checksum], - includeLower: false, - upper: [ownerId], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId], - upper: [ownerId, checksum], - includeUpper: false, - ), - ); - } - }); - } -} - -extension AssetQueryFilter on QueryBuilder { - QueryBuilder checksumEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'checksum', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'checksum', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'checksum', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'checksum', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'checksum', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'checksum', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'checksum', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'checksum', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'checksum', value: ''), - ); - }); - } - - QueryBuilder checksumIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'checksum', value: ''), - ); - }); - } - - QueryBuilder durationInSecondsEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'durationInSeconds', value: value), - ); - }); - } - - QueryBuilder - durationInSecondsGreaterThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'durationInSeconds', - value: value, - ), - ); - }); - } - - QueryBuilder durationInSecondsLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'durationInSeconds', - value: value, - ), - ); - }); - } - - QueryBuilder durationInSecondsBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'durationInSeconds', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder fileCreatedAtEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'fileCreatedAt', value: value), - ); - }); - } - - QueryBuilder fileCreatedAtGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'fileCreatedAt', - value: value, - ), - ); - }); - } - - QueryBuilder fileCreatedAtLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'fileCreatedAt', - value: value, - ), - ); - }); - } - - QueryBuilder fileCreatedAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'fileCreatedAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder fileModifiedAtEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'fileModifiedAt', value: value), - ); - }); - } - - QueryBuilder fileModifiedAtGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'fileModifiedAt', - value: value, - ), - ); - }); - } - - QueryBuilder fileModifiedAtLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'fileModifiedAt', - value: value, - ), - ); - }); - } - - QueryBuilder fileModifiedAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'fileModifiedAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder fileNameEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'fileName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'fileName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'fileName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'fileName', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'fileName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'fileName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'fileName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'fileName', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'fileName', value: ''), - ); - }); - } - - QueryBuilder fileNameIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'fileName', value: ''), - ); - }); - } - - QueryBuilder heightIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'height'), - ); - }); - } - - QueryBuilder heightIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'height'), - ); - }); - } - - QueryBuilder heightEqualTo(int? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'height', value: value), - ); - }); - } - - QueryBuilder heightGreaterThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'height', - value: value, - ), - ); - }); - } - - QueryBuilder heightLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'height', - value: value, - ), - ); - }); - } - - QueryBuilder heightBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'height', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder idGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder isArchivedEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isArchived', value: value), - ); - }); - } - - QueryBuilder isFavoriteEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isFavorite', value: value), - ); - }); - } - - QueryBuilder isOfflineEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isOffline', value: value), - ); - }); - } - - QueryBuilder isTrashedEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isTrashed', value: value), - ); - }); - } - - QueryBuilder livePhotoVideoIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'livePhotoVideoId'), - ); - }); - } - - QueryBuilder - livePhotoVideoIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'livePhotoVideoId'), - ); - }); - } - - QueryBuilder livePhotoVideoIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'livePhotoVideoId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'livePhotoVideoId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'livePhotoVideoId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'livePhotoVideoId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'livePhotoVideoId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'livePhotoVideoId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'livePhotoVideoId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'livePhotoVideoId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'livePhotoVideoId', value: ''), - ); - }); - } - - QueryBuilder - livePhotoVideoIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'livePhotoVideoId', value: ''), - ); - }); - } - - QueryBuilder localIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'localId'), - ); - }); - } - - QueryBuilder localIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'localId'), - ); - }); - } - - QueryBuilder localIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'localId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'localId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'localId', value: ''), - ); - }); - } - - QueryBuilder localIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'localId', value: ''), - ); - }); - } - - QueryBuilder ownerIdEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'ownerId', value: value), - ); - }); - } - - QueryBuilder ownerIdGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'ownerId', - value: value, - ), - ); - }); - } - - QueryBuilder ownerIdLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'ownerId', - value: value, - ), - ); - }); - } - - QueryBuilder ownerIdBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'ownerId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder remoteIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'remoteId'), - ); - }); - } - - QueryBuilder remoteIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'remoteId'), - ); - }); - } - - QueryBuilder remoteIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'remoteId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'remoteId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'remoteId', value: ''), - ); - }); - } - - QueryBuilder remoteIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'remoteId', value: ''), - ); - }); - } - - QueryBuilder stackCountEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'stackCount', value: value), - ); - }); - } - - QueryBuilder stackCountGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'stackCount', - value: value, - ), - ); - }); - } - - QueryBuilder stackCountLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'stackCount', - value: value, - ), - ); - }); - } - - QueryBuilder stackCountBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'stackCount', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder stackIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'stackId'), - ); - }); - } - - QueryBuilder stackIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'stackId'), - ); - }); - } - - QueryBuilder stackIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'stackId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'stackId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'stackId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'stackId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'stackId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'stackId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'stackId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'stackId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'stackId', value: ''), - ); - }); - } - - QueryBuilder stackIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'stackId', value: ''), - ); - }); - } - - QueryBuilder - stackPrimaryAssetIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'stackPrimaryAssetId'), - ); - }); - } - - QueryBuilder - stackPrimaryAssetIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'stackPrimaryAssetId'), - ); - }); - } - - QueryBuilder stackPrimaryAssetIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'stackPrimaryAssetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - stackPrimaryAssetIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'stackPrimaryAssetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackPrimaryAssetIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'stackPrimaryAssetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackPrimaryAssetIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'stackPrimaryAssetId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - stackPrimaryAssetIdStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'stackPrimaryAssetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackPrimaryAssetIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'stackPrimaryAssetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackPrimaryAssetIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'stackPrimaryAssetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackPrimaryAssetIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'stackPrimaryAssetId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - stackPrimaryAssetIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'stackPrimaryAssetId', value: ''), - ); - }); - } - - QueryBuilder - stackPrimaryAssetIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - property: r'stackPrimaryAssetId', - value: '', - ), - ); - }); - } - - QueryBuilder thumbhashIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'thumbhash'), - ); - }); - } - - QueryBuilder thumbhashIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'thumbhash'), - ); - }); - } - - QueryBuilder thumbhashEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'thumbhash', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'thumbhash', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'thumbhash', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'thumbhash', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'thumbhash', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'thumbhash', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'thumbhash', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'thumbhash', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'thumbhash', value: ''), - ); - }); - } - - QueryBuilder thumbhashIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'thumbhash', value: ''), - ); - }); - } - - QueryBuilder typeEqualTo( - AssetType value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'type', value: value), - ); - }); - } - - QueryBuilder typeGreaterThan( - AssetType value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'type', - value: value, - ), - ); - }); - } - - QueryBuilder typeLessThan( - AssetType value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'type', - value: value, - ), - ); - }); - } - - QueryBuilder typeBetween( - AssetType lower, - AssetType upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'type', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder updatedAtEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'updatedAt', value: value), - ); - }); - } - - QueryBuilder updatedAtGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'updatedAt', - value: value, - ), - ); - }); - } - - QueryBuilder updatedAtLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'updatedAt', - value: value, - ), - ); - }); - } - - QueryBuilder updatedAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'updatedAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder visibilityEqualTo( - AssetVisibilityEnum value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'visibility', value: value), - ); - }); - } - - QueryBuilder visibilityGreaterThan( - AssetVisibilityEnum value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'visibility', - value: value, - ), - ); - }); - } - - QueryBuilder visibilityLessThan( - AssetVisibilityEnum value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'visibility', - value: value, - ), - ); - }); - } - - QueryBuilder visibilityBetween( - AssetVisibilityEnum lower, - AssetVisibilityEnum upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'visibility', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder widthIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'width'), - ); - }); - } - - QueryBuilder widthIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'width'), - ); - }); - } - - QueryBuilder widthEqualTo(int? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'width', value: value), - ); - }); - } - - QueryBuilder widthGreaterThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'width', - value: value, - ), - ); - }); - } - - QueryBuilder widthLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'width', - value: value, - ), - ); - }); - } - - QueryBuilder widthBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'width', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension AssetQueryObject on QueryBuilder {} - -extension AssetQueryLinks on QueryBuilder {} - -extension AssetQuerySortBy on QueryBuilder { - QueryBuilder sortByChecksum() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'checksum', Sort.asc); - }); - } - - QueryBuilder sortByChecksumDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'checksum', Sort.desc); - }); - } - - QueryBuilder sortByDurationInSeconds() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'durationInSeconds', Sort.asc); - }); - } - - QueryBuilder sortByDurationInSecondsDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'durationInSeconds', Sort.desc); - }); - } - - QueryBuilder sortByFileCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileCreatedAt', Sort.asc); - }); - } - - QueryBuilder sortByFileCreatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileCreatedAt', Sort.desc); - }); - } - - QueryBuilder sortByFileModifiedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileModifiedAt', Sort.asc); - }); - } - - QueryBuilder sortByFileModifiedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileModifiedAt', Sort.desc); - }); - } - - QueryBuilder sortByFileName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileName', Sort.asc); - }); - } - - QueryBuilder sortByFileNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileName', Sort.desc); - }); - } - - QueryBuilder sortByHeight() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'height', Sort.asc); - }); - } - - QueryBuilder sortByHeightDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'height', Sort.desc); - }); - } - - QueryBuilder sortByIsArchived() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isArchived', Sort.asc); - }); - } - - QueryBuilder sortByIsArchivedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isArchived', Sort.desc); - }); - } - - QueryBuilder sortByIsFavorite() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isFavorite', Sort.asc); - }); - } - - QueryBuilder sortByIsFavoriteDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isFavorite', Sort.desc); - }); - } - - QueryBuilder sortByIsOffline() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isOffline', Sort.asc); - }); - } - - QueryBuilder sortByIsOfflineDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isOffline', Sort.desc); - }); - } - - QueryBuilder sortByIsTrashed() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isTrashed', Sort.asc); - }); - } - - QueryBuilder sortByIsTrashedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isTrashed', Sort.desc); - }); - } - - QueryBuilder sortByLivePhotoVideoId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'livePhotoVideoId', Sort.asc); - }); - } - - QueryBuilder sortByLivePhotoVideoIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'livePhotoVideoId', Sort.desc); - }); - } - - QueryBuilder sortByLocalId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.asc); - }); - } - - QueryBuilder sortByLocalIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.desc); - }); - } - - QueryBuilder sortByOwnerId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'ownerId', Sort.asc); - }); - } - - QueryBuilder sortByOwnerIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'ownerId', Sort.desc); - }); - } - - QueryBuilder sortByRemoteId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.asc); - }); - } - - QueryBuilder sortByRemoteIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.desc); - }); - } - - QueryBuilder sortByStackCount() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackCount', Sort.asc); - }); - } - - QueryBuilder sortByStackCountDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackCount', Sort.desc); - }); - } - - QueryBuilder sortByStackId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackId', Sort.asc); - }); - } - - QueryBuilder sortByStackIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackId', Sort.desc); - }); - } - - QueryBuilder sortByStackPrimaryAssetId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackPrimaryAssetId', Sort.asc); - }); - } - - QueryBuilder sortByStackPrimaryAssetIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackPrimaryAssetId', Sort.desc); - }); - } - - QueryBuilder sortByThumbhash() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'thumbhash', Sort.asc); - }); - } - - QueryBuilder sortByThumbhashDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'thumbhash', Sort.desc); - }); - } - - QueryBuilder sortByType() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'type', Sort.asc); - }); - } - - QueryBuilder sortByTypeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'type', Sort.desc); - }); - } - - QueryBuilder sortByUpdatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.asc); - }); - } - - QueryBuilder sortByUpdatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.desc); - }); - } - - QueryBuilder sortByVisibility() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'visibility', Sort.asc); - }); - } - - QueryBuilder sortByVisibilityDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'visibility', Sort.desc); - }); - } - - QueryBuilder sortByWidth() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'width', Sort.asc); - }); - } - - QueryBuilder sortByWidthDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'width', Sort.desc); - }); - } -} - -extension AssetQuerySortThenBy on QueryBuilder { - QueryBuilder thenByChecksum() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'checksum', Sort.asc); - }); - } - - QueryBuilder thenByChecksumDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'checksum', Sort.desc); - }); - } - - QueryBuilder thenByDurationInSeconds() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'durationInSeconds', Sort.asc); - }); - } - - QueryBuilder thenByDurationInSecondsDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'durationInSeconds', Sort.desc); - }); - } - - QueryBuilder thenByFileCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileCreatedAt', Sort.asc); - }); - } - - QueryBuilder thenByFileCreatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileCreatedAt', Sort.desc); - }); - } - - QueryBuilder thenByFileModifiedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileModifiedAt', Sort.asc); - }); - } - - QueryBuilder thenByFileModifiedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileModifiedAt', Sort.desc); - }); - } - - QueryBuilder thenByFileName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileName', Sort.asc); - }); - } - - QueryBuilder thenByFileNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileName', Sort.desc); - }); - } - - QueryBuilder thenByHeight() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'height', Sort.asc); - }); - } - - QueryBuilder thenByHeightDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'height', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIsArchived() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isArchived', Sort.asc); - }); - } - - QueryBuilder thenByIsArchivedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isArchived', Sort.desc); - }); - } - - QueryBuilder thenByIsFavorite() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isFavorite', Sort.asc); - }); - } - - QueryBuilder thenByIsFavoriteDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isFavorite', Sort.desc); - }); - } - - QueryBuilder thenByIsOffline() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isOffline', Sort.asc); - }); - } - - QueryBuilder thenByIsOfflineDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isOffline', Sort.desc); - }); - } - - QueryBuilder thenByIsTrashed() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isTrashed', Sort.asc); - }); - } - - QueryBuilder thenByIsTrashedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isTrashed', Sort.desc); - }); - } - - QueryBuilder thenByLivePhotoVideoId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'livePhotoVideoId', Sort.asc); - }); - } - - QueryBuilder thenByLivePhotoVideoIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'livePhotoVideoId', Sort.desc); - }); - } - - QueryBuilder thenByLocalId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.asc); - }); - } - - QueryBuilder thenByLocalIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.desc); - }); - } - - QueryBuilder thenByOwnerId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'ownerId', Sort.asc); - }); - } - - QueryBuilder thenByOwnerIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'ownerId', Sort.desc); - }); - } - - QueryBuilder thenByRemoteId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.asc); - }); - } - - QueryBuilder thenByRemoteIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.desc); - }); - } - - QueryBuilder thenByStackCount() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackCount', Sort.asc); - }); - } - - QueryBuilder thenByStackCountDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackCount', Sort.desc); - }); - } - - QueryBuilder thenByStackId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackId', Sort.asc); - }); - } - - QueryBuilder thenByStackIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackId', Sort.desc); - }); - } - - QueryBuilder thenByStackPrimaryAssetId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackPrimaryAssetId', Sort.asc); - }); - } - - QueryBuilder thenByStackPrimaryAssetIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackPrimaryAssetId', Sort.desc); - }); - } - - QueryBuilder thenByThumbhash() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'thumbhash', Sort.asc); - }); - } - - QueryBuilder thenByThumbhashDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'thumbhash', Sort.desc); - }); - } - - QueryBuilder thenByType() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'type', Sort.asc); - }); - } - - QueryBuilder thenByTypeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'type', Sort.desc); - }); - } - - QueryBuilder thenByUpdatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.asc); - }); - } - - QueryBuilder thenByUpdatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.desc); - }); - } - - QueryBuilder thenByVisibility() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'visibility', Sort.asc); - }); - } - - QueryBuilder thenByVisibilityDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'visibility', Sort.desc); - }); - } - - QueryBuilder thenByWidth() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'width', Sort.asc); - }); - } - - QueryBuilder thenByWidthDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'width', Sort.desc); - }); - } -} - -extension AssetQueryWhereDistinct on QueryBuilder { - QueryBuilder distinctByChecksum({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'checksum', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByDurationInSeconds() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'durationInSeconds'); - }); - } - - QueryBuilder distinctByFileCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'fileCreatedAt'); - }); - } - - QueryBuilder distinctByFileModifiedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'fileModifiedAt'); - }); - } - - QueryBuilder distinctByFileName({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'fileName', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByHeight() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'height'); - }); - } - - QueryBuilder distinctByIsArchived() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isArchived'); - }); - } - - QueryBuilder distinctByIsFavorite() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isFavorite'); - }); - } - - QueryBuilder distinctByIsOffline() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isOffline'); - }); - } - - QueryBuilder distinctByIsTrashed() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isTrashed'); - }); - } - - QueryBuilder distinctByLivePhotoVideoId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy( - r'livePhotoVideoId', - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder distinctByLocalId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'localId', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByOwnerId() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'ownerId'); - }); - } - - QueryBuilder distinctByRemoteId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'remoteId', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByStackCount() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'stackCount'); - }); - } - - QueryBuilder distinctByStackId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'stackId', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByStackPrimaryAssetId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy( - r'stackPrimaryAssetId', - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder distinctByThumbhash({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'thumbhash', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByType() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'type'); - }); - } - - QueryBuilder distinctByUpdatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'updatedAt'); - }); - } - - QueryBuilder distinctByVisibility() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'visibility'); - }); - } - - QueryBuilder distinctByWidth() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'width'); - }); - } -} - -extension AssetQueryProperty on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder checksumProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'checksum'); - }); - } - - QueryBuilder durationInSecondsProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'durationInSeconds'); - }); - } - - QueryBuilder fileCreatedAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'fileCreatedAt'); - }); - } - - QueryBuilder fileModifiedAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'fileModifiedAt'); - }); - } - - QueryBuilder fileNameProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'fileName'); - }); - } - - QueryBuilder heightProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'height'); - }); - } - - QueryBuilder isArchivedProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isArchived'); - }); - } - - QueryBuilder isFavoriteProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isFavorite'); - }); - } - - QueryBuilder isOfflineProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isOffline'); - }); - } - - QueryBuilder isTrashedProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isTrashed'); - }); - } - - QueryBuilder livePhotoVideoIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'livePhotoVideoId'); - }); - } - - QueryBuilder localIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'localId'); - }); - } - - QueryBuilder ownerIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'ownerId'); - }); - } - - QueryBuilder remoteIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'remoteId'); - }); - } - - QueryBuilder stackCountProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'stackCount'); - }); - } - - QueryBuilder stackIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'stackId'); - }); - } - - QueryBuilder stackPrimaryAssetIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'stackPrimaryAssetId'); - }); - } - - QueryBuilder thumbhashProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'thumbhash'); - }); - } - - QueryBuilder typeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'type'); - }); - } - - QueryBuilder updatedAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'updatedAt'); - }); - } - - QueryBuilder - visibilityProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'visibility'); - }); - } - - QueryBuilder widthProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'width'); - }); - } -} diff --git a/mobile/lib/entities/backup_album.entity.dart b/mobile/lib/entities/backup_album.entity.dart deleted file mode 100644 index ad2a5d6718..0000000000 --- a/mobile/lib/entities/backup_album.entity.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -part 'backup_album.entity.g.dart'; - -@Collection(inheritance: false) -class BackupAlbum { - String id; - DateTime lastBackup; - @Enumerated(EnumType.ordinal) - BackupSelection selection; - - BackupAlbum(this.id, this.lastBackup, this.selection); - - Id get isarId => fastHash(id); - - BackupAlbum copyWith({String? id, DateTime? lastBackup, BackupSelection? selection}) { - return BackupAlbum(id ?? this.id, lastBackup ?? this.lastBackup, selection ?? this.selection); - } -} - -enum BackupSelection { none, select, exclude } diff --git a/mobile/lib/entities/backup_album.entity.g.dart b/mobile/lib/entities/backup_album.entity.g.dart deleted file mode 100644 index 583aa55c4d..0000000000 --- a/mobile/lib/entities/backup_album.entity.g.dart +++ /dev/null @@ -1,679 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'backup_album.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetBackupAlbumCollection on Isar { - IsarCollection get backupAlbums => this.collection(); -} - -const BackupAlbumSchema = CollectionSchema( - name: r'BackupAlbum', - id: 8308487201128361847, - properties: { - r'id': PropertySchema(id: 0, name: r'id', type: IsarType.string), - r'lastBackup': PropertySchema( - id: 1, - name: r'lastBackup', - type: IsarType.dateTime, - ), - r'selection': PropertySchema( - id: 2, - name: r'selection', - type: IsarType.byte, - enumMap: _BackupAlbumselectionEnumValueMap, - ), - }, - - estimateSize: _backupAlbumEstimateSize, - serialize: _backupAlbumSerialize, - deserialize: _backupAlbumDeserialize, - deserializeProp: _backupAlbumDeserializeProp, - idName: r'isarId', - indexes: {}, - links: {}, - embeddedSchemas: {}, - - getId: _backupAlbumGetId, - getLinks: _backupAlbumGetLinks, - attach: _backupAlbumAttach, - version: '3.3.0-dev.3', -); - -int _backupAlbumEstimateSize( - BackupAlbum object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.id.length * 3; - return bytesCount; -} - -void _backupAlbumSerialize( - BackupAlbum object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeString(offsets[0], object.id); - writer.writeDateTime(offsets[1], object.lastBackup); - writer.writeByte(offsets[2], object.selection.index); -} - -BackupAlbum _backupAlbumDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = BackupAlbum( - reader.readString(offsets[0]), - reader.readDateTime(offsets[1]), - _BackupAlbumselectionValueEnumMap[reader.readByteOrNull(offsets[2])] ?? - BackupSelection.none, - ); - return object; -} - -P _backupAlbumDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readString(offset)) as P; - case 1: - return (reader.readDateTime(offset)) as P; - case 2: - return (_BackupAlbumselectionValueEnumMap[reader.readByteOrNull( - offset, - )] ?? - BackupSelection.none) - as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -const _BackupAlbumselectionEnumValueMap = { - 'none': 0, - 'select': 1, - 'exclude': 2, -}; -const _BackupAlbumselectionValueEnumMap = { - 0: BackupSelection.none, - 1: BackupSelection.select, - 2: BackupSelection.exclude, -}; - -Id _backupAlbumGetId(BackupAlbum object) { - return object.isarId; -} - -List> _backupAlbumGetLinks(BackupAlbum object) { - return []; -} - -void _backupAlbumAttach( - IsarCollection col, - Id id, - BackupAlbum object, -) {} - -extension BackupAlbumQueryWhereSort - on QueryBuilder { - QueryBuilder anyIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension BackupAlbumQueryWhere - on QueryBuilder { - QueryBuilder isarIdEqualTo( - Id isarId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between(lower: isarId, upper: isarId), - ); - }); - } - - QueryBuilder isarIdNotEqualTo( - Id isarId, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ); - } - }); - } - - QueryBuilder isarIdGreaterThan( - Id isarId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: include), - ); - }); - } - - QueryBuilder isarIdLessThan( - Id isarId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: include), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lowerIsarId, - Id upperIsarId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerIsarId, - includeLower: includeLower, - upper: upperIsarId, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension BackupAlbumQueryFilter - on QueryBuilder { - QueryBuilder idEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'id', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: ''), - ); - }); - } - - QueryBuilder idIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'id', value: ''), - ); - }); - } - - QueryBuilder isarIdEqualTo( - Id value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isarId', value: value), - ); - }); - } - - QueryBuilder - isarIdGreaterThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder isarIdLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'isarId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - lastBackupEqualTo(DateTime value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'lastBackup', value: value), - ); - }); - } - - QueryBuilder - lastBackupGreaterThan(DateTime value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'lastBackup', - value: value, - ), - ); - }); - } - - QueryBuilder - lastBackupLessThan(DateTime value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'lastBackup', - value: value, - ), - ); - }); - } - - QueryBuilder - lastBackupBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'lastBackup', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - selectionEqualTo(BackupSelection value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'selection', value: value), - ); - }); - } - - QueryBuilder - selectionGreaterThan(BackupSelection value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'selection', - value: value, - ), - ); - }); - } - - QueryBuilder - selectionLessThan(BackupSelection value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'selection', - value: value, - ), - ); - }); - } - - QueryBuilder - selectionBetween( - BackupSelection lower, - BackupSelection upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'selection', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension BackupAlbumQueryObject - on QueryBuilder {} - -extension BackupAlbumQueryLinks - on QueryBuilder {} - -extension BackupAlbumQuerySortBy - on QueryBuilder { - QueryBuilder sortById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder sortByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder sortByLastBackup() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastBackup', Sort.asc); - }); - } - - QueryBuilder sortByLastBackupDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastBackup', Sort.desc); - }); - } - - QueryBuilder sortBySelection() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'selection', Sort.asc); - }); - } - - QueryBuilder sortBySelectionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'selection', Sort.desc); - }); - } -} - -extension BackupAlbumQuerySortThenBy - on QueryBuilder { - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.asc); - }); - } - - QueryBuilder thenByIsarIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.desc); - }); - } - - QueryBuilder thenByLastBackup() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastBackup', Sort.asc); - }); - } - - QueryBuilder thenByLastBackupDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastBackup', Sort.desc); - }); - } - - QueryBuilder thenBySelection() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'selection', Sort.asc); - }); - } - - QueryBuilder thenBySelectionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'selection', Sort.desc); - }); - } -} - -extension BackupAlbumQueryWhereDistinct - on QueryBuilder { - QueryBuilder distinctById({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'id', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByLastBackup() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'lastBackup'); - }); - } - - QueryBuilder distinctBySelection() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'selection'); - }); - } -} - -extension BackupAlbumQueryProperty - on QueryBuilder { - QueryBuilder isarIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isarId'); - }); - } - - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder lastBackupProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'lastBackup'); - }); - } - - QueryBuilder - selectionProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'selection'); - }); - } -} diff --git a/mobile/lib/entities/device_asset.entity.dart b/mobile/lib/entities/device_asset.entity.dart deleted file mode 100644 index 0973dd4ff8..0000000000 --- a/mobile/lib/entities/device_asset.entity.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:isar/isar.dart'; - -class DeviceAsset { - DeviceAsset({required this.hash}); - - @Index(unique: false, type: IndexType.hash) - List hash; -} diff --git a/mobile/lib/entities/duplicated_asset.entity.dart b/mobile/lib/entities/duplicated_asset.entity.dart deleted file mode 100644 index 9368dc1a52..0000000000 --- a/mobile/lib/entities/duplicated_asset.entity.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -part 'duplicated_asset.entity.g.dart'; - -@Collection(inheritance: false) -class DuplicatedAsset { - String id; - DuplicatedAsset(this.id); - Id get isarId => fastHash(id); -} diff --git a/mobile/lib/entities/duplicated_asset.entity.g.dart b/mobile/lib/entities/duplicated_asset.entity.g.dart deleted file mode 100644 index 80d2f344e6..0000000000 --- a/mobile/lib/entities/duplicated_asset.entity.g.dart +++ /dev/null @@ -1,444 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'duplicated_asset.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetDuplicatedAssetCollection on Isar { - IsarCollection get duplicatedAssets => this.collection(); -} - -const DuplicatedAssetSchema = CollectionSchema( - name: r'DuplicatedAsset', - id: -2679334728174694496, - properties: { - r'id': PropertySchema(id: 0, name: r'id', type: IsarType.string), - }, - - estimateSize: _duplicatedAssetEstimateSize, - serialize: _duplicatedAssetSerialize, - deserialize: _duplicatedAssetDeserialize, - deserializeProp: _duplicatedAssetDeserializeProp, - idName: r'isarId', - indexes: {}, - links: {}, - embeddedSchemas: {}, - - getId: _duplicatedAssetGetId, - getLinks: _duplicatedAssetGetLinks, - attach: _duplicatedAssetAttach, - version: '3.3.0-dev.3', -); - -int _duplicatedAssetEstimateSize( - DuplicatedAsset object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.id.length * 3; - return bytesCount; -} - -void _duplicatedAssetSerialize( - DuplicatedAsset object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeString(offsets[0], object.id); -} - -DuplicatedAsset _duplicatedAssetDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = DuplicatedAsset(reader.readString(offsets[0])); - return object; -} - -P _duplicatedAssetDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readString(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _duplicatedAssetGetId(DuplicatedAsset object) { - return object.isarId; -} - -List> _duplicatedAssetGetLinks(DuplicatedAsset object) { - return []; -} - -void _duplicatedAssetAttach( - IsarCollection col, - Id id, - DuplicatedAsset object, -) {} - -extension DuplicatedAssetQueryWhereSort - on QueryBuilder { - QueryBuilder anyIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension DuplicatedAssetQueryWhere - on QueryBuilder { - QueryBuilder - isarIdEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between(lower: isarId, upper: isarId), - ); - }); - } - - QueryBuilder - isarIdNotEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ); - } - }); - } - - QueryBuilder - isarIdGreaterThan(Id isarId, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: include), - ); - }); - } - - QueryBuilder - isarIdLessThan(Id isarId, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: include), - ); - }); - } - - QueryBuilder - isarIdBetween( - Id lowerIsarId, - Id upperIsarId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerIsarId, - includeLower: includeLower, - upper: upperIsarId, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension DuplicatedAssetQueryFilter - on QueryBuilder { - QueryBuilder - idEqualTo(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idLessThan(String value, {bool include = false, bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'id', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: ''), - ); - }); - } - - QueryBuilder - idIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'id', value: ''), - ); - }); - } - - QueryBuilder - isarIdEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isarId', value: value), - ); - }); - } - - QueryBuilder - isarIdGreaterThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder - isarIdLessThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder - isarIdBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'isarId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension DuplicatedAssetQueryObject - on QueryBuilder {} - -extension DuplicatedAssetQueryLinks - on QueryBuilder {} - -extension DuplicatedAssetQuerySortBy - on QueryBuilder { - QueryBuilder sortById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder sortByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } -} - -extension DuplicatedAssetQuerySortThenBy - on QueryBuilder { - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.asc); - }); - } - - QueryBuilder - thenByIsarIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.desc); - }); - } -} - -extension DuplicatedAssetQueryWhereDistinct - on QueryBuilder { - QueryBuilder distinctById({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'id', caseSensitive: caseSensitive); - }); - } -} - -extension DuplicatedAssetQueryProperty - on QueryBuilder { - QueryBuilder isarIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isarId'); - }); - } - - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } -} diff --git a/mobile/lib/entities/etag.entity.dart b/mobile/lib/entities/etag.entity.dart deleted file mode 100644 index 3b8ef39c61..0000000000 --- a/mobile/lib/entities/etag.entity.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -part 'etag.entity.g.dart'; - -@Collection(inheritance: false) -class ETag { - ETag({required this.id, this.assetCount, this.time}); - Id get isarId => fastHash(id); - @Index(unique: true, replace: true, type: IndexType.hash) - String id; - int? assetCount; - DateTime? time; -} diff --git a/mobile/lib/entities/etag.entity.g.dart b/mobile/lib/entities/etag.entity.g.dart deleted file mode 100644 index 03b4ea9918..0000000000 --- a/mobile/lib/entities/etag.entity.g.dart +++ /dev/null @@ -1,796 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'etag.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetETagCollection on Isar { - IsarCollection get eTags => this.collection(); -} - -const ETagSchema = CollectionSchema( - name: r'ETag', - id: -644290296585643859, - properties: { - r'assetCount': PropertySchema( - id: 0, - name: r'assetCount', - type: IsarType.long, - ), - r'id': PropertySchema(id: 1, name: r'id', type: IsarType.string), - r'time': PropertySchema(id: 2, name: r'time', type: IsarType.dateTime), - }, - - estimateSize: _eTagEstimateSize, - serialize: _eTagSerialize, - deserialize: _eTagDeserialize, - deserializeProp: _eTagDeserializeProp, - idName: r'isarId', - indexes: { - r'id': IndexSchema( - id: -3268401673993471357, - name: r'id', - unique: true, - replace: true, - properties: [ - IndexPropertySchema( - name: r'id', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {}, - - getId: _eTagGetId, - getLinks: _eTagGetLinks, - attach: _eTagAttach, - version: '3.3.0-dev.3', -); - -int _eTagEstimateSize( - ETag object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.id.length * 3; - return bytesCount; -} - -void _eTagSerialize( - ETag object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeLong(offsets[0], object.assetCount); - writer.writeString(offsets[1], object.id); - writer.writeDateTime(offsets[2], object.time); -} - -ETag _eTagDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = ETag( - assetCount: reader.readLongOrNull(offsets[0]), - id: reader.readString(offsets[1]), - time: reader.readDateTimeOrNull(offsets[2]), - ); - return object; -} - -P _eTagDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readLongOrNull(offset)) as P; - case 1: - return (reader.readString(offset)) as P; - case 2: - return (reader.readDateTimeOrNull(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _eTagGetId(ETag object) { - return object.isarId; -} - -List> _eTagGetLinks(ETag object) { - return []; -} - -void _eTagAttach(IsarCollection col, Id id, ETag object) {} - -extension ETagByIndex on IsarCollection { - Future getById(String id) { - return getByIndex(r'id', [id]); - } - - ETag? getByIdSync(String id) { - return getByIndexSync(r'id', [id]); - } - - Future deleteById(String id) { - return deleteByIndex(r'id', [id]); - } - - bool deleteByIdSync(String id) { - return deleteByIndexSync(r'id', [id]); - } - - Future> getAllById(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return getAllByIndex(r'id', values); - } - - List getAllByIdSync(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return getAllByIndexSync(r'id', values); - } - - Future deleteAllById(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return deleteAllByIndex(r'id', values); - } - - int deleteAllByIdSync(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return deleteAllByIndexSync(r'id', values); - } - - Future putById(ETag object) { - return putByIndex(r'id', object); - } - - Id putByIdSync(ETag object, {bool saveLinks = true}) { - return putByIndexSync(r'id', object, saveLinks: saveLinks); - } - - Future> putAllById(List objects) { - return putAllByIndex(r'id', objects); - } - - List putAllByIdSync(List objects, {bool saveLinks = true}) { - return putAllByIndexSync(r'id', objects, saveLinks: saveLinks); - } -} - -extension ETagQueryWhereSort on QueryBuilder { - QueryBuilder anyIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension ETagQueryWhere on QueryBuilder { - QueryBuilder isarIdEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between(lower: isarId, upper: isarId), - ); - }); - } - - QueryBuilder isarIdNotEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ); - } - }); - } - - QueryBuilder isarIdGreaterThan( - Id isarId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: include), - ); - }); - } - - QueryBuilder isarIdLessThan( - Id isarId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: include), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lowerIsarId, - Id upperIsarId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerIsarId, - includeLower: includeLower, - upper: upperIsarId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idEqualTo(String id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'id', value: [id]), - ); - }); - } - - QueryBuilder idNotEqualTo(String id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [], - upper: [id], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [id], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [id], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [], - upper: [id], - includeUpper: false, - ), - ); - } - }); - } -} - -extension ETagQueryFilter on QueryBuilder { - QueryBuilder assetCountIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'assetCount'), - ); - }); - } - - QueryBuilder assetCountIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'assetCount'), - ); - }); - } - - QueryBuilder assetCountEqualTo( - int? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'assetCount', value: value), - ); - }); - } - - QueryBuilder assetCountGreaterThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'assetCount', - value: value, - ), - ); - }); - } - - QueryBuilder assetCountLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'assetCount', - value: value, - ), - ); - }); - } - - QueryBuilder assetCountBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'assetCount', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'id', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: ''), - ); - }); - } - - QueryBuilder idIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'id', value: ''), - ); - }); - } - - QueryBuilder isarIdEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isarId', value: value), - ); - }); - } - - QueryBuilder isarIdGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder isarIdLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'isarId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder timeIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'time'), - ); - }); - } - - QueryBuilder timeIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'time'), - ); - }); - } - - QueryBuilder timeEqualTo(DateTime? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'time', value: value), - ); - }); - } - - QueryBuilder timeGreaterThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'time', - value: value, - ), - ); - }); - } - - QueryBuilder timeLessThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'time', - value: value, - ), - ); - }); - } - - QueryBuilder timeBetween( - DateTime? lower, - DateTime? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'time', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension ETagQueryObject on QueryBuilder {} - -extension ETagQueryLinks on QueryBuilder {} - -extension ETagQuerySortBy on QueryBuilder { - QueryBuilder sortByAssetCount() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetCount', Sort.asc); - }); - } - - QueryBuilder sortByAssetCountDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetCount', Sort.desc); - }); - } - - QueryBuilder sortById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder sortByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder sortByTime() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'time', Sort.asc); - }); - } - - QueryBuilder sortByTimeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'time', Sort.desc); - }); - } -} - -extension ETagQuerySortThenBy on QueryBuilder { - QueryBuilder thenByAssetCount() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetCount', Sort.asc); - }); - } - - QueryBuilder thenByAssetCountDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetCount', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.asc); - }); - } - - QueryBuilder thenByIsarIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.desc); - }); - } - - QueryBuilder thenByTime() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'time', Sort.asc); - }); - } - - QueryBuilder thenByTimeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'time', Sort.desc); - }); - } -} - -extension ETagQueryWhereDistinct on QueryBuilder { - QueryBuilder distinctByAssetCount() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'assetCount'); - }); - } - - QueryBuilder distinctById({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'id', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByTime() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'time'); - }); - } -} - -extension ETagQueryProperty on QueryBuilder { - QueryBuilder isarIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isarId'); - }); - } - - QueryBuilder assetCountProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'assetCount'); - }); - } - - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder timeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'time'); - }); - } -} diff --git a/mobile/lib/entities/ios_device_asset.entity.dart b/mobile/lib/entities/ios_device_asset.entity.dart deleted file mode 100644 index dfd0a660f8..0000000000 --- a/mobile/lib/entities/ios_device_asset.entity.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:immich_mobile/entities/device_asset.entity.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -part 'ios_device_asset.entity.g.dart'; - -@Collection() -class IOSDeviceAsset extends DeviceAsset { - IOSDeviceAsset({required this.id, required super.hash}); - - @Index(replace: true, unique: true, type: IndexType.hash) - String id; - Id get isarId => fastHash(id); -} diff --git a/mobile/lib/entities/ios_device_asset.entity.g.dart b/mobile/lib/entities/ios_device_asset.entity.g.dart deleted file mode 100644 index 252fe127bb..0000000000 --- a/mobile/lib/entities/ios_device_asset.entity.g.dart +++ /dev/null @@ -1,766 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'ios_device_asset.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetIOSDeviceAssetCollection on Isar { - IsarCollection get iOSDeviceAssets => this.collection(); -} - -const IOSDeviceAssetSchema = CollectionSchema( - name: r'IOSDeviceAsset', - id: -1671546753821948030, - properties: { - r'hash': PropertySchema(id: 0, name: r'hash', type: IsarType.byteList), - r'id': PropertySchema(id: 1, name: r'id', type: IsarType.string), - }, - - estimateSize: _iOSDeviceAssetEstimateSize, - serialize: _iOSDeviceAssetSerialize, - deserialize: _iOSDeviceAssetDeserialize, - deserializeProp: _iOSDeviceAssetDeserializeProp, - idName: r'isarId', - indexes: { - r'id': IndexSchema( - id: -3268401673993471357, - name: r'id', - unique: true, - replace: true, - properties: [ - IndexPropertySchema( - name: r'id', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - r'hash': IndexSchema( - id: -7973251393006690288, - name: r'hash', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'hash', - type: IndexType.hash, - caseSensitive: false, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {}, - - getId: _iOSDeviceAssetGetId, - getLinks: _iOSDeviceAssetGetLinks, - attach: _iOSDeviceAssetAttach, - version: '3.3.0-dev.3', -); - -int _iOSDeviceAssetEstimateSize( - IOSDeviceAsset object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.hash.length; - bytesCount += 3 + object.id.length * 3; - return bytesCount; -} - -void _iOSDeviceAssetSerialize( - IOSDeviceAsset object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeByteList(offsets[0], object.hash); - writer.writeString(offsets[1], object.id); -} - -IOSDeviceAsset _iOSDeviceAssetDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = IOSDeviceAsset( - hash: reader.readByteList(offsets[0]) ?? [], - id: reader.readString(offsets[1]), - ); - return object; -} - -P _iOSDeviceAssetDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readByteList(offset) ?? []) as P; - case 1: - return (reader.readString(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _iOSDeviceAssetGetId(IOSDeviceAsset object) { - return object.isarId; -} - -List> _iOSDeviceAssetGetLinks(IOSDeviceAsset object) { - return []; -} - -void _iOSDeviceAssetAttach( - IsarCollection col, - Id id, - IOSDeviceAsset object, -) {} - -extension IOSDeviceAssetByIndex on IsarCollection { - Future getById(String id) { - return getByIndex(r'id', [id]); - } - - IOSDeviceAsset? getByIdSync(String id) { - return getByIndexSync(r'id', [id]); - } - - Future deleteById(String id) { - return deleteByIndex(r'id', [id]); - } - - bool deleteByIdSync(String id) { - return deleteByIndexSync(r'id', [id]); - } - - Future> getAllById(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return getAllByIndex(r'id', values); - } - - List getAllByIdSync(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return getAllByIndexSync(r'id', values); - } - - Future deleteAllById(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return deleteAllByIndex(r'id', values); - } - - int deleteAllByIdSync(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return deleteAllByIndexSync(r'id', values); - } - - Future putById(IOSDeviceAsset object) { - return putByIndex(r'id', object); - } - - Id putByIdSync(IOSDeviceAsset object, {bool saveLinks = true}) { - return putByIndexSync(r'id', object, saveLinks: saveLinks); - } - - Future> putAllById(List objects) { - return putAllByIndex(r'id', objects); - } - - List putAllByIdSync( - List objects, { - bool saveLinks = true, - }) { - return putAllByIndexSync(r'id', objects, saveLinks: saveLinks); - } -} - -extension IOSDeviceAssetQueryWhereSort - on QueryBuilder { - QueryBuilder anyIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension IOSDeviceAssetQueryWhere - on QueryBuilder { - QueryBuilder isarIdEqualTo( - Id isarId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between(lower: isarId, upper: isarId), - ); - }); - } - - QueryBuilder - isarIdNotEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ); - } - }); - } - - QueryBuilder - isarIdGreaterThan(Id isarId, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: include), - ); - }); - } - - QueryBuilder - isarIdLessThan(Id isarId, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: include), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lowerIsarId, - Id upperIsarId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerIsarId, - includeLower: includeLower, - upper: upperIsarId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idEqualTo( - String id, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'id', value: [id]), - ); - }); - } - - QueryBuilder idNotEqualTo( - String id, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [], - upper: [id], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [id], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [id], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [], - upper: [id], - includeUpper: false, - ), - ); - } - }); - } - - QueryBuilder hashEqualTo( - List hash, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'hash', value: [hash]), - ); - }); - } - - QueryBuilder - hashNotEqualTo(List hash) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [], - upper: [hash], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [hash], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [hash], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [], - upper: [hash], - includeUpper: false, - ), - ); - } - }); - } -} - -extension IOSDeviceAssetQueryFilter - on QueryBuilder { - QueryBuilder - hashElementEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'hash', value: value), - ); - }); - } - - QueryBuilder - hashElementGreaterThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'hash', - value: value, - ), - ); - }); - } - - QueryBuilder - hashElementLessThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'hash', - value: value, - ), - ); - }); - } - - QueryBuilder - hashElementBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'hash', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - hashLengthEqualTo(int length) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', length, true, length, true); - }); - } - - QueryBuilder - hashIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, true, 0, true); - }); - } - - QueryBuilder - hashIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, false, 999999, true); - }); - } - - QueryBuilder - hashLengthLessThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, true, length, include); - }); - } - - QueryBuilder - hashLengthGreaterThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', length, include, 999999, true); - }); - } - - QueryBuilder - hashLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'hash', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } - - QueryBuilder idEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idLessThan(String value, {bool include = false, bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'id', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: ''), - ); - }); - } - - QueryBuilder - idIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'id', value: ''), - ); - }); - } - - QueryBuilder - isarIdEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isarId', value: value), - ); - }); - } - - QueryBuilder - isarIdGreaterThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder - isarIdLessThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder - isarIdBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'isarId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension IOSDeviceAssetQueryObject - on QueryBuilder {} - -extension IOSDeviceAssetQueryLinks - on QueryBuilder {} - -extension IOSDeviceAssetQuerySortBy - on QueryBuilder { - QueryBuilder sortById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder sortByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } -} - -extension IOSDeviceAssetQuerySortThenBy - on QueryBuilder { - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.asc); - }); - } - - QueryBuilder - thenByIsarIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.desc); - }); - } -} - -extension IOSDeviceAssetQueryWhereDistinct - on QueryBuilder { - QueryBuilder distinctByHash() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'hash'); - }); - } - - QueryBuilder distinctById({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'id', caseSensitive: caseSensitive); - }); - } -} - -extension IOSDeviceAssetQueryProperty - on QueryBuilder { - QueryBuilder isarIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isarId'); - }); - } - - QueryBuilder, QQueryOperations> hashProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'hash'); - }); - } - - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } -} diff --git a/mobile/lib/entities/store.entity.dart b/mobile/lib/entities/store.entity.dart index 7b59e119d6..17ad88cee9 100644 --- a/mobile/lib/entities/store.entity.dart +++ b/mobile/lib/entities/store.entity.dart @@ -1,38 +1,4 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; // ignore: non_constant_identifier_names final Store = StoreService.I; - -class SSLClientCertStoreVal { - final Uint8List data; - final String? password; - - const SSLClientCertStoreVal(this.data, this.password); - - Future save() async { - final b64Str = base64Encode(data); - await Store.put(StoreKey.sslClientCertData, b64Str); - if (password != null) { - await Store.put(StoreKey.sslClientPasswd, password!); - } - } - - static SSLClientCertStoreVal? load() { - final b64Str = Store.tryGet(StoreKey.sslClientCertData); - if (b64Str == null) { - return null; - } - final Uint8List certData = base64Decode(b64Str); - final passwd = Store.tryGet(StoreKey.sslClientPasswd); - return SSLClientCertStoreVal(certData, passwd); - } - - static Future delete() async { - await Store.delete(StoreKey.sslClientCertData); - await Store.delete(StoreKey.sslClientPasswd); - } -} diff --git a/mobile/lib/extensions/asset_extensions.dart b/mobile/lib/extensions/asset_extensions.dart index f7f98b3da7..73a8ec4d05 100644 --- a/mobile/lib/extensions/asset_extensions.dart +++ b/mobile/lib/extensions/asset_extensions.dart @@ -1,26 +1,9 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart' as isar hide AssetTypeEnumHelper; import 'package:immich_mobile/extensions/string_extensions.dart'; import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; -import 'package:immich_mobile/utils/timezone.dart'; import 'package:openapi/api.dart' as api; -extension TZExtension on isar.Asset { - /// Returns the created time of the asset from the exif info (if available) or from - /// the fileCreatedAt field, adjusted to the timezone value from the exif info along with - /// the timezone offset in [Duration] - (DateTime, Duration) getTZAdjustedTimeAndOffset() { - DateTime dt = fileCreatedAt.toLocal(); - - if (exifInfo?.dateTimeOriginal != null) { - return applyTimezoneOffset(dateTime: exifInfo!.dateTimeOriginal!, timeZone: exifInfo?.timeZone); - } - - return (dt, dt.timeZoneOffset); - } -} - extension DTOToAsset on api.AssetResponseDto { RemoteAsset toDto() { return RemoteAsset( diff --git a/mobile/lib/extensions/collection_extensions.dart b/mobile/lib/extensions/collection_extensions.dart index 541db7ccaf..b861eb0570 100644 --- a/mobile/lib/extensions/collection_extensions.dart +++ b/mobile/lib/extensions/collection_extensions.dart @@ -1,9 +1,6 @@ import 'dart:typed_data'; import 'package:collection/collection.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/utils/hash.dart'; extension ListExtension on List { List uniqueConsecutive({int Function(E a, E b)? compare, void Function(E a, E b)? onDuplicate}) { @@ -40,31 +37,6 @@ extension IntListExtension on Iterable { } } -extension AssetListExtension on Iterable { - /// Returns the assets that are already available in the Immich server - Iterable remoteOnly({void Function()? errorCallback}) { - final bool onlyRemote = every((e) => e.isRemote); - if (!onlyRemote) { - if (errorCallback != null) errorCallback(); - return where((a) => a.isRemote); - } - return this; - } - - /// Returns the assets that are owned by the user passed to the [owner] param - /// If [owner] is null, an empty list is returned - Iterable ownedOnly(UserDto? owner, {void Function()? errorCallback}) { - if (owner == null) return []; - final isarUserId = fastHash(owner.id); - final bool onlyOwned = every((e) => e.ownerId == isarUserId); - if (!onlyOwned) { - if (errorCallback != null) errorCallback(); - return where((a) => a.ownerId == isarUserId); - } - return this; - } -} - extension SortedByProperty on Iterable { Iterable sortedByField(Comparable Function(T e) key) { return sorted((a, b) => key(a).compareTo(key(b))); diff --git a/mobile/lib/extensions/object_extensions.dart b/mobile/lib/extensions/object_extensions.dart new file mode 100644 index 0000000000..4e76532137 --- /dev/null +++ b/mobile/lib/extensions/object_extensions.dart @@ -0,0 +1,3 @@ +extension Let on T { + R let(R Function(T) transform) => transform(this); +} diff --git a/mobile/lib/extensions/translate_extensions.dart b/mobile/lib/extensions/translate_extensions.dart index 7677f3cbd8..b01203a90c 100644 --- a/mobile/lib/extensions/translate_extensions.dart +++ b/mobile/lib/extensions/translate_extensions.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; -import 'package:intl/message_format.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/utils/debug_print.dart'; +import 'package:intl/message_format.dart'; extension StringTranslateExtension on String { String t({BuildContext? context, Map? args}) { diff --git a/mobile/lib/infrastructure/entities/asset_edit.entity.dart b/mobile/lib/infrastructure/entities/asset_edit.entity.dart index 22d059bdb4..87a05ab8fe 100644 --- a/mobile/lib/infrastructure/entities/asset_edit.entity.dart +++ b/mobile/lib/infrastructure/entities/asset_edit.entity.dart @@ -1,8 +1,10 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/asset_edit.model.dart'; +import 'package:immich_mobile/extensions/object_extensions.dart'; import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; +import 'package:openapi/api.dart' hide AssetEditAction; @TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)') class AssetEditEntity extends Table with DriftDefaultsMixin { @@ -27,7 +29,12 @@ final JsonTypeConverter2, Uint8List, Object?> editParameter ); extension AssetEditEntityDataDomainEx on AssetEditEntityData { - AssetEdit toDto() { - return AssetEdit(action: action, parameters: parameters); + AssetEdit? toDto() { + return switch (action) { + AssetEditAction.crop => CropParameters.fromJson(parameters)?.let(CropEdit.new), + AssetEditAction.rotate => RotateParameters.fromJson(parameters)?.let(RotateEdit.new), + AssetEditAction.mirror => MirrorParameters.fromJson(parameters)?.let(MirrorEdit.new), + AssetEditAction.other => null, + }; } } diff --git a/mobile/lib/infrastructure/entities/device_asset.entity.dart b/mobile/lib/infrastructure/entities/device_asset.entity.dart deleted file mode 100644 index e3e4a0d4f4..0000000000 --- a/mobile/lib/infrastructure/entities/device_asset.entity.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'dart:typed_data'; - -import 'package:immich_mobile/domain/models/device_asset.model.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -part 'device_asset.entity.g.dart'; - -@Collection(inheritance: false) -class DeviceAssetEntity { - Id get id => fastHash(assetId); - - @Index(replace: true, unique: true, type: IndexType.hash) - final String assetId; - @Index(unique: false, type: IndexType.hash) - final List hash; - final DateTime modifiedTime; - - const DeviceAssetEntity({required this.assetId, required this.hash, required this.modifiedTime}); - - DeviceAsset toModel() => DeviceAsset(assetId: assetId, hash: Uint8List.fromList(hash), modifiedTime: modifiedTime); - - static DeviceAssetEntity fromDto(DeviceAsset dto) => - DeviceAssetEntity(assetId: dto.assetId, hash: dto.hash, modifiedTime: dto.modifiedTime); -} diff --git a/mobile/lib/infrastructure/entities/device_asset.entity.g.dart b/mobile/lib/infrastructure/entities/device_asset.entity.g.dart deleted file mode 100644 index b6c30aca6f..0000000000 --- a/mobile/lib/infrastructure/entities/device_asset.entity.g.dart +++ /dev/null @@ -1,874 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'device_asset.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetDeviceAssetEntityCollection on Isar { - IsarCollection get deviceAssetEntitys => this.collection(); -} - -const DeviceAssetEntitySchema = CollectionSchema( - name: r'DeviceAssetEntity', - id: 6967030785073446271, - properties: { - r'assetId': PropertySchema(id: 0, name: r'assetId', type: IsarType.string), - r'hash': PropertySchema(id: 1, name: r'hash', type: IsarType.byteList), - r'modifiedTime': PropertySchema( - id: 2, - name: r'modifiedTime', - type: IsarType.dateTime, - ), - }, - - estimateSize: _deviceAssetEntityEstimateSize, - serialize: _deviceAssetEntitySerialize, - deserialize: _deviceAssetEntityDeserialize, - deserializeProp: _deviceAssetEntityDeserializeProp, - idName: r'id', - indexes: { - r'assetId': IndexSchema( - id: 174362542210192109, - name: r'assetId', - unique: true, - replace: true, - properties: [ - IndexPropertySchema( - name: r'assetId', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - r'hash': IndexSchema( - id: -7973251393006690288, - name: r'hash', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'hash', - type: IndexType.hash, - caseSensitive: false, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {}, - - getId: _deviceAssetEntityGetId, - getLinks: _deviceAssetEntityGetLinks, - attach: _deviceAssetEntityAttach, - version: '3.3.0-dev.3', -); - -int _deviceAssetEntityEstimateSize( - DeviceAssetEntity object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.assetId.length * 3; - bytesCount += 3 + object.hash.length; - return bytesCount; -} - -void _deviceAssetEntitySerialize( - DeviceAssetEntity object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeString(offsets[0], object.assetId); - writer.writeByteList(offsets[1], object.hash); - writer.writeDateTime(offsets[2], object.modifiedTime); -} - -DeviceAssetEntity _deviceAssetEntityDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = DeviceAssetEntity( - assetId: reader.readString(offsets[0]), - hash: reader.readByteList(offsets[1]) ?? [], - modifiedTime: reader.readDateTime(offsets[2]), - ); - return object; -} - -P _deviceAssetEntityDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readString(offset)) as P; - case 1: - return (reader.readByteList(offset) ?? []) as P; - case 2: - return (reader.readDateTime(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _deviceAssetEntityGetId(DeviceAssetEntity object) { - return object.id; -} - -List> _deviceAssetEntityGetLinks( - DeviceAssetEntity object, -) { - return []; -} - -void _deviceAssetEntityAttach( - IsarCollection col, - Id id, - DeviceAssetEntity object, -) {} - -extension DeviceAssetEntityByIndex on IsarCollection { - Future getByAssetId(String assetId) { - return getByIndex(r'assetId', [assetId]); - } - - DeviceAssetEntity? getByAssetIdSync(String assetId) { - return getByIndexSync(r'assetId', [assetId]); - } - - Future deleteByAssetId(String assetId) { - return deleteByIndex(r'assetId', [assetId]); - } - - bool deleteByAssetIdSync(String assetId) { - return deleteByIndexSync(r'assetId', [assetId]); - } - - Future> getAllByAssetId(List assetIdValues) { - final values = assetIdValues.map((e) => [e]).toList(); - return getAllByIndex(r'assetId', values); - } - - List getAllByAssetIdSync(List assetIdValues) { - final values = assetIdValues.map((e) => [e]).toList(); - return getAllByIndexSync(r'assetId', values); - } - - Future deleteAllByAssetId(List assetIdValues) { - final values = assetIdValues.map((e) => [e]).toList(); - return deleteAllByIndex(r'assetId', values); - } - - int deleteAllByAssetIdSync(List assetIdValues) { - final values = assetIdValues.map((e) => [e]).toList(); - return deleteAllByIndexSync(r'assetId', values); - } - - Future putByAssetId(DeviceAssetEntity object) { - return putByIndex(r'assetId', object); - } - - Id putByAssetIdSync(DeviceAssetEntity object, {bool saveLinks = true}) { - return putByIndexSync(r'assetId', object, saveLinks: saveLinks); - } - - Future> putAllByAssetId(List objects) { - return putAllByIndex(r'assetId', objects); - } - - List putAllByAssetIdSync( - List objects, { - bool saveLinks = true, - }) { - return putAllByIndexSync(r'assetId', objects, saveLinks: saveLinks); - } -} - -extension DeviceAssetEntityQueryWhereSort - on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension DeviceAssetEntityQueryWhere - on QueryBuilder { - QueryBuilder - idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder - idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder - idGreaterThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder - idLessThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder - idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - assetIdEqualTo(String assetId) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'assetId', value: [assetId]), - ); - }); - } - - QueryBuilder - assetIdNotEqualTo(String assetId) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'assetId', - lower: [], - upper: [assetId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'assetId', - lower: [assetId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'assetId', - lower: [assetId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'assetId', - lower: [], - upper: [assetId], - includeUpper: false, - ), - ); - } - }); - } - - QueryBuilder - hashEqualTo(List hash) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'hash', value: [hash]), - ); - }); - } - - QueryBuilder - hashNotEqualTo(List hash) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [], - upper: [hash], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [hash], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [hash], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [], - upper: [hash], - includeUpper: false, - ), - ); - } - }); - } -} - -extension DeviceAssetEntityQueryFilter - on QueryBuilder { - QueryBuilder - assetIdEqualTo(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'assetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'assetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'assetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'assetId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'assetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'assetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'assetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'assetId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'assetId', value: ''), - ); - }); - } - - QueryBuilder - assetIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'assetId', value: ''), - ); - }); - } - - QueryBuilder - hashElementEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'hash', value: value), - ); - }); - } - - QueryBuilder - hashElementGreaterThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'hash', - value: value, - ), - ); - }); - } - - QueryBuilder - hashElementLessThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'hash', - value: value, - ), - ); - }); - } - - QueryBuilder - hashElementBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'hash', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - hashLengthEqualTo(int length) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', length, true, length, true); - }); - } - - QueryBuilder - hashIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, true, 0, true); - }); - } - - QueryBuilder - hashIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, false, 999999, true); - }); - } - - QueryBuilder - hashLengthLessThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, true, length, include); - }); - } - - QueryBuilder - hashLengthGreaterThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', length, include, 999999, true); - }); - } - - QueryBuilder - hashLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'hash', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } - - QueryBuilder - idEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder - idGreaterThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder - idLessThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder - idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - modifiedTimeEqualTo(DateTime value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'modifiedTime', value: value), - ); - }); - } - - QueryBuilder - modifiedTimeGreaterThan(DateTime value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'modifiedTime', - value: value, - ), - ); - }); - } - - QueryBuilder - modifiedTimeLessThan(DateTime value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'modifiedTime', - value: value, - ), - ); - }); - } - - QueryBuilder - modifiedTimeBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'modifiedTime', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension DeviceAssetEntityQueryObject - on QueryBuilder {} - -extension DeviceAssetEntityQueryLinks - on QueryBuilder {} - -extension DeviceAssetEntityQuerySortBy - on QueryBuilder { - QueryBuilder - sortByAssetId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetId', Sort.asc); - }); - } - - QueryBuilder - sortByAssetIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetId', Sort.desc); - }); - } - - QueryBuilder - sortByModifiedTime() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedTime', Sort.asc); - }); - } - - QueryBuilder - sortByModifiedTimeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedTime', Sort.desc); - }); - } -} - -extension DeviceAssetEntityQuerySortThenBy - on QueryBuilder { - QueryBuilder - thenByAssetId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetId', Sort.asc); - }); - } - - QueryBuilder - thenByAssetIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetId', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder - thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder - thenByModifiedTime() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedTime', Sort.asc); - }); - } - - QueryBuilder - thenByModifiedTimeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedTime', Sort.desc); - }); - } -} - -extension DeviceAssetEntityQueryWhereDistinct - on QueryBuilder { - QueryBuilder - distinctByAssetId({bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'assetId', caseSensitive: caseSensitive); - }); - } - - QueryBuilder - distinctByHash() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'hash'); - }); - } - - QueryBuilder - distinctByModifiedTime() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'modifiedTime'); - }); - } -} - -extension DeviceAssetEntityQueryProperty - on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder assetIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'assetId'); - }); - } - - QueryBuilder, QQueryOperations> hashProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'hash'); - }); - } - - QueryBuilder - modifiedTimeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'modifiedTime'); - }); - } -} diff --git a/mobile/lib/infrastructure/entities/exif.entity.dart b/mobile/lib/infrastructure/entities/exif.entity.dart index 77cae5dbbe..e009029ea7 100644 --- a/mobile/lib/infrastructure/entities/exif.entity.dart +++ b/mobile/lib/infrastructure/entities/exif.entity.dart @@ -4,96 +4,6 @@ import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; -import 'package:isar/isar.dart'; - -part 'exif.entity.g.dart'; - -/// Exif information 1:1 relation with Asset -@Collection(inheritance: false) -class ExifInfo { - final Id? id; - final int? fileSize; - final DateTime? dateTimeOriginal; - final String? timeZone; - final String? make; - final String? model; - final String? lens; - final float? f; - final float? mm; - final short? iso; - final float? exposureSeconds; - final float? lat; - final float? long; - final String? city; - final String? state; - final String? country; - final String? description; - final String? orientation; - - const ExifInfo({ - this.id, - this.fileSize, - this.dateTimeOriginal, - this.timeZone, - this.make, - this.model, - this.lens, - this.f, - this.mm, - this.iso, - this.exposureSeconds, - this.lat, - this.long, - this.city, - this.state, - this.country, - this.description, - this.orientation, - }); - - static ExifInfo fromDto(domain.ExifInfo dto) => ExifInfo( - id: dto.assetId, - fileSize: dto.fileSize, - dateTimeOriginal: dto.dateTimeOriginal, - timeZone: dto.timeZone, - make: dto.make, - model: dto.model, - lens: dto.lens, - f: dto.f, - mm: dto.mm, - iso: dto.iso?.toInt(), - exposureSeconds: dto.exposureSeconds, - lat: dto.latitude, - long: dto.longitude, - city: dto.city, - state: dto.state, - country: dto.country, - description: dto.description, - orientation: dto.orientation, - ); - - domain.ExifInfo toDto() => domain.ExifInfo( - assetId: id, - fileSize: fileSize, - description: description, - orientation: orientation, - timeZone: timeZone, - dateTimeOriginal: dateTimeOriginal, - isFlipped: ExifDtoConverter.isOrientationFlipped(orientation), - latitude: lat, - longitude: long, - city: city, - state: state, - country: country, - make: make, - model: model, - lens: lens, - f: f, - mm: mm, - iso: iso?.toInt(), - exposureSeconds: exposureSeconds, - ); -} @TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)') class RemoteExifEntity extends Table with DriftDefaultsMixin { @@ -152,6 +62,8 @@ extension RemoteExifEntityDataDomainEx on RemoteExifEntityData { fileSize: fileSize, dateTimeOriginal: dateTimeOriginal, rating: rating, + width: width, + height: height, timeZone: timeZone, make: make, model: model, diff --git a/mobile/lib/infrastructure/entities/exif.entity.g.dart b/mobile/lib/infrastructure/entities/exif.entity.g.dart deleted file mode 100644 index ffbfd0d8f0..0000000000 --- a/mobile/lib/infrastructure/entities/exif.entity.g.dart +++ /dev/null @@ -1,3200 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'exif.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetExifInfoCollection on Isar { - IsarCollection get exifInfos => this.collection(); -} - -const ExifInfoSchema = CollectionSchema( - name: r'ExifInfo', - id: -2409260054350835217, - properties: { - r'city': PropertySchema(id: 0, name: r'city', type: IsarType.string), - r'country': PropertySchema(id: 1, name: r'country', type: IsarType.string), - r'dateTimeOriginal': PropertySchema( - id: 2, - name: r'dateTimeOriginal', - type: IsarType.dateTime, - ), - r'description': PropertySchema( - id: 3, - name: r'description', - type: IsarType.string, - ), - r'exposureSeconds': PropertySchema( - id: 4, - name: r'exposureSeconds', - type: IsarType.float, - ), - r'f': PropertySchema(id: 5, name: r'f', type: IsarType.float), - r'fileSize': PropertySchema(id: 6, name: r'fileSize', type: IsarType.long), - r'iso': PropertySchema(id: 7, name: r'iso', type: IsarType.int), - r'lat': PropertySchema(id: 8, name: r'lat', type: IsarType.float), - r'lens': PropertySchema(id: 9, name: r'lens', type: IsarType.string), - r'long': PropertySchema(id: 10, name: r'long', type: IsarType.float), - r'make': PropertySchema(id: 11, name: r'make', type: IsarType.string), - r'mm': PropertySchema(id: 12, name: r'mm', type: IsarType.float), - r'model': PropertySchema(id: 13, name: r'model', type: IsarType.string), - r'orientation': PropertySchema( - id: 14, - name: r'orientation', - type: IsarType.string, - ), - r'state': PropertySchema(id: 15, name: r'state', type: IsarType.string), - r'timeZone': PropertySchema( - id: 16, - name: r'timeZone', - type: IsarType.string, - ), - }, - - estimateSize: _exifInfoEstimateSize, - serialize: _exifInfoSerialize, - deserialize: _exifInfoDeserialize, - deserializeProp: _exifInfoDeserializeProp, - idName: r'id', - indexes: {}, - links: {}, - embeddedSchemas: {}, - - getId: _exifInfoGetId, - getLinks: _exifInfoGetLinks, - attach: _exifInfoAttach, - version: '3.3.0-dev.3', -); - -int _exifInfoEstimateSize( - ExifInfo object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - { - final value = object.city; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.country; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.description; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.lens; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.make; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.model; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.orientation; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.state; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.timeZone; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - return bytesCount; -} - -void _exifInfoSerialize( - ExifInfo object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeString(offsets[0], object.city); - writer.writeString(offsets[1], object.country); - writer.writeDateTime(offsets[2], object.dateTimeOriginal); - writer.writeString(offsets[3], object.description); - writer.writeFloat(offsets[4], object.exposureSeconds); - writer.writeFloat(offsets[5], object.f); - writer.writeLong(offsets[6], object.fileSize); - writer.writeInt(offsets[7], object.iso); - writer.writeFloat(offsets[8], object.lat); - writer.writeString(offsets[9], object.lens); - writer.writeFloat(offsets[10], object.long); - writer.writeString(offsets[11], object.make); - writer.writeFloat(offsets[12], object.mm); - writer.writeString(offsets[13], object.model); - writer.writeString(offsets[14], object.orientation); - writer.writeString(offsets[15], object.state); - writer.writeString(offsets[16], object.timeZone); -} - -ExifInfo _exifInfoDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = ExifInfo( - city: reader.readStringOrNull(offsets[0]), - country: reader.readStringOrNull(offsets[1]), - dateTimeOriginal: reader.readDateTimeOrNull(offsets[2]), - description: reader.readStringOrNull(offsets[3]), - exposureSeconds: reader.readFloatOrNull(offsets[4]), - f: reader.readFloatOrNull(offsets[5]), - fileSize: reader.readLongOrNull(offsets[6]), - id: id, - iso: reader.readIntOrNull(offsets[7]), - lat: reader.readFloatOrNull(offsets[8]), - lens: reader.readStringOrNull(offsets[9]), - long: reader.readFloatOrNull(offsets[10]), - make: reader.readStringOrNull(offsets[11]), - mm: reader.readFloatOrNull(offsets[12]), - model: reader.readStringOrNull(offsets[13]), - orientation: reader.readStringOrNull(offsets[14]), - state: reader.readStringOrNull(offsets[15]), - timeZone: reader.readStringOrNull(offsets[16]), - ); - return object; -} - -P _exifInfoDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readStringOrNull(offset)) as P; - case 1: - return (reader.readStringOrNull(offset)) as P; - case 2: - return (reader.readDateTimeOrNull(offset)) as P; - case 3: - return (reader.readStringOrNull(offset)) as P; - case 4: - return (reader.readFloatOrNull(offset)) as P; - case 5: - return (reader.readFloatOrNull(offset)) as P; - case 6: - return (reader.readLongOrNull(offset)) as P; - case 7: - return (reader.readIntOrNull(offset)) as P; - case 8: - return (reader.readFloatOrNull(offset)) as P; - case 9: - return (reader.readStringOrNull(offset)) as P; - case 10: - return (reader.readFloatOrNull(offset)) as P; - case 11: - return (reader.readStringOrNull(offset)) as P; - case 12: - return (reader.readFloatOrNull(offset)) as P; - case 13: - return (reader.readStringOrNull(offset)) as P; - case 14: - return (reader.readStringOrNull(offset)) as P; - case 15: - return (reader.readStringOrNull(offset)) as P; - case 16: - return (reader.readStringOrNull(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _exifInfoGetId(ExifInfo object) { - return object.id ?? Isar.autoIncrement; -} - -List> _exifInfoGetLinks(ExifInfo object) { - return []; -} - -void _exifInfoAttach(IsarCollection col, Id id, ExifInfo object) {} - -extension ExifInfoQueryWhereSort on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension ExifInfoQueryWhere on QueryBuilder { - QueryBuilder idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder idGreaterThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder idLessThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension ExifInfoQueryFilter - on QueryBuilder { - QueryBuilder cityIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'city'), - ); - }); - } - - QueryBuilder cityIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'city'), - ); - }); - } - - QueryBuilder cityEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'city', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'city', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'city', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'city', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'city', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'city', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'city', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'city', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'city', value: ''), - ); - }); - } - - QueryBuilder cityIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'city', value: ''), - ); - }); - } - - QueryBuilder countryIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'country'), - ); - }); - } - - QueryBuilder countryIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'country'), - ); - }); - } - - QueryBuilder countryEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'country', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'country', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'country', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'country', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'country', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'country', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'country', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'country', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'country', value: ''), - ); - }); - } - - QueryBuilder countryIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'country', value: ''), - ); - }); - } - - QueryBuilder - dateTimeOriginalIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'dateTimeOriginal'), - ); - }); - } - - QueryBuilder - dateTimeOriginalIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'dateTimeOriginal'), - ); - }); - } - - QueryBuilder - dateTimeOriginalEqualTo(DateTime? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'dateTimeOriginal', value: value), - ); - }); - } - - QueryBuilder - dateTimeOriginalGreaterThan(DateTime? value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'dateTimeOriginal', - value: value, - ), - ); - }); - } - - QueryBuilder - dateTimeOriginalLessThan(DateTime? value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'dateTimeOriginal', - value: value, - ), - ); - }); - } - - QueryBuilder - dateTimeOriginalBetween( - DateTime? lower, - DateTime? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'dateTimeOriginal', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder descriptionIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'description'), - ); - }); - } - - QueryBuilder - descriptionIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'description'), - ); - }); - } - - QueryBuilder descriptionEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - descriptionGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'description', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'description', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'description', value: ''), - ); - }); - } - - QueryBuilder - descriptionIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'description', value: ''), - ); - }); - } - - QueryBuilder - exposureSecondsIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'exposureSeconds'), - ); - }); - } - - QueryBuilder - exposureSecondsIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'exposureSeconds'), - ); - }); - } - - QueryBuilder - exposureSecondsEqualTo(double? value, {double epsilon = Query.epsilon}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'exposureSeconds', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder - exposureSecondsGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'exposureSeconds', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder - exposureSecondsLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'exposureSeconds', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder - exposureSecondsBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'exposureSeconds', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder fIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'f'), - ); - }); - } - - QueryBuilder fIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'f'), - ); - }); - } - - QueryBuilder fEqualTo( - double? value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'f', value: value, epsilon: epsilon), - ); - }); - } - - QueryBuilder fGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'f', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder fLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'f', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder fBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'f', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder fileSizeIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'fileSize'), - ); - }); - } - - QueryBuilder fileSizeIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'fileSize'), - ); - }); - } - - QueryBuilder fileSizeEqualTo( - int? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'fileSize', value: value), - ); - }); - } - - QueryBuilder fileSizeGreaterThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'fileSize', - value: value, - ), - ); - }); - } - - QueryBuilder fileSizeLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'fileSize', - value: value, - ), - ); - }); - } - - QueryBuilder fileSizeBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'fileSize', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'id'), - ); - }); - } - - QueryBuilder idIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'id'), - ); - }); - } - - QueryBuilder idEqualTo(Id? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder idGreaterThan( - Id? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idLessThan( - Id? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idBetween( - Id? lower, - Id? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder isoIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'iso'), - ); - }); - } - - QueryBuilder isoIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'iso'), - ); - }); - } - - QueryBuilder isoEqualTo( - int? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'iso', value: value), - ); - }); - } - - QueryBuilder isoGreaterThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'iso', - value: value, - ), - ); - }); - } - - QueryBuilder isoLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'iso', - value: value, - ), - ); - }); - } - - QueryBuilder isoBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'iso', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder latIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'lat'), - ); - }); - } - - QueryBuilder latIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'lat'), - ); - }); - } - - QueryBuilder latEqualTo( - double? value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'lat', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder latGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'lat', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder latLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'lat', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder latBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'lat', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder lensIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'lens'), - ); - }); - } - - QueryBuilder lensIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'lens'), - ); - }); - } - - QueryBuilder lensEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'lens', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'lens', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'lens', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'lens', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'lens', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'lens', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'lens', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'lens', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'lens', value: ''), - ); - }); - } - - QueryBuilder lensIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'lens', value: ''), - ); - }); - } - - QueryBuilder longIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'long'), - ); - }); - } - - QueryBuilder longIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'long'), - ); - }); - } - - QueryBuilder longEqualTo( - double? value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'long', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder longGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'long', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder longLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'long', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder longBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'long', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder makeIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'make'), - ); - }); - } - - QueryBuilder makeIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'make'), - ); - }); - } - - QueryBuilder makeEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'make', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'make', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'make', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'make', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'make', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'make', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'make', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'make', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'make', value: ''), - ); - }); - } - - QueryBuilder makeIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'make', value: ''), - ); - }); - } - - QueryBuilder mmIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'mm'), - ); - }); - } - - QueryBuilder mmIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'mm'), - ); - }); - } - - QueryBuilder mmEqualTo( - double? value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'mm', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder mmGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'mm', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder mmLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'mm', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder mmBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'mm', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder modelIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'model'), - ); - }); - } - - QueryBuilder modelIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'model'), - ); - }); - } - - QueryBuilder modelEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'model', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'model', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'model', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'model', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'model', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'model', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'model', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'model', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'model', value: ''), - ); - }); - } - - QueryBuilder modelIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'model', value: ''), - ); - }); - } - - QueryBuilder orientationIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'orientation'), - ); - }); - } - - QueryBuilder - orientationIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'orientation'), - ); - }); - } - - QueryBuilder orientationEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'orientation', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - orientationGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'orientation', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'orientation', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'orientation', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'orientation', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'orientation', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'orientation', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'orientation', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'orientation', value: ''), - ); - }); - } - - QueryBuilder - orientationIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'orientation', value: ''), - ); - }); - } - - QueryBuilder stateIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'state'), - ); - }); - } - - QueryBuilder stateIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'state'), - ); - }); - } - - QueryBuilder stateEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'state', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'state', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'state', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'state', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'state', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'state', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'state', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'state', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'state', value: ''), - ); - }); - } - - QueryBuilder stateIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'state', value: ''), - ); - }); - } - - QueryBuilder timeZoneIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'timeZone'), - ); - }); - } - - QueryBuilder timeZoneIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'timeZone'), - ); - }); - } - - QueryBuilder timeZoneEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'timeZone', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'timeZone', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'timeZone', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'timeZone', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'timeZone', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'timeZone', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'timeZone', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'timeZone', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'timeZone', value: ''), - ); - }); - } - - QueryBuilder timeZoneIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'timeZone', value: ''), - ); - }); - } -} - -extension ExifInfoQueryObject - on QueryBuilder {} - -extension ExifInfoQueryLinks - on QueryBuilder {} - -extension ExifInfoQuerySortBy on QueryBuilder { - QueryBuilder sortByCity() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'city', Sort.asc); - }); - } - - QueryBuilder sortByCityDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'city', Sort.desc); - }); - } - - QueryBuilder sortByCountry() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'country', Sort.asc); - }); - } - - QueryBuilder sortByCountryDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'country', Sort.desc); - }); - } - - QueryBuilder sortByDateTimeOriginal() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'dateTimeOriginal', Sort.asc); - }); - } - - QueryBuilder sortByDateTimeOriginalDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'dateTimeOriginal', Sort.desc); - }); - } - - QueryBuilder sortByDescription() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.asc); - }); - } - - QueryBuilder sortByDescriptionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.desc); - }); - } - - QueryBuilder sortByExposureSeconds() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'exposureSeconds', Sort.asc); - }); - } - - QueryBuilder sortByExposureSecondsDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'exposureSeconds', Sort.desc); - }); - } - - QueryBuilder sortByF() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'f', Sort.asc); - }); - } - - QueryBuilder sortByFDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'f', Sort.desc); - }); - } - - QueryBuilder sortByFileSize() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileSize', Sort.asc); - }); - } - - QueryBuilder sortByFileSizeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileSize', Sort.desc); - }); - } - - QueryBuilder sortByIso() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'iso', Sort.asc); - }); - } - - QueryBuilder sortByIsoDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'iso', Sort.desc); - }); - } - - QueryBuilder sortByLat() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lat', Sort.asc); - }); - } - - QueryBuilder sortByLatDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lat', Sort.desc); - }); - } - - QueryBuilder sortByLens() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lens', Sort.asc); - }); - } - - QueryBuilder sortByLensDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lens', Sort.desc); - }); - } - - QueryBuilder sortByLong() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'long', Sort.asc); - }); - } - - QueryBuilder sortByLongDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'long', Sort.desc); - }); - } - - QueryBuilder sortByMake() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'make', Sort.asc); - }); - } - - QueryBuilder sortByMakeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'make', Sort.desc); - }); - } - - QueryBuilder sortByMm() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mm', Sort.asc); - }); - } - - QueryBuilder sortByMmDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mm', Sort.desc); - }); - } - - QueryBuilder sortByModel() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'model', Sort.asc); - }); - } - - QueryBuilder sortByModelDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'model', Sort.desc); - }); - } - - QueryBuilder sortByOrientation() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'orientation', Sort.asc); - }); - } - - QueryBuilder sortByOrientationDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'orientation', Sort.desc); - }); - } - - QueryBuilder sortByState() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'state', Sort.asc); - }); - } - - QueryBuilder sortByStateDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'state', Sort.desc); - }); - } - - QueryBuilder sortByTimeZone() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'timeZone', Sort.asc); - }); - } - - QueryBuilder sortByTimeZoneDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'timeZone', Sort.desc); - }); - } -} - -extension ExifInfoQuerySortThenBy - on QueryBuilder { - QueryBuilder thenByCity() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'city', Sort.asc); - }); - } - - QueryBuilder thenByCityDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'city', Sort.desc); - }); - } - - QueryBuilder thenByCountry() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'country', Sort.asc); - }); - } - - QueryBuilder thenByCountryDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'country', Sort.desc); - }); - } - - QueryBuilder thenByDateTimeOriginal() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'dateTimeOriginal', Sort.asc); - }); - } - - QueryBuilder thenByDateTimeOriginalDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'dateTimeOriginal', Sort.desc); - }); - } - - QueryBuilder thenByDescription() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.asc); - }); - } - - QueryBuilder thenByDescriptionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.desc); - }); - } - - QueryBuilder thenByExposureSeconds() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'exposureSeconds', Sort.asc); - }); - } - - QueryBuilder thenByExposureSecondsDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'exposureSeconds', Sort.desc); - }); - } - - QueryBuilder thenByF() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'f', Sort.asc); - }); - } - - QueryBuilder thenByFDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'f', Sort.desc); - }); - } - - QueryBuilder thenByFileSize() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileSize', Sort.asc); - }); - } - - QueryBuilder thenByFileSizeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileSize', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIso() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'iso', Sort.asc); - }); - } - - QueryBuilder thenByIsoDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'iso', Sort.desc); - }); - } - - QueryBuilder thenByLat() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lat', Sort.asc); - }); - } - - QueryBuilder thenByLatDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lat', Sort.desc); - }); - } - - QueryBuilder thenByLens() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lens', Sort.asc); - }); - } - - QueryBuilder thenByLensDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lens', Sort.desc); - }); - } - - QueryBuilder thenByLong() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'long', Sort.asc); - }); - } - - QueryBuilder thenByLongDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'long', Sort.desc); - }); - } - - QueryBuilder thenByMake() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'make', Sort.asc); - }); - } - - QueryBuilder thenByMakeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'make', Sort.desc); - }); - } - - QueryBuilder thenByMm() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mm', Sort.asc); - }); - } - - QueryBuilder thenByMmDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mm', Sort.desc); - }); - } - - QueryBuilder thenByModel() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'model', Sort.asc); - }); - } - - QueryBuilder thenByModelDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'model', Sort.desc); - }); - } - - QueryBuilder thenByOrientation() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'orientation', Sort.asc); - }); - } - - QueryBuilder thenByOrientationDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'orientation', Sort.desc); - }); - } - - QueryBuilder thenByState() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'state', Sort.asc); - }); - } - - QueryBuilder thenByStateDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'state', Sort.desc); - }); - } - - QueryBuilder thenByTimeZone() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'timeZone', Sort.asc); - }); - } - - QueryBuilder thenByTimeZoneDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'timeZone', Sort.desc); - }); - } -} - -extension ExifInfoQueryWhereDistinct - on QueryBuilder { - QueryBuilder distinctByCity({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'city', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByCountry({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'country', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByDateTimeOriginal() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'dateTimeOriginal'); - }); - } - - QueryBuilder distinctByDescription({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'description', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByExposureSeconds() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'exposureSeconds'); - }); - } - - QueryBuilder distinctByF() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'f'); - }); - } - - QueryBuilder distinctByFileSize() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'fileSize'); - }); - } - - QueryBuilder distinctByIso() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'iso'); - }); - } - - QueryBuilder distinctByLat() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'lat'); - }); - } - - QueryBuilder distinctByLens({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'lens', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByLong() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'long'); - }); - } - - QueryBuilder distinctByMake({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'make', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByMm() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'mm'); - }); - } - - QueryBuilder distinctByModel({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'model', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByOrientation({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'orientation', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByState({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'state', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByTimeZone({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'timeZone', caseSensitive: caseSensitive); - }); - } -} - -extension ExifInfoQueryProperty - on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder cityProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'city'); - }); - } - - QueryBuilder countryProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'country'); - }); - } - - QueryBuilder - dateTimeOriginalProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'dateTimeOriginal'); - }); - } - - QueryBuilder descriptionProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'description'); - }); - } - - QueryBuilder exposureSecondsProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'exposureSeconds'); - }); - } - - QueryBuilder fProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'f'); - }); - } - - QueryBuilder fileSizeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'fileSize'); - }); - } - - QueryBuilder isoProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'iso'); - }); - } - - QueryBuilder latProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'lat'); - }); - } - - QueryBuilder lensProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'lens'); - }); - } - - QueryBuilder longProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'long'); - }); - } - - QueryBuilder makeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'make'); - }); - } - - QueryBuilder mmProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'mm'); - }); - } - - QueryBuilder modelProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'model'); - }); - } - - QueryBuilder orientationProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'orientation'); - }); - } - - QueryBuilder stateProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'state'); - }); - } - - QueryBuilder timeZoneProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'timeZone'); - }); - } -} diff --git a/mobile/lib/infrastructure/entities/store.entity.dart b/mobile/lib/infrastructure/entities/store.entity.dart index d4b3eec84f..2de8eb713e 100644 --- a/mobile/lib/infrastructure/entities/store.entity.dart +++ b/mobile/lib/infrastructure/entities/store.entity.dart @@ -1,18 +1,5 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; -import 'package:isar/isar.dart'; - -part 'store.entity.g.dart'; - -/// Internal class for `Store`, do not use elsewhere. -@Collection(inheritance: false) -class StoreValue { - final Id id; - final int? intValue; - final String? strValue; - - const StoreValue(this.id, {this.intValue, this.strValue}); -} class StoreEntity extends Table with DriftDefaultsMixin { IntColumn get id => integer()(); diff --git a/mobile/lib/infrastructure/entities/store.entity.g.dart b/mobile/lib/infrastructure/entities/store.entity.g.dart deleted file mode 100644 index 626c3084fe..0000000000 --- a/mobile/lib/infrastructure/entities/store.entity.g.dart +++ /dev/null @@ -1,596 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'store.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetStoreValueCollection on Isar { - IsarCollection get storeValues => this.collection(); -} - -const StoreValueSchema = CollectionSchema( - name: r'StoreValue', - id: 902899285492123510, - properties: { - r'intValue': PropertySchema(id: 0, name: r'intValue', type: IsarType.long), - r'strValue': PropertySchema( - id: 1, - name: r'strValue', - type: IsarType.string, - ), - }, - - estimateSize: _storeValueEstimateSize, - serialize: _storeValueSerialize, - deserialize: _storeValueDeserialize, - deserializeProp: _storeValueDeserializeProp, - idName: r'id', - indexes: {}, - links: {}, - embeddedSchemas: {}, - - getId: _storeValueGetId, - getLinks: _storeValueGetLinks, - attach: _storeValueAttach, - version: '3.3.0-dev.3', -); - -int _storeValueEstimateSize( - StoreValue object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - { - final value = object.strValue; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - return bytesCount; -} - -void _storeValueSerialize( - StoreValue object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeLong(offsets[0], object.intValue); - writer.writeString(offsets[1], object.strValue); -} - -StoreValue _storeValueDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = StoreValue( - id, - intValue: reader.readLongOrNull(offsets[0]), - strValue: reader.readStringOrNull(offsets[1]), - ); - return object; -} - -P _storeValueDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readLongOrNull(offset)) as P; - case 1: - return (reader.readStringOrNull(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _storeValueGetId(StoreValue object) { - return object.id; -} - -List> _storeValueGetLinks(StoreValue object) { - return []; -} - -void _storeValueAttach(IsarCollection col, Id id, StoreValue object) {} - -extension StoreValueQueryWhereSort - on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension StoreValueQueryWhere - on QueryBuilder { - QueryBuilder idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder idGreaterThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder idLessThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension StoreValueQueryFilter - on QueryBuilder { - QueryBuilder idEqualTo( - Id value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder idGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder intValueIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'intValue'), - ); - }); - } - - QueryBuilder - intValueIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'intValue'), - ); - }); - } - - QueryBuilder intValueEqualTo( - int? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'intValue', value: value), - ); - }); - } - - QueryBuilder - intValueGreaterThan(int? value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'intValue', - value: value, - ), - ); - }); - } - - QueryBuilder intValueLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'intValue', - value: value, - ), - ); - }); - } - - QueryBuilder intValueBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'intValue', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder strValueIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'strValue'), - ); - }); - } - - QueryBuilder - strValueIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'strValue'), - ); - }); - } - - QueryBuilder strValueEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'strValue', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - strValueGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'strValue', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder strValueLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'strValue', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder strValueBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'strValue', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - strValueStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'strValue', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder strValueEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'strValue', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder strValueContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'strValue', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder strValueMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'strValue', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - strValueIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'strValue', value: ''), - ); - }); - } - - QueryBuilder - strValueIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'strValue', value: ''), - ); - }); - } -} - -extension StoreValueQueryObject - on QueryBuilder {} - -extension StoreValueQueryLinks - on QueryBuilder {} - -extension StoreValueQuerySortBy - on QueryBuilder { - QueryBuilder sortByIntValue() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'intValue', Sort.asc); - }); - } - - QueryBuilder sortByIntValueDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'intValue', Sort.desc); - }); - } - - QueryBuilder sortByStrValue() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'strValue', Sort.asc); - }); - } - - QueryBuilder sortByStrValueDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'strValue', Sort.desc); - }); - } -} - -extension StoreValueQuerySortThenBy - on QueryBuilder { - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIntValue() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'intValue', Sort.asc); - }); - } - - QueryBuilder thenByIntValueDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'intValue', Sort.desc); - }); - } - - QueryBuilder thenByStrValue() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'strValue', Sort.asc); - }); - } - - QueryBuilder thenByStrValueDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'strValue', Sort.desc); - }); - } -} - -extension StoreValueQueryWhereDistinct - on QueryBuilder { - QueryBuilder distinctByIntValue() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'intValue'); - }); - } - - QueryBuilder distinctByStrValue({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'strValue', caseSensitive: caseSensitive); - }); - } -} - -extension StoreValueQueryProperty - on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder intValueProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'intValue'); - }); - } - - QueryBuilder strValueProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'strValue'); - }); - } -} diff --git a/mobile/lib/infrastructure/entities/user.entity.dart b/mobile/lib/infrastructure/entities/user.entity.dart index 667a9d6a59..8d4371672c 100644 --- a/mobile/lib/infrastructure/entities/user.entity.dart +++ b/mobile/lib/infrastructure/entities/user.entity.dart @@ -1,79 +1,6 @@ import 'package:drift/drift.dart' hide Index; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -part 'user.entity.g.dart'; - -@Collection(inheritance: false) -class User { - Id get isarId => fastHash(id); - @Index(unique: true, replace: false, type: IndexType.hash) - final String id; - final DateTime updatedAt; - final String email; - final String name; - final bool isPartnerSharedBy; - final bool isPartnerSharedWith; - final bool isAdmin; - final String profileImagePath; - @Enumerated(EnumType.ordinal) - final AvatarColor avatarColor; - final bool memoryEnabled; - final bool inTimeline; - final int quotaUsageInBytes; - final int quotaSizeInBytes; - - const User({ - required this.id, - required this.updatedAt, - required this.email, - required this.name, - required this.isAdmin, - this.isPartnerSharedBy = false, - this.isPartnerSharedWith = false, - this.profileImagePath = '', - this.avatarColor = AvatarColor.primary, - this.memoryEnabled = true, - this.inTimeline = false, - this.quotaUsageInBytes = 0, - this.quotaSizeInBytes = 0, - }); - - static User fromDto(UserDto dto) => User( - id: dto.id, - updatedAt: dto.updatedAt ?? DateTime(2025), - email: dto.email, - name: dto.name, - isAdmin: dto.isAdmin, - isPartnerSharedBy: dto.isPartnerSharedBy, - isPartnerSharedWith: dto.isPartnerSharedWith, - profileImagePath: dto.hasProfileImage ? "HAS_PROFILE_IMAGE" : "", - avatarColor: dto.avatarColor, - memoryEnabled: dto.memoryEnabled, - inTimeline: dto.inTimeline, - quotaUsageInBytes: dto.quotaUsageInBytes, - quotaSizeInBytes: dto.quotaSizeInBytes, - ); - - UserDto toDto() => UserDto( - id: id, - email: email, - name: name, - isAdmin: isAdmin, - updatedAt: updatedAt, - avatarColor: avatarColor, - memoryEnabled: memoryEnabled, - inTimeline: inTimeline, - isPartnerSharedBy: isPartnerSharedBy, - isPartnerSharedWith: isPartnerSharedWith, - hasProfileImage: profileImagePath.isNotEmpty, - profileChangedAt: updatedAt, - quotaUsageInBytes: quotaUsageInBytes, - quotaSizeInBytes: quotaSizeInBytes, - ); -} class UserEntity extends Table with DriftDefaultsMixin { const UserEntity(); diff --git a/mobile/lib/infrastructure/entities/user.entity.g.dart b/mobile/lib/infrastructure/entities/user.entity.g.dart deleted file mode 100644 index 7e0af41b77..0000000000 --- a/mobile/lib/infrastructure/entities/user.entity.g.dart +++ /dev/null @@ -1,1854 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'user.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetUserCollection on Isar { - IsarCollection get users => this.collection(); -} - -const UserSchema = CollectionSchema( - name: r'User', - id: -7838171048429979076, - properties: { - r'avatarColor': PropertySchema( - id: 0, - name: r'avatarColor', - type: IsarType.byte, - enumMap: _UseravatarColorEnumValueMap, - ), - r'email': PropertySchema(id: 1, name: r'email', type: IsarType.string), - r'id': PropertySchema(id: 2, name: r'id', type: IsarType.string), - r'inTimeline': PropertySchema( - id: 3, - name: r'inTimeline', - type: IsarType.bool, - ), - r'isAdmin': PropertySchema(id: 4, name: r'isAdmin', type: IsarType.bool), - r'isPartnerSharedBy': PropertySchema( - id: 5, - name: r'isPartnerSharedBy', - type: IsarType.bool, - ), - r'isPartnerSharedWith': PropertySchema( - id: 6, - name: r'isPartnerSharedWith', - type: IsarType.bool, - ), - r'memoryEnabled': PropertySchema( - id: 7, - name: r'memoryEnabled', - type: IsarType.bool, - ), - r'name': PropertySchema(id: 8, name: r'name', type: IsarType.string), - r'profileImagePath': PropertySchema( - id: 9, - name: r'profileImagePath', - type: IsarType.string, - ), - r'quotaSizeInBytes': PropertySchema( - id: 10, - name: r'quotaSizeInBytes', - type: IsarType.long, - ), - r'quotaUsageInBytes': PropertySchema( - id: 11, - name: r'quotaUsageInBytes', - type: IsarType.long, - ), - r'updatedAt': PropertySchema( - id: 12, - name: r'updatedAt', - type: IsarType.dateTime, - ), - }, - - estimateSize: _userEstimateSize, - serialize: _userSerialize, - deserialize: _userDeserialize, - deserializeProp: _userDeserializeProp, - idName: r'isarId', - indexes: { - r'id': IndexSchema( - id: -3268401673993471357, - name: r'id', - unique: true, - replace: false, - properties: [ - IndexPropertySchema( - name: r'id', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {}, - - getId: _userGetId, - getLinks: _userGetLinks, - attach: _userAttach, - version: '3.3.0-dev.3', -); - -int _userEstimateSize( - User object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.email.length * 3; - bytesCount += 3 + object.id.length * 3; - bytesCount += 3 + object.name.length * 3; - bytesCount += 3 + object.profileImagePath.length * 3; - return bytesCount; -} - -void _userSerialize( - User object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeByte(offsets[0], object.avatarColor.index); - writer.writeString(offsets[1], object.email); - writer.writeString(offsets[2], object.id); - writer.writeBool(offsets[3], object.inTimeline); - writer.writeBool(offsets[4], object.isAdmin); - writer.writeBool(offsets[5], object.isPartnerSharedBy); - writer.writeBool(offsets[6], object.isPartnerSharedWith); - writer.writeBool(offsets[7], object.memoryEnabled); - writer.writeString(offsets[8], object.name); - writer.writeString(offsets[9], object.profileImagePath); - writer.writeLong(offsets[10], object.quotaSizeInBytes); - writer.writeLong(offsets[11], object.quotaUsageInBytes); - writer.writeDateTime(offsets[12], object.updatedAt); -} - -User _userDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = User( - avatarColor: - _UseravatarColorValueEnumMap[reader.readByteOrNull(offsets[0])] ?? - AvatarColor.primary, - email: reader.readString(offsets[1]), - id: reader.readString(offsets[2]), - inTimeline: reader.readBoolOrNull(offsets[3]) ?? false, - isAdmin: reader.readBool(offsets[4]), - isPartnerSharedBy: reader.readBoolOrNull(offsets[5]) ?? false, - isPartnerSharedWith: reader.readBoolOrNull(offsets[6]) ?? false, - memoryEnabled: reader.readBoolOrNull(offsets[7]) ?? true, - name: reader.readString(offsets[8]), - profileImagePath: reader.readStringOrNull(offsets[9]) ?? '', - quotaSizeInBytes: reader.readLongOrNull(offsets[10]) ?? 0, - quotaUsageInBytes: reader.readLongOrNull(offsets[11]) ?? 0, - updatedAt: reader.readDateTime(offsets[12]), - ); - return object; -} - -P _userDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (_UseravatarColorValueEnumMap[reader.readByteOrNull(offset)] ?? - AvatarColor.primary) - as P; - case 1: - return (reader.readString(offset)) as P; - case 2: - return (reader.readString(offset)) as P; - case 3: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 4: - return (reader.readBool(offset)) as P; - case 5: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 6: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 7: - return (reader.readBoolOrNull(offset) ?? true) as P; - case 8: - return (reader.readString(offset)) as P; - case 9: - return (reader.readStringOrNull(offset) ?? '') as P; - case 10: - return (reader.readLongOrNull(offset) ?? 0) as P; - case 11: - return (reader.readLongOrNull(offset) ?? 0) as P; - case 12: - return (reader.readDateTime(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -const _UseravatarColorEnumValueMap = { - 'primary': 0, - 'pink': 1, - 'red': 2, - 'yellow': 3, - 'blue': 4, - 'green': 5, - 'purple': 6, - 'orange': 7, - 'gray': 8, - 'amber': 9, -}; -const _UseravatarColorValueEnumMap = { - 0: AvatarColor.primary, - 1: AvatarColor.pink, - 2: AvatarColor.red, - 3: AvatarColor.yellow, - 4: AvatarColor.blue, - 5: AvatarColor.green, - 6: AvatarColor.purple, - 7: AvatarColor.orange, - 8: AvatarColor.gray, - 9: AvatarColor.amber, -}; - -Id _userGetId(User object) { - return object.isarId; -} - -List> _userGetLinks(User object) { - return []; -} - -void _userAttach(IsarCollection col, Id id, User object) {} - -extension UserByIndex on IsarCollection { - Future getById(String id) { - return getByIndex(r'id', [id]); - } - - User? getByIdSync(String id) { - return getByIndexSync(r'id', [id]); - } - - Future deleteById(String id) { - return deleteByIndex(r'id', [id]); - } - - bool deleteByIdSync(String id) { - return deleteByIndexSync(r'id', [id]); - } - - Future> getAllById(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return getAllByIndex(r'id', values); - } - - List getAllByIdSync(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return getAllByIndexSync(r'id', values); - } - - Future deleteAllById(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return deleteAllByIndex(r'id', values); - } - - int deleteAllByIdSync(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return deleteAllByIndexSync(r'id', values); - } - - Future putById(User object) { - return putByIndex(r'id', object); - } - - Id putByIdSync(User object, {bool saveLinks = true}) { - return putByIndexSync(r'id', object, saveLinks: saveLinks); - } - - Future> putAllById(List objects) { - return putAllByIndex(r'id', objects); - } - - List putAllByIdSync(List objects, {bool saveLinks = true}) { - return putAllByIndexSync(r'id', objects, saveLinks: saveLinks); - } -} - -extension UserQueryWhereSort on QueryBuilder { - QueryBuilder anyIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension UserQueryWhere on QueryBuilder { - QueryBuilder isarIdEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between(lower: isarId, upper: isarId), - ); - }); - } - - QueryBuilder isarIdNotEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ); - } - }); - } - - QueryBuilder isarIdGreaterThan( - Id isarId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: include), - ); - }); - } - - QueryBuilder isarIdLessThan( - Id isarId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: include), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lowerIsarId, - Id upperIsarId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerIsarId, - includeLower: includeLower, - upper: upperIsarId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idEqualTo(String id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'id', value: [id]), - ); - }); - } - - QueryBuilder idNotEqualTo(String id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [], - upper: [id], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [id], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [id], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [], - upper: [id], - includeUpper: false, - ), - ); - } - }); - } -} - -extension UserQueryFilter on QueryBuilder { - QueryBuilder avatarColorEqualTo( - AvatarColor value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'avatarColor', value: value), - ); - }); - } - - QueryBuilder avatarColorGreaterThan( - AvatarColor value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'avatarColor', - value: value, - ), - ); - }); - } - - QueryBuilder avatarColorLessThan( - AvatarColor value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'avatarColor', - value: value, - ), - ); - }); - } - - QueryBuilder avatarColorBetween( - AvatarColor lower, - AvatarColor upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'avatarColor', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder emailEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'email', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'email', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'email', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'email', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'email', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'email', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'email', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'email', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'email', value: ''), - ); - }); - } - - QueryBuilder emailIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'email', value: ''), - ); - }); - } - - QueryBuilder idEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'id', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: ''), - ); - }); - } - - QueryBuilder idIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'id', value: ''), - ); - }); - } - - QueryBuilder inTimelineEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'inTimeline', value: value), - ); - }); - } - - QueryBuilder isAdminEqualTo(bool value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isAdmin', value: value), - ); - }); - } - - QueryBuilder isPartnerSharedByEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isPartnerSharedBy', value: value), - ); - }); - } - - QueryBuilder isPartnerSharedWithEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isPartnerSharedWith', value: value), - ); - }); - } - - QueryBuilder isarIdEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isarId', value: value), - ); - }); - } - - QueryBuilder isarIdGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder isarIdLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'isarId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder memoryEnabledEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'memoryEnabled', value: value), - ); - }); - } - - QueryBuilder nameEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'name', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'name', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'name', value: ''), - ); - }); - } - - QueryBuilder nameIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'name', value: ''), - ); - }); - } - - QueryBuilder profileImagePathEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'profileImagePath', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'profileImagePath', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'profileImagePath', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'profileImagePath', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'profileImagePath', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'profileImagePath', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'profileImagePath', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'profileImagePath', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'profileImagePath', value: ''), - ); - }); - } - - QueryBuilder profileImagePathIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'profileImagePath', value: ''), - ); - }); - } - - QueryBuilder quotaSizeInBytesEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'quotaSizeInBytes', value: value), - ); - }); - } - - QueryBuilder quotaSizeInBytesGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'quotaSizeInBytes', - value: value, - ), - ); - }); - } - - QueryBuilder quotaSizeInBytesLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'quotaSizeInBytes', - value: value, - ), - ); - }); - } - - QueryBuilder quotaSizeInBytesBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'quotaSizeInBytes', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder quotaUsageInBytesEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'quotaUsageInBytes', value: value), - ); - }); - } - - QueryBuilder quotaUsageInBytesGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'quotaUsageInBytes', - value: value, - ), - ); - }); - } - - QueryBuilder quotaUsageInBytesLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'quotaUsageInBytes', - value: value, - ), - ); - }); - } - - QueryBuilder quotaUsageInBytesBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'quotaUsageInBytes', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder updatedAtEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'updatedAt', value: value), - ); - }); - } - - QueryBuilder updatedAtGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'updatedAt', - value: value, - ), - ); - }); - } - - QueryBuilder updatedAtLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'updatedAt', - value: value, - ), - ); - }); - } - - QueryBuilder updatedAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'updatedAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension UserQueryObject on QueryBuilder {} - -extension UserQueryLinks on QueryBuilder {} - -extension UserQuerySortBy on QueryBuilder { - QueryBuilder sortByAvatarColor() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'avatarColor', Sort.asc); - }); - } - - QueryBuilder sortByAvatarColorDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'avatarColor', Sort.desc); - }); - } - - QueryBuilder sortByEmail() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'email', Sort.asc); - }); - } - - QueryBuilder sortByEmailDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'email', Sort.desc); - }); - } - - QueryBuilder sortById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder sortByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder sortByInTimeline() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'inTimeline', Sort.asc); - }); - } - - QueryBuilder sortByInTimelineDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'inTimeline', Sort.desc); - }); - } - - QueryBuilder sortByIsAdmin() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isAdmin', Sort.asc); - }); - } - - QueryBuilder sortByIsAdminDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isAdmin', Sort.desc); - }); - } - - QueryBuilder sortByIsPartnerSharedBy() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedBy', Sort.asc); - }); - } - - QueryBuilder sortByIsPartnerSharedByDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedBy', Sort.desc); - }); - } - - QueryBuilder sortByIsPartnerSharedWith() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedWith', Sort.asc); - }); - } - - QueryBuilder sortByIsPartnerSharedWithDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedWith', Sort.desc); - }); - } - - QueryBuilder sortByMemoryEnabled() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'memoryEnabled', Sort.asc); - }); - } - - QueryBuilder sortByMemoryEnabledDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'memoryEnabled', Sort.desc); - }); - } - - QueryBuilder sortByName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.asc); - }); - } - - QueryBuilder sortByNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.desc); - }); - } - - QueryBuilder sortByProfileImagePath() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'profileImagePath', Sort.asc); - }); - } - - QueryBuilder sortByProfileImagePathDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'profileImagePath', Sort.desc); - }); - } - - QueryBuilder sortByQuotaSizeInBytes() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaSizeInBytes', Sort.asc); - }); - } - - QueryBuilder sortByQuotaSizeInBytesDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaSizeInBytes', Sort.desc); - }); - } - - QueryBuilder sortByQuotaUsageInBytes() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaUsageInBytes', Sort.asc); - }); - } - - QueryBuilder sortByQuotaUsageInBytesDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaUsageInBytes', Sort.desc); - }); - } - - QueryBuilder sortByUpdatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.asc); - }); - } - - QueryBuilder sortByUpdatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.desc); - }); - } -} - -extension UserQuerySortThenBy on QueryBuilder { - QueryBuilder thenByAvatarColor() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'avatarColor', Sort.asc); - }); - } - - QueryBuilder thenByAvatarColorDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'avatarColor', Sort.desc); - }); - } - - QueryBuilder thenByEmail() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'email', Sort.asc); - }); - } - - QueryBuilder thenByEmailDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'email', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByInTimeline() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'inTimeline', Sort.asc); - }); - } - - QueryBuilder thenByInTimelineDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'inTimeline', Sort.desc); - }); - } - - QueryBuilder thenByIsAdmin() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isAdmin', Sort.asc); - }); - } - - QueryBuilder thenByIsAdminDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isAdmin', Sort.desc); - }); - } - - QueryBuilder thenByIsPartnerSharedBy() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedBy', Sort.asc); - }); - } - - QueryBuilder thenByIsPartnerSharedByDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedBy', Sort.desc); - }); - } - - QueryBuilder thenByIsPartnerSharedWith() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedWith', Sort.asc); - }); - } - - QueryBuilder thenByIsPartnerSharedWithDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedWith', Sort.desc); - }); - } - - QueryBuilder thenByIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.asc); - }); - } - - QueryBuilder thenByIsarIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.desc); - }); - } - - QueryBuilder thenByMemoryEnabled() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'memoryEnabled', Sort.asc); - }); - } - - QueryBuilder thenByMemoryEnabledDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'memoryEnabled', Sort.desc); - }); - } - - QueryBuilder thenByName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.asc); - }); - } - - QueryBuilder thenByNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.desc); - }); - } - - QueryBuilder thenByProfileImagePath() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'profileImagePath', Sort.asc); - }); - } - - QueryBuilder thenByProfileImagePathDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'profileImagePath', Sort.desc); - }); - } - - QueryBuilder thenByQuotaSizeInBytes() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaSizeInBytes', Sort.asc); - }); - } - - QueryBuilder thenByQuotaSizeInBytesDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaSizeInBytes', Sort.desc); - }); - } - - QueryBuilder thenByQuotaUsageInBytes() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaUsageInBytes', Sort.asc); - }); - } - - QueryBuilder thenByQuotaUsageInBytesDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaUsageInBytes', Sort.desc); - }); - } - - QueryBuilder thenByUpdatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.asc); - }); - } - - QueryBuilder thenByUpdatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.desc); - }); - } -} - -extension UserQueryWhereDistinct on QueryBuilder { - QueryBuilder distinctByAvatarColor() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'avatarColor'); - }); - } - - QueryBuilder distinctByEmail({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'email', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctById({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'id', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByInTimeline() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'inTimeline'); - }); - } - - QueryBuilder distinctByIsAdmin() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isAdmin'); - }); - } - - QueryBuilder distinctByIsPartnerSharedBy() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isPartnerSharedBy'); - }); - } - - QueryBuilder distinctByIsPartnerSharedWith() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isPartnerSharedWith'); - }); - } - - QueryBuilder distinctByMemoryEnabled() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'memoryEnabled'); - }); - } - - QueryBuilder distinctByName({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'name', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByProfileImagePath({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy( - r'profileImagePath', - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder distinctByQuotaSizeInBytes() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'quotaSizeInBytes'); - }); - } - - QueryBuilder distinctByQuotaUsageInBytes() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'quotaUsageInBytes'); - }); - } - - QueryBuilder distinctByUpdatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'updatedAt'); - }); - } -} - -extension UserQueryProperty on QueryBuilder { - QueryBuilder isarIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isarId'); - }); - } - - QueryBuilder avatarColorProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'avatarColor'); - }); - } - - QueryBuilder emailProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'email'); - }); - } - - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder inTimelineProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'inTimeline'); - }); - } - - QueryBuilder isAdminProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isAdmin'); - }); - } - - QueryBuilder isPartnerSharedByProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isPartnerSharedBy'); - }); - } - - QueryBuilder isPartnerSharedWithProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isPartnerSharedWith'); - }); - } - - QueryBuilder memoryEnabledProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'memoryEnabled'); - }); - } - - QueryBuilder nameProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'name'); - }); - } - - QueryBuilder profileImagePathProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'profileImagePath'); - }); - } - - QueryBuilder quotaSizeInBytesProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'quotaSizeInBytes'); - }); - } - - QueryBuilder quotaUsageInBytesProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'quotaUsageInBytes'); - }); - } - - QueryBuilder updatedAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'updatedAt'); - }); - } -} diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index d41891e2ea..eca8810b91 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart'; import 'package:flutter/foundation.dart'; -import 'package:immich_mobile/domain/interfaces/db.interface.dart'; import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.dart'; import 'package:immich_mobile/infrastructure/entities/asset_face.entity.dart'; import 'package:immich_mobile/infrastructure/entities/auth_user.entity.dart'; @@ -27,22 +26,6 @@ import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.steps.dart'; -import 'package:isar/isar.dart' hide Index; - -// #zoneTxn is the symbol used by Isar to mark a transaction within the current zone -// ref: isar/isar_common.dart -const Symbol _kzoneTxn = #zoneTxn; - -class IsarDatabaseRepository implements IDatabaseRepository { - final Isar _db; - const IsarDatabaseRepository(Isar db) : _db = db; - - // Isar do not support nested transactions. This is a workaround to prevent us from making nested transactions - // Reuse the current transaction if it is already active, else start a new transaction - @override - Future transaction(Future Function() callback) => - Zone.current[_kzoneTxn] == null ? _db.writeTxn(callback) : callback(); -} @DriftDatabase( tables: [ @@ -70,7 +53,7 @@ class IsarDatabaseRepository implements IDatabaseRepository { ], include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'}, ) -class Drift extends $Drift implements IDatabaseRepository { +class Drift extends $Drift { Drift([QueryExecutor? executor]) : super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true))); @@ -261,10 +244,9 @@ class Drift extends $Drift implements IDatabaseRepository { ); } -class DriftDatabaseRepository implements IDatabaseRepository { +class DriftDatabaseRepository { final Drift _db; const DriftDatabaseRepository(this._db); - @override Future transaction(Future Function() callback) => _db.transaction(callback); } diff --git a/mobile/lib/infrastructure/repositories/device_asset.repository.dart b/mobile/lib/infrastructure/repositories/device_asset.repository.dart deleted file mode 100644 index 73ee148ab3..0000000000 --- a/mobile/lib/infrastructure/repositories/device_asset.repository.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:immich_mobile/domain/models/device_asset.model.dart'; -import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:isar/isar.dart'; - -class IsarDeviceAssetRepository extends IsarDatabaseRepository { - final Isar _db; - - const IsarDeviceAssetRepository(this._db) : super(_db); - - Future deleteIds(List ids) { - return transaction(() async { - await _db.deviceAssetEntitys.deleteAllByAssetId(ids.toList()); - }); - } - - Future> getByIds(List localIds) { - return _db.deviceAssetEntitys - .where() - .anyOf(localIds, (query, id) => query.assetIdEqualTo(id)) - .findAll() - .then((value) => value.map((e) => e.toModel()).toList()); - } - - Future updateAll(List assetHash) { - return transaction(() async { - await _db.deviceAssetEntitys.putAll(assetHash.map(DeviceAssetEntity.fromDto).toList()); - return true; - }); - } -} diff --git a/mobile/lib/infrastructure/repositories/exif.repository.dart b/mobile/lib/infrastructure/repositories/exif.repository.dart deleted file mode 100644 index 0ede30680e..0000000000 --- a/mobile/lib/infrastructure/repositories/exif.repository.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart' as entity; -import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:isar/isar.dart'; - -class IsarExifRepository extends IsarDatabaseRepository { - final Isar _db; - - const IsarExifRepository(this._db) : super(_db); - - Future delete(int assetId) async { - await transaction(() async { - await _db.exifInfos.delete(assetId); - }); - } - - Future deleteAll() async { - await transaction(() async { - await _db.exifInfos.clear(); - }); - } - - Future get(int assetId) async { - return (await _db.exifInfos.get(assetId))?.toDto(); - } - - Future update(ExifInfo exifInfo) { - return transaction(() async { - await _db.exifInfos.put(entity.ExifInfo.fromDto(exifInfo)); - return exifInfo; - }); - } - - Future> updateAll(List exifInfos) { - return transaction(() async { - await _db.exifInfos.putAll(exifInfos.map(entity.ExifInfo.fromDto).toList()); - return exifInfos; - }); - } -} diff --git a/mobile/lib/infrastructure/repositories/logger_db.repository.dart b/mobile/lib/infrastructure/repositories/logger_db.repository.dart index e494782fa6..d11174356d 100644 --- a/mobile/lib/infrastructure/repositories/logger_db.repository.dart +++ b/mobile/lib/infrastructure/repositories/logger_db.repository.dart @@ -1,11 +1,10 @@ import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart'; -import 'package:immich_mobile/domain/interfaces/db.interface.dart'; import 'package:immich_mobile/infrastructure/entities/log.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.drift.dart'; @DriftDatabase(tables: [LogMessageEntity]) -class DriftLogger extends $DriftLogger implements IDatabaseRepository { +class DriftLogger extends $DriftLogger { DriftLogger([QueryExecutor? executor]) : super( executor ?? driftDatabase(name: 'immich_logs', native: const DriftNativeOptions(shareAcrossIsolates: true)), diff --git a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart index df4172df99..6d19d17931 100644 --- a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart @@ -1,8 +1,10 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/domain/models/stack.model.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart' hide ExifInfo; +import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; @@ -264,4 +266,11 @@ class RemoteAssetRepository extends DriftDatabaseRepository { Future getCount() { return _db.managers.remoteAssetEntity.count(); } + + Future> getAssetEdits(String assetId) { + final query = _db.assetEditEntity.select() + ..where((row) => row.assetId.equals(assetId) & row.action.equals(AssetEditAction.other.index).not()) + ..orderBy([(row) => OrderingTerm.asc(row.sequence)]); + return query.map((row) => row.toDto()!).get(); + } } diff --git a/mobile/lib/infrastructure/repositories/store.repository.dart b/mobile/lib/infrastructure/repositories/store.repository.dart index d4e34a02f5..9680aa0425 100644 --- a/mobile/lib/infrastructure/repositories/store.repository.dart +++ b/mobile/lib/infrastructure/repositories/store.repository.dart @@ -1,150 +1,42 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; -import 'package:isar/isar.dart'; -// Temporary interface until Isar is removed to make the service work with both Isar and Sqlite -abstract class IStoreRepository { - Future deleteAll(); - Stream>> watchAll(); - Future delete(StoreKey key); - Future upsert(StoreKey key, T value); - Future tryGet(StoreKey key); - Stream watch(StoreKey key); - Future>> getAll(); -} - -class IsarStoreRepository extends IsarDatabaseRepository implements IStoreRepository { - final Isar _db; - final validStoreKeys = StoreKey.values.map((e) => e.id).toSet(); - - IsarStoreRepository(super.db) : _db = db; - - @override - Future deleteAll() async { - return await transaction(() async { - await _db.storeValues.clear(); - return true; - }); - } - - @override - Stream>> watchAll() { - return _db.storeValues - .filter() - .anyOf(validStoreKeys, (query, id) => query.idEqualTo(id)) - .watch(fireImmediately: true) - .asyncMap((entities) => Future.wait(entities.map((entity) => _toUpdateEvent(entity)))); - } - - @override - Future delete(StoreKey key) async { - return await transaction(() async => await _db.storeValues.delete(key.id)); - } - - @override - Future upsert(StoreKey key, T value) async { - return await transaction(() async { - await _db.storeValues.put(await _fromValue(key, value)); - return true; - }); - } - - @override - Future tryGet(StoreKey key) async { - final entity = (await _db.storeValues.get(key.id)); - if (entity == null) { - return null; - } - return await _toValue(key, entity); - } - - @override - Stream watch(StoreKey key) async* { - yield* _db.storeValues - .watchObject(key.id, fireImmediately: true) - .asyncMap((e) async => e == null ? null : await _toValue(key, e)); - } - - Future> _toUpdateEvent(StoreValue entity) async { - final key = StoreKey.values.firstWhere((e) => e.id == entity.id) as StoreKey; - final value = await _toValue(key, entity); - return StoreDto(key, value); - } - - Future _toValue(StoreKey key, StoreValue entity) async => - switch (key.type) { - const (int) => entity.intValue, - const (String) => entity.strValue, - const (bool) => entity.intValue == 1, - const (DateTime) => entity.intValue == null ? null : DateTime.fromMillisecondsSinceEpoch(entity.intValue!), - const (UserDto) => - entity.strValue == null ? null : await IsarUserRepository(_db).getByUserId(entity.strValue!), - _ => null, - } - as T?; - - Future _fromValue(StoreKey key, T value) async { - final (int? intValue, String? strValue) = switch (key.type) { - const (int) => (value as int, null), - const (String) => (null, value as String), - const (bool) => ((value as bool) ? 1 : 0, null), - const (DateTime) => ((value as DateTime).millisecondsSinceEpoch, null), - const (UserDto) => (null, (await IsarUserRepository(_db).update(value as UserDto)).id), - _ => throw UnsupportedError("Unsupported primitive type: ${key.type} for key: ${key.name}"), - }; - return StoreValue(key.id, intValue: intValue, strValue: strValue); - } - - @override - Future>> getAll() async { - final entities = await _db.storeValues.filter().anyOf(validStoreKeys, (query, id) => query.idEqualTo(id)).findAll(); - return Future.wait(entities.map((e) => _toUpdateEvent(e)).toList()); - } -} - -class DriftStoreRepository extends DriftDatabaseRepository implements IStoreRepository { +class DriftStoreRepository extends DriftDatabaseRepository { final Drift _db; final validStoreKeys = StoreKey.values.map((e) => e.id).toSet(); DriftStoreRepository(super.db) : _db = db; - @override Future deleteAll() async { await _db.storeEntity.deleteAll(); return true; } - @override Future>> getAll() async { final query = _db.storeEntity.select()..where((entity) => entity.id.isIn(validStoreKeys)); return query.asyncMap((entity) => _toUpdateEvent(entity)).get(); } - @override Stream>> watchAll() { final query = _db.storeEntity.select()..where((entity) => entity.id.isIn(validStoreKeys)); return query.asyncMap((entity) => _toUpdateEvent(entity)).watch(); } - @override Future delete(StoreKey key) async { await _db.storeEntity.deleteWhere((entity) => entity.id.equals(key.id)); return; } - @override Future upsert(StoreKey key, T value) async { await _db.storeEntity.insertOnConflictUpdate(await _fromValue(key, value)); return true; } - @override Future tryGet(StoreKey key) async { final entity = await _db.managers.storeEntity.filter((entity) => entity.id.equals(key.id)).getSingleOrNull(); if (entity == null) { @@ -153,7 +45,6 @@ class DriftStoreRepository extends DriftDatabaseRepository implements IStoreRepo return await _toValue(key, entity); } - @override Stream watch(StoreKey key) async* { final query = _db.storeEntity.select()..where((entity) => entity.id.equals(key.id)); diff --git a/mobile/lib/infrastructure/repositories/user.repository.dart b/mobile/lib/infrastructure/repositories/user.repository.dart index d4eb1ceed6..ce7cb124db 100644 --- a/mobile/lib/infrastructure/repositories/user.repository.dart +++ b/mobile/lib/infrastructure/repositories/user.repository.dart @@ -1,72 +1,9 @@ import 'package:drift/drift.dart'; -import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/models/user_metadata.model.dart'; import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_metadata.repository.dart'; -import 'package:isar/isar.dart'; - -class IsarUserRepository extends IsarDatabaseRepository { - final Isar _db; - const IsarUserRepository(super.db) : _db = db; - - Future delete(List ids) async { - await transaction(() async { - await _db.users.deleteAllById(ids); - }); - } - - Future deleteAll() async { - await transaction(() async { - await _db.users.clear(); - }); - } - - Future> getAll({SortUserBy? sortBy}) async { - return (await _db.users - .where() - .optional( - sortBy != null, - (query) => switch (sortBy!) { - SortUserBy.id => query.sortById(), - }, - ) - .findAll()) - .map((u) => u.toDto()) - .toList(); - } - - Future getByUserId(String id) async { - return (await _db.users.getById(id))?.toDto(); - } - - Future> getByUserIds(List ids) async { - return (await _db.users.getAllById(ids)).map((u) => u?.toDto()).toList(); - } - - Future insert(UserDto user) async { - await transaction(() async { - await _db.users.put(entity.User.fromDto(user)); - }); - return true; - } - - Future update(UserDto user) async { - await transaction(() async { - await _db.users.put(entity.User.fromDto(user)); - }); - return user; - } - - Future updateAll(List users) async { - await transaction(() async { - await _db.users.putAll(users.map(entity.User.fromDto).toList()); - }); - return true; - } -} class DriftAuthUserRepository extends DriftDatabaseRepository { final Drift _db; @@ -117,6 +54,7 @@ extension on AuthUserEntityData { id: id, email: email, name: name, + updatedAt: profileChangedAt, profileChangedAt: profileChangedAt, hasProfileImage: hasProfileImage, avatarColor: avatarColor, diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 7e7c709eeb..4a284b9bda 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -14,7 +14,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/locales.dart'; import 'package:immich_mobile/domain/services/background_worker.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart'; @@ -24,7 +23,6 @@ import 'package:immich_mobile/pages/common/splash_screen.page.dart'; import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; @@ -32,9 +30,7 @@ import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/theme.provider.dart'; import 'package:immich_mobile/routing/app_navigation_observer.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/services/deep_link.service.dart'; -import 'package:immich_mobile/services/local_notification.service.dart'; import 'package:immich_mobile/theme/dynamic_theme.dart'; import 'package:immich_mobile/theme/theme_data.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; @@ -53,23 +49,13 @@ void main() async { ImmichWidgetsBinding(); unawaited(BackgroundWorkerLockService(BackgroundWorkerLockApi()).lock()); await EasyLocalization.ensureInitialized(); - final (isar, drift, logDb) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDb); + final (drift, _) = await Bootstrap.initDomain(); await initApp(); // Warm-up isolate pool for worker manager await workerManagerPatch.init(dynamicSpawning: true, isolatesCount: max(Platform.numberOfProcessors - 1, 5)); - await migrateDatabaseIfNeeded(isar, drift); + await migrateDatabaseIfNeeded(); - runApp( - ProviderScope( - overrides: [ - dbProvider.overrideWithValue(isar), - isarProvider.overrideWithValue(isar), - driftProvider.overrideWith(driftOverride(drift)), - ], - child: const MainWidget(), - ), - ); + runApp(ProviderScope(overrides: [driftProvider.overrideWith(driftOverride(drift))], child: const MainWidget())); } catch (error, stack) { runApp(BootstrapErrorWidget(error: error.toString(), stack: stack.toString())); } @@ -176,7 +162,6 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve } } SystemChrome.setSystemUIOverlayStyle(overlayStyle); - await ref.read(localNotificationService).setup(); } Future _deepLinkBuilder(PlatformDeepLink deepLink) async { @@ -215,20 +200,14 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve initApp().then((_) => dPrint(() => "App Init Completed")); WidgetsBinding.instance.addPostFrameCallback((_) { // needs to be delayed so that EasyLocalization is working - if (Store.isBetaTimelineEnabled) { - ref.read(backgroundServiceProvider).disableService(); - ref.read(backgroundWorkerFgServiceProvider).enable(); - if (Platform.isAndroid) { - ref - .read(backgroundWorkerFgServiceProvider) - .saveNotificationMessage( - StaticTranslations.instance.uploading_media, - StaticTranslations.instance.backup_background_service_default_notification, - ); - } - } else { - ref.read(backgroundWorkerFgServiceProvider).disable(); - ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); + ref.read(backgroundWorkerFgServiceProvider).enable(); + if (Platform.isAndroid) { + ref + .read(backgroundWorkerFgServiceProvider) + .saveNotificationMessage( + StaticTranslations.instance.uploading_media, + StaticTranslations.instance.backup_background_service_default_notification, + ); } }); diff --git a/mobile/lib/models/albums/album_add_asset_response.model.dart b/mobile/lib/models/albums/album_add_asset_response.model.dart deleted file mode 100644 index 38dd989af5..0000000000 --- a/mobile/lib/models/albums/album_add_asset_response.model.dart +++ /dev/null @@ -1,38 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -import 'dart:convert'; - -import 'package:collection/collection.dart'; - -class AlbumAddAssetsResponse { - List alreadyInAlbum; - int successfullyAdded; - - AlbumAddAssetsResponse({required this.alreadyInAlbum, required this.successfullyAdded}); - - AlbumAddAssetsResponse copyWith({List? alreadyInAlbum, int? successfullyAdded}) { - return AlbumAddAssetsResponse( - alreadyInAlbum: alreadyInAlbum ?? this.alreadyInAlbum, - successfullyAdded: successfullyAdded ?? this.successfullyAdded, - ); - } - - Map toMap() { - return {'alreadyInAlbum': alreadyInAlbum, 'successfullyAdded': successfullyAdded}; - } - - String toJson() => json.encode(toMap()); - - @override - String toString() => 'AddAssetsResponse(alreadyInAlbum: $alreadyInAlbum, successfullyAdded: $successfullyAdded)'; - - @override - bool operator ==(covariant AlbumAddAssetsResponse other) { - if (identical(this, other)) return true; - final listEquals = const DeepCollectionEquality().equals; - - return listEquals(other.alreadyInAlbum, alreadyInAlbum) && other.successfullyAdded == successfullyAdded; - } - - @override - int get hashCode => alreadyInAlbum.hashCode ^ successfullyAdded.hashCode; -} diff --git a/mobile/lib/models/albums/album_viewer_page_state.model.dart b/mobile/lib/models/albums/album_viewer_page_state.model.dart deleted file mode 100644 index 70427899ae..0000000000 --- a/mobile/lib/models/albums/album_viewer_page_state.model.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'dart:convert'; - -class AlbumViewerPageState { - final bool isEditAlbum; - final String editTitleText; - final String editDescriptionText; - - const AlbumViewerPageState({ - required this.isEditAlbum, - required this.editTitleText, - required this.editDescriptionText, - }); - - AlbumViewerPageState copyWith({bool? isEditAlbum, String? editTitleText, String? editDescriptionText}) { - return AlbumViewerPageState( - isEditAlbum: isEditAlbum ?? this.isEditAlbum, - editTitleText: editTitleText ?? this.editTitleText, - editDescriptionText: editDescriptionText ?? this.editDescriptionText, - ); - } - - Map toMap() { - final result = {}; - - result.addAll({'isEditAlbum': isEditAlbum}); - result.addAll({'editTitleText': editTitleText}); - result.addAll({'editDescriptionText': editDescriptionText}); - - return result; - } - - factory AlbumViewerPageState.fromMap(Map map) { - return AlbumViewerPageState( - isEditAlbum: map['isEditAlbum'] ?? false, - editTitleText: map['editTitleText'] ?? '', - editDescriptionText: map['editDescriptionText'] ?? '', - ); - } - - String toJson() => json.encode(toMap()); - - factory AlbumViewerPageState.fromJson(String source) => AlbumViewerPageState.fromMap(json.decode(source)); - - @override - String toString() => - 'AlbumViewerPageState(isEditAlbum: $isEditAlbum, editTitleText: $editTitleText, editDescriptionText: $editDescriptionText)'; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is AlbumViewerPageState && - other.isEditAlbum == isEditAlbum && - other.editTitleText == editTitleText && - other.editDescriptionText == editDescriptionText; - } - - @override - int get hashCode => isEditAlbum.hashCode ^ editTitleText.hashCode ^ editDescriptionText.hashCode; -} diff --git a/mobile/lib/models/albums/asset_selection_page_result.model.dart b/mobile/lib/models/albums/asset_selection_page_result.model.dart deleted file mode 100644 index cc750f397f..0000000000 --- a/mobile/lib/models/albums/asset_selection_page_result.model.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; - -class AssetSelectionPageResult { - final Set selectedAssets; - - const AssetSelectionPageResult({required this.selectedAssets}); - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - final setEquals = const DeepCollectionEquality().equals; - - return other is AssetSelectionPageResult && setEquals(other.selectedAssets, selectedAssets); - } - - @override - int get hashCode => selectedAssets.hashCode; -} diff --git a/mobile/lib/models/asset_selection_state.dart b/mobile/lib/models/asset_selection_state.dart deleted file mode 100644 index aded3064ce..0000000000 --- a/mobile/lib/models/asset_selection_state.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; - -class AssetSelectionState { - final bool hasRemote; - final bool hasLocal; - final bool hasMerged; - final int selectedCount; - - const AssetSelectionState({ - this.hasRemote = false, - this.hasLocal = false, - this.hasMerged = false, - this.selectedCount = 0, - }); - - AssetSelectionState copyWith({bool? hasRemote, bool? hasLocal, bool? hasMerged, int? selectedCount}) { - return AssetSelectionState( - hasRemote: hasRemote ?? this.hasRemote, - hasLocal: hasLocal ?? this.hasLocal, - hasMerged: hasMerged ?? this.hasMerged, - selectedCount: selectedCount ?? this.selectedCount, - ); - } - - AssetSelectionState.fromSelection(Set selection) - : hasLocal = selection.any((e) => e.storage == AssetState.local), - hasMerged = selection.any((e) => e.storage == AssetState.merged), - hasRemote = selection.any((e) => e.storage == AssetState.remote), - selectedCount = selection.length; - - @override - String toString() => - 'SelectionAssetState(hasRemote: $hasRemote, hasLocal: $hasLocal, hasMerged: $hasMerged, selectedCount: $selectedCount)'; - - @override - bool operator ==(covariant AssetSelectionState other) { - if (identical(this, other)) return true; - - return other.hasRemote == hasRemote && - other.hasLocal == hasLocal && - other.hasMerged == hasMerged && - other.selectedCount == selectedCount; - } - - @override - int get hashCode => hasRemote.hashCode ^ hasLocal.hashCode ^ hasMerged.hashCode ^ selectedCount.hashCode; -} diff --git a/mobile/lib/models/backup/available_album.model.dart b/mobile/lib/models/backup/available_album.model.dart deleted file mode 100644 index 502d0b66be..0000000000 --- a/mobile/lib/models/backup/available_album.model.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:immich_mobile/entities/album.entity.dart'; - -class AvailableAlbum { - final Album album; - final int assetCount; - final DateTime? lastBackup; - const AvailableAlbum({required this.album, required this.assetCount, this.lastBackup}); - - AvailableAlbum copyWith({Album? album, int? assetCount, DateTime? lastBackup}) { - return AvailableAlbum( - album: album ?? this.album, - assetCount: assetCount ?? this.assetCount, - lastBackup: lastBackup ?? this.lastBackup, - ); - } - - String get name => album.name; - - String get id => album.localId!; - - bool get isAll => album.isAll; - - @override - String toString() => 'AvailableAlbum(albumEntity: $album, lastBackup: $lastBackup)'; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is AvailableAlbum && other.album == album; - } - - @override - int get hashCode => album.hashCode; -} diff --git a/mobile/lib/models/backup/backup_candidate.model.dart b/mobile/lib/models/backup/backup_candidate.model.dart deleted file mode 100644 index 01c257dc05..0000000000 --- a/mobile/lib/models/backup/backup_candidate.model.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; - -class BackupCandidate { - BackupCandidate({required this.asset, required this.albumNames}); - - Asset asset; - List albumNames; - - @override - int get hashCode => asset.hashCode; - - @override - bool operator ==(Object other) { - if (other is! BackupCandidate) { - return false; - } - return asset == other.asset; - } -} diff --git a/mobile/lib/models/backup/backup_state.model.dart b/mobile/lib/models/backup/backup_state.model.dart deleted file mode 100644 index 51a17de4fc..0000000000 --- a/mobile/lib/models/backup/backup_state.model.dart +++ /dev/null @@ -1,173 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first - -import 'package:collection/collection.dart'; -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; - -import 'package:immich_mobile/models/backup/available_album.model.dart'; -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; -import 'package:immich_mobile/models/server_info/server_disk_info.model.dart'; - -enum BackUpProgressEnum { idle, inProgress, manualInProgress, inBackground, done } - -class BackUpState { - // enum - final BackUpProgressEnum backupProgress; - final List allAssetsInDatabase; - final double progressInPercentage; - final String progressInFileSize; - final double progressInFileSpeed; - final List progressInFileSpeeds; - final DateTime progressInFileSpeedUpdateTime; - final int progressInFileSpeedUpdateSentBytes; - final double iCloudDownloadProgress; - final ServerDiskInfo serverInfo; - final bool autoBackup; - final bool backgroundBackup; - final bool backupRequireWifi; - final bool backupRequireCharging; - final int backupTriggerDelay; - - /// All available albums on the device - final List availableAlbums; - final Set selectedBackupAlbums; - final Set excludedBackupAlbums; - - /// Assets that are not overlapping in selected backup albums and excluded backup albums - final Set allUniqueAssets; - - /// All assets from the selected albums that have been backup - final Set selectedAlbumsBackupAssetsIds; - - // Current Backup Asset - final CurrentUploadAsset currentUploadAsset; - - const BackUpState({ - required this.backupProgress, - required this.allAssetsInDatabase, - required this.progressInPercentage, - required this.progressInFileSize, - required this.progressInFileSpeed, - required this.progressInFileSpeeds, - required this.progressInFileSpeedUpdateTime, - required this.progressInFileSpeedUpdateSentBytes, - required this.iCloudDownloadProgress, - required this.serverInfo, - required this.autoBackup, - required this.backgroundBackup, - required this.backupRequireWifi, - required this.backupRequireCharging, - required this.backupTriggerDelay, - required this.availableAlbums, - required this.selectedBackupAlbums, - required this.excludedBackupAlbums, - required this.allUniqueAssets, - required this.selectedAlbumsBackupAssetsIds, - required this.currentUploadAsset, - }); - - BackUpState copyWith({ - BackUpProgressEnum? backupProgress, - List? allAssetsInDatabase, - double? progressInPercentage, - String? progressInFileSize, - double? progressInFileSpeed, - List? progressInFileSpeeds, - DateTime? progressInFileSpeedUpdateTime, - int? progressInFileSpeedUpdateSentBytes, - double? iCloudDownloadProgress, - ServerDiskInfo? serverInfo, - bool? autoBackup, - bool? backgroundBackup, - bool? backupRequireWifi, - bool? backupRequireCharging, - int? backupTriggerDelay, - List? availableAlbums, - Set? selectedBackupAlbums, - Set? excludedBackupAlbums, - Set? allUniqueAssets, - Set? selectedAlbumsBackupAssetsIds, - CurrentUploadAsset? currentUploadAsset, - }) { - return BackUpState( - backupProgress: backupProgress ?? this.backupProgress, - allAssetsInDatabase: allAssetsInDatabase ?? this.allAssetsInDatabase, - progressInPercentage: progressInPercentage ?? this.progressInPercentage, - progressInFileSize: progressInFileSize ?? this.progressInFileSize, - progressInFileSpeed: progressInFileSpeed ?? this.progressInFileSpeed, - progressInFileSpeeds: progressInFileSpeeds ?? this.progressInFileSpeeds, - progressInFileSpeedUpdateTime: progressInFileSpeedUpdateTime ?? this.progressInFileSpeedUpdateTime, - progressInFileSpeedUpdateSentBytes: progressInFileSpeedUpdateSentBytes ?? this.progressInFileSpeedUpdateSentBytes, - iCloudDownloadProgress: iCloudDownloadProgress ?? this.iCloudDownloadProgress, - serverInfo: serverInfo ?? this.serverInfo, - autoBackup: autoBackup ?? this.autoBackup, - backgroundBackup: backgroundBackup ?? this.backgroundBackup, - backupRequireWifi: backupRequireWifi ?? this.backupRequireWifi, - backupRequireCharging: backupRequireCharging ?? this.backupRequireCharging, - backupTriggerDelay: backupTriggerDelay ?? this.backupTriggerDelay, - availableAlbums: availableAlbums ?? this.availableAlbums, - selectedBackupAlbums: selectedBackupAlbums ?? this.selectedBackupAlbums, - excludedBackupAlbums: excludedBackupAlbums ?? this.excludedBackupAlbums, - allUniqueAssets: allUniqueAssets ?? this.allUniqueAssets, - selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssetsIds ?? this.selectedAlbumsBackupAssetsIds, - currentUploadAsset: currentUploadAsset ?? this.currentUploadAsset, - ); - } - - @override - String toString() { - return 'BackUpState(backupProgress: $backupProgress, allAssetsInDatabase: $allAssetsInDatabase, progressInPercentage: $progressInPercentage, progressInFileSize: $progressInFileSize, progressInFileSpeed: $progressInFileSpeed, progressInFileSpeeds: $progressInFileSpeeds, progressInFileSpeedUpdateTime: $progressInFileSpeedUpdateTime, progressInFileSpeedUpdateSentBytes: $progressInFileSpeedUpdateSentBytes, iCloudDownloadProgress: $iCloudDownloadProgress, serverInfo: $serverInfo, autoBackup: $autoBackup, backgroundBackup: $backgroundBackup, backupRequireWifi: $backupRequireWifi, backupRequireCharging: $backupRequireCharging, backupTriggerDelay: $backupTriggerDelay, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds, currentUploadAsset: $currentUploadAsset)'; - } - - @override - bool operator ==(covariant BackUpState other) { - if (identical(this, other)) return true; - final collectionEquals = const DeepCollectionEquality().equals; - - return other.backupProgress == backupProgress && - collectionEquals(other.allAssetsInDatabase, allAssetsInDatabase) && - other.progressInPercentage == progressInPercentage && - other.progressInFileSize == progressInFileSize && - other.progressInFileSpeed == progressInFileSpeed && - collectionEquals(other.progressInFileSpeeds, progressInFileSpeeds) && - other.progressInFileSpeedUpdateTime == progressInFileSpeedUpdateTime && - other.progressInFileSpeedUpdateSentBytes == progressInFileSpeedUpdateSentBytes && - other.iCloudDownloadProgress == iCloudDownloadProgress && - other.serverInfo == serverInfo && - other.autoBackup == autoBackup && - other.backgroundBackup == backgroundBackup && - other.backupRequireWifi == backupRequireWifi && - other.backupRequireCharging == backupRequireCharging && - other.backupTriggerDelay == backupTriggerDelay && - collectionEquals(other.availableAlbums, availableAlbums) && - collectionEquals(other.selectedBackupAlbums, selectedBackupAlbums) && - collectionEquals(other.excludedBackupAlbums, excludedBackupAlbums) && - collectionEquals(other.allUniqueAssets, allUniqueAssets) && - collectionEquals(other.selectedAlbumsBackupAssetsIds, selectedAlbumsBackupAssetsIds) && - other.currentUploadAsset == currentUploadAsset; - } - - @override - int get hashCode { - return backupProgress.hashCode ^ - allAssetsInDatabase.hashCode ^ - progressInPercentage.hashCode ^ - progressInFileSize.hashCode ^ - progressInFileSpeed.hashCode ^ - progressInFileSpeeds.hashCode ^ - progressInFileSpeedUpdateTime.hashCode ^ - progressInFileSpeedUpdateSentBytes.hashCode ^ - iCloudDownloadProgress.hashCode ^ - serverInfo.hashCode ^ - autoBackup.hashCode ^ - backgroundBackup.hashCode ^ - backupRequireWifi.hashCode ^ - backupRequireCharging.hashCode ^ - backupTriggerDelay.hashCode ^ - availableAlbums.hashCode ^ - selectedBackupAlbums.hashCode ^ - excludedBackupAlbums.hashCode ^ - allUniqueAssets.hashCode ^ - selectedAlbumsBackupAssetsIds.hashCode ^ - currentUploadAsset.hashCode; - } -} diff --git a/mobile/lib/models/backup/current_upload_asset.model.dart b/mobile/lib/models/backup/current_upload_asset.model.dart deleted file mode 100644 index 2214897357..0000000000 --- a/mobile/lib/models/backup/current_upload_asset.model.dart +++ /dev/null @@ -1,95 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -import 'dart:convert'; - -class CurrentUploadAsset { - final String id; - final DateTime fileCreatedAt; - final String fileName; - final String fileType; - final int? fileSize; - final bool? iCloudAsset; - - const CurrentUploadAsset({ - required this.id, - required this.fileCreatedAt, - required this.fileName, - required this.fileType, - this.fileSize, - this.iCloudAsset, - }); - - @pragma('vm:prefer-inline') - bool get isIcloudAsset => iCloudAsset != null && iCloudAsset!; - - CurrentUploadAsset copyWith({ - String? id, - DateTime? fileCreatedAt, - String? fileName, - String? fileType, - int? fileSize, - bool? iCloudAsset, - }) { - return CurrentUploadAsset( - id: id ?? this.id, - fileCreatedAt: fileCreatedAt ?? this.fileCreatedAt, - fileName: fileName ?? this.fileName, - fileType: fileType ?? this.fileType, - fileSize: fileSize ?? this.fileSize, - iCloudAsset: iCloudAsset ?? this.iCloudAsset, - ); - } - - Map toMap() { - return { - 'id': id, - 'fileCreatedAt': fileCreatedAt.millisecondsSinceEpoch, - 'fileName': fileName, - 'fileType': fileType, - 'fileSize': fileSize, - 'iCloudAsset': iCloudAsset, - }; - } - - factory CurrentUploadAsset.fromMap(Map map) { - return CurrentUploadAsset( - id: map['id'] as String, - fileCreatedAt: DateTime.fromMillisecondsSinceEpoch(map['fileCreatedAt'] as int), - fileName: map['fileName'] as String, - fileType: map['fileType'] as String, - fileSize: map['fileSize'] as int, - iCloudAsset: map['iCloudAsset'] != null ? map['iCloudAsset'] as bool : null, - ); - } - - String toJson() => json.encode(toMap()); - - factory CurrentUploadAsset.fromJson(String source) => - CurrentUploadAsset.fromMap(json.decode(source) as Map); - - @override - String toString() { - return 'CurrentUploadAsset(id: $id, fileCreatedAt: $fileCreatedAt, fileName: $fileName, fileType: $fileType, fileSize: $fileSize, iCloudAsset: $iCloudAsset)'; - } - - @override - bool operator ==(covariant CurrentUploadAsset other) { - if (identical(this, other)) return true; - - return other.id == id && - other.fileCreatedAt == fileCreatedAt && - other.fileName == fileName && - other.fileType == fileType && - other.fileSize == fileSize && - other.iCloudAsset == iCloudAsset; - } - - @override - int get hashCode { - return id.hashCode ^ - fileCreatedAt.hashCode ^ - fileName.hashCode ^ - fileType.hashCode ^ - fileSize.hashCode ^ - iCloudAsset.hashCode; - } -} diff --git a/mobile/lib/models/backup/error_upload_asset.model.dart b/mobile/lib/models/backup/error_upload_asset.model.dart deleted file mode 100644 index 38f241e748..0000000000 --- a/mobile/lib/models/backup/error_upload_asset.model.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; - -class ErrorUploadAsset { - final String id; - final DateTime fileCreatedAt; - final String fileName; - final String fileType; - final Asset asset; - final String errorMessage; - - const ErrorUploadAsset({ - required this.id, - required this.fileCreatedAt, - required this.fileName, - required this.fileType, - required this.asset, - required this.errorMessage, - }); - - ErrorUploadAsset copyWith({ - String? id, - DateTime? fileCreatedAt, - String? fileName, - String? fileType, - Asset? asset, - String? errorMessage, - }) { - return ErrorUploadAsset( - id: id ?? this.id, - fileCreatedAt: fileCreatedAt ?? this.fileCreatedAt, - fileName: fileName ?? this.fileName, - fileType: fileType ?? this.fileType, - asset: asset ?? this.asset, - errorMessage: errorMessage ?? this.errorMessage, - ); - } - - @override - String toString() { - return 'ErrorUploadAsset(id: $id, fileCreatedAt: $fileCreatedAt, fileName: $fileName, fileType: $fileType, asset: $asset, errorMessage: $errorMessage)'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is ErrorUploadAsset && - other.id == id && - other.fileCreatedAt == fileCreatedAt && - other.fileName == fileName && - other.fileType == fileType && - other.asset == asset && - other.errorMessage == errorMessage; - } - - @override - int get hashCode { - return id.hashCode ^ - fileCreatedAt.hashCode ^ - fileName.hashCode ^ - fileType.hashCode ^ - asset.hashCode ^ - errorMessage.hashCode; - } -} diff --git a/mobile/lib/models/backup/manual_upload_state.model.dart b/mobile/lib/models/backup/manual_upload_state.model.dart deleted file mode 100644 index 120327c611..0000000000 --- a/mobile/lib/models/backup/manual_upload_state.model.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'package:collection/collection.dart'; - -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; - -class ManualUploadState { - // Current Backup Asset - final CurrentUploadAsset currentUploadAsset; - final int currentAssetIndex; - - final bool showDetailedNotification; - - /// Manual Upload Stats - final int totalAssetsToUpload; - final int successfulUploads; - final double progressInPercentage; - final String progressInFileSize; - final double progressInFileSpeed; - final List progressInFileSpeeds; - final DateTime progressInFileSpeedUpdateTime; - final int progressInFileSpeedUpdateSentBytes; - - const ManualUploadState({ - required this.progressInPercentage, - required this.progressInFileSize, - required this.progressInFileSpeed, - required this.progressInFileSpeeds, - required this.progressInFileSpeedUpdateTime, - required this.progressInFileSpeedUpdateSentBytes, - required this.currentUploadAsset, - required this.totalAssetsToUpload, - required this.currentAssetIndex, - required this.successfulUploads, - required this.showDetailedNotification, - }); - - ManualUploadState copyWith({ - double? progressInPercentage, - String? progressInFileSize, - double? progressInFileSpeed, - List? progressInFileSpeeds, - DateTime? progressInFileSpeedUpdateTime, - int? progressInFileSpeedUpdateSentBytes, - CurrentUploadAsset? currentUploadAsset, - int? totalAssetsToUpload, - int? successfulUploads, - int? currentAssetIndex, - bool? showDetailedNotification, - }) { - return ManualUploadState( - progressInPercentage: progressInPercentage ?? this.progressInPercentage, - progressInFileSize: progressInFileSize ?? this.progressInFileSize, - progressInFileSpeed: progressInFileSpeed ?? this.progressInFileSpeed, - progressInFileSpeeds: progressInFileSpeeds ?? this.progressInFileSpeeds, - progressInFileSpeedUpdateTime: progressInFileSpeedUpdateTime ?? this.progressInFileSpeedUpdateTime, - progressInFileSpeedUpdateSentBytes: progressInFileSpeedUpdateSentBytes ?? this.progressInFileSpeedUpdateSentBytes, - currentUploadAsset: currentUploadAsset ?? this.currentUploadAsset, - totalAssetsToUpload: totalAssetsToUpload ?? this.totalAssetsToUpload, - currentAssetIndex: currentAssetIndex ?? this.currentAssetIndex, - successfulUploads: successfulUploads ?? this.successfulUploads, - showDetailedNotification: showDetailedNotification ?? this.showDetailedNotification, - ); - } - - @override - String toString() { - return 'ManualUploadState(progressInPercentage: $progressInPercentage, progressInFileSize: $progressInFileSize, progressInFileSpeed: $progressInFileSpeed, progressInFileSpeeds: $progressInFileSpeeds, progressInFileSpeedUpdateTime: $progressInFileSpeedUpdateTime, progressInFileSpeedUpdateSentBytes: $progressInFileSpeedUpdateSentBytes, currentUploadAsset: $currentUploadAsset, totalAssetsToUpload: $totalAssetsToUpload, successfulUploads: $successfulUploads, currentAssetIndex: $currentAssetIndex, showDetailedNotification: $showDetailedNotification)'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - final collectionEquals = const DeepCollectionEquality().equals; - - return other is ManualUploadState && - other.progressInPercentage == progressInPercentage && - other.progressInFileSize == progressInFileSize && - other.progressInFileSpeed == progressInFileSpeed && - collectionEquals(other.progressInFileSpeeds, progressInFileSpeeds) && - other.progressInFileSpeedUpdateTime == progressInFileSpeedUpdateTime && - other.progressInFileSpeedUpdateSentBytes == progressInFileSpeedUpdateSentBytes && - other.currentUploadAsset == currentUploadAsset && - other.totalAssetsToUpload == totalAssetsToUpload && - other.currentAssetIndex == currentAssetIndex && - other.successfulUploads == successfulUploads && - other.showDetailedNotification == showDetailedNotification; - } - - @override - int get hashCode { - return progressInPercentage.hashCode ^ - progressInFileSize.hashCode ^ - progressInFileSpeed.hashCode ^ - progressInFileSpeeds.hashCode ^ - progressInFileSpeedUpdateTime.hashCode ^ - progressInFileSpeedUpdateSentBytes.hashCode ^ - currentUploadAsset.hashCode ^ - totalAssetsToUpload.hashCode ^ - currentAssetIndex.hashCode ^ - successfulUploads.hashCode ^ - showDetailedNotification.hashCode; - } -} diff --git a/mobile/lib/models/backup/success_upload_asset.model.dart b/mobile/lib/models/backup/success_upload_asset.model.dart deleted file mode 100644 index da1e104ba3..0000000000 --- a/mobile/lib/models/backup/success_upload_asset.model.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; - -class SuccessUploadAsset { - final BackupCandidate candidate; - final String remoteAssetId; - final bool isDuplicate; - - const SuccessUploadAsset({required this.candidate, required this.remoteAssetId, required this.isDuplicate}); - - SuccessUploadAsset copyWith({BackupCandidate? candidate, String? remoteAssetId, bool? isDuplicate}) { - return SuccessUploadAsset( - candidate: candidate ?? this.candidate, - remoteAssetId: remoteAssetId ?? this.remoteAssetId, - isDuplicate: isDuplicate ?? this.isDuplicate, - ); - } - - @override - String toString() => - 'SuccessUploadAsset(asset: $candidate, remoteAssetId: $remoteAssetId, isDuplicate: $isDuplicate)'; - - @override - bool operator ==(covariant SuccessUploadAsset other) { - if (identical(this, other)) return true; - - return other.candidate == candidate && other.remoteAssetId == remoteAssetId && other.isDuplicate == isDuplicate; - } - - @override - int get hashCode => candidate.hashCode ^ remoteAssetId.hashCode ^ isDuplicate.hashCode; -} diff --git a/mobile/lib/models/memories/memory.model.dart b/mobile/lib/models/memories/memory.model.dart deleted file mode 100644 index 8a9db5d51b..0000000000 --- a/mobile/lib/models/memories/memory.model.dart +++ /dev/null @@ -1,29 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first - -import 'package:collection/collection.dart'; - -import 'package:immich_mobile/entities/asset.entity.dart'; - -class Memory { - final String title; - final List assets; - const Memory({required this.title, required this.assets}); - - Memory copyWith({String? title, List? assets}) { - return Memory(title: title ?? this.title, assets: assets ?? this.assets); - } - - @override - String toString() => 'Memory(title: $title, assets: $assets)'; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - final listEquals = const DeepCollectionEquality().equals; - - return other is Memory && other.title == title && listEquals(other.assets, assets); - } - - @override - int get hashCode => title.hashCode ^ assets.hashCode; -} diff --git a/mobile/lib/models/search/search_filter.model.dart b/mobile/lib/models/search/search_filter.model.dart index 1b730e0c68..16f3be4655 100644 --- a/mobile/lib/models/search/search_filter.model.dart +++ b/mobile/lib/models/search/search_filter.model.dart @@ -1,8 +1,8 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first import 'dart:convert'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; class SearchLocationFilter { String? country; diff --git a/mobile/lib/models/search/search_result.model.dart b/mobile/lib/models/search/search_result.model.dart deleted file mode 100644 index 02553869bf..0000000000 --- a/mobile/lib/models/search/search_result.model.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:collection/collection.dart'; - -import 'package:immich_mobile/entities/asset.entity.dart'; - -class SearchResult { - final List assets; - final int? nextPage; - - const SearchResult({required this.assets, this.nextPage}); - - SearchResult copyWith({List? assets, int? nextPage}) { - return SearchResult(assets: assets ?? this.assets, nextPage: nextPage ?? this.nextPage); - } - - @override - String toString() => 'SearchResult(assets: $assets, nextPage: $nextPage)'; - - @override - bool operator ==(covariant SearchResult other) { - if (identical(this, other)) return true; - final listEquals = const DeepCollectionEquality().equals; - - return listEquals(other.assets, assets) && other.nextPage == nextPage; - } - - @override - int get hashCode => assets.hashCode ^ nextPage.hashCode; -} diff --git a/mobile/lib/models/search/search_result_page_state.model.dart b/mobile/lib/models/search/search_result_page_state.model.dart deleted file mode 100644 index 7c8a27b50c..0000000000 --- a/mobile/lib/models/search/search_result_page_state.model.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; - -class SearchResultPageState { - final bool isLoading; - final bool isSuccess; - final bool isError; - final bool isSmart; - final List searchResult; - - const SearchResultPageState({ - required this.isLoading, - required this.isSuccess, - required this.isError, - required this.isSmart, - required this.searchResult, - }); - - SearchResultPageState copyWith({ - bool? isLoading, - bool? isSuccess, - bool? isError, - bool? isSmart, - List? searchResult, - }) { - return SearchResultPageState( - isLoading: isLoading ?? this.isLoading, - isSuccess: isSuccess ?? this.isSuccess, - isError: isError ?? this.isError, - isSmart: isSmart ?? this.isSmart, - searchResult: searchResult ?? this.searchResult, - ); - } - - @override - String toString() { - return 'SearchresultPageState(isLoading: $isLoading, isSuccess: $isSuccess, isError: $isError, isSmart: $isSmart, searchResult: $searchResult)'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - final listEquals = const DeepCollectionEquality().equals; - - return other is SearchResultPageState && - other.isLoading == isLoading && - other.isSuccess == isSuccess && - other.isError == isError && - other.isSmart == isSmart && - listEquals(other.searchResult, searchResult); - } - - @override - int get hashCode { - return isLoading.hashCode ^ isSuccess.hashCode ^ isError.hashCode ^ isSmart.hashCode ^ searchResult.hashCode; - } -} diff --git a/mobile/lib/pages/album/album_additional_shared_user_selection.page.dart b/mobile/lib/pages/album/album_additional_shared_user_selection.page.dart deleted file mode 100644 index f40ac9ccae..0000000000 --- a/mobile/lib/pages/album/album_additional_shared_user_selection.page.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -@RoutePage() -class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget { - final Album album; - - const AlbumAdditionalSharedUserSelectionPage({super.key, required this.album}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final AsyncValue> suggestedShareUsers = ref.watch(otherUsersProvider); - final sharedUsersList = useState>({}); - - addNewUsersHandler() { - context.maybePop(sharedUsersList.value.map((e) => e.id).toList()); - } - - buildTileIcon(UserDto user) { - if (sharedUsersList.value.contains(user)) { - return CircleAvatar(backgroundColor: context.primaryColor, child: const Icon(Icons.check_rounded, size: 25)); - } else { - return UserCircleAvatar(user: user); - } - } - - buildUserList(List users) { - List usersChip = []; - - for (var user in sharedUsersList.value) { - usersChip.add( - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Chip( - backgroundColor: context.primaryColor.withValues(alpha: 0.15), - label: Text(user.name, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold)), - ), - ), - ); - } - return ListView( - children: [ - Wrap(children: [...usersChip]), - Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - 'suggestions'.tr(), - style: const TextStyle(fontSize: 14, color: Colors.grey, fontWeight: FontWeight.bold), - ), - ), - ListView.builder( - primary: false, - shrinkWrap: true, - itemBuilder: ((context, index) { - return ListTile( - leading: buildTileIcon(users[index]), - dense: true, - title: Text(users[index].name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - subtitle: Text(users[index].email, style: const TextStyle(fontSize: 12)), - onTap: () { - if (sharedUsersList.value.contains(users[index])) { - sharedUsersList.value = sharedUsersList.value - .where((selectedUser) => selectedUser.id != users[index].id) - .toSet(); - } else { - sharedUsersList.value = {...sharedUsersList.value, users[index]}; - } - }, - ); - }), - itemCount: users.length, - ), - ], - ); - } - - return Scaffold( - appBar: AppBar( - title: const Text('invite_to_album').tr(), - elevation: 0, - centerTitle: false, - leading: IconButton( - icon: const Icon(Icons.close_rounded), - onPressed: () { - context.maybePop(null); - }, - ), - actions: [ - TextButton( - onPressed: sharedUsersList.value.isEmpty ? null : addNewUsersHandler, - child: const Text("add", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)).tr(), - ), - ], - ), - body: suggestedShareUsers.widgetWhen( - onData: (users) { - for (var sharedUsers in album.sharedUsers) { - users.removeWhere((u) => u.id == sharedUsers.id || u.id == album.ownerId); - } - - return buildUserList(users); - }, - ), - ); - } -} diff --git a/mobile/lib/pages/album/album_asset_selection.page.dart b/mobile/lib/pages/album/album_asset_selection.page.dart deleted file mode 100644 index ccc4c44d43..0000000000 --- a/mobile/lib/pages/album/album_asset_selection.page.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; - -@RoutePage() -class AlbumAssetSelectionPage extends HookConsumerWidget { - const AlbumAssetSelectionPage({super.key, required this.existingAssets, this.canDeselect = false}); - - final Set existingAssets; - final bool canDeselect; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final assetSelectionRenderList = ref.watch(assetSelectionTimelineProvider); - final selected = useState>(existingAssets); - final selectionEnabledHook = useState(true); - - Widget buildBody(RenderList renderList) { - return ImmichAssetGrid( - renderList: renderList, - listener: (active, assets) { - selectionEnabledHook.value = active; - selected.value = assets; - }, - selectionActive: true, - preselectedAssets: existingAssets, - canDeselect: canDeselect, - showMultiSelectIndicator: false, - ); - } - - return Scaffold( - appBar: AppBar( - elevation: 0, - leading: IconButton( - icon: const Icon(Icons.close_rounded), - onPressed: () { - AutoRouter.of(context).popForced(null); - }, - ), - title: selected.value.isEmpty - ? const Text('add_photos', style: TextStyle(fontSize: 18)).tr() - : const Text( - 'share_assets_selected', - style: TextStyle(fontSize: 18), - ).tr(namedArgs: {'count': selected.value.length.toString()}), - centerTitle: false, - actions: [ - if (selected.value.isNotEmpty || canDeselect) - TextButton( - onPressed: () { - var payload = AssetSelectionPageResult(selectedAssets: selected.value); - AutoRouter.of(context).popForced(payload); - }, - child: Text( - canDeselect ? "done" : "add", - style: TextStyle(fontWeight: FontWeight.bold, color: context.primaryColor), - ).tr(), - ), - ], - ), - body: assetSelectionRenderList.widgetWhen(onData: (data) => buildBody(data)), - ); - } -} diff --git a/mobile/lib/pages/album/album_control_button.dart b/mobile/lib/pages/album/album_control_button.dart deleted file mode 100644 index 578eb839a0..0000000000 --- a/mobile/lib/pages/album/album_control_button.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/widgets/album/album_action_filled_button.dart'; - -class AlbumControlButton extends ConsumerWidget { - final void Function()? onAddPhotosPressed; - final void Function()? onAddUsersPressed; - - const AlbumControlButton({super.key, this.onAddPhotosPressed, this.onAddUsersPressed}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return SizedBox( - height: 36, - child: ListView( - scrollDirection: Axis.horizontal, - children: [ - if (onAddPhotosPressed != null) - AlbumActionFilledButton( - key: const ValueKey('add_photos_button'), - iconData: Icons.add_photo_alternate_outlined, - onPressed: onAddPhotosPressed, - labelText: "add_photos".tr(), - ), - if (onAddUsersPressed != null) - AlbumActionFilledButton( - key: const ValueKey('add_users_button'), - iconData: Icons.person_add_alt_rounded, - onPressed: onAddUsersPressed, - labelText: "album_viewer_page_share_add_users".tr(), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/pages/album/album_date_range.dart b/mobile/lib/pages/album/album_date_range.dart deleted file mode 100644 index dbfd9214f1..0000000000 --- a/mobile/lib/pages/album/album_date_range.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; - -class AlbumDateRange extends ConsumerWidget { - const AlbumDateRange({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final data = ref.watch( - currentAlbumProvider.select((album) { - if (album == null || album.assets.isEmpty) { - return null; - } - - final startDate = album.startDate; - final endDate = album.endDate; - if (startDate == null || endDate == null) { - return null; - } - return (startDate, endDate, album.shared); - }), - ); - - if (data == null) { - return const SizedBox(); - } - final (startDate, endDate, shared) = data; - - return Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Text( - _getDateRangeText(startDate, endDate), - style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceVariant), - ), - ); - } - - @pragma('vm:prefer-inline') - String _getDateRangeText(DateTime startDate, DateTime endDate) { - if (startDate.day == endDate.day && startDate.month == endDate.month && startDate.year == endDate.year) { - return DateFormat.yMMMd().format(startDate); - } - - final String startDateText = (startDate.year == endDate.year ? DateFormat.MMMd() : DateFormat.yMMMd()).format( - startDate, - ); - final String endDateText = DateFormat.yMMMd().format(endDate); - return "$startDateText - $endDateText"; - } -} diff --git a/mobile/lib/pages/album/album_description.dart b/mobile/lib/pages/album/album_description.dart deleted file mode 100644 index 383367e8b7..0000000000 --- a/mobile/lib/pages/album/album_description.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/widgets/album/album_viewer_editable_description.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; - -class AlbumDescription extends ConsumerWidget { - const AlbumDescription({super.key, required this.descriptionFocusNode}); - - final FocusNode descriptionFocusNode; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final userId = ref.watch(authProvider).userId; - final (isOwner, isRemote, albumDescription) = ref.watch( - currentAlbumProvider.select((album) { - if (album == null) { - return const (false, false, ''); - } - - return (album.ownerId == userId, album.isRemote, album.description); - }), - ); - - if (isOwner && isRemote) { - return Padding( - padding: const EdgeInsets.only(left: 8, right: 8), - child: AlbumViewerEditableDescription( - albumDescription: albumDescription ?? 'add_a_description'.tr(), - descriptionFocusNode: descriptionFocusNode, - ), - ); - } - - return Padding( - padding: const EdgeInsets.only(left: 16, right: 8), - child: Text(albumDescription ?? 'add_a_description'.tr(), style: context.textTheme.bodyLarge), - ); - } -} diff --git a/mobile/lib/pages/album/album_options.page.dart b/mobile/lib/pages/album/album_options.page.dart deleted file mode 100644 index ca65a92a79..0000000000 --- a/mobile/lib/pages/album/album_options.page.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -@RoutePage() -class AlbumOptionsPage extends HookConsumerWidget { - const AlbumOptionsPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final album = ref.watch(currentAlbumProvider); - if (album == null) { - return const SizedBox(); - } - - final sharedUsers = useState(album.sharedUsers.map((u) => u.toDto()).toList()); - final owner = album.owner.value; - final userId = ref.watch(authProvider).userId; - final activityEnabled = useState(album.activityEnabled); - final isProcessing = useProcessingOverlay(); - final isOwner = owner?.id == userId; - - void showErrorMessage() { - context.pop(); - ImmichToast.show( - context: context, - msg: "shared_album_section_people_action_error".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - - void leaveAlbum() async { - isProcessing.value = true; - - try { - final isSuccess = await ref.read(albumProvider.notifier).leaveAlbum(album); - - if (isSuccess) { - unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]))); - } else { - showErrorMessage(); - } - } catch (_) { - showErrorMessage(); - } - - isProcessing.value = false; - } - - void removeUserFromAlbum(UserDto user) async { - isProcessing.value = true; - - try { - await ref.read(albumProvider.notifier).removeUser(album, user); - album.sharedUsers.remove(entity.User.fromDto(user)); - sharedUsers.value = album.sharedUsers.map((u) => u.toDto()).toList(); - } catch (error) { - showErrorMessage(); - } - - context.pop(); - isProcessing.value = false; - } - - void handleUserClick(UserDto user) { - var actions = []; - - if (user.id == userId) { - actions = [ - ListTile( - leading: const Icon(Icons.exit_to_app_rounded), - title: const Text("shared_album_section_people_action_leave").tr(), - onTap: leaveAlbum, - ), - ]; - } - - if (isOwner) { - actions = [ - ListTile( - leading: const Icon(Icons.person_remove_rounded), - title: const Text("shared_album_section_people_action_remove_user").tr(), - onTap: () => removeUserFromAlbum(user), - ), - ]; - } - - showModalBottomSheet( - backgroundColor: context.colorScheme.surfaceContainer, - isScrollControlled: false, - context: context, - builder: (context) { - return SafeArea( - child: Padding( - padding: const EdgeInsets.only(top: 24.0), - child: Column(mainAxisSize: MainAxisSize.min, children: [...actions]), - ), - ); - }, - ); - } - - buildOwnerInfo() { - return ListTile( - leading: owner != null ? UserCircleAvatar(user: owner.toDto()) : const SizedBox(), - title: Text(album.owner.value?.name ?? "", style: const TextStyle(fontWeight: FontWeight.w500)), - subtitle: Text(album.owner.value?.email ?? "", style: TextStyle(color: context.colorScheme.onSurfaceSecondary)), - trailing: Text("owner", style: context.textTheme.labelLarge).tr(), - ); - } - - buildSharedUsersList() { - return ListView.builder( - primary: false, - shrinkWrap: true, - itemCount: sharedUsers.value.length, - itemBuilder: (context, index) { - final user = sharedUsers.value[index]; - return ListTile( - leading: UserCircleAvatar(user: user), - title: Text(user.name, style: const TextStyle(fontWeight: FontWeight.w500)), - subtitle: Text(user.email, style: TextStyle(color: context.colorScheme.onSurfaceSecondary)), - trailing: userId == user.id || isOwner ? const Icon(Icons.more_horiz_rounded) : const SizedBox(), - onTap: userId == user.id || isOwner ? () => handleUserClick(user) : null, - ); - }, - ); - } - - buildSectionTitle(String text) { - return Padding( - padding: const EdgeInsets.all(16.0), - child: Text(text, style: context.textTheme.bodySmall), - ); - } - - return Scaffold( - appBar: AppBar( - leading: IconButton( - icon: const Icon(Icons.arrow_back_ios_new_rounded), - onPressed: () => context.maybePop(null), - ), - centerTitle: true, - title: Text("options".tr()), - ), - body: ListView( - children: [ - if (isOwner && album.shared) - SwitchListTile.adaptive( - value: activityEnabled.value, - onChanged: (bool value) async { - activityEnabled.value = value; - if (await ref.read(albumProvider.notifier).setActivitystatus(album, value)) { - album.activityEnabled = value; - } - }, - activeThumbColor: activityEnabled.value ? context.primaryColor : context.themeData.disabledColor, - dense: true, - title: Text( - "comments_and_likes", - style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), - ).tr(), - subtitle: Text( - "let_others_respond", - style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ).tr(), - ), - buildSectionTitle("shared_album_section_people_title".tr()), - buildOwnerInfo(), - buildSharedUsersList(), - ], - ), - ); - } -} diff --git a/mobile/lib/pages/album/album_shared_user_icons.dart b/mobile/lib/pages/album/album_shared_user_icons.dart deleted file mode 100644 index 7cf6f387ae..0000000000 --- a/mobile/lib/pages/album/album_shared_user_icons.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -class AlbumSharedUserIcons extends HookConsumerWidget { - const AlbumSharedUserIcons({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final sharedUsers = useRef>(const []); - sharedUsers.value = ref.watch( - currentAlbumProvider.select((album) { - if (album == null) { - return const []; - } - - if (album.sharedUsers.length == sharedUsers.value.length) { - return sharedUsers.value; - } - - return album.sharedUsers.map((u) => u.toDto()).toList(growable: false); - }), - ); - - if (sharedUsers.value.isEmpty) { - return const SizedBox(); - } - - return GestureDetector( - onTap: () => context.pushRoute(const AlbumOptionsRoute()), - child: SizedBox( - height: 50, - child: ListView.builder( - padding: const EdgeInsets.only(left: 16, bottom: 8), - scrollDirection: Axis.horizontal, - itemBuilder: ((context, index) { - return Padding( - padding: const EdgeInsets.only(right: 8.0), - child: UserCircleAvatar(user: sharedUsers.value[index], size: 36), - ); - }), - itemCount: sharedUsers.value.length, - ), - ), - ); - } -} diff --git a/mobile/lib/pages/album/album_shared_user_selection.page.dart b/mobile/lib/pages/album/album_shared_user_selection.page.dart deleted file mode 100644 index ec084b1859..0000000000 --- a/mobile/lib/pages/album/album_shared_user_selection.page.dart +++ /dev/null @@ -1,140 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/album_title.provider.dart'; -import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -@RoutePage() -class AlbumSharedUserSelectionPage extends HookConsumerWidget { - const AlbumSharedUserSelectionPage({super.key, required this.assets}); - - final Set assets; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final sharedUsersList = useState>({}); - final suggestedShareUsers = ref.watch(otherUsersProvider); - - createSharedAlbum() async { - var newAlbum = await ref.watch(albumProvider.notifier).createAlbum(ref.watch(albumTitleProvider), assets); - - if (newAlbum != null) { - ref.watch(albumTitleProvider.notifier).clearAlbumTitle(); - unawaited(context.maybePop(true)); - unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]))); - } - - ScaffoldMessenger( - child: SnackBar( - content: Text( - 'select_user_for_sharing_page_err_album', - style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), - ).tr(), - ), - ); - } - - buildTileIcon(UserDto user) { - if (sharedUsersList.value.contains(user)) { - return CircleAvatar(backgroundColor: context.primaryColor, child: const Icon(Icons.check_rounded, size: 25)); - } else { - return UserCircleAvatar(user: user); - } - } - - buildUserList(List users) { - List usersChip = []; - - for (var user in sharedUsersList.value) { - usersChip.add( - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Chip( - backgroundColor: context.primaryColor.withValues(alpha: 0.15), - label: Text( - user.email, - style: const TextStyle(fontSize: 12, color: Colors.black87, fontWeight: FontWeight.bold), - ), - ), - ), - ); - } - return ListView( - children: [ - Wrap(children: [...usersChip]), - Padding( - padding: const EdgeInsets.all(16.0), - child: const Text( - 'suggestions', - style: TextStyle(fontSize: 14, color: Colors.grey, fontWeight: FontWeight.bold), - ).tr(), - ), - ListView.builder( - primary: false, - shrinkWrap: true, - itemBuilder: ((context, index) { - return ListTile( - leading: buildTileIcon(users[index]), - title: Text(users[index].email, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - onTap: () { - if (sharedUsersList.value.contains(users[index])) { - sharedUsersList.value = sharedUsersList.value - .where((selectedUser) => selectedUser.id != users[index].id) - .toSet(); - } else { - sharedUsersList.value = {...sharedUsersList.value, users[index]}; - } - }, - ); - }), - itemCount: users.length, - ), - ], - ); - } - - return Scaffold( - appBar: AppBar( - title: Text('invite_to_album', style: TextStyle(color: context.primaryColor)).tr(), - elevation: 0, - centerTitle: false, - leading: IconButton( - icon: const Icon(Icons.close_rounded), - onPressed: () { - unawaited(context.maybePop()); - }, - ), - actions: [ - TextButton( - style: TextButton.styleFrom(foregroundColor: context.primaryColor), - onPressed: sharedUsersList.value.isEmpty ? null : createSharedAlbum, - child: const Text( - "create_album", - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - // color: context.primaryColor, - ), - ).tr(), - ), - ], - ), - body: suggestedShareUsers.widgetWhen( - onData: (users) { - return buildUserList(users); - }, - ), - ); - } -} diff --git a/mobile/lib/pages/album/album_title.dart b/mobile/lib/pages/album/album_title.dart deleted file mode 100644 index 6c7fc3faaa..0000000000 --- a/mobile/lib/pages/album/album_title.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/widgets/album/album_viewer_editable_title.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; - -class AlbumTitle extends ConsumerWidget { - const AlbumTitle({super.key, required this.titleFocusNode}); - - final FocusNode titleFocusNode; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final userId = ref.watch(authProvider).userId; - final (isOwner, isRemote, albumName) = ref.watch( - currentAlbumProvider.select((album) { - if (album == null) { - return const (false, false, ''); - } - - return (album.ownerId == userId, album.isRemote, album.name); - }), - ); - - if (isOwner && isRemote) { - return Padding( - padding: const EdgeInsets.only(left: 8, right: 8), - child: AlbumViewerEditableTitle(albumName: albumName, titleFocusNode: titleFocusNode), - ); - } - - return Padding( - padding: const EdgeInsets.only(left: 16, right: 8), - child: Text(albumName, style: context.textTheme.headlineLarge?.copyWith(fontWeight: FontWeight.w700)), - ); - } -} diff --git a/mobile/lib/pages/album/album_viewer.dart b/mobile/lib/pages/album/album_viewer.dart deleted file mode 100644 index 97853fb96a..0000000000 --- a/mobile/lib/pages/album/album_viewer.dart +++ /dev/null @@ -1,165 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart'; -import 'package:immich_mobile/pages/album/album_control_button.dart'; -import 'package:immich_mobile/pages/album/album_date_range.dart'; -import 'package:immich_mobile/pages/album/album_description.dart'; -import 'package:immich_mobile/pages/album/album_shared_user_icons.dart'; -import 'package:immich_mobile/pages/album/album_title.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/widgets/album/album_viewer_appbar.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class AlbumViewer extends HookConsumerWidget { - const AlbumViewer({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final album = ref.watch(currentAlbumProvider); - if (album == null) { - return const SizedBox(); - } - - final titleFocusNode = useFocusNode(); - final descriptionFocusNode = useFocusNode(); - final userId = ref.watch(authProvider).userId; - final isMultiselecting = ref.watch(multiselectProvider); - final isProcessing = useProcessingOverlay(); - final isOwner = ref.watch( - currentAlbumProvider.select((album) { - return album?.ownerId == userId; - }), - ); - - Future onRemoveFromAlbumPressed(Iterable assets) async { - final bool isSuccess = await ref.read(albumProvider.notifier).removeAsset(album, assets); - - if (!isSuccess) { - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_remove".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - return isSuccess; - } - - /// Find out if the assets in album exist on the device - /// If they exist, add to selected asset state to show they are already selected. - void onAddPhotosPressed() async { - AssetSelectionPageResult? returnPayload = await context.pushRoute( - AlbumAssetSelectionRoute(existingAssets: album.assets, canDeselect: false), - ); - - if (returnPayload != null && returnPayload.selectedAssets.isNotEmpty) { - // Check if there is new assets add - isProcessing.value = true; - - await ref.watch(albumProvider.notifier).addAssets(album, returnPayload.selectedAssets); - - isProcessing.value = false; - } - } - - void onAddUsersPressed() async { - List? sharedUserIds = await context.pushRoute?>( - AlbumAdditionalSharedUserSelectionRoute(album: album), - ); - - if (sharedUserIds != null) { - isProcessing.value = true; - - await ref.watch(albumProvider.notifier).addUsers(album, sharedUserIds); - - isProcessing.value = false; - } - } - - onActivitiesPressed() { - if (album.remoteId != null) { - ref.read(currentAssetProvider.notifier).set(null); - context.pushRoute(const ActivitiesRoute()); - } - } - - return Stack( - children: [ - MultiselectGrid( - key: const ValueKey("albumViewerMultiselectGrid"), - renderListProvider: albumTimelineProvider(album.id), - topWidget: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - context.primaryColor.withValues(alpha: 0.06), - context.primaryColor.withValues(alpha: 0.04), - Colors.indigo.withValues(alpha: 0.02), - Colors.transparent, - ], - stops: const [0.0, 0.3, 0.7, 1.0], - ), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 32), - const AlbumDateRange(), - AlbumTitle(key: const ValueKey("albumTitle"), titleFocusNode: titleFocusNode), - AlbumDescription(key: const ValueKey("albumDescription"), descriptionFocusNode: descriptionFocusNode), - const AlbumSharedUserIcons(), - if (album.isRemote) - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: AlbumControlButton( - key: const ValueKey("albumControlButton"), - onAddPhotosPressed: onAddPhotosPressed, - onAddUsersPressed: isOwner ? onAddUsersPressed : null, - ), - ), - const SizedBox(height: 8), - ], - ), - ), - onRemoveFromAlbum: onRemoveFromAlbumPressed, - editEnabled: album.ownerId == userId, - ), - AnimatedPositioned( - key: const ValueKey("albumViewerAppbarPositioned"), - duration: const Duration(milliseconds: 300), - top: isMultiselecting ? -(kToolbarHeight + context.padding.top) : 0, - left: 0, - right: 0, - child: AlbumViewerAppbar( - key: const ValueKey("albumViewerAppbar"), - titleFocusNode: titleFocusNode, - descriptionFocusNode: descriptionFocusNode, - userId: userId, - onAddPhotos: onAddPhotosPressed, - onAddUsers: onAddUsersPressed, - onActivities: onActivitiesPressed, - ), - ), - ], - ); - } -} diff --git a/mobile/lib/pages/album/album_viewer.page.dart b/mobile/lib/pages/album/album_viewer.page.dart deleted file mode 100644 index c99dacd9b7..0000000000 --- a/mobile/lib/pages/album/album_viewer.page.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/pages/album/album_viewer.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; - -@RoutePage() -class AlbumViewerPage extends HookConsumerWidget { - final int albumId; - - const AlbumViewerPage({super.key, required this.albumId}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - // Listen provider to prevent autoDispose when navigating to other routes from within the viewer page - ref.listen(currentAlbumProvider, (_, __) {}); - - // This call helps rendering the asset selection instantly - ref.listen(assetSelectionTimelineProvider, (_, __) {}); - - ref.listen(albumWatcher(albumId), (_, albumFuture) { - albumFuture.whenData((value) => ref.read(currentAlbumProvider.notifier).set(value)); - }); - - return const Scaffold(body: AlbumViewer()); - } -} diff --git a/mobile/lib/pages/albums/albums.page.dart b/mobile/lib/pages/albums/albums.page.dart deleted file mode 100644 index 5f155c2f0d..0000000000 --- a/mobile/lib/pages/albums/albums.page.dart +++ /dev/null @@ -1,359 +0,0 @@ -import 'dart:async'; -import 'dart:math'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/models/albums/album_search.model.dart'; -import 'package:immich_mobile/pages/common/large_leading_tile.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; -import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; -import 'package:immich_mobile/widgets/common/search_field.dart'; - -@RoutePage() -class AlbumsPage extends HookConsumerWidget { - const AlbumsPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albums = ref.watch(albumProvider).where((album) => album.isRemote).toList(); - final albumSortOption = ref.watch(albumSortByOptionsProvider); - final albumSortIsReverse = ref.watch(albumSortOrderProvider); - final sorted = albumSortOption.sortFn(albums, albumSortIsReverse); - final isGrid = useState(false); - final searchController = useTextEditingController(); - final debounceTimer = useRef(null); - final filterMode = useState(QuickFilterMode.all); - final userId = ref.watch(currentUserProvider)?.id; - final searchFocusNode = useFocusNode(); - - toggleViewMode() { - isGrid.value = !isGrid.value; - } - - onSearch(String searchTerm, QuickFilterMode mode) { - debounceTimer.value?.cancel(); - debounceTimer.value = Timer(const Duration(milliseconds: 300), () { - ref.read(albumProvider.notifier).searchAlbums(searchTerm, mode); - }); - } - - changeFilter(QuickFilterMode mode) { - filterMode.value = mode; - } - - useEffect(() { - searchController.addListener(() { - onSearch(searchController.text, filterMode.value); - }); - - return () { - searchController.removeListener(() { - onSearch(searchController.text, filterMode.value); - }); - debounceTimer.value?.cancel(); - }; - }, []); - - clearSearch() { - filterMode.value = QuickFilterMode.all; - searchController.clear(); - onSearch('', QuickFilterMode.all); - } - - return Scaffold( - appBar: ImmichAppBar( - showUploadButton: false, - actions: [ - IconButton( - icon: const Icon(Icons.add_rounded, size: 28), - onPressed: () => context.pushRoute(CreateAlbumRoute()), - ), - ], - ), - body: RefreshIndicator( - displacement: 70, - onRefresh: () async { - await ref.read(albumProvider.notifier).refreshRemoteAlbums(); - }, - child: ListView( - shrinkWrap: true, - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12), - children: [ - Container( - decoration: BoxDecoration( - border: Border.all(color: context.colorScheme.onSurface.withAlpha(0), width: 0), - borderRadius: const BorderRadius.all(Radius.circular(24)), - gradient: LinearGradient( - colors: [ - context.colorScheme.primary.withValues(alpha: 0.075), - context.colorScheme.primary.withValues(alpha: 0.09), - context.colorScheme.primary.withValues(alpha: 0.075), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - transform: const GradientRotation(0.5 * pi), - ), - ), - child: SearchField( - autofocus: false, - contentPadding: const EdgeInsets.all(16), - hintText: 'search_albums'.tr(), - prefixIcon: const Icon(Icons.search_rounded), - suffixIcon: searchController.text.isNotEmpty - ? IconButton(icon: const Icon(Icons.clear_rounded), onPressed: clearSearch) - : null, - controller: searchController, - onChanged: (_) => onSearch(searchController.text, filterMode.value), - focusNode: searchFocusNode, - onTapOutside: (_) => searchFocusNode.unfocus(), - ), - ), - const SizedBox(height: 8), - Wrap( - spacing: 4, - runSpacing: 4, - children: [ - QuickFilterButton( - label: 'all'.tr(), - isSelected: filterMode.value == QuickFilterMode.all, - onTap: () { - changeFilter(QuickFilterMode.all); - onSearch(searchController.text, QuickFilterMode.all); - }, - ), - QuickFilterButton( - label: 'shared_with_me'.tr(), - isSelected: filterMode.value == QuickFilterMode.sharedWithMe, - onTap: () { - changeFilter(QuickFilterMode.sharedWithMe); - onSearch(searchController.text, QuickFilterMode.sharedWithMe); - }, - ), - QuickFilterButton( - label: 'my_albums'.tr(), - isSelected: filterMode.value == QuickFilterMode.myAlbums, - onTap: () { - changeFilter(QuickFilterMode.myAlbums); - onSearch(searchController.text, QuickFilterMode.myAlbums); - }, - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const SortButton(), - IconButton( - icon: Icon(isGrid.value ? Icons.view_list_outlined : Icons.grid_view_outlined, size: 24), - onPressed: toggleViewMode, - ), - ], - ), - const SizedBox(height: 5), - AnimatedSwitcher( - duration: const Duration(milliseconds: 500), - child: isGrid.value - ? GridView.builder( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 250, - mainAxisSpacing: 12, - crossAxisSpacing: 12, - childAspectRatio: .7, - ), - itemBuilder: (context, index) { - return AlbumThumbnailCard( - album: sorted[index], - onTap: () => context.pushRoute(AlbumViewerRoute(albumId: sorted[index].id)), - showOwner: true, - ); - }, - itemCount: sorted.length, - ) - : ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: sorted.length, - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: LargeLeadingTile( - title: Text( - sorted[index].name, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), - ), - subtitle: sorted[index].ownerId != null - ? Text( - '${'items_count'.t(context: context, args: {'count': sorted[index].assetCount})} • ${sorted[index].ownerId != userId ? 'shared_by_user'.t(context: context, args: {'user': sorted[index].ownerName!}) : 'owned'.t(context: context)}', - overflow: TextOverflow.ellipsis, - style: context.textTheme.bodyMedium?.copyWith( - color: context.colorScheme.onSurfaceSecondary, - ), - ) - : null, - onTap: () => context.pushRoute(AlbumViewerRoute(albumId: sorted[index].id)), - leadingPadding: const EdgeInsets.only(right: 16), - leading: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(15)), - child: ImmichThumbnail(asset: sorted[index].thumbnail.value, width: 80, height: 80), - ), - // minVerticalPadding: 1, - ), - ); - }, - ), - ), - ], - ), - ), - resizeToAvoidBottomInset: false, - ); - } -} - -class QuickFilterButton extends StatelessWidget { - const QuickFilterButton({super.key, required this.isSelected, required this.onTap, required this.label}); - - final bool isSelected; - final VoidCallback onTap; - final String label; - - @override - Widget build(BuildContext context) { - return TextButton( - onPressed: onTap, - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all(isSelected ? context.colorScheme.primary : Colors.transparent), - shape: WidgetStateProperty.all( - RoundedRectangleBorder( - borderRadius: const BorderRadius.all(Radius.circular(20)), - side: BorderSide(color: context.colorScheme.onSurface.withAlpha(25), width: 1), - ), - ), - ), - child: Text( - label, - style: TextStyle( - color: isSelected ? context.colorScheme.onPrimary : context.colorScheme.onSurface, - fontSize: 14, - ), - ), - ); - } -} - -class SortButton extends ConsumerWidget { - const SortButton({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albumSortOption = ref.watch(albumSortByOptionsProvider); - final albumSortIsReverse = ref.watch(albumSortOrderProvider); - - return MenuAnchor( - style: MenuStyle( - elevation: const WidgetStatePropertyAll(1), - shape: WidgetStateProperty.all( - const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(24))), - ), - padding: const WidgetStatePropertyAll(EdgeInsets.all(4)), - ), - consumeOutsideTap: true, - menuChildren: AlbumSortMode.values - .map( - (mode) => MenuItemButton( - leadingIcon: albumSortOption == mode - ? albumSortIsReverse - ? Icon( - Icons.keyboard_arrow_down, - color: albumSortOption == mode - ? context.colorScheme.onPrimary - : context.colorScheme.onSurface, - ) - : Icon( - Icons.keyboard_arrow_up_rounded, - color: albumSortOption == mode - ? context.colorScheme.onPrimary - : context.colorScheme.onSurface, - ) - : const Icon(Icons.abc, color: Colors.transparent), - onPressed: () { - final selected = albumSortOption == mode; - // Switch direction - if (selected) { - ref.read(albumSortOrderProvider.notifier).changeSortDirection(!albumSortIsReverse); - } else { - ref.read(albumSortByOptionsProvider.notifier).changeSortMode(mode); - } - }, - style: ButtonStyle( - padding: WidgetStateProperty.all(const EdgeInsets.fromLTRB(16, 16, 32, 16)), - backgroundColor: WidgetStateProperty.all( - albumSortOption == mode ? context.colorScheme.primary : Colors.transparent, - ), - shape: WidgetStateProperty.all( - const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(24))), - ), - ), - child: Text( - mode.label.tr(), - style: context.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w600, - color: albumSortOption == mode - ? context.colorScheme.onPrimary - : context.colorScheme.onSurface.withAlpha(185), - ), - ), - ), - ) - .toList(), - builder: (context, controller, child) { - return GestureDetector( - onTap: () { - if (controller.isOpen) { - controller.close(); - } else { - controller.open(); - } - }, - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: 5), - child: Transform.rotate( - angle: 90 * pi / 180, - child: Icon( - Icons.compare_arrows_rounded, - size: 18, - color: context.colorScheme.onSurface.withAlpha(225), - ), - ), - ), - Text( - albumSortOption.label.tr(), - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - color: context.colorScheme.onSurface.withAlpha(225), - ), - ), - ], - ), - ); - }, - ); - } -} diff --git a/mobile/lib/pages/backup/album_preview.page.dart b/mobile/lib/pages/backup/album_preview.page.dart deleted file mode 100644 index def31afcd4..0000000000 --- a/mobile/lib/pages/backup/album_preview.page.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; - -@RoutePage() -class AlbumPreviewPage extends HookConsumerWidget { - final Album album; - const AlbumPreviewPage({super.key, required this.album}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final assets = useState>([]); - - getAssetsInAlbum() async { - assets.value = await ref.read(albumMediaRepositoryProvider).getAssets(album.localId!); - } - - useEffect(() { - getAssetsInAlbum(); - return null; - }, []); - - return Scaffold( - appBar: AppBar( - elevation: 0, - title: Column( - children: [ - Text(album.name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - Padding( - padding: const EdgeInsets.only(top: 4.0), - child: Text( - "ID ${album.id}", - style: TextStyle( - fontSize: 10, - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_new_rounded)), - ), - body: GridView.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 5, - crossAxisSpacing: 2, - mainAxisSpacing: 2, - ), - itemCount: assets.value.length, - itemBuilder: (context, index) { - return ImmichThumbnail(asset: assets.value[index], width: 100, height: 100); - }, - ), - ); - } -} diff --git a/mobile/lib/pages/backup/backup_album_selection.page.dart b/mobile/lib/pages/backup/backup_album_selection.page.dart deleted file mode 100644 index d222211577..0000000000 --- a/mobile/lib/pages/backup/backup_album_selection.page.dart +++ /dev/null @@ -1,225 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; -import 'package:immich_mobile/widgets/backup/album_info_card.dart'; -import 'package:immich_mobile/widgets/backup/album_info_list_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; - -@RoutePage() -class BackupAlbumSelectionPage extends HookConsumerWidget { - const BackupAlbumSelectionPage({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums; - final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums; - final enableSyncUploadAlbum = useAppSettingsState(AppSettingsEnum.syncAlbums); - final isDarkTheme = context.isDarkTheme; - final albums = ref.watch(backupProvider).availableAlbums; - - useEffect(() { - ref.watch(backupProvider.notifier).getBackupInfo(); - return null; - }, []); - - buildAlbumSelectionList() { - if (albums.isEmpty) { - return const SliverToBoxAdapter(child: Center(child: CircularProgressIndicator())); - } - - return SliverPadding( - padding: const EdgeInsets.symmetric(vertical: 12.0), - sliver: SliverList( - delegate: SliverChildBuilderDelegate(((context, index) { - return AlbumInfoListTile(album: albums[index]); - }), childCount: albums.length), - ), - ); - } - - buildAlbumSelectionGrid() { - if (albums.isEmpty) { - return const SliverToBoxAdapter(child: Center(child: CircularProgressIndicator())); - } - - return SliverPadding( - padding: const EdgeInsets.all(12.0), - sliver: SliverGrid.builder( - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 300, - mainAxisSpacing: 12, - crossAxisSpacing: 12, - ), - itemCount: albums.length, - itemBuilder: ((context, index) { - return AlbumInfoCard(album: albums[index]); - }), - ), - ); - } - - buildSelectedAlbumNameChip() { - return selectedBackupAlbums.map((album) { - void removeSelection() => ref.read(backupProvider.notifier).removeAlbumForBackup(album); - - return Padding( - padding: const EdgeInsets.only(right: 8.0), - child: GestureDetector( - onTap: removeSelection, - child: Chip( - label: Text( - album.name, - style: TextStyle( - fontSize: 12, - color: isDarkTheme ? Colors.black : Colors.white, - fontWeight: FontWeight.bold, - ), - ), - backgroundColor: context.primaryColor, - deleteIconColor: isDarkTheme ? Colors.black : Colors.white, - deleteIcon: const Icon(Icons.cancel_rounded, size: 15), - onDeleted: removeSelection, - ), - ), - ); - }).toSet(); - } - - buildExcludedAlbumNameChip() { - return excludedBackupAlbums.map((album) { - void removeSelection() { - ref.watch(backupProvider.notifier).removeExcludedAlbumForBackup(album); - } - - return GestureDetector( - onTap: removeSelection, - child: Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Chip( - label: Text( - album.name, - style: TextStyle(fontSize: 12, color: context.scaffoldBackgroundColor, fontWeight: FontWeight.bold), - ), - backgroundColor: Colors.red[300], - deleteIconColor: context.scaffoldBackgroundColor, - deleteIcon: const Icon(Icons.cancel_rounded, size: 15), - onDeleted: removeSelection, - ), - ), - ); - }).toSet(); - } - - handleSyncAlbumToggle(bool isEnable) async { - if (isEnable) { - await ref.read(albumProvider.notifier).refreshRemoteAlbums(); - for (final album in selectedBackupAlbums) { - await ref.read(albumProvider.notifier).createSyncAlbum(album.name); - } - } - } - - return Scaffold( - appBar: AppBar( - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - title: const Text("backup_album_selection_page_select_albums").tr(), - elevation: 0, - ), - body: CustomScrollView( - physics: const ClampingScrollPhysics(), - slivers: [ - SliverToBoxAdapter( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), - child: Text("backup_album_selection_page_selection_info", style: context.textTheme.titleSmall).tr(), - ), - - // Selected Album Chips - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Wrap(children: [...buildSelectedAlbumNameChip(), ...buildExcludedAlbumNameChip()]), - ), - - SettingsSwitchListTile( - valueNotifier: enableSyncUploadAlbum, - title: "sync_albums".tr(), - subtitle: "sync_upload_album_setting_subtitle".tr(), - contentPadding: const EdgeInsets.symmetric(horizontal: 16), - titleStyle: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.bold), - subtitleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.primary), - onChanged: handleSyncAlbumToggle, - ), - - ListTile( - title: Text( - "backup_album_selection_page_albums_device".tr( - namedArgs: {'count': ref.watch(backupProvider).availableAlbums.length.toString()}, - ), - style: context.textTheme.titleSmall, - ), - subtitle: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text( - "backup_album_selection_page_albums_tap", - style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor), - ).tr(), - ), - trailing: IconButton( - splashRadius: 16, - icon: Icon(Icons.info, size: 20, color: context.primaryColor), - onPressed: () { - // show the dialog - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))), - elevation: 5, - title: Text( - 'backup_album_selection_page_selection_info', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: context.primaryColor), - ).tr(), - content: SingleChildScrollView( - child: ListBody( - children: [ - const Text( - 'backup_album_selection_page_assets_scatter', - style: TextStyle(fontSize: 14), - ).tr(), - ], - ), - ), - ); - }, - ); - }, - ), - ), - - // buildSearchBar(), - ], - ), - ), - SliverLayoutBuilder( - builder: (context, constraints) { - if (constraints.crossAxisExtent > 600) { - return buildAlbumSelectionGrid(); - } else { - return buildAlbumSelectionList(); - } - }, - ), - ], - ), - ); - } -} diff --git a/mobile/lib/pages/backup/backup_controller.page.dart b/mobile/lib/pages/backup/backup_controller.page.dart deleted file mode 100644 index 1e008be1bb..0000000000 --- a/mobile/lib/pages/backup/backup_controller.page.dart +++ /dev/null @@ -1,286 +0,0 @@ -import 'dart:io'; -import 'dart:math'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; -import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; -import 'package:immich_mobile/providers/websocket.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/backup/backup_info_card.dart'; -import 'package:immich_mobile/widgets/backup/current_backup_asset_info_box.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; - -@RoutePage() -class BackupControllerPage extends HookConsumerWidget { - const BackupControllerPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - BackUpState backupState = ref.watch(backupProvider); - final hasAnyAlbum = backupState.selectedBackupAlbums.isNotEmpty; - final didGetBackupInfo = useState(false); - - bool hasExclusiveAccess = backupState.backupProgress != BackUpProgressEnum.inBackground; - bool shouldBackup = - backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length == 0 || - !hasExclusiveAccess - ? false - : true; - - useEffect(() { - // Update the background settings information just to make sure we - // have the latest, since the platform channel will not update - // automatically - if (Platform.isIOS) { - ref.watch(iOSBackgroundSettingsProvider.notifier).refresh(); - } - - ref.watch(websocketProvider.notifier).stopListenToEvent('on_upload_success'); - - return () { - WakelockPlus.disable(); - }; - }, []); - - useEffect(() { - if (backupState.backupProgress == BackUpProgressEnum.idle && !didGetBackupInfo.value) { - ref.watch(backupProvider.notifier).getBackupInfo(); - didGetBackupInfo.value = true; - } - return null; - }, [backupState.backupProgress]); - - useEffect(() { - if (backupState.backupProgress == BackUpProgressEnum.inProgress) { - WakelockPlus.enable(); - } else { - WakelockPlus.disable(); - } - - return null; - }, [backupState.backupProgress]); - - Widget buildSelectedAlbumName() { - var text = "backup_controller_page_backup_selected".tr(); - var albums = ref.watch(backupProvider).selectedBackupAlbums; - - if (albums.isNotEmpty) { - for (var album in albums) { - if (album.name == "Recent" || album.name == "Recents") { - text += "${album.name} (${'all'.tr()}), "; - } else { - text += "${album.name}, "; - } - } - - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - text.trim().substring(0, text.length - 2), - style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor), - ), - ); - } else { - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - "backup_controller_page_none_selected".tr(), - style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor), - ), - ); - } - } - - Widget buildExcludedAlbumName() { - var text = "backup_controller_page_excluded".tr(); - var albums = ref.watch(backupProvider).excludedBackupAlbums; - - if (albums.isNotEmpty) { - for (var album in albums) { - text += "${album.name}, "; - } - - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - text.trim().substring(0, text.length - 2), - style: context.textTheme.labelLarge?.copyWith(color: Colors.red[300]), - ), - ); - } else { - return const SizedBox(); - } - } - - buildFolderSelectionTile() { - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Card( - shape: RoundedRectangleBorder( - borderRadius: const BorderRadius.all(Radius.circular(20)), - side: BorderSide(color: context.colorScheme.outlineVariant, width: 1), - ), - elevation: 0, - borderOnForeground: false, - child: ListTile( - minVerticalPadding: 18, - title: Text("backup_controller_page_albums", style: context.textTheme.titleMedium).tr(), - subtitle: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "backup_controller_page_to_backup", - style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ).tr(), - buildSelectedAlbumName(), - buildExcludedAlbumName(), - ], - ), - ), - trailing: ElevatedButton( - onPressed: () async { - await context.pushRoute(const BackupAlbumSelectionRoute()); - // waited until returning from selection - await ref.read(backupProvider.notifier).backupAlbumSelectionDone(); - // waited until backup albums are stored in DB - await ref.read(albumProvider.notifier).refreshDeviceAlbums(); - }, - child: const Text("select", style: TextStyle(fontWeight: FontWeight.bold)).tr(), - ), - ), - ), - ); - } - - void startBackup() { - ref.watch(errorBackupListProvider.notifier).empty(); - if (ref.watch(backupProvider).backupProgress != BackUpProgressEnum.inBackground) { - ref.watch(backupProvider.notifier).startBackupProcess(); - } - } - - Widget buildBackupButton() { - return Padding( - padding: const EdgeInsets.only(top: 24), - child: Container( - child: - backupState.backupProgress == BackUpProgressEnum.inProgress || - backupState.backupProgress == BackUpProgressEnum.manualInProgress - ? ElevatedButton( - style: ElevatedButton.styleFrom( - foregroundColor: Colors.grey[50], - backgroundColor: Colors.red[300], - // padding: const EdgeInsets.all(14), - ), - onPressed: () { - if (backupState.backupProgress == BackUpProgressEnum.manualInProgress) { - ref.read(manualUploadProvider.notifier).cancelBackup(); - } else { - ref.read(backupProvider.notifier).cancelBackup(); - } - }, - child: const Text("cancel", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)).tr(), - ) - : ElevatedButton( - onPressed: shouldBackup ? startBackup : null, - child: const Text( - "backup_controller_page_start_backup", - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ).tr(), - ), - ), - ); - } - - buildBackgroundBackupInfo() { - return ListTile( - leading: const Icon(Icons.info_outline_rounded), - title: Text('background_backup_running_error'.tr()), - ); - } - - buildLoadingIndicator() { - return const Padding( - padding: EdgeInsets.only(top: 42.0), - child: Center(child: CircularProgressIndicator()), - ); - } - - return Scaffold( - appBar: AppBar( - elevation: 0, - title: const Text("backup_controller_page_backup").tr(), - leading: IconButton( - onPressed: () { - ref.watch(websocketProvider.notifier).listenUploadEvent(); - context.maybePop(true); - }, - splashRadius: 24, - icon: const Icon(Icons.arrow_back_ios_rounded), - ), - actions: [ - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: IconButton( - onPressed: () => context.pushRoute(const BackupOptionsRoute()), - splashRadius: 24, - icon: const Icon(Icons.settings_outlined), - ), - ), - ], - ), - body: Stack( - children: [ - Padding( - padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 32), - child: ListView( - // crossAxisAlignment: CrossAxisAlignment.start, - children: hasAnyAlbum - ? [ - buildFolderSelectionTile(), - BackupInfoCard( - title: "total".tr(), - subtitle: "backup_controller_page_total_sub".tr(), - info: ref.watch(backupProvider).availableAlbums.isEmpty - ? "..." - : "${backupState.allUniqueAssets.length}", - ), - BackupInfoCard( - title: "backup_controller_page_backup".tr(), - subtitle: "backup_controller_page_backup_sub".tr(), - info: ref.watch(backupProvider).availableAlbums.isEmpty - ? "..." - : "${backupState.selectedAlbumsBackupAssetsIds.length}", - ), - BackupInfoCard( - title: "backup_controller_page_remainder".tr(), - subtitle: "backup_controller_page_remainder_sub".tr(), - info: ref.watch(backupProvider).availableAlbums.isEmpty - ? "..." - : "${max(0, backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length)}", - ), - const Divider(), - const CurrentUploadingAssetInfoBox(), - if (!hasExclusiveAccess) buildBackgroundBackupInfo(), - buildBackupButton(), - ] - : [buildFolderSelectionTile(), if (!didGetBackupInfo.value) buildLoadingIndicator()], - ), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/pages/backup/backup_options.page.dart b/mobile/lib/pages/backup/backup_options.page.dart deleted file mode 100644 index 846a32a742..0000000000 --- a/mobile/lib/pages/backup/backup_options.page.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart'; - -@RoutePage() -class BackupOptionsPage extends StatelessWidget { - const BackupOptionsPage({super.key}); - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - elevation: 0, - title: const Text("backup_options_page_title").tr(), - leading: IconButton( - onPressed: () => context.maybePop(true), - splashRadius: 24, - icon: const Icon(Icons.arrow_back_ios_rounded), - ), - ), - body: const BackupSettings(), - ); - } -} diff --git a/mobile/lib/pages/backup/failed_backup_status.page.dart b/mobile/lib/pages/backup/failed_backup_status.page.dart deleted file mode 100644 index a97a133b89..0000000000 --- a/mobile/lib/pages/backup/failed_backup_status.page.dart +++ /dev/null @@ -1,116 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart'; -import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; -import 'package:intl/intl.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as base_asset; - -@RoutePage() -class FailedBackupStatusPage extends HookConsumerWidget { - const FailedBackupStatusPage({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final errorBackupList = ref.watch(errorBackupListProvider); - - return Scaffold( - appBar: AppBar( - elevation: 0, - title: Text( - "Failed Backup (${errorBackupList.length})", - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ), - leading: IconButton( - onPressed: () { - context.maybePop(true); - }, - splashRadius: 24, - icon: const Icon(Icons.arrow_back_ios_rounded), - ), - ), - body: ListView.builder( - shrinkWrap: true, - itemCount: errorBackupList.length, - itemBuilder: ((context, index) { - var errorAsset = errorBackupList.elementAt(index); - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4), - child: Card( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(15), // if you need this - ), - side: BorderSide(color: Colors.black12, width: 1), - ), - elevation: 0, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100, minHeight: 100, maxWidth: 100, maxHeight: 150), - child: ClipRRect( - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(15), - topLeft: Radius.circular(15), - ), - clipBehavior: Clip.hardEdge, - child: Image( - fit: BoxFit.cover, - image: LocalThumbProvider(id: errorAsset.asset.localId!, assetType: base_asset.AssetType.video), - ), - ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - DateFormat.yMMMMd().format( - DateTime.parse(errorAsset.fileCreatedAt.toString()).toLocal(), - ), - style: TextStyle( - fontWeight: FontWeight.w600, - color: context.isDarkTheme ? Colors.white70 : Colors.grey[800], - ), - ), - Icon(Icons.error, color: Colors.red.withAlpha(200), size: 18), - ], - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text( - errorAsset.fileName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle(fontWeight: FontWeight.bold, color: context.primaryColor), - ), - ), - Text( - errorAsset.errorMessage, - style: TextStyle( - fontWeight: FontWeight.w500, - color: context.isDarkTheme ? Colors.white70 : Colors.grey[800], - ), - ), - ], - ), - ), - ), - ], - ), - ), - ); - }), - ), - ); - } -} diff --git a/mobile/lib/pages/common/activities.page.dart b/mobile/lib/pages/common/activities.page.dart deleted file mode 100644 index 9d1123dbca..0000000000 --- a/mobile/lib/pages/common/activities.page.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' hide Store; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/activities/activity_text_field.dart'; -import 'package:immich_mobile/widgets/activities/activity_tile.dart'; -import 'package:immich_mobile/widgets/activities/dismissible_activity.dart'; - -@RoutePage() -class ActivitiesPage extends HookConsumerWidget { - const ActivitiesPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - // Album has to be set in the provider before reaching this page - final album = ref.watch(currentAlbumProvider)!; - final asset = ref.watch(currentAssetProvider); - final user = ref.watch(currentUserProvider); - - final activityNotifier = ref.read(albumActivityProvider(album.remoteId!, asset?.remoteId).notifier); - final activities = ref.watch(albumActivityProvider(album.remoteId!, asset?.remoteId)); - - final listViewScrollController = useScrollController(); - - Future onAddComment(String comment) async { - await activityNotifier.addComment(comment); - // Scroll to the end of the list to show the newly added activity - await listViewScrollController.animateTo( - listViewScrollController.position.maxScrollExtent + 200, - duration: const Duration(milliseconds: 600), - curve: Curves.fastOutSlowIn, - ); - } - - return Scaffold( - appBar: AppBar(title: asset == null ? Text(album.name) : null), - body: activities.widgetWhen( - onData: (data) { - final liked = data.firstWhereOrNull( - (a) => a.type == ActivityType.like && a.user.id == user?.id && a.assetId == asset?.remoteId, - ); - - return SafeArea( - child: Stack( - children: [ - ListView.builder( - controller: listViewScrollController, - // +1 to display an additional over-scroll space after the last element - itemCount: data.length + 1, - itemBuilder: (context, index) { - // Additional vertical gap after the last element - if (index == data.length) { - return const SizedBox(height: 80); - } - - final activity = data[index]; - final canDelete = activity.user.id == user?.id || album.ownerId == user?.id; - - return Padding( - padding: const EdgeInsets.all(5), - child: DismissibleActivity( - activity.id, - ActivityTile(activity), - onDismiss: canDelete - ? (activityId) async => await activityNotifier.removeActivity(activity.id) - : null, - ), - ); - }, - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - color: context.scaffoldBackgroundColor, - child: ActivityTextField( - isEnabled: album.activityEnabled, - likeId: liked?.id, - onSubmit: onAddComment, - ), - ), - ), - ], - ), - ); - }, - ), - ); - } -} diff --git a/mobile/lib/pages/common/change_experience.page.dart b/mobile/lib/pages/common/change_experience.page.dart deleted file mode 100644 index 2cc3dede1e..0000000000 --- a/mobile/lib/pages/common/change_experience.page.dart +++ /dev/null @@ -1,168 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; -import 'package:immich_mobile/providers/gallery_permission.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; -import 'package:immich_mobile/providers/websocket.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:immich_mobile/utils/migration.dart'; -import 'package:logging/logging.dart'; -import 'package:permission_handler/permission_handler.dart'; - -@RoutePage() -class ChangeExperiencePage extends ConsumerStatefulWidget { - final bool switchingToBeta; - - const ChangeExperiencePage({super.key, required this.switchingToBeta}); - - @override - ConsumerState createState() => _ChangeExperiencePageState(); -} - -class _ChangeExperiencePageState extends ConsumerState { - AsyncValue hasMigrated = const AsyncValue.loading(); - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) => _handleMigration()); - } - - Future _handleMigration() async { - try { - await _performMigrationLogic().timeout( - const Duration(minutes: 3), - onTimeout: () async { - await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); - await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); - }, - ); - - if (mounted) { - setState(() { - HapticFeedback.heavyImpact(); - hasMigrated = const AsyncValue.data(true); - }); - } - } catch (e, s) { - Logger("ChangeExperiencePage").severe("Error during migration", e, s); - if (mounted) { - setState(() { - hasMigrated = AsyncValue.error(e, s); - }); - } - } - } - - Future _performMigrationLogic() async { - if (widget.switchingToBeta) { - final assetNotifier = ref.read(assetProvider.notifier); - if (assetNotifier.mounted) { - assetNotifier.dispose(); - } - final albumNotifier = ref.read(albumProvider.notifier); - if (albumNotifier.mounted) { - albumNotifier.dispose(); - } - - // Cancel uploads - await Store.put(StoreKey.backgroundBackup, false); - ref - .read(backupProvider.notifier) - .configureBackgroundBackup(enabled: false, onBatteryInfo: () {}, onError: (_) {}); - ref.read(backupProvider.notifier).setAutoBackup(false); - ref.read(backupProvider.notifier).cancelBackup(); - ref.read(manualUploadProvider.notifier).cancelBackup(); - // Start listening to new websocket events - ref.read(websocketProvider.notifier).stopListenToOldEvents(); - ref.read(websocketProvider.notifier).startListeningToBetaEvents(); - - await ref.read(driftProvider).reset(); - await Store.put(StoreKey.shouldResetSync, true); - final delay = Store.get(StoreKey.backupTriggerDelay, AppSettingsEnum.backupTriggerDelay.defaultValue); - if (delay >= 1000) { - await Store.put(StoreKey.backupTriggerDelay, (delay / 1000).toInt()); - } - final permission = await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); - - if (permission.isGranted) { - await ref.read(backgroundSyncProvider).syncLocal(full: true); - await migrateDeviceAssetToSqlite(ref.read(isarProvider), ref.read(driftProvider)); - await migrateBackupAlbumsToSqlite(ref.read(isarProvider), ref.read(driftProvider)); - await migrateStoreToSqlite(ref.read(isarProvider), ref.read(driftProvider)); - await ref.read(backgroundServiceProvider).disableService(); - } - } else { - await ref.read(backgroundSyncProvider).cancel(); - ref.read(websocketProvider.notifier).stopListeningToBetaEvents(); - ref.read(websocketProvider.notifier).startListeningToOldEvents(); - ref.read(readonlyModeProvider.notifier).setReadonlyMode(false); - await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider)); - await ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); - await ref.read(backgroundWorkerFgServiceProvider).disable(); - } - - await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); - await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - AnimatedSwitcher( - duration: Durations.long4, - child: hasMigrated.when( - data: (data) => const Icon(Icons.check_circle_rounded, color: Colors.green, size: 48.0), - error: (error, stackTrace) => const Icon(Icons.error, color: Colors.red, size: 48.0), - loading: () => const SizedBox(width: 50.0, height: 50.0, child: CircularProgressIndicator()), - ), - ), - const SizedBox(height: 16.0), - SizedBox( - width: 300.0, - child: AnimatedSwitcher( - duration: Durations.long4, - child: hasMigrated.when( - data: (data) => Text( - "Migration success!\nPlease close and reopen the app to apply changes", - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ), - error: (error, stackTrace) => Text( - "Migration failed!\nError: $error", - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ), - loading: () => Text( - "Data migration in progress...\nPlease wait and don't close this page", - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ), - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/pages/common/create_album.page.dart b/mobile/lib/pages/common/create_album.page.dart deleted file mode 100644 index 0a28dfeb5a..0000000000 --- a/mobile/lib/pages/common/create_album.page.dart +++ /dev/null @@ -1,238 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/album_title.provider.dart'; -import 'package:immich_mobile/providers/album/album_viewer.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/album/album_action_filled_button.dart'; -import 'package:immich_mobile/widgets/album/album_title_text_field.dart'; -import 'package:immich_mobile/widgets/album/album_viewer_editable_description.dart'; -import 'package:immich_mobile/widgets/album/shared_album_thumbnail_image.dart'; - -@RoutePage() -// ignore: must_be_immutable -class CreateAlbumPage extends HookConsumerWidget { - final List? assets; - - const CreateAlbumPage({super.key, this.assets}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albumTitleController = useTextEditingController.fromValue(TextEditingValue.empty); - final albumTitleTextFieldFocusNode = useFocusNode(); - final albumDescriptionTextFieldFocusNode = useFocusNode(); - final isAlbumTitleTextFieldFocus = useState(false); - final isAlbumTitleEmpty = useState(true); - final selectedAssets = useState>(assets != null ? Set.from(assets!) : const {}); - - void onBackgroundTapped() { - albumTitleTextFieldFocusNode.unfocus(); - albumDescriptionTextFieldFocusNode.unfocus(); - isAlbumTitleTextFieldFocus.value = false; - - if (albumTitleController.text.isEmpty) { - albumTitleController.text = 'create_album_page_untitled'.tr(); - isAlbumTitleEmpty.value = false; - ref.watch(albumTitleProvider.notifier).setAlbumTitle('create_album_page_untitled'.tr()); - } - } - - onSelectPhotosButtonPressed() async { - AssetSelectionPageResult? selectedAsset = await context.pushRoute( - AlbumAssetSelectionRoute(existingAssets: selectedAssets.value, canDeselect: true), - ); - if (selectedAsset == null) { - selectedAssets.value = const {}; - } else { - selectedAssets.value = selectedAsset.selectedAssets; - } - } - - buildTitleInputField() { - return Padding( - padding: const EdgeInsets.only(right: 10, left: 10), - child: AlbumTitleTextField( - isAlbumTitleEmpty: isAlbumTitleEmpty, - albumTitleTextFieldFocusNode: albumTitleTextFieldFocusNode, - albumTitleController: albumTitleController, - isAlbumTitleTextFieldFocus: isAlbumTitleTextFieldFocus, - ), - ); - } - - buildDescriptionInputField() { - return Padding( - padding: const EdgeInsets.only(right: 10, left: 10), - child: AlbumViewerEditableDescription( - albumDescription: '', - descriptionFocusNode: albumDescriptionTextFieldFocusNode, - ), - ); - } - - buildTitle() { - if (selectedAssets.value.isEmpty) { - return SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(top: 200, left: 18), - child: Text('create_shared_album_page_share_add_assets', style: context.textTheme.labelLarge).tr(), - ), - ); - } - - return const SliverToBoxAdapter(); - } - - buildSelectPhotosButton() { - if (selectedAssets.value.isEmpty) { - return SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(top: 16, left: 16, right: 16), - child: FilledButton.icon( - style: FilledButton.styleFrom( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 16), - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))), - backgroundColor: context.colorScheme.surfaceContainerHigh, - ), - onPressed: onSelectPhotosButtonPressed, - icon: Icon(Icons.add_rounded, color: context.primaryColor), - label: Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Text( - 'create_shared_album_page_share_select_photos', - style: context.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - color: context.primaryColor, - ), - ).tr(), - ), - ), - ), - ); - } - - return const SliverToBoxAdapter(); - } - - buildControlButton() { - return Padding( - padding: const EdgeInsets.only(left: 12.0, top: 16, bottom: 16), - child: SizedBox( - height: 42, - child: ListView( - scrollDirection: Axis.horizontal, - children: [ - AlbumActionFilledButton( - iconData: Icons.add_photo_alternate_outlined, - onPressed: onSelectPhotosButtonPressed, - labelText: "add_photos".tr(), - ), - ], - ), - ), - ); - } - - buildSelectedImageGrid() { - if (selectedAssets.value.isNotEmpty) { - return SliverPadding( - padding: const EdgeInsets.only(top: 16), - sliver: SliverGrid( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - crossAxisSpacing: 5.0, - mainAxisSpacing: 5, - ), - delegate: SliverChildBuilderDelegate((BuildContext context, int index) { - return GestureDetector( - onTap: onBackgroundTapped, - child: SharedAlbumThumbnailImage(asset: selectedAssets.value.elementAt(index)), - ); - }, childCount: selectedAssets.value.length), - ), - ); - } - - return const SliverToBoxAdapter(); - } - - Future createAlbum() async { - onBackgroundTapped(); - var newAlbum = await ref - .watch(albumProvider.notifier) - .createAlbum(ref.read(albumTitleProvider), selectedAssets.value); - - if (newAlbum != null) { - await ref.read(albumProvider.notifier).refreshRemoteAlbums(); - selectedAssets.value = {}; - ref.read(albumTitleProvider.notifier).clearAlbumTitle(); - ref.read(albumViewerProvider.notifier).disableEditAlbum(); - unawaited(context.replaceRoute(AlbumViewerRoute(albumId: newAlbum.id))); - } - } - - return Scaffold( - appBar: AppBar( - elevation: 0, - centerTitle: false, - backgroundColor: context.scaffoldBackgroundColor, - leading: IconButton( - onPressed: () { - selectedAssets.value = {}; - context.maybePop(); - }, - icon: const Icon(Icons.close_rounded), - ), - title: const Text('create_album').tr(), - actions: [ - TextButton( - onPressed: albumTitleController.text.isNotEmpty ? createAlbum : null, - child: Text( - 'create'.tr(), - style: TextStyle( - fontWeight: FontWeight.bold, - color: albumTitleController.text.isNotEmpty ? context.primaryColor : context.themeData.disabledColor, - ), - ), - ), - ], - ), - body: GestureDetector( - onTap: onBackgroundTapped, - child: CustomScrollView( - slivers: [ - SliverAppBar( - backgroundColor: context.scaffoldBackgroundColor, - elevation: 5, - automaticallyImplyLeading: false, - pinned: true, - floating: false, - bottom: PreferredSize( - preferredSize: const Size.fromHeight(125.0), - child: Column( - children: [ - buildTitleInputField(), - buildDescriptionInputField(), - if (selectedAssets.value.isNotEmpty) buildControlButton(), - ], - ), - ), - ), - buildTitle(), - buildSelectPhotosButton(), - buildSelectedImageGrid(), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/pages/common/gallery_stacked_children.dart b/mobile/lib/pages/common/gallery_stacked_children.dart deleted file mode 100644 index 68123509ae..0000000000 --- a/mobile/lib/pages/common/gallery_stacked_children.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; - -class GalleryStackedChildren extends HookConsumerWidget { - final ValueNotifier stackIndex; - - const GalleryStackedChildren(this.stackIndex, {super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final asset = ref.watch(currentAssetProvider); - if (asset == null) { - return const SizedBox(); - } - - final stackId = asset.stackId; - if (stackId == null) { - return const SizedBox(); - } - - final stackElements = ref.watch(assetStackStateProvider(stackId)); - final showControls = ref.watch(showControlsProvider); - - return IgnorePointer( - ignoring: !showControls, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 100), - opacity: showControls ? 1.0 : 0.0, - child: SizedBox( - height: 80, - child: ListView.builder( - shrinkWrap: true, - scrollDirection: Axis.horizontal, - itemCount: stackElements.length, - padding: const EdgeInsets.only(left: 5, right: 5, bottom: 30), - itemBuilder: (context, index) { - final currentAsset = stackElements.elementAt(index); - final assetId = currentAsset.remoteId; - if (assetId == null) { - return const SizedBox(); - } - - return Padding( - key: ValueKey(currentAsset.id), - padding: const EdgeInsets.only(right: 5), - child: GestureDetector( - onTap: () { - stackIndex.value = index; - ref.read(currentAssetProvider.notifier).set(currentAsset); - }, - child: Container( - width: 60, - height: 60, - decoration: index == stackIndex.value - ? const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(6)), - border: Border.fromBorderSide(BorderSide(color: Colors.white, width: 2)), - ) - : const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(6)), - border: null, - ), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(4)), - child: Image( - fit: BoxFit.cover, - image: RemoteImageProvider.thumbnail(assetId: assetId, thumbhash: asset.thumbhash ?? ""), - ), - ), - ), - ), - ); - }, - ), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart deleted file mode 100644 index 1d43bff167..0000000000 --- a/mobile/lib/pages/common/gallery_viewer.page.dart +++ /dev/null @@ -1,438 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:math'; -import 'dart:ui' as ui; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' hide Store; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/scroll_extensions.dart'; -import 'package:immich_mobile/pages/common/download_panel.dart'; -import 'package:immich_mobile/pages/common/gallery_stacked_children.dart'; -import 'package:immich_mobile/pages/common/native_video_viewer.page.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_viewer/advanced_bottom_sheet.dart'; -import 'package:immich_mobile/widgets/asset_viewer/bottom_gallery_bar.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/detail_panel.dart'; -import 'package:immich_mobile/widgets/asset_viewer/gallery_app_bar.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; -import 'package:immich_mobile/widgets/photo_view/photo_view_gallery.dart'; -import 'package:immich_mobile/widgets/photo_view/src/photo_view_computed_scale.dart'; -import 'package:immich_mobile/widgets/photo_view/src/photo_view_scale_state.dart'; -import 'package:immich_mobile/widgets/photo_view/src/utils/photo_view_hero_attributes.dart'; - -@RoutePage() -// ignore: must_be_immutable -/// Expects [currentAssetProvider] to be set before navigating to this page -class GalleryViewerPage extends HookConsumerWidget { - final int initialIndex; - final int heroOffset; - final bool showStack; - final RenderList renderList; - - GalleryViewerPage({ - super.key, - required this.renderList, - this.initialIndex = 0, - this.heroOffset = 0, - this.showStack = false, - }) : controller = PageController(initialPage: initialIndex); - - final PageController controller; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final totalAssets = useState(renderList.totalAssets); - final isZoomed = useState(false); - final stackIndex = useState(0); - final localPosition = useRef(null); - final currentIndex = useValueNotifier(initialIndex); - final loadAsset = renderList.loadAsset; - final isPlayingMotionVideo = ref.watch(isPlayingMotionVideoProvider); - final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); - - final videoPlayerKeys = useRef>({}); - - GlobalKey getVideoPlayerKey(int id) { - videoPlayerKeys.value.putIfAbsent(id, () => GlobalKey()); - return videoPlayerKeys.value[id]!; - } - - Future precacheNextImage(int index) async { - if (!context.mounted) { - return; - } - - void onError(Object exception, StackTrace? stackTrace) { - // swallow error silently - log.severe('Error precaching next image: $exception, $stackTrace'); - } - - try { - if (index < totalAssets.value && index >= 0) { - final asset = loadAsset(index); - await precacheImage( - ImmichImage.imageProvider(asset: asset, width: context.width, height: context.height), - context, - onError: onError, - ); - } - } catch (e) { - // swallow error silently - log.severe('Error precaching next image: $e'); - await context.maybePop(); - } - } - - useEffect(() { - if (ref.read(showControlsProvider)) { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - } else { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); - } - - // Delay this a bit so we can finish loading the page - Timer(const Duration(milliseconds: 400), () { - precacheNextImage(currentIndex.value + 1); - }); - - return null; - }, const []); - - useEffect(() { - final asset = loadAsset(currentIndex.value); - - if (asset.isRemote) { - ref.read(castProvider.notifier).loadMediaOld(asset, false); - } else { - if (isCasting) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (context.mounted) { - ref.read(castProvider.notifier).stop(); - context.scaffoldMessenger.showSnackBar( - SnackBar( - duration: const Duration(seconds: 1), - content: Text( - "local_asset_cast_failed".tr(), - style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), - ), - ), - ); - } - }); - } - } - return null; - }, [ref.watch(castProvider).isCasting]); - - void showInfo() { - final asset = ref.read(currentAssetProvider); - if (asset == null) { - return; - } - showModalBottomSheet( - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(15.0))), - barrierColor: Colors.transparent, - isScrollControlled: true, - showDragHandle: true, - enableDrag: true, - context: context, - useSafeArea: true, - builder: (context) { - return DraggableScrollableSheet( - minChildSize: 0.5, - maxChildSize: 1, - initialChildSize: 0.75, - expand: false, - builder: (context, scrollController) { - return Padding( - padding: EdgeInsets.only(bottom: context.viewInsets.bottom), - child: ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.advancedTroubleshooting) - ? AdvancedBottomSheet(assetDetail: asset, scrollController: scrollController) - : DetailPanel(asset: asset, scrollController: scrollController), - ); - }, - ); - }, - ); - } - - void handleSwipeUpDown(DragUpdateDetails details) { - const int sensitivity = 15; - const int dxThreshold = 50; - const double ratioThreshold = 3.0; - - if (isZoomed.value) { - return; - } - - // Guard [localPosition] null - if (localPosition.value == null) { - return; - } - - // Check for delta from initial down point - final d = details.localPosition - localPosition.value!; - // If the magnitude of the dx swipe is large, we probably didn't mean to go down - if (d.dx.abs() > dxThreshold) { - return; - } - - final ratio = d.dy / max(d.dx.abs(), 1); - if (d.dy > sensitivity && ratio > ratioThreshold) { - context.maybePop(); - } else if (d.dy < -sensitivity && ratio < -ratioThreshold) { - showInfo(); - } - } - - ref.listen(showControlsProvider, (_, show) { - if (show || Platform.isIOS) { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - return; - } - - // This prevents the bottom bar from "dropping" while the controls are being hidden - Timer(const Duration(milliseconds: 100), () { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); - }); - }); - - PhotoViewGalleryPageOptions buildImage(Asset asset) { - return PhotoViewGalleryPageOptions( - onDragStart: (_, details, __, ___) { - localPosition.value = details.localPosition; - }, - onDragUpdate: (_, details, __) { - handleSwipeUpDown(details); - }, - onTapDown: (ctx, tapDownDetails, _) { - final tapToNavigate = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.tapToNavigate); - if (!tapToNavigate) { - ref.read(showControlsProvider.notifier).toggle(); - return; - } - - double tapX = tapDownDetails.globalPosition.dx; - double screenWidth = ctx.width; - - // We want to change images if the user taps in the leftmost or - // rightmost quarter of the screen - bool tappedLeftSide = tapX < screenWidth / 4; - bool tappedRightSide = tapX > screenWidth * (3 / 4); - - int? currentPage = controller.page?.toInt(); - int maxPage = renderList.totalAssets - 1; - - if (tappedLeftSide && currentPage != null) { - // Nested if because we don't want to fallback to show/hide controls - if (currentPage != 0) { - controller.jumpToPage(currentPage - 1); - } - } else if (tappedRightSide && currentPage != null) { - // Nested if because we don't want to fallback to show/hide controls - if (currentPage != maxPage) { - controller.jumpToPage(currentPage + 1); - } - } else { - ref.read(showControlsProvider.notifier).toggle(); - } - }, - onLongPressStart: asset.isMotionPhoto - ? (_, __, ___) { - ref.read(isPlayingMotionVideoProvider.notifier).playing = true; - } - : null, - imageProvider: ImmichImage.imageProvider(asset: asset), - heroAttributes: _getHeroAttributes(asset), - filterQuality: FilterQuality.high, - tightMode: true, - initialScale: PhotoViewComputedScale.contained * 0.99, - minScale: PhotoViewComputedScale.contained * 0.99, - errorBuilder: (context, error, stackTrace) => ImmichImage(asset, fit: BoxFit.contain), - ); - } - - PhotoViewGalleryPageOptions buildVideo(BuildContext context, Asset asset) { - return PhotoViewGalleryPageOptions.customChild( - onDragStart: (_, details, __, ___) => localPosition.value = details.localPosition, - onDragUpdate: (_, details, __) => handleSwipeUpDown(details), - heroAttributes: _getHeroAttributes(asset), - filterQuality: FilterQuality.high, - initialScale: PhotoViewComputedScale.contained * 0.99, - maxScale: 1.0, - minScale: PhotoViewComputedScale.contained * 0.99, - basePosition: Alignment.center, - child: SizedBox( - width: context.width, - height: context.height, - child: NativeVideoViewerPage( - key: getVideoPlayerKey(asset.id), - asset: asset, - image: Image( - key: ValueKey(asset), - image: ImmichImage.imageProvider(asset: asset, width: context.width, height: context.height), - fit: BoxFit.contain, - height: context.height, - width: context.width, - alignment: Alignment.center, - ), - ), - ), - ); - } - - PhotoViewGalleryPageOptions buildAsset(BuildContext context, int index) { - var newAsset = loadAsset(index); - - final stackId = newAsset.stackId; - if (stackId != null && currentIndex.value == index) { - final stackElements = ref.read(assetStackStateProvider(newAsset.stackId!)); - if (stackIndex.value < stackElements.length) { - newAsset = stackElements.elementAt(stackIndex.value); - } - } - - if (newAsset.isImage && !isPlayingMotionVideo) { - return buildImage(newAsset); - } - return buildVideo(context, newAsset); - } - - return PopScope( - // Change immersive mode back to normal "edgeToEdge" mode - onPopInvokedWithResult: (didPop, _) => SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge), - child: Scaffold( - backgroundColor: Colors.black, - body: Stack( - children: [ - PhotoViewGallery.builder( - key: const ValueKey('gallery'), - scaleStateChangedCallback: (state) { - final asset = ref.read(currentAssetProvider); - if (asset == null) { - return; - } - - if (asset.isImage && !ref.read(isPlayingMotionVideoProvider)) { - isZoomed.value = state != PhotoViewScaleState.initial; - ref.read(showControlsProvider.notifier).show = !isZoomed.value; - } - }, - gaplessPlayback: true, - loadingBuilder: (context, event, index) { - final asset = loadAsset(index); - return ClipRect( - child: Stack( - fit: StackFit.expand, - children: [ - BackdropFilter(filter: ui.ImageFilter.blur(sigmaX: 10, sigmaY: 10)), - ImmichThumbnail(key: ValueKey(asset), asset: asset, fit: BoxFit.contain), - ], - ), - ); - }, - pageController: controller, - scrollPhysics: isZoomed.value - ? const NeverScrollableScrollPhysics() // Don't allow paging while scrolled in - : (Platform.isIOS - ? const FastScrollPhysics() // Use bouncing physics for iOS - : const FastClampingScrollPhysics() // Use heavy physics for Android - ), - itemCount: totalAssets.value, - scrollDirection: Axis.horizontal, - onPageChanged: (value, _) { - final next = currentIndex.value < value ? value + 1 : value - 1; - - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - - final newAsset = loadAsset(value); - - currentIndex.value = value; - stackIndex.value = 0; - - ref.read(currentAssetProvider.notifier).set(newAsset); - - // Wait for page change animation to finish, then precache the next image - Timer(const Duration(milliseconds: 400), () { - precacheNextImage(next); - }); - - context.scaffoldMessenger.hideCurrentSnackBar(); - - // send image to casting if the server has it - if (newAsset.isRemote) { - ref.read(castProvider.notifier).loadMediaOld(newAsset, false); - } else { - context.scaffoldMessenger.clearSnackBars(); - - if (isCasting) { - ref.read(castProvider.notifier).stop(); - context.scaffoldMessenger.showSnackBar( - SnackBar( - duration: const Duration(seconds: 2), - content: Text( - "local_asset_cast_failed".tr(), - style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), - ), - ), - ); - } - } - }, - builder: buildAsset, - ), - Positioned( - top: 0, - left: 0, - right: 0, - child: GalleryAppBar(key: const ValueKey('app-bar'), showInfo: showInfo), - ), - Positioned( - bottom: 0, - left: 0, - right: 0, - child: Column( - children: [ - GalleryStackedChildren(stackIndex), - BottomGalleryBar( - key: const ValueKey('bottom-bar'), - renderList: renderList, - totalAssets: totalAssets, - controller: controller, - showStack: showStack, - stackIndex: stackIndex, - assetIndex: currentIndex, - ), - ], - ), - ), - const DownloadPanel(), - ], - ), - ), - ); - } - - @pragma('vm:prefer-inline') - PhotoViewHeroAttributes _getHeroAttributes(Asset asset) { - return PhotoViewHeroAttributes( - tag: asset.isInDb ? asset.id + heroOffset : '${asset.remoteId}-$heroOffset', - transitionOnUserGestures: true, - ); - } -} diff --git a/mobile/lib/pages/common/native_video_viewer.page.dart b/mobile/lib/pages/common/native_video_viewer.page.dart deleted file mode 100644 index b1eed29c5c..0000000000 --- a/mobile/lib/pages/common/native_video_viewer.page.dart +++ /dev/null @@ -1,282 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' hide Store; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart'; -import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:immich_mobile/widgets/asset_viewer/custom_video_player_controls.dart'; -import 'package:logging/logging.dart'; -import 'package:native_video_player/native_video_player.dart'; - -@RoutePage() -class NativeVideoViewerPage extends HookConsumerWidget { - static final log = Logger('NativeVideoViewer'); - final Asset asset; - final bool showControls; - final int playbackDelayFactor; - final Widget image; - - const NativeVideoViewerPage({ - super.key, - required this.asset, - required this.image, - this.showControls = true, - this.playbackDelayFactor = 1, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final videoId = asset.id.toString(); - final controller = useState(null); - final shouldPlayOnForeground = useRef(true); - - final currentAsset = useState(ref.read(currentAssetProvider)); - final isCurrent = currentAsset.value == asset; - - // Used to show the placeholder during hero animations for remote videos to avoid a stutter - final isVisible = useState(Platform.isIOS && asset.isLocal); - - final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); - - final isVideoReady = useState(false); - - Future createSource() async { - if (!context.mounted) { - return null; - } - - try { - final local = asset.local; - if (local != null && asset.livePhotoVideoId == null) { - final file = await local.file; - if (file == null) { - throw Exception('No file found for the video'); - } - - final source = await VideoSource.init(path: file.path, type: VideoSourceType.file); - return source; - } - - // Use a network URL for the video player controller - final serverEndpoint = Store.get(StoreKey.serverEndpoint); - final isOriginalVideo = ref - .read(appSettingsServiceProvider) - .getSetting(AppSettingsEnum.loadOriginalVideo); - final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback'; - final String videoUrl = asset.livePhotoVideoId != null - ? '$serverEndpoint/assets/${asset.livePhotoVideoId}/$postfixUrl' - : '$serverEndpoint/assets/${asset.remoteId}/$postfixUrl'; - - final source = await VideoSource.init( - path: videoUrl, - type: VideoSourceType.network, - headers: ApiService.getRequestHeaders(), - ); - return source; - } catch (error) { - log.severe('Error creating video source for asset ${asset.fileName}: $error'); - return null; - } - } - - final videoSource = useMemoized>(() => createSource()); - final aspectRatio = useState(asset.aspectRatio); - useMemoized(() async { - if (!context.mounted || aspectRatio.value != null) { - return null; - } - - try { - aspectRatio.value = await ref.read(assetServiceProvider).getAspectRatio(asset); - } catch (error) { - log.severe('Error getting aspect ratio for asset ${asset.fileName}: $error'); - } - }); - - void onPlaybackReady() async { - final videoController = controller.value; - if (videoController == null || !isCurrent || !context.mounted) { - return; - } - - final notifier = ref.read(videoPlayerProvider(videoId).notifier); - notifier.onNativePlaybackReady(); - - isVideoReady.value = true; - - try { - final autoPlayVideo = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.autoPlayVideo); - if (autoPlayVideo) { - await notifier.play(); - } - await notifier.setVolume(1); - } catch (error) { - log.severe('Error playing video: $error'); - } - } - - void onPlaybackStatusChanged() { - if (!context.mounted) return; - ref.read(videoPlayerProvider(videoId).notifier).onNativeStatusChanged(); - } - - void onPlaybackPositionChanged() { - if (!context.mounted) return; - ref.read(videoPlayerProvider(videoId).notifier).onNativePositionChanged(); - } - - void onPlaybackEnded() { - if (!context.mounted) return; - - ref.read(videoPlayerProvider(videoId).notifier).onNativePlaybackEnded(); - - final videoController = controller.value; - if (videoController?.playbackInfo?.status == PlaybackStatus.stopped && - !ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.loopVideo)) { - ref.read(isPlayingMotionVideoProvider.notifier).playing = false; - } - } - - void removeListeners(NativeVideoPlayerController controller) { - controller.onPlaybackPositionChanged.removeListener(onPlaybackPositionChanged); - controller.onPlaybackStatusChanged.removeListener(onPlaybackStatusChanged); - controller.onPlaybackReady.removeListener(onPlaybackReady); - controller.onPlaybackEnded.removeListener(onPlaybackEnded); - } - - void initController(NativeVideoPlayerController nc) async { - if (controller.value != null || !context.mounted) { - return; - } - - final source = await videoSource; - if (source == null) { - return; - } - - final notifier = ref.read(videoPlayerProvider(videoId).notifier); - notifier.attachController(nc); - - nc.onPlaybackPositionChanged.addListener(onPlaybackPositionChanged); - nc.onPlaybackStatusChanged.addListener(onPlaybackStatusChanged); - nc.onPlaybackReady.addListener(onPlaybackReady); - nc.onPlaybackEnded.addListener(onPlaybackEnded); - - unawaited( - nc.loadVideoSource(source).catchError((error) { - log.severe('Error loading video source: $error'); - }), - ); - final loopVideo = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.loopVideo); - await notifier.setLoop(loopVideo); - - controller.value = nc; - } - - ref.listen(currentAssetProvider, (_, value) { - final playerController = controller.value; - if (playerController != null && value != asset) { - removeListeners(playerController); - } - - final curAsset = currentAsset.value; - if (curAsset == asset) { - return; - } - - final imageToVideo = curAsset != null && !curAsset.isVideo; - - // No need to delay video playback when swiping from an image to a video - if (imageToVideo && Platform.isIOS) { - currentAsset.value = value; - onPlaybackReady(); - return; - } - - // Delay the video playback to avoid a stutter in the swipe animation - Timer( - Platform.isIOS - ? Duration(milliseconds: 300 * playbackDelayFactor) - : imageToVideo - ? Duration(milliseconds: 200 * playbackDelayFactor) - : Duration(milliseconds: 400 * playbackDelayFactor), - () { - if (!context.mounted) { - return; - } - - currentAsset.value = value; - if (currentAsset.value == asset) { - onPlaybackReady(); - } - }, - ); - }); - - useEffect(() { - // If opening a remote video from a hero animation, delay visibility to avoid a stutter - final timer = isVisible.value ? null : Timer(const Duration(milliseconds: 300), () => isVisible.value = true); - - return () { - timer?.cancel(); - final playerController = controller.value; - if (playerController == null) { - return; - } - removeListeners(playerController); - playerController.stop().catchError((error) { - log.fine('Error stopping video: $error'); - }); - }; - }, const []); - - useOnAppLifecycleStateChange((_, state) async { - final notifier = ref.read(videoPlayerProvider(videoId).notifier); - if (state == AppLifecycleState.resumed && shouldPlayOnForeground.value) { - await notifier.play(); - } else if (state == AppLifecycleState.paused) { - final videoPlaying = await controller.value?.isPlaying(); - if (videoPlaying ?? true) { - shouldPlayOnForeground.value = true; - await notifier.pause(); - } else { - shouldPlayOnForeground.value = false; - } - } - }); - - return Stack( - children: [ - // This remains under the video to avoid flickering - // For motion videos, this is the image portion of the asset - if (!isVideoReady.value || asset.isMotionPhoto) Center(key: ValueKey(asset.id), child: image), - if (aspectRatio.value != null && !isCasting) - Visibility.maintain( - key: ValueKey(asset), - visible: isVisible.value, - child: Center( - key: ValueKey(asset), - child: AspectRatio( - key: ValueKey(asset), - aspectRatio: aspectRatio.value!, - child: isCurrent ? NativeVideoPlayerView(key: ValueKey(asset), onViewReady: initController) : null, - ), - ), - ), - if (showControls) Center(child: CustomVideoPlayerControls(videoId: videoId)), - ], - ); - } -} diff --git a/mobile/lib/pages/common/settings.page.dart b/mobile/lib/pages/common/settings.page.dart index e8f5eb2ee2..65970ee294 100644 --- a/mobile/lib/pages/common/settings.page.dart +++ b/mobile/lib/pages/common/settings.page.dart @@ -2,14 +2,11 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/settings/advanced_settings.dart'; import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_settings.dart'; import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart'; -import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart'; import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart'; import 'package:immich_mobile/widgets/settings/beta_sync_settings/sync_status_and_actions.dart'; import 'package:immich_mobile/widgets/settings/free_up_space_settings.dart'; @@ -38,8 +35,7 @@ enum SettingSection { Widget get widget => switch (this) { SettingSection.advanced => const AdvancedSettings(), SettingSection.assetViewer => const AssetViewerSettings(), - SettingSection.backup => - Store.tryGet(StoreKey.betaTimeline) ?? false ? const DriftBackupSettings() : const BackupSettings(), + SettingSection.backup => const DriftBackupSettings(), SettingSection.freeUpSpace => const FreeUpSpaceSettings(), SettingSection.languages => const LanguageSettings(), SettingSection.networking => const NetworkingSettings(), @@ -74,13 +70,12 @@ class _MobileLayout extends StatelessWidget { .expand( (setting) => setting == SettingSection.beta ? [ - if (Store.isBetaTimelineEnabled) - SettingsCard( - icon: Icons.sync_outlined, - title: 'sync_status'.tr(), - subtitle: 'sync_status_subtitle'.tr(), - settingRoute: const SyncStatusRoute(), - ), + SettingsCard( + icon: Icons.sync_outlined, + title: 'sync_status'.tr(), + subtitle: 'sync_status_subtitle'.tr(), + settingRoute: const SyncStatusRoute(), + ), ] : [ SettingsCard( diff --git a/mobile/lib/pages/common/splash_screen.page.dart b/mobile/lib/pages/common/splash_screen.page.dart index 37c6b95806..725f7f9e85 100644 --- a/mobile/lib/pages/common/splash_screen.page.dart +++ b/mobile/lib/pages/common/splash_screen.page.dart @@ -12,13 +12,9 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart'; import 'package:immich_mobile/generated/translations.g.dart'; -import 'package:path/path.dart' as path; -import 'package:path_provider/path_provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; -import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -27,6 +23,8 @@ import 'package:immich_mobile/theme/theme_data.dart'; import 'package:immich_mobile/widgets/common/immich_logo.dart'; import 'package:immich_mobile/widgets/common/immich_title_text.dart'; import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; import 'package:url_launcher/url_launcher.dart' show launchUrl, LaunchMode; class BootstrapErrorWidget extends StatelessWidget { @@ -323,29 +321,27 @@ class SplashScreenPageState extends ConsumerState { wsProvider.connect(); unawaited(infoProvider.getServerInfo()); - if (Store.isBetaTimelineEnabled) { - bool syncSuccess = false; + bool syncSuccess = false; + await Future.wait([ + backgroundManager.syncLocal(full: true), + backgroundManager.syncRemote().then((success) => syncSuccess = success), + ]); + + if (syncSuccess) { await Future.wait([ - backgroundManager.syncLocal(full: true), - backgroundManager.syncRemote().then((success) => syncSuccess = success), + backgroundManager.hashAssets().then((_) { + _resumeBackup(backupProvider); + }), + _resumeBackup(backupProvider), + // TODO: Bring back when the soft freeze issue is addressed + // backgroundManager.syncCloudIds(), ]); + } else { + await backgroundManager.hashAssets(); + } - if (syncSuccess) { - await Future.wait([ - backgroundManager.hashAssets().then((_) { - _resumeBackup(backupProvider); - }), - _resumeBackup(backupProvider), - // TODO: Bring back when the soft freeze issue is addressed - // backgroundManager.syncCloudIds(), - ]); - } else { - await backgroundManager.hashAssets(); - } - - if (Store.get(StoreKey.syncAlbums, false)) { - await backgroundManager.syncLinkedAlbum(); - } + if (Store.get(StoreKey.syncAlbums, false)) { + await backgroundManager.syncLinkedAlbum(); } } catch (e) { log.severe('Failed establishing connection to the server: $e'); @@ -368,58 +364,7 @@ class SplashScreenPageState extends ConsumerState { // clean install - change the default of the flag // current install not using beta timeline if (context.router.current.name == SplashScreenRoute.name) { - final needBetaMigration = Store.get(StoreKey.needBetaMigration, false); - if (needBetaMigration) { - bool migrate = - (await showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text("New Timeline Experience"), - content: const Text( - "The old timeline has been deprecated and will be removed in an upcoming release. Would you like to switch to the new timeline now?", - ), - actions: [ - TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: const Text("No")), - ElevatedButton(onPressed: () => Navigator.of(ctx).pop(true), child: const Text("Yes")), - ], - ), - )) ?? - false; - if (migrate != true) { - migrate = - (await showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text("Are you sure?"), - content: const Text( - "If you choose to remain on the old timeline, you will be automatically migrated to the new timeline in an upcoming release. Would you like to switch now?", - ), - actions: [ - TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: const Text("No")), - ElevatedButton(onPressed: () => Navigator.of(ctx).pop(true), child: const Text("Yes")), - ], - ), - )) ?? - false; - } - await Store.put(StoreKey.needBetaMigration, false); - if (migrate) { - unawaited(context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: true)])); - return; - } - } - - unawaited(context.replaceRoute(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute())); - } - - if (Store.isBetaTimelineEnabled) { - return; - } - - final hasPermission = await ref.read(galleryPermissionNotifier.notifier).hasPermission; - if (hasPermission) { - // Resume backup (if enable) then navigate - await ref.watch(backupProvider.notifier).resumeBackup(); + unawaited(context.replaceRoute(const TabShellRoute())); } } diff --git a/mobile/lib/pages/common/tab_controller.page.dart b/mobile/lib/pages/common/tab_controller.page.dart deleted file mode 100644 index ef637ba1c8..0000000000 --- a/mobile/lib/pages/common/tab_controller.page.dart +++ /dev/null @@ -1,142 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/search/search_input_focus.provider.dart'; -import 'package:immich_mobile/providers/tab.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; - -@RoutePage() -class TabControllerPage extends HookConsumerWidget { - const TabControllerPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isRefreshingAssets = ref.watch(assetProvider); - final isRefreshingRemoteAlbums = ref.watch(isRefreshingRemoteAlbumProvider); - final isScreenLandscape = MediaQuery.orientationOf(context) == Orientation.landscape; - - Widget buildIcon({required Widget icon, required bool isProcessing}) { - if (!isProcessing) return icon; - return Stack( - alignment: Alignment.center, - clipBehavior: Clip.none, - children: [ - icon, - Positioned( - right: -18, - child: SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(context.primaryColor), - ), - ), - ), - ], - ); - } - - void onNavigationSelected(TabsRouter router, int index) { - // On Photos page menu tapped - if (router.activeIndex == 0 && index == 0) { - scrollToTopNotifierProvider.scrollToTop(); - } - - // On Search page tapped - if (router.activeIndex == 1 && index == 1) { - ref.read(searchInputFocusProvider).requestFocus(); - } - - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - router.setActiveIndex(index); - ref.read(tabProvider.notifier).state = TabEnum.values[index]; - } - - final navigationDestinations = [ - NavigationDestination( - label: 'photos'.tr(), - icon: const Icon(Icons.photo_library_outlined), - selectedIcon: buildIcon( - isProcessing: isRefreshingAssets, - icon: Icon(Icons.photo_library, color: context.primaryColor), - ), - ), - NavigationDestination( - label: 'search'.tr(), - icon: const Icon(Icons.search_rounded), - selectedIcon: Icon(Icons.search, color: context.primaryColor), - ), - NavigationDestination( - label: 'albums'.tr(), - icon: const Icon(Icons.photo_album_outlined), - selectedIcon: buildIcon( - isProcessing: isRefreshingRemoteAlbums, - icon: Icon(Icons.photo_album_rounded, color: context.primaryColor), - ), - ), - NavigationDestination( - label: 'library'.tr(), - icon: const Icon(Icons.space_dashboard_outlined), - selectedIcon: buildIcon( - isProcessing: isRefreshingAssets, - icon: Icon(Icons.space_dashboard_rounded, color: context.primaryColor), - ), - ), - ]; - - Widget bottomNavigationBar(TabsRouter tabsRouter) { - return NavigationBar( - selectedIndex: tabsRouter.activeIndex, - onDestinationSelected: (index) => onNavigationSelected(tabsRouter, index), - destinations: navigationDestinations, - ); - } - - Widget navigationRail(TabsRouter tabsRouter) { - return NavigationRail( - destinations: navigationDestinations - .map((e) => NavigationRailDestination(icon: e.icon, label: Text(e.label), selectedIcon: e.selectedIcon)) - .toList(), - onDestinationSelected: (index) => onNavigationSelected(tabsRouter, index), - selectedIndex: tabsRouter.activeIndex, - labelType: NavigationRailLabelType.all, - groupAlignment: 0.0, - ); - } - - final multiselectEnabled = ref.watch(multiselectProvider); - return AutoTabsRouter( - routes: [const PhotosRoute(), SearchRoute(), const AlbumsRoute(), const LibraryRoute()], - duration: const Duration(milliseconds: 600), - transitionBuilder: (context, child, animation) => FadeTransition(opacity: animation, child: child), - builder: (context, child) { - final tabsRouter = AutoTabsRouter.of(context); - return PopScope( - canPop: tabsRouter.activeIndex == 0, - onPopInvokedWithResult: (didPop, _) => !didPop ? tabsRouter.setActiveIndex(0) : null, - child: Scaffold( - resizeToAvoidBottomInset: false, - body: isScreenLandscape - ? Row( - children: [ - navigationRail(tabsRouter), - const VerticalDivider(), - Expanded(child: child), - ], - ) - : child, - bottomNavigationBar: multiselectEnabled || isScreenLandscape ? null : bottomNavigationBar(tabsRouter), - ), - ); - }, - ); - } -} diff --git a/mobile/lib/pages/editing/crop.page.dart b/mobile/lib/pages/editing/crop.page.dart deleted file mode 100644 index a6a66c1358..0000000000 --- a/mobile/lib/pages/editing/crop.page.dart +++ /dev/null @@ -1,177 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:crop_image/crop_image.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/pages/editing/edit.page.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/hooks/crop_controller_hook.dart'; - -/// A widget for cropping an image. -/// This widget uses [HookWidget] to manage its lifecycle and state. It allows -/// users to crop an image and then navigate to the [EditImagePage] with the -/// cropped image. - -@RoutePage() -class CropImagePage extends HookWidget { - final Image image; - final Asset asset; - const CropImagePage({super.key, required this.image, required this.asset}); - - @override - Widget build(BuildContext context) { - final cropController = useCropController(); - final aspectRatio = useState(null); - - return Scaffold( - appBar: AppBar( - backgroundColor: context.scaffoldBackgroundColor, - title: Text("crop".tr()), - leading: CloseButton(color: context.primaryColor), - actions: [ - IconButton( - icon: Icon(Icons.done_rounded, color: context.primaryColor, size: 24), - onPressed: () async { - final croppedImage = await cropController.croppedImage(); - unawaited(context.pushRoute(EditImageRoute(asset: asset, image: croppedImage, isEdited: true))); - }, - ), - ], - ), - backgroundColor: context.scaffoldBackgroundColor, - body: SafeArea( - child: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return Column( - children: [ - Container( - padding: const EdgeInsets.only(top: 20), - width: constraints.maxWidth * 0.9, - height: constraints.maxHeight * 0.6, - child: CropImage(controller: cropController, image: image, gridColor: Colors.white), - ), - Expanded( - child: Container( - width: double.infinity, - decoration: BoxDecoration( - color: context.scaffoldBackgroundColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only(left: 20, right: 20, bottom: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - icon: Icon(Icons.rotate_left, color: context.themeData.iconTheme.color), - onPressed: () { - cropController.rotateLeft(); - }, - ), - IconButton( - icon: Icon(Icons.rotate_right, color: context.themeData.iconTheme.color), - onPressed: () { - cropController.rotateRight(); - }, - ), - ], - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: null, - label: 'Free', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 1.0, - label: '1:1', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 16.0 / 9.0, - label: '16:9', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 3.0 / 2.0, - label: '3:2', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 7.0 / 5.0, - label: '7:5', - ), - ], - ), - ], - ), - ), - ), - ), - ], - ); - }, - ), - ), - ); - } -} - -class _AspectRatioButton extends StatelessWidget { - final CropController cropController; - final ValueNotifier aspectRatio; - final double? ratio; - final String label; - - const _AspectRatioButton({ - required this.cropController, - required this.aspectRatio, - required this.ratio, - required this.label, - }); - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: Icon(switch (label) { - 'Free' => Icons.crop_free_rounded, - '1:1' => Icons.crop_square_rounded, - '16:9' => Icons.crop_16_9_rounded, - '3:2' => Icons.crop_3_2_rounded, - '7:5' => Icons.crop_7_5_rounded, - _ => Icons.crop_free_rounded, - }, color: aspectRatio.value == ratio ? context.primaryColor : context.themeData.iconTheme.color), - onPressed: () { - cropController.crop = const Rect.fromLTRB(0.1, 0.1, 0.9, 0.9); - aspectRatio.value = ratio; - cropController.aspectRatio = ratio; - }, - ), - Text(label, style: context.textTheme.displayMedium), - ], - ); - } -} diff --git a/mobile/lib/pages/editing/edit.page.dart b/mobile/lib/pages/editing/edit.page.dart deleted file mode 100644 index 2889785d0b..0000000000 --- a/mobile/lib/pages/editing/edit.page.dart +++ /dev/null @@ -1,131 +0,0 @@ -import 'dart:typed_data'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/image_converter.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:path/path.dart' as p; - -/// A stateless widget that provides functionality for editing an image. -/// -/// This widget allows users to edit an image provided either as an [Asset] or -/// directly as an [Image]. It ensures that exactly one of these is provided. -/// -/// It also includes a conversion method to convert an [Image] to a [Uint8List] to save the image on the user's phone -/// They automatically navigate to the [HomePage] with the edited image saved and they eventually get backed up to the server. -@immutable -@RoutePage() -class EditImagePage extends ConsumerWidget { - final Asset asset; - final Image image; - final bool isEdited; - - const EditImagePage({super.key, required this.asset, required this.image, required this.isEdited}); - - Future _saveEditedImage(BuildContext context, Asset asset, Image image, WidgetRef ref) async { - try { - final Uint8List imageData = await imageToUint8List(image); - await ref - .read(fileMediaRepositoryProvider) - .saveImage(imageData, title: "${p.withoutExtension(asset.fileName)}_edited.jpg"); - await ref.read(albumProvider.notifier).refreshDeviceAlbums(); - context.navigator.popUntil((route) => route.isFirst); - ImmichToast.show(durationInSecond: 3, context: context, msg: 'Image Saved!', gravity: ToastGravity.CENTER); - } catch (e) { - ImmichToast.show( - durationInSecond: 6, - context: context, - msg: "error_saving_image".tr(namedArgs: {'error': e.toString()}), - gravity: ToastGravity.CENTER, - ); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Scaffold( - appBar: AppBar( - title: Text("edit".tr()), - backgroundColor: context.scaffoldBackgroundColor, - leading: IconButton( - icon: Icon(Icons.close_rounded, color: context.primaryColor, size: 24), - onPressed: () => context.navigator.popUntil((route) => route.isFirst), - ), - actions: [ - TextButton( - onPressed: isEdited ? () => _saveEditedImage(context, asset, image, ref) : null, - child: Text("save_to_gallery".tr(), style: TextStyle(color: isEdited ? context.primaryColor : Colors.grey)), - ), - ], - ), - backgroundColor: context.scaffoldBackgroundColor, - body: Center( - child: ConstrainedBox( - constraints: BoxConstraints(maxHeight: context.height * 0.7, maxWidth: context.width * 0.9), - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(7)), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.2), - spreadRadius: 2, - blurRadius: 10, - offset: const Offset(0, 3), - ), - ], - ), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(7)), - child: Image(image: image.image, fit: BoxFit.contain), - ), - ), - ), - ), - bottomNavigationBar: Container( - height: 70, - margin: const EdgeInsets.only(bottom: 60, right: 10, left: 10, top: 10), - decoration: BoxDecoration( - color: context.scaffoldBackgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(30)), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: Icon(Icons.crop_rotate_rounded, color: context.themeData.iconTheme.color, size: 25), - onPressed: () { - context.pushRoute(CropImageRoute(asset: asset, image: image)); - }, - ), - Text("crop".tr(), style: context.textTheme.displayMedium), - ], - ), - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: Icon(Icons.filter, color: context.themeData.iconTheme.color, size: 25), - onPressed: () { - context.pushRoute(FilterImageRoute(asset: asset, image: image)); - }, - ), - Text("filter".tr(), style: context.textTheme.displayMedium), - ], - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/pages/editing/filter.page.dart b/mobile/lib/pages/editing/filter.page.dart deleted file mode 100644 index f8b144bb96..0000000000 --- a/mobile/lib/pages/editing/filter.page.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'dart:async'; -import 'dart:ui' as ui; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/constants/filters.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/routing/router.dart'; - -/// A widget for filtering an image. -/// This widget uses [HookWidget] to manage its lifecycle and state. It allows -/// users to add filters to an image and then navigate to the [EditImagePage] with the -/// final composition.' -@RoutePage() -class FilterImagePage extends HookWidget { - final Image image; - final Asset asset; - - const FilterImagePage({super.key, required this.image, required this.asset}); - - @override - Widget build(BuildContext context) { - final colorFilter = useState(filters[0]); - final selectedFilterIndex = useState(0); - - Future createFilteredImage(ui.Image inputImage, ColorFilter filter) { - final completer = Completer(); - final size = Size(inputImage.width.toDouble(), inputImage.height.toDouble()); - final recorder = ui.PictureRecorder(); - final canvas = Canvas(recorder); - - final paint = Paint()..colorFilter = filter; - canvas.drawImage(inputImage, Offset.zero, paint); - - recorder.endRecording().toImage(size.width.round(), size.height.round()).then((image) { - completer.complete(image); - }); - - return completer.future; - } - - void applyFilter(ColorFilter filter, int index) { - colorFilter.value = filter; - selectedFilterIndex.value = index; - } - - Future applyFilterAndConvert(ColorFilter filter) async { - final completer = Completer(); - image.image - .resolve(ImageConfiguration.empty) - .addListener( - ImageStreamListener((ImageInfo info, bool _) { - completer.complete(info.image); - }), - ); - final uiImage = await completer.future; - - final filteredUiImage = await createFilteredImage(uiImage, filter); - final byteData = await filteredUiImage.toByteData(format: ui.ImageByteFormat.png); - final pngBytes = byteData!.buffer.asUint8List(); - - return Image.memory(pngBytes, fit: BoxFit.contain); - } - - return Scaffold( - appBar: AppBar( - backgroundColor: context.scaffoldBackgroundColor, - title: Text("filter".tr()), - leading: CloseButton(color: context.primaryColor), - actions: [ - IconButton( - icon: Icon(Icons.done_rounded, color: context.primaryColor, size: 24), - onPressed: () async { - final filteredImage = await applyFilterAndConvert(colorFilter.value); - unawaited(context.pushRoute(EditImageRoute(asset: asset, image: filteredImage, isEdited: true))); - }, - ), - ], - ), - backgroundColor: context.scaffoldBackgroundColor, - body: Column( - children: [ - SizedBox( - height: context.height * 0.7, - child: Center( - child: ColorFiltered(colorFilter: colorFilter.value, child: image), - ), - ), - SizedBox( - height: 120, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: filters.length, - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: _FilterButton( - image: image, - label: filterNames[index], - filter: filters[index], - isSelected: selectedFilterIndex.value == index, - onTap: () => applyFilter(filters[index], index), - ), - ); - }, - ), - ), - ], - ), - ); - } -} - -class _FilterButton extends StatelessWidget { - final Image image; - final String label; - final ColorFilter filter; - final bool isSelected; - final VoidCallback onTap; - - const _FilterButton({ - required this.image, - required this.label, - required this.filter, - required this.isSelected, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - GestureDetector( - onTap: onTap, - child: Container( - width: 80, - height: 80, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(10)), - border: isSelected ? Border.all(color: context.primaryColor, width: 3) : null, - ), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(10)), - child: ColorFiltered( - colorFilter: filter, - child: FittedBox(fit: BoxFit.cover, child: image), - ), - ), - ), - ), - const SizedBox(height: 10), - Text(label, style: context.themeData.textTheme.bodyMedium), - ], - ); - } -} diff --git a/mobile/lib/pages/library/archive.page.dart b/mobile/lib/pages/library/archive.page.dart deleted file mode 100644 index 8ca1bb9752..0000000000 --- a/mobile/lib/pages/library/archive.page.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; - -@RoutePage() -class ArchivePage extends HookConsumerWidget { - const ArchivePage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - AppBar buildAppBar() { - final archiveRenderList = ref.watch(archiveTimelineProvider); - final count = archiveRenderList.value?.totalAssets.toString() ?? "?"; - return AppBar( - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - centerTitle: true, - automaticallyImplyLeading: false, - title: const Text('archive_page_title').tr(namedArgs: {'count': count}), - ); - } - - return Scaffold( - appBar: ref.watch(multiselectProvider) ? null : buildAppBar(), - body: MultiselectGrid( - renderListProvider: archiveTimelineProvider, - unarchive: true, - archiveEnabled: true, - deleteEnabled: true, - editEnabled: true, - ), - ); - } -} diff --git a/mobile/lib/pages/library/favorite.page.dart b/mobile/lib/pages/library/favorite.page.dart deleted file mode 100644 index 649d7727d5..0000000000 --- a/mobile/lib/pages/library/favorite.page.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; - -@RoutePage() -class FavoritesPage extends HookConsumerWidget { - const FavoritesPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - AppBar buildAppBar() { - return AppBar( - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - centerTitle: true, - automaticallyImplyLeading: false, - title: const Text('favorites').tr(), - ); - } - - return Scaffold( - appBar: ref.watch(multiselectProvider) ? null : buildAppBar(), - body: MultiselectGrid( - renderListProvider: favoriteTimelineProvider, - favoriteEnabled: true, - editEnabled: true, - unfavorite: true, - ), - ); - } -} diff --git a/mobile/lib/pages/library/library.page.dart b/mobile/lib/pages/library/library.page.dart deleted file mode 100644 index 99a534e9cf..0000000000 --- a/mobile/lib/pages/library/library.page.dart +++ /dev/null @@ -1,383 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/generated/translations.g.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/partner.provider.dart'; -import 'package:immich_mobile/providers/search/people.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; -import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; -import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; -import 'package:immich_mobile/widgets/common/user_avatar.dart'; -import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; - -@RoutePage() -class LibraryPage extends ConsumerWidget { - const LibraryPage({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - context.locale; - final trashEnabled = ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); - - return Scaffold( - appBar: const ImmichAppBar(), - body: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: ListView( - shrinkWrap: true, - children: [ - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Row( - children: [ - ActionButton( - onPressed: () => context.pushRoute(const FavoritesRoute()), - icon: Icons.favorite_outline_rounded, - label: context.t.favorites, - ), - const SizedBox(width: 8), - ActionButton( - onPressed: () => context.pushRoute(const ArchiveRoute()), - icon: Icons.archive_outlined, - label: context.t.archived, - ), - ], - ), - ), - const SizedBox(height: 8), - Row( - children: [ - ActionButton( - onPressed: () => context.pushRoute(const SharedLinkRoute()), - icon: Icons.link_outlined, - label: context.t.shared_links, - ), - SizedBox(width: trashEnabled ? 8 : 0), - trashEnabled - ? ActionButton( - onPressed: () => context.pushRoute(const TrashRoute()), - icon: Icons.delete_outline_rounded, - label: context.t.trash, - ) - : const SizedBox.shrink(), - ], - ), - const SizedBox(height: 12), - const Wrap( - spacing: 8, - runSpacing: 8, - children: [PeopleCollectionCard(), PlacesCollectionCard(), LocalAlbumsCollectionCard()], - ), - const SizedBox(height: 12), - const QuickAccessButtons(), - const SizedBox(height: 32), - ], - ), - ), - ); - } -} - -class QuickAccessButtons extends ConsumerWidget { - const QuickAccessButtons({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final partners = ref.watch(partnerSharedWithProvider); - - return Container( - decoration: BoxDecoration( - border: Border.all(color: context.colorScheme.onSurface.withAlpha(10), width: 1), - borderRadius: const BorderRadius.all(Radius.circular(20)), - gradient: LinearGradient( - colors: [ - context.colorScheme.primary.withAlpha(10), - context.colorScheme.primary.withAlpha(15), - context.colorScheme.primary.withAlpha(20), - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - child: ListView( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - children: [ - ListTile( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: const Radius.circular(20), - topRight: const Radius.circular(20), - bottomLeft: Radius.circular(partners.isEmpty ? 20 : 0), - bottomRight: Radius.circular(partners.isEmpty ? 20 : 0), - ), - ), - leading: const Icon(Icons.folder_outlined, size: 26), - title: Text(context.t.folders, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500)), - onTap: () => context.pushRoute(FolderRoute()), - ), - ListTile( - leading: const Icon(Icons.lock_outline_rounded, size: 26), - title: Text( - context.t.locked_folder, - style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500), - ), - onTap: () => context.pushRoute(const LockedRoute()), - ), - ListTile( - leading: const Icon(Icons.group_outlined, size: 26), - title: Text(context.t.partners, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500)), - onTap: () => context.pushRoute(const PartnerRoute()), - ), - PartnerList(partners: partners), - ], - ), - ); - } -} - -class PartnerList extends ConsumerWidget { - const PartnerList({super.key, required this.partners}); - - final List partners; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return ListView.builder( - physics: const NeverScrollableScrollPhysics(), - itemCount: partners.length, - shrinkWrap: true, - itemBuilder: (context, index) { - final partner = partners[index]; - final isLastItem = index == partners.length - 1; - return ListTile( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(isLastItem ? 20 : 0), - bottomRight: Radius.circular(isLastItem ? 20 : 0), - ), - ), - contentPadding: const EdgeInsets.only(left: 12.0, right: 18.0), - leading: userAvatar(context, partner, radius: 16), - title: const Text( - "partner_list_user_photos", - style: TextStyle(fontWeight: FontWeight.w500), - ).tr(namedArgs: {'user': partner.name}), - onTap: () => context.pushRoute((PartnerDetailRoute(partner: partner))), - ); - }, - ); - } -} - -class PeopleCollectionCard extends ConsumerWidget { - const PeopleCollectionCard({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final people = ref.watch(getAllPeopleProvider); - return LayoutBuilder( - builder: (context, constraints) { - final isTablet = constraints.maxWidth > 600; - final widthFactor = isTablet ? 0.25 : 0.5; - final size = context.width * widthFactor - 20.0; - - return GestureDetector( - onTap: () => context.pushRoute(const PeopleCollectionRoute()), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: size, - width: size, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(20)), - gradient: LinearGradient( - colors: [context.colorScheme.primary.withAlpha(30), context.colorScheme.primary.withAlpha(25)], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - child: people.widgetWhen( - onLoading: () => const Center(child: CircularProgressIndicator()), - onData: (people) { - return GridView.count( - crossAxisCount: 2, - padding: const EdgeInsets.all(12), - crossAxisSpacing: 8, - mainAxisSpacing: 8, - physics: const NeverScrollableScrollPhysics(), - children: people.take(4).map((person) { - return CircleAvatar(backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(person.id))); - }).toList(), - ); - }, - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - context.t.people, - style: context.textTheme.titleSmall?.copyWith( - color: context.colorScheme.onSurface, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - ); - }, - ); - } -} - -class LocalAlbumsCollectionCard extends HookConsumerWidget { - const LocalAlbumsCollectionCard({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albums = ref.watch(localAlbumsProvider); - - return LayoutBuilder( - builder: (context, constraints) { - final isTablet = constraints.maxWidth > 600; - final widthFactor = isTablet ? 0.25 : 0.5; - final size = context.width * widthFactor - 20.0; - - return GestureDetector( - onTap: () => context.pushRoute(const LocalAlbumsRoute()), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: size, - width: size, - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(20)), - gradient: LinearGradient( - colors: [context.colorScheme.primary.withAlpha(30), context.colorScheme.primary.withAlpha(25)], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - child: GridView.count( - crossAxisCount: 2, - padding: const EdgeInsets.all(12), - crossAxisSpacing: 8, - mainAxisSpacing: 8, - physics: const NeverScrollableScrollPhysics(), - children: albums.take(4).map((album) { - return AlbumThumbnailCard(album: album, showTitle: false); - }).toList(), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - context.t.on_this_device, - style: context.textTheme.titleSmall?.copyWith( - color: context.colorScheme.onSurface, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - ); - }, - ); - } -} - -class PlacesCollectionCard extends StatelessWidget { - const PlacesCollectionCard({super.key}); - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - final isTablet = constraints.maxWidth > 600; - final widthFactor = isTablet ? 0.25 : 0.5; - final size = context.width * widthFactor - 20.0; - - return GestureDetector( - onTap: () => context.pushRoute(PlacesCollectionRoute(currentLocation: null)), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: size, - width: size, - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(20)), - color: context.colorScheme.secondaryContainer.withAlpha(100), - ), - child: IgnorePointer( - child: MapThumbnail( - zoom: 8, - centre: const LatLng(21.44950, -157.91959), - showAttribution: false, - themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light, - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - context.t.places, - style: context.textTheme.titleSmall?.copyWith( - color: context.colorScheme.onSurface, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - ); - }, - ); - } -} - -class ActionButton extends StatelessWidget { - final VoidCallback onPressed; - final IconData icon; - final String label; - - const ActionButton({super.key, required this.onPressed, required this.icon, required this.label}); - - @override - Widget build(BuildContext context) { - return Expanded( - child: FilledButton.icon( - onPressed: onPressed, - label: Padding( - padding: const EdgeInsets.only(left: 4.0), - child: Text(label, style: TextStyle(color: context.colorScheme.onSurface, fontSize: 15)), - ), - style: FilledButton.styleFrom( - elevation: 0, - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - backgroundColor: context.colorScheme.surfaceContainerLow, - alignment: Alignment.centerLeft, - shape: RoundedRectangleBorder( - borderRadius: const BorderRadius.all(Radius.circular(25)), - side: BorderSide(color: context.colorScheme.onSurface.withAlpha(10), width: 1), - ), - ), - icon: Icon(icon, color: context.primaryColor), - ), - ); - } -} diff --git a/mobile/lib/pages/library/local_albums.page.dart b/mobile/lib/pages/library/local_albums.page.dart deleted file mode 100644 index e52a8326df..0000000000 --- a/mobile/lib/pages/library/local_albums.page.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/pages/common/large_leading_tile.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; - -@RoutePage() -class LocalAlbumsPage extends HookConsumerWidget { - const LocalAlbumsPage({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final albums = ref.watch(localAlbumsProvider); - - return Scaffold( - appBar: AppBar(title: Text('on_this_device'.tr())), - body: ListView.builder( - padding: const EdgeInsets.all(18.0), - itemCount: albums.length, - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: LargeLeadingTile( - leadingPadding: const EdgeInsets.only(right: 16), - leading: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(15)), - child: ImmichThumbnail(asset: albums[index].thumbnail.value, width: 80, height: 80), - ), - title: Text( - albums[index].name, - style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), - ), - subtitle: Text( - 'items_count'.t(context: context, args: {'count': albums[index].assetCount}), - style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ), - onTap: () => context.pushRoute(AlbumViewerRoute(albumId: albums[index].id)), - ), - ); - }, - ), - ); - } -} diff --git a/mobile/lib/pages/library/locked/locked.page.dart b/mobile/lib/pages/library/locked/locked.page.dart deleted file mode 100644 index aea62e0051..0000000000 --- a/mobile/lib/pages/library/locked/locked.page.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; - -@RoutePage() -class LockedPage extends HookConsumerWidget { - const LockedPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final appLifeCycle = useAppLifecycleState(); - final showOverlay = useState(false); - final authProviderNotifier = ref.read(authProvider.notifier); - // lock the page when it is destroyed - useEffect(() { - return () { - authProviderNotifier.lockPinCode(); - }; - }, []); - - useEffect(() { - if (context.mounted) { - if (appLifeCycle == AppLifecycleState.resumed) { - showOverlay.value = false; - } else { - showOverlay.value = true; - } - } - - return null; - }, [appLifeCycle]); - - return Scaffold( - appBar: ref.watch(multiselectProvider) ? null : const LockPageAppBar(), - body: showOverlay.value - ? const SizedBox() - : MultiselectGrid( - renderListProvider: lockedTimelineProvider, - topWidget: Padding( - padding: const EdgeInsets.all(16.0), - child: Center(child: Text('no_locked_photos_message'.tr(), style: context.textTheme.labelLarge)), - ), - editEnabled: false, - favoriteEnabled: false, - unfavorite: false, - archiveEnabled: false, - stackEnabled: false, - unarchive: false, - ), - ); - } -} - -class LockPageAppBar extends ConsumerWidget implements PreferredSizeWidget { - const LockPageAppBar({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return AppBar( - leading: IconButton( - onPressed: () { - ref.read(authProvider.notifier).lockPinCode(); - context.maybePop(); - }, - icon: const Icon(Icons.arrow_back_ios_rounded), - ), - centerTitle: true, - automaticallyImplyLeading: false, - title: const Text('locked_folder').tr(), - ); - } - - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); -} diff --git a/mobile/lib/pages/library/locked/pin_auth.page.dart b/mobile/lib/pages/library/locked/pin_auth.page.dart index a39c91871b..3af320dc5f 100644 --- a/mobile/lib/pages/library/locked/pin_auth.page.dart +++ b/mobile/lib/pages/library/locked/pin_auth.page.dart @@ -5,7 +5,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' show useState; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/local_auth.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -22,7 +21,6 @@ class PinAuthPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final localAuthState = ref.watch(localAuthProvider); final showPinRegistrationForm = useState(createPinCode); - final isBetaTimeline = Store.isBetaTimelineEnabled; Future registerBiometric(String pinCode) async { final isRegistered = await ref.read(localAuthProvider.notifier).registerBiometric(context, pinCode); @@ -36,11 +34,7 @@ class PinAuthPage extends HookConsumerWidget { ), ); - if (isBetaTimeline) { - unawaited(context.replaceRoute(const DriftLockedFolderRoute())); - } else { - unawaited(context.replaceRoute(const LockedRoute())); - } + unawaited(context.replaceRoute(const DriftLockedFolderRoute())); } } @@ -89,11 +83,7 @@ class PinAuthPage extends HookConsumerWidget { child: PinVerificationForm( autoFocus: true, onSuccess: (_) { - if (isBetaTimeline) { - context.replaceRoute(const DriftLockedFolderRoute()); - } else { - context.replaceRoute(const LockedRoute()); - } + context.replaceRoute(const DriftLockedFolderRoute()); }, ), ), diff --git a/mobile/lib/pages/library/partner/partner.page.dart b/mobile/lib/pages/library/partner/partner.page.dart deleted file mode 100644 index eae4228a2d..0000000000 --- a/mobile/lib/pages/library/partner/partner.page.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/partner.provider.dart'; -import 'package:immich_mobile/services/partner.service.dart'; -import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/widgets/common/user_avatar.dart'; - -@RoutePage() -class PartnerPage extends HookConsumerWidget { - const PartnerPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final List partners = ref.watch(partnerSharedByProvider); - final availableUsers = ref.watch(partnerAvailableProvider); - - addNewUsersHandler() async { - final users = availableUsers.value; - if (users == null || users.isEmpty) { - ImmichToast.show(context: context, msg: "partner_page_no_more_users".tr()); - return; - } - - final selectedUser = await showDialog( - context: context, - builder: (context) { - return SimpleDialog( - title: const Text("partner_page_select_partner").tr(), - children: [ - for (UserDto u in users) - SimpleDialogOption( - onPressed: () => context.pop(u), - child: Row( - children: [ - Padding(padding: const EdgeInsets.only(right: 8), child: userAvatar(context, u)), - Text(u.name), - ], - ), - ), - ], - ); - }, - ); - if (selectedUser != null) { - final ok = await ref.read(partnerServiceProvider).addPartner(selectedUser); - if (ok) { - ref.invalidate(partnerSharedByProvider); - } else { - ImmichToast.show(context: context, msg: "partner_page_partner_add_failed".tr(), toastType: ToastType.error); - } - } - } - - onDeleteUser(UserDto u) { - return showDialog( - context: context, - builder: (BuildContext context) { - return ConfirmDialog( - title: "stop_photo_sharing", - content: "partner_page_stop_sharing_content".tr(namedArgs: {'partner': u.name}), - onOk: () => ref.read(partnerServiceProvider).removePartner(u), - ); - }, - ); - } - - buildUserList(List users) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(left: 16.0, top: 16.0), - child: Text( - "partner_page_shared_to_title", - style: context.textTheme.titleSmall?.copyWith(color: context.colorScheme.onSurface.withAlpha(200)), - ).tr(), - ), - if (users.isNotEmpty) - ListView.builder( - shrinkWrap: true, - itemCount: users.length, - itemBuilder: ((context, index) { - return ListTile( - leading: userAvatar(context, users[index]), - title: Text(users[index].email, style: context.textTheme.bodyLarge), - trailing: IconButton( - icon: const Icon(Icons.person_remove), - onPressed: () => onDeleteUser(users[index]), - ), - ); - }), - ), - if (users.isEmpty) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: const Text("partner_page_empty_message", style: TextStyle(fontSize: 14)).tr(), - ), - Align( - alignment: Alignment.center, - child: ElevatedButton.icon( - onPressed: availableUsers.whenOrNull(data: (data) => addNewUsersHandler), - icon: const Icon(Icons.person_add), - label: const Text("add_partner").tr(), - ), - ), - ], - ), - ), - ], - ); - } - - return Scaffold( - appBar: AppBar( - title: const Text("partners").tr(), - elevation: 0, - centerTitle: false, - actions: [ - IconButton( - onPressed: availableUsers.whenOrNull(data: (data) => addNewUsersHandler), - icon: const Icon(Icons.person_add), - tooltip: "add_partner".tr(), - ), - ], - ), - body: buildUserList(partners), - ); - } -} diff --git a/mobile/lib/pages/library/partner/partner_detail.page.dart b/mobile/lib/pages/library/partner/partner_detail.page.dart deleted file mode 100644 index 1f15dab6a3..0000000000 --- a/mobile/lib/pages/library/partner/partner_detail.page.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/partner.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -@RoutePage() -class PartnerDetailPage extends HookConsumerWidget { - const PartnerDetailPage({super.key, required this.partner}); - - final UserDto partner; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final inTimeline = useState(partner.inTimeline); - bool toggleInProcess = false; - - useEffect(() { - Future.microtask(() async => {await ref.read(assetProvider.notifier).getAllAsset()}); - return null; - }, []); - - void toggleInTimeline() async { - if (toggleInProcess) return; - toggleInProcess = true; - try { - final ok = await ref - .read(partnerSharedWithProvider.notifier) - .updatePartner(partner, inTimeline: !inTimeline.value); - if (ok) { - inTimeline.value = !inTimeline.value; - final action = inTimeline.value ? "shown on" : "hidden from"; - ImmichToast.show( - context: context, - toastType: ToastType.success, - durationInSecond: 1, - msg: "${partner.name}'s assets $action your timeline", - ); - } else { - ImmichToast.show( - context: context, - toastType: ToastType.error, - durationInSecond: 1, - msg: "Failed to toggle the timeline setting", - ); - } - } finally { - toggleInProcess = false; - } - } - - return Scaffold( - appBar: ref.watch(multiselectProvider) - ? null - : AppBar(title: Text(partner.name), elevation: 0, centerTitle: false), - body: MultiselectGrid( - topWidget: Padding( - padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 16.0), - child: Container( - decoration: BoxDecoration( - border: Border.all(color: context.colorScheme.onSurface.withAlpha(10), width: 1), - borderRadius: const BorderRadius.all(Radius.circular(20)), - gradient: LinearGradient( - colors: [context.colorScheme.primary.withAlpha(10), context.colorScheme.primary.withAlpha(15)], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: ListTile( - title: Text( - "Show in timeline", - style: context.textTheme.titleSmall?.copyWith(color: context.colorScheme.primary), - ), - subtitle: Text( - "Show photos and videos from this user in your timeline", - style: context.textTheme.bodyMedium, - ), - trailing: Switch(value: inTimeline.value, onChanged: (_) => toggleInTimeline()), - ), - ), - ), - ), - renderListProvider: singleUserTimelineProvider(partner.id), - onRefresh: () => ref.read(assetProvider.notifier).getAllAsset(), - deleteEnabled: false, - favoriteEnabled: false, - ), - ); - } -} diff --git a/mobile/lib/pages/library/people/people_collection.page.dart b/mobile/lib/pages/library/people/people_collection.page.dart deleted file mode 100644 index bff52df6da..0000000000 --- a/mobile/lib/pages/library/people/people_collection.page.dart +++ /dev/null @@ -1,127 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/search/people.provider.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; -import 'package:immich_mobile/widgets/common/search_field.dart'; -import 'package:immich_mobile/widgets/search/person_name_edit_form.dart'; - -@RoutePage() -class PeopleCollectionPage extends HookConsumerWidget { - const PeopleCollectionPage({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final people = ref.watch(getAllPeopleProvider); - final formFocus = useFocusNode(); - final ValueNotifier search = useState(null); - - showNameEditModel(String personId, String personName) { - return showDialog( - context: context, - useRootNavigator: false, - builder: (BuildContext context) { - return PersonNameEditForm(personId: personId, personName: personName); - }, - ); - } - - return LayoutBuilder( - builder: (context, constraints) { - final isTablet = constraints.maxWidth > 600; - final isPortrait = context.orientation == Orientation.portrait; - - return Scaffold( - appBar: AppBar( - automaticallyImplyLeading: search.value == null, - title: search.value != null - ? SearchField( - focusNode: formFocus, - onTapOutside: (_) => formFocus.unfocus(), - onChanged: (value) => search.value = value, - filled: true, - hintText: 'filter_people'.tr(), - autofocus: true, - ) - : Text('people'.tr()), - actions: [ - IconButton( - icon: Icon(search.value != null ? Icons.close : Icons.search), - onPressed: () { - search.value = search.value == null ? '' : null; - }, - ), - ], - ), - body: SafeArea( - child: people.when( - data: (people) { - if (search.value != null) { - people = people.where((person) { - return person.name.toLowerCase().contains(search.value!.toLowerCase()); - }).toList(); - } - return GridView.builder( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: isTablet ? 6 : 3, - childAspectRatio: 0.85, - mainAxisSpacing: isPortrait && isTablet ? 36 : 0, - ), - padding: const EdgeInsets.symmetric(vertical: 32), - itemCount: people.length, - itemBuilder: (context, index) { - final person = people[index]; - - return Column( - children: [ - GestureDetector( - onTap: () { - context.pushRoute(PersonResultRoute(personId: person.id, personName: person.name)); - }, - child: Material( - shape: const CircleBorder(side: BorderSide.none), - elevation: 3, - child: CircleAvatar( - maxRadius: isTablet ? 120 / 2 : 96 / 2, - backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(person.id)), - ), - ), - ), - const SizedBox(height: 12), - GestureDetector( - onTap: () => showNameEditModel(person.id, person.name), - child: person.name.isEmpty - ? Text( - 'add_a_name'.tr(), - style: context.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w500, - color: context.colorScheme.primary, - ), - ) - : Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Text( - person.name, - overflow: TextOverflow.ellipsis, - style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500), - ), - ), - ), - ], - ); - }, - ); - }, - error: (error, stack) => const Text("error"), - loading: () => const Center(child: CircularProgressIndicator()), - ), - ), - ); - }, - ); - } -} diff --git a/mobile/lib/pages/library/places/places_collection.page.dart b/mobile/lib/pages/library/places/places_collection.page.dart deleted file mode 100644 index a4a6f66915..0000000000 --- a/mobile/lib/pages/library/places/places_collection.page.dart +++ /dev/null @@ -1,136 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' hide Store; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/search/search_filter.model.dart'; -import 'package:immich_mobile/pages/common/large_leading_tile.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; -import 'package:immich_mobile/providers/search/search_page_state.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/search_field.dart'; -import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; - -@RoutePage() -class PlacesCollectionPage extends HookConsumerWidget { - const PlacesCollectionPage({super.key, this.currentLocation}); - final LatLng? currentLocation; - @override - Widget build(BuildContext context, WidgetRef ref) { - final places = ref.watch(getAllPlacesProvider); - final formFocus = useFocusNode(); - final ValueNotifier search = useState(null); - - return Scaffold( - appBar: AppBar( - automaticallyImplyLeading: search.value == null, - title: search.value != null - ? SearchField( - autofocus: true, - filled: true, - focusNode: formFocus, - onChanged: (value) => search.value = value, - onTapOutside: (_) => formFocus.unfocus(), - hintText: 'filter_places'.tr(), - ) - : Text('places'.tr()), - actions: [ - IconButton( - icon: Icon(search.value != null ? Icons.close : Icons.search), - onPressed: () { - search.value = search.value == null ? '' : null; - }, - ), - ], - ), - body: ListView( - shrinkWrap: true, - children: [ - if (search.value == null) - Padding( - padding: const EdgeInsets.all(16.0), - child: SizedBox( - height: 200, - width: context.width, - child: MapThumbnail( - onTap: (_, __) => context.pushRoute(MapRoute(initialLocation: currentLocation)), - zoom: 8, - centre: currentLocation ?? const LatLng(21.44950, -157.91959), - showAttribution: false, - themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light, - ), - ), - ), - places.when( - data: (places) { - if (search.value != null) { - places = places.where((place) { - return place.label.toLowerCase().contains(search.value!.toLowerCase()); - }).toList(); - } - return ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: places.length, - itemBuilder: (context, index) { - final place = places[index]; - - return PlaceTile(id: place.id, name: place.label); - }, - ); - }, - error: (error, stask) => Text('error_getting_places'.tr()), - loading: () => const Center(child: CircularProgressIndicator()), - ), - ], - ), - ); - } -} - -class PlaceTile extends StatelessWidget { - const PlaceTile({super.key, required this.id, required this.name}); - - final String id; - final String name; - - @override - Widget build(BuildContext context) { - final thumbnailUrl = '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail'; - - void navigateToPlace() { - context.pushRoute( - SearchRoute( - prefilter: SearchFilter( - people: {}, - location: SearchLocationFilter(city: name), - camera: SearchCameraFilter(), - date: SearchDateFilter(), - display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), - rating: SearchRatingFilter(), - mediaType: AssetType.other, - ), - ), - ); - } - - return LargeLeadingTile( - onTap: () => navigateToPlace(), - title: Text(name, style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500)), - leading: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(20)), - child: SizedBox( - width: 80, - height: 80, - child: Thumbnail(imageProvider: RemoteImageProvider(url: thumbnailUrl)), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/library/trash.page.dart b/mobile/lib/pages/library/trash.page.dart deleted file mode 100644 index 2279998c2d..0000000000 --- a/mobile/lib/pages/library/trash.page.dart +++ /dev/null @@ -1,225 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/providers/trash.provider.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; -import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -@RoutePage() -class TrashPage extends HookConsumerWidget { - const TrashPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final trashRenderList = ref.watch(trashTimelineProvider); - final trashDays = ref.watch(serverInfoProvider.select((v) => v.serverConfig.trashDays)); - final selectionEnabledHook = useState(false); - final selection = useState({}); - final processing = useProcessingOverlay(); - - void selectionListener(bool multiselect, Set selectedAssets) { - selectionEnabledHook.value = multiselect; - selection.value = selectedAssets; - } - - onEmptyTrash() async { - processing.value = true; - await ref.read(trashProvider.notifier).emptyTrash(); - processing.value = false; - selectionEnabledHook.value = false; - if (context.mounted) { - ImmichToast.show(context: context, msg: 'trash_emptied'.tr(), gravity: ToastGravity.BOTTOM); - } - } - - handleEmptyTrash() async { - await showDialog( - context: context, - builder: (context) => ConfirmDialog( - onOk: () => onEmptyTrash(), - title: "empty_trash".tr(), - ok: "ok".tr(), - content: "trash_page_empty_trash_dialog_content".tr(), - ), - ); - } - - Future onPermanentlyDelete() async { - processing.value = true; - try { - if (selection.value.isNotEmpty) { - final isRemoved = await ref.read(assetProvider.notifier).deleteAssets(selection.value, force: true); - - if (isRemoved) { - if (context.mounted) { - ImmichToast.show( - context: context, - msg: 'assets_deleted_permanently'.tr(namedArgs: {'count': "${selection.value.length}"}), - gravity: ToastGravity.BOTTOM, - ); - } - } - } - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - handlePermanentDelete() async { - await showDialog( - context: context, - builder: (context) => DeleteDialog(alert: "delete_dialog_alert_remote", onDelete: () => onPermanentlyDelete()), - ); - } - - Future handleRestoreAll() async { - processing.value = true; - await ref.read(trashProvider.notifier).restoreTrash(); - processing.value = false; - selectionEnabledHook.value = false; - } - - Future handleRestore() async { - processing.value = true; - try { - if (selection.value.isNotEmpty) { - final result = await ref.read(trashProvider.notifier).restoreAssets(selection.value); - - if (result && context.mounted) { - ImmichToast.show( - context: context, - msg: 'assets_restored_successfully'.tr(namedArgs: {'count': "${selection.value.length}"}), - gravity: ToastGravity.BOTTOM, - ); - } - } - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - String getAppBarTitle(String count) { - if (selectionEnabledHook.value) { - return selection.value.isNotEmpty ? "${selection.value.length}" : "trash_page_select_assets_btn".tr(); - } - return 'trash_page_title'.tr(namedArgs: {'count': count}); - } - - AppBar buildAppBar(String count) { - return AppBar( - leading: IconButton( - onPressed: !selectionEnabledHook.value - ? () => context.maybePop() - : () { - selectionEnabledHook.value = false; - selection.value = {}; - }, - icon: !selectionEnabledHook.value - ? const Icon(Icons.arrow_back_ios_rounded) - : const Icon(Icons.close_rounded), - ), - centerTitle: !selectionEnabledHook.value, - automaticallyImplyLeading: false, - title: Text(getAppBarTitle(count)), - actions: [ - if (!selectionEnabledHook.value) - PopupMenuButton( - itemBuilder: (context) { - return [ - PopupMenuItem(value: () => selectionEnabledHook.value = true, child: const Text('select').tr()), - PopupMenuItem(value: handleEmptyTrash, child: const Text('empty_trash').tr()), - ]; - }, - onSelected: (fn) => fn(), - ), - ], - ); - } - - Widget buildBottomBar() { - return SafeArea( - child: Align( - alignment: Alignment.bottomCenter, - child: SizedBox( - height: 64, - child: Container( - color: context.themeData.canvasColor, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - TextButton.icon( - icon: Icon(Icons.delete_forever, color: Colors.red[400]), - label: Text( - selection.value.isEmpty ? 'trash_page_delete_all'.tr() : 'delete'.tr(), - style: TextStyle(fontSize: 14, color: Colors.red[400], fontWeight: FontWeight.bold), - ), - onPressed: processing.value - ? null - : selection.value.isEmpty - ? handleEmptyTrash - : handlePermanentDelete, - ), - TextButton.icon( - icon: const Icon(Icons.history_rounded), - label: Text( - selection.value.isEmpty ? 'trash_page_restore_all'.tr() : 'restore'.tr(), - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), - ), - onPressed: processing.value - ? null - : selection.value.isEmpty - ? handleRestoreAll - : handleRestore, - ), - ], - ), - ), - ), - ), - ); - } - - return Scaffold( - appBar: trashRenderList.maybeWhen( - orElse: () => buildAppBar("?"), - data: (data) => buildAppBar(data.totalAssets.toString()), - ), - body: trashRenderList.widgetWhen( - onData: (data) => data.isEmpty - ? Center(child: Text('trash_page_no_assets'.tr())) - : Stack( - children: [ - SafeArea( - child: ImmichAssetGrid( - renderList: data, - listener: selectionListener, - selectionActive: selectionEnabledHook.value, - showMultiSelectIndicator: false, - showStack: true, - topWidget: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 24), - child: const Text("trash_page_info").tr(namedArgs: {"days": "$trashDays"}), - ), - ), - ), - if (selectionEnabledHook.value) buildBottomBar(), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/pages/onboarding/permission_onboarding.page.dart b/mobile/lib/pages/onboarding/permission_onboarding.page.dart deleted file mode 100644 index 52d4ac0125..0000000000 --- a/mobile/lib/pages/onboarding/permission_onboarding.page.dart +++ /dev/null @@ -1,141 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/gallery_permission.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/immich_logo.dart'; -import 'package:immich_mobile/widgets/common/immich_title_text.dart'; -import 'package:permission_handler/permission_handler.dart'; - -@RoutePage() -class PermissionOnboardingPage extends HookConsumerWidget { - const PermissionOnboardingPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final PermissionStatus permission = ref.watch(galleryPermissionNotifier); - - // Navigate to the main Tab Controller when permission is granted - void goToBackup() => context.replaceRoute(const BackupControllerRoute()); - - // When the permission is denied, we show a request permission page - buildRequestPermission() { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('permission_onboarding_request', style: context.textTheme.titleMedium, textAlign: TextAlign.center).tr(), - const SizedBox(height: 18), - ElevatedButton( - onPressed: () => - ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission().then((permission) async { - if (permission.isGranted) { - // If permission is limited, we will show the limited - // permission page - goToBackup(); - } - }), - child: const Text('continue').tr(), - ), - ], - ); - } - - // When permission is granted from outside the app, this will show to - // let them continue on to the main timeline - buildPermissionGranted() { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'permission_onboarding_permission_granted', - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ).tr(), - const SizedBox(height: 18), - ElevatedButton(onPressed: () => goToBackup(), child: const Text('permission_onboarding_get_started').tr()), - ], - ); - } - - // iOS 14+ has limited permission options, which let someone just share - // a few photos with the app. If someone only has limited permissions, we - // inform that Immich works best when given full permission - buildPermissionLimited() { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.warning_outlined, color: Colors.yellow, size: 48), - const SizedBox(height: 8), - Text( - 'permission_onboarding_permission_limited', - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ).tr(), - const SizedBox(height: 18), - ElevatedButton( - onPressed: () => openAppSettings(), - child: const Text('permission_onboarding_go_to_settings').tr(), - ), - const SizedBox(height: 8.0), - TextButton(onPressed: () => goToBackup(), child: const Text('permission_onboarding_continue_anyway').tr()), - ], - ); - } - - buildPermissionDenied() { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.warning_outlined, color: Colors.red, size: 48), - const SizedBox(height: 8), - Text( - 'permission_onboarding_permission_denied', - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ).tr(), - const SizedBox(height: 18), - ElevatedButton( - onPressed: () => openAppSettings(), - child: const Text('permission_onboarding_go_to_settings').tr(), - ), - ], - ); - } - - final Widget child = switch (permission) { - PermissionStatus.limited => buildPermissionLimited(), - PermissionStatus.denied => buildRequestPermission(), - PermissionStatus.granted || PermissionStatus.provisional => buildPermissionGranted(), - PermissionStatus.restricted || PermissionStatus.permanentlyDenied => buildPermissionDenied(), - }; - - return Scaffold( - body: SafeArea( - child: Center( - child: SizedBox( - width: 380, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const ImmichLogo(heroTag: 'logo'), - const ImmichTitleText(), - AnimatedSwitcher( - duration: const Duration(milliseconds: 500), - child: Padding(padding: const EdgeInsets.all(18.0), child: child), - ), - TextButton(child: const Text('back').tr(), onPressed: () => context.maybePop()), - ], - ), - ), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/photos/memory.page.dart b/mobile/lib/pages/photos/memory.page.dart deleted file mode 100644 index bd7973bc21..0000000000 --- a/mobile/lib/pages/photos/memory.page.dart +++ /dev/null @@ -1,324 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/models/memories/memory.model.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; -import 'package:immich_mobile/widgets/memories/memory_bottom_info.dart'; -import 'package:immich_mobile/widgets/memories/memory_card.dart'; -import 'package:immich_mobile/widgets/memories/memory_epilogue.dart'; -import 'package:immich_mobile/widgets/memories/memory_progress_indicator.dart'; - -@RoutePage() -/// Expects [currentAssetProvider] to be set before navigating to this page -class MemoryPage extends HookConsumerWidget { - final List memories; - final int memoryIndex; - - const MemoryPage({required this.memories, required this.memoryIndex, super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final currentMemory = useState(memories[memoryIndex]); - final currentAssetPage = useState(0); - final currentMemoryIndex = useState(memoryIndex); - final assetProgress = useState("${currentAssetPage.value + 1}|${currentMemory.value.assets.length}"); - const bgColor = Colors.black; - final currentAsset = useState(null); - - /// The list of all of the asset page controllers - final memoryAssetPageControllers = List.generate(memories.length, (i) => usePageController()); - - /// The main vertically scrolling page controller with each list of memories - final memoryPageController = usePageController(initialPage: memoryIndex); - - useEffect(() { - // Memories is an immersive activity - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); - return () { - // Clean up to normal edge to edge when we are done - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - }; - }); - - toNextMemory() { - memoryPageController.nextPage(duration: const Duration(milliseconds: 500), curve: Curves.easeIn); - } - - void toPreviousMemory() { - if (currentMemoryIndex.value > 0) { - // Move to the previous memory page - memoryPageController.previousPage(duration: const Duration(milliseconds: 500), curve: Curves.easeIn); - - // Wait for the next frame to ensure the page is built - SchedulerBinding.instance.addPostFrameCallback((_) { - final previousIndex = currentMemoryIndex.value - 1; - final previousMemoryController = memoryAssetPageControllers[previousIndex]; - - // Ensure the controller is attached - if (previousMemoryController.hasClients) { - previousMemoryController.jumpToPage(memories[previousIndex].assets.length - 1); - } else { - // Wait for the next frame until it is attached - SchedulerBinding.instance.addPostFrameCallback((_) { - if (previousMemoryController.hasClients) { - previousMemoryController.jumpToPage(memories[previousIndex].assets.length - 1); - } - }); - } - }); - } - } - - toNextAsset(int currentAssetIndex) { - if (currentAssetIndex + 1 < currentMemory.value.assets.length) { - // Go to the next asset - PageController controller = memoryAssetPageControllers[currentMemoryIndex.value]; - - controller.nextPage(curve: Curves.easeInOut, duration: const Duration(milliseconds: 500)); - } else { - // Go to the next memory since we are at the end of our assets - toNextMemory(); - } - } - - toPreviousAsset(int currentAssetIndex) { - if (currentAssetIndex > 0) { - // Go to the previous asset - PageController controller = memoryAssetPageControllers[currentMemoryIndex.value]; - - controller.previousPage(curve: Curves.easeInOut, duration: const Duration(milliseconds: 500)); - } else { - // Go to the previous memory since we are at the end of our assets - toPreviousMemory(); - } - } - - updateProgressText() { - assetProgress.value = "${currentAssetPage.value + 1}|${currentMemory.value.assets.length}"; - } - - /// Downloads and caches the image for the asset at this [currentMemory]'s index - precacheAsset(int index) async { - // Guard index out of range - if (index < 0) { - return; - } - - // Context might be removed due to popping out of Memory Lane during Scroll handling - if (!context.mounted) { - return; - } - - late Asset asset; - if (index < currentMemory.value.assets.length) { - // Uses the next asset in this current memory - asset = currentMemory.value.assets[index]; - } else { - // Precache the first asset in the next memory if available - final currentMemoryIndex = memories.indexOf(currentMemory.value); - - // Guard no memory found - if (currentMemoryIndex == -1) { - return; - } - - final nextMemoryIndex = currentMemoryIndex + 1; - // Guard no next memory - if (nextMemoryIndex >= memories.length) { - return; - } - - // Get the first asset from the next memory - asset = memories[nextMemoryIndex].assets.first; - } - - // Precache the asset - final size = MediaQuery.sizeOf(context); - await precacheImage( - ImmichImage.imageProvider(asset: asset, width: size.width, height: size.height), - context, - size: size, - ); - } - - // Precache the next page right away if we are on the first page - if (currentAssetPage.value == 0) { - Future.delayed(const Duration(milliseconds: 200)).then((_) => precacheAsset(1)); - } - - Future onAssetChanged(int otherIndex) async { - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - currentAssetPage.value = otherIndex; - updateProgressText(); - - // Wait for page change animation to finish - await Future.delayed(const Duration(milliseconds: 400)); - // And then precache the next asset - await precacheAsset(otherIndex + 1); - - final asset = currentMemory.value.assets[otherIndex]; - currentAsset.value = asset; - ref.read(currentAssetProvider.notifier).set(asset); - } - - /* Notification listener is used instead of OnPageChanged callback since OnPageChanged is called - * when the page in the **center** of the viewer changes. We want to reset currentAssetPage only when the final - * page during the end of scroll is different than the current page - */ - return NotificationListener( - onNotification: (ScrollNotification notification) { - // Calculate OverScroll manually using the number of pixels away from maxScrollExtent - // maxScrollExtend contains the sum of horizontal pixels of all assets for depth = 1 - // or sum of vertical pixels of all memories for depth = 0 - if (notification is ScrollUpdateNotification) { - final isEpiloguePage = (memoryPageController.page?.floor() ?? 0) >= memories.length; - - final offset = notification.metrics.pixels; - if (isEpiloguePage && (offset > notification.metrics.maxScrollExtent + 150)) { - context.maybePop(); - return true; - } - } - - return false; - }, - child: Scaffold( - backgroundColor: bgColor, - body: SafeArea( - child: PageView.builder( - physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), - scrollDirection: Axis.vertical, - controller: memoryPageController, - onPageChanged: (pageNumber) { - ref.read(hapticFeedbackProvider.notifier).mediumImpact(); - if (pageNumber < memories.length) { - currentMemoryIndex.value = pageNumber; - currentMemory.value = memories[pageNumber]; - } - - currentAssetPage.value = 0; - - updateProgressText(); - }, - itemCount: memories.length + 1, - itemBuilder: (context, mIndex) { - // Build last page - if (mIndex == memories.length) { - return MemoryEpilogue( - onStartOver: () => memoryPageController.animateToPage( - 0, - duration: const Duration(seconds: 1), - curve: Curves.easeInOut, - ), - ); - } - // Build horizontal page - final assetController = memoryAssetPageControllers[mIndex]; - return Column( - children: [ - Padding( - padding: const EdgeInsets.only(left: 24.0, right: 24.0, top: 8.0, bottom: 2.0), - child: AnimatedBuilder( - animation: assetController, - builder: (context, child) { - double value = 0.0; - if (assetController.hasClients) { - // We can only access [page] if this has clients - value = assetController.page ?? 0; - } - return MemoryProgressIndicator( - ticks: memories[mIndex].assets.length, - value: (value + 1) / memories[mIndex].assets.length, - ); - }, - ), - ), - Expanded( - child: Stack( - children: [ - PageView.builder( - physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), - controller: assetController, - onPageChanged: onAssetChanged, - scrollDirection: Axis.horizontal, - itemCount: memories[mIndex].assets.length, - itemBuilder: (context, index) { - final asset = memories[mIndex].assets[index]; - return Stack( - children: [ - Container( - color: Colors.black, - child: MemoryCard(asset: asset, title: memories[mIndex].title, showTitle: index == 0), - ), - Positioned.fill( - child: Row( - children: [ - // Left side of the screen - Expanded( - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - toPreviousAsset(index); - }, - ), - ), - - // Right side of the screen - Expanded( - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - toNextAsset(index); - }, - ), - ), - ], - ), - ), - ], - ); - }, - ), - Positioned( - top: 8, - left: 8, - child: MaterialButton( - minWidth: 0, - onPressed: () { - // auto_route doesn't invoke pop scope, so - // turn off full screen mode here - // https://github.com/Milad-Akarie/auto_route_library/issues/1799 - context.maybePop(); - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - }, - shape: const CircleBorder(), - color: Colors.white.withValues(alpha: 0.2), - elevation: 0, - child: const Icon(Icons.close_rounded, color: Colors.white), - ), - ), - if (currentAsset.value != null && currentAsset.value!.isVideo) - Positioned( - bottom: 24, - right: 32, - child: Icon(Icons.videocam_outlined, color: Colors.grey[200]), - ), - ], - ), - ), - MemoryBottomInfo(memory: memories[mIndex]), - ], - ); - }, - ), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/photos/photos.page.dart b/mobile/lib/pages/photos/photos.page.dart deleted file mode 100644 index 7f57247ec4..0000000000 --- a/mobile/lib/pages/photos/photos.page.dart +++ /dev/null @@ -1,130 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/providers/websocket.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; -import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; -import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; -import 'package:immich_mobile/widgets/memories/memory_lane.dart'; - -@RoutePage() -class PhotosPage extends HookConsumerWidget { - const PhotosPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final currentUser = ref.watch(currentUserProvider); - final timelineUsers = ref.watch(timelineUsersIdsProvider); - final tipOneOpacity = useState(0.0); - final refreshCount = useState(0); - - useEffect(() { - ref.read(websocketProvider.notifier).connect(); - Future(() => ref.read(assetProvider.notifier).getAllAsset()); - Future(() => ref.read(albumProvider.notifier).refreshRemoteAlbums()); - ref.read(serverInfoProvider.notifier).getServerInfo(); - - return; - }, []); - - Widget buildLoadingIndicator() { - Timer(const Duration(seconds: 2), () => tipOneOpacity.value = 1); - - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const ImmichLoadingIndicator(), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Text( - 'home_page_building_timeline', - style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor), - ).tr(), - ), - const SizedBox(height: 8), - AnimatedOpacity( - duration: const Duration(milliseconds: 1000), - opacity: tipOneOpacity.value, - child: Column( - children: [ - SizedBox( - width: 320, - child: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - 'home_page_first_time_notice', - textAlign: TextAlign.center, - style: context.textTheme.bodyMedium, - ).tr(), - ), - ), - ], - ), - ), - ], - ), - ); - } - - Future refreshAssets() async { - final fullRefresh = refreshCount.value > 0; - - if (fullRefresh) { - unawaited( - Future.wait([ - ref.read(assetProvider.notifier).getAllAsset(clear: true), - ref.read(albumProvider.notifier).refreshRemoteAlbums(), - ]), - ); - - // refresh was forced: user requested another refresh within 2 seconds - refreshCount.value = 0; - } else { - await ref.read(assetProvider.notifier).getAllAsset(clear: false); - - refreshCount.value++; - // set counter back to 0 if user does not request refresh again - Timer(const Duration(seconds: 4), () => refreshCount.value = 0); - } - } - - return Stack( - children: [ - MultiselectGrid( - topWidget: (currentUser != null && currentUser.memoryEnabled) ? const MemoryLane() : const SizedBox(), - renderListProvider: timelineUsers.length > 1 - ? multiUsersTimelineProvider(timelineUsers) - : singleUserTimelineProvider(currentUser?.id), - buildLoadingIndicator: buildLoadingIndicator, - onRefresh: refreshAssets, - stackEnabled: true, - archiveEnabled: true, - editEnabled: true, - ), - AnimatedPositioned( - duration: const Duration(milliseconds: 300), - top: ref.watch(multiselectProvider) ? -(kToolbarHeight + context.padding.top) : 0, - left: 0, - right: 0, - child: Container( - height: kToolbarHeight + context.padding.top, - color: context.themeData.appBarTheme.backgroundColor, - child: const ImmichAppBar(), - ), - ), - ], - ); - } -} diff --git a/mobile/lib/pages/search/all_motion_videos.page.dart b/mobile/lib/pages/search/all_motion_videos.page.dart deleted file mode 100644 index 60bb8a6cff..0000000000 --- a/mobile/lib/pages/search/all_motion_videos.page.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; -import 'package:immich_mobile/providers/search/all_motion_photos.provider.dart'; - -@RoutePage() -class AllMotionPhotosPage extends HookConsumerWidget { - const AllMotionPhotosPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final motionPhotos = ref.watch(allMotionPhotosProvider); - - return Scaffold( - appBar: AppBar( - title: const Text('search_page_motion_photos').tr(), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - ), - body: motionPhotos.widgetWhen(onData: (assets) => ImmichAssetGrid(assets: assets)), - ); - } -} diff --git a/mobile/lib/pages/search/all_people.page.dart b/mobile/lib/pages/search/all_people.page.dart deleted file mode 100644 index b2814e6c13..0000000000 --- a/mobile/lib/pages/search/all_people.page.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/providers/search/people.provider.dart'; -import 'package:immich_mobile/widgets/search/explore_grid.dart'; - -@RoutePage() -class AllPeoplePage extends HookConsumerWidget { - const AllPeoplePage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final curatedPeople = ref.watch(getAllPeopleProvider); - - return Scaffold( - appBar: AppBar( - title: const Text('people').tr(), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - ), - body: curatedPeople.widgetWhen( - onData: (people) => ExploreGrid( - isPeople: true, - curatedContent: people.map((e) => SearchCuratedContent(label: e.name, id: e.id)).toList(), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/search/all_places.page.dart b/mobile/lib/pages/search/all_places.page.dart deleted file mode 100644 index c92f87d3ac..0000000000 --- a/mobile/lib/pages/search/all_places.page.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/providers/search/search_page_state.provider.dart'; -import 'package:immich_mobile/widgets/search/explore_grid.dart'; - -@RoutePage() -class AllPlacesPage extends HookConsumerWidget { - const AllPlacesPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - AsyncValue> places = ref.watch(getAllPlacesProvider); - - return Scaffold( - appBar: AppBar( - title: const Text('places').tr(), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - ), - body: places.widgetWhen(onData: (data) => ExploreGrid(curatedContent: data)), - ); - } -} diff --git a/mobile/lib/pages/search/all_videos.page.dart b/mobile/lib/pages/search/all_videos.page.dart deleted file mode 100644 index acad043a58..0000000000 --- a/mobile/lib/pages/search/all_videos.page.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; - -@RoutePage() -class AllVideosPage extends HookConsumerWidget { - const AllVideosPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Scaffold( - appBar: AppBar( - title: const Text('videos').tr(), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - ), - body: MultiselectGrid(renderListProvider: allVideosTimelineProvider), - ); - } -} diff --git a/mobile/lib/pages/search/map/map.page.dart b/mobile/lib/pages/search/map/map.page.dart deleted file mode 100644 index 993b91d8f7..0000000000 --- a/mobile/lib/pages/search/map/map.page.dart +++ /dev/null @@ -1,384 +0,0 @@ -import 'dart:async'; -import 'dart:math'; - -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:geolocator/geolocator.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart'; -import 'package:immich_mobile/models/map/map_event.model.dart'; -import 'package:immich_mobile/models/map/map_marker.model.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/providers/map/map_marker.provider.dart'; -import 'package:immich_mobile/providers/map/map_state.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/debounce.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/utils/map_utils.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/widgets/map/map_app_bar.dart'; -import 'package:immich_mobile/widgets/map/map_asset_grid.dart'; -import 'package:immich_mobile/widgets/map/map_bottom_sheet.dart'; -import 'package:immich_mobile/widgets/map/map_theme_override.dart'; -import 'package:immich_mobile/widgets/map/positioned_asset_marker_icon.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; - -@RoutePage() -class MapPage extends HookConsumerWidget { - const MapPage({super.key, this.initialLocation}); - final LatLng? initialLocation; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final mapController = useRef(null); - final markers = useRef>([]); - final markersInBounds = useRef>([]); - final bottomSheetStreamController = useStreamController(); - final selectedMarker = useValueNotifier<_AssetMarkerMeta?>(null); - final assetsDebouncer = useDebouncer(); - final layerDebouncer = useDebouncer(interval: const Duration(seconds: 1)); - final isLoading = useProcessingOverlay(); - final scrollController = useScrollController(); - final markerDebouncer = useDebouncer(interval: const Duration(milliseconds: 800)); - final selectedAssets = useValueNotifier>({}); - const mapZoomToAssetLevel = 12.0; - - // updates the markersInBounds value with the map markers that are visible in the current - // map camera bounds - Future updateAssetsInBounds() async { - // Guard map not created - if (mapController.value == null) { - return; - } - - final bounds = await mapController.value!.getVisibleRegion(); - final inBounds = markers.value - .where((m) => bounds.contains(LatLng(m.latLng.latitude, m.latLng.longitude))) - .toList(); - // Notify bottom sheet to update asset grid only when there are new assets - if (markersInBounds.value.length != inBounds.length) { - bottomSheetStreamController.add(MapAssetsInBoundsUpdated(inBounds.map((e) => e.assetRemoteId).toList())); - } - markersInBounds.value = inBounds; - } - - // removes all sources and layers and re-adds them with the updated markers - Future reloadLayers() async { - if (mapController.value != null) { - layerDebouncer.run(() => mapController.value!.reloadAllLayersForMarkers(markers.value)); - } - } - - Future loadMarkers() async { - try { - isLoading.value = true; - markers.value = await ref.read(mapMarkersProvider.future); - assetsDebouncer.run(updateAssetsInBounds); - await reloadLayers(); - } finally { - isLoading.value = false; - } - } - - useEffect(() { - final currentAssetLink = ref.read(currentAssetProvider.notifier).ref.keepAlive(); - - loadMarkers(); - return currentAssetLink.close; - }, []); - - // Refetch markers when map state is changed - ref.listen(mapStateNotifierProvider, (_, current) { - if (current.shouldRefetchMarkers) { - markerDebouncer.run(() { - ref.invalidate(mapMarkersProvider); - // Reset marker - selectedMarker.value = null; - loadMarkers(); - ref.read(mapStateNotifierProvider.notifier).setRefetchMarkers(false); - }); - } - }); - - // updates the selected markers position based on the current map camera - Future updateAssetMarkerPosition(MapMarker marker, {bool shouldAnimate = true}) async { - final assetPoint = await mapController.value!.toScreenLocation(marker.latLng); - selectedMarker.value = _AssetMarkerMeta(point: assetPoint, marker: marker, shouldAnimate: shouldAnimate); - (assetPoint, marker, shouldAnimate); - } - - // finds the nearest asset marker from the tap point and store it as the selectedMarker - Future onMarkerClicked(Point point, LatLng _) async { - // Guard map not created - if (mapController.value == null) { - return; - } - final latlngBound = await mapController.value!.getBoundsFromPoint(point, 50); - final marker = markersInBounds.value.firstWhereOrNull( - (m) => latlngBound.contains(LatLng(m.latLng.latitude, m.latLng.longitude)), - ); - - if (marker != null) { - await updateAssetMarkerPosition(marker); - } else { - // If no asset was previously selected and no new asset is available, close the bottom sheet - if (selectedMarker.value == null) { - bottomSheetStreamController.add(const MapCloseBottomSheet()); - } - selectedMarker.value = null; - } - } - - void onMapCreated(MapLibreMapController controller) async { - mapController.value = controller; - controller.addListener(() { - if (controller.isCameraMoving && selectedMarker.value != null) { - updateAssetMarkerPosition(selectedMarker.value!.marker, shouldAnimate: false); - } - }); - } - - Future onMarkerTapped() async { - final assetId = selectedMarker.value?.marker.assetRemoteId; - if (assetId == null) { - return; - } - - final asset = await ref.read(dbProvider).assets.getByRemoteId(assetId); - if (asset == null) { - return; - } - - // Since we only have a single asset, we can just show GroupAssetBy.none - final renderList = await RenderList.fromAssets([asset], GroupAssetsBy.none); - - ref.read(currentAssetProvider.notifier).set(asset); - if (asset.isVideo) { - ref.read(showControlsProvider.notifier).show = false; - } - unawaited(context.pushRoute(GalleryViewerRoute(initialIndex: 0, heroOffset: 0, renderList: renderList))); - } - - /// BOTTOM SHEET CALLBACKS - - Future onMapMoved() async { - assetsDebouncer.run(updateAssetsInBounds); - } - - void onBottomSheetScrolled(String assetRemoteId) { - final assetMarker = markersInBounds.value.firstWhereOrNull((m) => m.assetRemoteId == assetRemoteId); - if (assetMarker != null) { - updateAssetMarkerPosition(assetMarker); - } - } - - void onZoomToAsset(String assetRemoteId) { - final assetMarker = markersInBounds.value.firstWhereOrNull((m) => m.assetRemoteId == assetRemoteId); - if (mapController.value != null && assetMarker != null) { - // Offset the latitude a little to show the marker just above the viewports center - final offset = context.isMobile ? 0.02 : 0; - final latlng = LatLng(assetMarker.latLng.latitude - offset, assetMarker.latLng.longitude); - mapController.value!.animateCamera( - CameraUpdate.newLatLngZoom(latlng, mapZoomToAssetLevel), - duration: const Duration(milliseconds: 800), - ); - } - } - - void onZoomToLocation() async { - final (location, error) = await MapUtils.checkPermAndGetLocation(context: context); - if (error != null) { - if (error == LocationPermission.unableToDetermine && context.mounted) { - ImmichToast.show( - context: context, - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - msg: "map_cannot_get_user_location".tr(), - ); - } - return; - } - - if (mapController.value != null && location != null) { - await mapController.value!.animateCamera( - CameraUpdate.newLatLngZoom(LatLng(location.latitude, location.longitude), mapZoomToAssetLevel), - duration: const Duration(milliseconds: 800), - ); - } - } - - void onAssetsSelected(bool selected, Set selection) { - selectedAssets.value = selected ? selection : {}; - } - - return MapThemeOverride( - mapBuilder: (style) => context.isMobile - // Single-column - ? Scaffold( - extendBodyBehindAppBar: true, - appBar: MapAppBar(selectedAssets: selectedAssets), - body: Stack( - children: [ - _MapWithMarker( - initialLocation: initialLocation, - style: style, - selectedMarker: selectedMarker, - onMapCreated: onMapCreated, - onMapMoved: onMapMoved, - onMapClicked: onMarkerClicked, - onStyleLoaded: reloadLayers, - onMarkerTapped: onMarkerTapped, - ), - // Should be a part of the body and not scaffold::bottomsheet for the - // location button to be hit testable - MapBottomSheet( - mapEventStream: bottomSheetStreamController.stream, - onGridAssetChanged: onBottomSheetScrolled, - onZoomToAsset: onZoomToAsset, - onAssetsSelected: onAssetsSelected, - onZoomToLocation: onZoomToLocation, - selectedAssets: selectedAssets, - ), - ], - ), - ) - // Two-pane - : Row( - children: [ - Expanded( - child: Scaffold( - extendBodyBehindAppBar: true, - appBar: MapAppBar(selectedAssets: selectedAssets), - body: Stack( - children: [ - _MapWithMarker( - initialLocation: initialLocation, - style: style, - selectedMarker: selectedMarker, - onMapCreated: onMapCreated, - onMapMoved: onMapMoved, - onMapClicked: onMarkerClicked, - onStyleLoaded: reloadLayers, - onMarkerTapped: onMarkerTapped, - ), - Positioned( - right: 0, - bottom: context.padding.bottom + 16, - child: ElevatedButton( - onPressed: onZoomToLocation, - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.my_location), - ), - ), - ], - ), - ), - ), - Expanded( - child: LayoutBuilder( - builder: (ctx, constraints) => MapAssetGrid( - controller: scrollController, - mapEventStream: bottomSheetStreamController.stream, - onGridAssetChanged: onBottomSheetScrolled, - onZoomToAsset: onZoomToAsset, - onAssetsSelected: onAssetsSelected, - selectedAssets: selectedAssets, - ), - ), - ), - ], - ), - ); - } -} - -class _AssetMarkerMeta { - final Point point; - final MapMarker marker; - final bool shouldAnimate; - - const _AssetMarkerMeta({required this.point, required this.marker, required this.shouldAnimate}); - - @override - String toString() => '_AssetMarkerMeta(point: $point, marker: $marker, shouldAnimate: $shouldAnimate)'; -} - -class _MapWithMarker extends StatelessWidget { - final AsyncValue style; - final MapCreatedCallback onMapCreated; - final OnCameraIdleCallback onMapMoved; - final OnMapClickCallback onMapClicked; - final OnStyleLoadedCallback onStyleLoaded; - final Function()? onMarkerTapped; - final ValueNotifier<_AssetMarkerMeta?> selectedMarker; - final LatLng? initialLocation; - - const _MapWithMarker({ - required this.style, - required this.onMapCreated, - required this.onMapMoved, - required this.onMapClicked, - required this.onStyleLoaded, - required this.selectedMarker, - this.onMarkerTapped, - this.initialLocation, - }); - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (ctx, constraints) => SizedBox( - height: constraints.maxHeight, - width: constraints.maxWidth, - child: Stack( - children: [ - style.widgetWhen( - onData: (style) => MapLibreMap( - attributionButtonMargins: const Point(8, kToolbarHeight), - initialCameraPosition: CameraPosition( - target: initialLocation ?? const LatLng(0, 0), - zoom: initialLocation != null ? 12 : 0, - ), - styleString: style, - // This is needed to update the selectedMarker's position on map camera updates - // The changes are notified through the mapController ValueListener which is added in [onMapCreated] - trackCameraPosition: true, - onMapCreated: onMapCreated, - onCameraIdle: onMapMoved, - onMapClick: onMapClicked, - onStyleLoadedCallback: onStyleLoaded, - tiltGesturesEnabled: false, - dragEnabled: false, - myLocationEnabled: false, - attributionButtonPosition: AttributionButtonPosition.topRight, - rotateGesturesEnabled: false, - ), - ), - ValueListenableBuilder( - valueListenable: selectedMarker, - builder: (ctx, value, _) => value != null - ? PositionedAssetMarkerIcon( - point: value.point, - assetRemoteId: value.marker.assetRemoteId, - assetThumbhash: '', - durationInMilliseconds: value.shouldAnimate ? 100 : 0, - onTap: onMarkerTapped, - ) - : const SizedBox.shrink(), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/pages/search/person_result.page.dart b/mobile/lib/pages/search/person_result.page.dart deleted file mode 100644 index 8375eb14fd..0000000000 --- a/mobile/lib/pages/search/person_result.page.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' hide Store; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/providers/search/people.provider.dart'; -import 'package:immich_mobile/widgets/search/person_name_edit_form.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; - -@RoutePage() -class PersonResultPage extends HookConsumerWidget { - final String personId; - final String personName; - - const PersonResultPage({super.key, required this.personId, required this.personName}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final name = useState(personName); - - showEditNameDialog() { - showDialog( - context: context, - useRootNavigator: false, - builder: (BuildContext context) { - return PersonNameEditForm(personId: personId, personName: name.value); - }, - ).then((result) { - if (result != null && result.success) { - name.value = result.updatedName; - } - }); - } - - void buildBottomSheet() { - showModalBottomSheet( - backgroundColor: context.scaffoldBackgroundColor, - isScrollControlled: false, - context: context, - useSafeArea: true, - builder: (context) { - return SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: const Icon(Icons.edit_outlined), - title: const Text('edit_name', style: TextStyle(fontWeight: FontWeight.bold)).tr(), - onTap: showEditNameDialog, - ), - ], - ), - ); - }, - ); - } - - buildTitleBlock() { - return GestureDetector( - onTap: showEditNameDialog, - child: name.value.isEmpty - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('add_a_name', style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor)).tr(), - Text('find_them_fast', style: context.textTheme.labelLarge).tr(), - ], - ) - : Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [Text(name.value, style: context.textTheme.titleLarge, overflow: TextOverflow.ellipsis)], - ), - ); - } - - return Scaffold( - appBar: AppBar( - title: Text(name.value), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - actions: [IconButton(onPressed: buildBottomSheet, icon: const Icon(Icons.more_vert_rounded))], - ), - body: MultiselectGrid( - renderListProvider: personAssetsProvider(personId), - topWidget: Padding( - padding: const EdgeInsets.only(left: 8.0, top: 24), - child: Row( - children: [ - CircleAvatar(radius: 36, backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(personId))), - Expanded( - child: Padding(padding: const EdgeInsets.only(left: 16.0, right: 16.0), child: buildTitleBlock()), - ), - ], - ), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/search/recently_taken.page.dart b/mobile/lib/pages/search/recently_taken.page.dart deleted file mode 100644 index 988af2faf0..0000000000 --- a/mobile/lib/pages/search/recently_taken.page.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; -import 'package:immich_mobile/providers/search/recently_taken_asset.provider.dart'; - -@RoutePage() -class RecentlyTakenPage extends HookConsumerWidget { - const RecentlyTakenPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final recents = ref.watch(recentlyTakenAssetProvider); - - return Scaffold( - appBar: AppBar( - title: const Text('recently_taken_page_title').tr(), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - ), - body: recents.widgetWhen(onData: (searchResponse) => ImmichAssetGrid(assets: searchResponse)), - ); - } -} diff --git a/mobile/lib/pages/search/search.page.dart b/mobile/lib/pages/search/search.page.dart deleted file mode 100644 index dbd32ac94b..0000000000 --- a/mobile/lib/pages/search/search.page.dart +++ /dev/null @@ -1,760 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/person.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/search/search_filter.model.dart'; -import 'package:immich_mobile/providers/search/paginated_search.provider.dart'; -import 'package:immich_mobile/providers/search/search_input_focus.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; -import 'package:immich_mobile/widgets/common/search_field.dart'; -import 'package:immich_mobile/widgets/search/search_filter/camera_picker.dart'; -import 'package:immich_mobile/widgets/search/search_filter/display_option_picker.dart'; -import 'package:immich_mobile/widgets/search/search_filter/filter_bottom_sheet_scaffold.dart'; -import 'package:immich_mobile/widgets/search/search_filter/location_picker.dart'; -import 'package:immich_mobile/widgets/search/search_filter/media_type_picker.dart'; -import 'package:immich_mobile/widgets/search/search_filter/people_picker.dart'; -import 'package:immich_mobile/widgets/search/search_filter/search_filter_chip.dart'; -import 'package:immich_mobile/widgets/search/search_filter/search_filter_utils.dart'; - -@RoutePage() -class SearchPage extends HookConsumerWidget { - const SearchPage({super.key, this.prefilter}); - - final SearchFilter? prefilter; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final textSearchType = useState(TextSearchType.context); - final searchHintText = useState('sunrise_on_the_beach'.tr()); - final textSearchController = useTextEditingController(); - final filter = useState( - SearchFilter( - people: prefilter?.people ?? {}, - location: prefilter?.location ?? SearchLocationFilter(), - camera: prefilter?.camera ?? SearchCameraFilter(), - date: prefilter?.date ?? SearchDateFilter(), - display: prefilter?.display ?? SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), - mediaType: prefilter?.mediaType ?? AssetType.other, - rating: prefilter?.rating ?? SearchRatingFilter(), - language: "${context.locale.languageCode}-${context.locale.countryCode}", - ), - ); - - final previousFilter = useState(null); - - final peopleCurrentFilterWidget = useState(null); - final dateRangeCurrentFilterWidget = useState(null); - final cameraCurrentFilterWidget = useState(null); - final locationCurrentFilterWidget = useState(null); - final mediaTypeCurrentFilterWidget = useState(null); - final displayOptionCurrentFilterWidget = useState(null); - - final isSearching = useState(false); - - SnackBar searchInfoSnackBar(String message) { - return SnackBar( - content: Text(message, style: context.textTheme.labelLarge), - showCloseIcon: true, - behavior: SnackBarBehavior.fixed, - closeIconColor: context.colorScheme.onSurface, - ); - } - - search() async { - if (filter.value.isEmpty) { - return; - } - - if (prefilter == null && filter.value == previousFilter.value) { - return; - } - - isSearching.value = true; - ref.watch(paginatedSearchProvider.notifier).clear(); - final hasResult = await ref.watch(paginatedSearchProvider.notifier).search(filter.value); - - if (!hasResult) { - context.showSnackBar(searchInfoSnackBar('search_no_result'.tr())); - } - - previousFilter.value = filter.value; - isSearching.value = false; - } - - loadMoreSearchResult() async { - isSearching.value = true; - final hasResult = await ref.watch(paginatedSearchProvider.notifier).search(filter.value); - - if (!hasResult) { - context.showSnackBar(searchInfoSnackBar('search_no_more_result'.tr())); - } - - isSearching.value = false; - } - - searchPrefilter() { - if (prefilter != null) { - Future.delayed(Duration.zero, () { - search(); - - if (prefilter!.location.city != null) { - locationCurrentFilterWidget.value = Text(prefilter!.location.city!, style: context.textTheme.labelLarge); - } - }); - } - } - - useEffect(() { - Future.microtask(() => ref.invalidate(paginatedSearchProvider)); - searchPrefilter(); - - return null; - }, []); - - showPeoplePicker() { - handleOnSelect(Set value) { - filter.value = filter.value.copyWith(people: value); - - peopleCurrentFilterWidget.value = Text( - value.map((e) => e.name != '' ? e.name : 'no_name'.tr()).join(', '), - style: context.textTheme.labelLarge, - ); - } - - handleClear() { - filter.value = filter.value.copyWith(people: {}); - - peopleCurrentFilterWidget.value = null; - search(); - } - - showFilterBottomSheet( - context: context, - isScrollControlled: true, - child: FractionallySizedBox( - heightFactor: 0.8, - child: FilterBottomSheetScaffold( - title: 'search_filter_people_title'.tr(), - expanded: true, - onSearch: search, - onClear: handleClear, - child: PeoplePicker(onSelect: handleOnSelect, filter: filter.value.people), - ), - ), - ); - } - - showLocationPicker() { - handleOnSelect(Map value) { - filter.value = filter.value.copyWith( - location: SearchLocationFilter(country: value['country'], city: value['city'], state: value['state']), - ); - - final locationText = []; - if (value['country'] != null) { - locationText.add(value['country']!); - } - - if (value['state'] != null) { - locationText.add(value['state']!); - } - - if (value['city'] != null) { - locationText.add(value['city']!); - } - - locationCurrentFilterWidget.value = Text(locationText.join(', '), style: context.textTheme.labelLarge); - } - - handleClear() { - filter.value = filter.value.copyWith(location: SearchLocationFilter()); - - locationCurrentFilterWidget.value = null; - search(); - } - - showFilterBottomSheet( - context: context, - isScrollControlled: true, - isDismissible: true, - child: FilterBottomSheetScaffold( - title: 'search_filter_location_title'.tr(), - onSearch: search, - onClear: handleClear, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: Container( - padding: EdgeInsets.only(bottom: context.viewInsets.bottom), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: LocationPicker(onSelected: handleOnSelect, filter: filter.value.location), - ), - ), - ), - ), - ); - } - - showCameraPicker() { - handleOnSelect(Map value) { - filter.value = filter.value.copyWith( - camera: SearchCameraFilter(make: value['make'], model: value['model']), - ); - - cameraCurrentFilterWidget.value = Text( - '${value['make'] ?? ''} ${value['model'] ?? ''}', - style: context.textTheme.labelLarge, - ); - } - - handleClear() { - filter.value = filter.value.copyWith(camera: SearchCameraFilter()); - - cameraCurrentFilterWidget.value = null; - search(); - } - - showFilterBottomSheet( - context: context, - isScrollControlled: true, - isDismissible: true, - child: FilterBottomSheetScaffold( - title: 'search_filter_camera_title'.tr(), - onSearch: search, - onClear: handleClear, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: CameraPicker(onSelect: handleOnSelect, filter: filter.value.camera), - ), - ), - ); - } - - showDatePicker() async { - final firstDate = DateTime(1900); - final lastDate = DateTime.now(); - - final date = await showDateRangePicker( - context: context, - firstDate: firstDate, - lastDate: lastDate, - currentDate: DateTime.now(), - initialDateRange: DateTimeRange( - start: filter.value.date.takenAfter ?? lastDate, - end: filter.value.date.takenBefore ?? lastDate, - ), - helpText: 'search_filter_date_title'.tr(), - cancelText: 'cancel'.tr(), - confirmText: 'select'.tr(), - saveText: 'save'.tr(), - errorFormatText: 'invalid_date_format'.tr(), - errorInvalidText: 'invalid_date'.tr(), - fieldStartHintText: 'start_date'.tr(), - fieldEndHintText: 'end_date'.tr(), - initialEntryMode: DatePickerEntryMode.calendar, - keyboardType: TextInputType.text, - ); - - if (date == null) { - filter.value = filter.value.copyWith(date: SearchDateFilter()); - - dateRangeCurrentFilterWidget.value = null; - unawaited(search()); - return; - } - - filter.value = filter.value.copyWith( - date: SearchDateFilter( - takenAfter: date.start, - takenBefore: date.end.add(const Duration(hours: 23, minutes: 59, seconds: 59)), - ), - ); - - // If date range is less than 24 hours, set the end date to the end of the day - if (date.end.difference(date.start).inHours < 24) { - dateRangeCurrentFilterWidget.value = Text( - DateFormat.yMMMd().format(date.start.toLocal()), - style: context.textTheme.labelLarge, - ); - } else { - dateRangeCurrentFilterWidget.value = Text( - 'search_filter_date_interval'.tr( - namedArgs: { - "start": DateFormat.yMMMd().format(date.start.toLocal()), - "end": DateFormat.yMMMd().format(date.end.toLocal()), - }, - ), - style: context.textTheme.labelLarge, - ); - } - - unawaited(search()); - } - - // MEDIA PICKER - showMediaTypePicker() { - handleOnSelected(AssetType assetType) { - filter.value = filter.value.copyWith(mediaType: assetType); - - mediaTypeCurrentFilterWidget.value = Text( - assetType == AssetType.image - ? 'image'.tr() - : assetType == AssetType.video - ? 'video'.tr() - : 'all'.tr(), - style: context.textTheme.labelLarge, - ); - } - - handleClear() { - filter.value = filter.value.copyWith(mediaType: AssetType.other); - - mediaTypeCurrentFilterWidget.value = null; - search(); - } - - showFilterBottomSheet( - context: context, - child: FilterBottomSheetScaffold( - title: 'search_filter_media_type_title'.tr(), - onSearch: search, - onClear: handleClear, - child: MediaTypePicker(onSelect: handleOnSelected, filter: filter.value.mediaType), - ), - ); - } - - // DISPLAY OPTION - showDisplayOptionPicker() { - handleOnSelect(Map value) { - final filterText = []; - value.forEach((key, value) { - switch (key) { - case DisplayOption.notInAlbum: - filter.value = filter.value.copyWith(display: filter.value.display.copyWith(isNotInAlbum: value)); - if (value) { - filterText.add('search_filter_display_option_not_in_album'.tr()); - } - break; - case DisplayOption.archive: - filter.value = filter.value.copyWith(display: filter.value.display.copyWith(isArchive: value)); - if (value) { - filterText.add('archive'.tr()); - } - break; - case DisplayOption.favorite: - filter.value = filter.value.copyWith(display: filter.value.display.copyWith(isFavorite: value)); - if (value) { - filterText.add('favorite'.tr()); - } - break; - } - }); - - if (filterText.isEmpty) { - displayOptionCurrentFilterWidget.value = null; - return; - } - - displayOptionCurrentFilterWidget.value = Text(filterText.join(', '), style: context.textTheme.labelLarge); - } - - handleClear() { - filter.value = filter.value.copyWith( - display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), - ); - - displayOptionCurrentFilterWidget.value = null; - search(); - } - - showFilterBottomSheet( - context: context, - child: FilterBottomSheetScaffold( - title: 'display_options'.tr(), - onSearch: search, - onClear: handleClear, - child: DisplayOptionPicker(onSelect: handleOnSelect, filter: filter.value.display), - ), - ); - } - - handleTextSubmitted(String value) { - switch (textSearchType.value) { - case TextSearchType.context: - filter.value = filter.value.copyWith(filename: '', context: value, description: '', ocr: ''); - - break; - case TextSearchType.filename: - filter.value = filter.value.copyWith(filename: value, context: '', description: '', ocr: ''); - - break; - case TextSearchType.description: - filter.value = filter.value.copyWith(filename: '', context: '', description: value, ocr: ''); - break; - case TextSearchType.ocr: - filter.value = filter.value.copyWith(filename: '', context: '', description: '', ocr: value); - break; - } - - search(); - } - - IconData getSearchPrefixIcon() => switch (textSearchType.value) { - TextSearchType.context => Icons.image_search_rounded, - TextSearchType.filename => Icons.abc_rounded, - TextSearchType.description => Icons.text_snippet_outlined, - TextSearchType.ocr => Icons.document_scanner_outlined, - }; - - return Scaffold( - resizeToAvoidBottomInset: false, - appBar: AppBar( - automaticallyImplyLeading: true, - actions: [ - Padding( - padding: const EdgeInsets.only(right: 16.0), - child: MenuAnchor( - style: MenuStyle( - elevation: const WidgetStatePropertyAll(1), - shape: WidgetStateProperty.all( - const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(24))), - ), - padding: const WidgetStatePropertyAll(EdgeInsets.all(4)), - ), - builder: (BuildContext context, MenuController controller, Widget? child) { - return IconButton( - onPressed: () { - if (controller.isOpen) { - controller.close(); - } else { - controller.open(); - } - }, - icon: const Icon(Icons.more_vert_rounded), - tooltip: 'show_text_search_menu'.tr(), - ); - }, - menuChildren: [ - MenuItemButton( - child: ListTile( - leading: const Icon(Icons.image_search_rounded), - title: Text( - 'search_by_context'.tr(), - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - color: textSearchType.value == TextSearchType.context ? context.colorScheme.primary : null, - ), - ), - selectedColor: context.colorScheme.primary, - selected: textSearchType.value == TextSearchType.context, - ), - onPressed: () { - textSearchType.value = TextSearchType.context; - searchHintText.value = 'sunrise_on_the_beach'.tr(); - }, - ), - MenuItemButton( - child: ListTile( - leading: const Icon(Icons.abc_rounded), - title: Text( - 'search_filter_filename'.tr(), - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - color: textSearchType.value == TextSearchType.filename ? context.colorScheme.primary : null, - ), - ), - selectedColor: context.colorScheme.primary, - selected: textSearchType.value == TextSearchType.filename, - ), - onPressed: () { - textSearchType.value = TextSearchType.filename; - searchHintText.value = 'file_name_or_extension'.tr(); - }, - ), - MenuItemButton( - child: ListTile( - leading: const Icon(Icons.text_snippet_outlined), - title: Text( - 'search_by_description'.tr(), - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - color: textSearchType.value == TextSearchType.description ? context.colorScheme.primary : null, - ), - ), - selectedColor: context.colorScheme.primary, - selected: textSearchType.value == TextSearchType.description, - ), - onPressed: () { - textSearchType.value = TextSearchType.description; - searchHintText.value = 'search_by_description_example'.tr(); - }, - ), - MenuItemButton( - child: ListTile( - leading: const Icon(Icons.document_scanner_outlined), - title: Text( - 'search_filter_ocr'.tr(), - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - color: textSearchType.value == TextSearchType.ocr ? context.colorScheme.primary : null, - ), - ), - selectedColor: context.colorScheme.primary, - selected: textSearchType.value == TextSearchType.ocr, - ), - onPressed: () { - textSearchType.value = TextSearchType.ocr; - searchHintText.value = 'search_by_ocr_example'.tr(); - }, - ), - ], - ), - ), - ], - title: Container( - decoration: BoxDecoration( - border: Border.all(color: context.colorScheme.onSurface.withAlpha(0), width: 0), - borderRadius: const BorderRadius.all(Radius.circular(24)), - gradient: LinearGradient( - colors: [ - context.colorScheme.primary.withValues(alpha: 0.075), - context.colorScheme.primary.withValues(alpha: 0.09), - context.colorScheme.primary.withValues(alpha: 0.075), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - ), - child: SearchField( - hintText: searchHintText.value, - key: const Key('search_text_field'), - controller: textSearchController, - contentPadding: prefilter != null ? const EdgeInsets.only(left: 24) : const EdgeInsets.all(8), - prefixIcon: prefilter != null ? null : Icon(getSearchPrefixIcon(), color: context.colorScheme.primary), - onSubmitted: handleTextSubmitted, - focusNode: ref.watch(searchInputFocusProvider), - ), - ), - ), - body: Column( - children: [ - Padding( - padding: const EdgeInsets.only(top: 12.0), - child: SizedBox( - height: 50, - child: ListView( - key: const Key('search_filter_chip_list'), - shrinkWrap: true, - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 16), - children: [ - SearchFilterChip( - icon: Icons.people_alt_outlined, - onTap: showPeoplePicker, - label: 'people'.tr(), - currentFilter: peopleCurrentFilterWidget.value, - ), - SearchFilterChip( - icon: Icons.location_on_outlined, - onTap: showLocationPicker, - label: 'search_filter_location'.tr(), - currentFilter: locationCurrentFilterWidget.value, - ), - SearchFilterChip( - icon: Icons.camera_alt_outlined, - onTap: showCameraPicker, - label: 'camera'.tr(), - currentFilter: cameraCurrentFilterWidget.value, - ), - SearchFilterChip( - icon: Icons.date_range_outlined, - onTap: showDatePicker, - label: 'search_filter_date'.tr(), - currentFilter: dateRangeCurrentFilterWidget.value, - ), - SearchFilterChip( - key: const Key('media_type_chip'), - icon: Icons.video_collection_outlined, - onTap: showMediaTypePicker, - label: 'search_filter_media_type'.tr(), - currentFilter: mediaTypeCurrentFilterWidget.value, - ), - SearchFilterChip( - icon: Icons.display_settings_outlined, - onTap: showDisplayOptionPicker, - label: 'search_filter_display_options'.tr(), - currentFilter: displayOptionCurrentFilterWidget.value, - ), - ], - ), - ), - ), - if (isSearching.value) - const Expanded(child: Center(child: CircularProgressIndicator())) - else - SearchResultGrid(onScrollEnd: loadMoreSearchResult, isSearching: isSearching.value), - ], - ), - ); - } -} - -class SearchResultGrid extends StatelessWidget { - final VoidCallback onScrollEnd; - final bool isSearching; - - const SearchResultGrid({super.key, required this.onScrollEnd, this.isSearching = false}); - - @override - Widget build(BuildContext context) { - return Expanded( - child: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: NotificationListener( - onNotification: (notification) { - final isBottomSheetNotification = - notification.context?.findAncestorWidgetOfExactType() != null; - - final metrics = notification.metrics; - final isVerticalScroll = metrics.axis == Axis.vertical; - - if (metrics.pixels >= metrics.maxScrollExtent && isVerticalScroll && !isBottomSheetNotification) { - onScrollEnd(); - } - - return true; - }, - child: MultiselectGrid( - renderListProvider: paginatedSearchRenderListProvider, - archiveEnabled: true, - deleteEnabled: true, - editEnabled: true, - favoriteEnabled: true, - stackEnabled: false, - dragScrollLabelEnabled: false, - emptyIndicator: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: !isSearching ? const SearchEmptyContent() : const SizedBox.shrink(), - ), - ), - ), - ), - ); - } -} - -class SearchEmptyContent extends StatelessWidget { - const SearchEmptyContent({super.key}); - - @override - Widget build(BuildContext context) { - return NotificationListener( - onNotification: (_) => true, - child: ListView( - shrinkWrap: false, - children: [ - const SizedBox(height: 40), - Center( - child: Image.asset( - context.isDarkTheme ? 'assets/polaroid-dark.png' : 'assets/polaroid-light.png', - height: 125, - ), - ), - const SizedBox(height: 16), - Center(child: Text('search_page_search_photos_videos'.tr(), style: context.textTheme.labelLarge)), - const SizedBox(height: 32), - const QuickLinkList(), - ], - ), - ); - } -} - -class QuickLinkList extends StatelessWidget { - const QuickLinkList({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(20)), - border: Border.all(color: context.colorScheme.outline.withAlpha(10), width: 1), - gradient: LinearGradient( - colors: [ - context.colorScheme.primary.withAlpha(10), - context.colorScheme.primary.withAlpha(15), - context.colorScheme.primary.withAlpha(20), - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - child: ListView( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - children: [ - QuickLink( - title: 'recently_taken'.tr(), - icon: Icons.schedule_outlined, - isTop: true, - onTap: () => context.pushRoute(const RecentlyTakenRoute()), - ), - QuickLink( - title: 'videos'.tr(), - icon: Icons.play_circle_outline_rounded, - onTap: () => context.pushRoute(const AllVideosRoute()), - ), - QuickLink( - title: 'favorites'.tr(), - icon: Icons.favorite_border_rounded, - isBottom: true, - onTap: () => context.pushRoute(const FavoritesRoute()), - ), - ], - ), - ); - } -} - -class QuickLink extends StatelessWidget { - final String title; - final IconData icon; - final VoidCallback onTap; - final bool isTop; - final bool isBottom; - - const QuickLink({ - super.key, - required this.title, - required this.icon, - required this.onTap, - this.isTop = false, - this.isBottom = false, - }); - - @override - Widget build(BuildContext context) { - final borderRadius = BorderRadius.only( - topLeft: Radius.circular(isTop ? 20 : 0), - topRight: Radius.circular(isTop ? 20 : 0), - bottomLeft: Radius.circular(isBottom ? 20 : 0), - bottomRight: Radius.circular(isBottom ? 20 : 0), - ); - - return ListTile( - shape: RoundedRectangleBorder(borderRadius: borderRadius), - leading: Icon(icon, size: 26), - title: Text(title, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500)), - onTap: onTap, - ); - } -} diff --git a/mobile/lib/pages/share_intent/share_intent.page.dart b/mobile/lib/pages/share_intent/share_intent.page.dart index 2be51fbfc9..2744b187de 100644 --- a/mobile/lib/pages/share_intent/share_intent.page.dart +++ b/mobile/lib/pages/share_intent/share_intent.page.dart @@ -2,7 +2,6 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; @@ -66,7 +65,7 @@ class ShareIntentPage extends ConsumerWidget { ), leading: IconButton( onPressed: () { - context.navigateTo(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute()); + context.navigateTo(const TabShellRoute()); }, icon: const Icon(Icons.arrow_back), ), diff --git a/mobile/lib/presentation/pages/drift_people_collection.page.dart b/mobile/lib/presentation/pages/drift_people_collection.page.dart index d34ce3e776..32bbd7e60b 100644 --- a/mobile/lib/presentation/pages/drift_people_collection.page.dart +++ b/mobile/lib/presentation/pages/drift_people_collection.page.dart @@ -89,7 +89,7 @@ class _DriftPeopleCollectionPageState extends ConsumerState Function(List edits) applyEdits; + + const DriftEditImagePage({super.key, required this.image, required this.applyEdits}); + + @override + ConsumerState createState() => _DriftEditImagePageState(); +} + +class _DriftEditImagePageState extends ConsumerState with TickerProviderStateMixin { + Future _saveEditedImage() async { + ref.read(editorStateProvider.notifier).setIsEditing(true); + + final editorState = ref.read(editorStateProvider); + final cropParameters = convertRectToCropParameters( + editorState.crop, + editorState.originalWidth, + editorState.originalHeight, + ); + final edits = []; + + if (cropParameters.width != editorState.originalWidth || cropParameters.height != editorState.originalHeight) { + edits.add(CropEdit(cropParameters)); + } + + if (editorState.flipHorizontal) { + edits.add(MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal))); + } + + if (editorState.flipVertical) { + edits.add(MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical))); + } + + final normalizedRotation = (editorState.rotationAngle % 360 + 360) % 360; + if (normalizedRotation != 0) { + edits.add(RotateEdit(RotateParameters(angle: normalizedRotation))); + } + + try { + await widget.applyEdits(edits); + ImmichToast.show(context: context, msg: 'success'.tr(), toastType: ToastType.success); + Navigator.of(context).pop(); + } catch (e) { + ImmichToast.show(context: context, msg: 'error_title'.tr(), toastType: ToastType.error); + } finally { + ref.read(editorStateProvider.notifier).setIsEditing(false); + } + } + + Future _showDiscardChangesDialog() { + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('editor_discard_edits_title'.tr()), + content: Text('editor_discard_edits_prompt'.tr()), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + style: ButtonStyle( + foregroundColor: WidgetStateProperty.all(context.themeData.colorScheme.onSurfaceVariant), + ), + child: Text('cancel'.tr()), + ), + TextButton(onPressed: () => Navigator.of(context).pop(true), child: Text('confirm'.tr())), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + final hasUnsavedEdits = ref.watch(editorStateProvider.select((state) => state.hasUnsavedEdits)); + + return PopScope( + canPop: !hasUnsavedEdits, + onPopInvokedWithResult: (didPop, result) async { + if (didPop) return; + final shouldDiscard = await _showDiscardChangesDialog() ?? false; + if (shouldDiscard && mounted) { + Navigator.of(context).pop(); + } + }, + child: Theme( + data: getThemeData(colorScheme: ref.watch(immichThemeProvider).dark, locale: context.locale), + child: Scaffold( + appBar: AppBar( + backgroundColor: Colors.black, + title: Text("edit".tr()), + leading: ImmichCloseButton(onPressed: () => Navigator.of(context).maybePop()), + actions: [_SaveEditsButton(onSave: _saveEditedImage)], + ), + backgroundColor: Colors.black, + body: SafeArea( + bottom: false, + child: Column( + children: [ + Expanded(child: _EditorPreview(image: widget.image)), + AnimatedSize( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + alignment: Alignment.bottomCenter, + clipBehavior: Clip.none, + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: ref.watch(immichThemeProvider).dark.surface, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: const Column( + mainAxisSize: MainAxisSize.min, + children: [ + _TransformControls(), + Padding( + padding: EdgeInsets.only(bottom: 36, left: 24, right: 24), + child: Row(children: [Spacer(), _ResetEditsButton()]), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} + +class _AspectRatioButton extends StatelessWidget { + final AspectRatioPreset ratio; + final bool isSelected; + final VoidCallback onPressed; + + const _AspectRatioButton({required this.ratio, required this.isSelected, required this.onPressed}); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.max, + children: [ + IconButton( + iconSize: 36, + icon: Transform.rotate( + angle: ratio.iconRotated ? pi / 2 : 0, + child: Icon(ratio.icon, color: isSelected ? context.primaryColor : context.themeData.iconTheme.color), + ), + onPressed: onPressed, + ), + Text(ratio.label, style: context.textTheme.displayMedium), + ], + ); + } +} + +class _AspectRatioSelector extends ConsumerWidget { + const _AspectRatioSelector(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final editorState = ref.watch(editorStateProvider); + final editorNotifier = ref.read(editorStateProvider.notifier); + + // the whole crop view is rotated, so we need to swap the aspect ratio when the rotation is 90 or 270 degrees + double? selectedAspectRatio = editorState.aspectRatio; + if (editorState.rotationAngle % 180 != 0 && selectedAspectRatio != null) { + selectedAspectRatio = 1 / selectedAspectRatio; + } + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: AspectRatioPreset.values.map((entry) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: _AspectRatioButton( + ratio: entry, + isSelected: selectedAspectRatio == entry.ratio, + onPressed: () => editorNotifier.setAspectRatio(entry.ratio), + ), + ); + }).toList(), + ), + ); + } +} + +class _TransformControls extends ConsumerWidget { + const _TransformControls(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final editorNotifier = ref.read(editorStateProvider.notifier); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(left: 20, right: 20, top: 20, bottom: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + ImmichIconButton( + icon: Icons.rotate_left, + variant: ImmichVariant.ghost, + color: ImmichColor.secondary, + onPressed: editorNotifier.rotateCCW, + ), + const SizedBox(width: 8), + ImmichIconButton( + icon: Icons.rotate_right, + variant: ImmichVariant.ghost, + color: ImmichColor.secondary, + onPressed: editorNotifier.rotateCW, + ), + ], + ), + Row( + children: [ + ImmichIconButton( + icon: Icons.flip, + variant: ImmichVariant.ghost, + color: ImmichColor.secondary, + onPressed: editorNotifier.flipHorizontally, + ), + const SizedBox(width: 8), + Transform.rotate( + angle: pi / 2, + child: ImmichIconButton( + icon: Icons.flip, + variant: ImmichVariant.ghost, + color: ImmichColor.secondary, + onPressed: editorNotifier.flipVertically, + ), + ), + ], + ), + ], + ), + ), + const _AspectRatioSelector(), + const SizedBox(height: 32), + ], + ); + } +} + +class _SaveEditsButton extends ConsumerWidget { + final VoidCallback onSave; + + const _SaveEditsButton({required this.onSave}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isApplyingEdits = ref.watch(editorStateProvider.select((state) => state.isApplyingEdits)); + final hasUnsavedEdits = ref.watch(editorStateProvider.select((state) => state.hasUnsavedEdits)); + + return isApplyingEdits + ? const Padding( + padding: EdgeInsets.all(8.0), + child: SizedBox(width: 28, height: 28, child: CircularProgressIndicator(strokeWidth: 2.5)), + ) + : ImmichIconButton( + icon: Icons.done_rounded, + color: ImmichColor.primary, + variant: ImmichVariant.ghost, + disabled: !hasUnsavedEdits, + onPressed: onSave, + ); + } +} + +class _ResetEditsButton extends ConsumerWidget { + const _ResetEditsButton(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final editorState = ref.watch(editorStateProvider); + final editorNotifier = ref.read(editorStateProvider.notifier); + + return ImmichTextButton( + labelText: 'reset'.tr(), + onPressed: editorNotifier.resetEdits, + variant: ImmichVariant.ghost, + expanded: false, + disabled: !editorState.hasEdits || editorState.isApplyingEdits, + ); + } +} + +class _EditorPreview extends ConsumerStatefulWidget { + final Image image; + + const _EditorPreview({required this.image}); + + @override + ConsumerState<_EditorPreview> createState() => _EditorPreviewState(); +} + +class _EditorPreviewState extends ConsumerState<_EditorPreview> with TickerProviderStateMixin { + late final CropController cropController; + + @override + void initState() { + super.initState(); + + cropController = CropController(); + cropController.crop = ref.read(editorStateProvider.select((state) => state.crop)); + cropController.addListener(onCrop); + } + + void onCrop() { + if (!mounted || cropController.crop == ref.read(editorStateProvider).crop) { + return; + } + + ref.read(editorStateProvider.notifier).setCrop(cropController.crop); + } + + @override + void dispose() { + cropController.removeListener(onCrop); + cropController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final editorState = ref.watch(editorStateProvider); + final editorNotifier = ref.read(editorStateProvider.notifier); + + ref.listen(editorStateProvider, (_, current) { + cropController.aspectRatio = current.aspectRatio; + + if (cropController.crop != current.crop) { + cropController.crop = current.crop; + } + }); + + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + // Calculate the bounding box size needed for the rotated container + final baseWidth = constraints.maxWidth * 0.9; + final baseHeight = constraints.maxHeight * 0.95; + + return Center( + child: AnimatedRotation( + turns: editorState.rotationAngle / 360, + duration: editorState.animationDuration, + curve: Curves.easeInOut, + onEnd: editorNotifier.normalizeRotation, + child: Transform( + alignment: Alignment.center, + transform: Matrix4.identity() + ..scaleByDouble( + editorState.flipHorizontal ? -1.0 : 1.0, + editorState.flipVertical ? -1.0 : 1.0, + 1.0, + 1.0, + ), + child: Container( + padding: const EdgeInsets.all(10), + width: (editorState.rotationAngle % 180 == 0) ? baseWidth : baseHeight, + height: (editorState.rotationAngle % 180 == 0) ? baseHeight : baseWidth, + child: CropImage(controller: cropController, image: widget.image, gridColor: Colors.white), + ), + ), + ), + ); + }, + ); + } +} diff --git a/mobile/lib/presentation/pages/edit/editor.provider.dart b/mobile/lib/presentation/pages/edit/editor.provider.dart new file mode 100644 index 0000000000..21b5268912 --- /dev/null +++ b/mobile/lib/presentation/pages/edit/editor.provider.dart @@ -0,0 +1,210 @@ +import 'package:flutter/services.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart'; +import 'package:immich_mobile/domain/models/exif.model.dart'; +import 'package:immich_mobile/utils/editor.utils.dart'; + +final editorStateProvider = NotifierProvider(EditorProvider.new); + +class EditorProvider extends Notifier { + @override + EditorState build() { + return const EditorState(); + } + + void clear() { + state = const EditorState(); + } + + void init(List edits, ExifInfo exifInfo) { + clear(); + + final existingCrop = edits.whereType().firstOrNull; + + final originalWidth = exifInfo.isFlipped ? exifInfo.height : exifInfo.width; + final originalHeight = exifInfo.isFlipped ? exifInfo.width : exifInfo.height; + + Rect crop = existingCrop != null && originalWidth != null && originalHeight != null + ? convertCropParametersToRect(existingCrop.parameters, originalWidth, originalHeight) + : const Rect.fromLTRB(0, 0, 1, 1); + + final transform = normalizeTransformEdits(edits); + + state = state.copyWith( + originalWidth: originalWidth, + originalHeight: originalHeight, + crop: crop, + flipHorizontal: transform.mirrorHorizontal, + flipVertical: transform.mirrorVertical, + ); + + _animateRotation(transform.rotation.toInt(), duration: Duration.zero); + } + + void _animateRotation(int angle, {Duration duration = const Duration(milliseconds: 300)}) { + state = state.copyWith(rotationAngle: angle, animationDuration: duration); + } + + void normalizeRotation() { + final normalizedAngle = ((state.rotationAngle % 360) + 360) % 360; + if (normalizedAngle != state.rotationAngle) { + state = state.copyWith(rotationAngle: normalizedAngle, animationDuration: Duration.zero); + } + } + + void setIsEditing(bool isApplyingEdits) { + state = state.copyWith(isApplyingEdits: isApplyingEdits); + } + + void setCrop(Rect crop) { + state = state.copyWith(crop: crop, hasUnsavedEdits: true); + } + + void setAspectRatio(double? aspectRatio) { + if (aspectRatio != null && state.rotationAngle % 180 != 0) { + // When rotated 90 or 270 degrees, swap width and height for aspect ratio calculations + aspectRatio = 1 / aspectRatio; + } + + state = state.copyWith(aspectRatio: aspectRatio); + } + + void resetEdits() { + _animateRotation(0); + + state = state.copyWith( + flipHorizontal: false, + flipVertical: false, + crop: const Rect.fromLTRB(0, 0, 1, 1), + aspectRatio: null, + hasUnsavedEdits: true, + ); + } + + void rotateCCW() { + _animateRotation(state.rotationAngle - 90); + state = state.copyWith(hasUnsavedEdits: true); + } + + void rotateCW() { + _animateRotation(state.rotationAngle + 90); + state = state.copyWith(hasUnsavedEdits: true); + } + + void flipHorizontally() { + if (state.rotationAngle % 180 != 0) { + // When rotated 90 or 270 degrees, flipping horizontally is equivalent to flipping vertically + state = state.copyWith(flipVertical: !state.flipVertical, hasUnsavedEdits: true); + } else { + state = state.copyWith(flipHorizontal: !state.flipHorizontal, hasUnsavedEdits: true); + } + } + + void flipVertically() { + if (state.rotationAngle % 180 != 0) { + // When rotated 90 or 270 degrees, flipping vertically is equivalent to flipping horizontally + state = state.copyWith(flipHorizontal: !state.flipHorizontal, hasUnsavedEdits: true); + } else { + state = state.copyWith(flipVertical: !state.flipVertical, hasUnsavedEdits: true); + } + } +} + +class EditorState { + final bool isApplyingEdits; + + final int rotationAngle; + final bool flipHorizontal; + final bool flipVertical; + final Rect crop; + final double? aspectRatio; + + final int originalWidth; + final int originalHeight; + + final Duration animationDuration; + + final bool hasUnsavedEdits; + + const EditorState({ + bool? isApplyingEdits, + int? rotationAngle, + bool? flipHorizontal, + bool? flipVertical, + Rect? crop, + this.aspectRatio, + int? originalWidth, + int? originalHeight, + Duration? animationDuration, + bool? hasUnsavedEdits, + }) : isApplyingEdits = isApplyingEdits ?? false, + rotationAngle = rotationAngle ?? 0, + flipHorizontal = flipHorizontal ?? false, + flipVertical = flipVertical ?? false, + animationDuration = animationDuration ?? Duration.zero, + originalWidth = originalWidth ?? 0, + originalHeight = originalHeight ?? 0, + crop = crop ?? const Rect.fromLTRB(0, 0, 1, 1), + hasUnsavedEdits = hasUnsavedEdits ?? false; + + EditorState copyWith({ + bool? isApplyingEdits, + int? rotationAngle, + bool? flipHorizontal, + bool? flipVertical, + double? aspectRatio = double.infinity, + int? originalWidth, + int? originalHeight, + Duration? animationDuration, + Rect? crop, + bool? hasUnsavedEdits, + }) { + return EditorState( + isApplyingEdits: isApplyingEdits ?? this.isApplyingEdits, + rotationAngle: rotationAngle ?? this.rotationAngle, + flipHorizontal: flipHorizontal ?? this.flipHorizontal, + flipVertical: flipVertical ?? this.flipVertical, + aspectRatio: aspectRatio == double.infinity ? this.aspectRatio : aspectRatio, + animationDuration: animationDuration ?? this.animationDuration, + originalWidth: originalWidth ?? this.originalWidth, + originalHeight: originalHeight ?? this.originalHeight, + crop: crop ?? this.crop, + hasUnsavedEdits: hasUnsavedEdits ?? this.hasUnsavedEdits, + ); + } + + bool get hasEdits { + return rotationAngle != 0 || flipHorizontal || flipVertical || crop != const Rect.fromLTRB(0, 0, 1, 1); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is EditorState && + other.isApplyingEdits == isApplyingEdits && + other.rotationAngle == rotationAngle && + other.flipHorizontal == flipHorizontal && + other.flipVertical == flipVertical && + other.crop == crop && + other.aspectRatio == aspectRatio && + other.originalWidth == originalWidth && + other.originalHeight == originalHeight && + other.animationDuration == animationDuration && + other.hasUnsavedEdits == hasUnsavedEdits; + } + + @override + int get hashCode { + return isApplyingEdits.hashCode ^ + rotationAngle.hashCode ^ + flipHorizontal.hashCode ^ + flipVertical.hashCode ^ + crop.hashCode ^ + aspectRatio.hashCode ^ + originalWidth.hashCode ^ + originalHeight.hashCode ^ + animationDuration.hashCode ^ + hasUnsavedEdits.hashCode; + } +} diff --git a/mobile/lib/presentation/pages/editing/drift_crop.page.dart b/mobile/lib/presentation/pages/editing/drift_crop.page.dart deleted file mode 100644 index a213e4c640..0000000000 --- a/mobile/lib/presentation/pages/editing/drift_crop.page.dart +++ /dev/null @@ -1,179 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:crop_image/crop_image.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/hooks/crop_controller_hook.dart'; -import 'package:immich_ui/immich_ui.dart'; - -/// A widget for cropping an image. -/// This widget uses [HookWidget] to manage its lifecycle and state. It allows -/// users to crop an image and then navigate to the [EditImagePage] with the -/// cropped image. - -@RoutePage() -class DriftCropImagePage extends HookWidget { - final Image image; - final BaseAsset asset; - const DriftCropImagePage({super.key, required this.image, required this.asset}); - - @override - Widget build(BuildContext context) { - final cropController = useCropController(); - final aspectRatio = useState(null); - - return Scaffold( - appBar: AppBar( - backgroundColor: context.scaffoldBackgroundColor, - title: Text("crop".tr()), - leading: const ImmichCloseButton(), - actions: [ - ImmichIconButton( - icon: Icons.done_rounded, - color: ImmichColor.primary, - variant: ImmichVariant.ghost, - onPressed: () async { - final croppedImage = await cropController.croppedImage(); - unawaited(context.pushRoute(DriftEditImageRoute(asset: asset, image: croppedImage, isEdited: true))); - }, - ), - ], - ), - backgroundColor: context.scaffoldBackgroundColor, - body: SafeArea( - child: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return Column( - children: [ - Container( - padding: const EdgeInsets.only(top: 20), - width: constraints.maxWidth * 0.9, - height: constraints.maxHeight * 0.6, - child: CropImage(controller: cropController, image: image, gridColor: Colors.white), - ), - Expanded( - child: Container( - width: double.infinity, - decoration: BoxDecoration( - color: context.scaffoldBackgroundColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only(left: 20, right: 20, bottom: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ImmichIconButton( - icon: Icons.rotate_left, - variant: ImmichVariant.ghost, - color: ImmichColor.secondary, - onPressed: () => cropController.rotateLeft(), - ), - ImmichIconButton( - icon: Icons.rotate_right, - variant: ImmichVariant.ghost, - color: ImmichColor.secondary, - onPressed: () => cropController.rotateRight(), - ), - ], - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: null, - label: 'Free', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 1.0, - label: '1:1', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 16.0 / 9.0, - label: '16:9', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 3.0 / 2.0, - label: '3:2', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 7.0 / 5.0, - label: '7:5', - ), - ], - ), - ], - ), - ), - ), - ), - ], - ); - }, - ), - ), - ); - } -} - -class _AspectRatioButton extends StatelessWidget { - final CropController cropController; - final ValueNotifier aspectRatio; - final double? ratio; - final String label; - - const _AspectRatioButton({ - required this.cropController, - required this.aspectRatio, - required this.ratio, - required this.label, - }); - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: Icon(switch (label) { - 'Free' => Icons.crop_free_rounded, - '1:1' => Icons.crop_square_rounded, - '16:9' => Icons.crop_16_9_rounded, - '3:2' => Icons.crop_3_2_rounded, - '7:5' => Icons.crop_7_5_rounded, - _ => Icons.crop_free_rounded, - }, color: aspectRatio.value == ratio ? context.primaryColor : context.themeData.iconTheme.color), - onPressed: () { - cropController.crop = const Rect.fromLTRB(0.1, 0.1, 0.9, 0.9); - aspectRatio.value = ratio; - cropController.aspectRatio = ratio; - }, - ), - Text(label, style: context.textTheme.displayMedium), - ], - ); - } -} diff --git a/mobile/lib/presentation/pages/editing/drift_edit.page.dart b/mobile/lib/presentation/pages/editing/drift_edit.page.dart deleted file mode 100644 index 6d4ea4d3a6..0000000000 --- a/mobile/lib/presentation/pages/editing/drift_edit.page.dart +++ /dev/null @@ -1,153 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/foreground_upload.service.dart'; -import 'package:immich_mobile/utils/image_converter.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as p; - -/// A stateless widget that provides functionality for editing an image. -/// -/// This widget allows users to edit an image provided either as an [Asset] or -/// directly as an [Image]. It ensures that exactly one of these is provided. -/// -/// It also includes a conversion method to convert an [Image] to a [Uint8List] to save the image on the user's phone -/// They automatically navigate to the [HomePage] with the edited image saved and they eventually get backed up to the server. -@immutable -@RoutePage() -class DriftEditImagePage extends ConsumerWidget { - final BaseAsset asset; - final Image image; - final bool isEdited; - - const DriftEditImagePage({super.key, required this.asset, required this.image, required this.isEdited}); - - void _exitEditing(BuildContext context) { - // this assumes that the only way to get to this page is from the AssetViewerRoute - context.navigator.popUntil((route) => route.data?.name == AssetViewerRoute.name); - } - - Future _saveEditedImage(BuildContext context, BaseAsset asset, Image image, WidgetRef ref) async { - try { - final Uint8List imageData = await imageToUint8List(image); - LocalAsset? localAsset; - - try { - localAsset = await ref - .read(fileMediaRepositoryProvider) - .saveLocalAsset(imageData, title: "${p.withoutExtension(asset.name)}_edited.jpg"); - } on PlatformException catch (e) { - // OS might not return the saved image back, so we handle that gracefully - // This can happen if app does not have full library access - Logger("SaveEditedImage").warning("Failed to retrieve the saved image back from OS", e); - } - - unawaited(ref.read(backgroundSyncProvider).syncLocal(full: true)); - _exitEditing(context); - ImmichToast.show(durationInSecond: 3, context: context, msg: 'Image Saved!'); - - if (localAsset == null) { - return; - } - - await ref.read(foregroundUploadServiceProvider).uploadManual([localAsset]); - } catch (e) { - ImmichToast.show( - durationInSecond: 6, - context: context, - msg: "error_saving_image".tr(namedArgs: {'error': e.toString()}), - ); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Scaffold( - appBar: AppBar( - title: Text("edit".tr()), - backgroundColor: context.scaffoldBackgroundColor, - leading: IconButton( - icon: Icon(Icons.close_rounded, color: context.primaryColor, size: 24), - onPressed: () => _exitEditing(context), - ), - actions: [ - TextButton( - onPressed: isEdited ? () => _saveEditedImage(context, asset, image, ref) : null, - child: Text("save_to_gallery".tr(), style: TextStyle(color: isEdited ? context.primaryColor : Colors.grey)), - ), - ], - ), - backgroundColor: context.scaffoldBackgroundColor, - body: Center( - child: ConstrainedBox( - constraints: BoxConstraints(maxHeight: context.height * 0.7, maxWidth: context.width * 0.9), - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(7)), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.2), - spreadRadius: 2, - blurRadius: 10, - offset: const Offset(0, 3), - ), - ], - ), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(7)), - child: Image(image: image.image, fit: BoxFit.contain), - ), - ), - ), - ), - bottomNavigationBar: Container( - height: 70, - margin: const EdgeInsets.only(bottom: 60, right: 10, left: 10, top: 10), - decoration: BoxDecoration( - color: context.scaffoldBackgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(30)), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: Icon(Icons.crop_rotate_rounded, color: context.themeData.iconTheme.color, size: 25), - onPressed: () { - context.pushRoute(DriftCropImageRoute(asset: asset, image: image)); - }, - ), - Text("crop".tr(), style: context.textTheme.displayMedium), - ], - ), - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: Icon(Icons.filter, color: context.themeData.iconTheme.color, size: 25), - onPressed: () { - context.pushRoute(DriftFilterImageRoute(asset: asset, image: image)); - }, - ), - Text("filter".tr(), style: context.textTheme.displayMedium), - ], - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/presentation/pages/editing/drift_filter.page.dart b/mobile/lib/presentation/pages/editing/drift_filter.page.dart deleted file mode 100644 index 8198a41bbe..0000000000 --- a/mobile/lib/presentation/pages/editing/drift_filter.page.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'dart:async'; -import 'dart:ui' as ui; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/constants/filters.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/routing/router.dart'; - -/// A widget for filtering an image. -/// This widget uses [HookWidget] to manage its lifecycle and state. It allows -/// users to add filters to an image and then navigate to the [EditImagePage] with the -/// final composition.' -@RoutePage() -class DriftFilterImagePage extends HookWidget { - final Image image; - final BaseAsset asset; - - const DriftFilterImagePage({super.key, required this.image, required this.asset}); - - @override - Widget build(BuildContext context) { - final colorFilter = useState(filters[0]); - final selectedFilterIndex = useState(0); - - Future createFilteredImage(ui.Image inputImage, ColorFilter filter) { - final completer = Completer(); - final size = Size(inputImage.width.toDouble(), inputImage.height.toDouble()); - final recorder = ui.PictureRecorder(); - final canvas = Canvas(recorder); - - final paint = Paint()..colorFilter = filter; - canvas.drawImage(inputImage, Offset.zero, paint); - - recorder.endRecording().toImage(size.width.round(), size.height.round()).then((image) { - completer.complete(image); - }); - - return completer.future; - } - - void applyFilter(ColorFilter filter, int index) { - colorFilter.value = filter; - selectedFilterIndex.value = index; - } - - Future applyFilterAndConvert(ColorFilter filter) async { - final completer = Completer(); - image.image - .resolve(ImageConfiguration.empty) - .addListener( - ImageStreamListener((ImageInfo info, bool _) { - completer.complete(info.image); - }), - ); - final uiImage = await completer.future; - - final filteredUiImage = await createFilteredImage(uiImage, filter); - final byteData = await filteredUiImage.toByteData(format: ui.ImageByteFormat.png); - final pngBytes = byteData!.buffer.asUint8List(); - - return Image.memory(pngBytes, fit: BoxFit.contain); - } - - return Scaffold( - appBar: AppBar( - backgroundColor: context.scaffoldBackgroundColor, - title: Text("filter".tr()), - leading: CloseButton(color: context.primaryColor), - actions: [ - IconButton( - icon: Icon(Icons.done_rounded, color: context.primaryColor, size: 24), - onPressed: () async { - final filteredImage = await applyFilterAndConvert(colorFilter.value); - unawaited(context.pushRoute(DriftEditImageRoute(asset: asset, image: filteredImage, isEdited: true))); - }, - ), - ], - ), - backgroundColor: context.scaffoldBackgroundColor, - body: Column( - children: [ - SizedBox( - height: context.height * 0.7, - child: Center( - child: ColorFiltered(colorFilter: colorFilter.value, child: image), - ), - ), - SizedBox( - height: 120, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: filters.length, - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: _FilterButton( - image: image, - label: filterNames[index], - filter: filters[index], - isSelected: selectedFilterIndex.value == index, - onTap: () => applyFilter(filters[index], index), - ), - ); - }, - ), - ), - ], - ), - ); - } -} - -class _FilterButton extends StatelessWidget { - final Image image; - final String label; - final ColorFilter filter; - final bool isSelected; - final VoidCallback onTap; - - const _FilterButton({ - required this.image, - required this.label, - required this.filter, - required this.isSelected, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - GestureDetector( - onTap: onTap, - child: Container( - width: 80, - height: 80, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(10)), - border: isSelected ? Border.all(color: context.primaryColor, width: 3) : null, - ), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(10)), - child: ColorFiltered( - colorFilter: filter, - child: FittedBox(fit: BoxFit.cover, child: image), - ), - ), - ), - ), - const SizedBox(height: 10), - Text(label, style: context.themeData.textTheme.bodyMedium), - ], - ); - } -} diff --git a/mobile/lib/presentation/pages/search/drift_search.page.dart b/mobile/lib/presentation/pages/search/drift_search.page.dart index 7e47a742ae..3ba4cf3497 100644 --- a/mobile/lib/presentation/pages/search/drift_search.page.dart +++ b/mobile/lib/presentation/pages/search/drift_search.page.dart @@ -6,11 +6,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; import 'package:immich_mobile/domain/models/tag.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; diff --git a/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart index cad74ce658..564b02d884 100644 --- a/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart @@ -1,10 +1,17 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/pages/edit/editor.provider.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/routing/router.dart'; class EditImageActionButton extends ConsumerWidget { @@ -14,13 +21,33 @@ class EditImageActionButton extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final currentAsset = ref.watch(assetViewerProvider.select((s) => s.currentAsset)); - onPress() { - if (currentAsset == null) { + Future editImage(List edits) async { + if (currentAsset == null || currentAsset.remoteId == null) { return; } - final image = Image(image: getFullImageProvider(currentAsset)); - context.pushRoute(DriftEditImageRoute(asset: currentAsset, image: image, isEdited: false)); + await ref.read(actionProvider.notifier).applyEdits(ActionSource.viewer, edits); + } + + Future onPress() async { + if (currentAsset == null || currentAsset.remoteId == null) { + return; + } + + final imageProvider = getFullImageProvider(currentAsset, edited: false); + + final image = Image(image: imageProvider); + final (edits, exifInfo) = await ( + ref.read(remoteAssetRepositoryProvider).getAssetEdits(currentAsset.remoteId!), + ref.read(remoteAssetRepositoryProvider).getExif(currentAsset.remoteId!), + ).wait; + + if (exifInfo == null) { + return; + } + + ref.read(editorStateProvider.notifier).init(edits, exifInfo); + await context.pushRoute(DriftEditImageRoute(image: image, applyEdits: editImage)); } return BaseActionButton( diff --git a/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart index bb42140d0a..0acbbce613 100644 --- a/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart'; diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index 0c039847a4..e5b4607619 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -17,9 +17,9 @@ import 'package:immich_mobile/presentation/widgets/album/new_album_name_modal.wi import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart index b51960bb05..cf7ffbd234 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart @@ -3,16 +3,18 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_image_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/utils/semver.dart'; import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart'; class ViewerBottomBar extends ConsumerWidget { @@ -30,6 +32,7 @@ class ViewerBottomBar extends ConsumerWidget { final isOwner = asset is RemoteAsset && asset.ownerId == user?.id; final showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails)); final isInLockedView = ref.watch(inLockedViewProvider); + final serverInfo = ref.watch(serverInfoProvider); final originalTheme = context.themeData; @@ -38,7 +41,9 @@ class ViewerBottomBar extends ConsumerWidget { if (!isInLockedView) ...[ if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer), - if (asset.type == AssetType.image) const EditImageActionButton(), + // edit sync was added in 2.6.0 + if (asset.isEditable && serverInfo.serverVersion >= const SemVer(major: 2, minor: 6, patch: 0)) + const EditImageActionButton(), if (asset.hasRemote) AddActionButton(originalTheme: originalTheme), if (isOwner) ...[ diff --git a/mobile/lib/presentation/widgets/images/image_provider.dart b/mobile/lib/presentation/widgets/images/image_provider.dart index 47ebd37014..ea416d9d71 100644 --- a/mobile/lib/presentation/widgets/images/image_provider.dart +++ b/mobile/lib/presentation/widgets/images/image_provider.dart @@ -147,7 +147,7 @@ mixin CancellableImageProviderMixin on CancellableImageProvide } } -ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080, 1920)}) { +ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080, 1920), bool edited = true}) { // Create new provider and cache it final ImageProvider provider; if (_shouldUseLocalAsset(asset)) { @@ -170,13 +170,14 @@ ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080 thumbhash: thumbhash, assetType: asset.type, isAnimated: asset.isAnimatedImage, + edited: edited, ); } return provider; } -ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnailResolution}) { +ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnailResolution, bool edited = true}) { if (_shouldUseLocalAsset(asset)) { final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!; return LocalThumbProvider(id: id, size: size, assetType: asset.type); @@ -184,7 +185,7 @@ ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnai final assetId = asset is RemoteAsset ? asset.id : (asset as LocalAsset).remoteId; final thumbhash = asset is RemoteAsset ? asset.thumbHash ?? "" : ""; - return assetId != null ? RemoteImageProvider.thumbnail(assetId: assetId, thumbhash: thumbhash) : null; + return assetId != null ? RemoteImageProvider.thumbnail(assetId: assetId, thumbhash: thumbhash, edited: edited) : null; } bool _shouldUseLocalAsset(BaseAsset asset) => diff --git a/mobile/lib/presentation/widgets/images/remote_image_provider.dart b/mobile/lib/presentation/widgets/images/remote_image_provider.dart index d9cc053ccf..f7fc5868c3 100644 --- a/mobile/lib/presentation/widgets/images/remote_image_provider.dart +++ b/mobile/lib/presentation/widgets/images/remote_image_provider.dart @@ -13,11 +13,12 @@ import 'package:openapi/api.dart'; class RemoteImageProvider extends CancellableImageProvider with CancellableImageProviderMixin { final String url; + final bool edited; - RemoteImageProvider({required this.url}); + RemoteImageProvider({required this.url, this.edited = true}); - RemoteImageProvider.thumbnail({required String assetId, required String thumbhash}) - : url = getThumbnailUrlForRemoteId(assetId, thumbhash: thumbhash); + RemoteImageProvider.thumbnail({required String assetId, required String thumbhash, this.edited = true}) + : url = getThumbnailUrlForRemoteId(assetId, thumbhash: thumbhash, edited: edited); @override Future obtainKey(ImageConfiguration configuration) { @@ -45,13 +46,13 @@ class RemoteImageProvider extends CancellableImageProvider bool operator ==(Object other) { if (identical(this, other)) return true; if (other is RemoteImageProvider) { - return url == other.url; + return url == other.url && edited == other.edited; } return false; } @override - int get hashCode => url.hashCode; + int get hashCode => url.hashCode ^ edited.hashCode; } class RemoteFullImageProvider extends CancellableImageProvider @@ -60,12 +61,14 @@ class RemoteFullImageProvider extends CancellableImageProvider [ DiagnosticsProperty('Image provider', this), DiagnosticsProperty('Asset Id', key.assetId), @@ -109,7 +114,12 @@ class RemoteFullImageProvider extends CancellableImageProvider assetId.hashCode ^ thumbhash.hashCode ^ isAnimated.hashCode; + int get hashCode => assetId.hashCode ^ thumbhash.hashCode ^ isAnimated.hashCode ^ edited.hashCode; } diff --git a/mobile/lib/presentation/widgets/map/map.widget.dart b/mobile/lib/presentation/widgets/map/map.widget.dart index 72f4e8bda6..3f406dd551 100644 --- a/mobile/lib/presentation/widgets/map/map.widget.dart +++ b/mobile/lib/presentation/widgets/map/map.widget.dart @@ -132,7 +132,7 @@ class _DriftMapState extends ConsumerState { // If we continue to update bounds, the map-scoped timeline service gets recreated and the previous one disposed, // which can invalidate the TimelineService instance that was passed into AssetViewerRoute (causing "loading forever"). final currentRoute = ref.read(currentRouteNameProvider); - if (currentRoute == AssetViewerRoute.name || currentRoute == GalleryViewerRoute.name) { + if (currentRoute == AssetViewerRoute.name) { return; } diff --git a/mobile/lib/providers/album/album.provider.dart b/mobile/lib/providers/album/album.provider.dart deleted file mode 100644 index 35634d77c8..0000000000 --- a/mobile/lib/providers/album/album.provider.dart +++ /dev/null @@ -1,151 +0,0 @@ -import 'dart:async'; - -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/models/albums/album_search.model.dart'; -import 'package:immich_mobile/services/album.service.dart'; - -final isRefreshingRemoteAlbumProvider = StateProvider((ref) => false); - -class AlbumNotifier extends StateNotifier> { - AlbumNotifier(this.albumService, this.ref) : super([]) { - albumService.getAllRemoteAlbums().then((value) { - if (mounted) { - state = value; - } - }); - - _streamSub = albumService.watchRemoteAlbums().listen((data) => state = data); - } - - final AlbumService albumService; - final Ref ref; - late final StreamSubscription> _streamSub; - - Future refreshRemoteAlbums() async { - ref.read(isRefreshingRemoteAlbumProvider.notifier).state = true; - await albumService.refreshRemoteAlbums(); - ref.read(isRefreshingRemoteAlbumProvider.notifier).state = false; - } - - Future refreshDeviceAlbums() => albumService.refreshDeviceAlbums(); - - Future deleteAlbum(Album album) => albumService.deleteAlbum(album); - - Future createAlbum(String albumTitle, Set assets) => albumService.createAlbum(albumTitle, assets, []); - - Future getAlbumByName(String albumName, {bool? remote, bool? shared, bool? owner}) => - albumService.getAlbumByName(albumName, remote: remote, shared: shared, owner: owner); - - /// Create an album on the server with the same name as the selected album for backup - /// First this will check if the album already exists on the server with name - /// If it does not exist, it will create the album on the server - Future createSyncAlbum(String albumName) async { - final album = await getAlbumByName(albumName, remote: true, owner: true); - if (album != null) { - return; - } - - await createAlbum(albumName, {}); - } - - Future leaveAlbum(Album album) async { - var res = await albumService.leaveAlbum(album); - - if (res) { - await deleteAlbum(album); - return true; - } else { - return false; - } - } - - void searchAlbums(String searchTerm, QuickFilterMode filterMode) async { - state = await albumService.search(searchTerm, filterMode); - } - - Future addUsers(Album album, List userIds) async { - await albumService.addUsers(album, userIds); - } - - Future removeUser(Album album, UserDto user) async { - final isRemoved = await albumService.removeUser(album, user); - - if (isRemoved && album.sharedUsers.isEmpty) { - state = state.where((element) => element.id != album.id).toList(); - } - - return isRemoved; - } - - Future addAssets(Album album, Iterable assets) async { - await albumService.addAssets(album, assets); - } - - Future removeAsset(Album album, Iterable assets) async { - return await albumService.removeAsset(album, assets); - } - - Future setActivitystatus(Album album, bool enabled) { - return albumService.setActivityStatus(album, enabled); - } - - Future toggleSortOrder(Album album) { - final order = album.sortOrder == SortOrder.asc ? SortOrder.desc : SortOrder.asc; - - return albumService.updateSortOrder(album, order); - } - - @override - void dispose() { - _streamSub.cancel(); - super.dispose(); - } -} - -final albumProvider = StateNotifierProvider.autoDispose>((ref) { - return AlbumNotifier(ref.watch(albumServiceProvider), ref); -}); - -final albumWatcher = StreamProvider.autoDispose.family((ref, id) async* { - final albumService = ref.watch(albumServiceProvider); - - final album = await albumService.getAlbumById(id); - if (album != null) { - yield album; - } - - await for (final album in albumService.watchAlbum(id)) { - if (album != null) { - yield album; - } - } -}); - -class LocalAlbumsNotifier extends StateNotifier> { - LocalAlbumsNotifier(this.albumService) : super([]) { - albumService.getAllLocalAlbums().then((value) { - if (mounted) { - state = value; - } - }); - - _streamSub = albumService.watchLocalAlbums().listen((data) => state = data); - } - - final AlbumService albumService; - late final StreamSubscription> _streamSub; - - @override - void dispose() { - _streamSub.cancel(); - super.dispose(); - } -} - -final localAlbumsProvider = StateNotifierProvider.autoDispose>((ref) { - return LocalAlbumsNotifier(ref.watch(albumServiceProvider)); -}); diff --git a/mobile/lib/providers/album/album_sort_by_options.provider.dart b/mobile/lib/providers/album/album_sort_by_options.provider.dart index c969dbd37d..ec4ae71d03 100644 --- a/mobile/lib/providers/album/album_sort_by_options.provider.dart +++ b/mobile/lib/providers/album/album_sort_by_options.provider.dart @@ -1,119 +1,19 @@ -import 'package:collection/collection.dart'; import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'album_sort_by_options.provider.g.dart'; - -typedef AlbumSortFn = List Function(List albums, bool isReverse); - -class _AlbumSortHandlers { - const _AlbumSortHandlers._(); - - static const AlbumSortFn created = _sortByCreated; - static List _sortByCreated(List albums, bool isReverse) { - final sorted = albums.sortedBy((album) => album.createdAt); - return (isReverse ? sorted.reversed : sorted).toList(); - } - - static const AlbumSortFn title = _sortByTitle; - static List _sortByTitle(List albums, bool isReverse) { - final sorted = albums.sortedBy((album) => album.name); - return (isReverse ? sorted.reversed : sorted).toList(); - } - - static const AlbumSortFn lastModified = _sortByLastModified; - static List _sortByLastModified(List albums, bool isReverse) { - final sorted = albums.sortedBy((album) => album.modifiedAt); - return (isReverse ? sorted.reversed : sorted).toList(); - } - - static const AlbumSortFn assetCount = _sortByAssetCount; - static List _sortByAssetCount(List albums, bool isReverse) { - final sorted = albums.sorted((a, b) => a.assetCount.compareTo(b.assetCount)); - return (isReverse ? sorted.reversed : sorted).toList(); - } - - static const AlbumSortFn mostRecent = _sortByMostRecent; - static List _sortByMostRecent(List albums, bool isReverse) { - final sorted = albums.sorted((a, b) { - if (a.endDate == null && b.endDate == null) { - return 0; - } - - if (a.endDate == null) { - // Put nulls at the end for recent sorting - return 1; - } - - if (b.endDate == null) { - return -1; - } - - // Sort by descending recent date - return b.endDate!.compareTo(a.endDate!); - }); - return (isReverse ? sorted.reversed : sorted).toList(); - } - - static const AlbumSortFn mostOldest = _sortByMostOldest; - static List _sortByMostOldest(List albums, bool isReverse) { - final sorted = albums.sorted((a, b) { - if (a.startDate != null && b.startDate != null) { - return a.startDate!.compareTo(b.startDate!); - } - if (a.startDate == null) return 1; - if (b.startDate == null) return -1; - return 0; - }); - return (isReverse ? sorted.reversed : sorted).toList(); - } -} // Store index allows us to re-arrange the values without affecting the saved prefs enum AlbumSortMode { - title(1, "library_page_sort_title", _AlbumSortHandlers.title, SortOrder.asc), - assetCount(4, "library_page_sort_asset_count", _AlbumSortHandlers.assetCount, SortOrder.desc), - lastModified(3, "library_page_sort_last_modified", _AlbumSortHandlers.lastModified, SortOrder.desc), - created(0, "library_page_sort_created", _AlbumSortHandlers.created, SortOrder.desc), - mostRecent(2, "sort_recent", _AlbumSortHandlers.mostRecent, SortOrder.desc), - mostOldest(5, "sort_oldest", _AlbumSortHandlers.mostOldest, SortOrder.asc); + title(1, "library_page_sort_title", SortOrder.asc), + assetCount(4, "library_page_sort_asset_count", SortOrder.desc), + lastModified(3, "library_page_sort_last_modified", SortOrder.desc), + created(0, "library_page_sort_created", SortOrder.desc), + mostRecent(2, "sort_recent", SortOrder.desc), + mostOldest(5, "sort_oldest", SortOrder.asc); final int storeIndex; final String label; - final AlbumSortFn sortFn; final SortOrder defaultOrder; - const AlbumSortMode(this.storeIndex, this.label, this.sortFn, this.defaultOrder); + const AlbumSortMode(this.storeIndex, this.label, this.defaultOrder); SortOrder effectiveOrder(bool isReverse) => isReverse ? defaultOrder.reverse() : defaultOrder; } - -@riverpod -class AlbumSortByOptions extends _$AlbumSortByOptions { - @override - AlbumSortMode build() { - final sortOpt = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.selectedAlbumSortOrder); - return AlbumSortMode.values.firstWhere((e) => e.storeIndex == sortOpt, orElse: () => AlbumSortMode.title); - } - - void changeSortMode(AlbumSortMode sortOption) { - state = sortOption; - ref.watch(appSettingsServiceProvider).setSetting(AppSettingsEnum.selectedAlbumSortOrder, sortOption.storeIndex); - } -} - -@riverpod -class AlbumSortOrder extends _$AlbumSortOrder { - @override - bool build() { - return ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.selectedAlbumSortReverse); - } - - void changeSortDirection(bool isReverse) { - state = isReverse; - ref.watch(appSettingsServiceProvider).setSetting(AppSettingsEnum.selectedAlbumSortReverse, isReverse); - } -} diff --git a/mobile/lib/providers/album/album_sort_by_options.provider.g.dart b/mobile/lib/providers/album/album_sort_by_options.provider.g.dart deleted file mode 100644 index 750329c9d5..0000000000 --- a/mobile/lib/providers/album/album_sort_by_options.provider.g.dart +++ /dev/null @@ -1,43 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'album_sort_by_options.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$albumSortByOptionsHash() => - r'dd8da5e730af555de1b86c3b157b6c93183523ac'; - -/// See also [AlbumSortByOptions]. -@ProviderFor(AlbumSortByOptions) -final albumSortByOptionsProvider = - AutoDisposeNotifierProvider.internal( - AlbumSortByOptions.new, - name: r'albumSortByOptionsProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$albumSortByOptionsHash, - dependencies: null, - allTransitiveDependencies: null, - ); - -typedef _$AlbumSortByOptions = AutoDisposeNotifier; -String _$albumSortOrderHash() => r'573dea45b4519e69386fc7104c72522e35713440'; - -/// See also [AlbumSortOrder]. -@ProviderFor(AlbumSortOrder) -final albumSortOrderProvider = - AutoDisposeNotifierProvider.internal( - AlbumSortOrder.new, - name: r'albumSortOrderProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$albumSortOrderHash, - dependencies: null, - allTransitiveDependencies: null, - ); - -typedef _$AlbumSortOrder = AutoDisposeNotifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/album/album_viewer.provider.dart b/mobile/lib/providers/album/album_viewer.provider.dart deleted file mode 100644 index f4ce047464..0000000000 --- a/mobile/lib/providers/album/album_viewer.provider.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/models/albums/album_viewer_page_state.model.dart'; -import 'package:immich_mobile/services/album.service.dart'; - -class AlbumViewerNotifier extends StateNotifier { - AlbumViewerNotifier(this.ref) - : super(const AlbumViewerPageState(editTitleText: "", isEditAlbum: false, editDescriptionText: "")); - - final Ref ref; - - void enableEditAlbum() { - state = state.copyWith(isEditAlbum: true); - } - - void disableEditAlbum() { - state = state.copyWith(isEditAlbum: false); - } - - void setEditTitleText(String newTitle) { - state = state.copyWith(editTitleText: newTitle); - } - - void setEditDescriptionText(String newDescription) { - state = state.copyWith(editDescriptionText: newDescription); - } - - void remoteEditTitleText() { - state = state.copyWith(editTitleText: ""); - } - - void remoteEditDescriptionText() { - state = state.copyWith(editDescriptionText: ""); - } - - void resetState() { - state = state.copyWith(editTitleText: "", isEditAlbum: false, editDescriptionText: ""); - } - - Future changeAlbumTitle(Album album, String newAlbumTitle) async { - AlbumService service = ref.watch(albumServiceProvider); - - bool isSuccess = await service.changeTitleAlbum(album, newAlbumTitle); - - if (isSuccess) { - state = state.copyWith(editTitleText: "", isEditAlbum: false); - - return true; - } - - state = state.copyWith(editTitleText: "", isEditAlbum: false); - return false; - } - - Future changeAlbumDescription(Album album, String newAlbumDescription) async { - AlbumService service = ref.watch(albumServiceProvider); - - bool isSuccess = await service.changeDescriptionAlbum(album, newAlbumDescription); - - if (isSuccess) { - state = state.copyWith(editDescriptionText: "", isEditAlbum: false); - - return true; - } - - state = state.copyWith(editDescriptionText: "", isEditAlbum: false); - - return false; - } -} - -final albumViewerProvider = StateNotifierProvider((ref) { - return AlbumViewerNotifier(ref); -}); diff --git a/mobile/lib/providers/album/current_album.provider.dart b/mobile/lib/providers/album/current_album.provider.dart deleted file mode 100644 index bd22c7a7cd..0000000000 --- a/mobile/lib/providers/album/current_album.provider.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'current_album.provider.g.dart'; - -@riverpod -class CurrentAlbum extends _$CurrentAlbum { - @override - Album? build() => null; - - void set(Album? a) => state = a; -} - -/// Mock class for testing -abstract class CurrentAlbumInternal extends _$CurrentAlbum {} diff --git a/mobile/lib/providers/album/suggested_shared_users.provider.dart b/mobile/lib/providers/album/suggested_shared_users.provider.dart deleted file mode 100644 index 51146748c7..0000000000 --- a/mobile/lib/providers/album/suggested_shared_users.provider.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; - -final otherUsersProvider = FutureProvider.autoDispose>((ref) async { - UserService userService = ref.watch(userServiceProvider); - final currentUser = ref.watch(currentUserProvider); - - final allUsers = await userService.getAll(); - allUsers.removeWhere((u) => currentUser?.id == u.id); - return allUsers; -}); diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index 68007f283a..a5f67215a8 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -5,28 +5,17 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; -import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; -import 'package:immich_mobile/providers/memory.provider.dart'; import 'package:immich_mobile/providers/notification_permission.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/providers/tab.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; -import 'package:permission_handler/permission_handler.dart'; enum AppLifeCycleEnum { active, inactive, paused, resumed, detached, hidden } @@ -87,43 +76,15 @@ class AppLifeCycleNotifier extends StateNotifier { final endpoint = await _ref.read(authProvider.notifier).setOpenApiServiceEndpoint(); _log.info("Using server URL: $endpoint"); - if (!Store.isBetaTimelineEnabled) { - final permission = _ref.watch(galleryPermissionNotifier); - if (permission.isGranted || permission.isLimited) { - await _ref.read(backupProvider.notifier).resumeBackup(); - await _ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); - } - } - await _ref.read(serverInfoProvider.notifier).getServerVersion(); } - if (!Store.isBetaTimelineEnabled) { - switch (_ref.read(tabProvider)) { - case TabEnum.home: - await _ref.read(assetProvider.notifier).getAllAsset(); - - case TabEnum.albums: - await _ref.read(albumProvider.notifier).refreshRemoteAlbums(); - - case TabEnum.library: - case TabEnum.search: - break; - } - } else { - _ref.read(websocketProvider.notifier).connect(); - await _handleBetaTimelineResume(); - } + _ref.read(websocketProvider.notifier).connect(); + await _handleBetaTimelineResume(); await _ref.read(notificationPermissionProvider.notifier).getNotificationPermission(); await _ref.read(galleryPermissionNotifier.notifier).getGalleryPermissionStatus(); - - if (!Store.isBetaTimelineEnabled) { - await _ref.read(iOSBackgroundSettingsProvider.notifier).refresh(); - - _ref.invalidate(memoryFutureProvider); - } } Future _safeRun(Future action, String debugName) async { @@ -139,7 +100,6 @@ class AppLifeCycleNotifier extends StateNotifier { } Future _handleBetaTimelineResume() async { - _ref.read(backupProvider.notifier).cancelBackup(); unawaited(_ref.read(backgroundWorkerLockServiceProvider).lock()); // Give isolates time to complete any ongoing database transactions @@ -218,9 +178,7 @@ class AppLifeCycleNotifier extends StateNotifier { _pauseOperation = Completer(); try { - if (Store.isBetaTimelineEnabled) { - unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock()); - } + unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock()); await _performPause(); } catch (e, stackTrace) { _log.severe("Error during app pause", e, stackTrace); @@ -234,14 +192,7 @@ class AppLifeCycleNotifier extends StateNotifier { Future _performPause() { if (_ref.read(authProvider).isAuthenticated) { - if (!Store.isBetaTimelineEnabled) { - // Do not cancel backup if manual upload is in progress - if (_ref.read(backupProvider.notifier).backupProgress != BackUpProgressEnum.manualInProgress) { - _ref.read(backupProvider.notifier).cancelBackup(); - } - } else { - _ref.read(driftBackupProvider.notifier).stopForegroundBackup(); - } + _ref.read(driftBackupProvider.notifier).stopForegroundBackup(); _ref.read(websocketProvider.notifier).disconnect(); } @@ -252,31 +203,12 @@ class AppLifeCycleNotifier extends StateNotifier { Future handleAppDetached() async { state = AppLifeCycleEnum.detached; - if (Store.isBetaTimelineEnabled) { - unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock()); - } + unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock()); // Flush logs before closing database try { await LogService.I.flush(); } catch (_) {} - - // Close Isar database safely - try { - final isar = Isar.getInstance(); - if (isar != null && isar.isOpen) { - await isar.close(); - } - } catch (_) {} - - if (Store.isBetaTimelineEnabled) { - return; - } - - // no guarantee this is called at all - try { - _ref.read(manualUploadProvider.notifier).cancelBackup(); - } catch (_) {} } void handleAppHidden() { diff --git a/mobile/lib/providers/asset.provider.dart b/mobile/lib/providers/asset.provider.dart deleted file mode 100644 index d5a4e42b74..0000000000 --- a/mobile/lib/providers/asset.provider.dart +++ /dev/null @@ -1,182 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/providers/memory.provider.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:immich_mobile/services/etag.service.dart'; -import 'package:immich_mobile/services/exif.service.dart'; -import 'package:immich_mobile/services/sync.service.dart'; -import 'package:logging/logging.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; - -final assetProvider = StateNotifierProvider((ref) { - return AssetNotifier( - ref.watch(assetServiceProvider), - ref.watch(albumServiceProvider), - ref.watch(userServiceProvider), - ref.watch(syncServiceProvider), - ref.watch(etagServiceProvider), - ref.watch(exifServiceProvider), - ref, - ); -}); - -class AssetNotifier extends StateNotifier { - final AssetService _assetService; - final AlbumService _albumService; - final UserService _userService; - final SyncService _syncService; - final ETagService _etagService; - final ExifService _exifService; - final Ref _ref; - final log = Logger('AssetNotifier'); - bool _getAllAssetInProgress = false; - bool _deleteInProgress = false; - - AssetNotifier( - this._assetService, - this._albumService, - this._userService, - this._syncService, - this._etagService, - this._exifService, - this._ref, - ) : super(false); - - Future getAllAsset({bool clear = false}) async { - if (_getAllAssetInProgress || _deleteInProgress) { - // guard against multiple calls to this method while it's still working - return; - } - final stopwatch = Stopwatch()..start(); - try { - _getAllAssetInProgress = true; - state = true; - if (clear) { - await clearAllAssets(); - log.info("Manual refresh requested, cleared assets and albums from db"); - } - final users = await _syncService.getUsersFromServer(); - bool changedUsers = false; - if (users != null) { - changedUsers = await _syncService.syncUsersFromServer(users); - } - final bool newRemote = await _assetService.refreshRemoteAssets(); - final bool newLocal = await _albumService.refreshDeviceAlbums(); - dPrint(() => "changedUsers: $changedUsers, newRemote: $newRemote, newLocal: $newLocal"); - if (newRemote) { - _ref.invalidate(memoryFutureProvider); - } - - log.info("Load assets: ${stopwatch.elapsedMilliseconds}ms"); - } catch (error) { - // If there is error in getting the remote assets, still showing the new local assets - await _albumService.refreshDeviceAlbums(); - } finally { - _getAllAssetInProgress = false; - if (mounted) { - state = false; - } - } - } - - Future clearAllAssets() async { - await Store.delete(StoreKey.assetETag); - await Future.wait([ - _assetService.clearTable(), - _exifService.clearTable(), - _albumService.clearTable(), - _userService.deleteAll(), - _etagService.clearTable(), - ]); - } - - Future onNewAssetUploaded(Asset newAsset) async { - // eTag on device is not valid after partially modifying the assets - await Store.delete(StoreKey.assetETag); - await _syncService.syncNewAssetToDb(newAsset); - } - - Future deleteLocalAssets(List assets) async { - _deleteInProgress = true; - state = true; - try { - await _assetService.deleteLocalAssets(assets); - return true; - } catch (error) { - log.severe("Failed to delete local assets", error); - return false; - } finally { - _deleteInProgress = false; - state = false; - } - } - - /// Delete remote asset only - /// - /// Default behavior is trashing the asset - Future deleteRemoteAssets(Iterable deleteAssets, {bool shouldDeletePermanently = false}) async { - _deleteInProgress = true; - state = true; - try { - await _assetService.deleteRemoteAssets(deleteAssets, shouldDeletePermanently: shouldDeletePermanently); - return true; - } catch (error) { - log.severe("Failed to delete remote assets", error); - return false; - } finally { - _deleteInProgress = false; - state = false; - } - } - - Future deleteAssets(Iterable deleteAssets, {bool force = false}) async { - _deleteInProgress = true; - state = true; - try { - await _assetService.deleteAssets(deleteAssets, shouldDeletePermanently: force); - return true; - } catch (error) { - log.severe("Failed to delete assets", error); - return false; - } finally { - _deleteInProgress = false; - state = false; - } - } - - Future toggleFavorite(List assets, [bool? status]) { - status ??= !assets.every((a) => a.isFavorite); - return _assetService.changeFavoriteStatus(assets, status); - } - - Future toggleArchive(List assets, [bool? status]) { - status ??= !assets.every((a) => a.isArchived); - return _assetService.changeArchiveStatus(assets, status); - } - - Future setLockedView(List selection, AssetVisibilityEnum visibility) { - return _assetService.setVisibility(selection, visibility); - } -} - -final assetDetailProvider = StreamProvider.autoDispose.family((ref, asset) async* { - final assetService = ref.watch(assetServiceProvider); - yield await assetService.loadExif(asset); - - await for (final asset in assetService.watchAsset(asset.id)) { - if (asset != null) { - yield await ref.watch(assetServiceProvider).loadExif(asset); - } - } -}); - -final assetWatcher = StreamProvider.autoDispose.family((ref, asset) { - final assetService = ref.watch(assetServiceProvider); - return assetService.watchAsset(asset.id, fireImmediately: true); -}); diff --git a/mobile/lib/providers/asset_viewer/asset_people.provider.dart b/mobile/lib/providers/asset_viewer/asset_people.provider.dart deleted file mode 100644 index e2227920c7..0000000000 --- a/mobile/lib/providers/asset_viewer/asset_people.provider.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:logging/logging.dart'; -import 'package:openapi/api.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'asset_people.provider.g.dart'; - -/// Maintains the list of people for an asset. -@riverpod -class AssetPeopleNotifier extends _$AssetPeopleNotifier { - final log = Logger('AssetPeopleNotifier'); - - @override - Future> build(Asset asset) async { - if (!asset.isRemote) { - return []; - } - - final list = await ref.watch(assetServiceProvider).getRemotePeopleOfAsset(asset.remoteId!); - if (list == null) { - return []; - } - - // explicitly a sorted slice to make it deterministic - // named people will be at the beginning, and names are sorted - // ascendingly - list.sort((a, b) { - final aNotEmpty = a.name.isNotEmpty; - final bNotEmpty = b.name.isNotEmpty; - if (aNotEmpty && !bNotEmpty) { - return -1; - } else if (!aNotEmpty && bNotEmpty) { - return 1; - } else if (!aNotEmpty && !bNotEmpty) { - return 0; - } - - return a.name.compareTo(b.name); - }); - return list; - } - - Future refresh() async { - // invalidate the state – this way we don't have to - // duplicate the code from build. - ref.invalidateSelf(); - } -} diff --git a/mobile/lib/providers/asset_viewer/asset_people.provider.g.dart b/mobile/lib/providers/asset_viewer/asset_people.provider.g.dart deleted file mode 100644 index 031a70e0d9..0000000000 --- a/mobile/lib/providers/asset_viewer/asset_people.provider.g.dart +++ /dev/null @@ -1,192 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'asset_people.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$assetPeopleNotifierHash() => - r'9835b180984a750c91e923e7b64dbda94f6d7574'; - -/// Copied from Dart SDK -class _SystemHash { - _SystemHash._(); - - static int combine(int hash, int value) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + value); - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); - return hash ^ (hash >> 6); - } - - static int finish(int hash) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); - // ignore: parameter_assignments - hash = hash ^ (hash >> 11); - return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); - } -} - -abstract class _$AssetPeopleNotifier - extends - BuildlessAutoDisposeAsyncNotifier> { - late final Asset asset; - - FutureOr> build(Asset asset); -} - -/// Maintains the list of people for an asset. -/// -/// Copied from [AssetPeopleNotifier]. -@ProviderFor(AssetPeopleNotifier) -const assetPeopleNotifierProvider = AssetPeopleNotifierFamily(); - -/// Maintains the list of people for an asset. -/// -/// Copied from [AssetPeopleNotifier]. -class AssetPeopleNotifierFamily - extends Family>> { - /// Maintains the list of people for an asset. - /// - /// Copied from [AssetPeopleNotifier]. - const AssetPeopleNotifierFamily(); - - /// Maintains the list of people for an asset. - /// - /// Copied from [AssetPeopleNotifier]. - AssetPeopleNotifierProvider call(Asset asset) { - return AssetPeopleNotifierProvider(asset); - } - - @override - AssetPeopleNotifierProvider getProviderOverride( - covariant AssetPeopleNotifierProvider provider, - ) { - return call(provider.asset); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'assetPeopleNotifierProvider'; -} - -/// Maintains the list of people for an asset. -/// -/// Copied from [AssetPeopleNotifier]. -class AssetPeopleNotifierProvider - extends - AutoDisposeAsyncNotifierProviderImpl< - AssetPeopleNotifier, - List - > { - /// Maintains the list of people for an asset. - /// - /// Copied from [AssetPeopleNotifier]. - AssetPeopleNotifierProvider(Asset asset) - : this._internal( - () => AssetPeopleNotifier()..asset = asset, - from: assetPeopleNotifierProvider, - name: r'assetPeopleNotifierProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$assetPeopleNotifierHash, - dependencies: AssetPeopleNotifierFamily._dependencies, - allTransitiveDependencies: - AssetPeopleNotifierFamily._allTransitiveDependencies, - asset: asset, - ); - - AssetPeopleNotifierProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.asset, - }) : super.internal(); - - final Asset asset; - - @override - FutureOr> runNotifierBuild( - covariant AssetPeopleNotifier notifier, - ) { - return notifier.build(asset); - } - - @override - Override overrideWith(AssetPeopleNotifier Function() create) { - return ProviderOverride( - origin: this, - override: AssetPeopleNotifierProvider._internal( - () => create()..asset = asset, - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - asset: asset, - ), - ); - } - - @override - AutoDisposeAsyncNotifierProviderElement< - AssetPeopleNotifier, - List - > - createElement() { - return _AssetPeopleNotifierProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is AssetPeopleNotifierProvider && other.asset == asset; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, asset.hashCode); - - return _SystemHash.finish(hash); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin AssetPeopleNotifierRef - on AutoDisposeAsyncNotifierProviderRef> { - /// The parameter `asset` of this provider. - Asset get asset; -} - -class _AssetPeopleNotifierProviderElement - extends - AutoDisposeAsyncNotifierProviderElement< - AssetPeopleNotifier, - List - > - with AssetPeopleNotifierRef { - _AssetPeopleNotifierProviderElement(super.provider); - - @override - Asset get asset => (origin as AssetPeopleNotifierProvider).asset; -} - -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/asset_viewer/asset_stack.provider.dart b/mobile/lib/providers/asset_viewer/asset_stack.provider.dart deleted file mode 100644 index 8772e3d0cb..0000000000 --- a/mobile/lib/providers/asset_viewer/asset_stack.provider.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'asset_stack.provider.g.dart'; - -class AssetStackNotifier extends StateNotifier> { - final AssetService assetService; - final String _stackId; - - AssetStackNotifier(this.assetService, this._stackId) : super([]) { - _fetchStack(_stackId); - } - - void _fetchStack(String stackId) async { - if (!mounted) { - return; - } - - final stack = await assetService.getStackAssets(stackId); - if (stack.isNotEmpty) { - state = stack; - } - } - - void removeChild(int index) { - if (index < state.length) { - state.removeAt(index); - state = List.from(state); - } - } -} - -final assetStackStateProvider = StateNotifierProvider.autoDispose.family, String>( - (ref, stackId) => AssetStackNotifier(ref.watch(assetServiceProvider), stackId), -); - -@riverpod -int assetStackIndex(Ref _) { - return -1; -} diff --git a/mobile/lib/providers/asset_viewer/asset_stack.provider.g.dart b/mobile/lib/providers/asset_viewer/asset_stack.provider.g.dart deleted file mode 100644 index dcf82cdebd..0000000000 --- a/mobile/lib/providers/asset_viewer/asset_stack.provider.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'asset_stack.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$assetStackIndexHash() => r'086ddb782e3eb38b80d755666fe35be8fe7322d7'; - -/// See also [assetStackIndex]. -@ProviderFor(assetStackIndex) -final assetStackIndexProvider = AutoDisposeProvider.internal( - assetStackIndex, - name: r'assetStackIndexProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$assetStackIndexHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef AssetStackIndexRef = AutoDisposeProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/asset_viewer/current_asset.provider.dart b/mobile/lib/providers/asset_viewer/current_asset.provider.dart deleted file mode 100644 index 0e25660ab0..0000000000 --- a/mobile/lib/providers/asset_viewer/current_asset.provider.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'current_asset.provider.g.dart'; - -@riverpod -class CurrentAsset extends _$CurrentAsset { - @override - Asset? build() => null; - - void set(Asset? a) => state = a; -} - -/// Mock class for testing -abstract class CurrentAssetInternal extends _$CurrentAsset {} diff --git a/mobile/lib/providers/asset_viewer/current_asset.provider.g.dart b/mobile/lib/providers/asset_viewer/current_asset.provider.g.dart deleted file mode 100644 index e0d8d47d3a..0000000000 --- a/mobile/lib/providers/asset_viewer/current_asset.provider.g.dart +++ /dev/null @@ -1,26 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'current_asset.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$currentAssetHash() => r'2def10ea594152c984ae2974d687ab6856d7bdd0'; - -/// See also [CurrentAsset]. -@ProviderFor(CurrentAsset) -final currentAssetProvider = - AutoDisposeNotifierProvider.internal( - CurrentAsset.new, - name: r'currentAssetProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$currentAssetHash, - dependencies: null, - allTransitiveDependencies: null, - ); - -typedef _$CurrentAsset = AutoDisposeNotifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/asset_viewer/download.provider.dart b/mobile/lib/providers/asset_viewer/download.provider.dart index a461d5766a..25db76b077 100644 --- a/mobile/lib/providers/asset_viewer/download.provider.dart +++ b/mobile/lib/providers/asset_viewer/download.provider.dart @@ -1,26 +1,15 @@ import 'dart:async'; import 'package:background_downloader/background_downloader.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/download/download_state.model.dart'; import 'package:immich_mobile/models/download/livephotos_medatada.model.dart'; -import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/download.service.dart'; -import 'package:immich_mobile/services/share.service.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/widgets/common/share_dialog.dart'; class DownloadStateNotifier extends StateNotifier { final DownloadService _downloadService; - final ShareService _shareService; - final AlbumService _albumService; - DownloadStateNotifier(this._downloadService, this._shareService, this._albumService) + DownloadStateNotifier(this._downloadService) : super( const DownloadState( downloadStatus: TaskStatus.complete, @@ -132,18 +121,9 @@ class DownloadStateNotifier extends StateNotifier { if (state.taskProgress.isEmpty) { state = state.copyWith(showProgress: false); } - _albumService.refreshDeviceAlbums(); }); } - Future> downloadAllAsset(List assets) async { - return await _downloadService.downloadAll(assets); - } - - void downloadAsset(Asset asset) async { - await _downloadService.download(asset); - } - void cancelDownload(String id) async { final isCanceled = await _downloadService.cancelDownload(id); @@ -159,36 +139,8 @@ class DownloadStateNotifier extends StateNotifier { state = state.copyWith(showProgress: false); } } - - void shareAsset(Asset asset, BuildContext context) async { - unawaited( - showDialog( - context: context, - builder: (BuildContext buildContext) { - _shareService.shareAsset(asset, context).then((bool status) { - if (!status) { - ImmichToast.show( - context: context, - msg: 'image_viewer_page_state_provider_share_error'.tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - buildContext.pop(); - }); - return const ShareDialog(); - }, - barrierDismissible: false, - useRootNavigator: false, - ), - ); - } } final downloadStateProvider = StateNotifierProvider( - ((ref) => DownloadStateNotifier( - ref.watch(downloadServiceProvider), - ref.watch(shareServiceProvider), - ref.watch(albumServiceProvider), - )), + ((ref) => DownloadStateNotifier(ref.watch(downloadServiceProvider))), ); diff --git a/mobile/lib/providers/asset_viewer/render_list_status_provider.dart b/mobile/lib/providers/asset_viewer/render_list_status_provider.dart deleted file mode 100644 index 189ac85452..0000000000 --- a/mobile/lib/providers/asset_viewer/render_list_status_provider.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -enum RenderListStatusEnum { complete, empty, error, loading } - -final renderListStatusProvider = StateNotifierProvider((ref) { - return RenderListStatus(ref); -}); - -class RenderListStatus extends StateNotifier { - RenderListStatus(this.ref) : super(RenderListStatusEnum.complete); - - final Ref ref; - - RenderListStatusEnum get status => state; - - set status(RenderListStatusEnum value) { - state = value; - } -} diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart index 5f3ad3d058..a6dc272313 100644 --- a/mobile/lib/providers/backup/backup.provider.dart +++ b/mobile/lib/providers/backup/backup.provider.dart @@ -1,672 +1,23 @@ import 'dart:async'; -import 'dart:io'; -import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/models/auth/auth_state.model.dart'; -import 'package:immich_mobile/models/backup/available_album.model.dart'; -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; import 'package:immich_mobile/models/server_info/server_disk_info.model.dart'; -import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; -import 'package:immich_mobile/providers/gallery_permission.provider.dart'; -import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:immich_mobile/services/backup.service.dart'; -import 'package:immich_mobile/services/backup_album.service.dart'; import 'package:immich_mobile/services/server_info.service.dart'; -import 'package:immich_mobile/utils/backup_progress.dart'; -import 'package:immich_mobile/utils/diff.dart'; -import 'package:logging/logging.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; -import 'package:immich_mobile/utils/debug_print.dart'; -final backupProvider = StateNotifierProvider((ref) { - return BackupNotifier( - ref.watch(backupServiceProvider), - ref.watch(serverInfoServiceProvider), - ref.watch(authProvider), - ref.watch(backgroundServiceProvider), - ref.watch(galleryPermissionNotifier.notifier), - ref.watch(albumMediaRepositoryProvider), - ref.watch(fileMediaRepositoryProvider), - ref.watch(backupAlbumServiceProvider), - ref, - ); +final backupProvider = StateNotifierProvider((ref) { + return BackupNotifier(ref.watch(serverInfoServiceProvider)); }); -class BackupNotifier extends StateNotifier { - BackupNotifier( - this._backupService, - this._serverInfoService, - this._authState, - this._backgroundService, - this._galleryPermissionNotifier, - this._albumMediaRepository, - this._fileMediaRepository, - this._backupAlbumService, - this.ref, - ) : super( - BackUpState( - backupProgress: BackUpProgressEnum.idle, - allAssetsInDatabase: const [], - progressInPercentage: 0, - progressInFileSize: "0 B / 0 B", - progressInFileSpeed: 0, - progressInFileSpeeds: const [], - progressInFileSpeedUpdateTime: DateTime.now(), - progressInFileSpeedUpdateSentBytes: 0, - autoBackup: Store.get(StoreKey.autoBackup, false), - backgroundBackup: Store.get(StoreKey.backgroundBackup, false), - backupRequireWifi: Store.get(StoreKey.backupRequireWifi, true), - backupRequireCharging: Store.get(StoreKey.backupRequireCharging, false), - backupTriggerDelay: Store.get(StoreKey.backupTriggerDelay, 5000), - serverInfo: const ServerDiskInfo(diskAvailable: "0", diskSize: "0", diskUse: "0", diskUsagePercentage: 0), - availableAlbums: const [], - selectedBackupAlbums: const {}, - excludedBackupAlbums: const {}, - allUniqueAssets: const {}, - selectedAlbumsBackupAssetsIds: const {}, - currentUploadAsset: CurrentUploadAsset( - id: '...', - fileCreatedAt: DateTime.parse('2020-10-04'), - fileName: '...', - fileType: '...', - fileSize: 0, - iCloudAsset: false, - ), - iCloudDownloadProgress: 0.0, - ), - ); +class BackupNotifier extends StateNotifier { + BackupNotifier(this._serverInfoService) + : super(const ServerDiskInfo(diskAvailable: "0", diskSize: "0", diskUse: "0", diskUsagePercentage: 0)); - final log = Logger('BackupNotifier'); - final BackupService _backupService; final ServerInfoService _serverInfoService; - final AuthState _authState; - final BackgroundService _backgroundService; - final GalleryPermissionNotifier _galleryPermissionNotifier; - final AlbumMediaRepository _albumMediaRepository; - final FileMediaRepository _fileMediaRepository; - final BackupAlbumService _backupAlbumService; - final Ref ref; - Completer? _cancelToken; - - /// - /// UI INTERACTION - /// - /// Album selection - /// Due to the overlapping assets across multiple albums on the device - /// We have method to include and exclude albums - /// The total unique assets will be used for backing mechanism - /// - void addAlbumForBackup(AvailableAlbum album) { - if (state.excludedBackupAlbums.contains(album)) { - removeExcludedAlbumForBackup(album); - } - - state = state.copyWith(selectedBackupAlbums: {...state.selectedBackupAlbums, album}); - } - - void addExcludedAlbumForBackup(AvailableAlbum album) { - if (state.selectedBackupAlbums.contains(album)) { - removeAlbumForBackup(album); - } - state = state.copyWith(excludedBackupAlbums: {...state.excludedBackupAlbums, album}); - } - - void removeAlbumForBackup(AvailableAlbum album) { - Set currentSelectedAlbums = state.selectedBackupAlbums; - - currentSelectedAlbums.removeWhere((a) => a == album); - - state = state.copyWith(selectedBackupAlbums: currentSelectedAlbums); - } - - void removeExcludedAlbumForBackup(AvailableAlbum album) { - Set currentExcludedAlbums = state.excludedBackupAlbums; - - currentExcludedAlbums.removeWhere((a) => a == album); - - state = state.copyWith(excludedBackupAlbums: currentExcludedAlbums); - } - - Future backupAlbumSelectionDone() { - if (state.selectedBackupAlbums.isEmpty) { - // disable any backup - cancelBackup(); - setAutoBackup(false); - configureBackgroundBackup(enabled: false, onError: (msg) {}, onBatteryInfo: () {}); - } - return _updateBackupAssetCount(); - } - - void setAutoBackup(bool enabled) { - Store.put(StoreKey.autoBackup, enabled); - state = state.copyWith(autoBackup: enabled); - } - - void configureBackgroundBackup({ - bool? enabled, - bool? requireWifi, - bool? requireCharging, - int? triggerDelay, - required void Function(String msg) onError, - required void Function() onBatteryInfo, - }) async { - assert(enabled != null || requireWifi != null || requireCharging != null || triggerDelay != null); - final bool wasEnabled = state.backgroundBackup; - final bool wasWifi = state.backupRequireWifi; - final bool wasCharging = state.backupRequireCharging; - final int oldTriggerDelay = state.backupTriggerDelay; - state = state.copyWith( - backgroundBackup: enabled, - backupRequireWifi: requireWifi, - backupRequireCharging: requireCharging, - backupTriggerDelay: triggerDelay, - ); - - if (state.backgroundBackup) { - bool success = true; - if (!wasEnabled) { - if (!await _backgroundService.isIgnoringBatteryOptimizations()) { - onBatteryInfo(); - } - success &= await _backgroundService.enableService(immediate: true); - } - success &= - success && - await _backgroundService.configureService( - requireUnmetered: state.backupRequireWifi, - requireCharging: state.backupRequireCharging, - triggerUpdateDelay: state.backupTriggerDelay, - triggerMaxDelay: state.backupTriggerDelay * 10, - ); - if (success) { - await Store.put(StoreKey.backupRequireWifi, state.backupRequireWifi); - await Store.put(StoreKey.backupRequireCharging, state.backupRequireCharging); - await Store.put(StoreKey.backupTriggerDelay, state.backupTriggerDelay); - await Store.put(StoreKey.backgroundBackup, state.backgroundBackup); - } else { - state = state.copyWith( - backgroundBackup: wasEnabled, - backupRequireWifi: wasWifi, - backupRequireCharging: wasCharging, - backupTriggerDelay: oldTriggerDelay, - ); - onError("backup_controller_page_background_configure_error"); - } - } else { - final bool success = await _backgroundService.disableService(); - if (!success) { - state = state.copyWith(backgroundBackup: wasEnabled); - onError("backup_controller_page_background_configure_error"); - } - } - } - - /// - /// Get all album on the device - /// Get all selected and excluded album from the user's persistent storage - /// If this is the first time performing backup - set the default selected album to be - /// the one that has all assets (`Recent` on Android, `Recents` on iOS) - /// - Future _getBackupAlbumsInfo() async { - Stopwatch stopwatch = Stopwatch()..start(); - // Get all albums on the device - List availableAlbums = []; - List albums = await _albumMediaRepository.getAll(); - - // Map of id -> album for quick album lookup later on. - Map albumMap = {}; - - log.info('Found ${albums.length} local albums'); - - for (Album album in albums) { - AvailableAlbum availableAlbum = AvailableAlbum( - album: album, - assetCount: await ref.read(albumMediaRepositoryProvider).getAssetCount(album.localId!), - ); - - availableAlbums.add(availableAlbum); - - albumMap[album.localId!] = album; - } - state = state.copyWith(availableAlbums: availableAlbums); - - final List excludedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.exclude); - final List selectedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.select); - - final Set selectedAlbums = {}; - for (final BackupAlbum ba in selectedBackupAlbums) { - final albumAsset = albumMap[ba.id]; - - if (albumAsset != null) { - selectedAlbums.add( - AvailableAlbum( - album: albumAsset, - assetCount: await _albumMediaRepository.getAssetCount(albumAsset.localId!), - lastBackup: ba.lastBackup, - ), - ); - } else { - log.severe('Selected album not found'); - } - } - - final Set excludedAlbums = {}; - for (final BackupAlbum ba in excludedBackupAlbums) { - final albumAsset = albumMap[ba.id]; - - if (albumAsset != null) { - excludedAlbums.add( - AvailableAlbum( - album: albumAsset, - assetCount: await ref.read(albumMediaRepositoryProvider).getAssetCount(albumAsset.localId!), - lastBackup: ba.lastBackup, - ), - ); - } else { - log.severe('Excluded album not found'); - } - } - - state = state.copyWith(selectedBackupAlbums: selectedAlbums, excludedBackupAlbums: excludedAlbums); - - log.info("_getBackupAlbumsInfo: Found ${availableAlbums.length} available albums"); - dPrint(() => "_getBackupAlbumsInfo takes ${stopwatch.elapsedMilliseconds}ms"); - } - - /// - /// From all the selected and albums assets - /// Find the assets that are not overlapping between the two sets - /// Those assets are unique and are used as the total assets - /// - Future _updateBackupAssetCount() async { - // Save to persistent storage - await _updatePersistentAlbumsSelection(); - - final duplicatedAssetIds = await _backupService.getDuplicatedAssetIds(); - final Set assetsFromSelectedAlbums = {}; - final Set assetsFromExcludedAlbums = {}; - - for (final album in state.selectedBackupAlbums) { - final assetCount = await ref.read(albumMediaRepositoryProvider).getAssetCount(album.album.localId!); - - if (assetCount == 0) { - continue; - } - - final assets = await ref.read(albumMediaRepositoryProvider).getAssets(album.album.localId!); - - // Add album's name to the asset info - for (final asset in assets) { - List albumNames = [album.name]; - - final existingAsset = assetsFromSelectedAlbums.firstWhereOrNull((a) => a.asset.localId == asset.localId); - - if (existingAsset != null) { - albumNames.addAll(existingAsset.albumNames); - assetsFromSelectedAlbums.remove(existingAsset); - } - - assetsFromSelectedAlbums.add(BackupCandidate(asset: asset, albumNames: albumNames)); - } - } - - for (final album in state.excludedBackupAlbums) { - final assetCount = await ref.read(albumMediaRepositoryProvider).getAssetCount(album.album.localId!); - - if (assetCount == 0) { - continue; - } - - final assets = await ref.read(albumMediaRepositoryProvider).getAssets(album.album.localId!); - - for (final asset in assets) { - assetsFromExcludedAlbums.add(BackupCandidate(asset: asset, albumNames: [album.name])); - } - } - - final Set allUniqueAssets = assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums); - - final allAssetsInDatabase = await _backupService.getDeviceBackupAsset(); - - if (allAssetsInDatabase == null) { - return; - } - - // Find asset that were backup from selected albums - final Set selectedAlbumsBackupAssets = Set.from(allUniqueAssets.map((e) => e.asset.localId)); - - selectedAlbumsBackupAssets.removeWhere((assetId) => !allAssetsInDatabase.contains(assetId)); - - // Remove duplicated asset from all unique assets - allUniqueAssets.removeWhere((candidate) => duplicatedAssetIds.contains(candidate.asset.localId)); - - if (allUniqueAssets.isEmpty) { - log.info("No assets are selected for back up"); - state = state.copyWith( - backupProgress: BackUpProgressEnum.idle, - allAssetsInDatabase: allAssetsInDatabase, - allUniqueAssets: {}, - selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets, - ); - } else { - state = state.copyWith( - allAssetsInDatabase: allAssetsInDatabase, - allUniqueAssets: allUniqueAssets, - selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets, - ); - } - } - - /// Get all necessary information for calculating the available albums, - /// which albums are selected or excluded - /// and then update the UI according to those information - Future getBackupInfo() async { - final isEnabled = await _backgroundService.isBackgroundBackupEnabled(); - - state = state.copyWith(backgroundBackup: isEnabled); - if (isEnabled != Store.get(StoreKey.backgroundBackup, !isEnabled)) { - await Store.put(StoreKey.backgroundBackup, isEnabled); - } - - if (state.backupProgress != BackUpProgressEnum.inBackground) { - await _getBackupAlbumsInfo(); - await updateDiskInfo(); - await _updateBackupAssetCount(); - } else { - log.warning("cannot get backup info - background backup is in progress!"); - } - } - - /// Save user selection of selected albums and excluded albums to database - Future _updatePersistentAlbumsSelection() async { - final epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true); - final selected = state.selectedBackupAlbums.map( - (e) => BackupAlbum(e.id, e.lastBackup ?? epoch, BackupSelection.select), - ); - final excluded = state.excludedBackupAlbums.map( - (e) => BackupAlbum(e.id, e.lastBackup ?? epoch, BackupSelection.exclude), - ); - final candidates = selected.followedBy(excluded).toList(); - candidates.sortBy((e) => e.id); - - final savedBackupAlbums = await _backupAlbumService.getAll(sort: BackupAlbumSort.id); - final List toDelete = []; - final List toUpsert = []; - - diffSortedListsSync( - savedBackupAlbums, - candidates, - compare: (BackupAlbum a, BackupAlbum b) => a.id.compareTo(b.id), - both: (BackupAlbum a, BackupAlbum b) { - b.lastBackup = a.lastBackup.isAfter(b.lastBackup) ? a.lastBackup : b.lastBackup; - toUpsert.add(b); - return true; - }, - onlyFirst: (BackupAlbum a) => toDelete.add(a.isarId), - onlySecond: (BackupAlbum b) => toUpsert.add(b), - ); - - await _backupAlbumService.deleteAll(toDelete); - await _backupAlbumService.updateAll(toUpsert); - } - - /// Invoke backup process - Future startBackupProcess() async { - dPrint(() => "Start backup process"); - assert(state.backupProgress == BackUpProgressEnum.idle); - state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress); - - await getBackupInfo(); - - final hasPermission = _galleryPermissionNotifier.hasPermission; - if (hasPermission) { - await _fileMediaRepository.clearFileCache(); - - if (state.allUniqueAssets.isEmpty) { - log.info("No Asset On Device - Abort Backup Process"); - state = state.copyWith(backupProgress: BackUpProgressEnum.idle); - return; - } - - Set assetsWillBeBackup = Set.from(state.allUniqueAssets); - // Remove item that has already been backed up - for (final assetId in state.allAssetsInDatabase) { - assetsWillBeBackup.removeWhere((e) => e.asset.localId == assetId); - } - - if (assetsWillBeBackup.isEmpty) { - state = state.copyWith(backupProgress: BackUpProgressEnum.idle); - } - - // Perform Backup - _cancelToken?.complete(); - _cancelToken = Completer(); - - final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null; - - pmProgressHandler?.stream.listen((event) { - final double progress = event.progress; - state = state.copyWith(iCloudDownloadProgress: progress); - }); - - await _backupService.backupAsset( - assetsWillBeBackup, - _cancelToken!, - pmProgressHandler: pmProgressHandler, - onSuccess: _onAssetUploaded, - onProgress: _onUploadProgress, - onCurrentAsset: _onSetCurrentBackupAsset, - onError: _onBackupError, - ); - await notifyBackgroundServiceCanRun(); - } else { - await openAppSettings(); - } - } - - void setAvailableAlbums(availableAlbums) { - state = state.copyWith(availableAlbums: availableAlbums); - } - - void _onBackupError(ErrorUploadAsset errorAssetInfo) { - ref.watch(errorBackupListProvider.notifier).add(errorAssetInfo); - } - - void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) { - state = state.copyWith(currentUploadAsset: currentUploadAsset); - } - - void cancelBackup() { - if (state.backupProgress != BackUpProgressEnum.inProgress) { - notifyBackgroundServiceCanRun(); - } - _cancelToken?.complete(); - _cancelToken = null; - state = state.copyWith( - backupProgress: BackUpProgressEnum.idle, - progressInPercentage: 0.0, - progressInFileSize: "0 B / 0 B", - progressInFileSpeed: 0, - progressInFileSpeedUpdateTime: DateTime.now(), - progressInFileSpeedUpdateSentBytes: 0, - ); - } - - void _onAssetUploaded(SuccessUploadAsset result) async { - if (result.isDuplicate) { - state = state.copyWith( - allUniqueAssets: state.allUniqueAssets - .where((candidate) => candidate.asset.localId != result.candidate.asset.localId) - .toSet(), - ); - } else { - state = state.copyWith( - selectedAlbumsBackupAssetsIds: {...state.selectedAlbumsBackupAssetsIds, result.candidate.asset.localId!}, - allAssetsInDatabase: [...state.allAssetsInDatabase, result.candidate.asset.localId!], - ); - } - - if (state.allUniqueAssets.length - state.selectedAlbumsBackupAssetsIds.length == 0) { - final latestAssetBackup = state.allUniqueAssets - .map((candidate) => candidate.asset.fileModifiedAt) - .reduce((v, e) => e.isAfter(v) ? e : v); - state = state.copyWith( - selectedBackupAlbums: state.selectedBackupAlbums.map((e) => e.copyWith(lastBackup: latestAssetBackup)).toSet(), - excludedBackupAlbums: state.excludedBackupAlbums.map((e) => e.copyWith(lastBackup: latestAssetBackup)).toSet(), - backupProgress: BackUpProgressEnum.done, - progressInPercentage: 0.0, - progressInFileSize: "0 B / 0 B", - progressInFileSpeed: 0, - progressInFileSpeedUpdateTime: DateTime.now(), - progressInFileSpeedUpdateSentBytes: 0, - ); - await _updatePersistentAlbumsSelection(); - } - - await updateDiskInfo(); - } - - void _onUploadProgress(int sent, int total) { - double lastUploadSpeed = state.progressInFileSpeed; - List lastUploadSpeeds = state.progressInFileSpeeds.toList(); - DateTime lastUpdateTime = state.progressInFileSpeedUpdateTime; - int lastSentBytes = state.progressInFileSpeedUpdateSentBytes; - - final now = DateTime.now(); - final duration = now.difference(lastUpdateTime); - - // Keep the upload speed average span limited, to keep it somewhat relevant - if (lastUploadSpeeds.length > 10) { - lastUploadSpeeds.removeAt(0); - } - - if (duration.inSeconds > 0) { - lastUploadSpeeds.add(((sent - lastSentBytes) / duration.inSeconds).abs().roundToDouble()); - - lastUploadSpeed = lastUploadSpeeds.average.abs().roundToDouble(); - lastUpdateTime = now; - lastSentBytes = sent; - } - - state = state.copyWith( - progressInPercentage: (sent.toDouble() / total.toDouble() * 100), - progressInFileSize: humanReadableFileBytesProgress(sent, total), - progressInFileSpeed: lastUploadSpeed, - progressInFileSpeeds: lastUploadSpeeds, - progressInFileSpeedUpdateTime: lastUpdateTime, - progressInFileSpeedUpdateSentBytes: lastSentBytes, - ); - } Future updateDiskInfo() async { final diskInfo = await _serverInfoService.getDiskInfo(); - - // Update server info if (diskInfo != null) { - state = state.copyWith(serverInfo: diskInfo); + state = diskInfo; } } - - Future _resumeBackup() async { - // Check if user is login - final accessKey = Store.tryGet(StoreKey.accessToken); - - // User has been logged out return - if (accessKey == null || !_authState.isAuthenticated) { - log.info("[_resumeBackup] not authenticated - abort"); - return; - } - - // Check if this device is enable backup by the user - if (state.autoBackup) { - // check if backup is already in process - then return - if (state.backupProgress == BackUpProgressEnum.inProgress) { - log.info("[_resumeBackup] Auto Backup is already in progress - abort"); - return; - } - - if (state.backupProgress == BackUpProgressEnum.inBackground) { - log.info("[_resumeBackup] Background backup is running - abort"); - return; - } - - if (state.backupProgress == BackUpProgressEnum.manualInProgress) { - log.info("[_resumeBackup] Manual upload is running - abort"); - return; - } - - // Run backup - log.info("[_resumeBackup] Start back up"); - await startBackupProcess(); - } - return; - } - - Future resumeBackup() async { - final List selectedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.select); - final List excludedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.exclude); - Set selectedAlbums = state.selectedBackupAlbums; - Set excludedAlbums = state.excludedBackupAlbums; - if (selectedAlbums.isNotEmpty) { - selectedAlbums = _updateAlbumsBackupTime(selectedAlbums, selectedBackupAlbums); - } - - if (excludedAlbums.isNotEmpty) { - excludedAlbums = _updateAlbumsBackupTime(excludedAlbums, excludedBackupAlbums); - } - final BackUpProgressEnum previous = state.backupProgress; - state = state.copyWith( - backupProgress: BackUpProgressEnum.inBackground, - selectedBackupAlbums: selectedAlbums, - excludedBackupAlbums: excludedAlbums, - ); - // assumes the background service is currently running - // if true, waits until it has stopped to start the backup - final bool hasLock = await _backgroundService.acquireLock(); - if (hasLock) { - state = state.copyWith(backupProgress: previous); - } - return _resumeBackup(); - } - - Set _updateAlbumsBackupTime(Set albums, List backupAlbums) { - Set result = {}; - for (BackupAlbum ba in backupAlbums) { - try { - AvailableAlbum a = albums.firstWhere((e) => e.id == ba.id); - result.add(a.copyWith(lastBackup: ba.lastBackup)); - } on StateError { - log.severe("[_updateAlbumBackupTime] failed to find album in state", "State Error", StackTrace.current); - } - } - return result; - } - - Future notifyBackgroundServiceCanRun() async { - const allowedStates = [AppLifeCycleEnum.inactive, AppLifeCycleEnum.paused, AppLifeCycleEnum.detached]; - if (allowedStates.contains(ref.read(appStateProvider.notifier).state)) { - _backgroundService.releaseLock(); - } - } - - BackUpProgressEnum get backupProgress => state.backupProgress; - - void updateBackupProgress(BackUpProgressEnum backupProgress) { - state = state.copyWith(backupProgress: backupProgress); - } } diff --git a/mobile/lib/providers/backup/backup_verification.provider.dart b/mobile/lib/providers/backup/backup_verification.provider.dart deleted file mode 100644 index 50270e87ca..0000000000 --- a/mobile/lib/providers/backup/backup_verification.provider.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'dart:async'; - -import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/services/backup_verification.service.dart'; -import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; - -part 'backup_verification.provider.g.dart'; - -@riverpod -class BackupVerification extends _$BackupVerification { - @override - bool build() => false; - - void performBackupCheck(BuildContext context) async { - try { - state = true; - final backupState = ref.read(backupProvider); - - if (backupState.allUniqueAssets.length > backupState.selectedAlbumsBackupAssetsIds.length) { - if (context.mounted) { - ImmichToast.show( - context: context, - msg: "Backup all assets before starting this check!", - toastType: ToastType.error, - ); - } - return; - } - final connection = await Connectivity().checkConnectivity(); - if (!connection.contains(ConnectivityResult.wifi)) { - if (context.mounted) { - ImmichToast.show( - context: context, - msg: "Make sure to be connected to unmetered Wi-Fi", - toastType: ToastType.error, - ); - } - return; - } - unawaited(WakelockPlus.enable()); - - const limit = 100; - final toDelete = await ref.read(backupVerificationServiceProvider).findWronglyBackedUpAssets(limit: limit); - if (toDelete.isEmpty) { - if (context.mounted) { - ImmichToast.show( - context: context, - msg: "Did not find any corrupt asset backups!", - toastType: ToastType.success, - ); - } - } else { - if (context.mounted) { - await showDialog( - context: context, - builder: (ctx) => ConfirmDialog( - onOk: () => _performDeletion(context, toDelete), - title: "Corrupt backups!", - ok: "Delete", - content: - "Found ${toDelete.length} (max $limit at once) corrupt asset backups. " - "Run the check again to find more.\n" - "Do you want to delete the corrupt asset backups now?", - ), - ); - } - } - } finally { - unawaited(WakelockPlus.disable()); - state = false; - } - } - - Future _performDeletion(BuildContext context, List assets) async { - try { - state = true; - if (context.mounted) { - ImmichToast.show(context: context, msg: "Deleting ${assets.length} assets on the server..."); - } - await ref.read(assetProvider.notifier).deleteAssets(assets, force: true); - if (context.mounted) { - ImmichToast.show( - context: context, - msg: - "Deleted ${assets.length} assets on the server. " - "You can now start a manual backup", - toastType: ToastType.success, - ); - } - } finally { - state = false; - } - } -} diff --git a/mobile/lib/providers/backup/backup_verification.provider.g.dart b/mobile/lib/providers/backup/backup_verification.provider.g.dart deleted file mode 100644 index 13f6819fa7..0000000000 --- a/mobile/lib/providers/backup/backup_verification.provider.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'backup_verification.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$backupVerificationHash() => - r'b4b34909ed1af3f28877ea457d53a4a18b6417f8'; - -/// See also [BackupVerification]. -@ProviderFor(BackupVerification) -final backupVerificationProvider = - AutoDisposeNotifierProvider.internal( - BackupVerification.new, - name: r'backupVerificationProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$backupVerificationHash, - dependencies: null, - allTransitiveDependencies: null, - ); - -typedef _$BackupVerification = AutoDisposeNotifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/backup/error_backup_list.provider.dart b/mobile/lib/providers/backup/error_backup_list.provider.dart deleted file mode 100644 index db116e4bb9..0000000000 --- a/mobile/lib/providers/backup/error_backup_list.provider.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; - -class ErrorBackupListNotifier extends StateNotifier> { - ErrorBackupListNotifier() : super({}); - - add(ErrorUploadAsset errorAsset) { - state = state.union({errorAsset}); - } - - remove(ErrorUploadAsset errorAsset) { - state = state.difference({errorAsset}); - } - - empty() { - state = {}; - } -} - -final errorBackupListProvider = StateNotifierProvider>( - (ref) => ErrorBackupListNotifier(), -); diff --git a/mobile/lib/providers/backup/ios_background_settings.provider.dart b/mobile/lib/providers/backup/ios_background_settings.provider.dart deleted file mode 100644 index 98d55882cc..0000000000 --- a/mobile/lib/providers/backup/ios_background_settings.provider.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/services/background.service.dart'; - -class IOSBackgroundSettings { - final bool appRefreshEnabled; - final int numberOfBackgroundTasksQueued; - final DateTime? timeOfLastFetch; - final DateTime? timeOfLastProcessing; - - const IOSBackgroundSettings({ - required this.appRefreshEnabled, - required this.numberOfBackgroundTasksQueued, - this.timeOfLastFetch, - this.timeOfLastProcessing, - }); -} - -class IOSBackgroundSettingsNotifier extends StateNotifier { - final BackgroundService _service; - IOSBackgroundSettingsNotifier(this._service) : super(null); - - IOSBackgroundSettings? get settings => state; - - Future refresh() async { - final lastFetchTime = await _service.getIOSBackupLastRun(IosBackgroundTask.fetch); - final lastProcessingTime = await _service.getIOSBackupLastRun(IosBackgroundTask.processing); - int numberOfProcesses = await _service.getIOSBackupNumberOfProcesses(); - final appRefreshEnabled = await _service.getIOSBackgroundAppRefreshEnabled(); - - // If this is enabled and there are no background processes, - // the user just enabled app refresh in Settings. - // But we don't have any background services running, since it was disabled - // before. - if (await _service.isBackgroundBackupEnabled() && numberOfProcesses == 0) { - // We need to restart the background service - await _service.enableService(); - numberOfProcesses = await _service.getIOSBackupNumberOfProcesses(); - } - - final settings = IOSBackgroundSettings( - appRefreshEnabled: appRefreshEnabled, - numberOfBackgroundTasksQueued: numberOfProcesses, - timeOfLastFetch: lastFetchTime, - timeOfLastProcessing: lastProcessingTime, - ); - - state = settings; - return settings; - } -} - -final iOSBackgroundSettingsProvider = StateNotifierProvider( - (ref) => IOSBackgroundSettingsNotifier(ref.watch(backgroundServiceProvider)), -); diff --git a/mobile/lib/providers/backup/manual_upload.provider.dart b/mobile/lib/providers/backup/manual_upload.provider.dart deleted file mode 100644 index 40efcd7422..0000000000 --- a/mobile/lib/providers/backup/manual_upload.provider.dart +++ /dev/null @@ -1,391 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/widgets.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/manual_upload_state.model.dart'; -import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; -import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; -import 'package:immich_mobile/providers/gallery_permission.provider.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:immich_mobile/services/backup.service.dart'; -import 'package:immich_mobile/services/backup_album.service.dart'; -import 'package:immich_mobile/services/local_notification.service.dart'; -import 'package:immich_mobile/utils/backup_progress.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:logging/logging.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; - -final manualUploadProvider = StateNotifierProvider((ref) { - return ManualUploadNotifier( - ref.watch(localNotificationService), - ref.watch(backupProvider.notifier), - ref.watch(backupServiceProvider), - ref.watch(backupAlbumServiceProvider), - ref, - ); -}); - -class ManualUploadNotifier extends StateNotifier { - final Logger _log = Logger("ManualUploadNotifier"); - final LocalNotificationService _localNotificationService; - final BackupNotifier _backupProvider; - final BackupService _backupService; - final BackupAlbumService _backupAlbumService; - final Ref ref; - Completer? _cancelToken; - - ManualUploadNotifier( - this._localNotificationService, - this._backupProvider, - this._backupService, - this._backupAlbumService, - this.ref, - ) : super( - ManualUploadState( - progressInPercentage: 0, - progressInFileSize: "0 B / 0 B", - progressInFileSpeed: 0, - progressInFileSpeeds: const [], - progressInFileSpeedUpdateTime: DateTime.now(), - progressInFileSpeedUpdateSentBytes: 0, - currentUploadAsset: CurrentUploadAsset( - id: '...', - fileCreatedAt: DateTime.parse('2020-10-04'), - fileName: '...', - fileType: '...', - ), - totalAssetsToUpload: 0, - successfulUploads: 0, - currentAssetIndex: 0, - showDetailedNotification: false, - ), - ); - - String _lastPrintedDetailContent = ''; - String? _lastPrintedDetailTitle; - - static const notifyInterval = Duration(milliseconds: 500); - late final ThrottleProgressUpdate _throttledNotifiy = ThrottleProgressUpdate(_updateProgress, notifyInterval); - late final ThrottleProgressUpdate _throttledDetailNotify = ThrottleProgressUpdate( - _updateDetailProgress, - notifyInterval, - ); - - void _updateProgress(String? title, int progress, int total) { - // Guard against throttling calling this method after the upload is done - if (_backupProvider.backupProgress == BackUpProgressEnum.manualInProgress) { - _localNotificationService.showOrUpdateManualUploadStatus( - "backup_background_service_in_progress_notification".tr(), - formatAssetBackupProgress(state.currentAssetIndex, state.totalAssetsToUpload), - maxProgress: state.totalAssetsToUpload, - progress: state.currentAssetIndex, - showActions: true, - ); - } - } - - void _updateDetailProgress(String? title, int progress, int total) { - // Guard against throttling calling this method after the upload is done - if (_backupProvider.backupProgress == BackUpProgressEnum.manualInProgress) { - final String msg = total > 0 ? humanReadableBytesProgress(progress, total) : ""; - // only update if message actually differs (to stop many useless notification updates on large assets or slow connections) - if (msg != _lastPrintedDetailContent || title != _lastPrintedDetailTitle) { - _lastPrintedDetailContent = msg; - _lastPrintedDetailTitle = title; - _localNotificationService.showOrUpdateManualUploadStatus( - title ?? 'Uploading', - msg, - progress: total > 0 ? (progress * 1000) ~/ total : 0, - maxProgress: 1000, - isDetailed: true, - // Detailed noitifcation is displayed for Single asset uploads. Show actions for such case - showActions: state.totalAssetsToUpload == 1, - ); - } - } - } - - void _onAssetUploaded(SuccessUploadAsset result) { - state = state.copyWith(successfulUploads: state.successfulUploads + 1); - _backupProvider.updateDiskInfo(); - } - - void _onAssetUploadError(ErrorUploadAsset errorAssetInfo) { - ref.watch(errorBackupListProvider.notifier).add(errorAssetInfo); - } - - void _onProgress(int sent, int total) { - double lastUploadSpeed = state.progressInFileSpeed; - List lastUploadSpeeds = state.progressInFileSpeeds.toList(); - DateTime lastUpdateTime = state.progressInFileSpeedUpdateTime; - int lastSentBytes = state.progressInFileSpeedUpdateSentBytes; - - final now = DateTime.now(); - final duration = now.difference(lastUpdateTime); - - // Keep the upload speed average span limited, to keep it somewhat relevant - if (lastUploadSpeeds.length > 10) { - lastUploadSpeeds.removeAt(0); - } - - if (duration.inSeconds > 0) { - lastUploadSpeeds.add(((sent - lastSentBytes) / duration.inSeconds).abs().roundToDouble()); - - lastUploadSpeed = lastUploadSpeeds.average.abs().roundToDouble(); - lastUpdateTime = now; - lastSentBytes = sent; - } - - state = state.copyWith( - progressInPercentage: (sent.toDouble() / total.toDouble() * 100), - progressInFileSize: humanReadableFileBytesProgress(sent, total), - progressInFileSpeed: lastUploadSpeed, - progressInFileSpeeds: lastUploadSpeeds, - progressInFileSpeedUpdateTime: lastUpdateTime, - progressInFileSpeedUpdateSentBytes: lastSentBytes, - ); - - if (state.showDetailedNotification) { - final title = "backup_background_service_current_upload_notification".tr( - namedArgs: {'filename': state.currentUploadAsset.fileName}, - ); - _throttledDetailNotify(title: title, progress: sent, total: total); - } - } - - void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) { - state = state.copyWith(currentUploadAsset: currentUploadAsset, currentAssetIndex: state.currentAssetIndex + 1); - if (state.totalAssetsToUpload > 1) { - _throttledNotifiy(); - } - if (state.showDetailedNotification) { - _throttledDetailNotify.title = "backup_background_service_current_upload_notification".tr( - namedArgs: {'filename': currentUploadAsset.fileName}, - ); - _throttledDetailNotify.progress = 0; - _throttledDetailNotify.total = 0; - } - } - - Future _startUpload(Iterable allManualUploads) async { - bool hasErrors = false; - try { - _backupProvider.updateBackupProgress(BackUpProgressEnum.manualInProgress); - - if (ref.read(galleryPermissionNotifier.notifier).hasPermission) { - await ref.read(fileMediaRepositoryProvider).clearFileCache(); - - final allAssetsFromDevice = allManualUploads.where((e) => e.isLocal && !e.isRemote).toList(); - - if (allAssetsFromDevice.length != allManualUploads.length) { - _log.warning( - '[_startUpload] Refreshed upload list -> ${allManualUploads.length - allAssetsFromDevice.length} asset will not be uploaded', - ); - } - - final selectedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.select); - final excludedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.exclude); - - // Get candidates from selected albums and excluded albums - Set candidates = await _backupService.buildUploadCandidates( - selectedBackupAlbums, - excludedBackupAlbums, - useTimeFilter: false, - ); - - // Extrack candidate from allAssetsFromDevice - final uploadAssets = candidates.where( - (candidate) => - allAssetsFromDevice.firstWhereOrNull((asset) => asset.localId == candidate.asset.localId) != null, - ); - - if (uploadAssets.isEmpty) { - dPrint(() => "[_startUpload] No Assets to upload - Abort Process"); - _backupProvider.updateBackupProgress(BackUpProgressEnum.idle); - return false; - } - - state = state.copyWith( - progressInPercentage: 0, - progressInFileSize: "0 B / 0 B", - progressInFileSpeed: 0, - totalAssetsToUpload: uploadAssets.length, - successfulUploads: 0, - currentAssetIndex: 0, - currentUploadAsset: CurrentUploadAsset( - id: '...', - fileCreatedAt: DateTime.parse('2020-10-04'), - fileName: '...', - fileType: '...', - ), - ); - // Reset Error List - ref.watch(errorBackupListProvider.notifier).empty(); - - if (state.totalAssetsToUpload > 1) { - _throttledNotifiy(); - } - - // Show detailed asset if enabled in settings or if a single asset is uploaded - bool showDetailedNotification = - ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.backgroundBackupSingleProgress) || - state.totalAssetsToUpload == 1; - state = state.copyWith(showDetailedNotification: showDetailedNotification); - final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null; - - _cancelToken?.complete(); - _cancelToken = Completer(); - final bool ok = await ref - .read(backupServiceProvider) - .backupAsset( - uploadAssets, - _cancelToken!, - pmProgressHandler: pmProgressHandler, - onSuccess: _onAssetUploaded, - onProgress: _onProgress, - onCurrentAsset: _onSetCurrentBackupAsset, - onError: _onAssetUploadError, - ); - - // Close detailed notification - await _localNotificationService.closeNotification(LocalNotificationService.manualUploadDetailedNotificationID); - - _log.info( - '[_startUpload] Manual Upload Completed - success: ${state.successfulUploads},' - ' failed: ${state.totalAssetsToUpload - state.successfulUploads}', - ); - - // User cancelled upload - if (!ok && _cancelToken == null) { - await _localNotificationService.showOrUpdateManualUploadStatus( - "backup_manual_title".tr(), - "backup_manual_cancelled".tr(), - presentBanner: true, - ); - hasErrors = true; - } else if (state.successfulUploads == 0 || (!ok && _cancelToken != null)) { - await _localNotificationService.showOrUpdateManualUploadStatus( - "backup_manual_title".tr(), - "failed".tr(), - presentBanner: true, - ); - hasErrors = true; - } else { - await _localNotificationService.showOrUpdateManualUploadStatus( - "backup_manual_title".tr(), - "backup_manual_success".tr(), - presentBanner: true, - ); - } - } else { - unawaited(openAppSettings()); - dPrint(() => "[_startUpload] Do not have permission to the gallery"); - } - } catch (e) { - dPrint(() => "ERROR _startUpload: ${e.toString()}"); - hasErrors = true; - } finally { - _backupProvider.updateBackupProgress(BackUpProgressEnum.idle); - _handleAppInActivity(); - await _localNotificationService.closeNotification(LocalNotificationService.manualUploadDetailedNotificationID); - await _backupProvider.notifyBackgroundServiceCanRun(); - } - return !hasErrors; - } - - void _handleAppInActivity() { - final appState = ref.read(appStateProvider.notifier).getAppState(); - // The app is currently in background. Perform the necessary cleanups which - // are on-hold for upload completion - if (appState != AppLifeCycleEnum.active && appState != AppLifeCycleEnum.resumed) { - ref.read(backupProvider.notifier).cancelBackup(); - } - } - - void cancelBackup() { - if (_backupProvider.backupProgress != BackUpProgressEnum.inProgress && - _backupProvider.backupProgress != BackUpProgressEnum.manualInProgress) { - _backupProvider.notifyBackgroundServiceCanRun(); - } - _cancelToken?.complete(); - _cancelToken = null; - if (_backupProvider.backupProgress != BackUpProgressEnum.manualInProgress) { - _backupProvider.updateBackupProgress(BackUpProgressEnum.idle); - } - state = state.copyWith( - progressInPercentage: 0, - progressInFileSize: "0 B / 0 B", - progressInFileSpeed: 0, - progressInFileSpeedUpdateTime: DateTime.now(), - progressInFileSpeedUpdateSentBytes: 0, - ); - } - - Future uploadAssets(BuildContext context, Iterable allManualUploads) async { - // assumes the background service is currently running and - // waits until it has stopped to start the backup. - final bool hasLock = await ref.read(backgroundServiceProvider).acquireLock(); - if (!hasLock) { - dPrint(() => "[uploadAssets] could not acquire lock, exiting"); - ImmichToast.show( - context: context, - msg: "failed".tr(), - toastType: ToastType.info, - gravity: ToastGravity.BOTTOM, - durationInSecond: 3, - ); - return false; - } - - bool showInProgress = false; - - // check if backup is already in process - then return - if (_backupProvider.backupProgress == BackUpProgressEnum.manualInProgress) { - dPrint(() => "[uploadAssets] Manual upload is already running - abort"); - showInProgress = true; - } - - if (_backupProvider.backupProgress == BackUpProgressEnum.inProgress) { - dPrint(() => "[uploadAssets] Auto Backup is already in progress - abort"); - showInProgress = true; - return false; - } - - if (_backupProvider.backupProgress == BackUpProgressEnum.inBackground) { - dPrint(() => "[uploadAssets] Background backup is running - abort"); - showInProgress = true; - } - - if (showInProgress) { - if (context.mounted) { - ImmichToast.show( - context: context, - msg: "backup_manual_in_progress".tr(), - toastType: ToastType.info, - gravity: ToastGravity.BOTTOM, - durationInSecond: 3, - ); - } - return false; - } - - return _startUpload(allManualUploads); - } -} diff --git a/mobile/lib/providers/cast.provider.dart b/mobile/lib/providers/cast.provider.dart index fea95f42aa..b298514d67 100644 --- a/mobile/lib/providers/cast.provider.dart +++ b/mobile/lib/providers/cast.provider.dart @@ -1,6 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart' as old_asset_entity; import 'package:immich_mobile/models/cast/cast_manager_state.dart'; import 'package:immich_mobile/services/gcast.service.dart'; @@ -55,26 +54,6 @@ class CastNotifier extends StateNotifier { _gCastService.loadMedia(asset, reload); } - // TODO: remove this when we migrate to new timeline - void loadMediaOld(old_asset_entity.Asset asset, bool reload) { - final remoteAsset = RemoteAsset( - id: asset.remoteId.toString(), - name: asset.name, - ownerId: asset.ownerId.toString(), - checksum: asset.checksum, - type: asset.type == old_asset_entity.AssetType.image - ? AssetType.image - : asset.type == old_asset_entity.AssetType.video - ? AssetType.video - : AssetType.other, - createdAt: asset.fileCreatedAt, - updatedAt: asset.updatedAt, - isEdited: false, - ); - - _gCastService.loadMedia(remoteAsset, reload); - } - Future connect(CastDestinationType type, dynamic device) async { switch (type) { case CastDestinationType.googleCast: diff --git a/mobile/lib/providers/db.provider.dart b/mobile/lib/providers/db.provider.dart deleted file mode 100644 index e03e037f36..0000000000 --- a/mobile/lib/providers/db.provider.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:isar/isar.dart'; - -// overwritten in main.dart due to async loading -final dbProvider = Provider((_) => throw UnimplementedError()); diff --git a/mobile/lib/providers/image/exceptions/image_loading_exception.dart b/mobile/lib/providers/image/exceptions/image_loading_exception.dart deleted file mode 100644 index 98f633a88f..0000000000 --- a/mobile/lib/providers/image/exceptions/image_loading_exception.dart +++ /dev/null @@ -1,5 +0,0 @@ -/// An exception for the [ImageLoader] and the Immich image providers -class ImageLoadingException implements Exception { - final String message; - const ImageLoadingException(this.message); -} diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index bad0d986d0..434e930dcf 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -5,27 +5,26 @@ import 'package:background_downloader/background_downloader.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/domain/services/asset.service.dart'; import 'package:immich_mobile/models/download/livephotos_medatada.model.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; +import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart' show assetExifProvider; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart'; import 'package:immich_mobile/services/action.service.dart'; import 'package:immich_mobile/services/download.service.dart'; -import 'package:immich_mobile/services/timeline.service.dart'; import 'package:immich_mobile/services/foreground_upload.service.dart'; import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; import 'package:logging/logging.dart'; +import 'package:openapi/api.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -final actionProvider = NotifierProvider( - ActionNotifier.new, - dependencies: [multiSelectProvider, timelineServiceProvider], -); +final actionProvider = NotifierProvider(ActionNotifier.new, dependencies: [multiSelectProvider]); class ActionResult { final int count; @@ -490,6 +489,29 @@ class ActionNotifier extends Notifier { }); } } + + Future applyEdits(ActionSource source, List edits) async { + final ids = _getOwnedRemoteIdsForSource(source); + + if (ids.length != 1) { + _logger.warning('applyEdits called with multiple assets, expected single asset'); + return ActionResult(count: ids.length, success: false, error: 'Expected single asset for applying edits'); + } + + final completer = ref.read(websocketProvider.notifier).waitForEvent("AssetEditReadyV1", (dynamic data) { + final eventAsset = SyncAssetV1.fromJson(data["asset"]); + return eventAsset?.id == ids.first; + }, const Duration(seconds: 10)); + + try { + await _service.applyEdits(ids.first, edits); + await completer; + return const ActionResult(count: 1, success: true); + } catch (error, stack) { + _logger.severe('Failed to apply edits to assets', error, stack); + return ActionResult(count: ids.length, success: false, error: error.toString()); + } + } } extension on Iterable { diff --git a/mobile/lib/providers/infrastructure/db.provider.dart b/mobile/lib/providers/infrastructure/db.provider.dart index d38bcbfb55..2b4ba0129f 100644 --- a/mobile/lib/providers/infrastructure/db.provider.dart +++ b/mobile/lib/providers/infrastructure/db.provider.dart @@ -2,13 +2,6 @@ import 'dart:async'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:isar/isar.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'db.provider.g.dart'; - -@Riverpod(keepAlive: true) -Isar isar(Ref ref) => throw UnimplementedError('isar'); Drift Function(Ref ref) driftOverride(Drift drift) => (ref) { ref.onDispose(() => unawaited(drift.close())); diff --git a/mobile/lib/providers/infrastructure/db.provider.g.dart b/mobile/lib/providers/infrastructure/db.provider.g.dart deleted file mode 100644 index 46abfb66a9..0000000000 --- a/mobile/lib/providers/infrastructure/db.provider.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'db.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$isarHash() => r'69d3a06aa7e69a4381478e03f7956eb07d7f7feb'; - -/// See also [isar]. -@ProviderFor(isar) -final isarProvider = Provider.internal( - isar, - name: r'isarProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$isarHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef IsarRef = ProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/infrastructure/device_asset.provider.dart b/mobile/lib/providers/infrastructure/device_asset.provider.dart deleted file mode 100644 index 7854af016a..0000000000 --- a/mobile/lib/providers/infrastructure/device_asset.provider.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/infrastructure/repositories/device_asset.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; - -final deviceAssetRepositoryProvider = Provider( - (ref) => IsarDeviceAssetRepository(ref.watch(isarProvider)), -); diff --git a/mobile/lib/providers/infrastructure/exif.provider.dart b/mobile/lib/providers/infrastructure/exif.provider.dart deleted file mode 100644 index c126f6cac0..0000000000 --- a/mobile/lib/providers/infrastructure/exif.provider.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'exif.provider.g.dart'; - -@Riverpod(keepAlive: true) -IsarExifRepository exifRepository(Ref ref) => IsarExifRepository(ref.watch(isarProvider)); diff --git a/mobile/lib/providers/infrastructure/exif.provider.g.dart b/mobile/lib/providers/infrastructure/exif.provider.g.dart deleted file mode 100644 index 0261558707..0000000000 --- a/mobile/lib/providers/infrastructure/exif.provider.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'exif.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$exifRepositoryHash() => r'bf4a3f6a50d954a23d317659b4f3e2f381066463'; - -/// See also [exifRepository]. -@ProviderFor(exifRepository) -final exifRepositoryProvider = Provider.internal( - exifRepository, - name: r'exifRepositoryProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$exifRepositoryHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef ExifRepositoryRef = ProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/infrastructure/readonly_mode.provider.dart b/mobile/lib/providers/infrastructure/readonly_mode.provider.dart index 9e96c3cfc4..d503919c90 100644 --- a/mobile/lib/providers/infrastructure/readonly_mode.provider.dart +++ b/mobile/lib/providers/infrastructure/readonly_mode.provider.dart @@ -1,5 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; @@ -14,10 +15,11 @@ class ReadOnlyModeNotifier extends Notifier { } void setMode(bool value) { + final isLoggedIn = ref.read(authProvider).isAuthenticated; _appSettingService.setSetting(AppSettingsEnum.readonlyModeEnabled, value); state = value; - if (value) { + if (value && isLoggedIn) { ref.read(appRouterProvider).navigate(const MainTimelineRoute()); } } diff --git a/mobile/lib/providers/infrastructure/store.provider.dart b/mobile/lib/providers/infrastructure/store.provider.dart index 0bf42f3e8b..f867d30fdc 100644 --- a/mobile/lib/providers/infrastructure/store.provider.dart +++ b/mobile/lib/providers/infrastructure/store.provider.dart @@ -1,13 +1,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'store.provider.g.dart'; -@Riverpod(keepAlive: true) -IsarStoreRepository storeRepository(Ref ref) => IsarStoreRepository(ref.watch(isarProvider)); - @Riverpod(keepAlive: true) StoreService storeService(Ref _) => StoreService.I; diff --git a/mobile/lib/providers/infrastructure/store.provider.g.dart b/mobile/lib/providers/infrastructure/store.provider.g.dart index 98c978cb60..b5af7de3e0 100644 --- a/mobile/lib/providers/infrastructure/store.provider.g.dart +++ b/mobile/lib/providers/infrastructure/store.provider.g.dart @@ -6,23 +6,6 @@ part of 'store.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$storeRepositoryHash() => r'659cb134466e4b0d5f04e2fc93e426350d99545f'; - -/// See also [storeRepository]. -@ProviderFor(storeRepository) -final storeRepositoryProvider = Provider.internal( - storeRepository, - name: r'storeRepositoryProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$storeRepositoryHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef StoreRepositoryRef = ProviderRef; String _$storeServiceHash() => r'250e10497c42df360e9e1f9a618d0b19c1b5b0a0'; /// See also [storeService]. diff --git a/mobile/lib/providers/infrastructure/user.provider.dart b/mobile/lib/providers/infrastructure/user.provider.dart index 922b9866bb..6c3263229e 100644 --- a/mobile/lib/providers/infrastructure/user.provider.dart +++ b/mobile/lib/providers/infrastructure/user.provider.dart @@ -3,7 +3,6 @@ import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/partner.service.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/infrastructure/repositories/partner.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; @@ -14,18 +13,12 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'user.provider.g.dart'; -@Riverpod(keepAlive: true) -IsarUserRepository userRepository(Ref ref) => IsarUserRepository(ref.watch(isarProvider)); - @Riverpod(keepAlive: true) UserApiRepository userApiRepository(Ref ref) => UserApiRepository(ref.watch(apiServiceProvider).usersApi); @Riverpod(keepAlive: true) -UserService userService(Ref ref) => UserService( - isarUserRepository: ref.watch(userRepositoryProvider), - userApiRepository: ref.watch(userApiRepositoryProvider), - storeService: ref.watch(storeServiceProvider), -); +UserService userService(Ref ref) => + UserService(userApiRepository: ref.watch(userApiRepositoryProvider), storeService: ref.watch(storeServiceProvider)); /// Drifts final driftPartnerRepositoryProvider = Provider( diff --git a/mobile/lib/providers/infrastructure/user.provider.g.dart b/mobile/lib/providers/infrastructure/user.provider.g.dart index f9148bf3a7..2e9115dad9 100644 --- a/mobile/lib/providers/infrastructure/user.provider.g.dart +++ b/mobile/lib/providers/infrastructure/user.provider.g.dart @@ -6,23 +6,6 @@ part of 'user.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$userRepositoryHash() => r'538791a4ad126ed086c9db682c67fc5c654d54f3'; - -/// See also [userRepository]. -@ProviderFor(userRepository) -final userRepositoryProvider = Provider.internal( - userRepository, - name: r'userRepositoryProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$userRepositoryHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef UserRepositoryRef = ProviderRef; String _$userApiRepositoryHash() => r'8a7340ca4544c8c6b20225c65bff2abb9e96baa2'; /// See also [userApiRepository]. @@ -40,7 +23,7 @@ final userApiRepositoryProvider = Provider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef UserApiRepositoryRef = ProviderRef; -String _$userServiceHash() => r'181414dddc7891be6237e13d568c287a804228d1'; +String _$userServiceHash() => r'47e607f3b484b51bcb634d47e3cbf1f6ef25da97'; /// See also [userService]. @ProviderFor(userService) diff --git a/mobile/lib/providers/memory.provider.dart b/mobile/lib/providers/memory.provider.dart deleted file mode 100644 index 7fef3060cc..0000000000 --- a/mobile/lib/providers/memory.provider.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/memories/memory.model.dart'; -import 'package:immich_mobile/services/memory.service.dart'; - -final memoryFutureProvider = FutureProvider.autoDispose?>((ref) async { - final service = ref.watch(memoryServiceProvider); - - return await service.getMemoryLane(); -}); diff --git a/mobile/lib/providers/partner.provider.dart b/mobile/lib/providers/partner.provider.dart deleted file mode 100644 index 5a85cea1d4..0000000000 --- a/mobile/lib/providers/partner.provider.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart'; -import 'package:immich_mobile/services/partner.service.dart'; - -class PartnerSharedWithNotifier extends StateNotifier> { - final PartnerService _partnerService; - late final StreamSubscription> streamSub; - - PartnerSharedWithNotifier(this._partnerService) : super([]) { - Function eq = const ListEquality().equals; - _partnerService - .getSharedWith() - .then((partners) { - if (!eq(state, partners)) { - state = partners; - } - }) - .then((_) { - streamSub = _partnerService.watchSharedWith().listen((partners) { - if (!eq(state, partners)) { - state = partners; - } - }); - }); - } - - Future updatePartner(UserDto partner, {required bool inTimeline}) { - return _partnerService.updatePartner(partner, inTimeline: inTimeline); - } - - @override - void dispose() { - if (mounted) { - streamSub.cancel(); - } - super.dispose(); - } -} - -final partnerSharedWithProvider = StateNotifierProvider>((ref) { - return PartnerSharedWithNotifier(ref.watch(partnerServiceProvider)); -}); - -class PartnerSharedByNotifier extends StateNotifier> { - final PartnerService _partnerService; - late final StreamSubscription> streamSub; - - PartnerSharedByNotifier(this._partnerService) : super([]) { - Function eq = const ListEquality().equals; - _partnerService - .getSharedBy() - .then((partners) { - if (!eq(state, partners)) { - state = partners; - } - }) - .then((_) { - streamSub = _partnerService.watchSharedBy().listen((partners) { - if (!eq(state, partners)) { - state = partners; - } - }); - }); - } - - @override - void dispose() { - if (mounted) { - streamSub.cancel(); - } - super.dispose(); - } -} - -final partnerSharedByProvider = StateNotifierProvider>((ref) { - return PartnerSharedByNotifier(ref.watch(partnerServiceProvider)); -}); - -final partnerAvailableProvider = FutureProvider.autoDispose>((ref) async { - final otherUsers = await ref.watch(otherUsersProvider.future); - final currentPartners = ref.watch(partnerSharedByProvider); - final available = Set.of(otherUsers); - available.removeAll(currentPartners); - return available.toList(); -}); diff --git a/mobile/lib/providers/search/all_motion_photos.provider.dart b/mobile/lib/providers/search/all_motion_photos.provider.dart deleted file mode 100644 index 48bc1bb80c..0000000000 --- a/mobile/lib/providers/search/all_motion_photos.provider.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/asset.service.dart'; - -final allMotionPhotosProvider = FutureProvider>((ref) async { - return ref.watch(assetServiceProvider).getMotionAssets(); -}); diff --git a/mobile/lib/providers/search/paginated_search.provider.dart b/mobile/lib/providers/search/paginated_search.provider.dart deleted file mode 100644 index 9a37d83320..0000000000 --- a/mobile/lib/providers/search/paginated_search.provider.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/search/search_result.model.dart'; -import 'package:immich_mobile/services/timeline.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/models/search/search_filter.model.dart'; -import 'package:immich_mobile/services/search.service.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'paginated_search.provider.g.dart'; - -final paginatedSearchProvider = StateNotifierProvider( - (ref) => PaginatedSearchNotifier(ref.watch(searchServiceProvider)), -); - -class PaginatedSearchNotifier extends StateNotifier { - final SearchService _searchService; - - PaginatedSearchNotifier(this._searchService) : super(const SearchResult(assets: [], nextPage: 1)); - - Future search(SearchFilter filter) async { - if (state.nextPage == null) { - return false; - } - - final result = await _searchService.search(filter, state.nextPage!); - - if (result == null) { - return false; - } - - state = SearchResult(assets: [...state.assets, ...result.assets], nextPage: result.nextPage); - - return true; - } - - clear() { - state = const SearchResult(assets: [], nextPage: 1); - } -} - -@riverpod -Future paginatedSearchRenderList(Ref ref) { - final result = ref.watch(paginatedSearchProvider); - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.getTimelineFromAssets(result.assets, GroupAssetsBy.none); -} diff --git a/mobile/lib/providers/search/paginated_search.provider.g.dart b/mobile/lib/providers/search/paginated_search.provider.g.dart deleted file mode 100644 index e984997967..0000000000 --- a/mobile/lib/providers/search/paginated_search.provider.g.dart +++ /dev/null @@ -1,29 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'paginated_search.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$paginatedSearchRenderListHash() => - r'22d715ff7864e5a946be38322ce7813616f899c2'; - -/// See also [paginatedSearchRenderList]. -@ProviderFor(paginatedSearchRenderList) -final paginatedSearchRenderListProvider = - AutoDisposeFutureProvider.internal( - paginatedSearchRenderList, - name: r'paginatedSearchRenderListProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$paginatedSearchRenderListHash, - dependencies: null, - allTransitiveDependencies: null, - ); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef PaginatedSearchRenderListRef = AutoDisposeFutureProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/search/people.provider.dart b/mobile/lib/providers/search/people.provider.dart index 3ff8d67983..1f6f983154 100644 --- a/mobile/lib/providers/search/people.provider.dart +++ b/mobile/lib/providers/search/people.provider.dart @@ -1,9 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/services/person.service.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'people.provider.g.dart'; @@ -17,16 +14,6 @@ Future> getAllPeople(Ref ref) async { return people; } -@riverpod -Future personAssets(Ref ref, String personId) async { - final PersonService personService = ref.read(personServiceProvider); - final assets = await personService.getPersonAssets(personId); - - final settings = ref.read(appSettingsServiceProvider); - final groupBy = GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)]; - return await RenderList.fromAssets(assets, groupBy); -} - @riverpod Future updatePersonName(Ref ref, String personId, String updatedName) async { final PersonService personService = ref.read(personServiceProvider); diff --git a/mobile/lib/providers/search/people.provider.g.dart b/mobile/lib/providers/search/people.provider.g.dart index 9595c36eec..23424c068f 100644 --- a/mobile/lib/providers/search/people.provider.g.dart +++ b/mobile/lib/providers/search/people.provider.g.dart @@ -24,7 +24,7 @@ final getAllPeopleProvider = @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef GetAllPeopleRef = AutoDisposeFutureProviderRef>; -String _$personAssetsHash() => r'c1d35ee0e024bd6915e21bc724be4b458a14bc24'; +String _$updatePersonNameHash() => r'45f7693172de522a227406d8198811434cf2bbbc'; /// Copied from Dart SDK class _SystemHash { @@ -47,126 +47,6 @@ class _SystemHash { } } -/// See also [personAssets]. -@ProviderFor(personAssets) -const personAssetsProvider = PersonAssetsFamily(); - -/// See also [personAssets]. -class PersonAssetsFamily extends Family> { - /// See also [personAssets]. - const PersonAssetsFamily(); - - /// See also [personAssets]. - PersonAssetsProvider call(String personId) { - return PersonAssetsProvider(personId); - } - - @override - PersonAssetsProvider getProviderOverride( - covariant PersonAssetsProvider provider, - ) { - return call(provider.personId); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'personAssetsProvider'; -} - -/// See also [personAssets]. -class PersonAssetsProvider extends AutoDisposeFutureProvider { - /// See also [personAssets]. - PersonAssetsProvider(String personId) - : this._internal( - (ref) => personAssets(ref as PersonAssetsRef, personId), - from: personAssetsProvider, - name: r'personAssetsProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$personAssetsHash, - dependencies: PersonAssetsFamily._dependencies, - allTransitiveDependencies: - PersonAssetsFamily._allTransitiveDependencies, - personId: personId, - ); - - PersonAssetsProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.personId, - }) : super.internal(); - - final String personId; - - @override - Override overrideWith( - FutureOr Function(PersonAssetsRef provider) create, - ) { - return ProviderOverride( - origin: this, - override: PersonAssetsProvider._internal( - (ref) => create(ref as PersonAssetsRef), - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - personId: personId, - ), - ); - } - - @override - AutoDisposeFutureProviderElement createElement() { - return _PersonAssetsProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is PersonAssetsProvider && other.personId == personId; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, personId.hashCode); - - return _SystemHash.finish(hash); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin PersonAssetsRef on AutoDisposeFutureProviderRef { - /// The parameter `personId` of this provider. - String get personId; -} - -class _PersonAssetsProviderElement - extends AutoDisposeFutureProviderElement - with PersonAssetsRef { - _PersonAssetsProviderElement(super.provider); - - @override - String get personId => (origin as PersonAssetsProvider).personId; -} - -String _$updatePersonNameHash() => r'45f7693172de522a227406d8198811434cf2bbbc'; - /// See also [updatePersonName]. @ProviderFor(updatePersonName) const updatePersonNameProvider = UpdatePersonNameFamily(); diff --git a/mobile/lib/providers/search/recently_taken_asset.provider.dart b/mobile/lib/providers/search/recently_taken_asset.provider.dart deleted file mode 100644 index 157e7c2a74..0000000000 --- a/mobile/lib/providers/search/recently_taken_asset.provider.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/asset.service.dart'; - -final recentlyTakenAssetProvider = FutureProvider>((ref) async { - final assetService = ref.read(assetServiceProvider); - - return assetService.getRecentlyTakenAssets(); -}); diff --git a/mobile/lib/providers/timeline.provider.dart b/mobile/lib/providers/timeline.provider.dart deleted file mode 100644 index 71ea308dbf..0000000000 --- a/mobile/lib/providers/timeline.provider.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/locale_provider.dart'; -import 'package:immich_mobile/services/timeline.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; - -final singleUserTimelineProvider = StreamProvider.family((ref, userId) { - if (userId == null) { - return const Stream.empty(); - } - - ref.watch(localeProvider); - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchHomeTimeline(userId); -}, dependencies: [localeProvider]); - -final multiUsersTimelineProvider = StreamProvider.family>((ref, userIds) { - ref.watch(localeProvider); - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchMultiUsersTimeline(userIds); -}, dependencies: [localeProvider]); - -final albumTimelineProvider = StreamProvider.autoDispose.family((ref, id) { - final album = ref.watch(albumWatcher(id)).value; - final timelineService = ref.watch(timelineServiceProvider); - - if (album != null) { - return timelineService.watchAlbumTimeline(album); - } - - return const Stream.empty(); -}); - -final archiveTimelineProvider = StreamProvider((ref) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchArchiveTimeline(); -}); - -final favoriteTimelineProvider = StreamProvider((ref) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchFavoriteTimeline(); -}); - -final trashTimelineProvider = StreamProvider((ref) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchTrashTimeline(); -}); - -final allVideosTimelineProvider = StreamProvider((ref) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchAllVideosTimeline(); -}); - -final assetSelectionTimelineProvider = StreamProvider((ref) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchAssetSelectionTimeline(); -}); - -final assetsTimelineProvider = FutureProvider.family>((ref, assets) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.getTimelineFromAssets(assets, null); -}); - -final lockedTimelineProvider = StreamProvider((ref) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchLockedTimelineProvider(); -}); diff --git a/mobile/lib/providers/trash.provider.dart b/mobile/lib/providers/trash.provider.dart deleted file mode 100644 index 41b9160b9b..0000000000 --- a/mobile/lib/providers/trash.provider.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/trash.service.dart'; -import 'package:logging/logging.dart'; - -class TrashNotifier extends StateNotifier { - final TrashService _trashService; - final _log = Logger('TrashNotifier'); - - TrashNotifier(this._trashService) : super(false); - - Future emptyTrash() async { - try { - await _trashService.emptyTrash(); - state = true; - } catch (error, stack) { - _log.severe("Cannot empty trash", error, stack); - state = false; - } - } - - Future restoreAssets(Iterable assetList) async { - try { - await _trashService.restoreAssets(assetList); - return true; - } catch (error, stack) { - _log.severe("Cannot restore assets", error, stack); - return false; - } - } - - Future restoreTrash() async { - try { - await _trashService.restoreTrash(); - state = true; - } catch (error, stack) { - _log.severe("Cannot restore trash", error, stack); - state = false; - } - } -} - -final trashProvider = StateNotifierProvider((ref) { - return TrashNotifier(ref.watch(trashServiceProvider)); -}); diff --git a/mobile/lib/providers/user.provider.dart b/mobile/lib/providers/user.provider.dart index 10dcb2aff5..5a56b65793 100644 --- a/mobile/lib/providers/user.provider.dart +++ b/mobile/lib/providers/user.provider.dart @@ -1,11 +1,9 @@ import 'dart:async'; -import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/services/timeline.service.dart'; class CurrentUserProvider extends StateNotifier { CurrentUserProvider(this._userService) : super(null) { @@ -32,28 +30,3 @@ class CurrentUserProvider extends StateNotifier { final currentUserProvider = StateNotifierProvider((ref) { return CurrentUserProvider(ref.watch(userServiceProvider)); }); - -class TimelineUserIdsProvider extends StateNotifier> { - TimelineUserIdsProvider(this._timelineService) : super([]) { - final listEquality = const ListEquality(); - _timelineService.getTimelineUserIds().then((users) => state = users); - streamSub = _timelineService.watchTimelineUserIds().listen((users) { - if (!listEquality.equals(state, users)) { - state = users; - } - }); - } - - late final StreamSubscription> streamSub; - final TimelineService _timelineService; - - @override - void dispose() { - streamSub.cancel(); - super.dispose(); - } -} - -final timelineUsersIdsProvider = StateNotifierProvider>((ref) { - return TimelineUserIdsProvider(ref.watch(timelineServiceProvider)); -}); diff --git a/mobile/lib/providers/websocket.provider.dart b/mobile/lib/providers/websocket.provider.dart index 6643404786..c79f40a25d 100644 --- a/mobile/lib/providers/websocket.provider.dart +++ b/mobile/lib/providers/websocket.provider.dart @@ -1,60 +1,27 @@ import 'dart:async'; -import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/models/server_info/server_version.model.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/services/sync.service.dart'; import 'package:immich_mobile/utils/debounce.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; import 'package:socket_io_client/socket_io_client.dart'; -enum PendingAction { assetDelete, assetUploaded, assetHidden, assetTrash } - -class PendingChange { - final String id; - final PendingAction action; - final dynamic value; - - const PendingChange(this.id, this.action, this.value); - - @override - String toString() => 'PendingChange(id: $id, action: $action, value: $value)'; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is PendingChange && other.id == id && other.action == action; - } - - @override - int get hashCode => id.hashCode ^ action.hashCode; -} - class WebsocketState { final Socket? socket; final bool isConnected; - final List pendingChanges; - const WebsocketState({this.socket, required this.isConnected, required this.pendingChanges}); + const WebsocketState({this.socket, required this.isConnected}); - WebsocketState copyWith({Socket? socket, bool? isConnected, List? pendingChanges}) { - return WebsocketState( - socket: socket ?? this.socket, - isConnected: isConnected ?? this.isConnected, - pendingChanges: pendingChanges ?? this.pendingChanges, - ); + WebsocketState copyWith({Socket? socket, bool? isConnected}) { + return WebsocketState(socket: socket ?? this.socket, isConnected: isConnected ?? this.isConnected); } @override @@ -72,11 +39,10 @@ class WebsocketState { } class WebsocketNotifier extends StateNotifier { - WebsocketNotifier(this._ref) : super(const WebsocketState(socket: null, isConnected: false, pendingChanges: [])); + WebsocketNotifier(this._ref) : super(const WebsocketState(socket: null, isConnected: false)); final _log = Logger('WebsocketNotifier'); final Ref _ref; - final Debouncer _debounce = Debouncer(interval: const Duration(milliseconds: 500)); final Debouncer _batchDebouncer = Debouncer( interval: const Duration(seconds: 5), @@ -115,32 +81,21 @@ class WebsocketNotifier extends StateNotifier { socket.onConnect((_) { dPrint(() => "Established Websocket Connection"); - state = WebsocketState(isConnected: true, socket: socket, pendingChanges: state.pendingChanges); + state = WebsocketState(isConnected: true, socket: socket); }); socket.onDisconnect((_) { dPrint(() => "Disconnect to Websocket Connection"); - state = WebsocketState(isConnected: false, socket: null, pendingChanges: state.pendingChanges); + state = const WebsocketState(isConnected: false, socket: null); }); socket.on('error', (errorMessage) { _log.severe("Websocket Error - $errorMessage"); - state = WebsocketState(isConnected: false, socket: null, pendingChanges: state.pendingChanges); + state = const WebsocketState(isConnected: false, socket: null); }); - if (!Store.isBetaTimelineEnabled) { - socket.on('on_upload_success', _handleOnUploadSuccess); - socket.on('on_asset_delete', _handleOnAssetDelete); - socket.on('on_asset_trash', _handleOnAssetTrash); - socket.on('on_asset_restore', _handleServerUpdates); - socket.on('on_asset_update', _handleServerUpdates); - socket.on('on_asset_stack_update', _handleServerUpdates); - socket.on('on_asset_hidden', _handleOnAssetHidden); - } else { - socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReady); - socket.on('AssetEditReadyV1', _handleSyncAssetEditReady); - } - + socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReady); + socket.on('AssetEditReadyV1', _handleSyncAssetEditReady); socket.on('on_config_update', _handleOnConfigUpdate); socket.on('on_new_release', _handleReleaseUpdates); } catch (e) { @@ -155,109 +110,28 @@ class WebsocketNotifier extends StateNotifier { _batchedAssetUploadReady.clear(); state.socket?.dispose(); - state = WebsocketState(isConnected: false, socket: null, pendingChanges: state.pendingChanges); + state = const WebsocketState(isConnected: false, socket: null); } - void stopListenToEvent(String eventName) { - state.socket?.off(eventName); - } + Future waitForEvent(String event, bool Function(dynamic)? predicate, Duration timeout) { + final completer = Completer(); - void stopListenToOldEvents() { - state.socket?.off('on_upload_success'); - state.socket?.off('on_asset_delete'); - state.socket?.off('on_asset_trash'); - state.socket?.off('on_asset_restore'); - state.socket?.off('on_asset_update'); - state.socket?.off('on_asset_stack_update'); - state.socket?.off('on_asset_hidden'); - } - - void startListeningToOldEvents() { - state.socket?.on('on_upload_success', _handleOnUploadSuccess); - state.socket?.on('on_asset_delete', _handleOnAssetDelete); - state.socket?.on('on_asset_trash', _handleOnAssetTrash); - state.socket?.on('on_asset_restore', _handleServerUpdates); - state.socket?.on('on_asset_update', _handleServerUpdates); - state.socket?.on('on_asset_stack_update', _handleServerUpdates); - state.socket?.on('on_asset_hidden', _handleOnAssetHidden); - } - - void stopListeningToBetaEvents() { - state.socket?.off('AssetUploadReadyV1'); - state.socket?.off('AssetEditReadyV1'); - } - - void startListeningToBetaEvents() { - state.socket?.on('AssetUploadReadyV1', _handleSyncAssetUploadReady); - state.socket?.on('AssetEditReadyV1', _handleSyncAssetEditReady); - } - - void listenUploadEvent() { - dPrint(() => "Start listening to event on_upload_success"); - state.socket?.on('on_upload_success', _handleOnUploadSuccess); - } - - void addPendingChange(PendingAction action, dynamic value) { - final now = DateTime.now(); - state = state.copyWith( - pendingChanges: [...state.pendingChanges, PendingChange(now.millisecondsSinceEpoch.toString(), action, value)], - ); - _debounce.run(handlePendingChanges); - } - - Future _handlePendingTrashes() async { - final trashChanges = state.pendingChanges.where((c) => c.action == PendingAction.assetTrash).toList(); - if (trashChanges.isNotEmpty) { - List remoteIds = trashChanges.expand((a) => (a.value as List).map((e) => e.toString())).toList(); - - await _ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds); - await _ref.read(assetProvider.notifier).getAllAsset(); - - state = state.copyWith(pendingChanges: state.pendingChanges.whereNot((c) => trashChanges.contains(c)).toList()); - } - } - - Future _handlePendingDeletes() async { - final deleteChanges = state.pendingChanges.where((c) => c.action == PendingAction.assetDelete).toList(); - if (deleteChanges.isNotEmpty) { - List remoteIds = deleteChanges.map((a) => a.value.toString()).toList(); - await _ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds); - state = state.copyWith(pendingChanges: state.pendingChanges.whereNot((c) => deleteChanges.contains(c)).toList()); - } - } - - Future _handlePendingUploaded() async { - final uploadedChanges = state.pendingChanges.where((c) => c.action == PendingAction.assetUploaded).toList(); - if (uploadedChanges.isNotEmpty) { - List remoteAssets = uploadedChanges.map((a) => AssetResponseDto.fromJson(a.value)).toList(); - for (final dto in remoteAssets) { - if (dto != null) { - final newAsset = Asset.remote(dto); - await _ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset); - } + void handler(dynamic data) { + if (predicate == null || predicate(data)) { + completer.complete(); + state.socket?.off(event, handler); } - state = state.copyWith( - pendingChanges: state.pendingChanges.whereNot((c) => uploadedChanges.contains(c)).toList(), - ); } - } - Future _handlingPendingHidden() async { - final hiddenChanges = state.pendingChanges.where((c) => c.action == PendingAction.assetHidden).toList(); - if (hiddenChanges.isNotEmpty) { - List remoteIds = hiddenChanges.map((a) => a.value.toString()).toList(); - final db = _ref.watch(dbProvider); - await db.writeTxn(() => db.assets.deleteAllByRemoteId(remoteIds)); + state.socket?.on(event, handler); - state = state.copyWith(pendingChanges: state.pendingChanges.whereNot((c) => hiddenChanges.contains(c)).toList()); - } - } - - Future handlePendingChanges() async { - await _handlePendingUploaded(); - await _handlePendingDeletes(); - await _handlingPendingHidden(); - await _handlePendingTrashes(); + return completer.future.timeout( + timeout, + onTimeout: () { + state.socket?.off(event, handler); + completer.completeError(TimeoutException("Timeout waiting for event: $event")); + }, + ); } void _handleOnConfigUpdate(dynamic _) { @@ -265,21 +139,6 @@ class WebsocketNotifier extends StateNotifier { _ref.read(serverInfoProvider.notifier).getServerConfig(); } - // Refresh updated assets - void _handleServerUpdates(dynamic _) { - _ref.read(assetProvider.notifier).getAllAsset(); - } - - void _handleOnUploadSuccess(dynamic data) => addPendingChange(PendingAction.assetUploaded, data); - - void _handleOnAssetDelete(dynamic data) => addPendingChange(PendingAction.assetDelete, data); - - void _handleOnAssetTrash(dynamic data) { - addPendingChange(PendingAction.assetTrash, data); - } - - void _handleOnAssetHidden(dynamic data) => addPendingChange(PendingAction.assetHidden, data); - _handleReleaseUpdates(dynamic data) { // Json guard if (data is! Map) { diff --git a/mobile/lib/repositories/album.repository.dart b/mobile/lib/repositories/album.repository.dart deleted file mode 100644 index 2d24004944..0000000000 --- a/mobile/lib/repositories/album.repository.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; -import 'package:immich_mobile/models/albums/album_search.model.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -enum AlbumSort { remoteId, localId } - -final albumRepositoryProvider = Provider((ref) => AlbumRepository(ref.watch(dbProvider))); - -class AlbumRepository extends DatabaseRepository { - const AlbumRepository(super.db); - - Future count({bool? local}) { - final baseQuery = db.albums.where(); - final QueryBuilder query = switch (local) { - null => baseQuery.noOp(), - true => baseQuery.localIdIsNotNull(), - false => baseQuery.remoteIdIsNotNull(), - }; - return query.count(); - } - - Future create(Album album) => txn(() => db.albums.store(album)); - - Future getByName(String name, {bool? shared, bool? remote, bool? owner}) { - var query = db.albums.filter().nameEqualTo(name); - if (shared != null) { - query = query.sharedEqualTo(shared); - } - final isarUserId = fastHash(Store.get(StoreKey.currentUser).id); - if (owner == true) { - query = query.owner((q) => q.isarIdEqualTo(isarUserId)); - } else if (owner == false) { - query = query.owner((q) => q.not().isarIdEqualTo(isarUserId)); - } - if (remote == true) { - query = query.localIdIsNull(); - } else if (remote == false) { - query = query.remoteIdIsNull(); - } - return query.findFirst(); - } - - Future update(Album album) => txn(() => db.albums.store(album)); - - Future delete(int albumId) => txn(() => db.albums.delete(albumId)); - - Future> getAll({bool? shared, bool? remote, int? ownerId, AlbumSort? sortBy}) { - final baseQuery = db.albums.where(); - final QueryBuilder afterWhere; - if (remote == null) { - afterWhere = baseQuery.noOp(); - } else if (remote) { - afterWhere = baseQuery.remoteIdIsNotNull(); - } else { - afterWhere = baseQuery.localIdIsNotNull(); - } - QueryBuilder filterQuery = afterWhere.filter().noOp(); - if (shared != null) { - filterQuery = filterQuery.sharedEqualTo(true); - } - if (ownerId != null) { - filterQuery = filterQuery.owner((q) => q.isarIdEqualTo(ownerId)); - } - final QueryBuilder query = switch (sortBy) { - null => filterQuery.noOp(), - AlbumSort.remoteId => filterQuery.sortByRemoteId(), - AlbumSort.localId => filterQuery.sortByLocalId(), - }; - return query.findAll(); - } - - Future get(int id) => db.albums.get(id); - - Future getByRemoteId(String remoteId) { - return db.albums.filter().remoteIdEqualTo(remoteId).findFirst(); - } - - Future removeUsers(Album album, List users) => - txn(() => album.sharedUsers.update(unlink: users.map(entity.User.fromDto))); - - Future addAssets(Album album, List assets) => txn(() => album.assets.update(link: assets)); - - Future removeAssets(Album album, List assets) => txn(() => album.assets.update(unlink: assets)); - - Future recalculateMetadata(Album album) async { - album.startDate = await album.assets.filter().fileCreatedAtProperty().min(); - album.endDate = await album.assets.filter().fileCreatedAtProperty().max(); - album.lastModifiedAssetTimestamp = await album.assets.filter().updatedAtProperty().max(); - return album; - } - - Future addUsers(Album album, List users) => - txn(() => album.sharedUsers.update(link: users.map(entity.User.fromDto))); - - Future deleteAllLocal() => txn(() => db.albums.where().localIdIsNotNull().deleteAll()); - - Future> search(String searchTerm, QuickFilterMode filterMode) async { - var query = db.albums.filter().nameContains(searchTerm, caseSensitive: false).remoteIdIsNotNull(); - final isarUserId = fastHash(Store.get(StoreKey.currentUser).id); - - switch (filterMode) { - case QuickFilterMode.sharedWithMe: - query = query.owner((q) => q.not().isarIdEqualTo(isarUserId)); - case QuickFilterMode.myAlbums: - query = query.owner((q) => q.isarIdEqualTo(isarUserId)); - case QuickFilterMode.all: - break; - } - - return await query.findAll(); - } - - Future clearTable() async { - await txn(() async { - await db.albums.clear(); - }); - } - - Stream> watchRemoteAlbums() { - return db.albums.where().remoteIdIsNotNull().watch(); - } - - Stream> watchLocalAlbums() { - return db.albums.where().localIdIsNotNull().watch(); - } - - Stream watchAlbum(int id) { - return db.albums.watchObject(id, fireImmediately: true); - } -} diff --git a/mobile/lib/repositories/album_api.repository.dart b/mobile/lib/repositories/album_api.repository.dart deleted file mode 100644 index 525f0906ba..0000000000 --- a/mobile/lib/repositories/album_api.repository.dart +++ /dev/null @@ -1,171 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/album/album.model.dart' show AlbumAssetOrder, RemoteAlbum; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; -import 'package:immich_mobile/infrastructure/utils/user.converter.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/repositories/api.repository.dart'; -import 'package:openapi/api.dart'; - -final albumApiRepositoryProvider = Provider((ref) => AlbumApiRepository(ref.watch(apiServiceProvider).albumsApi)); - -class AlbumApiRepository extends ApiRepository { - final AlbumsApi _api; - - AlbumApiRepository(this._api); - - Future get(String id) async { - final dto = await checkNull(_api.getAlbumInfo(id)); - return _toAlbum(dto); - } - - Future> getAll({bool? shared}) async { - final dtos = await checkNull(_api.getAllAlbums(shared: shared)); - return dtos.map(_toAlbum).toList(); - } - - Future create( - String name, { - required Iterable assetIds, - Iterable sharedUserIds = const [], - String? description, - }) async { - final users = sharedUserIds.map((id) => AlbumUserCreateDto(userId: id, role: AlbumUserRole.editor)); - final responseDto = await checkNull( - _api.createAlbum( - CreateAlbumDto( - albumName: name, - description: description, - assetIds: assetIds.toList(), - albumUsers: users.toList(), - ), - ), - ); - return _toAlbum(responseDto); - } - - // TODO: Change name after removing old method - Future createDriftAlbum(String name, {required Iterable assetIds, String? description}) async { - final responseDto = await checkNull( - _api.createAlbum(CreateAlbumDto(albumName: name, description: description, assetIds: assetIds.toList())), - ); - - return _toRemoteAlbum(responseDto); - } - - Future update( - String albumId, { - String? name, - String? thumbnailAssetId, - String? description, - bool? activityEnabled, - SortOrder? sortOrder, - }) async { - AssetOrder? order; - if (sortOrder != null) { - order = sortOrder == SortOrder.asc ? AssetOrder.asc : AssetOrder.desc; - } - - final response = await checkNull( - _api.updateAlbumInfo( - albumId, - UpdateAlbumDto( - albumName: name, - albumThumbnailAssetId: thumbnailAssetId, - description: description, - isActivityEnabled: activityEnabled, - order: order, - ), - ), - ); - - return _toAlbum(response); - } - - Future delete(String albumId) { - return _api.deleteAlbum(albumId); - } - - Future<({List added, List duplicates})> addAssets(String albumId, Iterable assetIds) async { - final response = await checkNull(_api.addAssetsToAlbum(albumId, BulkIdsDto(ids: assetIds.toList()))); - - final List added = []; - final List duplicates = []; - - for (final result in response) { - if (result.success) { - added.add(result.id); - } else if (result.error == BulkIdResponseDtoErrorEnum.duplicate) { - duplicates.add(result.id); - } - } - return (added: added, duplicates: duplicates); - } - - Future<({List removed, List failed})> removeAssets(String albumId, Iterable assetIds) async { - final response = await checkNull(_api.removeAssetFromAlbum(albumId, BulkIdsDto(ids: assetIds.toList()))); - final List removed = [], failed = []; - for (final dto in response) { - if (dto.success) { - removed.add(dto.id); - } else { - failed.add(dto.id); - } - } - return (removed: removed, failed: failed); - } - - Future addUsers(String albumId, Iterable userIds) async { - final albumUsers = userIds.map((userId) => AlbumUserAddDto(userId: userId)).toList(); - final response = await checkNull(_api.addUsersToAlbum(albumId, AddUsersDto(albumUsers: albumUsers))); - return _toAlbum(response); - } - - Future removeUser(String albumId, {required String userId}) { - return _api.removeUserFromAlbum(albumId, userId); - } - - static Album _toAlbum(AlbumResponseDto dto) { - final Album album = Album( - remoteId: dto.id, - name: dto.albumName, - createdAt: dto.createdAt, - modifiedAt: dto.updatedAt, - lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp, - shared: dto.shared, - startDate: dto.startDate, - description: dto.description, - endDate: dto.endDate, - activityEnabled: dto.isActivityEnabled, - sortOrder: dto.order == AssetOrder.asc ? SortOrder.asc : SortOrder.desc, - ); - album.remoteAssetCount = dto.assetCount; - album.owner.value = entity.User.fromDto(UserConverter.fromSimpleUserDto(dto.owner)); - album.remoteThumbnailAssetId = dto.albumThumbnailAssetId; - final users = dto.albumUsers.map((albumUser) => UserConverter.fromSimpleUserDto(albumUser.user)); - album.sharedUsers.addAll(users.map(entity.User.fromDto)); - final assets = dto.assets.map(Asset.remote).toList(); - album.assets.addAll(assets); - - return album; - } - - static RemoteAlbum _toRemoteAlbum(AlbumResponseDto dto) { - return RemoteAlbum( - id: dto.id, - name: dto.albumName, - ownerId: dto.owner.id, - description: dto.description, - createdAt: dto.createdAt, - updatedAt: dto.updatedAt, - thumbnailAssetId: dto.albumThumbnailAssetId, - isActivityEnabled: dto.isActivityEnabled, - order: dto.order == AssetOrder.asc ? AlbumAssetOrder.asc : AlbumAssetOrder.desc, - assetCount: dto.assetCount, - ownerName: dto.owner.name, - isShared: dto.albumUsers.length > 2, - ); - } -} diff --git a/mobile/lib/repositories/album_media.repository.dart b/mobile/lib/repositories/album_media.repository.dart deleted file mode 100644 index 89860f4e75..0000000000 --- a/mobile/lib/repositories/album_media.repository.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/repositories/asset_media.repository.dart'; -import 'package:photo_manager/photo_manager.dart' hide AssetType; - -final albumMediaRepositoryProvider = Provider((ref) => const AlbumMediaRepository()); - -class AlbumMediaRepository { - const AlbumMediaRepository(); - - bool get useCustomFilter => Store.get(StoreKey.photoManagerCustomFilter, true); - - FilterOptionGroup? _getAlbumFilter({ - DateTimeCond? updateTimeCond, - bool? containsPathModified, - List? orderBy, - }) => useCustomFilter - ? FilterOptionGroup( - imageOption: const FilterOption(needTitle: true, sizeConstraint: SizeConstraint(ignoreSize: true)), - videoOption: const FilterOption( - needTitle: true, - sizeConstraint: SizeConstraint(ignoreSize: true), - durationConstraint: DurationConstraint(allowNullable: true), - ), - containsPathModified: containsPathModified ?? false, - createTimeCond: DateTimeCond.def().copyWith(ignore: true), - updateTimeCond: updateTimeCond ?? DateTimeCond.def().copyWith(ignore: true), - orders: orderBy ?? [], - ) - : null; - - Future> getAll() async { - final filter = useCustomFilter - ? CustomFilter.sql(where: '${CustomColumns.base.width} > 0') - : FilterOptionGroup(containsPathModified: true); - - final List assetPathEntities = await PhotoManager.getAssetPathList( - hasAll: true, - filterOption: filter, - ); - return assetPathEntities.map(_toAlbum).toList(); - } - - Future> getAssetIds(String albumId) async { - final album = await AssetPathEntity.fromId(albumId, filterOption: _getAlbumFilter()); - final List assets = await album.getAssetListRange(start: 0, end: 0x7fffffffffffffff); - return assets.map((e) => e.id).toList(); - } - - Future getAssetCount(String albumId) async { - final album = await AssetPathEntity.fromId(albumId, filterOption: _getAlbumFilter()); - return album.assetCountAsync; - } - - Future> getAssets( - String albumId, { - int start = 0, - int end = 0x7fffffffffffffff, - DateTime? modifiedFrom, - DateTime? modifiedUntil, - bool orderByModificationDate = false, - }) async { - final onDevice = await AssetPathEntity.fromId( - albumId, - filterOption: _getAlbumFilter( - updateTimeCond: modifiedFrom == null && modifiedUntil == null - ? null - : DateTimeCond(min: modifiedFrom ?? DateTime.utc(-271820), max: modifiedUntil ?? DateTime.utc(275760)), - orderBy: orderByModificationDate ? [const OrderOption(type: OrderOptionType.updateDate)] : [], - ), - ); - - final List assets = await onDevice.getAssetListRange(start: start, end: end); - return assets.map(AssetMediaRepository.toAsset).toList().cast(); - } - - Future get(String id) async { - final assetPathEntity = await AssetPathEntity.fromId(id, filterOption: _getAlbumFilter(containsPathModified: true)); - return _toAlbum(assetPathEntity); - } - - static Album _toAlbum(AssetPathEntity assetPathEntity) { - final Album album = Album( - name: assetPathEntity.name, - createdAt: assetPathEntity.lastModified?.toUtc() ?? DateTime.now().toUtc(), - modifiedAt: assetPathEntity.lastModified?.toUtc() ?? DateTime.now().toUtc(), - shared: false, - activityEnabled: false, - ); - album.owner.value = User.fromDto(Store.get(StoreKey.currentUser)); - album.localId = assetPathEntity.id; - album.isAll = assetPathEntity.isAll; - return album; - } -} diff --git a/mobile/lib/repositories/asset.repository.dart b/mobile/lib/repositories/asset.repository.dart deleted file mode 100644 index 79af8b4921..0000000000 --- a/mobile/lib/repositories/asset.repository.dart +++ /dev/null @@ -1,220 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -enum AssetSort { checksum, ownerIdChecksum } - -final assetRepositoryProvider = Provider((ref) => AssetRepository(ref.watch(dbProvider))); - -class AssetRepository extends DatabaseRepository { - const AssetRepository(super.db); - - Future> getByAlbum( - Album album, { - Iterable notOwnedBy = const [], - String? ownerId, - AssetState? state, - AssetSort? sortBy, - }) { - var query = album.assets.filter(); - final isarUserIds = notOwnedBy.map(fastHash).toList(); - if (notOwnedBy.length == 1) { - query = query.not().ownerIdEqualTo(isarUserIds.first); - } else if (notOwnedBy.isNotEmpty) { - query = query.not().anyOf(isarUserIds, (q, int id) => q.ownerIdEqualTo(id)); - } - if (ownerId != null) { - query = query.ownerIdEqualTo(fastHash(ownerId)); - } - - if (state != null) { - query = switch (state) { - AssetState.local => query.remoteIdIsNull(), - AssetState.remote => query.localIdIsNull(), - AssetState.merged => query.localIdIsNotNull().remoteIdIsNotNull(), - }; - } - - final QueryBuilder sortedQuery = switch (sortBy) { - null => query.noOp(), - AssetSort.checksum => query.sortByChecksum(), - AssetSort.ownerIdChecksum => query.sortByOwnerId().thenByChecksum(), - }; - - return sortedQuery.findAll(); - } - - Future deleteByIds(List ids) => txn(() async { - await db.assets.deleteAll(ids); - await db.exifInfos.deleteAll(ids); - }); - - Future getByRemoteId(String id) => db.assets.getByRemoteId(id); - - Future> getAllByRemoteId(Iterable ids, {AssetState? state}) async { - if (ids.isEmpty) { - return []; - } - - return _getAllByRemoteIdImpl(ids, state).findAll(); - } - - QueryBuilder _getAllByRemoteIdImpl(Iterable ids, AssetState? state) { - final query = db.assets.remote(ids).filter(); - return switch (state) { - null => query.noOp(), - AssetState.local => query.remoteIdIsNull(), - AssetState.remote => query.localIdIsNull(), - AssetState.merged => query.localIdIsNotEmpty().remoteIdIsNotNull(), - }; - } - - Future> getAll({required String ownerId, AssetState? state, AssetSort? sortBy, int? limit}) { - final baseQuery = db.assets.where(); - final isarUserIds = fastHash(ownerId); - final QueryBuilder filteredQuery = switch (state) { - null => baseQuery.ownerIdEqualToAnyChecksum(isarUserIds).noOp(), - AssetState.local => baseQuery.remoteIdIsNull().filter().localIdIsNotNull().ownerIdEqualTo(isarUserIds), - AssetState.remote => baseQuery.localIdIsNull().filter().remoteIdIsNotNull().ownerIdEqualTo(isarUserIds), - AssetState.merged => - baseQuery.ownerIdEqualToAnyChecksum(isarUserIds).filter().remoteIdIsNotNull().localIdIsNotNull(), - }; - - final QueryBuilder query = switch (sortBy) { - null => filteredQuery.noOp(), - AssetSort.checksum => filteredQuery.sortByChecksum(), - AssetSort.ownerIdChecksum => filteredQuery.sortByOwnerId().thenByChecksum(), - }; - - return limit == null ? query.findAll() : query.limit(limit).findAll(); - } - - Future> updateAll(List assets) async { - await txn(() => db.assets.putAll(assets)); - return assets; - } - - Future> getMatches({ - required List assets, - required String ownerId, - AssetState? state, - int limit = 100, - }) { - final baseQuery = db.assets.where(); - final QueryBuilder query = switch (state) { - null => baseQuery.noOp(), - AssetState.local => baseQuery.remoteIdIsNull().filter().localIdIsNotNull(), - AssetState.remote => baseQuery.localIdIsNull().filter().remoteIdIsNotNull(), - AssetState.merged => baseQuery.localIdIsNotNull().filter().remoteIdIsNotNull(), - }; - return _getMatchesImpl(query, fastHash(ownerId), assets, limit); - } - - Future update(Asset asset) async { - await txn(() => asset.put(db)); - return asset; - } - - Future upsertDuplicatedAssets(Iterable duplicatedAssets) => - txn(() => db.duplicatedAssets.putAll(duplicatedAssets.map(DuplicatedAsset.new).toList())); - - Future> getAllDuplicatedAssetIds() => db.duplicatedAssets.where().idProperty().findAll(); - - Future getByOwnerIdChecksum(int ownerId, String checksum) => - db.assets.getByOwnerIdChecksum(ownerId, checksum); - - Future> getAllByOwnerIdChecksum(List ownerIds, List checksums) => - db.assets.getAllByOwnerIdChecksum(ownerIds, checksums); - - Future> getAllLocal() => db.assets.where().localIdIsNotNull().findAll(); - - Future deleteAllByRemoteId(List ids, {AssetState? state}) => - txn(() => _getAllByRemoteIdImpl(ids, state).deleteAll()); - - Future> getStackAssets(String stackId) { - return db.assets - .filter() - .isArchivedEqualTo(false) - .isTrashedEqualTo(false) - .stackIdEqualTo(stackId) - // orders primary asset first as its ID is null - .sortByStackPrimaryAssetId() - .thenByFileCreatedAtDesc() - .findAll(); - } - - Future clearTable() async { - await txn(() async { - await db.assets.clear(); - }); - } - - Stream watchAsset(int id, {bool fireImmediately = false}) { - return db.assets.watchObject(id, fireImmediately: fireImmediately); - } - - Future> getTrashAssets(String userId) { - return db.assets - .where() - .remoteIdIsNotNull() - .filter() - .ownerIdEqualTo(fastHash(userId)) - .isTrashedEqualTo(true) - .findAll(); - } - - Future> getRecentlyTakenAssets(String userId) { - return db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .visibilityEqualTo(AssetVisibilityEnum.timeline) - .sortByFileCreatedAtDesc() - .findAll(); - } - - Future> getMotionAssets(String userId) { - return db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .visibilityEqualTo(AssetVisibilityEnum.timeline) - .livePhotoVideoIdIsNotNull() - .findAll(); - } -} - -Future> _getMatchesImpl( - QueryBuilder query, - int ownerId, - List assets, - int limit, -) => query - .ownerIdEqualTo(ownerId) - .anyOf( - assets, - (q, Asset a) => q - .fileNameEqualTo(a.fileName) - .and() - .durationInSecondsEqualTo(a.durationInSeconds) - .and() - .fileCreatedAtBetween( - a.fileCreatedAt.subtract(const Duration(hours: 12)), - a.fileCreatedAt.add(const Duration(hours: 12)), - ) - .and() - .not() - .checksumEqualTo(a.checksum), - ) - .sortByFileName() - .thenByFileCreatedAt() - .thenByFileModifiedAt() - .limit(limit) - .findAll(); diff --git a/mobile/lib/repositories/asset_api.repository.dart b/mobile/lib/repositories/asset_api.repository.dart index 011b1edc94..2943177d60 100644 --- a/mobile/lib/repositories/asset_api.repository.dart +++ b/mobile/lib/repositories/asset_api.repository.dart @@ -1,8 +1,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:http/http.dart'; import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart' hide AssetEditAction; import 'package:immich_mobile/domain/models/stack.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/repositories/api.repository.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -11,7 +11,6 @@ import 'package:openapi/api.dart'; final assetApiRepositoryProvider = Provider( (ref) => AssetApiRepository( ref.watch(apiServiceProvider).assetsApi, - ref.watch(apiServiceProvider).searchApi, ref.watch(apiServiceProvider).stacksApi, ref.watch(apiServiceProvider).trashApi, ), @@ -19,32 +18,10 @@ final assetApiRepositoryProvider = Provider( class AssetApiRepository extends ApiRepository { final AssetsApi _api; - final SearchApi _searchApi; final StacksApi _stacksApi; final TrashApi _trashApi; - AssetApiRepository(this._api, this._searchApi, this._stacksApi, this._trashApi); - - Future update(String id, {String? description}) async { - final response = await checkNull(_api.updateAsset(id, UpdateAssetDto(description: description))); - return Asset.remote(response); - } - - Future> search({List personIds = const []}) async { - // TODO this always fetches all assets, change API and usage to actually do pagination - final List result = []; - bool hasNext = true; - int currentPage = 1; - while (hasNext) { - final response = await checkNull( - _searchApi.searchAssets(MetadataSearchDto(personIds: personIds, page: currentPage, size: 1000)), - ); - result.addAll(response.assets.items.map(Asset.remote)); - hasNext = response.assets.nextPage != null; - currentPage++; - } - return result; - } + AssetApiRepository(this._api, this._stacksApi, this._trashApi); Future delete(List ids, bool force) async { return _api.deleteAssets(AssetBulkDeleteDto(ids: ids, force: force)); @@ -105,6 +82,14 @@ class AssetApiRepository extends ApiRepository { Future updateRating(String assetId, int rating) { return _api.updateAsset(assetId, UpdateAssetDto(rating: rating)); } + + Future editAsset(String assetId, List edits) { + return _api.editAsset(assetId, AssetEditsCreateDto(edits: edits.map((e) => e.toApi()).toList())); + } + + Future removeEdits(String assetId) async { + return _api.removeAssetEdits(assetId); + } } extension on StackResponseDto { @@ -112,3 +97,22 @@ extension on StackResponseDto { return StackResponse(id: id, primaryAssetId: primaryAssetId, assetIds: assets.map((asset) => asset.id).toList()); } } + +extension on AssetEdit { + AssetEditActionItemDto toApi() { + return switch (this) { + CropEdit(:final parameters) => AssetEditActionItemDto( + action: AssetEditAction.crop, + parameters: parameters.toJson(), + ), + RotateEdit(:final parameters) => AssetEditActionItemDto( + action: AssetEditAction.rotate, + parameters: parameters.toJson(), + ), + MirrorEdit(:final parameters) => AssetEditActionItemDto( + action: AssetEditAction.mirror, + parameters: parameters.toJson(), + ), + }; + } +} diff --git a/mobile/lib/repositories/asset_media.repository.dart b/mobile/lib/repositories/asset_media.repository.dart index fecfe6df4d..a2d8bfe162 100644 --- a/mobile/lib/repositories/asset_media.repository.dart +++ b/mobile/lib/repositories/asset_media.repository.dart @@ -5,15 +5,10 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/widgets.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/exif.model.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart' as asset_entity; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/response_extensions.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; -import 'package:immich_mobile/utils/hash.dart'; import 'package:logging/logging.dart'; import 'package:path_provider/path_provider.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -50,39 +45,9 @@ class AssetMediaRepository { return PhotoManager.editor.deleteWithIds(ids); } - Future get(String id) async { + Future get(String id) async { final entity = await AssetEntity.fromId(id); - return toAsset(entity); - } - - static asset_entity.Asset? toAsset(AssetEntity? local) { - if (local == null) return null; - - final asset_entity.Asset asset = asset_entity.Asset( - checksum: "", - localId: local.id, - ownerId: fastHash(Store.get(StoreKey.currentUser).id), - fileCreatedAt: local.createDateTime, - fileModifiedAt: local.modifiedDateTime, - updatedAt: local.modifiedDateTime, - durationInSeconds: local.duration, - type: asset_entity.AssetType.values[local.typeInt], - fileName: local.title!, - width: local.width, - height: local.height, - isFavorite: local.isFavorite, - ); - - if (asset.fileCreatedAt.year == 1970) { - asset.fileCreatedAt = asset.fileModifiedAt; - } - - if (local.latitude != null) { - asset.exifInfo = ExifInfo(latitude: local.latitude, longitude: local.longitude); - } - - asset.local = local; - return asset; + return entity; } Future getOriginalFilename(String id) async { diff --git a/mobile/lib/repositories/auth.repository.dart b/mobile/lib/repositories/auth.repository.dart index a8544ef6c0..c16b728ae5 100644 --- a/mobile/lib/repositories/auth.repository.dart +++ b/mobile/lib/repositories/auth.repository.dart @@ -2,40 +2,21 @@ import 'dart:convert'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -final authRepositoryProvider = Provider( - (ref) => AuthRepository(ref.watch(dbProvider), ref.watch(driftProvider)), -); +final authRepositoryProvider = Provider((ref) => AuthRepository(ref.watch(driftProvider))); -class AuthRepository extends DatabaseRepository { +class AuthRepository { final Drift _drift; - const AuthRepository(super.db, this._drift); + const AuthRepository(this._drift); Future clearLocalData() async { await SyncStreamRepository(_drift).reset(); - - return db.writeTxn(() { - return Future.wait([ - db.assets.clear(), - db.exifInfos.clear(), - db.albums.clear(), - db.eTags.clear(), - db.users.clear(), - ]); - }); } bool getEndpointSwitchingFeature() { diff --git a/mobile/lib/repositories/backup.repository.dart b/mobile/lib/repositories/backup.repository.dart deleted file mode 100644 index 6cee6a4427..0000000000 --- a/mobile/lib/repositories/backup.repository.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:isar/isar.dart'; - -enum BackupAlbumSort { id } - -final backupAlbumRepositoryProvider = Provider((ref) => BackupAlbumRepository(ref.watch(dbProvider))); - -class BackupAlbumRepository extends DatabaseRepository { - const BackupAlbumRepository(super.db); - - Future> getAll({BackupAlbumSort? sort}) { - final baseQuery = db.backupAlbums.where(); - final QueryBuilder query = switch (sort) { - null => baseQuery.noOp(), - BackupAlbumSort.id => baseQuery.sortById(), - }; - return query.findAll(); - } - - Future> getIdsBySelection(BackupSelection backup) => - db.backupAlbums.filter().selectionEqualTo(backup).idProperty().findAll(); - - Future> getAllBySelection(BackupSelection backup) => - db.backupAlbums.filter().selectionEqualTo(backup).findAll(); - - Future deleteAll(List ids) => txn(() => db.backupAlbums.deleteAll(ids)); - - Future updateAll(List backupAlbums) => txn(() => db.backupAlbums.putAll(backupAlbums)); -} diff --git a/mobile/lib/repositories/database.repository.dart b/mobile/lib/repositories/database.repository.dart deleted file mode 100644 index 71c15e1c40..0000000000 --- a/mobile/lib/repositories/database.repository.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'dart:async'; -import 'package:immich_mobile/interfaces/database.interface.dart'; -import 'package:isar/isar.dart'; - -/// copied from Isar; needed to check if an async transaction is already active -const Symbol _zoneTxn = #zoneTxn; - -abstract class DatabaseRepository implements IDatabaseRepository { - final Isar db; - const DatabaseRepository(this.db); - - bool get inTxn => Zone.current[_zoneTxn] != null; - - Future txn(Future Function() callback) => inTxn ? callback() : transaction(callback); - - @override - Future transaction(Future Function() callback) => db.writeTxn(callback); -} - -extension Asd on QueryBuilder { - QueryBuilder noOp() { - // ignore: invalid_use_of_protected_member - return QueryBuilder.apply(this, (query) => query); - } -} diff --git a/mobile/lib/repositories/etag.repository.dart b/mobile/lib/repositories/etag.repository.dart deleted file mode 100644 index 768d95b95c..0000000000 --- a/mobile/lib/repositories/etag.repository.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:isar/isar.dart'; - -final etagRepositoryProvider = Provider((ref) => ETagRepository(ref.watch(dbProvider))); - -class ETagRepository extends DatabaseRepository { - const ETagRepository(super.db); - - Future> getAllIds() => db.eTags.where().idProperty().findAll(); - - Future get(String id) => db.eTags.getById(id); - - Future upsertAll(List etags) => txn(() => db.eTags.putAll(etags)); - - Future deleteByIds(List ids) => txn(() => db.eTags.deleteAllById(ids)); - - Future getById(String id) => db.eTags.getById(id); - - Future clearTable() async { - await txn(() async { - await db.eTags.clear(); - }); - } -} diff --git a/mobile/lib/repositories/file_media.repository.dart b/mobile/lib/repositories/file_media.repository.dart index f5cdb6d5c0..c54813a757 100644 --- a/mobile/lib/repositories/file_media.repository.dart +++ b/mobile/lib/repositories/file_media.repository.dart @@ -3,18 +3,12 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart' hide AssetType; -import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:photo_manager/photo_manager.dart' hide AssetType; final fileMediaRepositoryProvider = Provider((ref) => const FileMediaRepository()); class FileMediaRepository { const FileMediaRepository(); - Future saveImage(Uint8List data, {required String title, String? relativePath}) async { - final entity = await PhotoManager.editor.saveImage(data, filename: title, title: title, relativePath: relativePath); - return AssetMediaRepository.toAsset(entity); - } Future saveLocalAsset(Uint8List data, {required String title, String? relativePath}) async { final entity = await PhotoManager.editor.saveImage(data, filename: title, title: title, relativePath: relativePath); @@ -30,24 +24,18 @@ class FileMediaRepository { ); } - Future saveImageWithFile(String filePath, {String? title, String? relativePath}) async { + Future saveImageWithFile(String filePath, {String? title, String? relativePath}) async { final entity = await PhotoManager.editor.saveImageWithPath(filePath, title: title, relativePath: relativePath); - return AssetMediaRepository.toAsset(entity); + return entity; } - Future saveLivePhoto({required File image, required File video, required String title}) async { + Future saveLivePhoto({required File image, required File video, required String title}) async { final entity = await PhotoManager.editor.darwin.saveLivePhoto(imageFile: image, videoFile: video, title: title); - return AssetMediaRepository.toAsset(entity); + return entity; } - Future saveVideo(File file, {required String title, String? relativePath}) async { + Future saveVideo(File file, {required String title, String? relativePath}) async { final entity = await PhotoManager.editor.saveVideo(file, title: title, relativePath: relativePath); - return AssetMediaRepository.toAsset(entity); + return entity; } - - Future clearFileCache() => PhotoManager.clearFileCache(); - - Future enableBackgroundAccess() => PhotoManager.setIgnorePermissionCheck(true); - - Future requestExtendedPermissions() => PhotoManager.requestPermissionExtend(); } diff --git a/mobile/lib/repositories/partner.repository.dart b/mobile/lib/repositories/partner.repository.dart deleted file mode 100644 index 7f5ce62e0c..0000000000 --- a/mobile/lib/repositories/partner.repository.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:isar/isar.dart'; - -final partnerRepositoryProvider = Provider((ref) => PartnerRepository(ref.watch(dbProvider))); - -class PartnerRepository extends DatabaseRepository { - const PartnerRepository(super.db); - - Future> getSharedBy() async { - return (await db.users.filter().isPartnerSharedByEqualTo(true).sortById().findAll()).map((u) => u.toDto()).toList(); - } - - Future> getSharedWith() async { - return (await db.users.filter().isPartnerSharedWithEqualTo(true).sortById().findAll()) - .map((u) => u.toDto()) - .toList(); - } - - Stream> watchSharedBy() { - return (db.users.filter().isPartnerSharedByEqualTo(true).sortById().watch()).map( - (users) => users.map((u) => u.toDto()).toList(), - ); - } - - Stream> watchSharedWith() { - return (db.users.filter().isPartnerSharedWithEqualTo(true).sortById().watch()).map( - (users) => users.map((u) => u.toDto()).toList(), - ); - } -} diff --git a/mobile/lib/repositories/partner_api.repository.dart b/mobile/lib/repositories/partner_api.repository.dart index d497da4d4c..69b6740cbe 100644 --- a/mobile/lib/repositories/partner_api.repository.dart +++ b/mobile/lib/repositories/partner_api.repository.dart @@ -21,8 +21,8 @@ class PartnerApiRepository extends ApiRepository { return response.map(UserConverter.fromPartnerDto).toList(); } - Future create(String id) async { - final dto = await checkNull(_api.createPartnerDeprecated(id)); + Future create(String sharedWithId) async { + final dto = await checkNull(_api.createPartner(PartnerCreateDto(sharedWithId: sharedWithId))); return UserConverter.fromPartnerDto(dto); } diff --git a/mobile/lib/repositories/timeline.repository.dart b/mobile/lib/repositories/timeline.repository.dart deleted file mode 100644 index c8c173b6f6..0000000000 --- a/mobile/lib/repositories/timeline.repository.dart +++ /dev/null @@ -1,146 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:isar/isar.dart'; - -final timelineRepositoryProvider = Provider((ref) => TimelineRepository(ref.watch(dbProvider))); - -class TimelineRepository extends DatabaseRepository { - const TimelineRepository(super.db); - - Future> getTimelineUserIds(String id) { - return db.users.filter().inTimelineEqualTo(true).or().idEqualTo(id).idProperty().findAll(); - } - - Stream> watchTimelineUsers(String id) { - return db.users.filter().inTimelineEqualTo(true).or().idEqualTo(id).idProperty().watch(); - } - - Stream watchArchiveTimeline(String userId) { - final query = db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .isTrashedEqualTo(false) - .visibilityEqualTo(AssetVisibilityEnum.archive) - .sortByFileCreatedAtDesc(); - - return _watchRenderList(query, GroupAssetsBy.none); - } - - Stream watchFavoriteTimeline(String userId) { - final query = db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .isFavoriteEqualTo(true) - .not() - .visibilityEqualTo(AssetVisibilityEnum.locked) - .isTrashedEqualTo(false) - .sortByFileCreatedAtDesc(); - - return _watchRenderList(query, GroupAssetsBy.none); - } - - Stream watchAlbumTimeline(Album album, GroupAssetsBy groupAssetByOption) { - final query = album.assets.filter().isTrashedEqualTo(false).not().visibilityEqualTo(AssetVisibilityEnum.locked); - - final withSortedOption = switch (album.sortOrder) { - SortOrder.asc => query.sortByFileCreatedAt(), - SortOrder.desc => query.sortByFileCreatedAtDesc(), - }; - - return _watchRenderList(withSortedOption, groupAssetByOption); - } - - Stream watchTrashTimeline(String userId) { - final query = db.assets.filter().ownerIdEqualTo(fastHash(userId)).isTrashedEqualTo(true).sortByFileCreatedAtDesc(); - - return _watchRenderList(query, GroupAssetsBy.none); - } - - Stream watchAllVideosTimeline(String userId) { - final query = db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .isTrashedEqualTo(false) - .visibilityEqualTo(AssetVisibilityEnum.timeline) - .typeEqualTo(AssetType.video) - .sortByFileCreatedAtDesc(); - - return _watchRenderList(query, GroupAssetsBy.none); - } - - Stream watchHomeTimeline(String userId, GroupAssetsBy groupAssetByOption) { - final query = db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .isTrashedEqualTo(false) - .stackPrimaryAssetIdIsNull() - .visibilityEqualTo(AssetVisibilityEnum.timeline) - .sortByFileCreatedAtDesc(); - - return _watchRenderList(query, groupAssetByOption); - } - - Stream watchMultiUsersTimeline(List userIds, GroupAssetsBy groupAssetByOption) { - final isarUserIds = userIds.map(fastHash).toList(); - final query = db.assets - .where() - .anyOf(isarUserIds, (qb, id) => qb.ownerIdEqualToAnyChecksum(id)) - .filter() - .isTrashedEqualTo(false) - .visibilityEqualTo(AssetVisibilityEnum.timeline) - .stackPrimaryAssetIdIsNull() - .sortByFileCreatedAtDesc(); - return _watchRenderList(query, groupAssetByOption); - } - - Future getTimelineFromAssets(List assets, GroupAssetsBy getGroupByOption) { - return RenderList.fromAssets(assets, getGroupByOption); - } - - Stream watchAssetSelectionTimeline(String userId) { - final query = db.assets - .where() - .remoteIdIsNotNull() - .filter() - .ownerIdEqualTo(fastHash(userId)) - .visibilityEqualTo(AssetVisibilityEnum.timeline) - .isTrashedEqualTo(false) - .stackPrimaryAssetIdIsNull() - .sortByFileCreatedAtDesc(); - - return _watchRenderList(query, GroupAssetsBy.none); - } - - Stream watchLockedTimeline(String userId, GroupAssetsBy getGroupByOption) { - final query = db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .visibilityEqualTo(AssetVisibilityEnum.locked) - .isTrashedEqualTo(false) - .sortByFileCreatedAtDesc(); - - return _watchRenderList(query, getGroupByOption); - } - - Stream _watchRenderList( - QueryBuilder query, - GroupAssetsBy groupAssetsBy, - ) async* { - yield await RenderList.fromQuery(query, groupAssetsBy); - await for (final _ in query.watchLazy()) { - yield await RenderList.fromQuery(query, groupAssetsBy); - } - } -} diff --git a/mobile/lib/routing/app_navigation_observer.dart b/mobile/lib/routing/app_navigation_observer.dart index b05a28172d..b6b08d7831 100644 --- a/mobile/lib/routing/app_navigation_observer.dart +++ b/mobile/lib/routing/app_navigation_observer.dart @@ -19,7 +19,6 @@ class AppNavigationObserver extends AutoRouterObserver { @override void didPush(Route route, Route? previousRoute) { - _handleLockedViewState(route, previousRoute); _handleDriftLockedFolderState(route, previousRoute); Future(() { ref.read(currentRouteNameProvider.notifier).state = route.settings.name; @@ -28,21 +27,6 @@ class AppNavigationObserver extends AutoRouterObserver { }); } - _handleLockedViewState(Route route, Route? previousRoute) { - final isInLockedView = ref.read(inLockedViewProvider); - final isFromLockedViewToDetailView = - route.settings.name == GalleryViewerRoute.name && previousRoute?.settings.name == LockedRoute.name; - - final isFromDetailViewToInfoPanelView = - route.settings.name == null && previousRoute?.settings.name == GalleryViewerRoute.name && isInLockedView; - - if (route.settings.name == LockedRoute.name || isFromLockedViewToDetailView || isFromDetailViewToInfoPanelView) { - Future(() => ref.read(inLockedViewProvider.notifier).state = true); - } else { - Future(() => ref.read(inLockedViewProvider.notifier).state = false); - } - } - _handleDriftLockedFolderState(Route route, Route? previousRoute) { final isInLockedView = ref.read(inLockedViewProvider); final isFromLockedViewToDetailView = diff --git a/mobile/lib/routing/backup_permission_guard.dart b/mobile/lib/routing/backup_permission_guard.dart deleted file mode 100644 index f52516f2e5..0000000000 --- a/mobile/lib/routing/backup_permission_guard.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:immich_mobile/providers/gallery_permission.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; - -class BackupPermissionGuard extends AutoRouteGuard { - final GalleryPermissionNotifier _permission; - - const BackupPermissionGuard(this._permission); - - @override - void onNavigation(NavigationResolver resolver, StackRouter router) async { - final p = _permission.hasPermission; - if (p) { - resolver.next(true); - } else { - unawaited(router.push(const PermissionOnboardingRoute())); - } - } -} diff --git a/mobile/lib/routing/gallery_guard.dart b/mobile/lib/routing/gallery_guard.dart deleted file mode 100644 index 6a4b1bddab..0000000000 --- a/mobile/lib/routing/gallery_guard.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:immich_mobile/routing/router.dart'; - -/// Handles duplicate navigation to this route (primarily for deep linking) -class GalleryGuard extends AutoRouteGuard { - const GalleryGuard(); - @override - void onNavigation(NavigationResolver resolver, StackRouter router) async { - final newRouteName = resolver.route.name; - final currentTopRouteName = router.stack.isNotEmpty ? router.stack.last.name : null; - - if (currentTopRouteName == newRouteName) { - // Replace instead of pushing duplicate - final args = resolver.route.args as GalleryViewerRouteArgs; - - unawaited( - router.replace( - GalleryViewerRoute( - renderList: args.renderList, - initialIndex: args.initialIndex, - heroOffset: args.heroOffset, - showStack: args.showStack, - ), - ), - ); - // Prevent further navigation since we replaced the route - resolver.next(false); - return; - } - resolver.next(true); - } -} diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index b385bcbf71..9c539a37a6 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -4,78 +4,34 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/domain/models/memory.model.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/models/folder/recursive_folder.model.dart'; -import 'package:immich_mobile/models/memories/memory.model.dart'; -import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/models/shared_link/shared_link.model.dart'; import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart'; -import 'package:immich_mobile/pages/album/album_additional_shared_user_selection.page.dart'; -import 'package:immich_mobile/pages/album/album_asset_selection.page.dart'; -import 'package:immich_mobile/pages/album/album_options.page.dart'; -import 'package:immich_mobile/pages/album/album_shared_user_selection.page.dart'; -import 'package:immich_mobile/pages/album/album_viewer.page.dart'; -import 'package:immich_mobile/pages/albums/albums.page.dart'; -import 'package:immich_mobile/pages/backup/album_preview.page.dart'; -import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart'; -import 'package:immich_mobile/pages/backup/backup_controller.page.dart'; -import 'package:immich_mobile/pages/backup/backup_options.page.dart'; import 'package:immich_mobile/pages/backup/drift_backup.page.dart'; import 'package:immich_mobile/pages/backup/drift_backup_album_selection.page.dart'; import 'package:immich_mobile/pages/backup/drift_backup_asset_detail.page.dart'; import 'package:immich_mobile/pages/backup/drift_backup_options.page.dart'; import 'package:immich_mobile/pages/backup/drift_upload_detail.page.dart'; -import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart'; -import 'package:immich_mobile/pages/common/activities.page.dart'; import 'package:immich_mobile/pages/common/app_log.page.dart'; import 'package:immich_mobile/pages/common/app_log_detail.page.dart'; -import 'package:immich_mobile/pages/common/change_experience.page.dart'; -import 'package:immich_mobile/pages/common/create_album.page.dart'; -import 'package:immich_mobile/pages/common/gallery_viewer.page.dart'; import 'package:immich_mobile/pages/common/headers_settings.page.dart'; -import 'package:immich_mobile/pages/common/native_video_viewer.page.dart'; import 'package:immich_mobile/pages/common/settings.page.dart'; import 'package:immich_mobile/pages/common/splash_screen.page.dart'; -import 'package:immich_mobile/pages/common/tab_controller.page.dart'; import 'package:immich_mobile/pages/common/tab_shell.page.dart'; -import 'package:immich_mobile/pages/editing/crop.page.dart'; -import 'package:immich_mobile/pages/editing/edit.page.dart'; -import 'package:immich_mobile/pages/editing/filter.page.dart'; -import 'package:immich_mobile/pages/library/archive.page.dart'; -import 'package:immich_mobile/pages/library/favorite.page.dart'; import 'package:immich_mobile/pages/library/folder/folder.page.dart'; -import 'package:immich_mobile/pages/library/library.page.dart'; -import 'package:immich_mobile/pages/library/local_albums.page.dart'; -import 'package:immich_mobile/pages/library/locked/locked.page.dart'; import 'package:immich_mobile/pages/library/locked/pin_auth.page.dart'; import 'package:immich_mobile/pages/library/partner/drift_partner.page.dart'; -import 'package:immich_mobile/pages/library/partner/partner.page.dart'; -import 'package:immich_mobile/pages/library/partner/partner_detail.page.dart'; -import 'package:immich_mobile/pages/library/people/people_collection.page.dart'; -import 'package:immich_mobile/pages/library/places/places_collection.page.dart'; import 'package:immich_mobile/pages/library/shared_link/shared_link.page.dart'; import 'package:immich_mobile/pages/library/shared_link/shared_link_edit.page.dart'; -import 'package:immich_mobile/pages/library/trash.page.dart'; import 'package:immich_mobile/pages/login/change_password.page.dart'; import 'package:immich_mobile/pages/login/login.page.dart'; -import 'package:immich_mobile/pages/onboarding/permission_onboarding.page.dart'; -import 'package:immich_mobile/pages/photos/memory.page.dart'; -import 'package:immich_mobile/pages/photos/photos.page.dart'; -import 'package:immich_mobile/pages/search/all_motion_videos.page.dart'; -import 'package:immich_mobile/pages/search/all_people.page.dart'; -import 'package:immich_mobile/pages/search/all_places.page.dart'; -import 'package:immich_mobile/pages/search/all_videos.page.dart'; -import 'package:immich_mobile/pages/search/map/map.page.dart'; import 'package:immich_mobile/pages/search/map/map_location_picker.page.dart'; -import 'package:immich_mobile/pages/search/person_result.page.dart'; -import 'package:immich_mobile/pages/search/recently_taken.page.dart'; -import 'package:immich_mobile/pages/search/search.page.dart'; import 'package:immich_mobile/pages/settings/sync_status.page.dart'; import 'package:immich_mobile/pages/share_intent/share_intent.page.dart'; import 'package:immich_mobile/presentation/pages/cleanup_preview.page.dart'; @@ -105,25 +61,19 @@ import 'package:immich_mobile/presentation/pages/drift_remote_album.page.dart'; import 'package:immich_mobile/presentation/pages/drift_trash.page.dart'; import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart'; import 'package:immich_mobile/presentation/pages/drift_video.page.dart'; -import 'package:immich_mobile/presentation/pages/editing/drift_crop.page.dart'; -import 'package:immich_mobile/presentation/pages/profile/profile_picture_crop.page.dart'; -import 'package:immich_mobile/presentation/pages/editing/drift_edit.page.dart'; -import 'package:immich_mobile/presentation/pages/editing/drift_filter.page.dart'; +import 'package:immich_mobile/presentation/pages/edit/drift_edit.page.dart'; import 'package:immich_mobile/presentation/pages/local_timeline.page.dart'; +import 'package:immich_mobile/presentation/pages/profile/profile_picture_crop.page.dart'; import 'package:immich_mobile/presentation/pages/search/drift_search.page.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/routing/auth_guard.dart'; -import 'package:immich_mobile/routing/backup_permission_guard.dart'; -import 'package:immich_mobile/routing/custom_transition_builders.dart'; import 'package:immich_mobile/routing/duplicate_guard.dart'; -import 'package:immich_mobile/routing/gallery_guard.dart'; import 'package:immich_mobile/routing/locked_guard.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/local_auth.service.dart'; import 'package:immich_mobile/services/secure_storage.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; part 'router.gr.dart'; @@ -141,9 +91,7 @@ final appRouterProvider = Provider( class AppRouter extends RootStackRouter { late final AuthGuard _authGuard; late final DuplicateGuard _duplicateGuard; - late final BackupPermissionGuard _backupPermissionGuard; late final LockedGuard _lockedGuard; - late final GalleryGuard _galleryGuard; AppRouter( ApiService apiService, @@ -154,8 +102,6 @@ class AppRouter extends RootStackRouter { _authGuard = AuthGuard(apiService); _duplicateGuard = const DuplicateGuard(); _lockedGuard = LockedGuard(apiService, secureStorageService, localAuthService); - _backupPermissionGuard = BackupPermissionGuard(galleryPermissionNotifier); - _galleryGuard = const GalleryGuard(); } @override @@ -164,20 +110,8 @@ class AppRouter extends RootStackRouter { @override late final List routes = [ AutoRoute(page: SplashScreenRoute.page, initial: true), - AutoRoute(page: PermissionOnboardingRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: LoginRoute.page), AutoRoute(page: ChangePasswordRoute.page), - AutoRoute(page: SearchRoute.page, guards: [_authGuard, _duplicateGuard], maintainState: false), - AutoRoute( - page: TabControllerRoute.page, - guards: [_authGuard, _duplicateGuard], - children: [ - AutoRoute(page: PhotosRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: SearchRoute.page, guards: [_authGuard, _duplicateGuard], maintainState: false), - AutoRoute(page: LibraryRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: AlbumsRoute.page, guards: [_authGuard, _duplicateGuard]), - ], - ), AutoRoute( page: TabShellRoute.page, guards: [_authGuard, _duplicateGuard], @@ -188,105 +122,17 @@ class AppRouter extends RootStackRouter { AutoRoute(page: DriftAlbumsRoute.page, guards: [_authGuard, _duplicateGuard]), ], ), - CustomRoute( - page: GalleryViewerRoute.page, - guards: [_authGuard, _galleryGuard], - transitionsBuilder: CustomTransitionsBuilders.zoomedPage, - ), - AutoRoute(page: BackupControllerRoute.page, guards: [_authGuard, _duplicateGuard, _backupPermissionGuard]), - AutoRoute(page: AllPlacesRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: CreateAlbumRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: EditImageRoute.page), - AutoRoute(page: CropImageRoute.page), - AutoRoute(page: FilterImageRoute.page), AutoRoute(page: ProfilePictureCropRoute.page), - CustomRoute( - page: FavoritesRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), - AutoRoute(page: AllVideosRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: AllMotionPhotosRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: RecentlyTakenRoute.page, guards: [_authGuard, _duplicateGuard]), - CustomRoute( - page: AlbumAssetSelectionRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideBottom, - ), - CustomRoute( - page: AlbumSharedUserSelectionRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideBottom, - ), - AutoRoute(page: AlbumViewerRoute.page, guards: [_authGuard, _duplicateGuard]), - CustomRoute( - page: AlbumAdditionalSharedUserSelectionRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideBottom, - ), - AutoRoute(page: BackupAlbumSelectionRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: AlbumPreviewRoute.page, guards: [_authGuard, _duplicateGuard]), - CustomRoute( - page: FailedBackupStatusRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideBottom, - ), AutoRoute(page: SettingsRoute.page, guards: [_duplicateGuard]), AutoRoute(page: SettingsSubRoute.page, guards: [_duplicateGuard]), AutoRoute(page: AppLogRoute.page, guards: [_duplicateGuard]), AutoRoute(page: AppLogDetailRoute.page, guards: [_duplicateGuard]), - CustomRoute( - page: ArchiveRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), - CustomRoute( - page: PartnerRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), AutoRoute(page: FolderRoute.page, guards: [_authGuard]), - AutoRoute(page: PartnerDetailRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: PersonResultRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: AllPeopleRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: MemoryRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: MapRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: AlbumOptionsRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: TrashRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: SharedLinkRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: SharedLinkEditRoute.page, guards: [_authGuard, _duplicateGuard]), - CustomRoute( - page: ActivitiesRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - durationInMilliseconds: 200, - ), CustomRoute(page: MapLocationPickerRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: BackupOptionsRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: HeaderSettingsRoute.page, guards: [_duplicateGuard]), - CustomRoute( - page: PeopleCollectionRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), - CustomRoute( - page: AlbumsRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), - CustomRoute( - page: LocalAlbumsRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), - CustomRoute( - page: PlacesCollectionRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), - AutoRoute(page: NativeVideoViewerRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: ShareIntentRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: LockedRoute.page, guards: [_authGuard, _lockedGuard, _duplicateGuard]), AutoRoute(page: PinAuthRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: LocalMediaSummaryRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: RemoteMediaSummaryRoute.page, guards: [_authGuard, _duplicateGuard]), @@ -323,7 +169,6 @@ class AppRouter extends RootStackRouter { AutoRoute(page: DriftPlaceRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftPlaceDetailRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftUserSelectionRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: ChangeExperienceRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftPartnerRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftUploadDetailRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: SyncStatusRoute.page, guards: [_duplicateGuard]), @@ -333,8 +178,6 @@ class AppRouter extends RootStackRouter { AutoRoute(page: DriftAlbumOptionsRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftMapRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftEditImageRoute.page), - AutoRoute(page: DriftCropImageRoute.page), - AutoRoute(page: DriftFilterImageRoute.page), AutoRoute(page: DriftActivitiesRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftBackupAssetDetailRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: AssetTroubleshootRoute.page, guards: [_authGuard, _duplicateGuard]), diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 2d57c16573..07c3a52b49 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -10,330 +10,6 @@ part of 'router.dart'; -/// generated route for -/// [ActivitiesPage] -class ActivitiesRoute extends PageRouteInfo { - const ActivitiesRoute({List? children}) - : super(ActivitiesRoute.name, initialChildren: children); - - static const String name = 'ActivitiesRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const ActivitiesPage(); - }, - ); -} - -/// generated route for -/// [AlbumAdditionalSharedUserSelectionPage] -class AlbumAdditionalSharedUserSelectionRoute - extends PageRouteInfo { - AlbumAdditionalSharedUserSelectionRoute({ - Key? key, - required Album album, - List? children, - }) : super( - AlbumAdditionalSharedUserSelectionRoute.name, - args: AlbumAdditionalSharedUserSelectionRouteArgs( - key: key, - album: album, - ), - initialChildren: children, - ); - - static const String name = 'AlbumAdditionalSharedUserSelectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AlbumAdditionalSharedUserSelectionPage( - key: args.key, - album: args.album, - ); - }, - ); -} - -class AlbumAdditionalSharedUserSelectionRouteArgs { - const AlbumAdditionalSharedUserSelectionRouteArgs({ - this.key, - required this.album, - }); - - final Key? key; - - final Album album; - - @override - String toString() { - return 'AlbumAdditionalSharedUserSelectionRouteArgs{key: $key, album: $album}'; - } -} - -/// generated route for -/// [AlbumAssetSelectionPage] -class AlbumAssetSelectionRoute - extends PageRouteInfo { - AlbumAssetSelectionRoute({ - Key? key, - required Set existingAssets, - bool canDeselect = false, - List? children, - }) : super( - AlbumAssetSelectionRoute.name, - args: AlbumAssetSelectionRouteArgs( - key: key, - existingAssets: existingAssets, - canDeselect: canDeselect, - ), - initialChildren: children, - ); - - static const String name = 'AlbumAssetSelectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AlbumAssetSelectionPage( - key: args.key, - existingAssets: args.existingAssets, - canDeselect: args.canDeselect, - ); - }, - ); -} - -class AlbumAssetSelectionRouteArgs { - const AlbumAssetSelectionRouteArgs({ - this.key, - required this.existingAssets, - this.canDeselect = false, - }); - - final Key? key; - - final Set existingAssets; - - final bool canDeselect; - - @override - String toString() { - return 'AlbumAssetSelectionRouteArgs{key: $key, existingAssets: $existingAssets, canDeselect: $canDeselect}'; - } -} - -/// generated route for -/// [AlbumOptionsPage] -class AlbumOptionsRoute extends PageRouteInfo { - const AlbumOptionsRoute({List? children}) - : super(AlbumOptionsRoute.name, initialChildren: children); - - static const String name = 'AlbumOptionsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AlbumOptionsPage(); - }, - ); -} - -/// generated route for -/// [AlbumPreviewPage] -class AlbumPreviewRoute extends PageRouteInfo { - AlbumPreviewRoute({ - Key? key, - required Album album, - List? children, - }) : super( - AlbumPreviewRoute.name, - args: AlbumPreviewRouteArgs(key: key, album: album), - initialChildren: children, - ); - - static const String name = 'AlbumPreviewRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AlbumPreviewPage(key: args.key, album: args.album); - }, - ); -} - -class AlbumPreviewRouteArgs { - const AlbumPreviewRouteArgs({this.key, required this.album}); - - final Key? key; - - final Album album; - - @override - String toString() { - return 'AlbumPreviewRouteArgs{key: $key, album: $album}'; - } -} - -/// generated route for -/// [AlbumSharedUserSelectionPage] -class AlbumSharedUserSelectionRoute - extends PageRouteInfo { - AlbumSharedUserSelectionRoute({ - Key? key, - required Set assets, - List? children, - }) : super( - AlbumSharedUserSelectionRoute.name, - args: AlbumSharedUserSelectionRouteArgs(key: key, assets: assets), - initialChildren: children, - ); - - static const String name = 'AlbumSharedUserSelectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AlbumSharedUserSelectionPage(key: args.key, assets: args.assets); - }, - ); -} - -class AlbumSharedUserSelectionRouteArgs { - const AlbumSharedUserSelectionRouteArgs({this.key, required this.assets}); - - final Key? key; - - final Set assets; - - @override - String toString() { - return 'AlbumSharedUserSelectionRouteArgs{key: $key, assets: $assets}'; - } -} - -/// generated route for -/// [AlbumViewerPage] -class AlbumViewerRoute extends PageRouteInfo { - AlbumViewerRoute({ - Key? key, - required int albumId, - List? children, - }) : super( - AlbumViewerRoute.name, - args: AlbumViewerRouteArgs(key: key, albumId: albumId), - initialChildren: children, - ); - - static const String name = 'AlbumViewerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AlbumViewerPage(key: args.key, albumId: args.albumId); - }, - ); -} - -class AlbumViewerRouteArgs { - const AlbumViewerRouteArgs({this.key, required this.albumId}); - - final Key? key; - - final int albumId; - - @override - String toString() { - return 'AlbumViewerRouteArgs{key: $key, albumId: $albumId}'; - } -} - -/// generated route for -/// [AlbumsPage] -class AlbumsRoute extends PageRouteInfo { - const AlbumsRoute({List? children}) - : super(AlbumsRoute.name, initialChildren: children); - - static const String name = 'AlbumsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AlbumsPage(); - }, - ); -} - -/// generated route for -/// [AllMotionPhotosPage] -class AllMotionPhotosRoute extends PageRouteInfo { - const AllMotionPhotosRoute({List? children}) - : super(AllMotionPhotosRoute.name, initialChildren: children); - - static const String name = 'AllMotionPhotosRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AllMotionPhotosPage(); - }, - ); -} - -/// generated route for -/// [AllPeoplePage] -class AllPeopleRoute extends PageRouteInfo { - const AllPeopleRoute({List? children}) - : super(AllPeopleRoute.name, initialChildren: children); - - static const String name = 'AllPeopleRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AllPeoplePage(); - }, - ); -} - -/// generated route for -/// [AllPlacesPage] -class AllPlacesRoute extends PageRouteInfo { - const AllPlacesRoute({List? children}) - : super(AllPlacesRoute.name, initialChildren: children); - - static const String name = 'AllPlacesRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AllPlacesPage(); - }, - ); -} - -/// generated route for -/// [AllVideosPage] -class AllVideosRoute extends PageRouteInfo { - const AllVideosRoute({List? children}) - : super(AllVideosRoute.name, initialChildren: children); - - static const String name = 'AllVideosRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AllVideosPage(); - }, - ); -} - /// generated route for /// [AppLogDetailPage] class AppLogDetailRoute extends PageRouteInfo { @@ -387,22 +63,6 @@ class AppLogRoute extends PageRouteInfo { ); } -/// generated route for -/// [ArchivePage] -class ArchiveRoute extends PageRouteInfo { - const ArchiveRoute({List? children}) - : super(ArchiveRoute.name, initialChildren: children); - - static const String name = 'ArchiveRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const ArchivePage(); - }, - ); -} - /// generated route for /// [AssetTroubleshootPage] class AssetTroubleshootRoute extends PageRouteInfo { @@ -504,97 +164,6 @@ class AssetViewerRouteArgs { } } -/// generated route for -/// [BackupAlbumSelectionPage] -class BackupAlbumSelectionRoute extends PageRouteInfo { - const BackupAlbumSelectionRoute({List? children}) - : super(BackupAlbumSelectionRoute.name, initialChildren: children); - - static const String name = 'BackupAlbumSelectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const BackupAlbumSelectionPage(); - }, - ); -} - -/// generated route for -/// [BackupControllerPage] -class BackupControllerRoute extends PageRouteInfo { - const BackupControllerRoute({List? children}) - : super(BackupControllerRoute.name, initialChildren: children); - - static const String name = 'BackupControllerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const BackupControllerPage(); - }, - ); -} - -/// generated route for -/// [BackupOptionsPage] -class BackupOptionsRoute extends PageRouteInfo { - const BackupOptionsRoute({List? children}) - : super(BackupOptionsRoute.name, initialChildren: children); - - static const String name = 'BackupOptionsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const BackupOptionsPage(); - }, - ); -} - -/// generated route for -/// [ChangeExperiencePage] -class ChangeExperienceRoute extends PageRouteInfo { - ChangeExperienceRoute({ - Key? key, - required bool switchingToBeta, - List? children, - }) : super( - ChangeExperienceRoute.name, - args: ChangeExperienceRouteArgs( - key: key, - switchingToBeta: switchingToBeta, - ), - initialChildren: children, - ); - - static const String name = 'ChangeExperienceRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return ChangeExperiencePage( - key: args.key, - switchingToBeta: args.switchingToBeta, - ); - }, - ); -} - -class ChangeExperienceRouteArgs { - const ChangeExperienceRouteArgs({this.key, required this.switchingToBeta}); - - final Key? key; - - final bool switchingToBeta; - - @override - String toString() { - return 'ChangeExperienceRouteArgs{key: $key, switchingToBeta: $switchingToBeta}'; - } -} - /// generated route for /// [ChangePasswordPage] class ChangePasswordRoute extends PageRouteInfo { @@ -648,89 +217,6 @@ class CleanupPreviewRouteArgs { } } -/// generated route for -/// [CreateAlbumPage] -class CreateAlbumRoute extends PageRouteInfo { - CreateAlbumRoute({ - Key? key, - List? assets, - List? children, - }) : super( - CreateAlbumRoute.name, - args: CreateAlbumRouteArgs(key: key, assets: assets), - initialChildren: children, - ); - - static const String name = 'CreateAlbumRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const CreateAlbumRouteArgs(), - ); - return CreateAlbumPage(key: args.key, assets: args.assets); - }, - ); -} - -class CreateAlbumRouteArgs { - const CreateAlbumRouteArgs({this.key, this.assets}); - - final Key? key; - - final List? assets; - - @override - String toString() { - return 'CreateAlbumRouteArgs{key: $key, assets: $assets}'; - } -} - -/// generated route for -/// [CropImagePage] -class CropImageRoute extends PageRouteInfo { - CropImageRoute({ - Key? key, - required Image image, - required Asset asset, - List? children, - }) : super( - CropImageRoute.name, - args: CropImageRouteArgs(key: key, image: image, asset: asset), - initialChildren: children, - ); - - static const String name = 'CropImageRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return CropImagePage(key: args.key, image: args.image, asset: args.asset); - }, - ); -} - -class CropImageRouteArgs { - const CropImageRouteArgs({ - this.key, - required this.image, - required this.asset, - }); - - final Key? key; - - final Image image; - - final Asset asset; - - @override - String toString() { - return 'CropImageRouteArgs{key: $key, image: $image, asset: $asset}'; - } -} - /// generated route for /// [DownloadInfoPage] class DownloadInfoRoute extends PageRouteInfo { @@ -1003,70 +489,20 @@ class DriftCreateAlbumRoute extends PageRouteInfo { ); } -/// generated route for -/// [DriftCropImagePage] -class DriftCropImageRoute extends PageRouteInfo { - DriftCropImageRoute({ - Key? key, - required Image image, - required BaseAsset asset, - List? children, - }) : super( - DriftCropImageRoute.name, - args: DriftCropImageRouteArgs(key: key, image: image, asset: asset), - initialChildren: children, - ); - - static const String name = 'DriftCropImageRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return DriftCropImagePage( - key: args.key, - image: args.image, - asset: args.asset, - ); - }, - ); -} - -class DriftCropImageRouteArgs { - const DriftCropImageRouteArgs({ - this.key, - required this.image, - required this.asset, - }); - - final Key? key; - - final Image image; - - final BaseAsset asset; - - @override - String toString() { - return 'DriftCropImageRouteArgs{key: $key, image: $image, asset: $asset}'; - } -} - /// generated route for /// [DriftEditImagePage] class DriftEditImageRoute extends PageRouteInfo { DriftEditImageRoute({ Key? key, - required BaseAsset asset, required Image image, - required bool isEdited, + required Future Function(List) applyEdits, List? children, }) : super( DriftEditImageRoute.name, args: DriftEditImageRouteArgs( key: key, - asset: asset, image: image, - isEdited: isEdited, + applyEdits: applyEdits, ), initialChildren: children, ); @@ -1079,9 +515,8 @@ class DriftEditImageRoute extends PageRouteInfo { final args = data.argsAs(); return DriftEditImagePage( key: args.key, - asset: args.asset, image: args.image, - isEdited: args.isEdited, + applyEdits: args.applyEdits, ); }, ); @@ -1090,22 +525,19 @@ class DriftEditImageRoute extends PageRouteInfo { class DriftEditImageRouteArgs { const DriftEditImageRouteArgs({ this.key, - required this.asset, required this.image, - required this.isEdited, + required this.applyEdits, }); final Key? key; - final BaseAsset asset; - final Image image; - final bool isEdited; + final Future Function(List) applyEdits; @override String toString() { - return 'DriftEditImageRouteArgs{key: $key, asset: $asset, image: $image, isEdited: $isEdited}'; + return 'DriftEditImageRouteArgs{key: $key, image: $image, applyEdits: $applyEdits}'; } } @@ -1125,54 +557,6 @@ class DriftFavoriteRoute extends PageRouteInfo { ); } -/// generated route for -/// [DriftFilterImagePage] -class DriftFilterImageRoute extends PageRouteInfo { - DriftFilterImageRoute({ - Key? key, - required Image image, - required BaseAsset asset, - List? children, - }) : super( - DriftFilterImageRoute.name, - args: DriftFilterImageRouteArgs(key: key, image: image, asset: asset), - initialChildren: children, - ); - - static const String name = 'DriftFilterImageRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return DriftFilterImagePage( - key: args.key, - image: args.image, - asset: args.asset, - ); - }, - ); -} - -class DriftFilterImageRouteArgs { - const DriftFilterImageRouteArgs({ - this.key, - required this.image, - required this.asset, - }); - - final Key? key; - - final Image image; - - final BaseAsset asset; - - @override - String toString() { - return 'DriftFilterImageRouteArgs{key: $key, image: $image, asset: $asset}'; - } -} - /// generated route for /// [DriftLibraryPage] class DriftLibraryRoute extends PageRouteInfo { @@ -1616,144 +1000,6 @@ class DriftVideoRoute extends PageRouteInfo { ); } -/// generated route for -/// [EditImagePage] -class EditImageRoute extends PageRouteInfo { - EditImageRoute({ - Key? key, - required Asset asset, - required Image image, - required bool isEdited, - List? children, - }) : super( - EditImageRoute.name, - args: EditImageRouteArgs( - key: key, - asset: asset, - image: image, - isEdited: isEdited, - ), - initialChildren: children, - ); - - static const String name = 'EditImageRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return EditImagePage( - key: args.key, - asset: args.asset, - image: args.image, - isEdited: args.isEdited, - ); - }, - ); -} - -class EditImageRouteArgs { - const EditImageRouteArgs({ - this.key, - required this.asset, - required this.image, - required this.isEdited, - }); - - final Key? key; - - final Asset asset; - - final Image image; - - final bool isEdited; - - @override - String toString() { - return 'EditImageRouteArgs{key: $key, asset: $asset, image: $image, isEdited: $isEdited}'; - } -} - -/// generated route for -/// [FailedBackupStatusPage] -class FailedBackupStatusRoute extends PageRouteInfo { - const FailedBackupStatusRoute({List? children}) - : super(FailedBackupStatusRoute.name, initialChildren: children); - - static const String name = 'FailedBackupStatusRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const FailedBackupStatusPage(); - }, - ); -} - -/// generated route for -/// [FavoritesPage] -class FavoritesRoute extends PageRouteInfo { - const FavoritesRoute({List? children}) - : super(FavoritesRoute.name, initialChildren: children); - - static const String name = 'FavoritesRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const FavoritesPage(); - }, - ); -} - -/// generated route for -/// [FilterImagePage] -class FilterImageRoute extends PageRouteInfo { - FilterImageRoute({ - Key? key, - required Image image, - required Asset asset, - List? children, - }) : super( - FilterImageRoute.name, - args: FilterImageRouteArgs(key: key, image: image, asset: asset), - initialChildren: children, - ); - - static const String name = 'FilterImageRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return FilterImagePage( - key: args.key, - image: args.image, - asset: args.asset, - ); - }, - ); -} - -class FilterImageRouteArgs { - const FilterImageRouteArgs({ - this.key, - required this.image, - required this.asset, - }); - - final Key? key; - - final Image image; - - final Asset asset; - - @override - String toString() { - return 'FilterImageRouteArgs{key: $key, image: $image, asset: $asset}'; - } -} - /// generated route for /// [FolderPage] class FolderRoute extends PageRouteInfo { @@ -1793,70 +1039,6 @@ class FolderRouteArgs { } } -/// generated route for -/// [GalleryViewerPage] -class GalleryViewerRoute extends PageRouteInfo { - GalleryViewerRoute({ - Key? key, - required RenderList renderList, - int initialIndex = 0, - int heroOffset = 0, - bool showStack = false, - List? children, - }) : super( - GalleryViewerRoute.name, - args: GalleryViewerRouteArgs( - key: key, - renderList: renderList, - initialIndex: initialIndex, - heroOffset: heroOffset, - showStack: showStack, - ), - initialChildren: children, - ); - - static const String name = 'GalleryViewerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return GalleryViewerPage( - key: args.key, - renderList: args.renderList, - initialIndex: args.initialIndex, - heroOffset: args.heroOffset, - showStack: args.showStack, - ); - }, - ); -} - -class GalleryViewerRouteArgs { - const GalleryViewerRouteArgs({ - this.key, - required this.renderList, - this.initialIndex = 0, - this.heroOffset = 0, - this.showStack = false, - }); - - final Key? key; - - final RenderList renderList; - - final int initialIndex; - - final int heroOffset; - - final bool showStack; - - @override - String toString() { - return 'GalleryViewerRouteArgs{key: $key, renderList: $renderList, initialIndex: $initialIndex, heroOffset: $heroOffset, showStack: $showStack}'; - } -} - /// generated route for /// [HeaderSettingsPage] class HeaderSettingsRoute extends PageRouteInfo { @@ -1873,38 +1055,6 @@ class HeaderSettingsRoute extends PageRouteInfo { ); } -/// generated route for -/// [LibraryPage] -class LibraryRoute extends PageRouteInfo { - const LibraryRoute({List? children}) - : super(LibraryRoute.name, initialChildren: children); - - static const String name = 'LibraryRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const LibraryPage(); - }, - ); -} - -/// generated route for -/// [LocalAlbumsPage] -class LocalAlbumsRoute extends PageRouteInfo { - const LocalAlbumsRoute({List? children}) - : super(LocalAlbumsRoute.name, initialChildren: children); - - static const String name = 'LocalAlbumsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const LocalAlbumsPage(); - }, - ); -} - /// generated route for /// [LocalMediaSummaryPage] class LocalMediaSummaryRoute extends PageRouteInfo { @@ -1958,22 +1108,6 @@ class LocalTimelineRouteArgs { } } -/// generated route for -/// [LockedPage] -class LockedRoute extends PageRouteInfo { - const LockedRoute({List? children}) - : super(LockedRoute.name, initialChildren: children); - - static const String name = 'LockedRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const LockedPage(); - }, - ); -} - /// generated route for /// [LoginPage] class LoginRoute extends PageRouteInfo { @@ -2054,311 +1188,6 @@ class MapLocationPickerRouteArgs { } } -/// generated route for -/// [MapPage] -class MapRoute extends PageRouteInfo { - MapRoute({Key? key, LatLng? initialLocation, List? children}) - : super( - MapRoute.name, - args: MapRouteArgs(key: key, initialLocation: initialLocation), - initialChildren: children, - ); - - static const String name = 'MapRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const MapRouteArgs(), - ); - return MapPage(key: args.key, initialLocation: args.initialLocation); - }, - ); -} - -class MapRouteArgs { - const MapRouteArgs({this.key, this.initialLocation}); - - final Key? key; - - final LatLng? initialLocation; - - @override - String toString() { - return 'MapRouteArgs{key: $key, initialLocation: $initialLocation}'; - } -} - -/// generated route for -/// [MemoryPage] -class MemoryRoute extends PageRouteInfo { - MemoryRoute({ - required List memories, - required int memoryIndex, - Key? key, - List? children, - }) : super( - MemoryRoute.name, - args: MemoryRouteArgs( - memories: memories, - memoryIndex: memoryIndex, - key: key, - ), - initialChildren: children, - ); - - static const String name = 'MemoryRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return MemoryPage( - memories: args.memories, - memoryIndex: args.memoryIndex, - key: args.key, - ); - }, - ); -} - -class MemoryRouteArgs { - const MemoryRouteArgs({ - required this.memories, - required this.memoryIndex, - this.key, - }); - - final List memories; - - final int memoryIndex; - - final Key? key; - - @override - String toString() { - return 'MemoryRouteArgs{memories: $memories, memoryIndex: $memoryIndex, key: $key}'; - } -} - -/// generated route for -/// [NativeVideoViewerPage] -class NativeVideoViewerRoute extends PageRouteInfo { - NativeVideoViewerRoute({ - Key? key, - required Asset asset, - required Widget image, - bool showControls = true, - int playbackDelayFactor = 1, - List? children, - }) : super( - NativeVideoViewerRoute.name, - args: NativeVideoViewerRouteArgs( - key: key, - asset: asset, - image: image, - showControls: showControls, - playbackDelayFactor: playbackDelayFactor, - ), - initialChildren: children, - ); - - static const String name = 'NativeVideoViewerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return NativeVideoViewerPage( - key: args.key, - asset: args.asset, - image: args.image, - showControls: args.showControls, - playbackDelayFactor: args.playbackDelayFactor, - ); - }, - ); -} - -class NativeVideoViewerRouteArgs { - const NativeVideoViewerRouteArgs({ - this.key, - required this.asset, - required this.image, - this.showControls = true, - this.playbackDelayFactor = 1, - }); - - final Key? key; - - final Asset asset; - - final Widget image; - - final bool showControls; - - final int playbackDelayFactor; - - @override - String toString() { - return 'NativeVideoViewerRouteArgs{key: $key, asset: $asset, image: $image, showControls: $showControls, playbackDelayFactor: $playbackDelayFactor}'; - } -} - -/// generated route for -/// [PartnerDetailPage] -class PartnerDetailRoute extends PageRouteInfo { - PartnerDetailRoute({ - Key? key, - required UserDto partner, - List? children, - }) : super( - PartnerDetailRoute.name, - args: PartnerDetailRouteArgs(key: key, partner: partner), - initialChildren: children, - ); - - static const String name = 'PartnerDetailRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return PartnerDetailPage(key: args.key, partner: args.partner); - }, - ); -} - -class PartnerDetailRouteArgs { - const PartnerDetailRouteArgs({this.key, required this.partner}); - - final Key? key; - - final UserDto partner; - - @override - String toString() { - return 'PartnerDetailRouteArgs{key: $key, partner: $partner}'; - } -} - -/// generated route for -/// [PartnerPage] -class PartnerRoute extends PageRouteInfo { - const PartnerRoute({List? children}) - : super(PartnerRoute.name, initialChildren: children); - - static const String name = 'PartnerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const PartnerPage(); - }, - ); -} - -/// generated route for -/// [PeopleCollectionPage] -class PeopleCollectionRoute extends PageRouteInfo { - const PeopleCollectionRoute({List? children}) - : super(PeopleCollectionRoute.name, initialChildren: children); - - static const String name = 'PeopleCollectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const PeopleCollectionPage(); - }, - ); -} - -/// generated route for -/// [PermissionOnboardingPage] -class PermissionOnboardingRoute extends PageRouteInfo { - const PermissionOnboardingRoute({List? children}) - : super(PermissionOnboardingRoute.name, initialChildren: children); - - static const String name = 'PermissionOnboardingRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const PermissionOnboardingPage(); - }, - ); -} - -/// generated route for -/// [PersonResultPage] -class PersonResultRoute extends PageRouteInfo { - PersonResultRoute({ - Key? key, - required String personId, - required String personName, - List? children, - }) : super( - PersonResultRoute.name, - args: PersonResultRouteArgs( - key: key, - personId: personId, - personName: personName, - ), - initialChildren: children, - ); - - static const String name = 'PersonResultRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return PersonResultPage( - key: args.key, - personId: args.personId, - personName: args.personName, - ); - }, - ); -} - -class PersonResultRouteArgs { - const PersonResultRouteArgs({ - this.key, - required this.personId, - required this.personName, - }); - - final Key? key; - - final String personId; - - final String personName; - - @override - String toString() { - return 'PersonResultRouteArgs{key: $key, personId: $personId, personName: $personName}'; - } -} - -/// generated route for -/// [PhotosPage] -class PhotosRoute extends PageRouteInfo { - const PhotosRoute({List? children}) - : super(PhotosRoute.name, initialChildren: children); - - static const String name = 'PhotosRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const PhotosPage(); - }, - ); -} - /// generated route for /// [PinAuthPage] class PinAuthRoute extends PageRouteInfo { @@ -2398,51 +1227,6 @@ class PinAuthRouteArgs { } } -/// generated route for -/// [PlacesCollectionPage] -class PlacesCollectionRoute extends PageRouteInfo { - PlacesCollectionRoute({ - Key? key, - LatLng? currentLocation, - List? children, - }) : super( - PlacesCollectionRoute.name, - args: PlacesCollectionRouteArgs( - key: key, - currentLocation: currentLocation, - ), - initialChildren: children, - ); - - static const String name = 'PlacesCollectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const PlacesCollectionRouteArgs(), - ); - return PlacesCollectionPage( - key: args.key, - currentLocation: args.currentLocation, - ); - }, - ); -} - -class PlacesCollectionRouteArgs { - const PlacesCollectionRouteArgs({this.key, this.currentLocation}); - - final Key? key; - - final LatLng? currentLocation; - - @override - String toString() { - return 'PlacesCollectionRouteArgs{key: $key, currentLocation: $currentLocation}'; - } -} - /// generated route for /// [ProfilePictureCropPage] class ProfilePictureCropRoute @@ -2481,22 +1265,6 @@ class ProfilePictureCropRouteArgs { } } -/// generated route for -/// [RecentlyTakenPage] -class RecentlyTakenRoute extends PageRouteInfo { - const RecentlyTakenRoute({List? children}) - : super(RecentlyTakenRoute.name, initialChildren: children); - - static const String name = 'RecentlyTakenRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const RecentlyTakenPage(); - }, - ); -} - /// generated route for /// [RemoteAlbumPage] class RemoteAlbumRoute extends PageRouteInfo { @@ -2550,45 +1318,6 @@ class RemoteMediaSummaryRoute extends PageRouteInfo { ); } -/// generated route for -/// [SearchPage] -class SearchRoute extends PageRouteInfo { - SearchRoute({ - Key? key, - SearchFilter? prefilter, - List? children, - }) : super( - SearchRoute.name, - args: SearchRouteArgs(key: key, prefilter: prefilter), - initialChildren: children, - ); - - static const String name = 'SearchRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const SearchRouteArgs(), - ); - return SearchPage(key: args.key, prefilter: args.prefilter); - }, - ); -} - -class SearchRouteArgs { - const SearchRouteArgs({this.key, this.prefilter}); - - final Key? key; - - final SearchFilter? prefilter; - - @override - String toString() { - return 'SearchRouteArgs{key: $key, prefilter: $prefilter}'; - } -} - /// generated route for /// [SettingsPage] class SettingsRoute extends PageRouteInfo { @@ -2787,22 +1516,6 @@ class SyncStatusRoute extends PageRouteInfo { ); } -/// generated route for -/// [TabControllerPage] -class TabControllerRoute extends PageRouteInfo { - const TabControllerRoute({List? children}) - : super(TabControllerRoute.name, initialChildren: children); - - static const String name = 'TabControllerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const TabControllerPage(); - }, - ); -} - /// generated route for /// [TabShellPage] class TabShellRoute extends PageRouteInfo { @@ -2818,19 +1531,3 @@ class TabShellRoute extends PageRouteInfo { }, ); } - -/// generated route for -/// [TrashPage] -class TrashRoute extends PageRouteInfo { - const TrashRoute({List? children}) - : super(TrashRoute.name, initialChildren: children); - - static const String name = 'TrashRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const TrashPage(); - }, - ); -} diff --git a/mobile/lib/services/action.service.dart b/mobile/lib/services/action.service.dart index c435bf9d79..1a6333215a 100644 --- a/mobile/lib/services/action.service.dart +++ b/mobile/lib/services/action.service.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; @@ -246,6 +247,14 @@ class ActionService { return true; } + Future applyEdits(String remoteId, List edits) async { + if (edits.isEmpty) { + await _assetApiRepository.removeEdits(remoteId); + } else { + await _assetApiRepository.editAsset(remoteId, edits); + } + } + Future _deleteLocalAssets(List localIds) async { final deletedIds = await _assetMediaRepository.deleteAll(localIds); if (deletedIds.isEmpty) { diff --git a/mobile/lib/services/activity.service.dart b/mobile/lib/services/activity.service.dart index 382a7fe107..0ef1badacb 100644 --- a/mobile/lib/services/activity.service.dart +++ b/mobile/lib/services/activity.service.dart @@ -9,7 +9,6 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da import 'package:immich_mobile/repositories/activity_api.repository.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:logging/logging.dart'; -import 'package:immich_mobile/entities/store.entity.dart' as immich_store; class ActivityService with ErrorLoggerMixin { final ActivityApiRepository _activityApiRepository; @@ -60,20 +59,16 @@ class ActivityService with ErrorLoggerMixin { } Future buildAssetViewerRoute(String assetId, WidgetRef ref) async { - if (immich_store.Store.isBetaTimelineEnabled) { - final asset = await _assetService.getRemoteAsset(assetId); - if (asset == null) { - return null; - } - - AssetViewer.setAsset(ref, asset); - return AssetViewerRoute( - initialIndex: 0, - timelineService: _timelineFactory.fromAssets([asset], TimelineOrigin.albumActivities), - currentAlbum: ref.read(currentRemoteAlbumProvider), - ); + final asset = await _assetService.getRemoteAsset(assetId); + if (asset == null) { + return null; } - return null; + AssetViewer.setAsset(ref, asset); + return AssetViewerRoute( + initialIndex: 0, + timelineService: _timelineFactory.fromAssets([asset], TimelineOrigin.albumActivities), + currentAlbum: ref.read(currentRemoteAlbumProvider), + ); } } diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart deleted file mode 100644 index 8d77b569e6..0000000000 --- a/mobile/lib/services/album.service.dart +++ /dev/null @@ -1,425 +0,0 @@ -import 'dart:async'; -import 'dart:collection'; -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; -import 'package:immich_mobile/models/albums/album_add_asset_response.model.dart'; -import 'package:immich_mobile/models/albums/album_search.model.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/album.repository.dart'; -import 'package:immich_mobile/repositories/album_api.repository.dart'; -import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; -import 'package:immich_mobile/services/entity.service.dart'; -import 'package:immich_mobile/services/sync.service.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:logging/logging.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; - -final albumServiceProvider = Provider( - (ref) => AlbumService( - ref.watch(syncServiceProvider), - ref.watch(userServiceProvider), - ref.watch(entityServiceProvider), - ref.watch(albumRepositoryProvider), - ref.watch(assetRepositoryProvider), - ref.watch(backupAlbumRepositoryProvider), - ref.watch(albumMediaRepositoryProvider), - ref.watch(albumApiRepositoryProvider), - ), -); - -class AlbumService { - final SyncService _syncService; - final UserService _userService; - final EntityService _entityService; - final AlbumRepository _albumRepository; - final AssetRepository _assetRepository; - final BackupAlbumRepository _backupAlbumRepository; - final AlbumMediaRepository _albumMediaRepository; - final AlbumApiRepository _albumApiRepository; - final Logger _log = Logger('AlbumService'); - Completer _localCompleter = Completer()..complete(false); - Completer _remoteCompleter = Completer()..complete(false); - - AlbumService( - this._syncService, - this._userService, - this._entityService, - this._albumRepository, - this._assetRepository, - this._backupAlbumRepository, - this._albumMediaRepository, - this._albumApiRepository, - ); - - /// Checks all selected device albums for changes of albums and their assets - /// Updates the local database and returns `true` if there were any changes - Future refreshDeviceAlbums() async { - if (!_localCompleter.isCompleted) { - // guard against concurrent calls - _log.info("refreshDeviceAlbums is already in progress"); - return _localCompleter.future; - } - _localCompleter = Completer(); - final Stopwatch sw = Stopwatch()..start(); - bool changes = false; - try { - final (selectedIds, excludedIds, onDevice) = await ( - _backupAlbumRepository.getIdsBySelection(BackupSelection.select).then((value) => value.toSet()), - _backupAlbumRepository.getIdsBySelection(BackupSelection.exclude).then((value) => value.toSet()), - _albumMediaRepository.getAll(), - ).wait; - _log.info("Found ${onDevice.length} device albums"); - if (selectedIds.isEmpty) { - final numLocal = await _albumRepository.count(local: true); - if (numLocal > 0) { - await _syncService.removeAllLocalAlbumsAndAssets(); - } - return false; - } - Set? excludedAssets; - if (excludedIds.isNotEmpty) { - if (Platform.isIOS) { - // iOS and Android device album working principle differ significantly - // on iOS, an asset can be in multiple albums - // on Android, an asset can only be in exactly one album (folder!) at the same time - // thus, on Android, excluding an album can be done by ignoring that album - // however, on iOS, it it necessary to load the assets from all excluded - // albums and check every asset from any selected album against the set - // of excluded assets - excludedAssets = await _loadExcludedAssetIds(onDevice, excludedIds); - _log.info("Found ${excludedAssets.length} assets to exclude"); - } - // remove all excluded albums - onDevice.removeWhere((e) => excludedIds.contains(e.localId)); - _log.info("Ignoring ${excludedIds.length} excluded albums resulting in ${onDevice.length} device albums"); - } - - final allAlbum = onDevice.firstWhereOrNull((album) => album.isAll); - final hasAll = allAlbum != null && selectedIds.contains(allAlbum.localId); - if (hasAll) { - if (Platform.isAndroid) { - // remove the virtual "Recent" album and keep and individual albums - // on Android, the virtual "Recent" `lastModified` value is always null - onDevice.removeWhere((album) => album.isAll); - _log.info("'Recents' is selected, keeping all individual albums"); - } - } else { - // keep only the explicitly selected albums - onDevice.removeWhere((album) => !selectedIds.contains(album.localId)); - _log.info("'Recents' is not selected, keeping only selected albums"); - } - changes = await _syncService.syncLocalAlbumAssetsToDb(onDevice, excludedAssets); - _log.info("Syncing completed. Changes: $changes"); - } finally { - _localCompleter.complete(changes); - } - dPrint(() => "refreshDeviceAlbums took ${sw.elapsedMilliseconds}ms"); - return changes; - } - - Future> _loadExcludedAssetIds(List albums, Set excludedAlbumIds) async { - final Set result = HashSet(); - for (final batchAlbums in albums.where((album) => excludedAlbumIds.contains(album.localId)).slices(5)) { - await batchAlbums - .map((album) => _albumMediaRepository.getAssetIds(album.localId!).then((assetIds) => result.addAll(assetIds))) - .wait; - } - return result; - } - - /// Checks remote albums (owned if `isShared` is false) for changes, - /// updates the local database and returns `true` if there were any changes - Future refreshRemoteAlbums() async { - if (!_remoteCompleter.isCompleted) { - // guard against concurrent calls - return _remoteCompleter.future; - } - _remoteCompleter = Completer(); - final Stopwatch sw = Stopwatch()..start(); - bool changes = false; - try { - final users = await _syncService.getUsersFromServer(); - if (users != null) { - await _syncService.syncUsersFromServer(users); - } - final (sharedAlbum, ownedAlbum) = await ( - // Note: `shared: true` is required to get albums that don't belong to - // us due to unusual behaviour on the API but this will also return our - // own shared albums - _albumApiRepository.getAll(shared: true), - // Passing null (or nothing) for `shared` returns only albums that - // explicitly belong to us - _albumApiRepository.getAll(shared: null), - ).wait; - - final albums = HashSet(equals: (a, b) => a.remoteId == b.remoteId, hashCode: (a) => a.remoteId.hashCode); - - albums.addAll(sharedAlbum); - albums.addAll(ownedAlbum); - - changes = await _syncService.syncRemoteAlbumsToDb(albums.toList()); - } finally { - _remoteCompleter.complete(changes); - } - dPrint(() => "refreshRemoteAlbums took ${sw.elapsedMilliseconds}ms"); - return changes; - } - - Future createAlbum( - String albumName, - Iterable assets, [ - Iterable sharedUsers = const [], - ]) async { - final Album album = await _albumApiRepository.create( - albumName, - assetIds: assets.map((asset) => asset.remoteId!), - sharedUserIds: sharedUsers.map((user) => user.id), - ); - await _entityService.fillAlbumWithDatabaseEntities(album); - return _albumRepository.create(album); - } - - /* - * Creates names like Untitled, Untitled (1), Untitled (2), ... - */ - Future _getNextAlbumName() async { - const baseName = "Untitled"; - for (int round = 0; ; round++) { - final proposedName = "$baseName${round == 0 ? "" : " ($round)"}"; - - if (null == await _albumRepository.getByName(proposedName, owner: true)) { - return proposedName; - } - } - } - - Future createAlbumWithGeneratedName(Iterable assets) async { - return createAlbum(await _getNextAlbumName(), assets, []); - } - - Future addAssets(Album album, Iterable assets) async { - try { - final result = await _albumApiRepository.addAssets(album.remoteId!, assets.map((asset) => asset.remoteId!)); - - final List addedAssets = result.added - .map((id) => assets.firstWhere((asset) => asset.remoteId == id)) - .toList(); - - await _updateAssets(album.id, add: addedAssets); - - return AlbumAddAssetsResponse(alreadyInAlbum: result.duplicates, successfullyAdded: addedAssets.length); - } catch (e) { - dPrint(() => "Error addAssets ${e.toString()}"); - } - return null; - } - - Future _updateAssets(int albumId, {List add = const [], List remove = const []}) => - _albumRepository.transaction(() async { - final album = await _albumRepository.get(albumId); - if (album == null) return; - await _albumRepository.addAssets(album, add); - await _albumRepository.removeAssets(album, remove); - await _albumRepository.recalculateMetadata(album); - await _albumRepository.update(album); - }); - - Future setActivityStatus(Album album, bool enabled) async { - try { - final updatedAlbum = await _albumApiRepository.update(album.remoteId!, activityEnabled: enabled); - album.activityEnabled = updatedAlbum.activityEnabled; - await _albumRepository.update(album); - return true; - } catch (e) { - dPrint(() => "Error setActivityEnabled ${e.toString()}"); - } - return false; - } - - Future deleteAlbum(Album album) async { - try { - final userId = _userService.getMyUser().id; - if (album.owner.value?.isarId == fastHash(userId)) { - await _albumApiRepository.delete(album.remoteId!); - } - if (album.shared) { - final foreignAssets = await _assetRepository.getByAlbum(album, notOwnedBy: [userId]); - await _albumRepository.delete(album.id); - - final List albums = await _albumRepository.getAll(shared: true); - final List existing = []; - for (Album album in albums) { - existing.addAll(await _assetRepository.getByAlbum(album, notOwnedBy: [userId])); - } - final List idsToRemove = _syncService.sharedAssetsToRemove(foreignAssets, existing); - if (idsToRemove.isNotEmpty) { - await _assetRepository.deleteByIds(idsToRemove); - } - } else { - await _albumRepository.delete(album.id); - } - return true; - } catch (e) { - dPrint(() => "Error deleteAlbum ${e.toString()}"); - } - return false; - } - - Future leaveAlbum(Album album) async { - try { - await _albumApiRepository.removeUser(album.remoteId!, userId: "me"); - return true; - } catch (e) { - dPrint(() => "Error leaveAlbum ${e.toString()}"); - return false; - } - } - - Future removeAsset(Album album, Iterable assets) async { - try { - final result = await _albumApiRepository.removeAssets(album.remoteId!, assets.map((asset) => asset.remoteId!)); - final toRemove = result.removed.map((id) => assets.firstWhere((asset) => asset.remoteId == id)); - await _updateAssets(album.id, remove: toRemove.toList()); - return true; - } catch (e) { - dPrint(() => "Error removeAssetFromAlbum ${e.toString()}"); - } - return false; - } - - Future removeUser(Album album, UserDto user) async { - try { - await _albumApiRepository.removeUser(album.remoteId!, userId: user.id); - - album.sharedUsers.remove(entity.User.fromDto(user)); - await _albumRepository.removeUsers(album, [user]); - final a = await _albumRepository.get(album.id); - // trigger watcher - await _albumRepository.update(a!); - - return true; - } catch (error) { - dPrint(() => "Error removeUser ${error.toString()}"); - return false; - } - } - - Future addUsers(Album album, List userIds) async { - try { - final updatedAlbum = await _albumApiRepository.addUsers(album.remoteId!, userIds); - - album.sharedUsers.addAll(updatedAlbum.remoteUsers); - album.shared = true; - - await _albumRepository.addUsers(album, album.sharedUsers.map((u) => u.toDto()).toList()); - await _albumRepository.update(album); - - return true; - } catch (error) { - dPrint(() => "Error addUsers ${error.toString()}"); - } - return false; - } - - Future changeTitleAlbum(Album album, String newAlbumTitle) async { - try { - final updatedAlbum = await _albumApiRepository.update(album.remoteId!, name: newAlbumTitle); - - album.name = updatedAlbum.name; - await _albumRepository.update(album); - return true; - } catch (e) { - dPrint(() => "Error changeTitleAlbum ${e.toString()}"); - return false; - } - } - - Future changeDescriptionAlbum(Album album, String newAlbumDescription) async { - try { - final updatedAlbum = await _albumApiRepository.update(album.remoteId!, description: newAlbumDescription); - - album.description = updatedAlbum.description; - await _albumRepository.update(album); - return true; - } catch (e) { - dPrint(() => "Error changeDescriptionAlbum ${e.toString()}"); - return false; - } - } - - Future getAlbumByName(String name, {bool? remote, bool? shared, bool? owner}) => - _albumRepository.getByName(name, remote: remote, shared: shared, owner: owner); - - /// - /// Add the uploaded asset to the selected albums - /// - Future syncUploadAlbums(List albumNames, List assetIds) async { - for (final albumName in albumNames) { - Album? album = await getAlbumByName(albumName, remote: true, owner: true); - album ??= await createAlbum(albumName, []); - if (album != null && album.remoteId != null) { - await _albumApiRepository.addAssets(album.remoteId!, assetIds); - } - } - } - - Future> getAllRemoteAlbums() async { - return _albumRepository.getAll(remote: true); - } - - Future> getAllLocalAlbums() async { - return _albumRepository.getAll(remote: false); - } - - Stream> watchRemoteAlbums() { - return _albumRepository.watchRemoteAlbums(); - } - - Stream> watchLocalAlbums() { - return _albumRepository.watchLocalAlbums(); - } - - /// Get album by Isar ID - Future getAlbumById(int id) { - return _albumRepository.get(id); - } - - Future getAlbumByRemoteId(String remoteId) { - return _albumRepository.getByRemoteId(remoteId); - } - - Stream watchAlbum(int id) { - return _albumRepository.watchAlbum(id); - } - - Future> search(String searchTerm, QuickFilterMode filterMode) async { - return _albumRepository.search(searchTerm, filterMode); - } - - Future updateSortOrder(Album album, SortOrder order) async { - try { - final updateAlbum = await _albumApiRepository.update(album.remoteId!, sortOrder: order); - album.sortOrder = updateAlbum.sortOrder; - - return _albumRepository.update(album); - } catch (error, stackTrace) { - _log.severe("Error updating album sort order", error, stackTrace); - } - return null; - } - - Future clearTable() async { - await _albumRepository.clearTable(); - } -} diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart deleted file mode 100644 index b9fab35442..0000000000 --- a/mobile/lib/services/asset.service.dart +++ /dev/null @@ -1,465 +0,0 @@ -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/asset_api.repository.dart'; -import 'package:immich_mobile/repositories/asset_media.repository.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; -import 'package:immich_mobile/repositories/etag.repository.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/services/backup.service.dart'; -import 'package:immich_mobile/services/sync.service.dart'; -import 'package:logging/logging.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; -import 'package:openapi/api.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; - -final assetServiceProvider = Provider( - (ref) => AssetService( - ref.watch(assetApiRepositoryProvider), - ref.watch(assetRepositoryProvider), - ref.watch(exifRepositoryProvider), - ref.watch(userRepositoryProvider), - ref.watch(etagRepositoryProvider), - ref.watch(backupAlbumRepositoryProvider), - ref.watch(apiServiceProvider), - ref.watch(syncServiceProvider), - ref.watch(backupServiceProvider), - ref.watch(albumServiceProvider), - ref.watch(userServiceProvider), - ref.watch(assetMediaRepositoryProvider), - ), -); - -class AssetService { - final AssetApiRepository _assetApiRepository; - final AssetRepository _assetRepository; - final IsarExifRepository _exifInfoRepository; - final IsarUserRepository _isarUserRepository; - final ETagRepository _etagRepository; - final BackupAlbumRepository _backupRepository; - final ApiService _apiService; - final SyncService _syncService; - final BackupService _backupService; - final AlbumService _albumService; - final UserService _userService; - final AssetMediaRepository _assetMediaRepository; - final log = Logger('AssetService'); - - AssetService( - this._assetApiRepository, - this._assetRepository, - this._exifInfoRepository, - this._isarUserRepository, - this._etagRepository, - this._backupRepository, - this._apiService, - this._syncService, - this._backupService, - this._albumService, - this._userService, - this._assetMediaRepository, - ); - - /// Checks the server for updated assets and updates the local database if - /// required. Returns `true` if there were any changes. - Future refreshRemoteAssets() async { - final syncedUserIds = await _etagRepository.getAllIds(); - final List syncedUsers = syncedUserIds.isEmpty - ? [] - : (await _isarUserRepository.getByUserIds(syncedUserIds)).nonNulls.toList(); - final Stopwatch sw = Stopwatch()..start(); - final bool changes = await _syncService.syncRemoteAssetsToDb( - users: syncedUsers, - getChangedAssets: _getRemoteAssetChanges, - loadAssets: _getRemoteAssets, - ); - dPrint(() => "refreshRemoteAssets full took ${sw.elapsedMilliseconds}ms"); - return changes; - } - - /// Returns `(null, null)` if changes are invalid -> requires full sync - Future<(List? toUpsert, List? toDelete)> _getRemoteAssetChanges( - List users, - DateTime since, - ) async { - final dto = AssetDeltaSyncDto(updatedAfter: since, userIds: users.map((e) => e.id).toList()); - final changes = await _apiService.syncApi.getDeltaSync(dto); - return changes == null || changes.needsFullSync - ? (null, null) - : (changes.upserted.map(Asset.remote).toList(), changes.deleted); - } - - /// Returns the list of people of the given asset id. - // If the server is not reachable `null` is returned. - Future?> getRemotePeopleOfAsset(String remoteId) async { - try { - final AssetResponseDto? dto = await _apiService.assetsApi.getAssetInfo(remoteId); - - return dto?.people; - } catch (error, stack) { - log.severe('Error while getting remote asset info: ${error.toString()}', error, stack); - - return null; - } - } - - /// Returns `null` if the server state did not change, else list of assets - Future?> _getRemoteAssets(UserDto user, DateTime until) async { - const int chunkSize = 10000; - try { - final List allAssets = []; - String? lastId; - // will break on error or once all assets are loaded - while (true) { - final dto = AssetFullSyncDto(limit: chunkSize, updatedUntil: until, lastId: lastId, userId: user.id); - log.fine("Requesting $chunkSize assets from $lastId"); - final List? assets = await _apiService.syncApi.getFullSyncForUser(dto); - if (assets == null) return null; - log.fine("Received ${assets.length} assets from ${assets.firstOrNull?.id} to ${assets.lastOrNull?.id}"); - allAssets.addAll(assets.map(Asset.remote)); - if (assets.length != chunkSize) break; - lastId = assets.last.id; - } - return allAssets; - } catch (error, stack) { - log.severe('Error while getting remote assets', error, stack); - return null; - } - } - - /// Loads the exif information from the database. If there is none, loads - /// the exif info from the server (remote assets only) - Future loadExif(Asset a) async { - a.exifInfo ??= (await _exifInfoRepository.get(a.id)); - // fileSize is always filled on the server but not set on client - if (a.exifInfo?.fileSize == null) { - if (a.isRemote) { - final dto = await _apiService.assetsApi.getAssetInfo(a.remoteId!); - if (dto != null && dto.exifInfo != null) { - final newExif = Asset.remote(dto).exifInfo!.copyWith(assetId: a.id); - a.exifInfo = newExif; - if (newExif != a.exifInfo) { - if (a.isInDb) { - await _assetRepository.transaction(() => _assetRepository.update(a)); - } else { - dPrint(() => "[loadExif] parameter Asset is not from DB!"); - } - } - } - } else { - // TODO implement local exif info parsing - } - } - return a; - } - - Future updateAssets(List assets, UpdateAssetDto updateAssetDto) async { - return await _apiService.assetsApi.updateAssets( - AssetBulkUpdateDto( - ids: assets.map((e) => e.remoteId!).toList(), - dateTimeOriginal: updateAssetDto.dateTimeOriginal, - isFavorite: updateAssetDto.isFavorite, - visibility: updateAssetDto.visibility, - latitude: updateAssetDto.latitude, - longitude: updateAssetDto.longitude, - ), - ); - } - - Future> changeFavoriteStatus(List assets, bool isFavorite) async { - try { - await updateAssets(assets, UpdateAssetDto(isFavorite: isFavorite)); - - for (var element in assets) { - element.isFavorite = isFavorite; - } - - await _syncService.upsertAssetsWithExif(assets); - - return assets; - } catch (error, stack) { - log.severe("Error while changing favorite status", error, stack); - return []; - } - } - - Future> changeArchiveStatus(List assets, bool isArchived) async { - try { - await updateAssets( - assets, - UpdateAssetDto(visibility: isArchived ? AssetVisibility.archive : AssetVisibility.timeline), - ); - - for (var element in assets) { - element.isArchived = isArchived; - element.visibility = isArchived ? AssetVisibilityEnum.archive : AssetVisibilityEnum.timeline; - } - - await _syncService.upsertAssetsWithExif(assets); - - return assets; - } catch (error, stack) { - log.severe("Error while changing archive status", error, stack); - return []; - } - } - - Future?> changeDateTime(List assets, String updatedDt) async { - try { - await updateAssets(assets, UpdateAssetDto(dateTimeOriginal: updatedDt)); - - for (var element in assets) { - element.fileCreatedAt = DateTime.parse(updatedDt); - element.exifInfo = element.exifInfo?.copyWith(dateTimeOriginal: DateTime.parse(updatedDt)); - } - - await _syncService.upsertAssetsWithExif(assets); - - return assets; - } catch (error, stack) { - log.severe("Error while changing date/time status", error, stack); - return Future.value(null); - } - } - - Future?> changeLocation(List assets, LatLng location) async { - try { - await updateAssets(assets, UpdateAssetDto(latitude: location.latitude, longitude: location.longitude)); - - for (var element in assets) { - element.exifInfo = element.exifInfo?.copyWith(latitude: location.latitude, longitude: location.longitude); - } - - await _syncService.upsertAssetsWithExif(assets); - - return assets; - } catch (error, stack) { - log.severe("Error while changing location status", error, stack); - return Future.value(null); - } - } - - Future syncUploadedAssetToAlbums() async { - try { - final selectedAlbums = await _backupRepository.getAllBySelection(BackupSelection.select); - final excludedAlbums = await _backupRepository.getAllBySelection(BackupSelection.exclude); - - final candidates = await _backupService.buildUploadCandidates( - selectedAlbums, - excludedAlbums, - useTimeFilter: false, - ); - - await refreshRemoteAssets(); - final owner = _userService.getMyUser(); - final remoteAssets = await _assetRepository.getAll(ownerId: owner.id, state: AssetState.merged); - - /// Map - Map> assetToAlbums = {}; - - for (BackupCandidate candidate in candidates) { - final asset = remoteAssets.firstWhereOrNull((a) => a.localId == candidate.asset.localId); - - if (asset != null) { - for (final albumName in candidate.albumNames) { - assetToAlbums.putIfAbsent(albumName, () => []).add(asset.remoteId!); - } - } - } - - // Upload assets to albums - for (final entry in assetToAlbums.entries) { - final albumName = entry.key; - final assetIds = entry.value; - - await _albumService.syncUploadAlbums([albumName], assetIds); - } - } catch (error, stack) { - log.severe("Error while syncing uploaded asset to albums", error, stack); - } - } - - Future setDescription(Asset asset, String newDescription) async { - final remoteAssetId = asset.remoteId; - final localExifId = asset.exifInfo?.assetId; - - // Guard [remoteAssetId] and [localExifId] null - if (remoteAssetId == null || localExifId == null) { - return; - } - - final result = await _assetApiRepository.update(remoteAssetId, description: newDescription); - - final description = result.exifInfo?.description; - - if (description != null) { - var exifInfo = await _exifInfoRepository.get(localExifId); - - if (exifInfo != null) { - await _exifInfoRepository.update(exifInfo.copyWith(description: description)); - } - } - } - - Future getDescription(Asset asset) async { - final localExifId = asset.exifInfo?.assetId; - - // Guard [remoteAssetId] and [localExifId] null - if (localExifId == null) { - return ""; - } - - final exifInfo = await _exifInfoRepository.get(localExifId); - - return exifInfo?.description ?? ""; - } - - Future getAspectRatio(Asset asset) async { - if (asset.isRemote) { - asset = await loadExif(asset); - } else if (asset.isLocal) { - await asset.localAsync; - } - - final aspectRatio = asset.aspectRatio; - if (aspectRatio != null) { - return aspectRatio; - } - - final width = asset.width; - final height = asset.height; - if (width != null && height != null) { - // we don't know the orientation, so assume it's normal - return width / height; - } - - return 1.0; - } - - Future> getStackAssets(String stackId) { - return _assetRepository.getStackAssets(stackId); - } - - Future clearTable() { - return _assetRepository.clearTable(); - } - - /// Delete assets from local file system and unreference from the database - Future deleteLocalAssets(Iterable assets) async { - // Delete files from local gallery - final candidates = assets.where((asset) => asset.isLocal); - - final deletedIds = await _assetMediaRepository.deleteAll(candidates.map((asset) => asset.localId!).toList()); - - // Modify local database by removing the reference to the local assets - if (deletedIds.isNotEmpty) { - // Delete records from local database - final isarIds = assets.where((asset) => asset.storage == AssetState.local).map((asset) => asset.id).toList(); - await _assetRepository.deleteByIds(isarIds); - - // Modify Merged asset to be remote only - final updatedAssets = assets.where((asset) => asset.storage == AssetState.merged).map((asset) { - asset.localId = null; - return asset; - }).toList(); - - await _assetRepository.updateAll(updatedAssets); - } - } - - /// Delete assets from the server and unreference from the database - Future deleteRemoteAssets(Iterable assets, {bool shouldDeletePermanently = false}) async { - final candidates = assets.where((a) => a.isRemote); - - if (candidates.isEmpty) { - return; - } - - await _apiService.assetsApi.deleteAssets( - AssetBulkDeleteDto(ids: candidates.map((a) => a.remoteId!).toList(), force: shouldDeletePermanently), - ); - - /// Update asset info bassed on the deletion type. - final payload = shouldDeletePermanently - ? assets.where((asset) => asset.storage == AssetState.merged).map((asset) { - asset.remoteId = null; - asset.visibility = AssetVisibilityEnum.timeline; - return asset; - }) - : assets.where((asset) => asset.isRemote).map((asset) { - asset.isTrashed = true; - return asset; - }); - - await _assetRepository.transaction(() async { - await _assetRepository.updateAll(payload.toList()); - - if (shouldDeletePermanently) { - final remoteAssetIds = assets - .where((asset) => asset.storage == AssetState.remote) - .map((asset) => asset.id) - .toList(); - await _assetRepository.deleteByIds(remoteAssetIds); - } - }); - } - - /// Delete assets on both local file system and the server. - /// Unreference from the database. - Future deleteAssets(Iterable assets, {bool shouldDeletePermanently = false}) async { - final hasLocal = assets.any((asset) => asset.isLocal); - final hasRemote = assets.any((asset) => asset.isRemote); - - if (hasLocal) { - await deleteLocalAssets(assets); - } - - if (hasRemote) { - await deleteRemoteAssets(assets, shouldDeletePermanently: shouldDeletePermanently); - } - } - - Stream watchAsset(int id, {bool fireImmediately = false}) { - return _assetRepository.watchAsset(id, fireImmediately: fireImmediately); - } - - Future> getRecentlyTakenAssets() { - final me = _userService.getMyUser(); - return _assetRepository.getRecentlyTakenAssets(me.id); - } - - Future> getMotionAssets() { - final me = _userService.getMyUser(); - return _assetRepository.getMotionAssets(me.id); - } - - Future setVisibility(List assets, AssetVisibilityEnum visibility) async { - await _assetApiRepository.updateVisibility(assets.map((asset) => asset.remoteId!).toList(), visibility); - - final updatedAssets = assets.map((asset) { - asset.visibility = visibility; - return asset; - }).toList(); - - await _assetRepository.updateAll(updatedAssets); - } - - Future getAssetByRemoteId(String remoteId) async { - final assets = await _assetRepository.getAllByRemoteId([remoteId]); - return assets.isNotEmpty ? assets.first : null; - } -} diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart deleted file mode 100644 index 03278d25fc..0000000000 --- a/mobile/lib/services/background.service.dart +++ /dev/null @@ -1,595 +0,0 @@ -import 'dart:async'; -import 'dart:developer'; -import 'dart:io'; -import 'dart:isolate'; -import 'dart:ui' show DartPluginRegistrant, IsolateNameServer, PluginUtilities; - -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/auth.service.dart'; -import 'package:immich_mobile/services/backup.service.dart'; -import 'package:immich_mobile/services/localization.service.dart'; -import 'package:immich_mobile/utils/backup_progress.dart'; -import 'package:immich_mobile/utils/bootstrap.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; -import 'package:immich_mobile/utils/diff.dart'; -import 'package:path_provider_foundation/path_provider_foundation.dart'; -import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; - -final backgroundServiceProvider = Provider((ref) => BackgroundService()); - -/// Background backup service -class BackgroundService { - static const String _portNameLock = "immichLock"; - static const MethodChannel _foregroundChannel = MethodChannel('immich/foregroundChannel'); - static const MethodChannel _backgroundChannel = MethodChannel('immich/backgroundChannel'); - static const notifyInterval = Duration(milliseconds: 400); - bool _isBackgroundInitialized = false; - Completer? _cancellationToken; - bool _canceledBySystem = false; - int _wantsLockTime = 0; - bool _hasLock = false; - SendPort? _waitingIsolate; - ReceivePort? _rp; - bool _errorGracePeriodExceeded = true; - int _uploadedAssetsCount = 0; - int _assetsToUploadCount = 0; - String _lastPrintedDetailContent = ""; - String? _lastPrintedDetailTitle; - late final ThrottleProgressUpdate _throttledNotifiy = ThrottleProgressUpdate(_updateProgress, notifyInterval); - late final ThrottleProgressUpdate _throttledDetailNotify = ThrottleProgressUpdate( - _updateDetailProgress, - notifyInterval, - ); - - bool get isBackgroundInitialized { - return _isBackgroundInitialized; - } - - /// Ensures that the background service is enqueued if enabled in settings - Future resumeServiceIfEnabled() async { - return await isBackgroundBackupEnabled() && await enableService(); - } - - /// Enqueues the background service - Future enableService({bool immediate = false}) async { - try { - final callback = PluginUtilities.getCallbackHandle(_nativeEntry)!; - final String title = "backup_background_service_default_notification".tr(); - final bool ok = await _foregroundChannel.invokeMethod('enable', [callback.toRawHandle(), title, immediate]); - return ok; - } catch (error) { - return false; - } - } - - /// Configures the background service - Future configureService({ - bool requireUnmetered = true, - bool requireCharging = false, - int triggerUpdateDelay = 5000, - int triggerMaxDelay = 50000, - }) async { - try { - final bool ok = await _foregroundChannel.invokeMethod('configure', [ - requireUnmetered, - requireCharging, - triggerUpdateDelay, - triggerMaxDelay, - ]); - return ok; - } catch (error) { - return false; - } - } - - /// Cancels the background service (if currently running) and removes it from work queue - Future disableService() async { - try { - final ok = await _foregroundChannel.invokeMethod('disable'); - return ok; - } catch (error) { - return false; - } - } - - /// Returns `true` if the background service is enabled - Future isBackgroundBackupEnabled() async { - try { - return await _foregroundChannel.invokeMethod("isEnabled"); - } catch (error) { - return false; - } - } - - /// Returns `true` if battery optimizations are disabled - Future isIgnoringBatteryOptimizations() async { - // iOS does not need battery optimizations enabled - if (Platform.isIOS) { - return true; - } - try { - return await _foregroundChannel.invokeMethod('isIgnoringBatteryOptimizations'); - } catch (error) { - return false; - } - } - - // Yet to be implemented - Future digestFile(String path) { - return _foregroundChannel.invokeMethod("digestFile", [path]); - } - - Future?> digestFiles(List paths) { - return _foregroundChannel.invokeListMethod("digestFiles", paths); - } - - /// Updates the notification shown by the background service - Future _updateNotification({ - String? title, - String? content, - int progress = 0, - int max = 0, - bool indeterminate = false, - bool isDetail = false, - bool onlyIfFG = false, - }) async { - try { - if (_isBackgroundInitialized) { - return _backgroundChannel.invokeMethod('updateNotification', [ - title, - content, - progress, - max, - indeterminate, - isDetail, - onlyIfFG, - ]); - } - } catch (error) { - dPrint(() => "[_updateNotification] failed to communicate with plugin"); - } - return false; - } - - /// Shows a new priority notification - Future _showErrorNotification({required String title, String? content, String? individualTag}) async { - try { - if (_isBackgroundInitialized && _errorGracePeriodExceeded) { - return await _backgroundChannel.invokeMethod('showError', [title, content, individualTag]); - } - } catch (error) { - dPrint(() => "[_showErrorNotification] failed to communicate with plugin"); - } - return false; - } - - Future _clearErrorNotifications() async { - try { - if (_isBackgroundInitialized) { - return await _backgroundChannel.invokeMethod('clearErrorNotifications'); - } - } catch (error) { - dPrint(() => "[_clearErrorNotifications] failed to communicate with plugin"); - } - return false; - } - - /// await to ensure this thread (foreground or background) has exclusive access - Future acquireLock() async { - if (_hasLock) { - dPrint(() => "WARNING: [acquireLock] called more than once"); - return true; - } - final int lockTime = Timeline.now; - _wantsLockTime = lockTime; - final ReceivePort rp = ReceivePort(_portNameLock); - _rp = rp; - final SendPort sp = rp.sendPort; - - while (!IsolateNameServer.registerPortWithName(sp, _portNameLock)) { - try { - await _checkLockReleasedWithHeartbeat(lockTime); - } catch (error) { - return false; - } - if (_wantsLockTime != lockTime) { - return false; - } - } - _hasLock = true; - rp.listen(_heartbeatListener); - return true; - } - - Future _checkLockReleasedWithHeartbeat(final int lockTime) async { - SendPort? other = IsolateNameServer.lookupPortByName(_portNameLock); - if (other != null) { - final ReceivePort tempRp = ReceivePort(); - final SendPort tempSp = tempRp.sendPort; - final bs = tempRp.asBroadcastStream(); - while (_wantsLockTime == lockTime) { - other.send(tempSp); - final dynamic answer = await bs.first.timeout(const Duration(seconds: 3), onTimeout: () => null); - if (_wantsLockTime != lockTime) { - break; - } - if (answer == null) { - // other isolate failed to answer, assuming it exited without releasing the lock - if (other == IsolateNameServer.lookupPortByName(_portNameLock)) { - IsolateNameServer.removePortNameMapping(_portNameLock); - } - break; - } else if (answer == true) { - // other isolate released the lock - break; - } else if (answer == false) { - // other isolate is still active - } - final dynamic isFinished = await bs.first.timeout(const Duration(seconds: 3), onTimeout: () => false); - if (isFinished == true) { - break; - } - } - tempRp.close(); - } - } - - void _heartbeatListener(dynamic msg) { - if (msg is SendPort) { - _waitingIsolate = msg; - msg.send(false); - } - } - - /// releases the exclusive access lock - void releaseLock() { - _wantsLockTime = 0; - if (_hasLock) { - IsolateNameServer.removePortNameMapping(_portNameLock); - _waitingIsolate?.send(true); - _waitingIsolate = null; - _hasLock = false; - } - _rp?.close(); - _rp = null; - } - - void _setupBackgroundCallHandler() { - _backgroundChannel.setMethodCallHandler(_callHandler); - _isBackgroundInitialized = true; - _backgroundChannel.invokeMethod('initialized'); - } - - Future _callHandler(MethodCall call) async { - DartPluginRegistrant.ensureInitialized(); - if (Platform.isIOS) { - // NOTE: I'm not sure this is strictly necessary anymore, but - // out of an abundance of caution, we will keep it in until someone - // can say for sure - PathProviderFoundation.registerWith(); - } - switch (call.method) { - case "backgroundProcessing": - case "onAssetsChanged": - try { - unawaited(_clearErrorNotifications()); - - // iOS should time out after some threshold so it doesn't wait - // indefinitely and can run later - // Android is fine to wait here until the lock releases - final waitForLock = Platform.isIOS - ? acquireLock().timeout(const Duration(seconds: 5), onTimeout: () => false) - : acquireLock(); - - final bool hasAccess = await waitForLock; - if (!hasAccess) { - dPrint(() => "[_callHandler] could not acquire lock, exiting"); - return false; - } - - final translationsOk = await loadTranslations(); - if (!translationsOk) { - dPrint(() => "[_callHandler] could not load translations"); - } - - final bool ok = await _onAssetsChanged(); - return ok; - } catch (error) { - dPrint(() => error.toString()); - return false; - } finally { - releaseLock(); - } - case "systemStop": - _canceledBySystem = true; - _cancellationToken?.complete(); - _cancellationToken = null; - return true; - default: - dPrint(() => "Unknown method ${call.method}"); - return false; - } - } - - Future _onAssetsChanged() async { - final (isar, drift, logDb) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDb, shouldBufferLogs: false, listenStoreUpdates: false); - - final ref = ProviderContainer( - overrides: [ - dbProvider.overrideWithValue(isar), - isarProvider.overrideWithValue(isar), - driftProvider.overrideWith(driftOverride(drift)), - ], - ); - - await ref.read(authServiceProvider).setOpenApiServiceEndpoint(); - dPrint(() => "[BG UPLOAD] Using endpoint: ${ref.read(apiServiceProvider).apiClient.basePath}"); - - final selectedAlbums = await ref.read(backupAlbumRepositoryProvider).getAllBySelection(BackupSelection.select); - final excludedAlbums = await ref.read(backupAlbumRepositoryProvider).getAllBySelection(BackupSelection.exclude); - if (selectedAlbums.isEmpty) { - return true; - } - - await ref.read(fileMediaRepositoryProvider).enableBackgroundAccess(); - - do { - final bool backupOk = await _runBackup( - ref.read(backupServiceProvider), - ref.read(appSettingsServiceProvider), - selectedAlbums, - excludedAlbums, - ); - if (backupOk) { - await Store.delete(StoreKey.backupFailedSince); - final backupAlbums = [...selectedAlbums, ...excludedAlbums]; - backupAlbums.sortBy((e) => e.id); - - final dbAlbums = await ref.read(backupAlbumRepositoryProvider).getAll(sort: BackupAlbumSort.id); - final List toDelete = []; - final List toUpsert = []; - // stores the most recent `lastBackup` per album but always keeps the `selection` from the most recent DB state - diffSortedListsSync( - dbAlbums, - backupAlbums, - compare: (BackupAlbum a, BackupAlbum b) => a.id.compareTo(b.id), - both: (BackupAlbum a, BackupAlbum b) { - a.lastBackup = a.lastBackup.isAfter(b.lastBackup) ? a.lastBackup : b.lastBackup; - toUpsert.add(a); - return true; - }, - onlyFirst: (BackupAlbum a) => toUpsert.add(a), - onlySecond: (BackupAlbum b) => toDelete.add(b.isarId), - ); - await ref.read(backupAlbumRepositoryProvider).deleteAll(toDelete); - await ref.read(backupAlbumRepositoryProvider).updateAll(toUpsert); - } else if (Store.tryGet(StoreKey.backupFailedSince) == null) { - await Store.put(StoreKey.backupFailedSince, DateTime.now()); - return false; - } - // Android should check for new assets added while performing backup - } while (Platform.isAndroid && true == await _backgroundChannel.invokeMethod("hasContentChanged")); - return true; - } - - Future _runBackup( - BackupService backupService, - AppSettingsService settingsService, - List selectedAlbums, - List excludedAlbums, - ) async { - _errorGracePeriodExceeded = _isErrorGracePeriodExceeded(settingsService); - final bool notifyTotalProgress = settingsService.getSetting(AppSettingsEnum.backgroundBackupTotalProgress); - final bool notifySingleProgress = settingsService.getSetting(AppSettingsEnum.backgroundBackupSingleProgress); - - if (_canceledBySystem) { - return false; - } - - Set toUpload = await backupService.buildUploadCandidates(selectedAlbums, excludedAlbums); - - try { - toUpload = await backupService.removeAlreadyUploadedAssets(toUpload); - } catch (e) { - unawaited( - _showErrorNotification( - title: "backup_background_service_error_title".tr(), - content: "backup_background_service_connection_failed_message".tr(), - ), - ); - return false; - } - - if (_canceledBySystem) { - return false; - } - - if (toUpload.isEmpty) { - return true; - } - _assetsToUploadCount = toUpload.length; - _uploadedAssetsCount = 0; - unawaited( - _updateNotification( - title: "backup_background_service_in_progress_notification".tr(), - content: notifyTotalProgress ? formatAssetBackupProgress(_uploadedAssetsCount, _assetsToUploadCount) : null, - progress: 0, - max: notifyTotalProgress ? _assetsToUploadCount : 0, - indeterminate: !notifyTotalProgress, - onlyIfFG: !notifyTotalProgress, - ), - ); - - _cancellationToken?.complete(); - _cancellationToken = Completer(); - final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null; - - final bool ok = await backupService.backupAsset( - toUpload, - _cancellationToken!, - pmProgressHandler: pmProgressHandler, - onSuccess: (result) => _onAssetUploaded(shouldNotify: notifyTotalProgress), - onProgress: (bytes, totalBytes) => _onProgress(bytes, totalBytes, shouldNotify: notifySingleProgress), - onCurrentAsset: (asset) => _onSetCurrentBackupAsset(asset, shouldNotify: notifySingleProgress), - onError: _onBackupError, - isBackground: true, - ); - - if (!ok && !_cancellationToken!.isCompleted) { - unawaited( - _showErrorNotification( - title: "backup_background_service_error_title".tr(), - content: "backup_background_service_backup_failed_message".tr(), - ), - ); - } - - return ok; - } - - void _onAssetUploaded({bool shouldNotify = false}) { - if (!shouldNotify) { - return; - } - - _uploadedAssetsCount++; - _throttledNotifiy(); - } - - void _onProgress(int bytes, int totalBytes, {bool shouldNotify = false}) { - if (!shouldNotify) { - return; - } - - _throttledDetailNotify(progress: bytes, total: totalBytes); - } - - void _updateDetailProgress(String? title, int progress, int total) { - final String msg = total > 0 ? humanReadableBytesProgress(progress, total) : ""; - // only update if message actually differs (to stop many useless notification updates on large assets or slow connections) - if (msg != _lastPrintedDetailContent || _lastPrintedDetailTitle != title) { - _lastPrintedDetailContent = msg; - _lastPrintedDetailTitle = title; - _updateNotification( - progress: total > 0 ? (progress * 1000) ~/ total : 0, - max: 1000, - isDetail: true, - title: title, - content: msg, - ); - } - } - - void _updateProgress(String? title, int progress, int total) { - _updateNotification( - progress: _uploadedAssetsCount, - max: _assetsToUploadCount, - title: title, - content: formatAssetBackupProgress(_uploadedAssetsCount, _assetsToUploadCount), - ); - } - - void _onBackupError(ErrorUploadAsset errorAssetInfo) { - _showErrorNotification( - title: "backup_background_service_upload_failure_notification".tr( - namedArgs: {'filename': errorAssetInfo.fileName}, - ), - individualTag: errorAssetInfo.id, - ); - } - - void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset, {bool shouldNotify = false}) { - if (!shouldNotify) { - return; - } - - _throttledDetailNotify.title = "backup_background_service_current_upload_notification".tr( - namedArgs: {'filename': currentUploadAsset.fileName}, - ); - _throttledDetailNotify.progress = 0; - _throttledDetailNotify.total = 0; - } - - bool _isErrorGracePeriodExceeded(AppSettingsService appSettingsService) { - final int value = appSettingsService.getSetting(AppSettingsEnum.uploadErrorNotificationGracePeriod); - if (value == 0) { - return true; - } else if (value == 5) { - return false; - } - final DateTime? failedSince = Store.tryGet(StoreKey.backupFailedSince); - if (failedSince == null) { - return false; - } - final Duration duration = DateTime.now().difference(failedSince); - if (value == 1) { - return duration > const Duration(minutes: 30); - } else if (value == 2) { - return duration > const Duration(hours: 2); - } else if (value == 3) { - return duration > const Duration(hours: 8); - } else if (value == 4) { - return duration > const Duration(hours: 24); - } - assert(false, "Invalid value"); - return true; - } - - Future getIOSBackupLastRun(IosBackgroundTask task) async { - if (!Platform.isIOS) { - return null; - } - // Seconds since last run - final double? lastRun = task == IosBackgroundTask.fetch - ? await _foregroundChannel.invokeMethod('lastBackgroundFetchTime') - : await _foregroundChannel.invokeMethod('lastBackgroundProcessingTime'); - if (lastRun == null) { - return null; - } - final time = DateTime.fromMillisecondsSinceEpoch(lastRun.toInt() * 1000); - return time; - } - - Future getIOSBackupNumberOfProcesses() async { - if (!Platform.isIOS) { - return 0; - } - return await _foregroundChannel.invokeMethod('numberOfBackgroundProcesses'); - } - - Future getIOSBackgroundAppRefreshEnabled() async { - if (!Platform.isIOS) { - return false; - } - return await _foregroundChannel.invokeMethod('backgroundAppRefreshEnabled'); - } -} - -enum IosBackgroundTask { fetch, processing } - -/// entry point called by Kotlin/Java code; needs to be a top-level function -@pragma('vm:entry-point') -void _nativeEntry() { - WidgetsFlutterBinding.ensureInitialized(); - DartPluginRegistrant.ensureInitialized(); - BackgroundService backgroundService = BackgroundService(); - backgroundService._setupBackgroundCallHandler(); -} diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart deleted file mode 100644 index 9b6a26be03..0000000000 --- a/mobile/lib/services/backup.service.dart +++ /dev/null @@ -1,473 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:http/http.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; -import 'package:immich_mobile/repositories/upload.repository.dart'; -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/asset_media.repository.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:logging/logging.dart'; -import 'package:openapi/api.dart'; -import 'package:path/path.dart' as p; -import 'package:permission_handler/permission_handler.dart' as pm; -import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; -import 'package:immich_mobile/utils/debug_print.dart'; - -final backupServiceProvider = Provider( - (ref) => BackupService( - ref.watch(apiServiceProvider), - ref.watch(appSettingsServiceProvider), - ref.watch(albumServiceProvider), - ref.watch(albumMediaRepositoryProvider), - ref.watch(fileMediaRepositoryProvider), - ref.watch(assetRepositoryProvider), - ref.watch(assetMediaRepositoryProvider), - ), -); - -class BackupService { - final ApiService _apiService; - final Logger _log = Logger("BackupService"); - final AppSettingsService _appSetting; - final AlbumService _albumService; - final AlbumMediaRepository _albumMediaRepository; - final FileMediaRepository _fileMediaRepository; - final AssetRepository _assetRepository; - final AssetMediaRepository _assetMediaRepository; - - BackupService( - this._apiService, - this._appSetting, - this._albumService, - this._albumMediaRepository, - this._fileMediaRepository, - this._assetRepository, - this._assetMediaRepository, - ); - - Future?> getDeviceBackupAsset() async { - final String deviceId = Store.get(StoreKey.deviceId); - - try { - return await _apiService.assetsApi.getAllUserAssetsByDeviceId(deviceId); - } catch (e) { - dPrint(() => 'Error [getDeviceBackupAsset] ${e.toString()}'); - return null; - } - } - - Future _saveDuplicatedAssetIds(List deviceAssetIds) => - _assetRepository.transaction(() => _assetRepository.upsertDuplicatedAssets(deviceAssetIds)); - - /// Get duplicated asset id from database - Future> getDuplicatedAssetIds() async { - final duplicates = await _assetRepository.getAllDuplicatedAssetIds(); - return duplicates.toSet(); - } - - /// Returns all assets newer than the last successful backup per album - /// if `useTimeFilter` is set to true, all assets will be returned - Future> buildUploadCandidates( - List selectedBackupAlbums, - List excludedBackupAlbums, { - bool useTimeFilter = true, - }) async { - final now = DateTime.now(); - - final Set toAdd = await _fetchAssetsAndUpdateLastBackup( - selectedBackupAlbums, - now, - useTimeFilter: useTimeFilter, - ); - - if (toAdd.isEmpty) return {}; - - final Set toRemove = await _fetchAssetsAndUpdateLastBackup( - excludedBackupAlbums, - now, - useTimeFilter: useTimeFilter, - ); - - return toAdd.difference(toRemove); - } - - Future> _fetchAssetsAndUpdateLastBackup( - List backupAlbums, - DateTime now, { - bool useTimeFilter = true, - }) async { - Set candidates = {}; - - for (final BackupAlbum backupAlbum in backupAlbums) { - final Album localAlbum; - try { - localAlbum = await _albumMediaRepository.get(backupAlbum.id); - } on StateError { - // the album no longer exists - continue; - } - - if (useTimeFilter && localAlbum.modifiedAt.isBefore(backupAlbum.lastBackup)) { - continue; - } - final List assets; - try { - assets = await _albumMediaRepository.getAssets( - backupAlbum.id, - modifiedFrom: useTimeFilter - ? - // subtract 2 seconds to prevent missing assets due to rounding issues - backupAlbum.lastBackup.subtract(const Duration(seconds: 2)) - : null, - modifiedUntil: useTimeFilter ? now : null, - ); - } on StateError { - // either there are no assets matching the filter criteria OR the album no longer exists - continue; - } - - // Add album's name to the asset info - for (final asset in assets) { - List albumNames = [localAlbum.name]; - - final existingAsset = candidates.firstWhereOrNull((candidate) => candidate.asset.localId == asset.localId); - - if (existingAsset != null) { - albumNames.addAll(existingAsset.albumNames); - candidates.remove(existingAsset); - } - - candidates.add(BackupCandidate(asset: asset, albumNames: albumNames)); - } - - backupAlbum.lastBackup = now; - } - - return candidates; - } - - /// Returns a new list of assets not yet uploaded - Future> removeAlreadyUploadedAssets(Set candidates) async { - if (candidates.isEmpty) { - return candidates; - } - - final Set duplicatedAssetIds = await getDuplicatedAssetIds(); - candidates.removeWhere((candidate) => duplicatedAssetIds.contains(candidate.asset.localId)); - - if (candidates.isEmpty) { - return candidates; - } - - final Set existing = {}; - try { - final String deviceId = Store.get(StoreKey.deviceId); - final CheckExistingAssetsResponseDto? duplicates = await _apiService.assetsApi.checkExistingAssets( - CheckExistingAssetsDto(deviceAssetIds: candidates.map((c) => c.asset.localId!).toList(), deviceId: deviceId), - ); - if (duplicates != null) { - existing.addAll(duplicates.existingIds); - } - } on ApiException { - // workaround for older server versions or when checking for too many assets at once - final List? allAssetsInDatabase = await getDeviceBackupAsset(); - if (allAssetsInDatabase != null) { - existing.addAll(allAssetsInDatabase); - } - } - - if (existing.isNotEmpty) { - candidates.removeWhere((c) => existing.contains(c.asset.localId)); - } - - return candidates; - } - - Future _checkPermissions() async { - if (Platform.isAndroid && !(await pm.Permission.accessMediaLocation.status).isGranted) { - // double check that permission is granted here, to guard against - // uploading corrupt assets without EXIF information - _log.warning( - "Media location permission is not granted. " - "Cannot access original assets for backup.", - ); - - return false; - } - - // DON'T KNOW WHY BUT THIS HELPS BACKGROUND BACKUP TO WORK ON IOS - if (Platform.isIOS) { - await _fileMediaRepository.requestExtendedPermissions(); - } - - return true; - } - - /// Upload images before video assets for background tasks - /// these are further sorted by using their creation date - List _sortPhotosFirst(List candidates) { - return candidates.sorted((a, b) { - final cmp = a.asset.type.index - b.asset.type.index; - if (cmp != 0) return cmp; - return a.asset.fileCreatedAt.compareTo(b.asset.fileCreatedAt); - }); - } - - Future backupAsset( - Iterable assets, - Completer cancelToken, { - bool isBackground = false, - PMProgressHandler? pmProgressHandler, - required void Function(SuccessUploadAsset result) onSuccess, - required void Function(int bytes, int totalBytes) onProgress, - required void Function(CurrentUploadAsset asset) onCurrentAsset, - required void Function(ErrorUploadAsset error) onError, - }) async { - final bool isIgnoreIcloudAssets = _appSetting.getSetting(AppSettingsEnum.ignoreIcloudAssets); - final shouldSyncAlbums = _appSetting.getSetting(AppSettingsEnum.syncAlbums); - final String deviceId = Store.get(StoreKey.deviceId); - final String savedEndpoint = Store.get(StoreKey.serverEndpoint); - final List duplicatedAssetIds = []; - bool anyErrors = false; - - final hasPermission = await _checkPermissions(); - if (!hasPermission) { - return false; - } - - List candidates = assets.toList(); - if (isBackground) { - candidates = _sortPhotosFirst(candidates); - } - - for (final candidate in candidates) { - final Asset asset = candidate.asset; - File? file; - File? livePhotoFile; - - try { - final isAvailableLocally = await asset.local!.isLocallyAvailable(isOrigin: true); - - // Handle getting files from iCloud - if (!isAvailableLocally && Platform.isIOS) { - // Skip iCloud assets if the user has disabled this feature - if (isIgnoreIcloudAssets) { - continue; - } - - onCurrentAsset( - CurrentUploadAsset( - id: asset.localId!, - fileCreatedAt: asset.fileCreatedAt.year == 1970 ? asset.fileModifiedAt : asset.fileCreatedAt, - fileName: asset.fileName, - fileType: _getAssetType(asset.type), - iCloudAsset: true, - ), - ); - - file = await asset.local!.loadFile(progressHandler: pmProgressHandler); - if (asset.local!.isLivePhoto) { - livePhotoFile = await asset.local!.loadFile(withSubtype: true, progressHandler: pmProgressHandler); - } - } else { - file = await asset.local!.originFile.timeout(const Duration(seconds: 5)); - - if (asset.local!.isLivePhoto) { - livePhotoFile = await asset.local!.originFileWithSubtype.timeout(const Duration(seconds: 5)); - } - } - - if (file != null) { - String? originalFileName = await _assetMediaRepository.getOriginalFilename(asset.localId!); - originalFileName ??= asset.fileName; - - if (asset.local!.isLivePhoto) { - if (livePhotoFile == null) { - _log.warning("Failed to obtain motion part of the livePhoto - $originalFileName"); - } - } - - final fileStream = file.openRead(); - final assetRawUploadData = MultipartFile( - "assetData", - fileStream, - file.lengthSync(), - filename: originalFileName, - ); - - final baseRequest = ProgressMultipartRequest( - 'POST', - Uri.parse('$savedEndpoint/assets'), - abortTrigger: cancelToken.future, - onProgress: ((bytes, totalBytes) => onProgress(bytes, totalBytes)), - ); - - baseRequest.fields['deviceAssetId'] = asset.localId!; - baseRequest.fields['deviceId'] = deviceId; - baseRequest.fields['fileCreatedAt'] = asset.fileCreatedAt.toUtc().toIso8601String(); - baseRequest.fields['fileModifiedAt'] = asset.fileModifiedAt.toUtc().toIso8601String(); - baseRequest.fields['isFavorite'] = asset.isFavorite.toString(); - baseRequest.fields['duration'] = asset.duration.toString(); - baseRequest.files.add(assetRawUploadData); - - onCurrentAsset( - CurrentUploadAsset( - id: asset.localId!, - fileCreatedAt: asset.fileCreatedAt.year == 1970 ? asset.fileModifiedAt : asset.fileCreatedAt, - fileName: originalFileName, - fileType: _getAssetType(asset.type), - fileSize: file.lengthSync(), - iCloudAsset: false, - ), - ); - - String? livePhotoVideoId; - if (asset.local!.isLivePhoto && livePhotoFile != null) { - livePhotoVideoId = await uploadLivePhotoVideo(originalFileName, livePhotoFile, baseRequest, cancelToken); - } - - if (livePhotoVideoId != null) { - baseRequest.fields['livePhotoVideoId'] = livePhotoVideoId; - } - - final response = await NetworkRepository.client.send(baseRequest); - - final responseBody = jsonDecode(await response.stream.bytesToString()); - - if (![200, 201].contains(response.statusCode)) { - final error = responseBody; - final errorMessage = error['message'] ?? error['error']; - - dPrint( - () => - "Error(${error['statusCode']}) uploading ${asset.localId} | $originalFileName | Created on ${asset.fileCreatedAt} | ${error['error']}", - ); - - onError( - ErrorUploadAsset( - asset: asset, - id: asset.localId!, - fileCreatedAt: asset.fileCreatedAt, - fileName: originalFileName, - fileType: _getAssetType(candidate.asset.type), - errorMessage: errorMessage, - ), - ); - - if (errorMessage == "Quota has been exceeded!") { - anyErrors = true; - break; - } - - continue; - } - - bool isDuplicate = false; - if (response.statusCode == 200) { - isDuplicate = true; - duplicatedAssetIds.add(asset.localId!); - } - - onSuccess( - SuccessUploadAsset( - candidate: candidate, - remoteAssetId: responseBody['id'] as String, - isDuplicate: isDuplicate, - ), - ); - - if (shouldSyncAlbums) { - await _albumService.syncUploadAlbums(candidate.albumNames, [responseBody['id'] as String]); - } - } - } on RequestAbortedException { - dPrint(() => "Backup was cancelled by the user"); - anyErrors = true; - break; - } catch (error, stackTrace) { - dPrint(() => "Error backup asset: ${error.toString()}: $stackTrace"); - anyErrors = true; - continue; - } finally { - if (Platform.isIOS) { - try { - await file?.delete(); - await livePhotoFile?.delete(); - } catch (e) { - dPrint(() => "ERROR deleting file: ${e.toString()}"); - } - } - } - } - - if (duplicatedAssetIds.isNotEmpty) { - await _saveDuplicatedAssetIds(duplicatedAssetIds); - } - - return !anyErrors; - } - - Future uploadLivePhotoVideo( - String originalFileName, - File? livePhotoVideoFile, - MultipartRequest baseRequest, - Completer cancelToken, - ) async { - if (livePhotoVideoFile == null) { - return null; - } - final livePhotoTitle = p.setExtension(originalFileName, p.extension(livePhotoVideoFile.path)); - final fileStream = livePhotoVideoFile.openRead(); - final livePhotoRawUploadData = MultipartFile( - "assetData", - fileStream, - livePhotoVideoFile.lengthSync(), - filename: livePhotoTitle, - ); - final livePhotoReq = ProgressMultipartRequest(baseRequest.method, baseRequest.url, abortTrigger: cancelToken.future) - ..headers.addAll(baseRequest.headers) - ..fields.addAll(baseRequest.fields); - - livePhotoReq.files.add(livePhotoRawUploadData); - - var response = await NetworkRepository.client.send(livePhotoReq); - - var responseBody = jsonDecode(await response.stream.bytesToString()); - - if (![200, 201].contains(response.statusCode)) { - var error = responseBody; - - dPrint( - () => "Error(${error['statusCode']}) uploading livePhoto for assetId | $livePhotoTitle | ${error['error']}", - ); - } - - return responseBody.containsKey('id') ? responseBody['id'] : null; - } - - String _getAssetType(AssetType assetType) => switch (assetType) { - AssetType.audio => "AUDIO", - AssetType.image => "IMAGE", - AssetType.video => "VIDEO", - AssetType.other => "OTHER", - }; -} diff --git a/mobile/lib/services/backup_album.service.dart b/mobile/lib/services/backup_album.service.dart deleted file mode 100644 index ef9d1031de..0000000000 --- a/mobile/lib/services/backup_album.service.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; - -final backupAlbumServiceProvider = Provider((ref) { - return BackupAlbumService(ref.watch(backupAlbumRepositoryProvider)); -}); - -class BackupAlbumService { - final BackupAlbumRepository _backupAlbumRepository; - - const BackupAlbumService(this._backupAlbumRepository); - - Future> getAll({BackupAlbumSort? sort}) { - return _backupAlbumRepository.getAll(sort: sort); - } - - Future> getIdsBySelection(BackupSelection backup) { - return _backupAlbumRepository.getIdsBySelection(backup); - } - - Future> getAllBySelection(BackupSelection backup) { - return _backupAlbumRepository.getAllBySelection(backup); - } - - Future deleteAll(List ids) { - return _backupAlbumRepository.deleteAll(ids); - } - - Future updateAll(List backupAlbums) { - return _backupAlbumRepository.updateAll(backupAlbums); - } -} diff --git a/mobile/lib/services/backup_verification.service.dart b/mobile/lib/services/backup_verification.service.dart deleted file mode 100644 index 2efd52cc81..0000000000 --- a/mobile/lib/services/backup_verification.service.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; -import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/utils/bootstrap.dart'; -import 'package:immich_mobile/utils/diff.dart'; - -/// Finds duplicates originating from missing EXIF information -class BackupVerificationService { - final UserService _userService; - final FileMediaRepository _fileMediaRepository; - final AssetRepository _assetRepository; - final IsarExifRepository _exifInfoRepository; - - const BackupVerificationService( - this._userService, - this._fileMediaRepository, - this._assetRepository, - this._exifInfoRepository, - ); - - /// Returns at most [limit] assets that were backed up without exif - Future> findWronglyBackedUpAssets({int limit = 100}) async { - final owner = _userService.getMyUser().id; - final List onlyLocal = await _assetRepository.getAll(ownerId: owner, state: AssetState.local, limit: limit); - final List remoteMatches = await _assetRepository.getMatches( - assets: onlyLocal, - ownerId: owner, - state: AssetState.remote, - limit: limit, - ); - final List localMatches = await _assetRepository.getMatches( - assets: remoteMatches, - ownerId: owner, - state: AssetState.local, - limit: limit, - ); - - final List deleteCandidates = [], originals = []; - - await diffSortedLists( - remoteMatches, - localMatches, - compare: (a, b) => a.fileName.compareTo(b.fileName), - both: (a, b) async { - a.exifInfo = await _exifInfoRepository.get(a.id); - deleteCandidates.add(a); - originals.add(b); - return false; - }, - onlyFirst: (a) {}, - onlySecond: (b) {}, - ); - final isolateToken = ServicesBinding.rootIsolateToken!; - final List toDelete; - if (deleteCandidates.length > 10) { - // performs 2 checks in parallel for a nice speedup - final half = deleteCandidates.length ~/ 2; - final lower = compute(_computeSaveToDelete, ( - deleteCandidates: deleteCandidates.slice(0, half), - originals: originals.slice(0, half), - endpoint: Store.get(StoreKey.serverEndpoint), - rootIsolateToken: isolateToken, - fileMediaRepository: _fileMediaRepository, - )); - final upper = compute(_computeSaveToDelete, ( - deleteCandidates: deleteCandidates.slice(half), - originals: originals.slice(half), - endpoint: Store.get(StoreKey.serverEndpoint), - rootIsolateToken: isolateToken, - fileMediaRepository: _fileMediaRepository, - )); - toDelete = await lower + await upper; - } else { - toDelete = await compute(_computeSaveToDelete, ( - deleteCandidates: deleteCandidates, - originals: originals, - endpoint: Store.get(StoreKey.serverEndpoint), - rootIsolateToken: isolateToken, - fileMediaRepository: _fileMediaRepository, - )); - } - return toDelete; - } - - static Future> _computeSaveToDelete( - ({ - List deleteCandidates, - List originals, - String endpoint, - RootIsolateToken rootIsolateToken, - FileMediaRepository fileMediaRepository, - }) - tuple, - ) async { - assert(tuple.deleteCandidates.length == tuple.originals.length); - final List result = []; - BackgroundIsolateBinaryMessenger.ensureInitialized(tuple.rootIsolateToken); - final (isar, drift, logDb) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDb); - await tuple.fileMediaRepository.enableBackgroundAccess(); - final ApiService apiService = ApiService(); - apiService.setEndpoint(tuple.endpoint); - for (int i = 0; i < tuple.deleteCandidates.length; i++) { - if (await _compareAssets(tuple.deleteCandidates[i], tuple.originals[i], apiService)) { - result.add(tuple.deleteCandidates[i]); - } - } - return result; - } - - static Future _compareAssets(Asset remote, Asset local, ApiService apiService) async { - if (remote.checksum == local.checksum) return false; - ExifInfo? exif = remote.exifInfo; - if (exif != null && exif.latitude != null) return false; - if (exif == null || exif.fileSize == null) { - final dto = await apiService.assetsApi.getAssetInfo(remote.remoteId!); - if (dto != null && dto.exifInfo != null) { - exif = ExifDtoConverter.fromDto(dto.exifInfo!); - } - } - final file = await local.local!.originFile; - if (exif != null && file != null && exif.fileSize != null) { - final origSize = await file.length(); - if (exif.fileSize! == origSize || exif.fileSize! != origSize) { - final latLng = await local.local!.latlngAsync(); - - if (exif.latitude == null && - latLng.latitude != null && - (remote.fileCreatedAt.isAtSameMomentAs(local.fileCreatedAt) || - remote.fileModifiedAt.isAtSameMomentAs(local.fileModifiedAt) || - _sameExceptTimeZone(remote.fileCreatedAt, local.fileCreatedAt))) { - if (remote.type == AssetType.video) { - // it's very unlikely that a video of same length, filesize, name - // and date is wrong match. Cannot easily compare videos anyway - return true; - } - - // for images: make sure they are pixel-wise identical - // (skip first few KBs containing metadata) - final Uint64List localImage = _fakeDecodeImg(await file.readAsBytes()); - final res = await apiService.assetsApi.downloadAssetWithHttpInfo(remote.remoteId!); - final Uint64List remoteImage = _fakeDecodeImg(res.bodyBytes); - - final eq = const ListEquality().equals(remoteImage, localImage); - return eq; - } - } - } - - return false; - } - - static Uint64List _fakeDecodeImg(Uint8List bytes) { - const headerLength = 131072; // assume header is at most 128 KB - final start = bytes.length < headerLength * 2 ? (bytes.length ~/ (4 * 8)) * 8 : headerLength; - return bytes.buffer.asUint64List(start); - } - - static bool _sameExceptTimeZone(DateTime a, DateTime b) { - final ms = a.isAfter(b) - ? a.millisecondsSinceEpoch - b.millisecondsSinceEpoch - : b.millisecondsSinceEpoch - a.microsecondsSinceEpoch; - final x = ms / (1000 * 60 * 30); - final y = ms ~/ (1000 * 60 * 30); - return y.toDouble() == x && y < 24; - } -} - -final backupVerificationServiceProvider = Provider( - (ref) => BackupVerificationService( - ref.watch(userServiceProvider), - ref.watch(fileMediaRepositoryProvider), - ref.watch(assetRepositoryProvider), - ref.watch(exifRepositoryProvider), - ), -); diff --git a/mobile/lib/services/deep_link.service.dart b/mobile/lib/services/deep_link.service.dart index 9d2bdbe4a0..5ff0fa8a4d 100644 --- a/mobile/lib/services/deep_link.service.dart +++ b/mobile/lib/services/deep_link.service.dart @@ -7,10 +7,7 @@ import 'package:immich_mobile/domain/services/memory.service.dart'; import 'package:immich_mobile/domain/services/people.service.dart'; import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart' as beta_asset_provider; import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; @@ -18,19 +15,9 @@ import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:immich_mobile/services/memory.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; final deepLinkServiceProvider = Provider( (ref) => DeepLinkService( - ref.watch(memoryServiceProvider), - ref.watch(assetServiceProvider), - ref.watch(albumServiceProvider), - ref.watch(currentAssetProvider.notifier), - ref.watch(currentAlbumProvider.notifier), - // Below is used for beta timeline ref.watch(timelineFactoryProvider), ref.watch(beta_asset_provider.assetServiceProvider), ref.watch(remoteAlbumServiceProvider), @@ -41,14 +28,6 @@ final deepLinkServiceProvider = Provider( ); class DeepLinkService { - /// TODO: Remove this when beta is default - final MemoryService _memoryService; - final AssetService _assetService; - final AlbumService _albumService; - final CurrentAsset _currentAsset; - final CurrentAlbum _currentAlbum; - - /// Used for beta timeline final TimelineFactory _betaTimelineFactory; final beta_asset_service.AssetService _betaAssetService; final RemoteAlbumService _betaRemoteAlbumService; @@ -58,11 +37,6 @@ class DeepLinkService { final UserDto? _currentUser; const DeepLinkService( - this._memoryService, - this._assetService, - this._albumService, - this._currentAsset, - this._currentAlbum, this._betaTimelineFactory, this._betaAssetService, this._betaRemoteAlbumService, @@ -75,7 +49,7 @@ class DeepLinkService { return DeepLink([ // we need something to segue back to if the app was cold started // TODO: use MainTimelineRoute this when beta is default - if (isColdStart) (Store.isBetaTimelineEnabled) ? const TabShellRoute() : const PhotosRoute(), + if (isColdStart) const TabShellRoute(), route, ]); } @@ -138,95 +112,52 @@ class DeepLinkService { } Future _buildMemoryDeepLink(String? memoryId) async { - if (Store.isBetaTimelineEnabled) { - List memories = []; + List memories = []; - if (memoryId == null) { - if (_currentUser == null) { - return null; - } - - memories = await _betaMemoryService.getMemoryLane(_currentUser.id); - } else { - final memory = await _betaMemoryService.get(memoryId); - if (memory != null) { - memories = [memory]; - } - } - - if (memories.isEmpty) { + if (memoryId == null) { + if (_currentUser == null) { return null; } - return DriftMemoryRoute(memories: memories, memoryIndex: 0); + memories = await _betaMemoryService.getMemoryLane(_currentUser.id); } else { - // TODO: Remove this when beta is default - if (memoryId == null) { - return null; + final memory = await _betaMemoryService.get(memoryId); + if (memory != null) { + memories = [memory]; } - final memory = await _memoryService.getMemoryById(memoryId); - - if (memory == null) { - return null; - } - - return MemoryRoute(memories: [memory], memoryIndex: 0); } - } - Future _buildAssetDeepLink(String assetId, WidgetRef ref) async { - if (Store.isBetaTimelineEnabled) { - final asset = await _betaAssetService.getRemoteAsset(assetId); - if (asset == null) { - return null; - } - - AssetViewer.setAsset(ref, asset); - return AssetViewerRoute( - initialIndex: 0, - timelineService: _betaTimelineFactory.fromAssets([asset], TimelineOrigin.deepLink), - ); - } else { - // TODO: Remove this when beta is default - final asset = await _assetService.getAssetByRemoteId(assetId); - if (asset == null) { - return null; - } - - _currentAsset.set(asset); - final renderList = await RenderList.fromAssets([asset], GroupAssetsBy.auto); - - return GalleryViewerRoute(renderList: renderList, initialIndex: 0, heroOffset: 0, showStack: true); - } - } - - Future _buildAlbumDeepLink(String albumId) async { - if (Store.isBetaTimelineEnabled) { - final album = await _betaRemoteAlbumService.get(albumId); - - if (album == null) { - return null; - } - - return RemoteAlbumRoute(album: album); - } else { - // TODO: Remove this when beta is default - final album = await _albumService.getAlbumByRemoteId(albumId); - - if (album == null) { - return null; - } - - _currentAlbum.set(album); - return AlbumViewerRoute(albumId: album.id); - } - } - - Future _buildActivityDeepLink(String albumId) async { - if (Store.isBetaTimelineEnabled == false) { + if (memories.isEmpty) { return null; } + return DriftMemoryRoute(memories: memories, memoryIndex: 0); + } + + Future _buildAssetDeepLink(String assetId, WidgetRef ref) async { + final asset = await _betaAssetService.getRemoteAsset(assetId); + if (asset == null) { + return null; + } + + AssetViewer.setAsset(ref, asset); + return AssetViewerRoute( + initialIndex: 0, + timelineService: _betaTimelineFactory.fromAssets([asset], TimelineOrigin.deepLink), + ); + } + + Future _buildAlbumDeepLink(String albumId) async { + final album = await _betaRemoteAlbumService.get(albumId); + + if (album == null) { + return null; + } + + return RemoteAlbumRoute(album: album); + } + + Future _buildActivityDeepLink(String albumId) async { final album = await _betaRemoteAlbumService.get(albumId); if (album == null || album.isActivityEnabled == false) { @@ -237,10 +168,6 @@ class DeepLinkService { } Future _buildPeopleDeepLink(String personId) async { - if (Store.isBetaTimelineEnabled == false) { - return null; - } - final person = await _betaPeopleService.get(personId); if (person == null) { diff --git a/mobile/lib/services/device.service.dart b/mobile/lib/services/device.service.dart deleted file mode 100644 index 50a0d93b24..0000000000 --- a/mobile/lib/services/device.service.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter_udid/flutter_udid.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; - -final deviceServiceProvider = Provider((ref) => const DeviceService()); - -class DeviceService { - const DeviceService(); - - createDeviceId() { - return FlutterUdid.consistentUdid; - } - - /// Returns the device ID from local storage or creates a new one if not found. - /// - /// This method first attempts to retrieve the device ID from the local store using - /// [StoreKey.deviceId]. If no device ID is found (returns null), it generates a - /// new device ID by calling [createDeviceId]. - /// - /// Returns a [String] representing the device's unique identifier. - String getDeviceId() { - return Store.tryGet(StoreKey.deviceId) ?? createDeviceId(); - } -} diff --git a/mobile/lib/services/download.service.dart b/mobile/lib/services/download.service.dart index 8e810ced2a..3f2c36fa7e 100644 --- a/mobile/lib/services/download.service.dart +++ b/mobile/lib/services/download.service.dart @@ -3,14 +3,9 @@ import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/models/download/livephotos_medatada.model.dart'; import 'package:immich_mobile/repositories/download.repository.dart'; import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/services/api.service.dart'; import 'package:logging/logging.dart'; final downloadServiceProvider = Provider( @@ -54,7 +49,7 @@ class DownloadService { final title = task.filename; final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null; try { - final Asset? resultAsset = await _fileMediaRepository.saveImageWithFile( + final resultAsset = await _fileMediaRepository.saveImageWithFile( filePath, title: title, relativePath: relativePath, @@ -76,7 +71,7 @@ class DownloadService { final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null; final file = File(filePath); try { - final Asset? resultAsset = await _fileMediaRepository.saveVideo(file, title: title, relativePath: relativePath); + final resultAsset = await _fileMediaRepository.saveVideo(file, title: title, relativePath: relativePath); return resultAsset != null; } catch (error, stack) { _log.severe("Error saving video", error, stack); @@ -136,62 +131,6 @@ class DownloadService { Future cancelDownload(String id) async { return await FileDownloader().cancelTaskWithId(id); } - - Future> downloadAll(List assets) async { - return await _downloadRepository.downloadAll(assets.expand(_createDownloadTasks).toList()); - } - - Future download(Asset asset) async { - final tasks = _createDownloadTasks(asset); - await _downloadRepository.downloadAll(tasks); - } - - List _createDownloadTasks(Asset asset) { - if (asset.isImage && asset.livePhotoVideoId != null && Platform.isIOS) { - return [ - _buildDownloadTask( - asset.remoteId!, - asset.fileName, - group: kDownloadGroupLivePhoto, - metadata: LivePhotosMetadata(part: LivePhotosPart.image, id: asset.remoteId!).toJson(), - ), - _buildDownloadTask( - asset.livePhotoVideoId!, - asset.fileName.toUpperCase().replaceAll(RegExp(r"\.(JPG|HEIC)$"), '.MOV'), - group: kDownloadGroupLivePhoto, - metadata: LivePhotosMetadata(part: LivePhotosPart.video, id: asset.remoteId!).toJson(), - ), - ]; - } - - if (asset.remoteId == null) { - return []; - } - - return [ - _buildDownloadTask( - asset.remoteId!, - asset.fileName, - group: asset.isImage ? kDownloadGroupImage : kDownloadGroupVideo, - ), - ]; - } - - DownloadTask _buildDownloadTask(String id, String filename, {String? group, String? metadata}) { - final path = r'/assets/{id}/original'.replaceAll('{id}', id); - final serverEndpoint = Store.get(StoreKey.serverEndpoint); - final headers = ApiService.getRequestHeaders(); - - return DownloadTask( - taskId: id, - url: serverEndpoint + path, - headers: headers, - filename: filename, - updates: Updates.statusAndProgress, - group: group ?? '', - metaData: metadata ?? '', - ); - } } TaskRecord _findTaskRecord(List records, String livePhotosId, LivePhotosPart part) { diff --git a/mobile/lib/services/entity.service.dart b/mobile/lib/services/entity.service.dart deleted file mode 100644 index fe7358fce6..0000000000 --- a/mobile/lib/services/entity.service.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; - -class EntityService { - final AssetRepository _assetRepository; - final IsarUserRepository _isarUserRepository; - const EntityService(this._assetRepository, this._isarUserRepository); - - Future fillAlbumWithDatabaseEntities(Album album) async { - final ownerId = album.ownerId; - if (ownerId != null) { - // replace owner with user from database - final user = await _isarUserRepository.getByUserId(ownerId); - album.owner.value = user == null ? null : User.fromDto(user); - } - final thumbnailAssetId = album.remoteThumbnailAssetId ?? album.thumbnail.value?.remoteId; - if (thumbnailAssetId != null) { - // set thumbnail with asset from database - album.thumbnail.value = await _assetRepository.getByRemoteId(thumbnailAssetId); - } - if (album.remoteUsers.isNotEmpty) { - // replace all users with users from database - final users = await _isarUserRepository.getByUserIds(album.remoteUsers.map((user) => user.id).toList()); - album.sharedUsers.clear(); - album.sharedUsers.addAll(users.nonNulls.map(User.fromDto)); - album.shared = true; - } - if (album.remoteAssets.isNotEmpty) { - // replace all assets with assets from database - final assets = await _assetRepository.getAllByRemoteId(album.remoteAssets.map((asset) => asset.remoteId!)); - album.assets.clear(); - album.assets.addAll(assets); - } - return album; - } -} - -final entityServiceProvider = Provider( - (ref) => EntityService(ref.watch(assetRepositoryProvider), ref.watch(userRepositoryProvider)), -); diff --git a/mobile/lib/services/etag.service.dart b/mobile/lib/services/etag.service.dart deleted file mode 100644 index 00eb83fcea..0000000000 --- a/mobile/lib/services/etag.service.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/repositories/etag.repository.dart'; - -final etagServiceProvider = Provider((ref) => ETagService(ref.watch(etagRepositoryProvider))); - -class ETagService { - final ETagRepository _eTagRepository; - - const ETagService(this._eTagRepository); - - Future clearTable() { - return _eTagRepository.clearTable(); - } -} diff --git a/mobile/lib/services/exif.service.dart b/mobile/lib/services/exif.service.dart deleted file mode 100644 index 57f793b21e..0000000000 --- a/mobile/lib/services/exif.service.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; - -final exifServiceProvider = Provider((ref) => ExifService(ref.watch(exifRepositoryProvider))); - -class ExifService { - final IsarExifRepository _exifInfoRepository; - - const ExifService(this._exifInfoRepository); - - Future clearTable() { - return _exifInfoRepository.deleteAll(); - } -} diff --git a/mobile/lib/services/hash.service.dart b/mobile/lib/services/hash.service.dart deleted file mode 100644 index 9d1f4e51e8..0000000000 --- a/mobile/lib/services/hash.service.dart +++ /dev/null @@ -1,191 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:flutter/foundation.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/domain/models/device_asset.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/device_asset.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/device_asset.provider.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:logging/logging.dart'; - -class HashService { - HashService({ - required IsarDeviceAssetRepository deviceAssetRepository, - required BackgroundService backgroundService, - this.batchSizeLimit = kBatchHashSizeLimit, - int? batchFileLimit, - }) : _deviceAssetRepository = deviceAssetRepository, - _backgroundService = backgroundService, - batchFileLimit = batchFileLimit ?? kBatchHashFileLimit; - - final IsarDeviceAssetRepository _deviceAssetRepository; - final BackgroundService _backgroundService; - final int batchSizeLimit; - final int batchFileLimit; - final _log = Logger('HashService'); - - /// Processes a list of local [Asset]s, storing their hash and returning only those - /// that were successfully hashed. Hashes are looked up in a DB table - /// [DeviceAsset] by local id. Only missing entries are newly hashed and added to the DB table. - Future> hashAssets(List assets) async { - assets.sort(Asset.compareByLocalId); - - // Get and sort DB entries - guaranteed to be a subset of assets - final hashesInDB = await _deviceAssetRepository.getByIds(assets.map((a) => a.localId!).toList()); - hashesInDB.sort((a, b) => a.assetId.compareTo(b.assetId)); - - int dbIndex = 0; - int bytesProcessed = 0; - final hashedAssets = []; - final toBeHashed = <_AssetPath>[]; - final toBeDeleted = []; - - for (int assetIndex = 0; assetIndex < assets.length; assetIndex++) { - final asset = assets[assetIndex]; - DeviceAsset? matchingDbEntry; - - if (dbIndex < hashesInDB.length) { - final deviceAsset = hashesInDB[dbIndex]; - if (deviceAsset.assetId == asset.localId) { - matchingDbEntry = deviceAsset; - dbIndex++; - } - } - - if (matchingDbEntry != null && - matchingDbEntry.hash.isNotEmpty && - matchingDbEntry.modifiedTime.isAtSameMomentAs(asset.fileModifiedAt)) { - // Reuse the existing hash - hashedAssets.add(asset.copyWith(checksum: base64.encode(matchingDbEntry.hash))); - continue; - } - - final file = await _tryGetAssetFile(asset); - if (file == null) { - // Can't access file, delete any DB entry - if (matchingDbEntry != null) { - toBeDeleted.add(matchingDbEntry.assetId); - } - continue; - } - - bytesProcessed += await file.length(); - toBeHashed.add(_AssetPath(asset: asset, path: file.path)); - - if (_shouldProcessBatch(toBeHashed.length, bytesProcessed)) { - hashedAssets.addAll(await _processBatch(toBeHashed, toBeDeleted)); - toBeHashed.clear(); - toBeDeleted.clear(); - bytesProcessed = 0; - } - } - assert(dbIndex == hashesInDB.length, "All hashes should've been processed"); - - // Process any remaining files - if (toBeHashed.isNotEmpty) { - hashedAssets.addAll(await _processBatch(toBeHashed, toBeDeleted)); - } - - // Clean up deleted references - if (toBeDeleted.isNotEmpty) { - await _deviceAssetRepository.deleteIds(toBeDeleted); - } - - return hashedAssets; - } - - bool _shouldProcessBatch(int assetCount, int bytesProcessed) => - assetCount >= batchFileLimit || bytesProcessed >= batchSizeLimit; - - Future _tryGetAssetFile(Asset asset) async { - try { - final file = await asset.local!.originFile; - if (file == null) { - _log.warning( - "Failed to get file for asset ${asset.localId ?? ''}, name: ${asset.fileName}, created on: ${asset.fileCreatedAt}, skipping", - ); - return null; - } - return file; - } catch (error, stackTrace) { - _log.warning( - "Error getting file to hash for asset ${asset.localId ?? ''}, name: ${asset.fileName}, created on: ${asset.fileCreatedAt}, skipping", - error, - stackTrace, - ); - return null; - } - } - - /// Processes a batch of files and returns a list of successfully hashed assets after saving - /// them in [DeviceAssetToHash] for future retrieval - Future> _processBatch(List<_AssetPath> toBeHashed, List toBeDeleted) async { - _log.info("Hashing ${toBeHashed.length} files"); - final hashes = await _hashFiles(toBeHashed.map((e) => e.path).toList()); - assert( - hashes.length == toBeHashed.length, - "Number of Hashes returned from platform should be the same as the input", - ); - - final hashedAssets = []; - final toBeAdded = []; - - for (final (index, hash) in hashes.indexed) { - final asset = toBeHashed.elementAtOrNull(index)?.asset; - if (asset != null && hash?.length == 20) { - hashedAssets.add(asset.copyWith(checksum: base64.encode(hash!))); - toBeAdded.add(DeviceAsset(assetId: asset.localId!, hash: hash, modifiedTime: asset.fileModifiedAt)); - } else { - _log.warning("Failed to hash file ${asset?.localId ?? ''}"); - if (asset != null) { - toBeDeleted.add(asset.localId!); - } - } - } - - // Update the DB for future retrieval - await _deviceAssetRepository.transaction(() async { - await _deviceAssetRepository.updateAll(toBeAdded); - await _deviceAssetRepository.deleteIds(toBeDeleted); - }); - - _log.fine("Hashed ${hashedAssets.length}/${toBeHashed.length} assets"); - return hashedAssets; - } - - /// Hashes the given files and returns a list of the same length. - /// Files that could not be hashed will have a `null` value - Future> _hashFiles(List paths) async { - try { - final hashes = await _backgroundService.digestFiles(paths); - if (hashes != null) { - return hashes; - } - _log.severe("Hashing ${paths.length} files failed"); - } catch (e, s) { - _log.severe("Error occurred while hashing assets", e, s); - } - return List.filled(paths.length, null); - } -} - -class _AssetPath { - final Asset asset; - final String path; - - const _AssetPath({required this.asset, required this.path}); - - _AssetPath copyWith({Asset? asset, String? path}) { - return _AssetPath(asset: asset ?? this.asset, path: path ?? this.path); - } -} - -final hashServiceProvider = Provider( - (ref) => HashService( - deviceAssetRepository: ref.watch(deviceAssetRepositoryProvider), - backgroundService: ref.watch(backgroundServiceProvider), - ), -); diff --git a/mobile/lib/services/local_notification.service.dart b/mobile/lib/services/local_notification.service.dart deleted file mode 100644 index bf85f4a9a9..0000000000 --- a/mobile/lib/services/local_notification.service.dart +++ /dev/null @@ -1,118 +0,0 @@ -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; -import 'package:immich_mobile/providers/notification_permission.provider.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; - -final localNotificationService = Provider( - (ref) => LocalNotificationService(ref.watch(notificationPermissionProvider), ref), -); - -class LocalNotificationService { - final FlutterLocalNotificationsPlugin _localNotificationPlugin = FlutterLocalNotificationsPlugin(); - final PermissionStatus _permissionStatus; - final Ref ref; - - LocalNotificationService(this._permissionStatus, this.ref); - - static const manualUploadNotificationID = 4; - static const manualUploadDetailedNotificationID = 5; - static const manualUploadChannelName = 'Manual Asset Upload'; - static const manualUploadChannelID = 'immich/manualUpload'; - static const manualUploadChannelNameDetailed = 'Manual Asset Upload Detailed'; - static const manualUploadDetailedChannelID = 'immich/manualUploadDetailed'; - static const cancelUploadActionID = 'cancel_upload'; - - Future setup() async { - const androidSetting = AndroidInitializationSettings('@drawable/notification_icon'); - const iosSetting = DarwinInitializationSettings(); - - const initSettings = InitializationSettings(android: androidSetting, iOS: iosSetting); - - await _localNotificationPlugin.initialize( - initSettings, - onDidReceiveNotificationResponse: _onDidReceiveForegroundNotificationResponse, - ); - } - - Future _showOrUpdateNotification( - int id, - String title, - String body, - AndroidNotificationDetails androidNotificationDetails, - DarwinNotificationDetails iosNotificationDetails, - ) async { - final notificationDetails = NotificationDetails(android: androidNotificationDetails, iOS: iosNotificationDetails); - - if (_permissionStatus == PermissionStatus.granted) { - await _localNotificationPlugin.show(id, title, body, notificationDetails); - } - } - - Future closeNotification(int id) { - return _localNotificationPlugin.cancel(id); - } - - Future showOrUpdateManualUploadStatus( - String title, - String body, { - bool? isDetailed, - bool? presentBanner, - bool? showActions, - int? maxProgress, - int? progress, - }) { - var notificationlId = manualUploadNotificationID; - var androidChannelID = manualUploadChannelID; - var androidChannelName = manualUploadChannelName; - // Separate Notification for Info/Alerts and Progress - if (isDetailed != null && isDetailed) { - notificationlId = manualUploadDetailedNotificationID; - androidChannelID = manualUploadDetailedChannelID; - androidChannelName = manualUploadChannelNameDetailed; - } - // Progress notification - final androidNotificationDetails = (maxProgress != null && progress != null) - ? AndroidNotificationDetails( - androidChannelID, - androidChannelName, - ticker: title, - showProgress: true, - onlyAlertOnce: true, - maxProgress: maxProgress, - progress: progress, - indeterminate: false, - playSound: false, - priority: Priority.low, - importance: Importance.low, - ongoing: true, - actions: (showActions ?? false) - ? [ - const AndroidNotificationAction(cancelUploadActionID, 'Cancel', showsUserInterface: true), - ] - : null, - ) - // Non-progress notification - : AndroidNotificationDetails(androidChannelID, androidChannelName, playSound: false); - - final iosNotificationDetails = DarwinNotificationDetails( - presentBadge: true, - presentList: true, - presentBanner: presentBanner, - ); - - return _showOrUpdateNotification(notificationlId, title, body, androidNotificationDetails, iosNotificationDetails); - } - - void _onDidReceiveForegroundNotificationResponse(NotificationResponse notificationResponse) { - // Handle notification actions - switch (notificationResponse.actionId) { - case cancelUploadActionID: - { - dPrint(() => "User cancelled manual upload operation"); - ref.read(manualUploadProvider.notifier).cancelBackup(); - } - } - } -} diff --git a/mobile/lib/services/memory.service.dart b/mobile/lib/services/memory.service.dart deleted file mode 100644 index e485bb0957..0000000000 --- a/mobile/lib/services/memory.service.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/models/memories/memory.model.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:logging/logging.dart'; - -final memoryServiceProvider = StateProvider((ref) { - return MemoryService(ref.watch(apiServiceProvider), ref.watch(assetRepositoryProvider)); -}); - -class MemoryService { - final log = Logger("MemoryService"); - - final ApiService _apiService; - final AssetRepository _assetRepository; - - MemoryService(this._apiService, this._assetRepository); - - Future?> getMemoryLane() async { - try { - final now = DateTime.now(); - final data = await _apiService.memoriesApi.searchMemories( - for_: DateTime.utc(now.year, now.month, now.day, 0, 0, 0), - ); - - if (data == null) { - return null; - } - - List memories = []; - - for (final memory in data) { - final dbAssets = await _assetRepository.getAllByRemoteId(memory.assets.map((e) => e.id)); - final yearsAgo = now.year - memory.data.year; - if (dbAssets.isNotEmpty) { - final String title = 'years_ago'.t(args: {'years': yearsAgo.toString()}); - memories.add(Memory(title: title, assets: dbAssets)); - } - } - - return memories.isNotEmpty ? memories : null; - } catch (error, stack) { - log.severe("Cannot get memories", error, stack); - return null; - } - } - - Future getMemoryById(String id) async { - try { - final memoryResponse = await _apiService.memoriesApi.getMemory(id); - - if (memoryResponse == null) { - return null; - } - final dbAssets = await _assetRepository.getAllByRemoteId(memoryResponse.assets.map((e) => e.id)); - if (dbAssets.isEmpty) { - log.warning("No assets found for memory with ID: $id"); - return null; - } - final yearsAgo = DateTime.now().year - memoryResponse.data.year; - final String title = 'years_ago'.t(args: {'years': yearsAgo.toString()}); - - return Memory(title: title, assets: dbAssets); - } catch (error, stack) { - log.severe("Cannot get memory with ID: $id", error, stack); - return null; - } - } -} diff --git a/mobile/lib/services/partner.service.dart b/mobile/lib/services/partner.service.dart deleted file mode 100644 index b8e5ae9a4d..0000000000 --- a/mobile/lib/services/partner.service.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/partner.repository.dart'; -import 'package:immich_mobile/repositories/partner_api.repository.dart'; -import 'package:logging/logging.dart'; - -final partnerServiceProvider = Provider( - (ref) => PartnerService( - ref.watch(partnerApiRepositoryProvider), - ref.watch(userRepositoryProvider), - ref.watch(partnerRepositoryProvider), - ), -); - -class PartnerService { - final PartnerApiRepository _partnerApiRepository; - final PartnerRepository _partnerRepository; - final IsarUserRepository _isarUserRepository; - final Logger _log = Logger("PartnerService"); - - PartnerService(this._partnerApiRepository, this._isarUserRepository, this._partnerRepository); - - Future> getSharedWith() async { - return _partnerRepository.getSharedWith(); - } - - Future> getSharedBy() async { - return _partnerRepository.getSharedBy(); - } - - Stream> watchSharedWith() { - return _partnerRepository.watchSharedWith(); - } - - Stream> watchSharedBy() { - return _partnerRepository.watchSharedBy(); - } - - Future removePartner(UserDto partner) async { - try { - await _partnerApiRepository.delete(partner.id); - await _isarUserRepository.update(partner.copyWith(isPartnerSharedBy: false)); - } catch (e) { - _log.warning("Failed to remove partner ${partner.id}", e); - return false; - } - return true; - } - - Future addPartner(UserDto partner) async { - try { - await _partnerApiRepository.create(partner.id); - await _isarUserRepository.update(partner.copyWith(isPartnerSharedBy: true)); - return true; - } catch (e) { - _log.warning("Failed to add partner ${partner.id}", e); - } - return false; - } - - Future updatePartner(UserDto partner, {required bool inTimeline}) async { - try { - final dto = await _partnerApiRepository.update(partner.id, inTimeline: inTimeline); - await _isarUserRepository.update(partner.copyWith(inTimeline: dto.inTimeline)); - return true; - } catch (e) { - _log.warning("Failed to update partner ${partner.id}", e); - } - return false; - } -} diff --git a/mobile/lib/services/person.service.dart b/mobile/lib/services/person.service.dart index 37b16a8d29..023c62ed78 100644 --- a/mobile/lib/services/person.service.dart +++ b/mobile/lib/services/person.service.dart @@ -1,8 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/asset_api.repository.dart'; import 'package:immich_mobile/repositories/person_api.repository.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -10,19 +7,12 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'person.service.g.dart'; @riverpod -PersonService personService(Ref ref) => PersonService( - ref.watch(personApiRepositoryProvider), - ref.watch(assetApiRepositoryProvider), - ref.read(assetRepositoryProvider), -); +PersonService personService(Ref ref) => PersonService(ref.watch(personApiRepositoryProvider)); class PersonService { final Logger _log = Logger("PersonService"); final PersonApiRepository _personApiRepository; - final AssetApiRepository _assetApiRepository; - final AssetRepository _assetRepository; - - PersonService(this._personApiRepository, this._assetApiRepository, this._assetRepository); + PersonService(this._personApiRepository); Future> getAllPeople() async { try { @@ -33,16 +23,6 @@ class PersonService { } } - Future> getPersonAssets(String id) async { - try { - final assets = await _assetApiRepository.search(personIds: [id]); - return await _assetRepository.getAllByRemoteId(assets.map((a) => a.remoteId!)); - } catch (error, stack) { - _log.severe("Error while fetching person assets", error, stack); - } - return []; - } - Future updateName(String id, String name) async { try { return await _personApiRepository.update(id, name: name); diff --git a/mobile/lib/services/person.service.g.dart b/mobile/lib/services/person.service.g.dart index 8c2d46b3bd..4caf1ea434 100644 --- a/mobile/lib/services/person.service.g.dart +++ b/mobile/lib/services/person.service.g.dart @@ -6,7 +6,7 @@ part of 'person.service.dart'; // RiverpodGenerator // ************************************************************************** -String _$personServiceHash() => r'10883bccc6c402205e6785cf9ee6cd7142cd0983'; +String _$personServiceHash() => r'646e38d764c52e63d9fca86992e440f34196d519'; /// See also [personService]. @ProviderFor(personService) diff --git a/mobile/lib/services/search.service.dart b/mobile/lib/services/search.service.dart index f33adf80f9..0330c8485c 100644 --- a/mobile/lib/services/search.service.dart +++ b/mobile/lib/services/search.service.dart @@ -1,31 +1,22 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/string_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/search_api.repository.dart'; -import 'package:immich_mobile/models/search/search_filter.model.dart'; -import 'package:immich_mobile/models/search/search_result.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/infrastructure/search.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; final searchServiceProvider = Provider( - (ref) => SearchService( - ref.watch(apiServiceProvider), - ref.watch(assetRepositoryProvider), - ref.watch(searchApiRepositoryProvider), - ), + (ref) => SearchService(ref.watch(apiServiceProvider), ref.watch(searchApiRepositoryProvider)), ); class SearchService { final ApiService _apiService; - final AssetRepository _assetRepository; final SearchApiRepository _searchApiRepository; final _log = Logger("SearchService"); - SearchService(this._apiService, this._assetRepository, this._searchApiRepository); + SearchService(this._apiService, this._searchApiRepository); Future?> getSearchSuggestions( SearchSuggestionType type, { @@ -48,24 +39,6 @@ class SearchService { } } - Future search(SearchFilter filter, int page) async { - try { - final response = await _searchApiRepository.search(filter, page); - - if (response == null || response.assets.items.isEmpty) { - return null; - } - - return SearchResult( - assets: await _assetRepository.getAllByRemoteId(response.assets.items.map((e) => e.id)), - nextPage: response.assets.nextPage?.toInt(), - ); - } catch (error, stackTrace) { - _log.severe("Failed to search for assets", error, stackTrace); - } - return null; - } - Future?> getExploreData() async { try { return await _apiService.searchApi.getExploreData(); diff --git a/mobile/lib/services/share.service.dart b/mobile/lib/services/share.service.dart deleted file mode 100644 index a0998d6d3d..0000000000 --- a/mobile/lib/services/share.service.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/response_extensions.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:logging/logging.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:share_plus/share_plus.dart'; - -final shareServiceProvider = Provider((ref) => ShareService(ref.watch(apiServiceProvider))); - -class ShareService { - final ApiService _apiService; - final Logger _log = Logger("ShareService"); - - ShareService(this._apiService); - - Future shareAsset(Asset asset, BuildContext context) async { - return await shareAssets([asset], context); - } - - Future shareAssets(List assets, BuildContext context) async { - try { - final downloadedXFiles = []; - - for (var asset in assets) { - if (asset.isLocal) { - // Prefer local assets to share - File? f = await asset.local!.originFile; - downloadedXFiles.add(XFile(f!.path)); - } else if (asset.isRemote) { - // Download remote asset otherwise - final tempDir = await getTemporaryDirectory(); - final fileName = asset.fileName; - final tempFile = await File('${tempDir.path}/$fileName').create(); - final res = await _apiService.assetsApi.downloadAssetWithHttpInfo(asset.remoteId!); - - if (res.statusCode != 200) { - _log.severe("Asset download for ${asset.fileName} failed", res.toLoggerString()); - continue; - } - - tempFile.writeAsBytesSync(res.bodyBytes); - downloadedXFiles.add(XFile(tempFile.path)); - } - } - - if (downloadedXFiles.isEmpty) { - _log.warning("No asset can be retrieved for share"); - return false; - } - - if (downloadedXFiles.length != assets.length) { - _log.warning("Partial share - Requested: ${assets.length}, Sharing: ${downloadedXFiles.length}"); - } - - final size = MediaQuery.of(context).size; - unawaited( - Share.shareXFiles( - downloadedXFiles, - sharePositionOrigin: Rect.fromPoints(Offset.zero, Offset(size.width / 3, size.height)), - ), - ); - return true; - } catch (error) { - _log.severe("Share failed", error); - } - return false; - } -} diff --git a/mobile/lib/services/stack.service.dart b/mobile/lib/services/stack.service.dart deleted file mode 100644 index 88189c6bcd..0000000000 --- a/mobile/lib/services/stack.service.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:openapi/api.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; - -class StackService { - const StackService(this._api, this._assetRepository); - - final ApiService _api; - final AssetRepository _assetRepository; - - Future getStack(String stackId) async { - try { - return _api.stacksApi.getStack(stackId); - } catch (error) { - dPrint(() => "Error while fetching stack: $error"); - } - return null; - } - - Future createStack(List assetIds) async { - try { - return _api.stacksApi.createStack(StackCreateDto(assetIds: assetIds)); - } catch (error) { - dPrint(() => "Error while creating stack: $error"); - } - return null; - } - - Future updateStack(String stackId, String primaryAssetId) async { - try { - return await _api.stacksApi.updateStack(stackId, StackUpdateDto(primaryAssetId: primaryAssetId)); - } catch (error) { - dPrint(() => "Error while updating stack children: $error"); - } - return null; - } - - Future deleteStack(String stackId, List assets) async { - try { - await _api.stacksApi.deleteStack(stackId); - - // Update local database to trigger rerendering - final List removeAssets = []; - for (final asset in assets) { - asset.stackId = null; - asset.stackPrimaryAssetId = null; - asset.stackCount = 0; - - removeAssets.add(asset); - } - await _assetRepository.transaction(() => _assetRepository.updateAll(removeAssets)); - } catch (error) { - dPrint(() => "Error while deleting stack: $error"); - } - } -} - -final stackServiceProvider = Provider( - (ref) => StackService(ref.watch(apiServiceProvider), ref.watch(assetRepositoryProvider)), -); diff --git a/mobile/lib/services/sync.service.dart b/mobile/lib/services/sync.service.dart deleted file mode 100644 index f5b55f36eb..0000000000 --- a/mobile/lib/services/sync.service.dart +++ /dev/null @@ -1,945 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/extensions/collection_extensions.dart'; -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/album.repository.dart'; -import 'package:immich_mobile/repositories/album_api.repository.dart'; -import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/etag.repository.dart'; -import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; -import 'package:immich_mobile/repositories/partner.repository.dart'; -import 'package:immich_mobile/repositories/partner_api.repository.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/entity.service.dart'; -import 'package:immich_mobile/services/hash.service.dart'; -import 'package:immich_mobile/utils/async_mutex.dart'; -import 'package:immich_mobile/utils/datetime_comparison.dart'; -import 'package:immich_mobile/utils/diff.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:logging/logging.dart'; - -final syncServiceProvider = Provider( - (ref) => SyncService( - ref.watch(hashServiceProvider), - ref.watch(entityServiceProvider), - ref.watch(albumMediaRepositoryProvider), - ref.watch(albumApiRepositoryProvider), - ref.watch(albumRepositoryProvider), - ref.watch(assetRepositoryProvider), - ref.watch(exifRepositoryProvider), - ref.watch(partnerRepositoryProvider), - ref.watch(userRepositoryProvider), - ref.watch(userServiceProvider), - ref.watch(etagRepositoryProvider), - ref.watch(appSettingsServiceProvider), - ref.watch(localFilesManagerRepositoryProvider), - ref.watch(partnerApiRepositoryProvider), - ref.watch(userApiRepositoryProvider), - ), -); - -class SyncService { - final HashService _hashService; - final EntityService _entityService; - final AlbumMediaRepository _albumMediaRepository; - final AlbumApiRepository _albumApiRepository; - final AlbumRepository _albumRepository; - final AssetRepository _assetRepository; - final IsarExifRepository _exifInfoRepository; - final IsarUserRepository _isarUserRepository; - final UserService _userService; - final PartnerRepository _partnerRepository; - final ETagRepository _eTagRepository; - final PartnerApiRepository _partnerApiRepository; - final UserApiRepository _userApiRepository; - final AsyncMutex _lock = AsyncMutex(); - final Logger _log = Logger('SyncService'); - final AppSettingsService _appSettingsService; - final LocalFilesManagerRepository _localFilesManager; - - SyncService( - this._hashService, - this._entityService, - this._albumMediaRepository, - this._albumApiRepository, - this._albumRepository, - this._assetRepository, - this._exifInfoRepository, - this._partnerRepository, - this._isarUserRepository, - this._userService, - this._eTagRepository, - this._appSettingsService, - this._localFilesManager, - this._partnerApiRepository, - this._userApiRepository, - ); - - // public methods: - - /// Syncs users from the server to the local database - /// Returns `true`if there were any changes - Future syncUsersFromServer(List users) => _lock.run(() => _syncUsersFromServer(users)); - - /// Syncs remote assets owned by the logged-in user to the DB - /// Returns `true` if there were any changes - Future syncRemoteAssetsToDb({ - required List users, - required Future<(List? toUpsert, List? toDelete)> Function(List users, DateTime since) - getChangedAssets, - required FutureOr?> Function(UserDto user, DateTime until) loadAssets, - }) => _lock.run( - () async => - await _syncRemoteAssetChanges(users, getChangedAssets) ?? - await _syncRemoteAssetsFull(getUsersFromServer, loadAssets), - ); - - /// Syncs remote albums to the database - /// returns `true` if there were any changes - Future syncRemoteAlbumsToDb(List remote) => _lock.run(() => _syncRemoteAlbumsToDb(remote)); - - /// Syncs all device albums and their assets to the database - /// Returns `true` if there were any changes - Future syncLocalAlbumAssetsToDb(List onDevice, [Set? excludedAssets]) => - _lock.run(() => _syncLocalAlbumAssetsToDb(onDevice, excludedAssets)); - - /// returns all Asset IDs that are not contained in the existing list - List sharedAssetsToRemove(List deleteCandidates, List existing) { - if (deleteCandidates.isEmpty) { - return []; - } - deleteCandidates.sort(Asset.compareById); - existing.sort(Asset.compareById); - return _diffAssets(existing, deleteCandidates, compare: Asset.compareById).$3.map((e) => e.id).toList(); - } - - /// Syncs a new asset to the db. Returns `true` if successful - Future syncNewAssetToDb(Asset newAsset) => _lock.run(() => _syncNewAssetToDb(newAsset)); - - Future removeAllLocalAlbumsAndAssets() => _lock.run(_removeAllLocalAlbumsAndAssets); - - // private methods: - - /// Syncs users from the server to the local database - /// Returns `true`if there were any changes - Future _syncUsersFromServer(List users) async { - users.sortBy((u) => u.id); - final dbUsers = await _isarUserRepository.getAll(sortBy: SortUserBy.id); - final List toDelete = []; - final List toUpsert = []; - final changes = diffSortedListsSync( - users, - dbUsers, - compare: (UserDto a, UserDto b) => a.id.compareTo(b.id), - both: (UserDto a, UserDto b) { - if ((a.updatedAt == null && b.updatedAt != null) || - (a.updatedAt != null && b.updatedAt == null) || - (a.updatedAt != null && b.updatedAt != null && !a.updatedAt!.isAtSameMomentAs(b.updatedAt!)) || - a.isPartnerSharedBy != b.isPartnerSharedBy || - a.isPartnerSharedWith != b.isPartnerSharedWith || - a.inTimeline != b.inTimeline) { - toUpsert.add(a); - return true; - } - return false; - }, - onlyFirst: (UserDto a) => toUpsert.add(a), - onlySecond: (UserDto b) => toDelete.add(b.id), - ); - if (changes) { - await _isarUserRepository.transaction(() async { - await _isarUserRepository.delete(toDelete); - await _isarUserRepository.updateAll(toUpsert); - }); - } - return changes; - } - - /// Syncs a new asset to the db. Returns `true` if successful - Future _syncNewAssetToDb(Asset a) async { - final Asset? inDb = await _assetRepository.getByOwnerIdChecksum(a.ownerId, a.checksum); - if (inDb != null) { - // unify local/remote assets by replacing the - // local-only asset in the DB with a local&remote asset - a = inDb.updatedCopy(a); - } - try { - await _assetRepository.update(a); - } catch (e) { - _log.severe("Failed to put new asset into db", e); - return false; - } - return true; - } - - /// Efficiently syncs assets via changes. Returns `null` when a full sync is required. - Future _syncRemoteAssetChanges( - List users, - Future<(List? toUpsert, List? toDelete)> Function(List users, DateTime since) - getChangedAssets, - ) async { - final currentUser = _userService.getMyUser(); - final DateTime? since = (await _eTagRepository.get(currentUser.id))?.time?.toUtc(); - if (since == null) return null; - final DateTime now = DateTime.now(); - final (toUpsert, toDelete) = await getChangedAssets(users, since); - if (toUpsert == null || toDelete == null) { - await _clearUserAssetsETag(users); - return null; - } - try { - if (toDelete.isNotEmpty) { - await handleRemoteAssetRemoval(toDelete); - } - if (toUpsert.isNotEmpty) { - final (_, updated) = await _linkWithExistingFromDb(toUpsert); - await upsertAssetsWithExif(updated); - } - if (toUpsert.isNotEmpty || toDelete.isNotEmpty) { - await _updateUserAssetsETag(users, now); - return true; - } - return false; - } catch (e) { - _log.severe("Failed to sync remote assets to db", e); - } - return null; - } - - Future _moveToTrashMatchedAssets(Iterable idsToDelete) async { - final List localAssets = await _assetRepository.getAllLocal(); - final List matchedAssets = localAssets.where((asset) => idsToDelete.contains(asset.remoteId)).toList(); - - final mediaUrls = await Future.wait(matchedAssets.map((asset) => asset.local?.getMediaUrl() ?? Future.value(null))); - - await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList()); - } - - /// Deletes remote-only assets, updates merged assets to be local-only - Future handleRemoteAssetRemoval(List idsToDelete) async { - return _assetRepository.transaction(() async { - await _assetRepository.deleteAllByRemoteId(idsToDelete, state: AssetState.remote); - final merged = await _assetRepository.getAllByRemoteId(idsToDelete, state: AssetState.merged); - if (Platform.isAndroid && _appSettingsService.getSetting(AppSettingsEnum.manageLocalMediaAndroid)) { - await _moveToTrashMatchedAssets(idsToDelete); - } - if (merged.isEmpty) return; - for (final Asset asset in merged) { - asset.remoteId = null; - asset.isTrashed = false; - } - await _assetRepository.updateAll(merged); - }); - } - - Future> _getAllAccessibleUsers() async { - final sharedWith = (await _partnerRepository.getSharedWith()).toSet(); - sharedWith.add(_userService.getMyUser()); - return sharedWith.toList(); - } - - /// Syncs assets by loading and comparing all assets from the server. - Future _syncRemoteAssetsFull( - FutureOr?> Function() refreshUsers, - FutureOr?> Function(UserDto user, DateTime until) loadAssets, - ) async { - final serverUsers = await refreshUsers(); - if (serverUsers == null) { - _log.warning("_syncRemoteAssetsFull aborted because user refresh failed"); - return false; - } - await _syncUsersFromServer(serverUsers); - final List users = await _getAllAccessibleUsers(); - bool changes = false; - for (UserDto u in users) { - changes |= await _syncRemoteAssetsForUser(u, loadAssets); - } - return changes; - } - - Future _syncRemoteAssetsForUser( - UserDto user, - FutureOr?> Function(UserDto user, DateTime until) loadAssets, - ) async { - final DateTime now = DateTime.now().toUtc(); - final List? remote = await loadAssets(user, now); - if (remote == null) { - return false; - } - final List inDb = await _assetRepository.getAll(ownerId: user.id, sortBy: AssetSort.checksum); - assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!"); - - remote.sort(Asset.compareByChecksum); - - // filter our duplicates that might be introduced by the chunked retrieval - remote.uniqueConsecutive(compare: Asset.compareByChecksum); - - final (toAdd, toUpdate, toRemove) = _diffAssets(remote, inDb, remote: true); - if (toAdd.isEmpty && toUpdate.isEmpty && toRemove.isEmpty) { - await _updateUserAssetsETag([user], now); - return false; - } - final idsToDelete = toRemove.map((e) => e.id).toList(); - try { - await _assetRepository.deleteByIds(idsToDelete); - await upsertAssetsWithExif(toAdd + toUpdate); - } catch (e) { - _log.severe("Failed to sync remote assets to db", e); - } - await _updateUserAssetsETag([user], now); - return true; - } - - Future _updateUserAssetsETag(List users, DateTime time) { - final etags = users.map((u) => ETag(id: u.id, time: time)).toList(); - return _eTagRepository.upsertAll(etags); - } - - Future _clearUserAssetsETag(List users) { - final ids = users.map((u) => u.id).toList(); - return _eTagRepository.deleteByIds(ids); - } - - /// Syncs remote albums to the database - /// returns `true` if there were any changes - Future _syncRemoteAlbumsToDb(List remoteAlbums) async { - remoteAlbums.sortBy((e) => e.remoteId!); - - final List dbAlbums = await _albumRepository.getAll(remote: true, sortBy: AlbumSort.remoteId); - - final List toDelete = []; - final List existing = []; - - final bool changes = await diffSortedLists( - remoteAlbums, - dbAlbums, - compare: (remoteAlbum, dbAlbum) => remoteAlbum.remoteId!.compareTo(dbAlbum.remoteId!), - both: (remoteAlbum, dbAlbum) => _syncRemoteAlbum(remoteAlbum, dbAlbum, toDelete, existing), - onlyFirst: (remoteAlbum) => _addAlbumFromServer(remoteAlbum, existing), - onlySecond: (dbAlbum) => _removeAlbumFromDb(dbAlbum, toDelete), - ); - - if (toDelete.isNotEmpty) { - final List idsToRemove = sharedAssetsToRemove(toDelete, existing); - if (idsToRemove.isNotEmpty) { - await _assetRepository.deleteByIds(idsToRemove); - } - } else { - assert(toDelete.isEmpty); - } - return changes; - } - - /// syncs albums from the server to the local database (does not support - /// syncing changes from local back to server) - /// accumulates - Future _syncRemoteAlbum(Album dto, Album album, List deleteCandidates, List existing) async { - if (!_hasRemoteAlbumChanged(dto, album)) { - return false; - } - // loadDetails (/api/album/:id) will not include lastModifiedAssetTimestamp, - // i.e. it will always be null. Save it here. - final originalDto = dto; - dto = await _albumApiRepository.get(dto.remoteId!); - - final assetsInDb = await _assetRepository.getByAlbum(album, sortBy: AssetSort.ownerIdChecksum); - assert(assetsInDb.isSorted(Asset.compareByOwnerChecksum), "inDb unsorted!"); - final List assetsOnRemote = dto.remoteAssets.toList(); - assetsOnRemote.sort(Asset.compareByOwnerChecksum); - final (toAdd, toUpdate, toUnlink) = _diffAssets(assetsOnRemote, assetsInDb, compare: Asset.compareByOwnerChecksum); - - // update shared users - final List sharedUsers = album.sharedUsers.map((u) => u.toDto()).toList(growable: false); - sharedUsers.sort((a, b) => a.id.compareTo(b.id)); - final List users = dto.remoteUsers.map((u) => u.toDto()).toList()..sort((a, b) => a.id.compareTo(b.id)); - final List userIdsToAdd = []; - final List usersToUnlink = []; - diffSortedListsSync( - users, - sharedUsers, - compare: (UserDto a, UserDto b) => a.id.compareTo(b.id), - both: (a, b) => false, - onlyFirst: (UserDto a) => userIdsToAdd.add(a.id), - onlySecond: (UserDto a) => usersToUnlink.add(a), - ); - - // for shared album: put missing album assets into local DB - final (existingInDb, updated) = await _linkWithExistingFromDb(toAdd); - await upsertAssetsWithExif(updated); - final assetsToLink = existingInDb + updated; - final usersToLink = await _isarUserRepository.getByUserIds(userIdsToAdd); - - album.name = dto.name; - album.description = dto.description; - album.shared = dto.shared; - album.createdAt = dto.createdAt; - album.modifiedAt = dto.modifiedAt; - album.startDate = dto.startDate; - album.endDate = dto.endDate; - album.lastModifiedAssetTimestamp = originalDto.lastModifiedAssetTimestamp; - album.shared = dto.shared; - album.activityEnabled = dto.activityEnabled; - album.sortOrder = dto.sortOrder; - - final remoteThumbnailAssetId = dto.remoteThumbnailAssetId; - if (remoteThumbnailAssetId != null && album.thumbnail.value?.remoteId != remoteThumbnailAssetId) { - album.thumbnail.value = await _assetRepository.getByRemoteId(remoteThumbnailAssetId); - } - - // write & commit all changes to DB - try { - await _assetRepository.transaction(() async { - await _assetRepository.updateAll(toUpdate); - await _albumRepository.addUsers(album, usersToLink.nonNulls.toList()); - await _albumRepository.removeUsers(album, usersToUnlink); - await _albumRepository.addAssets(album, assetsToLink); - await _albumRepository.removeAssets(album, toUnlink); - await _albumRepository.recalculateMetadata(album); - await _albumRepository.update(album); - }); - _log.info("Synced changes of remote album ${album.name} to DB"); - } catch (e) { - _log.severe("Failed to sync remote album to database", e); - } - - if (album.shared || dto.shared) { - final userId = (_userService.getMyUser()).id; - final foreign = await _assetRepository.getByAlbum(album, notOwnedBy: [userId]); - existing.addAll(foreign); - - // delete assets in DB unless they belong to this user or part of some other shared album - final isarUserId = fastHash(userId); - deleteCandidates.addAll(toUnlink.where((a) => a.ownerId != isarUserId)); - } - - return true; - } - - /// Adds a remote album to the database while making sure to add any foreign - /// (shared) assets to the database beforehand - /// accumulates assets already existing in the database - Future _addAlbumFromServer(Album album, List existing) async { - if (album.remoteAssetCount != album.remoteAssets.length) { - album = await _albumApiRepository.get(album.remoteId!); - } - if (album.remoteAssetCount == album.remoteAssets.length) { - // in case an album contains assets not yet present in local DB: - // put missing album assets into local DB - final (existingInDb, updated) = await _linkWithExistingFromDb(album.remoteAssets.toList()); - existing.addAll(existingInDb); - await upsertAssetsWithExif(updated); - - await _entityService.fillAlbumWithDatabaseEntities(album); - await _albumRepository.create(album); - } else { - _log.warning( - "Failed to add album from server: assetCount ${album.remoteAssetCount} != " - "asset array length ${album.remoteAssets.length} for album ${album.name}", - ); - } - } - - /// Accumulates all suitable album assets to the `deleteCandidates` and - /// removes the album from the database. - Future _removeAlbumFromDb(Album album, List deleteCandidates) async { - if (album.isLocal) { - _log.info("Removing local album $album from DB"); - // delete assets in DB unless they are remote or part of some other album - deleteCandidates.addAll(await _assetRepository.getByAlbum(album, state: AssetState.local)); - } else if (album.shared) { - // delete assets in DB unless they belong to this user or are part of some other shared album or belong to a partner - final userIds = (await _getAllAccessibleUsers()).map((user) => user.id); - final orphanedAssets = await _assetRepository.getByAlbum(album, notOwnedBy: userIds); - deleteCandidates.addAll(orphanedAssets); - } - try { - await _albumRepository.delete(album.id); - _log.info("Removed local album $album from DB"); - } catch (e) { - _log.severe("Failed to remove local album $album from DB", e); - } - } - - /// Syncs all device albums and their assets to the database - /// Returns `true` if there were any changes - Future _syncLocalAlbumAssetsToDb(List onDevice, [Set? excludedAssets]) async { - onDevice.sort((a, b) => a.localId!.compareTo(b.localId!)); - final inDb = await _albumRepository.getAll(remote: false, sortBy: AlbumSort.localId); - final List deleteCandidates = []; - final List existing = []; - final bool anyChanges = await diffSortedLists( - onDevice, - inDb, - compare: (Album a, Album b) => a.localId!.compareTo(b.localId!), - both: (Album a, Album b) => _syncAlbumInDbAndOnDevice(a, b, deleteCandidates, existing, excludedAssets), - onlyFirst: (Album a) => _addAlbumFromDevice(a, existing, excludedAssets), - onlySecond: (Album a) => _removeAlbumFromDb(a, deleteCandidates), - ); - _log.fine("Syncing all local albums almost done. Collected ${deleteCandidates.length} asset candidates to delete"); - final (toDelete, toUpdate) = _handleAssetRemoval(deleteCandidates, existing, remote: false); - _log.fine("${toDelete.length} assets to delete, ${toUpdate.length} to update"); - if (toDelete.isNotEmpty || toUpdate.isNotEmpty) { - await _assetRepository.transaction(() async { - await _assetRepository.deleteByIds(toDelete); - await _assetRepository.updateAll(toUpdate); - }); - _log.info("Removed ${toDelete.length} and updated ${toUpdate.length} local assets from DB"); - } - return anyChanges; - } - - /// Syncs the device album to the album in the database - /// returns `true` if there were any changes - /// Accumulates asset candidates to delete and those already existing in DB - Future _syncAlbumInDbAndOnDevice( - Album deviceAlbum, - Album dbAlbum, - List deleteCandidates, - List existing, [ - Set? excludedAssets, - bool forceRefresh = false, - ]) async { - _log.info("Syncing a local album to DB: ${deviceAlbum.name}"); - if (!forceRefresh && !await _hasAlbumChangeOnDevice(deviceAlbum, dbAlbum)) { - _log.info("Local album ${deviceAlbum.name} has not changed. Skipping sync."); - return false; - } - _log.info("Local album ${deviceAlbum.name} has changed. Syncing..."); - if (!forceRefresh && excludedAssets == null && await _syncDeviceAlbumFast(deviceAlbum, dbAlbum)) { - _log.info("Fast synced local album ${deviceAlbum.name} to DB"); - return true; - } - // general case, e.g. some assets have been deleted or there are excluded albums on iOS - final inDb = await _assetRepository.getByAlbum( - dbAlbum, - ownerId: (_userService.getMyUser()).id, - sortBy: AssetSort.checksum, - ); - - assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!"); - final int assetCountOnDevice = await _albumMediaRepository.getAssetCount(deviceAlbum.localId!); - final List onDevice = await _getHashedAssets(deviceAlbum, excludedAssets: excludedAssets); - _removeDuplicates(onDevice); - // _removeDuplicates sorts `onDevice` by checksum - final (toAdd, toUpdate, toDelete) = _diffAssets(onDevice, inDb); - if (toAdd.isEmpty && - toUpdate.isEmpty && - toDelete.isEmpty && - dbAlbum.name == deviceAlbum.name && - dbAlbum.description == deviceAlbum.description && - dbAlbum.modifiedAt.isAtSameMomentAs(deviceAlbum.modifiedAt)) { - // changes only affeted excluded albums - _log.info("Only excluded assets in local album ${deviceAlbum.name} changed. Stopping sync."); - if (assetCountOnDevice != (await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))?.assetCount) { - await _eTagRepository.upsertAll([ETag(id: deviceAlbum.eTagKeyAssetCount, assetCount: assetCountOnDevice)]); - } - return false; - } - _log.info( - "Syncing local album ${deviceAlbum.name}. ${toAdd.length} assets to add, ${toUpdate.length} to update, ${toDelete.length} to delete", - ); - final (existingInDb, updated) = await _linkWithExistingFromDb(toAdd); - _log.info( - "Linking assets to add with existing from db. ${existingInDb.length} existing, ${updated.length} to update", - ); - deleteCandidates.addAll(toDelete); - existing.addAll(existingInDb); - dbAlbum.name = deviceAlbum.name; - dbAlbum.description = deviceAlbum.description; - dbAlbum.modifiedAt = deviceAlbum.modifiedAt; - if (dbAlbum.thumbnail.value != null && toDelete.contains(dbAlbum.thumbnail.value)) { - dbAlbum.thumbnail.value = null; - } - try { - await _assetRepository.transaction(() async { - await _assetRepository.updateAll(updated + toUpdate); - await _albumRepository.addAssets(dbAlbum, existingInDb + updated); - await _albumRepository.removeAssets(dbAlbum, toDelete); - await _albumRepository.recalculateMetadata(dbAlbum); - await _albumRepository.update(dbAlbum); - await _eTagRepository.upsertAll([ETag(id: deviceAlbum.eTagKeyAssetCount, assetCount: assetCountOnDevice)]); - }); - _log.info("Synced changes of local album ${deviceAlbum.name} to DB"); - } catch (e) { - _log.severe("Failed to update synced album ${deviceAlbum.name} in DB", e); - } - - return true; - } - - /// fast path for common case: only new assets were added to device album - /// returns `true` if successful, else `false` - Future _syncDeviceAlbumFast(Album deviceAlbum, Album dbAlbum) async { - if (!deviceAlbum.modifiedAt.isAfter(dbAlbum.modifiedAt)) { - _log.info("Local album ${deviceAlbum.name} has not changed. Skipping sync."); - return false; - } - final int totalOnDevice = await _albumMediaRepository.getAssetCount(deviceAlbum.localId!); - final int lastKnownTotal = (await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))?.assetCount ?? 0; - if (totalOnDevice <= lastKnownTotal) { - _log.info("Local album ${deviceAlbum.name} totalOnDevice is less than lastKnownTotal. Skipping sync."); - return false; - } - final List newAssets = await _getHashedAssets( - deviceAlbum, - modifiedFrom: dbAlbum.modifiedAt.add(const Duration(seconds: 1)), - modifiedUntil: deviceAlbum.modifiedAt, - ); - - if (totalOnDevice != lastKnownTotal + newAssets.length) { - _log.info( - "Local album ${deviceAlbum.name} totalOnDevice is not equal to lastKnownTotal + newAssets.length. Skipping sync.", - ); - return false; - } - dbAlbum.modifiedAt = deviceAlbum.modifiedAt; - _removeDuplicates(newAssets); - final (existingInDb, updated) = await _linkWithExistingFromDb(newAssets); - try { - await _assetRepository.transaction(() async { - await _assetRepository.updateAll(updated); - await _albumRepository.addAssets(dbAlbum, existingInDb + updated); - await _albumRepository.recalculateMetadata(dbAlbum); - await _albumRepository.update(dbAlbum); - await _eTagRepository.upsertAll([ETag(id: deviceAlbum.eTagKeyAssetCount, assetCount: totalOnDevice)]); - }); - _log.info("Fast synced local album ${deviceAlbum.name} to DB"); - } catch (e) { - _log.severe("Failed to fast sync local album ${deviceAlbum.name} to DB", e); - return false; - } - - return true; - } - - /// Adds a new album from the device to the database and Accumulates all - /// assets already existing in the database to the list of `existing` assets - Future _addAlbumFromDevice(Album album, List existing, [Set? excludedAssets]) async { - _log.info("Adding a new local album to DB: ${album.name}"); - final assets = await _getHashedAssets(album, excludedAssets: excludedAssets); - _removeDuplicates(assets); - final (existingInDb, updated) = await _linkWithExistingFromDb(assets); - _log.info("${existingInDb.length} assets already existed in DB, to upsert ${updated.length}"); - await upsertAssetsWithExif(updated); - existing.addAll(existingInDb); - album.assets.addAll(existingInDb); - album.assets.addAll(updated); - final thumb = existingInDb.firstOrNull ?? updated.firstOrNull; - album.thumbnail.value = thumb; - try { - await _albumRepository.create(album); - final int assetCount = await _albumMediaRepository.getAssetCount(album.localId!); - await _eTagRepository.upsertAll([ETag(id: album.eTagKeyAssetCount, assetCount: assetCount)]); - _log.info("Added a new local album to DB: ${album.name}"); - } catch (e) { - _log.severe("Failed to add new local album ${album.name} to DB", e); - } - } - - /// Returns a tuple (existing, updated) - Future<(List existing, List updated)> _linkWithExistingFromDb(List assets) async { - if (assets.isEmpty) return ([].cast(), [].cast()); - - final List inDb = await _assetRepository.getAllByOwnerIdChecksum( - assets.map((a) => a.ownerId).toInt64List(), - assets.map((a) => a.checksum).toList(growable: false), - ); - assert(inDb.length == assets.length); - final List existing = [], toUpsert = []; - for (int i = 0; i < assets.length; i++) { - final Asset? b = inDb[i]; - if (b == null) { - toUpsert.add(assets[i]); - continue; - } - if (b.canUpdate(assets[i])) { - final updated = b.updatedCopy(assets[i]); - assert(updated.isInDb); - toUpsert.add(updated); - } else { - existing.add(b); - } - } - assert(existing.length + toUpsert.length == assets.length); - return (existing, toUpsert); - } - - Future _toggleTrashStatusForAssets(List assetsList) async { - final trashMediaUrls = []; - - for (final asset in assetsList) { - if (asset.isTrashed) { - final mediaUrl = await asset.local?.getMediaUrl(); - if (mediaUrl == null) { - _log.warning("Failed to get media URL for asset ${asset.name} while moving to trash"); - continue; - } - trashMediaUrls.add(mediaUrl); - } else { - await _localFilesManager.restoreFromTrash(asset.fileName, asset.type.index); - } - } - - if (trashMediaUrls.isNotEmpty) { - await _localFilesManager.moveToTrash(trashMediaUrls); - } - } - - /// Inserts or updates the assets in the database with their ExifInfo (if any) - Future upsertAssetsWithExif(List assets) async { - if (assets.isEmpty) return; - - if (Platform.isAndroid && _appSettingsService.getSetting(AppSettingsEnum.manageLocalMediaAndroid)) { - await _toggleTrashStatusForAssets(assets); - } - - try { - await _assetRepository.transaction(() async { - await _assetRepository.updateAll(assets); - for (final Asset added in assets) { - added.exifInfo = added.exifInfo?.copyWith(assetId: added.id); - } - final exifInfos = assets.map((e) => e.exifInfo).nonNulls.toList(); - await _exifInfoRepository.updateAll(exifInfos); - }); - _log.info("Upserted ${assets.length} assets into the DB"); - } catch (e) { - _log.severe("Failed to upsert ${assets.length} assets into the DB", e); - // give details on the errors - assets.sort(Asset.compareByOwnerChecksum); - final inDb = await _assetRepository.getAllByOwnerIdChecksum( - assets.map((e) => e.ownerId).toInt64List(), - assets.map((e) => e.checksum).toList(growable: false), - ); - for (int i = 0; i < assets.length; i++) { - final Asset a = assets[i]; - final Asset? b = inDb[i]; - if (b == null) { - if (!a.isInDb) { - _log.warning("Trying to update an asset that does not exist in DB:\n$a"); - } - } else if (a.id != b.id) { - _log.warning("Trying to insert another asset with the same checksum+owner. In DB:\n$b\nTo insert:\n$a"); - } - } - for (int i = 1; i < assets.length; i++) { - if (Asset.compareByOwnerChecksum(assets[i - 1], assets[i]) == 0) { - _log.warning("Trying to insert duplicate assets:\n${assets[i - 1]}\n${assets[i]}"); - } - } - } - } - - /// Returns all assets that were successfully hashed - Future> _getHashedAssets( - Album album, { - int start = 0, - int end = 0x7fffffffffffffff, - DateTime? modifiedFrom, - DateTime? modifiedUntil, - Set? excludedAssets, - }) async { - final entities = await _albumMediaRepository.getAssets( - album.localId!, - start: start, - end: end, - modifiedFrom: modifiedFrom, - modifiedUntil: modifiedUntil, - ); - final filtered = excludedAssets == null - ? entities - : entities.where((e) => !excludedAssets.contains(e.localId!)).toList(); - return _hashService.hashAssets(filtered); - } - - List _removeDuplicates(List assets) { - final int before = assets.length; - assets.sort(Asset.compareByOwnerChecksumCreatedModified); - assets.uniqueConsecutive(compare: Asset.compareByOwnerChecksum, onDuplicate: (a, b) => {}); - final int duplicates = before - assets.length; - if (duplicates > 0) { - _log.warning("Ignored $duplicates duplicate assets on device"); - } - return assets; - } - - /// returns `true` if the albums differ on the surface - Future _hasAlbumChangeOnDevice(Album deviceAlbum, Album dbAlbum) async { - return deviceAlbum.name != dbAlbum.name || - deviceAlbum.description != dbAlbum.description || - !deviceAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) || - await _albumMediaRepository.getAssetCount(deviceAlbum.localId!) != - (await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))?.assetCount; - } - - Future _removeAllLocalAlbumsAndAssets() async { - try { - final assets = await _assetRepository.getAllLocal(); - final (toDelete, toUpdate) = _handleAssetRemoval(assets, [], remote: false); - await _assetRepository.transaction(() async { - await _assetRepository.deleteByIds(toDelete); - await _assetRepository.updateAll(toUpdate); - await _albumRepository.deleteAllLocal(); - }); - return true; - } catch (e) { - _log.severe("Failed to remove all local albums and assets", e); - return false; - } - } - - Future?> getUsersFromServer() async { - List? users; - try { - users = await _userApiRepository.getAll(); - } catch (e) { - _log.warning("Failed to fetch users", e); - users = null; - } - final List sharedBy = await _partnerApiRepository.getAll(Direction.sharedByMe); - final List sharedWith = await _partnerApiRepository.getAll(Direction.sharedWithMe); - - if (users == null) { - _log.warning("Failed to refresh users"); - return null; - } - - users.sortBy((u) => u.id); - sharedBy.sortBy((u) => u.id); - sharedWith.sortBy((u) => u.id); - - final updatedSharedBy = []; - - diffSortedListsSync( - users, - sharedBy, - compare: (UserDto a, UserDto b) => a.id.compareTo(b.id), - both: (UserDto a, UserDto b) { - updatedSharedBy.add(a.copyWith(isPartnerSharedBy: true)); - return true; - }, - onlyFirst: (UserDto a) => updatedSharedBy.add(a), - onlySecond: (UserDto b) => updatedSharedBy.add(b), - ); - - final updatedSharedWith = []; - - diffSortedListsSync( - updatedSharedBy, - sharedWith, - compare: (UserDto a, UserDto b) => a.id.compareTo(b.id), - both: (UserDto a, UserDto b) { - updatedSharedWith.add(a.copyWith(inTimeline: b.inTimeline, isPartnerSharedWith: true)); - return true; - }, - onlyFirst: (UserDto a) => updatedSharedWith.add(a), - onlySecond: (UserDto b) => updatedSharedWith.add(b), - ); - - return updatedSharedWith; - } -} - -/// Returns a triple(toAdd, toUpdate, toRemove) -(List toAdd, List toUpdate, List toRemove) _diffAssets( - List assets, - List inDb, { - bool? remote, - int Function(Asset, Asset) compare = Asset.compareByChecksum, -}) { - // fast paths for trivial cases: reduces memory usage during initial sync etc. - if (assets.isEmpty && inDb.isEmpty) { - return const ([], [], []); - } else if (assets.isEmpty && remote == null) { - // remove all from database - return (const [], const [], inDb); - } else if (inDb.isEmpty) { - // add all assets - return (assets, const [], const []); - } - - final List toAdd = []; - final List toUpdate = []; - final List toRemove = []; - diffSortedListsSync( - inDb, - assets, - compare: compare, - both: (Asset a, Asset b) { - if (a.canUpdate(b)) { - toUpdate.add(a.updatedCopy(b)); - return true; - } - return false; - }, - onlyFirst: (Asset a) { - if (remote == true && a.isLocal) { - if (a.remoteId != null) { - a.remoteId = null; - toUpdate.add(a); - } - } else if (remote == false && a.isRemote) { - if (a.isLocal) { - a.localId = null; - toUpdate.add(a); - } - } else { - toRemove.add(a); - } - }, - onlySecond: (Asset b) => toAdd.add(b), - ); - return (toAdd, toUpdate, toRemove); -} - -/// returns a tuple (toDelete toUpdate) when assets are to be deleted -(List toDelete, List toUpdate) _handleAssetRemoval( - List deleteCandidates, - List existing, { - bool? remote, -}) { - if (deleteCandidates.isEmpty) { - return const ([], []); - } - deleteCandidates.sort(Asset.compareById); - deleteCandidates.uniqueConsecutive(compare: Asset.compareById); - existing.sort(Asset.compareById); - existing.uniqueConsecutive(compare: Asset.compareById); - final (tooAdd, toUpdate, toRemove) = _diffAssets( - existing, - deleteCandidates, - compare: Asset.compareById, - remote: remote, - ); - assert(tooAdd.isEmpty, "toAdd should be empty in _handleAssetRemoval"); - return (toRemove.map((e) => e.id).toList(), toUpdate); -} - -/// returns `true` if the albums differ on the surface -bool _hasRemoteAlbumChanged(Album remoteAlbum, Album dbAlbum) { - return remoteAlbum.remoteAssetCount != dbAlbum.assetCount || - remoteAlbum.name != dbAlbum.name || - remoteAlbum.description != dbAlbum.description || - remoteAlbum.remoteThumbnailAssetId != dbAlbum.thumbnail.value?.remoteId || - remoteAlbum.shared != dbAlbum.shared || - remoteAlbum.remoteUsers.length != dbAlbum.sharedUsers.length || - !remoteAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) || - !isAtSameMomentAs(remoteAlbum.startDate, dbAlbum.startDate) || - !isAtSameMomentAs(remoteAlbum.endDate, dbAlbum.endDate) || - !isAtSameMomentAs(remoteAlbum.lastModifiedAssetTimestamp, dbAlbum.lastModifiedAssetTimestamp); -} diff --git a/mobile/lib/services/timeline.service.dart b/mobile/lib/services/timeline.service.dart deleted file mode 100644 index eaff1027d8..0000000000 --- a/mobile/lib/services/timeline.service.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/timeline.repository.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; - -final timelineServiceProvider = Provider((ref) { - return TimelineService( - ref.watch(timelineRepositoryProvider), - ref.watch(appSettingsServiceProvider), - ref.watch(userServiceProvider), - ); -}); - -class TimelineService { - final TimelineRepository _timelineRepository; - final AppSettingsService _appSettingsService; - final UserService _userService; - - const TimelineService(this._timelineRepository, this._appSettingsService, this._userService); - - Future> getTimelineUserIds() async { - final me = _userService.getMyUser(); - return _timelineRepository.getTimelineUserIds(me.id); - } - - Stream> watchTimelineUserIds() async* { - final me = _userService.getMyUser(); - yield* _timelineRepository.watchTimelineUsers(me.id); - } - - Stream watchHomeTimeline(String userId) { - return _timelineRepository.watchHomeTimeline(userId, _getGroupByOption()); - } - - Stream watchMultiUsersTimeline(List userIds) { - return _timelineRepository.watchMultiUsersTimeline(userIds, _getGroupByOption()); - } - - Stream watchArchiveTimeline() async* { - final user = _userService.getMyUser(); - - yield* _timelineRepository.watchArchiveTimeline(user.id); - } - - Stream watchFavoriteTimeline() async* { - final user = _userService.getMyUser(); - - yield* _timelineRepository.watchFavoriteTimeline(user.id); - } - - Stream watchAlbumTimeline(Album album) async* { - yield* _timelineRepository.watchAlbumTimeline(album, _getGroupByOption()); - } - - Stream watchTrashTimeline() async* { - final user = _userService.getMyUser(); - - yield* _timelineRepository.watchTrashTimeline(user.id); - } - - Stream watchAllVideosTimeline() { - final user = _userService.getMyUser(); - - return _timelineRepository.watchAllVideosTimeline(user.id); - } - - Future getTimelineFromAssets(List assets, GroupAssetsBy? groupBy) { - GroupAssetsBy groupOption = GroupAssetsBy.none; - if (groupBy == null) { - groupOption = _getGroupByOption(); - } else { - groupOption = groupBy; - } - - return _timelineRepository.getTimelineFromAssets(assets, groupOption); - } - - Stream watchAssetSelectionTimeline() async* { - final user = _userService.getMyUser(); - - yield* _timelineRepository.watchAssetSelectionTimeline(user.id); - } - - GroupAssetsBy _getGroupByOption() { - return GroupAssetsBy.values[_appSettingsService.getSetting(AppSettingsEnum.groupAssetsBy)]; - } - - Stream watchLockedTimelineProvider() async* { - final user = _userService.getMyUser(); - - yield* _timelineRepository.watchLockedTimeline(user.id, _getGroupByOption()); - } -} diff --git a/mobile/lib/services/trash.service.dart b/mobile/lib/services/trash.service.dart deleted file mode 100644 index 2c51a68c59..0000000000 --- a/mobile/lib/services/trash.service.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:openapi/api.dart'; - -final trashServiceProvider = Provider((ref) { - return TrashService( - ref.watch(apiServiceProvider), - ref.watch(assetRepositoryProvider), - ref.watch(userServiceProvider), - ); -}); - -class TrashService { - final ApiService _apiService; - final AssetRepository _assetRepository; - final UserService _userService; - - const TrashService(this._apiService, this._assetRepository, this._userService); - - Future restoreAssets(Iterable assetList) async { - final remoteAssets = assetList.where((a) => a.isRemote); - await _apiService.trashApi.restoreAssets(BulkIdsDto(ids: remoteAssets.map((e) => e.remoteId!).toList())); - - final updatedAssets = remoteAssets.map((asset) { - asset.isTrashed = false; - return asset; - }).toList(); - - await _assetRepository.updateAll(updatedAssets); - } - - Future emptyTrash() async { - final user = _userService.getMyUser(); - - await _apiService.trashApi.emptyTrash(); - - final trashedAssets = await _assetRepository.getTrashAssets(user.id); - final ids = trashedAssets.map((e) => e.remoteId!).toList(); - - await _assetRepository.transaction(() async { - await _assetRepository.deleteAllByRemoteId(ids, state: AssetState.remote); - - final merged = await _assetRepository.getAllByRemoteId(ids, state: AssetState.merged); - if (merged.isEmpty) { - return; - } - - for (final Asset asset in merged) { - asset.remoteId = null; - asset.isTrashed = false; - } - - await _assetRepository.updateAll(merged); - }); - } - - Future restoreTrash() async { - final user = _userService.getMyUser(); - - await _apiService.trashApi.restoreTrash(); - - final trashedAssets = await _assetRepository.getTrashAssets(user.id); - final updatedAssets = trashedAssets.map((asset) { - asset.isTrashed = false; - return asset; - }).toList(); - - await _assetRepository.updateAll(updatedAssets); - } -} diff --git a/mobile/lib/utils/backup_progress.dart b/mobile/lib/utils/backup_progress.dart deleted file mode 100644 index 36050f5e20..0000000000 --- a/mobile/lib/utils/backup_progress.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'dart:async'; -import 'dart:developer'; - -import 'package:easy_localization/easy_localization.dart'; - -final NumberFormat numberFormat = NumberFormat("###0.##"); - -String formatAssetBackupProgress(int uploadedAssets, int assetsToUpload) { - final int percent = (uploadedAssets * 100) ~/ assetsToUpload; - return "$percent% ($uploadedAssets/$assetsToUpload)"; -} - -/// prints progress in useful (kilo/mega/giga)bytes -String humanReadableFileBytesProgress(int bytes, int bytesTotal) { - String unit = "KB"; - - if (bytesTotal >= 0x40000000) { - unit = "GB"; - bytes >>= 20; - bytesTotal >>= 20; - } else if (bytesTotal >= 0x100000) { - unit = "MB"; - bytes >>= 10; - bytesTotal >>= 10; - } else if (bytesTotal < 0x400) { - return "${(bytes).toStringAsFixed(2)} B / ${(bytesTotal).toStringAsFixed(2)} B"; - } - - return "${(bytes / 1024.0).toStringAsFixed(2)} $unit / ${(bytesTotal / 1024.0).toStringAsFixed(2)} $unit"; -} - -/// prints percentage and absolute progress in useful (kilo/mega/giga)bytes -String humanReadableBytesProgress(int bytes, int bytesTotal) { - String unit = "KB"; // Kilobyte - if (bytesTotal >= 0x40000000) { - unit = "GB"; // Gigabyte - bytes >>= 20; - bytesTotal >>= 20; - } else if (bytesTotal >= 0x100000) { - unit = "MB"; // Megabyte - bytes >>= 10; - bytesTotal >>= 10; - } else if (bytesTotal < 0x400) { - return "$bytes / $bytesTotal B"; - } - final int percent = (bytes * 100) ~/ bytesTotal; - final String done = numberFormat.format(bytes / 1024.0); - final String total = numberFormat.format(bytesTotal / 1024.0); - return "$percent% ($done/$total$unit)"; -} - -class ThrottleProgressUpdate { - ThrottleProgressUpdate(this._fun, Duration interval) : _interval = interval.inMicroseconds; - final void Function(String?, int, int) _fun; - final int _interval; - int _invokedAt = 0; - Timer? _timer; - - String? title; - int progress = 0; - int total = 0; - - void call({final String? title, final int progress = 0, final int total = 0}) { - final time = Timeline.now; - this.title = title ?? this.title; - this.progress = progress; - this.total = total; - if (time > _invokedAt + _interval) { - _timer?.cancel(); - _onTimeElapsed(); - } else { - _timer ??= Timer(Duration(microseconds: _interval), _onTimeElapsed); - } - } - - void _onTimeElapsed() { - _invokedAt = Timeline.now; - _fun(title, progress, total); - _timer = null; - // clear title to not send/overwrite it next time if unchanged - title = null; - } -} diff --git a/mobile/lib/utils/bootstrap.dart b/mobile/lib/utils/bootstrap.dart index d63a92ba37..e79b06f53b 100644 --- a/mobile/lib/utils/bootstrap.dart +++ b/mobile/lib/utils/bootstrap.dart @@ -1,30 +1,14 @@ -import 'dart:io'; - import 'package:background_downloader/background_downloader.dart'; -import 'package:flutter/foundation.dart'; import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/android_device_asset.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:isar/isar.dart'; -import 'package:path_provider/path_provider.dart'; +import 'package:photo_manager/photo_manager.dart'; void configureFileDownloaderNotifications() { FileDownloader().configureNotificationForGroup( @@ -57,48 +41,10 @@ void configureFileDownloaderNotifications() { } abstract final class Bootstrap { - static Future<(Isar isar, Drift drift, DriftLogger logDb)> initDB() async { + static Future<(Drift, DriftLogger)> initDomain({bool listenStoreUpdates = true, bool shouldBufferLogs = true}) async { final drift = Drift(); final logDb = DriftLogger(); - - Isar? isar = Isar.getInstance(); - - if (isar != null) { - return (isar, drift, logDb); - } - - final dir = await getApplicationDocumentsDirectory(); - isar = await Isar.open( - [ - StoreValueSchema, - AssetSchema, - AlbumSchema, - ExifInfoSchema, - UserSchema, - BackupAlbumSchema, - DuplicatedAssetSchema, - ETagSchema, - if (Platform.isAndroid) AndroidDeviceAssetSchema, - if (Platform.isIOS) IOSDeviceAssetSchema, - DeviceAssetEntitySchema, - ], - directory: dir.path, - maxSizeMiB: 2048, - inspector: kDebugMode, - ); - - return (isar, drift, logDb); - } - - static Future initDomain( - Isar db, - Drift drift, - DriftLogger logDb, { - bool listenStoreUpdates = true, - bool shouldBufferLogs = true, - }) async { - final isBeta = await IsarStoreRepository(db).tryGet(StoreKey.betaTimeline) ?? true; - final IStoreRepository storeRepo = isBeta ? DriftStoreRepository(drift) : IsarStoreRepository(db); + final DriftStoreRepository storeRepo = DriftStoreRepository(drift); await StoreService.init(storeRepository: storeRepo, listenUpdates: listenStoreUpdates); @@ -109,5 +55,8 @@ abstract final class Bootstrap { ); await NetworkRepository.init(); + // Remove once all asset operations are migrated to Native APIs + await PhotoManager.setIgnorePermissionCheck(true); + return (drift, logDb); } } diff --git a/mobile/lib/utils/color_filter_generator.dart b/mobile/lib/utils/color_filter_generator.dart deleted file mode 100644 index 92aed4b1a0..0000000000 --- a/mobile/lib/utils/color_filter_generator.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:flutter/widgets.dart'; - -class InvertionFilter extends StatelessWidget { - final Widget? child; - const InvertionFilter({super.key, this.child}); - - @override - Widget build(BuildContext context) { - return ColorFiltered( - colorFilter: const ColorFilter.matrix([ - -1, 0, 0, 0, 255, // - 0, -1, 0, 0, 255, // - 0, 0, -1, 0, 255, // - 0, 0, 0, 1, 0, // - ]), - child: child, - ); - } -} - -// -1 - darkest, 1 - brightest, 0 - unchanged -class BrightnessFilter extends StatelessWidget { - final Widget? child; - final double brightness; - const BrightnessFilter({super.key, this.child, this.brightness = 0}); - - @override - Widget build(BuildContext context) { - return ColorFiltered( - colorFilter: ColorFilter.matrix(_ColorFilterGenerator.brightnessAdjustMatrix(brightness)), - child: child, - ); - } -} - -// -1 - greyscale, 1 - most saturated, 0 - unchanged -class SaturationFilter extends StatelessWidget { - final Widget? child; - final double saturation; - const SaturationFilter({super.key, this.child, this.saturation = 0}); - - @override - Widget build(BuildContext context) { - return ColorFiltered( - colorFilter: ColorFilter.matrix(_ColorFilterGenerator.saturationAdjustMatrix(saturation)), - child: child, - ); - } -} - -class _ColorFilterGenerator { - static List brightnessAdjustMatrix(double value) { - value = value * 10; - - if (value == 0) { - return [ - 1, 0, 0, 0, 0, // - 0, 1, 0, 0, 0, // - 0, 0, 1, 0, 0, // - 0, 0, 0, 1, 0, // - ]; - } - - return List.from([ - 1, 0, 0, 0, value, 0, 1, 0, 0, value, 0, 0, 1, 0, value, 0, 0, 0, 1, 0, // - ]).map((i) => i.toDouble()).toList(); - } - - static List saturationAdjustMatrix(double value) { - value = value * 100; - - if (value == 0) { - return [ - 1, 0, 0, 0, 0, // - 0, 1, 0, 0, 0, // - 0, 0, 1, 0, 0, // - 0, 0, 0, 1, 0, // - ]; - } - - double x = ((1 + ((value > 0) ? ((3 * value) / 100) : (value / 100)))).toDouble(); - double lumR = 0.3086; - double lumG = 0.6094; - double lumB = 0.082; - - return List.from([ - (lumR * (1 - x)) + x, lumG * (1 - x), lumB * (1 - x), // - 0, 0, // - lumR * (1 - x), // - (lumG * (1 - x)) + x, // - lumB * (1 - x), // - 0, 0, // - lumR * (1 - x), // - lumG * (1 - x), // - (lumB * (1 - x)) + x, // - 0, 0, 0, 0, 0, 1, 0, // - ]).map((i) => i.toDouble()).toList(); - } -} diff --git a/mobile/lib/utils/datetime_comparison.dart b/mobile/lib/utils/datetime_comparison.dart deleted file mode 100644 index f8ddcfea11..0000000000 --- a/mobile/lib/utils/datetime_comparison.dart +++ /dev/null @@ -1,2 +0,0 @@ -bool isAtSameMomentAs(DateTime? a, DateTime? b) => - (a == null && b == null) || ((a != null && b != null) && a.isAtSameMomentAs(b)); diff --git a/mobile/lib/utils/editor.utils.dart b/mobile/lib/utils/editor.utils.dart new file mode 100644 index 0000000000..fa2dedf383 --- /dev/null +++ b/mobile/lib/utils/editor.utils.dart @@ -0,0 +1,65 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart'; +import 'package:immich_mobile/utils/matrix.utils.dart'; +import 'package:openapi/api.dart' hide AssetEditAction; + +Rect convertCropParametersToRect(CropParameters parameters, int originalWidth, int originalHeight) { + return Rect.fromLTWH( + parameters.x.toDouble() / originalWidth, + parameters.y.toDouble() / originalHeight, + parameters.width.toDouble() / originalWidth, + parameters.height.toDouble() / originalHeight, + ); +} + +CropParameters convertRectToCropParameters(Rect rect, int originalWidth, int originalHeight) { + final x = (rect.left * originalWidth).truncate(); + final y = (rect.top * originalHeight).truncate(); + final width = (rect.width * originalWidth).truncate(); + final height = (rect.height * originalHeight).truncate(); + + return CropParameters( + x: max(x, 0).clamp(0, originalWidth), + y: max(y, 0).clamp(0, originalHeight), + width: max(width, 0).clamp(0, originalWidth - x), + height: max(height, 0).clamp(0, originalHeight - y), + ); +} + +AffineMatrix buildAffineFromEdits(List edits) { + return AffineMatrix.compose( + edits.map((edit) { + return switch (edit) { + RotateEdit(:final parameters) => AffineMatrix.rotate(parameters.angle * pi / 180), + MirrorEdit(:final parameters) => + parameters.axis == MirrorAxis.horizontal ? AffineMatrix.flipY() : AffineMatrix.flipX(), + CropEdit() => AffineMatrix.identity(), + }; + }).toList(), + ); +} + +bool isCloseToZero(double value, [double epsilon = 1e-15]) { + return value.abs() < epsilon; +} + +typedef NormalizedTransform = ({double rotation, bool mirrorHorizontal, bool mirrorVertical}); + +NormalizedTransform normalizeTransformEdits(List edits) { + final matrix = buildAffineFromEdits(edits); + + double a = matrix.a; + double b = matrix.b; + double c = matrix.c; + double d = matrix.d; + + final rotation = ((isCloseToZero(a) ? asin(c) : acos(a)) * 180) / pi; + + return ( + rotation: rotation < 0 ? 360 + rotation : rotation, + mirrorHorizontal: false, + mirrorVertical: isCloseToZero(a) ? b == c : a == -d, + ); +} diff --git a/mobile/lib/utils/hooks/blurhash_hook.dart b/mobile/lib/utils/hooks/blurhash_hook.dart index ac5fd31724..534c0ad8fb 100644 --- a/mobile/lib/utils/hooks/blurhash_hook.dart +++ b/mobile/lib/utils/hooks/blurhash_hook.dart @@ -1,20 +1,10 @@ import 'dart:convert'; import 'dart:typed_data'; + import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:thumbhash/thumbhash.dart' as thumbhash; -ObjectRef useBlurHashRef(Asset? asset) { - if (asset?.thumbhash == null) { - return useRef(null); - } - - final rbga = thumbhash.thumbHashToRGBA(base64Decode(asset!.thumbhash!)); - - return useRef(thumbhash.rgbaToBmp(rbga)); -} - ObjectRef useDriftBlurHashRef(RemoteAsset? asset) { if (asset?.thumbHash == null) { return useRef(null); diff --git a/mobile/lib/utils/image_url_builder.dart b/mobile/lib/utils/image_url_builder.dart index 079f0e51fa..c562049b1d 100644 --- a/mobile/lib/utils/image_url_builder.dart +++ b/mobile/lib/utils/image_url_builder.dart @@ -1,47 +1,7 @@ import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:openapi/api.dart'; -String getThumbnailUrl(final Asset asset, {AssetMediaSize type = AssetMediaSize.thumbnail}) { - return getThumbnailUrlForRemoteId(asset.remoteId!, type: type); -} - -String getThumbnailCacheKey(final Asset asset, {AssetMediaSize type = AssetMediaSize.thumbnail}) { - return getThumbnailCacheKeyForRemoteId(asset.remoteId!, asset.thumbhash!, type: type); -} - -String getThumbnailCacheKeyForRemoteId( - final String id, - final String thumbhash, { - AssetMediaSize type = AssetMediaSize.thumbnail, -}) { - if (type == AssetMediaSize.thumbnail) { - return 'thumbnail-image-$id-$thumbhash'; - } else { - return '${id}_${thumbhash}_previewStage'; - } -} - -String getAlbumThumbnailUrl(final Album album, {AssetMediaSize type = AssetMediaSize.thumbnail}) { - if (album.thumbnail.value?.remoteId == null) { - return ''; - } - return getThumbnailUrlForRemoteId(album.thumbnail.value!.remoteId!, type: type); -} - -String getAlbumThumbNailCacheKey(final Album album, {AssetMediaSize type = AssetMediaSize.thumbnail}) { - if (album.thumbnail.value?.remoteId == null) { - return ''; - } - return getThumbnailCacheKeyForRemoteId( - album.thumbnail.value!.remoteId!, - album.thumbnail.value!.thumbhash!, - type: type, - ); -} - String getOriginalUrlForRemoteId(final String id, {bool edited = true}) { return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/original?edited=$edited'; } diff --git a/mobile/lib/utils/immich_loading_overlay.dart b/mobile/lib/utils/immich_loading_overlay.dart deleted file mode 100644 index be49c3bae9..0000000000 --- a/mobile/lib/utils/immich_loading_overlay.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart'; - -final _loadingEntry = OverlayEntry( - builder: (context) => SizedBox.square( - dimension: double.infinity, - child: DecoratedBox( - decoration: BoxDecoration(color: context.colorScheme.surface.withAlpha(200)), - child: const Center( - child: DelayedLoadingIndicator(delay: Duration(seconds: 1), fadeInDuration: Duration(milliseconds: 400)), - ), - ), - ), -); - -ValueNotifier useProcessingOverlay() { - return use(const _LoadingOverlay()); -} - -class _LoadingOverlay extends Hook> { - const _LoadingOverlay(); - - @override - _LoadingOverlayState createState() => _LoadingOverlayState(); -} - -class _LoadingOverlayState extends HookState, _LoadingOverlay> { - late final _isLoading = ValueNotifier(false)..addListener(_listener); - OverlayEntry? _loadingOverlay; - - void _listener() { - setState(() { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (_isLoading.value) { - _loadingOverlay?.remove(); - _loadingOverlay = _loadingEntry; - Overlay.of(context).insert(_loadingEntry); - } else { - _loadingOverlay?.remove(); - _loadingOverlay = null; - } - }); - }); - } - - @override - ValueNotifier build(BuildContext context) { - return _isLoading; - } - - @override - void dispose() { - _isLoading.dispose(); - super.dispose(); - } - - @override - Object? get debugValue => _isLoading.value; - - @override - String get debugLabel => 'useProcessingOverlay<>'; -} diff --git a/mobile/lib/utils/isolate.dart b/mobile/lib/utils/isolate.dart index c8224b9c55..20b56d4875 100644 --- a/mobile/lib/utils/isolate.dart +++ b/mobile/lib/utils/isolate.dart @@ -5,7 +5,6 @@ import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; @@ -38,13 +37,9 @@ Cancelable runInIsolateGentle({ BackgroundIsolateBinaryMessenger.ensureInitialized(token); DartPluginRegistrant.ensureInitialized(); - final (isar, drift, logDb) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDb, shouldBufferLogs: false, listenStoreUpdates: false); + final (drift, logDb) = await Bootstrap.initDomain(shouldBufferLogs: false, listenStoreUpdates: false); final ref = ProviderContainer( overrides: [ - // TODO: Remove once isar is removed - dbProvider.overrideWithValue(isar), - isarProvider.overrideWithValue(isar), cancellationProvider.overrideWithValue(cancelledChecker), driftProvider.overrideWith(driftOverride(drift)), ], @@ -66,15 +61,6 @@ Cancelable runInIsolateGentle({ await LogService.I.dispose(); await logDb.close(); await drift.close(); - - // Close Isar safely - try { - if (isar.isOpen) { - await isar.close(); - } - } catch (e) { - dPrint(() => "Error closing Isar: $e"); - } } catch (error, stack) { dPrint(() => "Error closing resources in isolate: $error, $stack"); } finally { diff --git a/mobile/lib/utils/matrix.utils.dart b/mobile/lib/utils/matrix.utils.dart new file mode 100644 index 0000000000..8363a8b93d --- /dev/null +++ b/mobile/lib/utils/matrix.utils.dart @@ -0,0 +1,50 @@ +import 'dart:math'; + +class AffineMatrix { + final double a; + final double b; + final double c; + final double d; + final double e; + final double f; + + const AffineMatrix(this.a, this.b, this.c, this.d, this.e, this.f); + + @override + String toString() { + return 'AffineMatrix(a: $a, b: $b, c: $c, d: $d, e: $e, f: $f)'; + } + + factory AffineMatrix.identity() { + return const AffineMatrix(1, 0, 0, 1, 0, 0); + } + + AffineMatrix multiply(AffineMatrix other) { + return AffineMatrix( + a * other.a + c * other.b, + b * other.a + d * other.b, + a * other.c + c * other.d, + b * other.c + d * other.d, + a * other.e + c * other.f + e, + b * other.e + d * other.f + f, + ); + } + + factory AffineMatrix.compose([List transformations = const []]) { + return transformations.fold(AffineMatrix.identity(), (acc, matrix) => acc.multiply(matrix)); + } + + factory AffineMatrix.rotate(double angle) { + final cosAngle = cos(angle); + final sinAngle = sin(angle); + return AffineMatrix(cosAngle, -sinAngle, sinAngle, cosAngle, 0, 0); + } + + factory AffineMatrix.flipY() { + return const AffineMatrix(-1, 0, 0, 1, 0, 0); + } + + factory AffineMatrix.flipX() { + return const AffineMatrix(1, 0, 0, -1, 0, 0); + } +} diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index 76916cee1e..9ac805af39 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -1,115 +1,14 @@ import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'package:collection/collection.dart'; -import 'package:drift/drift.dart'; -import 'package:immich_mobile/domain/models/album/local_album.model.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/android_device_asset.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart' as isar_backup_album; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; -import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; -import 'package:immich_mobile/platform/network_api.g.dart'; -import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/utils/datetime_helpers.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; -import 'package:immich_mobile/utils/diff.dart'; -import 'package:isar/isar.dart'; -// ignore: import_rule_photo_manager -import 'package:photo_manager/photo_manager.dart'; const int targetVersion = 25; -Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { - final hasVersion = Store.tryGet(StoreKey.version) != null; +Future migrateDatabaseIfNeeded() async { final int version = Store.get(StoreKey.version, targetVersion); - if (version < 9) { - await Store.put(StoreKey.version, targetVersion); - final value = await db.storeValues.get(StoreKey.currentUser.id); - if (value != null) { - final id = value.intValue; - if (id != null) { - await db.writeTxn(() async { - final user = await db.users.get(id); - await db.storeValues.put(StoreValue(StoreKey.currentUser.id, strValue: user?.id)); - }); - } - } - } - - if (version < 10) { - await Store.put(StoreKey.version, targetVersion); - await _migrateDeviceAsset(db); - } - - if (version < 13) { - await Store.put(StoreKey.photoManagerCustomFilter, true); - } - - // This means that the SQLite DB is just created and has no version - if (version < 14 || !hasVersion) { - await migrateStoreToSqlite(db, drift); - await Store.populateCache(); - } - - final syncStreamRepository = SyncStreamRepository(drift); - await handleBetaMigration(version, await _isNewInstallation(db, drift), syncStreamRepository); - - if (version < 17 && Store.isBetaTimelineEnabled) { - final delay = Store.get(StoreKey.backupTriggerDelay, AppSettingsEnum.backupTriggerDelay.defaultValue); - if (delay >= 1000) { - await Store.put(StoreKey.backupTriggerDelay, (delay / 1000).toInt()); - } - } - - if (version < 18 && Store.isBetaTimelineEnabled) { - await syncStreamRepository.reset(); - await Store.put(StoreKey.shouldResetSync, true); - } - - if (version < 19 && Store.isBetaTimelineEnabled) { - if (!await _populateLocalAssetTime(drift)) { - return; - } - } - - if (version < 20 && Store.isBetaTimelineEnabled) { - await _syncLocalAlbumIsIosSharedAlbum(drift); - } - - if (version < 21) { - final certData = SSLClientCertStoreVal.load(); - if (certData != null) { - await networkApi.addCertificate(ClientCertData(data: certData.data, password: certData.password ?? "")); - } - } - - if (version < 23 && Store.isBetaTimelineEnabled) { - await _populateLocalAssetPlaybackStyle(drift); - } - - if (version < 24 && Store.isBetaTimelineEnabled) { - await _applyLocalAssetOrientation(drift); - } if (version < 25) { final accessToken = Store.tryGet(StoreKey.accessToken); @@ -121,365 +20,6 @@ Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { } } - if (version < 22 && !Store.isBetaTimelineEnabled) { - await Store.put(StoreKey.needBetaMigration, true); - } - - if (targetVersion >= 12) { - await Store.put(StoreKey.version, targetVersion); - return; - } - - final shouldTruncate = version < 8 || version < targetVersion; - - if (shouldTruncate) { - await _migrateTo(db, targetVersion); - } -} - -Future handleBetaMigration(int version, bool isNewInstallation, SyncStreamRepository syncStreamRepository) async { - // Handle migration only for this version - // TODO: remove when old timeline is removed - final isBeta = Store.tryGet(StoreKey.betaTimeline); - final needBetaMigration = Store.tryGet(StoreKey.needBetaMigration); - if (version <= 15 && needBetaMigration == null) { - // For new installations, no migration needed - // For existing installations, only migrate if beta timeline is not enabled (null or false) - if (isNewInstallation || isBeta == true) { - await Store.put(StoreKey.needBetaMigration, false); - await Store.put(StoreKey.betaTimeline, true); - } else { - await Store.put(StoreKey.needBetaMigration, true); - } - } - - if (version > 15) { - if (isBeta == null || isBeta) { - await Store.put(StoreKey.needBetaMigration, false); - await Store.put(StoreKey.betaTimeline, true); - } else { - await Store.put(StoreKey.needBetaMigration, false); - } - } - - if (version < 16) { - await syncStreamRepository.reset(); - await Store.put(StoreKey.shouldResetSync, true); - } -} - -Future _isNewInstallation(Isar db, Drift drift) async { - try { - final isarUserCount = await db.users.count(); - if (isarUserCount > 0) { - return false; - } - - final isarAssetCount = await db.assets.count(); - if (isarAssetCount > 0) { - return false; - } - - final driftStoreCount = await drift.storeEntity.select().get().then((list) => list.length); - if (driftStoreCount > 0) { - return false; - } - - final driftAssetCount = await drift.localAssetEntity.select().get().then((list) => list.length); - if (driftAssetCount > 0) { - return false; - } - - return true; - } catch (error) { - dPrint(() => "[MIGRATION] Error checking if new installation: $error"); - return false; - } -} - -Future _migrateTo(Isar db, int version) async { - await Store.delete(StoreKey.assetETag); - await db.writeTxn(() async { - await db.assets.clear(); - await db.exifInfos.clear(); - await db.albums.clear(); - await db.eTags.clear(); - await db.users.clear(); - }); - await Store.put(StoreKey.version, version); -} - -Future _migrateDeviceAsset(Isar db) async { - final ids = Platform.isAndroid - ? (await db.androidDeviceAssets.where().findAll()) - .map((a) => _DeviceAsset(assetId: a.id.toString(), hash: a.hash)) - .toList() - : (await db.iOSDeviceAssets.where().findAll()).map((i) => _DeviceAsset(assetId: i.id, hash: i.hash)).toList(); - - final PermissionState ps = await PhotoManager.requestPermissionExtend(); - if (!ps.hasAccess) { - dPrint(() => "[MIGRATION] Photo library permission not granted. Skipping device asset migration."); - return; - } - - List<_DeviceAsset> localAssets = []; - final List paths = await PhotoManager.getAssetPathList(onlyAll: true); - - if (paths.isEmpty) { - localAssets = (await db.assets.where().anyOf(ids, (query, id) => query.localIdEqualTo(id.assetId)).findAll()) - .map((a) => _DeviceAsset(assetId: a.localId!, dateTime: a.fileModifiedAt)) - .toList(); - } else { - final AssetPathEntity albumWithAll = paths.first; - final int assetCount = await albumWithAll.assetCountAsync; - - final List allDeviceAssets = await albumWithAll.getAssetListRange(start: 0, end: assetCount); - - localAssets = allDeviceAssets.map((a) => _DeviceAsset(assetId: a.id, dateTime: a.modifiedDateTime)).toList(); - } - - dPrint(() => "[MIGRATION] Device Asset Ids length - ${ids.length}"); - dPrint(() => "[MIGRATION] Local Asset Ids length - ${localAssets.length}"); - ids.sort((a, b) => a.assetId.compareTo(b.assetId)); - localAssets.sort((a, b) => a.assetId.compareTo(b.assetId)); - final List toAdd = []; - await diffSortedLists( - ids, - localAssets, - compare: (a, b) => a.assetId.compareTo(b.assetId), - both: (deviceAsset, asset) { - toAdd.add( - DeviceAssetEntity(assetId: deviceAsset.assetId, hash: deviceAsset.hash!, modifiedTime: asset.dateTime!), - ); - return false; - }, - onlyFirst: (deviceAsset) { - dPrint(() => '[MIGRATION] Local asset not found in DeviceAsset: ${deviceAsset.assetId}'); - }, - onlySecond: (asset) { - dPrint(() => '[MIGRATION] Local asset not found in DeviceAsset: ${asset.assetId}'); - }, - ); - - dPrint(() => "[MIGRATION] Total number of device assets migrated - ${toAdd.length}"); - - await db.writeTxn(() async { - await db.deviceAssetEntitys.putAll(toAdd); - }); -} - -Future _populateLocalAssetTime(Drift db) async { - try { - final nativeApi = NativeSyncApi(); - final albums = await nativeApi.getAlbums(); - for (final album in albums) { - final assets = await nativeApi.getAssetsForAlbum(album.id); - await db.batch((batch) async { - for (final asset in assets) { - batch.update( - db.localAssetEntity, - LocalAssetEntityCompanion( - longitude: Value(asset.longitude), - latitude: Value(asset.latitude), - adjustmentTime: Value(tryFromSecondsSinceEpoch(asset.adjustmentTime, isUtc: true)), - updatedAt: Value(tryFromSecondsSinceEpoch(asset.updatedAt, isUtc: true) ?? DateTime.timestamp()), - ), - where: (t) => t.id.equals(asset.id), - ); - } - }); - } - - return true; - } catch (error) { - dPrint(() => "[MIGRATION] Error while populating asset time: $error"); - return false; - } -} - -Future _syncLocalAlbumIsIosSharedAlbum(Drift db) async { - try { - final nativeApi = NativeSyncApi(); - final albums = await nativeApi.getAlbums(); - await db.batch((batch) { - for (final album in albums) { - batch.update( - db.localAlbumEntity, - LocalAlbumEntityCompanion(isIosSharedAlbum: Value(album.isCloud)), - where: (t) => t.id.equals(album.id), - ); - } - }); - dPrint(() => "[MIGRATION] Successfully updated isIosSharedAlbum for ${albums.length} albums"); - } catch (error) { - dPrint(() => "[MIGRATION] Error while syncing local album isIosSharedAlbum: $error"); - } -} - -Future migrateDeviceAssetToSqlite(Isar db, Drift drift) async { - try { - final isarDeviceAssets = await db.deviceAssetEntitys.where().findAll(); - await drift.batch((batch) { - for (final deviceAsset in isarDeviceAssets) { - batch.update( - drift.localAssetEntity, - LocalAssetEntityCompanion(checksum: Value(base64.encode(deviceAsset.hash))), - where: (t) => t.id.equals(deviceAsset.assetId), - ); - } - }); - } catch (error) { - dPrint(() => "[MIGRATION] Error while migrating device assets to SQLite: $error"); - } -} - -Future migrateBackupAlbumsToSqlite(Isar db, Drift drift) async { - try { - final isarBackupAlbums = await db.backupAlbums.where().findAll(); - // Recents is a virtual album on Android, and we don't have it with the new sync - // If recents is selected previously, select all albums during migration except the excluded ones - if (Platform.isAndroid) { - final recentAlbum = isarBackupAlbums.firstWhereOrNull((album) => album.id == 'isAll'); - if (recentAlbum != null) { - await drift.localAlbumEntity.update().write( - const LocalAlbumEntityCompanion(backupSelection: Value(BackupSelection.selected)), - ); - final excluded = isarBackupAlbums - .where((album) => album.selection == isar_backup_album.BackupSelection.exclude) - .map((album) => album.id) - .toList(); - await drift.batch((batch) async { - for (final id in excluded) { - batch.update( - drift.localAlbumEntity, - const LocalAlbumEntityCompanion(backupSelection: Value(BackupSelection.excluded)), - where: (t) => t.id.equals(id), - ); - } - }); - return; - } - } - - await drift.batch((batch) { - for (final album in isarBackupAlbums) { - batch.update( - drift.localAlbumEntity, - LocalAlbumEntityCompanion( - backupSelection: Value(switch (album.selection) { - isar_backup_album.BackupSelection.none => BackupSelection.none, - isar_backup_album.BackupSelection.select => BackupSelection.selected, - isar_backup_album.BackupSelection.exclude => BackupSelection.excluded, - }), - ), - where: (t) => t.id.equals(album.id), - ); - } - }); - } catch (error) { - dPrint(() => "[MIGRATION] Error while migrating backup albums to SQLite: $error"); - } -} - -Future migrateStoreToSqlite(Isar db, Drift drift) async { - try { - final isarStoreValues = await db.storeValues.where().findAll(); - await drift.batch((batch) { - for (final storeValue in isarStoreValues) { - final companion = StoreEntityCompanion( - id: Value(storeValue.id), - stringValue: Value(storeValue.strValue), - intValue: Value(storeValue.intValue), - ); - batch.insert(drift.storeEntity, companion, onConflict: DoUpdate((_) => companion)); - } - }); - } catch (error) { - dPrint(() => "[MIGRATION] Error while migrating store values to SQLite: $error"); - } -} - -Future migrateStoreToIsar(Isar db, Drift drift) async { - try { - final driftStoreValues = await drift.storeEntity - .select() - .map((entity) => StoreValue(entity.id, intValue: entity.intValue, strValue: entity.stringValue)) - .get(); - - await db.writeTxn(() async { - await db.storeValues.putAll(driftStoreValues); - }); - } catch (error) { - dPrint(() => "[MIGRATION] Error while migrating store values to Isar: $error"); - } -} - -Future _populateLocalAssetPlaybackStyle(Drift db) async { - try { - final nativeApi = NativeSyncApi(); - - final albums = await nativeApi.getAlbums(); - for (final album in albums) { - final assets = await nativeApi.getAssetsForAlbum(album.id); - await db.batch((batch) { - for (final asset in assets) { - batch.update( - db.localAssetEntity, - LocalAssetEntityCompanion(playbackStyle: Value(_toPlaybackStyle(asset.playbackStyle))), - where: (t) => t.id.equals(asset.id), - ); - } - }); - } - - if (Platform.isAndroid) { - final trashedAssetMap = await nativeApi.getTrashedAssets(); - for (final entry in trashedAssetMap.cast>().entries) { - final assets = entry.value.cast(); - await db.batch((batch) { - for (final asset in assets) { - batch.update( - db.trashedLocalAssetEntity, - TrashedLocalAssetEntityCompanion(playbackStyle: Value(_toPlaybackStyle(asset.playbackStyle))), - where: (t) => t.id.equals(asset.id), - ); - } - }); - } - dPrint(() => "[MIGRATION] Successfully populated playbackStyle for local and trashed assets"); - } else { - dPrint(() => "[MIGRATION] Successfully populated playbackStyle for local assets"); - } - } catch (error) { - dPrint(() => "[MIGRATION] Error while populating playbackStyle: $error"); - } -} - -Future _applyLocalAssetOrientation(Drift db) { - final query = db.localAssetEntity.update() - ..where((filter) => (filter.orientation.equals(90) | (filter.orientation.equals(270)))); - return query.write( - LocalAssetEntityCompanion.custom( - width: db.localAssetEntity.height, - height: db.localAssetEntity.width, - orientation: const Variable(0), - ), - ); -} - -AssetPlaybackStyle _toPlaybackStyle(PlatformAssetPlaybackStyle style) => switch (style) { - PlatformAssetPlaybackStyle.unknown => AssetPlaybackStyle.unknown, - PlatformAssetPlaybackStyle.image => AssetPlaybackStyle.image, - PlatformAssetPlaybackStyle.video => AssetPlaybackStyle.video, - PlatformAssetPlaybackStyle.imageAnimated => AssetPlaybackStyle.imageAnimated, - PlatformAssetPlaybackStyle.livePhoto => AssetPlaybackStyle.livePhoto, - PlatformAssetPlaybackStyle.videoLooping => AssetPlaybackStyle.videoLooping, -}; - -class _DeviceAsset { - final String assetId; - final List? hash; - final DateTime? dateTime; - - const _DeviceAsset({required this.assetId, this.hash, this.dateTime}); + await Store.put(StoreKey.version, targetVersion); + return; } diff --git a/mobile/lib/utils/openapi_patching.dart b/mobile/lib/utils/openapi_patching.dart index 090889ff32..38c805a42e 100644 --- a/mobile/lib/utils/openapi_patching.dart +++ b/mobile/lib/utils/openapi_patching.dart @@ -5,13 +5,13 @@ dynamic upgradeDto(dynamic value, String targetType) { case 'UserPreferencesResponseDto': if (value is Map) { addDefault(value, 'download.includeEmbeddedVideos', false); - addDefault(value, 'folders', FoldersResponse().toJson()); - addDefault(value, 'memories', MemoriesResponse().toJson()); - addDefault(value, 'ratings', RatingsResponse().toJson()); - addDefault(value, 'people', PeopleResponse().toJson()); - addDefault(value, 'tags', TagsResponse().toJson()); - addDefault(value, 'sharedLinks', SharedLinksResponse().toJson()); - addDefault(value, 'cast', CastResponse().toJson()); + addDefault(value, 'folders', FoldersResponse(enabled: false, sidebarWeb: false).toJson()); + addDefault(value, 'memories', MemoriesResponse(enabled: true, duration: 5).toJson()); + addDefault(value, 'ratings', RatingsResponse(enabled: false).toJson()); + addDefault(value, 'people', PeopleResponse(enabled: true, sidebarWeb: false).toJson()); + addDefault(value, 'tags', TagsResponse(enabled: false, sidebarWeb: false).toJson()); + addDefault(value, 'sharedLinks', SharedLinksResponse(enabled: true, sidebarWeb: false).toJson()); + addDefault(value, 'cast', CastResponse(gCastEnabled: false).toJson()); addDefault(value, 'albums', {'defaultAssetOrder': 'desc'}); } break; diff --git a/mobile/lib/utils/provider_utils.dart b/mobile/lib/utils/provider_utils.dart index 6c2d6e0f11..9524433c05 100644 --- a/mobile/lib/utils/provider_utils.dart +++ b/mobile/lib/utils/provider_utils.dart @@ -2,21 +2,17 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/infrastructure/search.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/repositories/activity_api.repository.dart'; -import 'package:immich_mobile/repositories/album_api.repository.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; import 'package:immich_mobile/repositories/partner_api.repository.dart'; import 'package:immich_mobile/repositories/person_api.repository.dart'; -import 'package:immich_mobile/repositories/timeline.repository.dart'; void invalidateAllApiRepositoryProviders(WidgetRef ref) { ref.invalidate(userApiRepositoryProvider); ref.invalidate(activityApiRepositoryProvider); ref.invalidate(partnerApiRepositoryProvider); - ref.invalidate(albumApiRepositoryProvider); ref.invalidate(personApiRepositoryProvider); ref.invalidate(assetApiRepositoryProvider); - ref.invalidate(timelineRepositoryProvider); ref.invalidate(searchApiRepositoryProvider); // Drift diff --git a/mobile/lib/utils/selection_handlers.dart b/mobile/lib/utils/selection_handlers.dart deleted file mode 100644 index f0d333e262..0000000000 --- a/mobile/lib/utils/selection_handlers.dart +++ /dev/null @@ -1,143 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asset_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:immich_mobile/services/share.service.dart'; -import 'package:immich_mobile/widgets/common/date_time_picker.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/widgets/common/location_picker.dart'; -import 'package:immich_mobile/widgets/common/share_dialog.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; - -void handleShareAssets(WidgetRef ref, BuildContext context, Iterable selection) { - showDialog( - context: context, - builder: (BuildContext buildContext) { - ref.watch(shareServiceProvider).shareAssets(selection.toList(), context).then((bool status) { - if (!status) { - ImmichToast.show( - context: context, - msg: 'image_viewer_page_state_provider_share_error'.tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - buildContext.pop(); - }); - return const ShareDialog(); - }, - barrierDismissible: false, - useRootNavigator: false, - ); -} - -Future handleArchiveAssets( - WidgetRef ref, - BuildContext context, - List selection, { - bool? shouldArchive, - ToastGravity toastGravity = ToastGravity.BOTTOM, -}) async { - if (selection.isNotEmpty) { - shouldArchive ??= !selection.every((a) => a.isArchived); - await ref.read(assetProvider.notifier).toggleArchive(selection, shouldArchive); - final message = shouldArchive - ? 'moved_to_archive'.t(context: context, args: {'count': selection.length}) - : 'moved_to_library'.t(context: context, args: {'count': selection.length}); - if (context.mounted) { - ImmichToast.show(context: context, msg: message, gravity: toastGravity); - } - } -} - -Future handleFavoriteAssets( - WidgetRef ref, - BuildContext context, - List selection, { - bool? shouldFavorite, - ToastGravity toastGravity = ToastGravity.BOTTOM, -}) async { - if (selection.isNotEmpty) { - shouldFavorite ??= !selection.every((a) => a.isFavorite); - await ref.watch(assetProvider.notifier).toggleFavorite(selection, shouldFavorite); - - final assetOrAssets = selection.length > 1 ? 'assets' : 'asset'; - final toastMessage = shouldFavorite - ? 'Added ${selection.length} $assetOrAssets to favorites' - : 'Removed ${selection.length} $assetOrAssets from favorites'; - if (context.mounted) { - ImmichToast.show(context: context, msg: toastMessage, gravity: toastGravity); - } - } -} - -Future handleEditDateTime(WidgetRef ref, BuildContext context, List selection) async { - DateTime? initialDate; - String? timeZone; - Duration? offset; - if (selection.length == 1) { - final asset = selection.first; - final assetWithExif = await ref.watch(assetServiceProvider).loadExif(asset); - final (dt, oft) = assetWithExif.getTZAdjustedTimeAndOffset(); - initialDate = dt; - offset = oft; - timeZone = assetWithExif.exifInfo?.timeZone; - } - final dateTime = await showDateTimePicker( - context: context, - initialDateTime: initialDate, - initialTZ: timeZone, - initialTZOffset: offset, - ); - - if (dateTime == null) { - return; - } - - await ref.read(assetServiceProvider).changeDateTime(selection.toList(), dateTime); -} - -Future handleEditLocation(WidgetRef ref, BuildContext context, List selection) async { - LatLng? initialLatLng; - if (selection.length == 1) { - final asset = selection.first; - final assetWithExif = await ref.watch(assetServiceProvider).loadExif(asset); - if (assetWithExif.exifInfo?.latitude != null && assetWithExif.exifInfo?.longitude != null) { - initialLatLng = LatLng(assetWithExif.exifInfo!.latitude!, assetWithExif.exifInfo!.longitude!); - } - } - - final location = await showLocationPicker(context: context, initialLatLng: initialLatLng); - - if (location == null) { - return; - } - - await ref.read(assetServiceProvider).changeLocation(selection.toList(), location); -} - -Future handleSetAssetsVisibility( - WidgetRef ref, - BuildContext context, - AssetVisibilityEnum visibility, - List selection, -) async { - if (selection.isNotEmpty) { - await ref.watch(assetProvider.notifier).setLockedView(selection, visibility); - - final assetOrAssets = selection.length > 1 ? 'assets' : 'asset'; - final toastMessage = visibility == AssetVisibilityEnum.locked - ? 'Added ${selection.length} $assetOrAssets to locked folder' - : 'Removed ${selection.length} $assetOrAssets from locked folder'; - if (context.mounted) { - ImmichToast.show(context: context, msg: toastMessage, gravity: ToastGravity.BOTTOM); - } - } -} diff --git a/mobile/lib/utils/string_helper.dart b/mobile/lib/utils/string_helper.dart deleted file mode 100644 index 201d141531..0000000000 --- a/mobile/lib/utils/string_helper.dart +++ /dev/null @@ -1,7 +0,0 @@ -extension StringExtension on String { - String capitalizeFirstLetter() { - return "${this[0].toUpperCase()}${substring(1).toLowerCase()}"; - } -} - -String s(num count) => (count == 1 ? '' : 's'); diff --git a/mobile/lib/utils/throttle.dart b/mobile/lib/utils/throttle.dart deleted file mode 100644 index 8b41d92318..0000000000 --- a/mobile/lib/utils/throttle.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter_hooks/flutter_hooks.dart'; - -/// Throttles function calls with the [interval] provided. -/// Also make sures to call the last Action after the elapsed interval -class Throttler { - final Duration interval; - DateTime? _lastActionTime; - - Throttler({required this.interval}); - - T? run(T Function() action) { - if (_lastActionTime == null || (DateTime.now().difference(_lastActionTime!) > interval)) { - final response = action(); - _lastActionTime = DateTime.now(); - return response; - } - - return null; - } - - void dispose() { - _lastActionTime = null; - } -} - -/// Creates a [Throttler] that will be disposed automatically. If no [interval] is provided, a -/// default interval of 300ms is used to throttle the function calls -Throttler useThrottler({Duration interval = const Duration(milliseconds: 300), List? keys}) => - use(_ThrottleHook(interval: interval, keys: keys)); - -class _ThrottleHook extends Hook { - const _ThrottleHook({required this.interval, super.keys}); - - final Duration interval; - - @override - HookState> createState() => _ThrottlerHookState(); -} - -class _ThrottlerHookState extends HookState { - late final throttler = Throttler(interval: hook.interval); - - @override - Throttler build(_) => throttler; - - @override - void dispose() => throttler.dispose(); - - @override - String get debugLabel => 'useThrottler'; -} diff --git a/mobile/lib/utils/thumbnail_utils.dart b/mobile/lib/utils/thumbnail_utils.dart deleted file mode 100644 index 685dc2b1c2..0000000000 --- a/mobile/lib/utils/thumbnail_utils.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; - -String getAltText(ExifInfo? exifInfo, DateTime fileCreatedAt, AssetType type, List peopleNames) { - if (exifInfo?.description != null && exifInfo!.description!.isNotEmpty) { - return exifInfo.description!; - } - final (template, args) = getAltTextTemplate(exifInfo, fileCreatedAt, type, peopleNames); - return template.t(args: args); -} - -(String, Map) getAltTextTemplate( - ExifInfo? exifInfo, - DateTime fileCreatedAt, - AssetType type, - List peopleNames, -) { - final isVideo = type == AssetType.video; - final hasLocation = exifInfo?.city != null && exifInfo?.country != null; - final date = DateFormat.yMMMMd().format(fileCreatedAt); - final args = { - "isVideo": isVideo.toString(), - "date": date, - "city": exifInfo?.city ?? "", - "country": exifInfo?.country ?? "", - "person1": peopleNames.elementAtOrNull(0) ?? "", - "person2": peopleNames.elementAtOrNull(1) ?? "", - "person3": peopleNames.elementAtOrNull(2) ?? "", - "additionalCount": (peopleNames.length - 3).toString(), - }; - final template = hasLocation - ? (switch (peopleNames.length) { - 0 => "image_alt_text_date_place", - 1 => "image_alt_text_date_place_1_person", - 2 => "image_alt_text_date_place_2_people", - 3 => "image_alt_text_date_place_3_people", - _ => "image_alt_text_date_place_4_or_more_people", - }) - : (switch (peopleNames.length) { - 0 => "image_alt_text_date", - 1 => "image_alt_text_date_1_person", - 2 => "image_alt_text_date_2_people", - 3 => "image_alt_text_date_3_people", - _ => "image_alt_text_date_4_or_more_people", - }); - return (template, args); -} diff --git a/mobile/lib/widgets/activities/activity_text_field.dart b/mobile/lib/widgets/activities/activity_text_field.dart deleted file mode 100644 index d21cdfbc94..0000000000 --- a/mobile/lib/widgets/activities/activity_text_field.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -class ActivityTextField extends HookConsumerWidget { - final bool isEnabled; - final String? likeId; - final Function(String) onSubmit; - - const ActivityTextField({required this.onSubmit, this.isEnabled = true, this.likeId, super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final album = ref.watch(currentAlbumProvider)!; - final asset = ref.watch(currentAssetProvider); - final activityNotifier = ref.read(albumActivityProvider(album.remoteId!, asset?.remoteId).notifier); - final user = ref.watch(currentUserProvider); - final inputController = useTextEditingController(); - final inputFocusNode = useFocusNode(); - final liked = likeId != null; - - // Show keyboard immediately on activities open - useEffect(() { - inputFocusNode.requestFocus(); - return null; - }, []); - - // Pass text to callback and reset controller - void onEditingComplete() { - onSubmit(inputController.text); - inputController.clear(); - inputFocusNode.unfocus(); - } - - Future addLike() async { - await activityNotifier.addLike(); - } - - Future removeLike() async { - if (liked) { - await activityNotifier.removeActivity(likeId!); - } - } - - return Padding( - padding: const EdgeInsets.only(bottom: 10), - child: TextField( - controller: inputController, - enabled: isEnabled, - focusNode: inputFocusNode, - textInputAction: TextInputAction.send, - autofocus: false, - decoration: InputDecoration( - border: InputBorder.none, - focusedBorder: InputBorder.none, - prefixIcon: user != null - ? Padding( - padding: const EdgeInsets.symmetric(horizontal: 15), - child: UserCircleAvatar(user: user, size: 30), - ) - : null, - suffixIcon: Padding( - padding: const EdgeInsets.only(right: 10), - child: IconButton( - icon: Icon(liked ? Icons.thumb_up : Icons.thumb_up_off_alt), - onPressed: liked ? removeLike : addLike, - ), - ), - suffixIconColor: liked ? context.primaryColor : null, - hintText: !isEnabled ? 'shared_album_activities_input_disable'.tr() : 'say_something'.tr(), - hintStyle: TextStyle(fontWeight: FontWeight.normal, fontSize: 14, color: Colors.grey[600]), - ), - onEditingComplete: onEditingComplete, - onTapOutside: (_) => inputFocusNode.unfocus(), - ), - ); - } -} diff --git a/mobile/lib/widgets/activities/activity_tile.dart b/mobile/lib/widgets/activities/activity_tile.dart deleted file mode 100644 index ac3b6c95a4..0000000000 --- a/mobile/lib/widgets/activities/activity_tile.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/datetime_extensions.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/providers/activity_service.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -class ActivityTile extends HookConsumerWidget { - final Activity activity; - final bool isBottomSheet; - - const ActivityTile(this.activity, {super.key, this.isBottomSheet = false}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final asset = ref.watch(currentAssetProvider); - final isLike = activity.type == ActivityType.like; - // Asset thumbnail is displayed when we are accessing activities from the album page - // currentAssetProvider will not be set until we open the gallery viewer - final showAssetThumbnail = asset == null && activity.assetId != null && !isBottomSheet; - - onTap() async { - final activityService = ref.read(activityServiceProvider); - final route = await activityService.buildAssetViewerRoute(activity.assetId!, ref); - if (route != null) { - await context.pushRoute(route); - } - } - - return ListTile( - minVerticalPadding: 15, - leading: isLike - ? Container( - width: isBottomSheet ? 30 : 44, - alignment: Alignment.center, - child: Icon(Icons.thumb_up, color: context.primaryColor), - ) - : isBottomSheet - ? UserCircleAvatar(user: activity.user, size: 30) - : UserCircleAvatar(user: activity.user), - title: _ActivityTitle( - userName: activity.user.name, - createdAt: activity.createdAt.timeAgo(), - leftAlign: isBottomSheet ? false : (isLike || showAssetThumbnail), - ), - // No subtitle for like, so center title - titleAlignment: !isLike ? ListTileTitleAlignment.top : ListTileTitleAlignment.center, - trailing: showAssetThumbnail ? _ActivityAssetThumbnail(activity.assetId!, onTap) : null, - subtitle: !isLike ? Text(activity.comment!) : null, - ); - } -} - -class _ActivityTitle extends StatelessWidget { - final String userName; - final String createdAt; - final bool leftAlign; - - const _ActivityTitle({required this.userName, required this.createdAt, required this.leftAlign}); - - @override - Widget build(BuildContext context) { - final textColor = context.isDarkTheme ? Colors.white : Colors.black; - final textStyle = context.textTheme.bodyMedium?.copyWith(color: textColor.withValues(alpha: 0.6)); - - return Row( - mainAxisAlignment: leftAlign ? MainAxisAlignment.start : MainAxisAlignment.spaceBetween, - mainAxisSize: leftAlign ? MainAxisSize.min : MainAxisSize.max, - children: [ - Text(userName, style: textStyle, overflow: TextOverflow.ellipsis), - if (leftAlign) Text(" • ", style: textStyle), - Expanded( - child: Text( - createdAt, - style: textStyle, - overflow: TextOverflow.ellipsis, - textAlign: leftAlign ? TextAlign.left : TextAlign.right, - ), - ), - ], - ); - } -} - -class _ActivityAssetThumbnail extends StatelessWidget { - final String assetId; - final GestureTapCallback? onTap; - - const _ActivityAssetThumbnail(this.assetId, this.onTap); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: Container( - width: 40, - height: 30, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(4)), - image: DecorationImage( - image: RemoteImageProvider.thumbnail(assetId: assetId, thumbhash: ""), - fit: BoxFit.cover, - ), - ), - child: const SizedBox.shrink(), - ), - ); - } -} diff --git a/mobile/lib/widgets/activities/dismissible_activity.dart b/mobile/lib/widgets/activities/dismissible_activity.dart index 806181ecdc..c056f5ee35 100644 --- a/mobile/lib/widgets/activities/dismissible_activity.dart +++ b/mobile/lib/widgets/activities/dismissible_activity.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:immich_mobile/widgets/activities/activity_tile.dart'; import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; -/// Wraps an [ActivityTile] and makes it dismissible class DismissibleActivity extends StatelessWidget { final String activityId; final Widget body; diff --git a/mobile/lib/widgets/album/add_to_album_bottom_sheet.dart b/mobile/lib/widgets/album/add_to_album_bottom_sheet.dart deleted file mode 100644 index d8f6a8885a..0000000000 --- a/mobile/lib/widgets/album/add_to_album_bottom_sheet.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/widgets/common/drag_sheet.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class AddToAlbumBottomSheet extends HookConsumerWidget { - /// The asset to add to an album - final List assets; - - const AddToAlbumBottomSheet({super.key, required this.assets}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList(); - final albumService = ref.watch(albumServiceProvider); - - useEffect(() { - // Fetch album updates, e.g., cover image - ref.read(albumProvider.notifier).refreshRemoteAlbums(); - - return null; - }, []); - - void addToAlbum(Album album) async { - final result = await albumService.addAssets(album, assets); - - if (result != null) { - if (result.alreadyInAlbum.isNotEmpty) { - ImmichToast.show( - context: context, - msg: 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {"album": album.name}), - ); - } else { - ImmichToast.show( - context: context, - msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {"album": album.name}), - ); - } - } - context.pop(); - } - - return Card( - elevation: 0, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only(topLeft: Radius.circular(15), topRight: Radius.circular(15)), - ), - child: CustomScrollView( - slivers: [ - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16), - sliver: SliverToBoxAdapter( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 12), - const Align(alignment: Alignment.center, child: CustomDraggingHandle()), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('add_to_album'.tr(), style: context.textTheme.displayMedium), - TextButton.icon( - icon: Icon(Icons.add, color: context.primaryColor), - label: Text('common_create_new_album'.tr(), style: TextStyle(color: context.primaryColor)), - onPressed: () { - context.pushRoute(CreateAlbumRoute(assets: assets)); - }, - ), - ], - ), - ], - ), - ), - ), - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16), - sliver: AddToAlbumSliverList( - albums: albums, - sharedAlbums: albums.where((a) => a.shared).toList(), - onAddToAlbum: addToAlbum, - ), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/album/add_to_album_sliverlist.dart b/mobile/lib/widgets/album/add_to_album_sliverlist.dart deleted file mode 100644 index defbd90388..0000000000 --- a/mobile/lib/widgets/album/add_to_album_sliverlist.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; -import 'package:immich_mobile/widgets/album/album_thumbnail_listtile.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; - -class AddToAlbumSliverList extends HookConsumerWidget { - /// The asset to add to an album - final List albums; - final List sharedAlbums; - final void Function(Album) onAddToAlbum; - final bool enabled; - - const AddToAlbumSliverList({ - super.key, - required this.onAddToAlbum, - required this.albums, - required this.sharedAlbums, - this.enabled = true, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albumSortMode = ref.watch(albumSortByOptionsProvider); - final albumSortIsReverse = ref.watch(albumSortOrderProvider); - final sortedAlbums = albumSortMode.sortFn(albums, albumSortIsReverse); - final sortedSharedAlbums = albumSortMode.sortFn(sharedAlbums, albumSortIsReverse); - - return SliverList( - delegate: SliverChildBuilderDelegate(childCount: albums.length + (sharedAlbums.isEmpty ? 0 : 1), ( - context, - index, - ) { - // Build shared expander - if (index == 0 && sortedSharedAlbums.isNotEmpty) { - return Padding( - padding: const EdgeInsets.only(bottom: 8), - child: ExpansionTile( - title: Text('shared'.tr()), - tilePadding: const EdgeInsets.symmetric(horizontal: 10.0), - leading: const Icon(Icons.group), - children: [ - ListView.builder( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - itemCount: sortedSharedAlbums.length, - itemBuilder: (context, index) => AlbumThumbnailListTile( - album: sortedSharedAlbums[index], - onTap: enabled ? () => onAddToAlbum(sortedSharedAlbums[index]) : () {}, - ), - ), - ], - ), - ); - } - - // Build albums list - final offset = index - (sharedAlbums.isNotEmpty ? 1 : 0); - final album = sortedAlbums[offset]; - return AlbumThumbnailListTile(album: album, onTap: enabled ? () => onAddToAlbum(album) : () {}); - }), - ); - } -} diff --git a/mobile/lib/widgets/album/album_thumbnail_card.dart b/mobile/lib/widgets/album/album_thumbnail_card.dart deleted file mode 100644 index 6c56f5d843..0000000000 --- a/mobile/lib/widgets/album/album_thumbnail_card.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; - -class AlbumThumbnailCard extends ConsumerWidget { - final Function()? onTap; - - /// Whether or not to show the owner of the album (or "Owned") - /// in the subtitle of the album - final bool showOwner; - final bool showTitle; - - const AlbumThumbnailCard({super.key, required this.album, this.onTap, this.showOwner = false, this.showTitle = true}); - - final Album album; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return LayoutBuilder( - builder: (context, constraints) { - var cardSize = constraints.maxWidth; - - buildEmptyThumbnail() { - return Container( - height: cardSize, - width: cardSize, - decoration: BoxDecoration(color: context.colorScheme.surfaceContainerHigh), - child: Center( - child: Icon(Icons.no_photography, size: cardSize * .15, color: context.colorScheme.primary), - ), - ); - } - - buildAlbumThumbnail() => ImmichThumbnail(asset: album.thumbnail.value, width: cardSize, height: cardSize); - - buildAlbumTextRow() { - // Add the owner name to the subtitle - String? owner; - if (showOwner) { - if (album.ownerId == ref.read(currentUserProvider)?.id) { - owner = 'owned'.tr(); - } else if (album.ownerName != null) { - owner = 'shared_by_user'.t(context: context, args: {'user': album.ownerName!}); - } - } - - return Text.rich( - TextSpan( - children: [ - TextSpan( - text: 'items_count'.t(context: context, args: {'count': album.assetCount}), - ), - if (owner != null) const TextSpan(text: ' • '), - if (owner != null) TextSpan(text: owner), - ], - style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ), - overflow: TextOverflow.fade, - ); - } - - return GestureDetector( - onTap: onTap, - child: Flex( - direction: Axis.vertical, - children: [ - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: cardSize, - height: cardSize, - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(20)), - child: album.thumbnail.value == null ? buildEmptyThumbnail() : buildAlbumThumbnail(), - ), - ), - if (showTitle) ...[ - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: SizedBox( - width: cardSize, - child: Text( - album.name, - overflow: TextOverflow.ellipsis, - style: context.textTheme.titleSmall?.copyWith( - color: context.colorScheme.onSurface, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - buildAlbumTextRow(), - ], - ], - ), - ), - ], - ), - ); - }, - ); - } -} diff --git a/mobile/lib/widgets/album/album_thumbnail_listtile.dart b/mobile/lib/widgets/album/album_thumbnail_listtile.dart deleted file mode 100644 index 386084b034..0000000000 --- a/mobile/lib/widgets/album/album_thumbnail_listtile.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; -import 'package:openapi/api.dart'; - -class AlbumThumbnailListTile extends StatelessWidget { - const AlbumThumbnailListTile({super.key, required this.album, this.onTap}); - - final Album album; - final void Function()? onTap; - - @override - Widget build(BuildContext context) { - var cardSize = 68.0; - - buildEmptyThumbnail() { - return Container( - decoration: BoxDecoration(color: context.isDarkTheme ? Colors.grey[800] : Colors.grey[200]), - child: SizedBox( - height: cardSize, - width: cardSize, - child: const Center(child: Icon(Icons.no_photography)), - ), - ); - } - - buildAlbumThumbnail() { - return SizedBox( - width: cardSize, - height: cardSize, - child: Thumbnail( - imageProvider: RemoteImageProvider(url: getAlbumThumbnailUrl(album, type: AssetMediaSize.thumbnail)), - ), - ); - } - - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: - onTap ?? - () { - context.pushRoute(AlbumViewerRoute(albumId: album.id)); - }, - child: Padding( - padding: const EdgeInsets.only(bottom: 12.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(8)), - child: album.thumbnail.value == null ? buildEmptyThumbnail() : buildAlbumThumbnail(), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - album.name, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'items_count'.t(context: context, args: {'count': album.assetCount}), - style: const TextStyle(fontSize: 12), - ), - if (album.shared) ...[ - const Text(' • ', style: TextStyle(fontSize: 12)), - Text('shared'.tr(), style: const TextStyle(fontSize: 12)), - ], - ], - ), - ], - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/album/album_title_text_field.dart b/mobile/lib/widgets/album/album_title_text_field.dart deleted file mode 100644 index 0a7438b7ae..0000000000 --- a/mobile/lib/widgets/album/album_title_text_field.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album_title.provider.dart'; - -class AlbumTitleTextField extends ConsumerWidget { - const AlbumTitleTextField({ - super.key, - required this.isAlbumTitleEmpty, - required this.albumTitleTextFieldFocusNode, - required this.albumTitleController, - required this.isAlbumTitleTextFieldFocus, - }); - - final ValueNotifier isAlbumTitleEmpty; - final FocusNode albumTitleTextFieldFocusNode; - final TextEditingController albumTitleController; - final ValueNotifier isAlbumTitleTextFieldFocus; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return TextField( - onChanged: (v) { - if (v.isEmpty) { - isAlbumTitleEmpty.value = true; - } else { - isAlbumTitleEmpty.value = false; - } - - ref.watch(albumTitleProvider.notifier).setAlbumTitle(v); - }, - focusNode: albumTitleTextFieldFocusNode, - style: TextStyle(fontSize: 28, color: context.colorScheme.onSurface, fontWeight: FontWeight.bold), - controller: albumTitleController, - onTap: () { - isAlbumTitleTextFieldFocus.value = true; - - if (albumTitleController.text == 'Untitled') { - albumTitleController.clear(); - } - }, - decoration: InputDecoration( - contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), - suffixIcon: !isAlbumTitleEmpty.value && isAlbumTitleTextFieldFocus.value - ? IconButton( - onPressed: () { - albumTitleController.clear(); - isAlbumTitleEmpty.value = true; - }, - icon: Icon(Icons.cancel_rounded, color: context.primaryColor), - splashRadius: 10, - ) - : null, - enabledBorder: const OutlineInputBorder( - borderSide: BorderSide(color: Colors.transparent), - borderRadius: BorderRadius.all(Radius.circular(10)), - ), - focusedBorder: const OutlineInputBorder( - borderSide: BorderSide(color: Colors.transparent), - borderRadius: BorderRadius.all(Radius.circular(10)), - ), - hintText: 'add_a_title'.tr(), - hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith( - fontSize: 28, - fontWeight: FontWeight.bold, - ), - focusColor: Colors.grey[300], - fillColor: context.colorScheme.surfaceContainerHigh, - filled: isAlbumTitleTextFieldFocus.value, - ), - ); - } -} diff --git a/mobile/lib/widgets/album/album_viewer_appbar.dart b/mobile/lib/widgets/album/album_viewer_appbar.dart deleted file mode 100644 index 4fd4b31013..0000000000 --- a/mobile/lib/widgets/album/album_viewer_appbar.dart +++ /dev/null @@ -1,307 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/activity_statistics.provider.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/album_viewer.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class AlbumViewerAppbar extends HookConsumerWidget implements PreferredSizeWidget { - const AlbumViewerAppbar({ - super.key, - required this.userId, - required this.titleFocusNode, - required this.descriptionFocusNode, - this.onAddPhotos, - this.onAddUsers, - required this.onActivities, - }); - - final String userId; - final FocusNode titleFocusNode; - final FocusNode descriptionFocusNode; - final void Function()? onAddPhotos; - final void Function()? onAddUsers; - final void Function() onActivities; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albumState = useState(ref.read(currentAlbumProvider)); - final album = albumState.value; - ref.listen(currentAlbumProvider, (_, newAlbum) { - final oldAlbum = albumState.value; - if (oldAlbum != null && newAlbum != null && oldAlbum.id == newAlbum.id) { - return; - } - - albumState.value = newAlbum; - }); - - if (album == null) { - return const SizedBox(); - } - - final albumViewer = ref.watch(albumViewerProvider); - final newAlbumTitle = albumViewer.editTitleText; - final newAlbumDescription = albumViewer.editDescriptionText; - final isEditAlbum = albumViewer.isEditAlbum; - - final comments = album.shared ? ref.watch(activityStatisticsProvider(album.remoteId!)) : 0; - - deleteAlbum() async { - final bool success = await ref.watch(albumProvider.notifier).deleteAlbum(album); - - unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]))); - - if (!success) { - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_delete".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - } - - Future onDeleteAlbumPressed() { - return showDialog( - context: context, - barrierDismissible: false, // user must tap button! - builder: (BuildContext context) { - return AlertDialog( - title: const Text('delete_album').tr(), - content: const Text('album_viewer_appbar_delete_confirm').tr(), - actions: [ - TextButton( - onPressed: () => context.pop('Cancel'), - child: Text( - 'cancel', - style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold), - ).tr(), - ), - TextButton( - onPressed: () { - context.pop('Confirm'); - deleteAlbum(); - }, - child: Text( - 'confirm', - style: TextStyle(fontWeight: FontWeight.bold, color: context.colorScheme.error), - ).tr(), - ), - ], - ); - }, - ); - } - - void onLeaveAlbumPressed() async { - bool isSuccess = await ref.watch(albumProvider.notifier).leaveAlbum(album); - - if (isSuccess) { - unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]))); - } else { - context.pop(); - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_leave".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - } - - buildBottomSheetActions() { - return [ - album.ownerId == userId - ? ListTile( - leading: const Icon(Icons.delete_forever_rounded), - title: const Text('delete_album', style: TextStyle(fontWeight: FontWeight.w500)).tr(), - onTap: onDeleteAlbumPressed, - ) - : ListTile( - leading: const Icon(Icons.person_remove_rounded), - title: const Text( - 'album_viewer_appbar_share_leave', - style: TextStyle(fontWeight: FontWeight.w500), - ).tr(), - onTap: onLeaveAlbumPressed, - ), - ]; - // } - } - - void onSortOrderToggled() async { - final updatedAlbum = await ref.read(albumProvider.notifier).toggleSortOrder(album); - - if (updatedAlbum == null) { - ImmichToast.show( - context: context, - msg: "error_change_sort_album".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - - context.pop(); - } - - void buildBottomSheet() { - final ownerActions = [ - ListTile( - leading: const Icon(Icons.person_add_alt_rounded), - onTap: () { - context.pop(); - final onAddUsers = this.onAddUsers; - if (onAddUsers != null) { - onAddUsers(); - } - }, - title: const Text("album_viewer_page_share_add_users", style: TextStyle(fontWeight: FontWeight.w500)).tr(), - ), - ListTile( - leading: const Icon(Icons.swap_vert_rounded), - onTap: onSortOrderToggled, - title: const Text("change_display_order", style: TextStyle(fontWeight: FontWeight.w500)).tr(), - ), - ListTile( - leading: const Icon(Icons.link_rounded), - onTap: () { - context.pushRoute(SharedLinkEditRoute(albumId: album.remoteId)); - context.pop(); - }, - title: const Text("control_bottom_app_bar_share_link", style: TextStyle(fontWeight: FontWeight.w500)).tr(), - ), - ListTile( - leading: const Icon(Icons.settings_rounded), - onTap: () => context.navigateTo(const AlbumOptionsRoute()), - title: const Text("options", style: TextStyle(fontWeight: FontWeight.w500)).tr(), - ), - ]; - - final commonActions = [ - ListTile( - leading: const Icon(Icons.add_photo_alternate_outlined), - onTap: () { - context.pop(); - final onAddPhotos = this.onAddPhotos; - if (onAddPhotos != null) { - onAddPhotos(); - } - }, - title: const Text("add_photos", style: TextStyle(fontWeight: FontWeight.w500)).tr(), - ), - ]; - showModalBottomSheet( - backgroundColor: context.scaffoldBackgroundColor, - isScrollControlled: false, - context: context, - builder: (context) { - return SafeArea( - child: Padding( - padding: const EdgeInsets.only(top: 24.0), - child: ListView( - shrinkWrap: true, - children: [ - ...buildBottomSheetActions(), - if (onAddPhotos != null) ...commonActions, - if (onAddPhotos != null && userId == album.ownerId) ...ownerActions, - ], - ), - ), - ); - }, - ); - } - - Widget buildActivitiesButton() { - return IconButton( - onPressed: onActivities, - icon: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon(Icons.mode_comment_outlined), - if (comments != 0) - Padding( - padding: const EdgeInsets.only(left: 5), - child: Text( - comments.toString(), - style: TextStyle(fontWeight: FontWeight.bold, color: context.primaryColor), - ), - ), - ], - ), - ); - } - - buildLeadingButton() { - if (isEditAlbum) { - return IconButton( - onPressed: () async { - if (newAlbumTitle.isNotEmpty) { - bool isSuccess = await ref.watch(albumViewerProvider.notifier).changeAlbumTitle(album, newAlbumTitle); - if (!isSuccess) { - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_title".tr(), - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - ); - } - titleFocusNode.unfocus(); - } else if (newAlbumDescription.isNotEmpty) { - bool isSuccessDescription = await ref - .watch(albumViewerProvider.notifier) - .changeAlbumDescription(album, newAlbumDescription); - if (!isSuccessDescription) { - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_description".tr(), - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - ); - } - descriptionFocusNode.unfocus(); - } else { - titleFocusNode.unfocus(); - descriptionFocusNode.unfocus(); - ref.read(albumViewerProvider.notifier).disableEditAlbum(); - } - }, - icon: const Icon(Icons.check_rounded), - splashRadius: 25, - ); - } else { - return IconButton( - onPressed: context.maybePop, - icon: const Icon(Icons.arrow_back_ios_rounded), - splashRadius: 25, - ); - } - } - - return AppBar( - elevation: 0, - backgroundColor: context.scaffoldBackgroundColor, - leading: buildLeadingButton(), - centerTitle: false, - actions: [ - if (album.shared && (album.activityEnabled || comments != 0)) buildActivitiesButton(), - if (album.isRemote) ...[ - IconButton(splashRadius: 25, onPressed: buildBottomSheet, icon: const Icon(Icons.more_horiz_rounded)), - ], - ], - ); - } - - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); -} diff --git a/mobile/lib/widgets/album/album_viewer_editable_description.dart b/mobile/lib/widgets/album/album_viewer_editable_description.dart deleted file mode 100644 index decd268ff3..0000000000 --- a/mobile/lib/widgets/album/album_viewer_editable_description.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album_viewer.provider.dart'; - -class AlbumViewerEditableDescription extends HookConsumerWidget { - final String albumDescription; - final FocusNode descriptionFocusNode; - const AlbumViewerEditableDescription({super.key, required this.albumDescription, required this.descriptionFocusNode}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albumViewerState = ref.watch(albumViewerProvider); - - final descriptionTextEditController = useTextEditingController( - text: albumViewerState.isEditAlbum && albumViewerState.editDescriptionText.isNotEmpty - ? albumViewerState.editDescriptionText - : albumDescription, - ); - - void onFocusModeChange() { - if (!descriptionFocusNode.hasFocus && descriptionTextEditController.text.isEmpty) { - ref.watch(albumViewerProvider.notifier).setEditDescriptionText(""); - descriptionTextEditController.text = ""; - } - } - - useEffect(() { - descriptionFocusNode.addListener(onFocusModeChange); - return () { - descriptionFocusNode.removeListener(onFocusModeChange); - }; - }, []); - - return Material( - color: Colors.transparent, - child: TextField( - onChanged: (value) { - if (value.isEmpty) { - } else { - ref.watch(albumViewerProvider.notifier).setEditDescriptionText(value); - } - }, - focusNode: descriptionFocusNode, - style: context.textTheme.bodyLarge, - maxLines: 3, - minLines: 1, - controller: descriptionTextEditController, - onTap: () { - context.focusScope.requestFocus(descriptionFocusNode); - - ref.watch(albumViewerProvider.notifier).setEditDescriptionText(albumDescription); - ref.watch(albumViewerProvider.notifier).enableEditAlbum(); - - if (descriptionTextEditController.text == '') { - descriptionTextEditController.clear(); - } - }, - decoration: InputDecoration( - contentPadding: const EdgeInsets.all(8), - suffixIcon: descriptionFocusNode.hasFocus - ? IconButton( - onPressed: () { - descriptionTextEditController.clear(); - }, - icon: Icon(Icons.cancel_rounded, color: context.primaryColor), - splashRadius: 10, - ) - : null, - enabledBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)), - focusedBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)), - focusColor: Colors.grey[300], - fillColor: context.scaffoldBackgroundColor, - filled: descriptionFocusNode.hasFocus, - hintText: 'add_a_description'.tr(), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/album/album_viewer_editable_title.dart b/mobile/lib/widgets/album/album_viewer_editable_title.dart deleted file mode 100644 index c84e613017..0000000000 --- a/mobile/lib/widgets/album/album_viewer_editable_title.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album_viewer.provider.dart'; - -class AlbumViewerEditableTitle extends HookConsumerWidget { - final String albumName; - final FocusNode titleFocusNode; - const AlbumViewerEditableTitle({super.key, required this.albumName, required this.titleFocusNode}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albumViewerState = ref.watch(albumViewerProvider); - - final titleTextEditController = useTextEditingController( - text: albumViewerState.isEditAlbum && albumViewerState.editTitleText.isNotEmpty - ? albumViewerState.editTitleText - : albumName, - ); - - void onFocusModeChange() { - if (!titleFocusNode.hasFocus && titleTextEditController.text.isEmpty) { - ref.watch(albumViewerProvider.notifier).setEditTitleText("Untitled"); - titleTextEditController.text = "Untitled"; - } - } - - useEffect(() { - titleFocusNode.addListener(onFocusModeChange); - return () { - titleFocusNode.removeListener(onFocusModeChange); - }; - }, []); - - return Material( - color: Colors.transparent, - child: TextField( - onChanged: (value) { - if (value.isEmpty) { - } else { - ref.watch(albumViewerProvider.notifier).setEditTitleText(value); - } - }, - focusNode: titleFocusNode, - style: context.textTheme.headlineLarge?.copyWith(fontWeight: FontWeight.w700), - controller: titleTextEditController, - onTap: () { - context.focusScope.requestFocus(titleFocusNode); - - ref.watch(albumViewerProvider.notifier).setEditTitleText(albumName); - ref.watch(albumViewerProvider.notifier).enableEditAlbum(); - - if (titleTextEditController.text == 'Untitled') { - titleTextEditController.clear(); - } - }, - decoration: InputDecoration( - contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 0), - suffixIcon: titleFocusNode.hasFocus - ? IconButton( - onPressed: () { - titleTextEditController.clear(); - }, - icon: Icon(Icons.cancel_rounded, color: context.primaryColor), - splashRadius: 10, - ) - : null, - enabledBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)), - focusedBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)), - focusColor: Colors.grey[300], - fillColor: context.scaffoldBackgroundColor, - filled: titleFocusNode.hasFocus, - hintText: 'add_a_title'.tr(), - hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith(fontSize: 28), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/album/shared_album_thumbnail_image.dart b/mobile/lib/widgets/album/shared_album_thumbnail_image.dart deleted file mode 100644 index b21e86d145..0000000000 --- a/mobile/lib/widgets/album/shared_album_thumbnail_image.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; - -class SharedAlbumThumbnailImage extends HookConsumerWidget { - final Asset asset; - - const SharedAlbumThumbnailImage({super.key, required this.asset}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return GestureDetector( - onTap: () { - // debugPrint("View ${asset.id}"); - }, - child: Stack(children: [ImmichThumbnail(asset: asset, width: 500, height: 500)]), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/asset_drag_region.dart b/mobile/lib/widgets/asset_grid/asset_drag_region.dart deleted file mode 100644 index 71e55acbd6..0000000000 --- a/mobile/lib/widgets/asset_grid/asset_drag_region.dart +++ /dev/null @@ -1,207 +0,0 @@ -// Based on https://stackoverflow.com/a/52625182 - -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; - -class AssetDragRegion extends StatefulWidget { - final Widget child; - - final void Function(AssetIndex valueKey)? onStart; - final void Function(AssetIndex valueKey)? onAssetEnter; - final void Function()? onEnd; - final void Function()? onScrollStart; - final void Function(ScrollDirection direction)? onScroll; - - const AssetDragRegion({ - super.key, - required this.child, - this.onStart, - this.onAssetEnter, - this.onEnd, - this.onScrollStart, - this.onScroll, - }); - @override - State createState() => _AssetDragRegionState(); -} - -class _AssetDragRegionState extends State { - late AssetIndex? assetUnderPointer; - late AssetIndex? anchorAsset; - - // Scroll related state - static const double scrollOffset = 0.10; - double? topScrollOffset; - double? bottomScrollOffset; - Timer? scrollTimer; - late bool scrollNotified; - - @override - void initState() { - super.initState(); - assetUnderPointer = null; - anchorAsset = null; - scrollNotified = false; - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - topScrollOffset = null; - bottomScrollOffset = null; - } - - @override - void dispose() { - scrollTimer?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return RawGestureDetector( - gestures: { - _CustomLongPressGestureRecognizer: GestureRecognizerFactoryWithHandlers<_CustomLongPressGestureRecognizer>( - () => _CustomLongPressGestureRecognizer(), - _registerCallbacks, - ), - }, - child: widget.child, - ); - } - - void _registerCallbacks(_CustomLongPressGestureRecognizer recognizer) { - recognizer.onLongPressMoveUpdate = (details) => _onLongPressMove(details); - recognizer.onLongPressStart = (details) => _onLongPressStart(details); - recognizer.onLongPressUp = _onLongPressEnd; - } - - AssetIndex? _getValueKeyAtPositon(Offset position) { - final box = context.findAncestorRenderObjectOfType(); - if (box == null) return null; - - final hitTestResult = BoxHitTestResult(); - final local = box.globalToLocal(position); - if (!box.hitTest(hitTestResult, position: local)) return null; - - return (hitTestResult.path.firstWhereOrNull((hit) => hit.target is _AssetIndexProxy)?.target as _AssetIndexProxy?) - ?.index; - } - - void _onLongPressStart(LongPressStartDetails event) { - /// Calculate widget height and scroll offset when long press starting instead of in [initState] - /// or [didChangeDependencies] as the grid might still be rendering into view to get the actual size - final height = context.size?.height; - if (height != null && (topScrollOffset == null || bottomScrollOffset == null)) { - topScrollOffset = height * scrollOffset; - bottomScrollOffset = height - topScrollOffset!; - } - - final initialHit = _getValueKeyAtPositon(event.globalPosition); - anchorAsset = initialHit; - if (initialHit == null) return; - - if (anchorAsset != null) { - widget.onStart?.call(anchorAsset!); - } - } - - void _onLongPressEnd() { - scrollNotified = false; - scrollTimer?.cancel(); - widget.onEnd?.call(); - } - - void _onLongPressMove(LongPressMoveUpdateDetails event) { - if (anchorAsset == null) return; - if (topScrollOffset == null || bottomScrollOffset == null) return; - - final currentDy = event.localPosition.dy; - - if (currentDy > bottomScrollOffset!) { - scrollTimer ??= Timer.periodic( - const Duration(milliseconds: 50), - (_) => widget.onScroll?.call(ScrollDirection.forward), - ); - } else if (currentDy < topScrollOffset!) { - scrollTimer ??= Timer.periodic( - const Duration(milliseconds: 50), - (_) => widget.onScroll?.call(ScrollDirection.reverse), - ); - } else { - scrollTimer?.cancel(); - scrollTimer = null; - } - - final currentlyTouchingAsset = _getValueKeyAtPositon(event.globalPosition); - if (currentlyTouchingAsset == null) return; - - if (assetUnderPointer != currentlyTouchingAsset) { - if (!scrollNotified) { - scrollNotified = true; - widget.onScrollStart?.call(); - } - - widget.onAssetEnter?.call(currentlyTouchingAsset); - assetUnderPointer = currentlyTouchingAsset; - } - } -} - -class _CustomLongPressGestureRecognizer extends LongPressGestureRecognizer { - @override - void rejectGesture(int pointer) { - acceptGesture(pointer); - } -} - -class AssetIndexWrapper extends SingleChildRenderObjectWidget { - final int rowIndex; - final int sectionIndex; - - const AssetIndexWrapper({required Widget super.child, required this.rowIndex, required this.sectionIndex, super.key}); - - @override - // ignore: library_private_types_in_public_api - _AssetIndexProxy createRenderObject(BuildContext context) { - return _AssetIndexProxy( - index: AssetIndex(rowIndex: rowIndex, sectionIndex: sectionIndex), - ); - } - - @override - void updateRenderObject( - BuildContext context, - // ignore: library_private_types_in_public_api - _AssetIndexProxy renderObject, - ) { - renderObject.index = AssetIndex(rowIndex: rowIndex, sectionIndex: sectionIndex); - } -} - -class _AssetIndexProxy extends RenderProxyBox { - AssetIndex index; - - _AssetIndexProxy({required this.index}); -} - -class AssetIndex { - final int rowIndex; - final int sectionIndex; - - const AssetIndex({required this.rowIndex, required this.sectionIndex}); - - @override - bool operator ==(covariant AssetIndex other) { - if (identical(this, other)) return true; - - return other.rowIndex == rowIndex && other.sectionIndex == sectionIndex; - } - - @override - int get hashCode => rowIndex.hashCode ^ sectionIndex.hashCode; -} diff --git a/mobile/lib/widgets/asset_grid/asset_grid_data_structure.dart b/mobile/lib/widgets/asset_grid/asset_grid_data_structure.dart deleted file mode 100644 index d95d6efe2e..0000000000 --- a/mobile/lib/widgets/asset_grid/asset_grid_data_structure.dart +++ /dev/null @@ -1,307 +0,0 @@ -import 'dart:math'; - -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:isar/isar.dart'; -import 'package:logging/logging.dart'; - -final log = Logger('AssetGridDataStructure'); - -enum RenderAssetGridElementType { assets, assetRow, groupDividerTitle, monthTitle } - -class RenderAssetGridElement { - final RenderAssetGridElementType type; - final String? title; - final DateTime date; - final int count; - final int offset; - final int totalCount; - - const RenderAssetGridElement( - this.type, { - this.title, - required this.date, - this.count = 0, - this.offset = 0, - this.totalCount = 0, - }); -} - -enum GroupAssetsBy { day, month, auto, none } - -class RenderList { - final List elements; - final List? allAssets; - final QueryBuilder? query; - final int totalAssets; - - /// reference to batch of assets loaded from DB with offset [_bufOffset] - List _buf = []; - - /// global offset of assets in [_buf] - int _bufOffset = 0; - - RenderList(this.elements, this.query, this.allAssets) : totalAssets = allAssets?.length ?? query!.countSync(); - - bool get isEmpty => totalAssets == 0; - - /// Loads the requested assets from the database to an internal buffer if not cached - /// and returns a slice of that buffer - List loadAssets(int offset, int count) { - assert(offset >= 0); - assert(count > 0); - assert(offset + count <= totalAssets); - if (allAssets != null) { - // if we already loaded all assets (e.g. from search result) - // simply return the requested slice of that array - return allAssets!.slice(offset, offset + count); - } else if (query != null) { - // general case: we have the query to load assets via offset from the DB on demand - if (offset < _bufOffset || offset + count > _bufOffset + _buf.length) { - // the requested slice (offset:offset+count) is not contained in the cache buffer `_buf` - // thus, fill the buffer with a new batch of assets that at least contains the requested - // assets and some more - - final bool forward = _bufOffset < offset; - // if the requested offset is greater than the cached offset, the user scrolls forward "down" - const batchSize = 256; - const oppositeSize = 64; - - // make sure to load a meaningful amount of data (and not only the requested slice) - // otherwise, each call to [loadAssets] would result in DB call trashing performance - // fills small requests to [batchSize], adds some legroom into the opposite scroll direction for large requests - final len = max(batchSize, count + oppositeSize); - // when scrolling forward, start shortly before the requested offset... - // when scrolling backward, end shortly after the requested offset... - // ... to guard against the user scrolling in the other direction - // a tiny bit resulting in a another required load from the DB - final start = max(0, forward ? offset - oppositeSize : (len > batchSize ? offset : offset + count - len)); - // load the calculated batch (start:start+len) from the DB and put it into the buffer - _buf = query!.offset(start).limit(len).findAllSync(); - _bufOffset = start; - } - assert(_bufOffset <= offset); - assert(_bufOffset + _buf.length >= offset + count); - // return the requested slice from the buffer (we made sure before that the assets are loaded!) - return _buf.slice(offset - _bufOffset, offset - _bufOffset + count); - } - throw Exception("RenderList has neither assets nor query"); - } - - /// Returns the requested asset either from cached buffer or directly from the database - Asset loadAsset(int index) { - if (allAssets != null) { - // all assets are already loaded (e.g. from search result) - return allAssets![index]; - } else if (query != null) { - // general case: we have the DB query to load asset(s) on demand - if (index >= _bufOffset && index < _bufOffset + _buf.length) { - // lucky case: the requested asset is already cached in the buffer! - return _buf[index - _bufOffset]; - } - // request the asset from the database (not changing the buffer!) - final asset = query!.offset(index).findFirstSync(); - if (asset == null) { - throw Exception("Asset at index $index does no longer exist in database"); - } - return asset; - } - throw Exception("RenderList has neither assets nor query"); - } - - static Future fromQuery(QueryBuilder query, GroupAssetsBy groupBy) => - _buildRenderList(null, query, groupBy); - - static Future _buildRenderList( - List? assets, - QueryBuilder? query, - GroupAssetsBy groupBy, - ) async { - final List elements = []; - - const pageSize = 50000; - const sectionSize = 60; // divides evenly by 2,3,4,5,6 - - if (groupBy == GroupAssetsBy.none) { - final int total = assets?.length ?? query!.countSync(); - - final dateLoader = query != null ? DateBatchLoader(query: query, batchSize: 1000 * sectionSize) : null; - - for (int i = 0; i < total; i += sectionSize) { - final date = assets != null ? assets[i].fileCreatedAt : await dateLoader?.getDate(i); - - final int count = i + sectionSize > total ? total - i : sectionSize; - if (date == null) break; - elements.add( - RenderAssetGridElement( - RenderAssetGridElementType.assets, - date: date, - count: count, - totalCount: total, - offset: i, - ), - ); - } - return RenderList(elements, query, assets); - } - - final formatSameYear = groupBy == GroupAssetsBy.month ? DateFormat.MMMM() : DateFormat.MMMEd(); - final formatOtherYear = groupBy == GroupAssetsBy.month ? DateFormat.yMMMM() : DateFormat.yMMMEd(); - final currentYear = DateTime.now().year; - final formatMergedSameYear = DateFormat.MMMd(); - final formatMergedOtherYear = DateFormat.yMMMd(); - - int offset = 0; - DateTime? last; - DateTime? current; - int lastOffset = 0; - int count = 0; - int monthCount = 0; - int lastMonthIndex = 0; - - String formatDateRange(DateTime from, DateTime to) { - final startDate = (from.year == currentYear ? formatMergedSameYear : formatMergedOtherYear).format(from); - final endDate = (to.year == currentYear ? formatMergedSameYear : formatMergedOtherYear).format(to); - if (DateTime(from.year, from.month, from.day) == DateTime(to.year, to.month, to.day)) { - // format range with time when both dates are on the same day - final startTime = DateFormat.Hm().format(from); - final endTime = DateFormat.Hm().format(to); - return "$startDate $startTime - $endTime"; - } - return "$startDate - $endDate"; - } - - void mergeMonth() { - if (last != null && groupBy == GroupAssetsBy.auto && monthCount <= 30 && elements.length > lastMonthIndex + 1) { - // merge all days into a single section - assert(elements[lastMonthIndex].date.month == last.month); - final e = elements[lastMonthIndex]; - - elements[lastMonthIndex] = RenderAssetGridElement( - RenderAssetGridElementType.monthTitle, - date: e.date, - count: monthCount, - totalCount: monthCount, - offset: e.offset, - title: formatDateRange(e.date, elements.last.date), - ); - elements.removeRange(lastMonthIndex + 1, elements.length); - } - } - - void addElems(DateTime d, DateTime? prevDate) { - final bool newMonth = last == null || last.year != d.year || last.month != d.month; - if (newMonth) { - mergeMonth(); - lastMonthIndex = elements.length; - monthCount = 0; - } - for (int j = 0; j < count; j += sectionSize) { - final type = j == 0 - ? (groupBy != GroupAssetsBy.month && newMonth - ? RenderAssetGridElementType.monthTitle - : RenderAssetGridElementType.groupDividerTitle) - : (groupBy == GroupAssetsBy.auto - ? RenderAssetGridElementType.groupDividerTitle - : RenderAssetGridElementType.assets); - final sectionCount = j + sectionSize > count ? count - j : sectionSize; - assert(sectionCount > 0 && sectionCount <= sectionSize); - elements.add( - RenderAssetGridElement( - type, - date: d, - count: sectionCount, - totalCount: groupBy == GroupAssetsBy.auto ? sectionCount : count, - offset: lastOffset + j, - title: j == 0 - ? (d.year == currentYear ? formatSameYear.format(d) : formatOtherYear.format(d)) - : (groupBy == GroupAssetsBy.auto ? formatDateRange(d, prevDate ?? d) : null), - ), - ); - } - monthCount += count; - } - - DateTime? prevDate; - while (true) { - // this iterates all assets (only their createdAt property) in batches - // memory usage is okay, however runtime is linear with number of assets - // TODO replace with groupBy once Isar supports such queries - final dates = assets != null - ? assets.map((a) => a.fileCreatedAt) - : await query!.offset(offset).limit(pageSize).fileCreatedAtProperty().findAll(); - int i = 0; - for (final date in dates) { - final d = DateTime(date.year, date.month, groupBy == GroupAssetsBy.month ? 1 : date.day); - current ??= d; - if (current != d) { - addElems(current, prevDate); - last = current; - current = d; - lastOffset = offset + i; - count = 0; - } - prevDate = date; - count++; - i++; - } - - if (assets != null || dates.length != pageSize) break; - offset += pageSize; - } - if (count > 0 && current != null) { - addElems(current, prevDate); - mergeMonth(); - } - assert(elements.every((e) => e.count <= sectionSize), "too large section"); - return RenderList(elements, query, assets); - } - - static RenderList empty() => RenderList([], null, []); - - static Future fromAssets(List assets, GroupAssetsBy groupBy) => - _buildRenderList(assets, null, groupBy); - - /// Deletes an asset from the render list and clears the buffer - /// This is only a workaround for deleted images still appearing in the gallery - void deleteAsset(Asset deleteAsset) { - allAssets?.remove(deleteAsset); - _buf.clear(); - _bufOffset = 0; - } -} - -class DateBatchLoader { - final QueryBuilder query; - final int batchSize; - - List _buffer = []; - int _bufferStart = 0; - - DateBatchLoader({required this.query, required this.batchSize}); - - Future getDate(int index) async { - if (!_isIndexInBuffer(index)) { - await _loadBatch(index); - } - - if (_isIndexInBuffer(index)) { - return _buffer[index - _bufferStart]; - } - - return null; - } - - Future _loadBatch(int targetIndex) async { - final batchStart = (targetIndex ~/ batchSize) * batchSize; - - _buffer = await query.offset(batchStart).limit(batchSize).fileCreatedAtProperty().findAll(); - - _bufferStart = batchStart; - } - - bool _isIndexInBuffer(int index) { - return index >= _bufferStart && index < _bufferStart + _buffer.length; - } -} diff --git a/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart b/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart deleted file mode 100644 index cd2dc70dae..0000000000 --- a/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart +++ /dev/null @@ -1,388 +0,0 @@ -import 'dart:io'; - -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/routes.provider.dart'; -import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart'; -import 'package:immich_mobile/widgets/album/add_to_album_bottom_sheet.dart'; -import 'package:immich_mobile/models/asset_selection_state.dart'; -import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; -import 'package:immich_mobile/widgets/asset_grid/upload_dialog.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/widgets/common/drag_sheet.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/utils/draggable_scroll_controller.dart'; - -final controlBottomAppBarNotifier = ControlBottomAppBarNotifier(); - -class ControlBottomAppBarNotifier with ChangeNotifier { - void minimize() { - notifyListeners(); - } -} - -class ControlBottomAppBar extends HookConsumerWidget { - final void Function(bool shareLocal) onShare; - final void Function()? onFavorite; - final void Function()? onArchive; - final void Function([bool force])? onDelete; - final void Function([bool force])? onDeleteServer; - final void Function(bool onlyBackedUp)? onDeleteLocal; - final Function(Album album) onAddToAlbum; - final void Function() onCreateNewAlbum; - final void Function() onUpload; - final void Function()? onStack; - final void Function()? onEditTime; - final void Function()? onEditLocation; - final void Function()? onRemoveFromAlbum; - final void Function()? onToggleLocked; - final void Function()? onDownload; - - final bool enabled; - final bool unfavorite; - final bool unarchive; - final AssetSelectionState selectionAssetState; - final List selectedAssets; - - const ControlBottomAppBar({ - super.key, - required this.onShare, - this.onFavorite, - this.onArchive, - this.onDelete, - this.onDeleteServer, - this.onDeleteLocal, - required this.onAddToAlbum, - required this.onCreateNewAlbum, - required this.onUpload, - this.onDownload, - this.onStack, - this.onEditTime, - this.onEditLocation, - this.onRemoveFromAlbum, - this.onToggleLocked, - this.selectionAssetState = const AssetSelectionState(), - this.selectedAssets = const [], - this.enabled = true, - this.unarchive = false, - this.unfavorite = false, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final hasRemote = selectionAssetState.hasRemote || selectionAssetState.hasMerged; - final hasLocal = selectionAssetState.hasLocal || selectionAssetState.hasMerged; - final trashEnabled = ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); - final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList(); - final sharedAlbums = ref.watch(albumProvider).where((a) => a.shared).toList(); - const bottomPadding = 0.24; - final scrollController = useDraggableScrollController(); - final isInLockedView = ref.watch(inLockedViewProvider); - - void minimize() { - scrollController.animateTo(bottomPadding, duration: const Duration(milliseconds: 300), curve: Curves.easeOut); - } - - useEffect(() { - controlBottomAppBarNotifier.addListener(minimize); - return () { - controlBottomAppBarNotifier.removeListener(minimize); - }; - }, []); - - void showForceDeleteDialog(Function(bool) deleteCb, {String? alertMsg}) { - showDialog( - context: context, - builder: (BuildContext context) { - return DeleteDialog(alert: alertMsg, onDelete: () => deleteCb(true)); - }, - ); - } - - /// Show existing AddToAlbumBottomSheet - void showAddToAlbumBottomSheet() { - showModalBottomSheet( - elevation: 0, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(15.0))), - context: context, - builder: (BuildContext _) { - return AddToAlbumBottomSheet(assets: selectedAssets); - }, - ); - } - - void handleRemoteDelete(bool force, Function(bool) deleteCb, {String? alertMsg}) { - if (!force) { - deleteCb(force); - return; - } - return showForceDeleteDialog(deleteCb, alertMsg: alertMsg); - } - - List renderActionButtons() { - return [ - ControlBoxButton( - iconData: Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded, - label: "share".tr(), - onPressed: enabled ? () => onShare(true) : null, - ), - if (!isInLockedView && hasRemote) - ControlBoxButton( - iconData: Icons.link_rounded, - label: "share_link".tr(), - onPressed: enabled ? () => onShare(false) : null, - ), - if (!isInLockedView && hasRemote && albums.isNotEmpty) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 100), - child: ControlBoxButton( - iconData: Icons.photo_album, - label: "add_to_album".tr(), - onPressed: enabled ? showAddToAlbumBottomSheet : null, - ), - ), - if (hasRemote && onArchive != null) - ControlBoxButton( - iconData: unarchive ? Icons.unarchive_outlined : Icons.archive_outlined, - label: (unarchive ? "unarchive" : "archive").tr(), - onPressed: enabled ? onArchive : null, - ), - if (hasRemote && onFavorite != null) - ControlBoxButton( - iconData: unfavorite ? Icons.favorite_border_rounded : Icons.favorite_rounded, - label: (unfavorite ? "unfavorite" : "favorite").tr(), - onPressed: enabled ? onFavorite : null, - ), - if (hasRemote && onDownload != null) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 90), - child: ControlBoxButton(iconData: Icons.download, label: "download".tr(), onPressed: onDownload), - ), - if (hasLocal && hasRemote && onDelete != null && !isInLockedView) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 90), - child: ControlBoxButton( - iconData: Icons.delete_sweep_outlined, - label: "delete".tr(), - onPressed: enabled ? () => handleRemoteDelete(!trashEnabled, onDelete!) : null, - onLongPressed: enabled ? () => showForceDeleteDialog(onDelete!) : null, - ), - ), - if (hasRemote && onDeleteServer != null && !isInLockedView) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 85), - child: ControlBoxButton( - iconData: Icons.cloud_off_outlined, - label: trashEnabled - ? "control_bottom_app_bar_trash_from_immich".tr() - : "control_bottom_app_bar_delete_from_immich".tr(), - onPressed: enabled - ? () => handleRemoteDelete(!trashEnabled, onDeleteServer!, alertMsg: "delete_dialog_alert_remote") - : null, - onLongPressed: enabled - ? () => showForceDeleteDialog(onDeleteServer!, alertMsg: "delete_dialog_alert_remote") - : null, - ), - ), - if (isInLockedView) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 110), - child: ControlBoxButton( - iconData: Icons.delete_forever, - label: "delete_dialog_title".tr(), - onPressed: enabled - ? () => showForceDeleteDialog(onDeleteServer!, alertMsg: "delete_dialog_alert_remote") - : null, - ), - ), - if (hasLocal && onDeleteLocal != null && !isInLockedView) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 95), - child: ControlBoxButton( - iconData: Icons.no_cell_outlined, - label: "control_bottom_app_bar_delete_from_local".tr(), - onPressed: enabled - ? () { - if (!selectionAssetState.hasLocal) { - return onDeleteLocal?.call(true); - } - - showDialog( - context: context, - builder: (BuildContext context) { - return DeleteLocalOnlyDialog(onDeleteLocal: onDeleteLocal!); - }, - ); - } - : null, - ), - ), - if (hasRemote && onEditTime != null) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 95), - child: ControlBoxButton( - iconData: Icons.edit_calendar_outlined, - label: "control_bottom_app_bar_edit_time".tr(), - onPressed: enabled ? onEditTime : null, - ), - ), - if (hasRemote && onEditLocation != null) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 90), - child: ControlBoxButton( - iconData: Icons.edit_location_alt_outlined, - label: "control_bottom_app_bar_edit_location".tr(), - onPressed: enabled ? onEditLocation : null, - ), - ), - if (hasRemote) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 100), - child: ControlBoxButton( - iconData: isInLockedView ? Icons.lock_open_rounded : Icons.lock_outline_rounded, - label: isInLockedView ? "remove_from_locked_folder".tr() : "move_to_locked_folder".tr(), - onPressed: enabled ? onToggleLocked : null, - ), - ), - if (!selectionAssetState.hasLocal && selectionAssetState.selectedCount > 1 && onStack != null) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 90), - child: ControlBoxButton( - iconData: Icons.filter_none_rounded, - label: "stack".tr(), - onPressed: enabled ? onStack : null, - ), - ), - if (onRemoveFromAlbum != null) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 90), - child: ControlBoxButton( - iconData: Icons.remove_circle_outline, - label: 'remove_from_album'.tr(), - onPressed: enabled ? onRemoveFromAlbum : null, - ), - ), - if (selectionAssetState.hasLocal) - ControlBoxButton( - iconData: Icons.backup_outlined, - label: "upload".tr(), - onPressed: enabled - ? () => showDialog( - context: context, - builder: (BuildContext context) { - return UploadDialog(onUpload: onUpload); - }, - ) - : null, - ), - ]; - } - - getInitialSize() { - if (isInLockedView) { - return bottomPadding; - } - if (hasRemote) { - return 0.35; - } - return bottomPadding; - } - - getMaxChildSize() { - if (isInLockedView) { - return bottomPadding; - } - if (hasRemote) { - return 0.65; - } - return bottomPadding; - } - - return DraggableScrollableSheet( - initialChildSize: getInitialSize(), - minChildSize: bottomPadding, - maxChildSize: getMaxChildSize(), - snap: true, - controller: scrollController, - builder: (BuildContext context, ScrollController scrollController) { - return Card( - color: context.colorScheme.surfaceContainerHigh, - surfaceTintColor: context.colorScheme.surfaceContainerHigh, - elevation: 6.0, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only(topLeft: Radius.circular(12), topRight: Radius.circular(12)), - ), - margin: const EdgeInsets.all(0), - child: CustomScrollView( - controller: scrollController, - slivers: [ - SliverToBoxAdapter( - child: Column( - children: [ - const SizedBox(height: 12), - const CustomDraggingHandle(), - const SizedBox(height: 12), - SizedBox( - height: 120, - child: ListView( - shrinkWrap: true, - scrollDirection: Axis.horizontal, - children: renderActionButtons(), - ), - ), - if (hasRemote && !isInLockedView) ...[ - const Divider(indent: 16, endIndent: 16, thickness: 1), - _AddToAlbumTitleRow(onCreateNewAlbum: enabled ? onCreateNewAlbum : null), - ], - ], - ), - ), - if (hasRemote && !isInLockedView) - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16), - sliver: AddToAlbumSliverList( - albums: albums, - sharedAlbums: sharedAlbums, - onAddToAlbum: onAddToAlbum, - enabled: enabled, - ), - ), - ], - ), - ); - }, - ); - } -} - -class _AddToAlbumTitleRow extends StatelessWidget { - const _AddToAlbumTitleRow({required this.onCreateNewAlbum}); - - final VoidCallback? onCreateNewAlbum; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("add_to_album", style: context.textTheme.titleSmall).tr(), - TextButton.icon( - onPressed: onCreateNewAlbum, - icon: Icon(Icons.add, color: context.primaryColor), - label: Text( - "common_create_new_album", - style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 14), - ).tr(), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/delete_dialog.dart b/mobile/lib/widgets/asset_grid/delete_dialog.dart index adb22889a8..ff5aac617a 100644 --- a/mobile/lib/widgets/asset_grid/delete_dialog.dart +++ b/mobile/lib/widgets/asset_grid/delete_dialog.dart @@ -1,18 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; - -class DeleteDialog extends ConfirmDialog { - const DeleteDialog({super.key, String? alert, required Function onDelete}) - : super( - title: "delete_dialog_title", - content: alert ?? "delete_dialog_alert", - cancel: "cancel", - ok: "delete", - onOk: onDelete, - ); -} class DeleteLocalOnlyDialog extends StatelessWidget { final void Function(bool onlyMerged) onDeleteLocal; diff --git a/mobile/lib/widgets/asset_grid/disable_multi_select_button.dart b/mobile/lib/widgets/asset_grid/disable_multi_select_button.dart deleted file mode 100644 index 93a1d53f4e..0000000000 --- a/mobile/lib/widgets/asset_grid/disable_multi_select_button.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; - -class DisableMultiSelectButton extends ConsumerWidget { - const DisableMultiSelectButton({super.key, required this.onPressed, required this.selectedItemCount}); - - final Function onPressed; - final int selectedItemCount; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Align( - alignment: Alignment.topLeft, - child: Padding( - padding: const EdgeInsets.only(left: 16.0, top: 8.0), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: ElevatedButton.icon( - onPressed: () => onPressed(), - icon: Icon(Icons.close_rounded, color: context.colorScheme.onPrimary), - label: Text( - '$selectedItemCount', - style: context.textTheme.titleMedium?.copyWith(height: 2.5, color: context.colorScheme.onPrimary), - ), - ), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/draggable_scrollbar.dart b/mobile/lib/widgets/asset_grid/draggable_scrollbar.dart deleted file mode 100644 index 3de52c2816..0000000000 --- a/mobile/lib/widgets/asset_grid/draggable_scrollbar.dart +++ /dev/null @@ -1,559 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; - -/// Build the Scroll Thumb and label using the current configuration -typedef ScrollThumbBuilder = - Widget Function( - Color backgroundColor, - Animation thumbAnimation, - Animation labelAnimation, - double height, { - Text? labelText, - BoxConstraints? labelConstraints, - }); - -/// Build a Text widget using the current scroll offset -typedef LabelTextBuilder = Text Function(double offsetY); - -/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged -/// for quick navigation of the BoxScrollView. -class DraggableScrollbar extends StatefulWidget { - /// The view that will be scrolled with the scroll thumb - final CustomScrollView child; - - /// A function that builds a thumb using the current configuration - final ScrollThumbBuilder scrollThumbBuilder; - - /// The height of the scroll thumb - final double heightScrollThumb; - - /// The background color of the label and thumb - final Color backgroundColor; - - /// The amount of padding that should surround the thumb - final EdgeInsetsGeometry? padding; - - /// Determines how quickly the scrollbar will animate in and out - final Duration scrollbarAnimationDuration; - - /// How long should the thumb be visible before fading out - final Duration scrollbarTimeToFade; - - /// Build a Text widget from the current offset in the BoxScrollView - final LabelTextBuilder? labelTextBuilder; - - /// Determines box constraints for Container displaying label - final BoxConstraints? labelConstraints; - - /// The ScrollController for the BoxScrollView - final ScrollController controller; - - /// Determines scrollThumb displaying. If you draw own ScrollThumb and it is true you just don't need to use animation parameters in [scrollThumbBuilder] - final bool alwaysVisibleScrollThumb; - - DraggableScrollbar({ - super.key, - this.alwaysVisibleScrollThumb = false, - required this.heightScrollThumb, - required this.backgroundColor, - required this.scrollThumbBuilder, - required this.child, - required this.controller, - this.padding, - this.scrollbarAnimationDuration = const Duration(milliseconds: 300), - this.scrollbarTimeToFade = const Duration(milliseconds: 600), - this.labelTextBuilder, - this.labelConstraints, - }) : assert(child.scrollDirection == Axis.vertical); - - DraggableScrollbar.rrect({ - super.key, - Key? scrollThumbKey, - this.alwaysVisibleScrollThumb = false, - required this.child, - required this.controller, - this.heightScrollThumb = 48.0, - this.backgroundColor = Colors.white, - this.padding, - this.scrollbarAnimationDuration = const Duration(milliseconds: 300), - this.scrollbarTimeToFade = const Duration(milliseconds: 600), - this.labelTextBuilder, - this.labelConstraints, - }) : assert(child.scrollDirection == Axis.vertical), - scrollThumbBuilder = _thumbRRectBuilder(alwaysVisibleScrollThumb); - - DraggableScrollbar.arrows({ - super.key, - Key? scrollThumbKey, - this.alwaysVisibleScrollThumb = false, - required this.child, - required this.controller, - this.heightScrollThumb = 48.0, - this.backgroundColor = Colors.white, - this.padding, - this.scrollbarAnimationDuration = const Duration(milliseconds: 300), - this.scrollbarTimeToFade = const Duration(milliseconds: 600), - this.labelTextBuilder, - this.labelConstraints, - }) : assert(child.scrollDirection == Axis.vertical), - scrollThumbBuilder = _thumbArrowBuilder(alwaysVisibleScrollThumb); - - DraggableScrollbar.semicircle({ - super.key, - Key? scrollThumbKey, - this.alwaysVisibleScrollThumb = false, - required this.child, - required this.controller, - this.heightScrollThumb = 48.0, - this.backgroundColor = Colors.white, - this.padding, - this.scrollbarAnimationDuration = const Duration(milliseconds: 300), - this.scrollbarTimeToFade = const Duration(milliseconds: 600), - this.labelTextBuilder, - this.labelConstraints, - }) : assert(child.scrollDirection == Axis.vertical), - scrollThumbBuilder = _thumbSemicircleBuilder(heightScrollThumb * 0.6, scrollThumbKey, alwaysVisibleScrollThumb); - - @override - DraggableScrollbarState createState() => DraggableScrollbarState(); - - static buildScrollThumbAndLabel({ - required Widget scrollThumb, - required Color backgroundColor, - required Animation? thumbAnimation, - required Animation? labelAnimation, - required Text? labelText, - required BoxConstraints? labelConstraints, - required bool alwaysVisibleScrollThumb, - }) { - var scrollThumbAndLabel = labelText == null - ? scrollThumb - : Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ScrollLabel( - animation: labelAnimation, - backgroundColor: backgroundColor, - constraints: labelConstraints, - child: labelText, - ), - scrollThumb, - ], - ); - - if (alwaysVisibleScrollThumb) { - return scrollThumbAndLabel; - } - return SlideFadeTransition(animation: thumbAnimation!, child: scrollThumbAndLabel); - } - - static ScrollThumbBuilder _thumbSemicircleBuilder(double width, Key? scrollThumbKey, bool alwaysVisibleScrollThumb) { - return ( - Color backgroundColor, - Animation thumbAnimation, - Animation labelAnimation, - double height, { - Text? labelText, - BoxConstraints? labelConstraints, - }) { - final scrollThumb = CustomPaint( - key: scrollThumbKey, - foregroundPainter: ArrowCustomPainter(Colors.white), - child: Material( - elevation: 4.0, - color: backgroundColor, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(height), - bottomLeft: Radius.circular(height), - topRight: const Radius.circular(4.0), - bottomRight: const Radius.circular(4.0), - ), - child: Container(constraints: BoxConstraints.tight(Size(width, height))), - ), - ); - - return buildScrollThumbAndLabel( - scrollThumb: scrollThumb, - backgroundColor: backgroundColor, - thumbAnimation: thumbAnimation, - labelAnimation: labelAnimation, - labelText: labelText, - labelConstraints: labelConstraints, - alwaysVisibleScrollThumb: alwaysVisibleScrollThumb, - ); - }; - } - - static ScrollThumbBuilder _thumbArrowBuilder(bool alwaysVisibleScrollThumb) { - return ( - Color backgroundColor, - Animation thumbAnimation, - Animation labelAnimation, - double height, { - Text? labelText, - BoxConstraints? labelConstraints, - }) { - final scrollThumb = ClipPath( - clipper: const ArrowClipper(), - child: Container( - height: height, - width: 20.0, - decoration: BoxDecoration( - color: backgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(12.0)), - ), - ), - ); - - return buildScrollThumbAndLabel( - scrollThumb: scrollThumb, - backgroundColor: backgroundColor, - thumbAnimation: thumbAnimation, - labelAnimation: labelAnimation, - labelText: labelText, - labelConstraints: labelConstraints, - alwaysVisibleScrollThumb: alwaysVisibleScrollThumb, - ); - }; - } - - static ScrollThumbBuilder _thumbRRectBuilder(bool alwaysVisibleScrollThumb) { - return ( - Color backgroundColor, - Animation thumbAnimation, - Animation labelAnimation, - double height, { - Text? labelText, - BoxConstraints? labelConstraints, - }) { - final scrollThumb = Material( - elevation: 4.0, - color: backgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(7.0)), - child: Container(constraints: BoxConstraints.tight(Size(16.0, height))), - ); - - return buildScrollThumbAndLabel( - scrollThumb: scrollThumb, - backgroundColor: backgroundColor, - thumbAnimation: thumbAnimation, - labelAnimation: labelAnimation, - labelText: labelText, - labelConstraints: labelConstraints, - alwaysVisibleScrollThumb: alwaysVisibleScrollThumb, - ); - }; - } -} - -class ScrollLabel extends StatelessWidget { - final Animation? animation; - final Color backgroundColor; - final Text child; - - final BoxConstraints? constraints; - static const BoxConstraints _defaultConstraints = BoxConstraints.tightFor(width: 72.0, height: 28.0); - - const ScrollLabel({ - super.key, - required this.child, - required this.animation, - required this.backgroundColor, - this.constraints = _defaultConstraints, - }); - - @override - Widget build(BuildContext context) { - return FadeTransition( - opacity: animation!, - child: Container( - margin: const EdgeInsets.only(right: 12.0), - child: Material( - elevation: 4.0, - color: backgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(16.0)), - child: Container(constraints: constraints ?? _defaultConstraints, alignment: Alignment.center, child: child), - ), - ), - ); - } -} - -class DraggableScrollbarState extends State with TickerProviderStateMixin { - late double _barOffset; - late double _viewOffset; - late bool _isDragInProcess; - - late AnimationController _thumbAnimationController; - late Animation _thumbAnimation; - late AnimationController _labelAnimationController; - late Animation _labelAnimation; - Timer? _fadeoutTimer; - - @override - void initState() { - super.initState(); - _barOffset = 0.0; - _viewOffset = 0.0; - _isDragInProcess = false; - - _thumbAnimationController = AnimationController(vsync: this, duration: widget.scrollbarAnimationDuration); - - _thumbAnimation = CurvedAnimation(parent: _thumbAnimationController, curve: Curves.fastOutSlowIn); - - _labelAnimationController = AnimationController(vsync: this, duration: widget.scrollbarAnimationDuration); - - _labelAnimation = CurvedAnimation(parent: _labelAnimationController, curve: Curves.fastOutSlowIn); - } - - @override - void dispose() { - _thumbAnimationController.dispose(); - _labelAnimationController.dispose(); - _fadeoutTimer?.cancel(); - super.dispose(); - } - - double get barMaxScrollExtent => context.size!.height - widget.heightScrollThumb; - - double get barMinScrollExtent => 0; - - double get viewMaxScrollExtent => widget.controller.position.maxScrollExtent; - - double get viewMinScrollExtent => widget.controller.position.minScrollExtent; - - @override - Widget build(BuildContext context) { - Text? labelText; - if (widget.labelTextBuilder != null && _isDragInProcess) { - labelText = widget.labelTextBuilder!(_viewOffset + _barOffset + widget.heightScrollThumb / 2); - } - - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - //print("LayoutBuilder constraints=$constraints"); - - return NotificationListener( - onNotification: (ScrollNotification notification) { - changePosition(notification); - return false; - }, - child: Stack( - children: [ - RepaintBoundary(child: widget.child), - RepaintBoundary( - child: GestureDetector( - onVerticalDragStart: _onVerticalDragStart, - onVerticalDragUpdate: _onVerticalDragUpdate, - onVerticalDragEnd: _onVerticalDragEnd, - child: Container( - alignment: Alignment.topRight, - margin: EdgeInsets.only(top: _barOffset), - padding: widget.padding, - child: widget.scrollThumbBuilder( - widget.backgroundColor, - _thumbAnimation, - _labelAnimation, - widget.heightScrollThumb, - labelText: labelText, - labelConstraints: widget.labelConstraints, - ), - ), - ), - ), - ], - ), - ); - }, - ); - } - - //scroll bar has received notification that it's view was scrolled - //so it should also changes his position - //but only if it isn't dragged - changePosition(ScrollNotification notification) { - if (_isDragInProcess) { - return; - } - - setState(() { - if (notification is ScrollUpdateNotification) { - _barOffset += getBarDelta(notification.scrollDelta!, barMaxScrollExtent, viewMaxScrollExtent); - - if (_barOffset < barMinScrollExtent) { - _barOffset = barMinScrollExtent; - } - if (_barOffset > barMaxScrollExtent) { - _barOffset = barMaxScrollExtent; - } - - _viewOffset += notification.scrollDelta!; - if (_viewOffset < widget.controller.position.minScrollExtent) { - _viewOffset = widget.controller.position.minScrollExtent; - } - if (_viewOffset > viewMaxScrollExtent) { - _viewOffset = viewMaxScrollExtent; - } - } - - if (notification is ScrollUpdateNotification || notification is OverscrollNotification) { - if (_thumbAnimationController.status != AnimationStatus.forward) { - _thumbAnimationController.forward(); - } - - _fadeoutTimer?.cancel(); - _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { - _thumbAnimationController.reverse(); - _labelAnimationController.reverse(); - _fadeoutTimer = null; - }); - } - }); - } - - double getBarDelta(double scrollViewDelta, double barMaxScrollExtent, double viewMaxScrollExtent) { - return scrollViewDelta * barMaxScrollExtent / viewMaxScrollExtent; - } - - double getScrollViewDelta(double barDelta, double barMaxScrollExtent, double viewMaxScrollExtent) { - return barDelta * viewMaxScrollExtent / barMaxScrollExtent; - } - - void _onVerticalDragStart(DragStartDetails details) { - setState(() { - _isDragInProcess = true; - _labelAnimationController.forward(); - _fadeoutTimer?.cancel(); - }); - } - - void _onVerticalDragUpdate(DragUpdateDetails details) { - setState(() { - if (_thumbAnimationController.status != AnimationStatus.forward) { - _thumbAnimationController.forward(); - } - if (_isDragInProcess) { - _barOffset += details.delta.dy; - - if (_barOffset < barMinScrollExtent) { - _barOffset = barMinScrollExtent; - } - if (_barOffset > barMaxScrollExtent) { - _barOffset = barMaxScrollExtent; - } - - double viewDelta = getScrollViewDelta(details.delta.dy, barMaxScrollExtent, viewMaxScrollExtent); - - _viewOffset = widget.controller.position.pixels + viewDelta; - if (_viewOffset < widget.controller.position.minScrollExtent) { - _viewOffset = widget.controller.position.minScrollExtent; - } - if (_viewOffset > viewMaxScrollExtent) { - _viewOffset = viewMaxScrollExtent; - } - widget.controller.jumpTo(_viewOffset); - } - }); - } - - void _onVerticalDragEnd(DragEndDetails details) { - _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { - _thumbAnimationController.reverse(); - _labelAnimationController.reverse(); - _fadeoutTimer = null; - }); - setState(() { - _isDragInProcess = false; - }); - } -} - -/// Draws 2 triangles like arrow up and arrow down -class ArrowCustomPainter extends CustomPainter { - Color color; - - ArrowCustomPainter(this.color); - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint()..color = color; - const width = 12.0; - const height = 8.0; - final baseX = size.width / 2; - final baseY = size.height / 2; - - canvas.drawPath(_trianglePath(Offset(baseX, baseY - 2.0), width, height, true), paint); - canvas.drawPath(_trianglePath(Offset(baseX, baseY + 2.0), width, height, false), paint); - } - - static Path _trianglePath(Offset o, double width, double height, bool isUp) { - return Path() - ..moveTo(o.dx, o.dy) - ..lineTo(o.dx + width, o.dy) - ..lineTo(o.dx + (width / 2), isUp ? o.dy - height : o.dy + height) - ..close(); - } -} - -///This cut 2 lines in arrow shape -class ArrowClipper extends CustomClipper { - const ArrowClipper(); - @override - Path getClip(Size size) { - Path path = Path(); - path.lineTo(0.0, size.height); - path.lineTo(size.width, size.height); - path.lineTo(size.width, 0.0); - path.lineTo(0.0, 0.0); - path.close(); - - double arrowWidth = 8.0; - double startPointX = (size.width - arrowWidth) / 2; - double startPointY = size.height / 2 - arrowWidth / 2; - path.moveTo(startPointX, startPointY); - path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2); - path.lineTo(startPointX + arrowWidth, startPointY); - path.lineTo(startPointX + arrowWidth, startPointY + 1.0); - path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2 + 1.0); - path.lineTo(startPointX, startPointY + 1.0); - path.close(); - - startPointY = size.height / 2 + arrowWidth / 2; - path.moveTo(startPointX + arrowWidth, startPointY); - path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2); - path.lineTo(startPointX, startPointY); - path.lineTo(startPointX, startPointY - 1.0); - path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2 - 1.0); - path.lineTo(startPointX + arrowWidth, startPointY - 1.0); - path.close(); - - return path; - } - - @override - bool shouldReclip(CustomClipper oldClipper) => false; -} - -class SlideFadeTransition extends StatelessWidget { - final Animation animation; - final Widget child; - - const SlideFadeTransition({super.key, required this.animation, required this.child}); - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: animation, - builder: (context, child) => animation.value == 0.0 ? const SizedBox() : child!, - child: SlideTransition( - position: Tween(begin: const Offset(0.3, 0.0), end: const Offset(0.0, 0.0)).animate(animation), - child: FadeTransition(opacity: animation, child: child), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/draggable_scrollbar_custom.dart b/mobile/lib/widgets/asset_grid/draggable_scrollbar_custom.dart deleted file mode 100644 index 17f35311f0..0000000000 --- a/mobile/lib/widgets/asset_grid/draggable_scrollbar_custom.dart +++ /dev/null @@ -1,490 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; - -/// Build the Scroll Thumb and label using the current configuration -typedef ScrollThumbBuilder = - Widget Function( - Color backgroundColor, - Animation thumbAnimation, - Animation labelAnimation, - double height, { - Text? labelText, - BoxConstraints? labelConstraints, - }); - -/// Build a Text widget using the current scroll offset -typedef LabelTextBuilder = Text Function(int item); - -/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged -/// for quick navigation of the BoxScrollView. -class DraggableScrollbar extends StatefulWidget { - /// The view that will be scrolled with the scroll thumb - final ScrollablePositionedList child; - - final ItemPositionsListener itemPositionsListener; - - /// A function that builds a thumb using the current configuration - final ScrollThumbBuilder scrollThumbBuilder; - - /// The height of the scroll thumb - final double heightScrollThumb; - - /// The background color of the label and thumb - final Color backgroundColor; - - /// The amount of padding that should surround the thumb - final EdgeInsetsGeometry? padding; - - /// The height offset of the thumb/bar from the bottom of the page - final double? heightOffset; - - /// Determines how quickly the scrollbar will animate in and out - final Duration scrollbarAnimationDuration; - - /// How long should the thumb be visible before fading out - final Duration scrollbarTimeToFade; - - /// Build a Text widget from the current offset in the BoxScrollView - final LabelTextBuilder? labelTextBuilder; - - /// Determines box constraints for Container displaying label - final BoxConstraints? labelConstraints; - - /// The ScrollController for the BoxScrollView - final ItemScrollController controller; - - /// Determines scrollThumb displaying. If you draw own ScrollThumb and it is true you just don't need to use animation parameters in [scrollThumbBuilder] - final bool alwaysVisibleScrollThumb; - - final Function(bool scrolling) scrollStateListener; - - DraggableScrollbar.semicircle({ - super.key, - Key? scrollThumbKey, - this.alwaysVisibleScrollThumb = false, - required this.child, - required this.controller, - required this.itemPositionsListener, - required this.scrollStateListener, - this.heightScrollThumb = 48.0, - this.backgroundColor = Colors.white, - this.padding, - this.heightOffset, - this.scrollbarAnimationDuration = const Duration(milliseconds: 300), - this.scrollbarTimeToFade = const Duration(milliseconds: 600), - this.labelTextBuilder, - this.labelConstraints, - }) : assert(child.scrollDirection == Axis.vertical), - scrollThumbBuilder = _thumbSemicircleBuilder(heightScrollThumb * 0.6, scrollThumbKey, alwaysVisibleScrollThumb); - - @override - DraggableScrollbarState createState() => DraggableScrollbarState(); - - static buildScrollThumbAndLabel({ - required Widget scrollThumb, - required Color backgroundColor, - required Animation? thumbAnimation, - required Animation? labelAnimation, - required Text? labelText, - required BoxConstraints? labelConstraints, - required bool alwaysVisibleScrollThumb, - }) { - var scrollThumbAndLabel = labelText == null - ? scrollThumb - : Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ScrollLabel( - animation: labelAnimation, - backgroundColor: backgroundColor, - constraints: labelConstraints, - child: labelText, - ), - scrollThumb, - ], - ); - - if (alwaysVisibleScrollThumb) { - return scrollThumbAndLabel; - } - return SlideFadeTransition(animation: thumbAnimation!, child: scrollThumbAndLabel); - } - - static ScrollThumbBuilder _thumbSemicircleBuilder(double width, Key? scrollThumbKey, bool alwaysVisibleScrollThumb) { - return ( - Color backgroundColor, - Animation thumbAnimation, - Animation labelAnimation, - double height, { - Text? labelText, - BoxConstraints? labelConstraints, - }) { - final scrollThumb = CustomPaint( - key: scrollThumbKey, - foregroundPainter: ArrowCustomPainter(Colors.white), - child: Material( - elevation: 4.0, - color: backgroundColor, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(height), - bottomLeft: Radius.circular(height), - topRight: const Radius.circular(4.0), - bottomRight: const Radius.circular(4.0), - ), - child: Container(constraints: BoxConstraints.tight(Size(width, height))), - ), - ); - - return buildScrollThumbAndLabel( - scrollThumb: scrollThumb, - backgroundColor: backgroundColor, - thumbAnimation: thumbAnimation, - labelAnimation: labelAnimation, - labelText: labelText, - labelConstraints: labelConstraints, - alwaysVisibleScrollThumb: alwaysVisibleScrollThumb, - ); - }; - } -} - -class ScrollLabel extends StatelessWidget { - final Animation? animation; - final Color backgroundColor; - final Text child; - - final BoxConstraints? constraints; - static const BoxConstraints _defaultConstraints = BoxConstraints.tightFor(width: 72.0, height: 28.0); - - const ScrollLabel({ - super.key, - required this.child, - required this.animation, - required this.backgroundColor, - this.constraints = _defaultConstraints, - }); - - @override - Widget build(BuildContext context) { - return FadeTransition( - opacity: animation!, - child: Container( - margin: const EdgeInsets.only(right: 12.0), - child: Material( - elevation: 4.0, - color: backgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(16.0)), - child: Container( - constraints: constraints ?? _defaultConstraints, - padding: const EdgeInsets.symmetric(horizontal: 10.0), - alignment: Alignment.center, - child: child, - ), - ), - ), - ); - } -} - -class DraggableScrollbarState extends State with TickerProviderStateMixin { - late double _barOffset; - late bool _isDragInProcess; - late int _currentItem; - - late AnimationController _thumbAnimationController; - late Animation _thumbAnimation; - late AnimationController _labelAnimationController; - late Animation _labelAnimation; - Timer? _fadeoutTimer; - - @override - void initState() { - super.initState(); - _barOffset = 0.0; - _isDragInProcess = false; - _currentItem = 0; - - _thumbAnimationController = AnimationController(vsync: this, duration: widget.scrollbarAnimationDuration); - - _thumbAnimation = CurvedAnimation(parent: _thumbAnimationController, curve: Curves.fastOutSlowIn); - - _labelAnimationController = AnimationController(vsync: this, duration: widget.scrollbarAnimationDuration); - - _labelAnimation = CurvedAnimation(parent: _labelAnimationController, curve: Curves.fastOutSlowIn); - } - - @override - void dispose() { - _thumbAnimationController.dispose(); - _labelAnimationController.dispose(); - _fadeoutTimer?.cancel(); - super.dispose(); - } - - double get barMaxScrollExtent => (context.size?.height ?? 0) - widget.heightScrollThumb - (widget.heightOffset ?? 0); - - double get barMinScrollExtent => 0; - - int get maxItemCount => widget.child.itemCount; - - @override - Widget build(BuildContext context) { - Text? labelText; - if (widget.labelTextBuilder != null && _isDragInProcess) { - labelText = widget.labelTextBuilder!(_currentItem); - } - - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - //print("LayoutBuilder constraints=$constraints"); - - return NotificationListener( - onNotification: (ScrollNotification notification) { - changePosition(notification); - return false; - }, - child: Stack( - children: [ - RepaintBoundary(child: widget.child), - RepaintBoundary( - child: GestureDetector( - onVerticalDragStart: _onVerticalDragStart, - onVerticalDragUpdate: _onVerticalDragUpdate, - onVerticalDragEnd: _onVerticalDragEnd, - child: Container( - alignment: Alignment.topRight, - margin: EdgeInsets.only(top: _barOffset), - padding: widget.padding, - child: widget.scrollThumbBuilder( - widget.backgroundColor, - _thumbAnimation, - _labelAnimation, - widget.heightScrollThumb, - labelText: labelText, - labelConstraints: widget.labelConstraints, - ), - ), - ), - ), - ], - ), - ); - }, - ); - } - - // scroll bar has received notification that it's view was scrolled - // so it should also changes his position - // but only if it isn't dragged - changePosition(ScrollNotification notification) { - if (_isDragInProcess) { - return; - } - - setState(() { - try { - int firstItemIndex = widget.itemPositionsListener.itemPositions.value.first.index; - - if (notification is ScrollUpdateNotification) { - _barOffset = (firstItemIndex / maxItemCount) * barMaxScrollExtent; - - if (_barOffset < barMinScrollExtent) { - _barOffset = barMinScrollExtent; - } - if (_barOffset > barMaxScrollExtent) { - _barOffset = barMaxScrollExtent; - } - } - - if (notification is ScrollUpdateNotification || notification is OverscrollNotification) { - if (_thumbAnimationController.status != AnimationStatus.forward) { - _thumbAnimationController.forward(); - } - - if (itemPosition < maxItemCount) { - _currentItem = itemPosition; - } - - _fadeoutTimer?.cancel(); - _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { - _thumbAnimationController.reverse(); - _labelAnimationController.reverse(); - _fadeoutTimer = null; - }); - } - } catch (_) {} - }); - } - - void _onVerticalDragStart(DragStartDetails details) { - setState(() { - _isDragInProcess = true; - _labelAnimationController.forward(); - _fadeoutTimer?.cancel(); - }); - - widget.scrollStateListener(true); - } - - int get itemPosition { - int numberOfItems = widget.child.itemCount; - return ((_barOffset / barMaxScrollExtent) * numberOfItems).toInt(); - } - - void _jumpToBarPosition() { - if (itemPosition > maxItemCount - 1) { - return; - } - - _currentItem = itemPosition; - - /// If the bar is at the bottom but the item position is still smaller than the max item count (due to rounding error) - /// jump to the end of the list - if (barMaxScrollExtent - _barOffset < 10 && itemPosition < maxItemCount) { - widget.controller.jumpTo(index: maxItemCount); - - return; - } - - widget.controller.jumpTo(index: itemPosition); - } - - Timer? dragHaltTimer; - int lastTimerPosition = 0; - - void _onVerticalDragUpdate(DragUpdateDetails details) { - setState(() { - if (_thumbAnimationController.status != AnimationStatus.forward) { - _thumbAnimationController.forward(); - } - if (_isDragInProcess) { - _barOffset += details.delta.dy; - - if (_barOffset < barMinScrollExtent) { - _barOffset = barMinScrollExtent; - } - if (_barOffset > barMaxScrollExtent) { - _barOffset = barMaxScrollExtent; - } - - if (itemPosition != lastTimerPosition) { - lastTimerPosition = itemPosition; - dragHaltTimer?.cancel(); - widget.scrollStateListener(true); - - dragHaltTimer = Timer(const Duration(milliseconds: 500), () { - widget.scrollStateListener(false); - }); - } - - _jumpToBarPosition(); - } - }); - } - - void _onVerticalDragEnd(DragEndDetails details) { - _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { - _thumbAnimationController.reverse(); - _labelAnimationController.reverse(); - _fadeoutTimer = null; - }); - - setState(() { - _jumpToBarPosition(); - _isDragInProcess = false; - }); - - widget.scrollStateListener(false); - } -} - -/// Draws 2 triangles like arrow up and arrow down -class ArrowCustomPainter extends CustomPainter { - Color color; - - ArrowCustomPainter(this.color); - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint()..color = color; - const width = 12.0; - const height = 8.0; - final baseX = size.width / 2; - final baseY = size.height / 2; - - canvas.drawPath(_trianglePath(Offset(baseX, baseY - 2.0), width, height, true), paint); - canvas.drawPath(_trianglePath(Offset(baseX, baseY + 2.0), width, height, false), paint); - } - - static Path _trianglePath(Offset o, double width, double height, bool isUp) { - return Path() - ..moveTo(o.dx, o.dy) - ..lineTo(o.dx + width, o.dy) - ..lineTo(o.dx + (width / 2), isUp ? o.dy - height : o.dy + height) - ..close(); - } -} - -///This cut 2 lines in arrow shape -class ArrowClipper extends CustomClipper { - const ArrowClipper(); - @override - Path getClip(Size size) { - Path path = Path(); - path.lineTo(0.0, size.height); - path.lineTo(size.width, size.height); - path.lineTo(size.width, 0.0); - path.lineTo(0.0, 0.0); - path.close(); - - double arrowWidth = 8.0; - double startPointX = (size.width - arrowWidth) / 2; - double startPointY = size.height / 2 - arrowWidth / 2; - path.moveTo(startPointX, startPointY); - path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2); - path.lineTo(startPointX + arrowWidth, startPointY); - path.lineTo(startPointX + arrowWidth, startPointY + 1.0); - path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2 + 1.0); - path.lineTo(startPointX, startPointY + 1.0); - path.close(); - - startPointY = size.height / 2 + arrowWidth / 2; - path.moveTo(startPointX + arrowWidth, startPointY); - path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2); - path.lineTo(startPointX, startPointY); - path.lineTo(startPointX, startPointY - 1.0); - path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2 - 1.0); - path.lineTo(startPointX + arrowWidth, startPointY - 1.0); - path.close(); - - return path; - } - - @override - bool shouldReclip(CustomClipper oldClipper) => false; -} - -class SlideFadeTransition extends StatelessWidget { - final Animation animation; - final Widget child; - - const SlideFadeTransition({super.key, required this.animation, required this.child}); - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: animation, - builder: (context, child) => animation.value == 0.0 ? const SizedBox() : child!, - child: SlideTransition( - position: Tween(begin: const Offset(0.3, 0.0), end: const Offset(0.0, 0.0)).animate(animation), - child: FadeTransition(opacity: animation, child: child), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/group_divider_title.dart b/mobile/lib/widgets/asset_grid/group_divider_title.dart deleted file mode 100644 index 1464c941f0..0000000000 --- a/mobile/lib/widgets/asset_grid/group_divider_title.dart +++ /dev/null @@ -1,84 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; - -class GroupDividerTitle extends HookConsumerWidget { - const GroupDividerTitle({ - super.key, - required this.text, - required this.multiselectEnabled, - required this.onSelect, - required this.onDeselect, - required this.selected, - }); - - final String text; - final bool multiselectEnabled; - final Function onSelect; - final Function onDeselect; - final bool selected; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final appSettingService = ref.watch(appSettingsServiceProvider); - final groupBy = useState(GroupAssetsBy.day); - - useEffect(() { - groupBy.value = GroupAssetsBy.values[appSettingService.getSetting(AppSettingsEnum.groupAssetsBy)]; - return null; - }, []); - - void handleTitleIconClick() { - ref.read(hapticFeedbackProvider.notifier).heavyImpact(); - if (selected) { - onDeselect(); - } else { - onSelect(); - } - } - - return Padding( - padding: EdgeInsets.only( - top: groupBy.value == GroupAssetsBy.month ? 32.0 : 16.0, - bottom: 16.0, - left: 12.0, - right: 12.0, - ), - child: Row( - children: [ - Text( - text, - style: groupBy.value == GroupAssetsBy.month - ? context.textTheme.bodyLarge?.copyWith(fontSize: 24.0) - : context.textTheme.labelLarge?.copyWith( - color: context.textTheme.labelLarge?.color?.withAlpha(250), - fontWeight: FontWeight.w500, - ), - ), - const Spacer(), - GestureDetector( - onTap: handleTitleIconClick, - child: multiselectEnabled && selected - ? Icon( - Icons.check_circle_rounded, - color: context.primaryColor, - semanticLabel: "unselect_all_in".tr(namedArgs: {"group": text}), - ) - : Icon( - Icons.check_circle_outline_rounded, - color: context.colorScheme.onSurfaceSecondary, - semanticLabel: "select_all_in".tr(namedArgs: {"group": text}), - ), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/immich_asset_grid.dart b/mobile/lib/widgets/asset_grid/immich_asset_grid.dart deleted file mode 100644 index ab6b350a7b..0000000000 --- a/mobile/lib/widgets/asset_grid/immich_asset_grid.dart +++ /dev/null @@ -1,135 +0,0 @@ -import 'dart:math'; - -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid_view.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; - -class ImmichAssetGrid extends HookConsumerWidget { - final int? assetsPerRow; - final double margin; - final bool? showStorageIndicator; - final ImmichAssetGridSelectionListener? listener; - final bool selectionActive; - final List? assets; - final RenderList? renderList; - final Future Function()? onRefresh; - final Set? preselectedAssets; - final bool canDeselect; - final bool? dynamicLayout; - final bool showMultiSelectIndicator; - final void Function(Iterable itemPositions)? visibleItemsListener; - final Widget? topWidget; - final bool shrinkWrap; - final bool showDragScroll; - final bool showDragScrollLabel; - final bool showStack; - - const ImmichAssetGrid({ - super.key, - this.assets, - this.onRefresh, - this.renderList, - this.assetsPerRow, - this.showStorageIndicator, - this.listener, - this.margin = 2.0, - this.selectionActive = false, - this.preselectedAssets, - this.canDeselect = true, - this.dynamicLayout, - this.showMultiSelectIndicator = true, - this.visibleItemsListener, - this.topWidget, - this.shrinkWrap = false, - this.showDragScroll = true, - this.showDragScrollLabel = true, - this.showStack = false, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - var settings = ref.watch(appSettingsServiceProvider); - - final perRow = useState(assetsPerRow ?? settings.getSetting(AppSettingsEnum.tilesPerRow)!); - final scaleFactor = useState(7.0 - perRow.value); - final baseScaleFactor = useState(7.0 - perRow.value); - - /// assets need different hero tags across tabs / modals - /// otherwise, hero animations are performed across tabs (looks buggy!) - int heroOffset() { - const int range = 1152921504606846976; // 2^60 - final tabScope = TabsRouterScope.of(context); - if (tabScope != null) { - final int tabIndex = tabScope.controller.activeIndex; - return tabIndex * range; - } - return range * 7; - } - - Widget buildAssetGridView(RenderList renderList) { - return RawGestureDetector( - gestures: { - CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers( - () => CustomScaleGestureRecognizer(), - (CustomScaleGestureRecognizer scale) { - scale.onStart = (details) { - baseScaleFactor.value = scaleFactor.value; - }; - - scale.onUpdate = (details) { - scaleFactor.value = max(min(5.0, baseScaleFactor.value * details.scale), 1.0); - if (7 - scaleFactor.value.toInt() != perRow.value) { - perRow.value = 7 - scaleFactor.value.toInt(); - settings.setSetting(AppSettingsEnum.tilesPerRow, perRow.value); - } - }; - }, - ), - }, - child: ImmichAssetGridView( - onRefresh: onRefresh, - assetsPerRow: perRow.value, - listener: listener, - showStorageIndicator: showStorageIndicator ?? settings.getSetting(AppSettingsEnum.storageIndicator), - renderList: renderList, - margin: margin, - selectionActive: selectionActive, - preselectedAssets: preselectedAssets, - canDeselect: canDeselect, - dynamicLayout: dynamicLayout ?? settings.getSetting(AppSettingsEnum.dynamicLayout), - showMultiSelectIndicator: showMultiSelectIndicator, - visibleItemsListener: visibleItemsListener, - topWidget: topWidget, - heroOffset: heroOffset(), - shrinkWrap: shrinkWrap, - showDragScroll: showDragScroll, - showStack: showStack, - showLabel: showDragScrollLabel, - ), - ); - } - - if (renderList != null) return buildAssetGridView(renderList!); - - final renderListFuture = ref.watch(assetsTimelineProvider(assets!)); - return renderListFuture.widgetWhen(onData: (renderList) => buildAssetGridView(renderList)); - } -} - -/// accepts a gesture even though it should reject it (because child won) -class CustomScaleGestureRecognizer extends ScaleGestureRecognizer { - @override - void rejectGesture(int pointer) { - acceptGesture(pointer); - } -} diff --git a/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart b/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart deleted file mode 100644 index c323c573b4..0000000000 --- a/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart +++ /dev/null @@ -1,828 +0,0 @@ -import 'dart:collection'; -import 'dart:developer'; -import 'dart:math'; - -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/collection_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/scroll_to_date_notifier.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/providers/tab.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_drag_region.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_grid/control_bottom_app_bar.dart'; -import 'package:immich_mobile/widgets/asset_grid/disable_multi_select_button.dart'; -import 'package:immich_mobile/widgets/asset_grid/draggable_scrollbar_custom.dart'; -import 'package:immich_mobile/widgets/asset_grid/group_divider_title.dart'; -import 'package:immich_mobile/widgets/asset_grid/thumbnail_image.dart'; -import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; - -typedef ImmichAssetGridSelectionListener = void Function(bool, Set); - -class ImmichAssetGridView extends ConsumerStatefulWidget { - final RenderList renderList; - final int assetsPerRow; - final double margin; - final bool showStorageIndicator; - final ImmichAssetGridSelectionListener? listener; - final bool selectionActive; - final Future Function()? onRefresh; - final Set? preselectedAssets; - final bool canDeselect; - final bool dynamicLayout; - final bool showMultiSelectIndicator; - final void Function(Iterable itemPositions)? visibleItemsListener; - final Widget? topWidget; - final int heroOffset; - final bool shrinkWrap; - final bool showDragScroll; - final bool showStack; - final bool showLabel; - - const ImmichAssetGridView({ - super.key, - required this.renderList, - required this.assetsPerRow, - required this.showStorageIndicator, - this.listener, - this.margin = 5.0, - this.selectionActive = false, - this.onRefresh, - this.preselectedAssets, - this.canDeselect = true, - this.dynamicLayout = true, - this.showMultiSelectIndicator = true, - this.visibleItemsListener, - this.topWidget, - this.heroOffset = 0, - this.shrinkWrap = false, - this.showDragScroll = true, - this.showStack = false, - this.showLabel = true, - }); - - @override - createState() { - return ImmichAssetGridViewState(); - } -} - -class ImmichAssetGridViewState extends ConsumerState { - final ItemScrollController _itemScrollController = ItemScrollController(); - final ScrollOffsetController _scrollOffsetController = ScrollOffsetController(); - final ItemPositionsListener _itemPositionsListener = ItemPositionsListener.create(); - late final KeepAliveLink currentAssetLink; - - /// The timestamp when the haptic feedback was last invoked - int _hapticFeedbackTS = 0; - DateTime? _prevItemTime; - bool _scrolling = false; - final Set _selectedAssets = LinkedHashSet(equals: (a, b) => a.id == b.id, hashCode: (a) => a.id); - - bool _dragging = false; - int? _dragAnchorAssetIndex; - int? _dragAnchorSectionIndex; - final Set _draggedAssets = HashSet(equals: (a, b) => a.id == b.id, hashCode: (a) => a.id); - - ScrollPhysics? _scrollPhysics; - - Set _getSelectedAssets() { - return Set.from(_selectedAssets); - } - - void _callSelectionListener(bool selectionActive) { - widget.listener?.call(selectionActive, _getSelectedAssets()); - } - - void _selectAssets(List assets) { - setState(() { - if (_dragging) { - _draggedAssets.addAll(assets); - } - _selectedAssets.addAll(assets); - _callSelectionListener(true); - }); - } - - void _deselectAssets(List assets) { - final assetsToDeselect = assets.where( - (a) => widget.canDeselect || !(widget.preselectedAssets?.contains(a) ?? false), - ); - - setState(() { - _selectedAssets.removeAll(assetsToDeselect); - if (_dragging) { - _draggedAssets.removeAll(assetsToDeselect); - } - _callSelectionListener(_selectedAssets.isNotEmpty); - }); - } - - void _deselectAll() { - setState(() { - _selectedAssets.clear(); - _dragAnchorAssetIndex = null; - _dragAnchorSectionIndex = null; - _draggedAssets.clear(); - _dragging = false; - if (!widget.canDeselect && widget.preselectedAssets != null && widget.preselectedAssets!.isNotEmpty) { - _selectedAssets.addAll(widget.preselectedAssets!); - } - _callSelectionListener(false); - }); - } - - bool _allAssetsSelected(List assets) { - return widget.selectionActive && assets.firstWhereOrNull((e) => !_selectedAssets.contains(e)) == null; - } - - Future _scrollToIndex(int index) async { - // if the index is so far down, that the end of the list is reached on the screen - // the scroll_position widget crashes. This is a workaround to prevent this. - // If the index is within the last 10 elements, we jump instead of scrolling. - if (widget.renderList.elements.length <= index + 10) { - _itemScrollController.jumpTo(index: index); - return; - } - await _itemScrollController.scrollTo(index: index, alignment: 0, duration: const Duration(milliseconds: 500)); - } - - Widget _itemBuilder(BuildContext c, int position) { - int index = position; - if (widget.topWidget != null) { - if (index == 0) { - return widget.topWidget!; - } - index--; - } - - final section = widget.renderList.elements[index]; - return _Section( - showStorageIndicator: widget.showStorageIndicator, - selectedAssets: _selectedAssets, - selectionActive: widget.selectionActive, - sectionIndex: index, - section: section, - margin: widget.margin, - renderList: widget.renderList, - assetsPerRow: widget.assetsPerRow, - scrolling: _scrolling, - dynamicLayout: widget.dynamicLayout, - selectAssets: _selectAssets, - deselectAssets: _deselectAssets, - allAssetsSelected: _allAssetsSelected, - showStack: widget.showStack, - heroOffset: widget.heroOffset, - onAssetTap: (asset) { - ref.read(currentAssetProvider.notifier).set(asset); - ref.read(isPlayingMotionVideoProvider.notifier).playing = false; - if (asset.isVideo) { - ref.read(showControlsProvider.notifier).show = false; - } - }, - ); - } - - Text _labelBuilder(int pos) { - final maxLength = widget.renderList.elements.length; - if (pos < 0 || pos >= maxLength) { - return const Text(""); - } - - final date = widget.renderList.elements[pos % maxLength].date; - - return Text( - DateFormat.yMMMM().format(date), - style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), - ); - } - - Widget _buildMultiSelectIndicator() { - return DisableMultiSelectButton(onPressed: () => _deselectAll(), selectedItemCount: _selectedAssets.length); - } - - Widget _buildAssetGrid() { - final useDragScrolling = widget.showDragScroll && widget.renderList.totalAssets >= 20; - - void dragScrolling(bool active) { - if (active != _scrolling) { - setState(() { - _scrolling = active; - }); - } - } - - bool appBarOffset() { - return (ref.watch(tabProvider).index == 0 && ModalRoute.of(context)?.settings.name == TabControllerRoute.name) || - (ModalRoute.of(context)?.settings.name == AlbumViewerRoute.name); - } - - final listWidget = ScrollablePositionedList.builder( - padding: EdgeInsets.only(top: appBarOffset() ? 60 : 0, bottom: 220), - itemBuilder: _itemBuilder, - itemPositionsListener: _itemPositionsListener, - physics: _scrollPhysics, - itemScrollController: _itemScrollController, - scrollOffsetController: _scrollOffsetController, - itemCount: widget.renderList.elements.length + (widget.topWidget != null ? 1 : 0), - addRepaintBoundaries: true, - shrinkWrap: widget.shrinkWrap, - ); - - final child = (useDragScrolling && ModalRoute.of(context) != null) - ? DraggableScrollbar.semicircle( - scrollStateListener: dragScrolling, - itemPositionsListener: _itemPositionsListener, - controller: _itemScrollController, - backgroundColor: context.isDarkTheme - ? context.colorScheme.primary.darken(amount: .5) - : context.colorScheme.primary, - labelTextBuilder: widget.showLabel ? _labelBuilder : null, - padding: appBarOffset() ? const EdgeInsets.only(top: 60) : const EdgeInsets.only(), - heightOffset: appBarOffset() ? 60 : 0, - labelConstraints: const BoxConstraints(maxHeight: 28), - scrollbarAnimationDuration: const Duration(milliseconds: 300), - scrollbarTimeToFade: const Duration(milliseconds: 1000), - child: listWidget, - ) - : listWidget; - - return widget.onRefresh == null - ? child - : appBarOffset() - ? RefreshIndicator(onRefresh: widget.onRefresh!, edgeOffset: 30, child: child) - : RefreshIndicator(onRefresh: widget.onRefresh!, child: child); - } - - void _scrollToDate() { - final date = scrollToDateNotifierProvider.value; - if (date == null) { - ImmichToast.show( - context: context, - msg: "Scroll To Date failed, date is null.", - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - ); - return; - } - - // Search for the index of the exact date in the list - var index = widget.renderList.elements.indexWhere( - (e) => e.date.year == date.year && e.date.month == date.month && e.date.day == date.day, - ); - - // If the exact date is not found, the timeline is grouped by month, - // thus we search for the month - if (index == -1) { - index = widget.renderList.elements.indexWhere((e) => e.date.year == date.year && e.date.month == date.month); - } - - if (index < widget.renderList.elements.length) { - // Not sure why the index is shifted, but it works. :3 - _scrollToIndex(index + 1); - } else { - ImmichToast.show( - context: context, - msg: "The date (${DateFormat.yMd().format(date)}) could not be found in the timeline.", - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - ); - } - } - - @override - void didUpdateWidget(ImmichAssetGridView oldWidget) { - super.didUpdateWidget(oldWidget); - if (!widget.selectionActive) { - setState(() { - _selectedAssets.clear(); - }); - } - } - - @override - void initState() { - super.initState(); - currentAssetLink = ref.read(currentAssetProvider.notifier).ref.keepAlive(); - scrollToTopNotifierProvider.addListener(_scrollToTop); - scrollToDateNotifierProvider.addListener(_scrollToDate); - - if (widget.visibleItemsListener != null) { - _itemPositionsListener.itemPositions.addListener(_positionListener); - } - if (widget.preselectedAssets != null) { - _selectedAssets.addAll(widget.preselectedAssets!); - } - - _itemPositionsListener.itemPositions.addListener(_hapticsListener); - } - - @override - void dispose() { - scrollToTopNotifierProvider.removeListener(_scrollToTop); - scrollToDateNotifierProvider.removeListener(_scrollToDate); - if (widget.visibleItemsListener != null) { - _itemPositionsListener.itemPositions.removeListener(_positionListener); - } - _itemPositionsListener.itemPositions.removeListener(_hapticsListener); - currentAssetLink.close(); - super.dispose(); - } - - void _positionListener() { - final values = _itemPositionsListener.itemPositions.value; - widget.visibleItemsListener?.call(values); - } - - void _hapticsListener() { - /// throttle interval for the haptic feedback in microseconds. - /// Currently set to 100ms. - const feedbackInterval = 100000; - - final values = _itemPositionsListener.itemPositions.value; - final start = values.firstOrNull; - - if (start != null) { - final pos = start.index; - final maxLength = widget.renderList.elements.length; - if (pos < 0 || pos >= maxLength) { - return; - } - - final date = widget.renderList.elements[pos].date; - - // only provide the feedback if the prev. date is known. - // Otherwise the app would provide the haptic feedback - // on startup. - if (_prevItemTime == null) { - _prevItemTime = date; - } else if (_prevItemTime?.year != date.year || _prevItemTime?.month != date.month) { - _prevItemTime = date; - - final now = Timeline.now; - if (now > (_hapticFeedbackTS + feedbackInterval)) { - _hapticFeedbackTS = now; - ref.read(hapticFeedbackProvider.notifier).mediumImpact(); - } - } - } - } - - void _scrollToTop() { - // for some reason, this is necessary as well in order - // to correctly reposition the drag thumb scroll bar - _itemScrollController.jumpTo(index: 0); - _itemScrollController.scrollTo(index: 0, duration: const Duration(milliseconds: 200)); - } - - void _setDragStartIndex(AssetIndex index) { - setState(() { - _scrollPhysics = const ClampingScrollPhysics(); - _dragAnchorAssetIndex = index.rowIndex; - _dragAnchorSectionIndex = index.sectionIndex; - _dragging = true; - }); - } - - void _stopDrag() { - WidgetsBinding.instance.addPostFrameCallback((_) { - // Update the physics post frame to prevent sudden change in physics on iOS. - setState(() { - _scrollPhysics = null; - }); - }); - setState(() { - _dragging = false; - _draggedAssets.clear(); - }); - } - - void _dragDragScroll(ScrollDirection direction) { - _scrollOffsetController.animateScroll( - offset: direction == ScrollDirection.forward ? 175 : -175, - duration: const Duration(milliseconds: 125), - ); - } - - void _handleDragAssetEnter(AssetIndex index) { - if (_dragAnchorSectionIndex == null || _dragAnchorAssetIndex == null) { - return; - } - - final dragAnchorSectionIndex = _dragAnchorSectionIndex!; - final dragAnchorAssetIndex = _dragAnchorAssetIndex!; - - late final int startSectionIndex; - late final int startSectionAssetIndex; - late final int endSectionIndex; - late final int endSectionAssetIndex; - - if (index.sectionIndex < dragAnchorSectionIndex) { - startSectionIndex = index.sectionIndex; - startSectionAssetIndex = index.rowIndex; - endSectionIndex = dragAnchorSectionIndex; - endSectionAssetIndex = dragAnchorAssetIndex; - } else if (index.sectionIndex > dragAnchorSectionIndex) { - startSectionIndex = dragAnchorSectionIndex; - startSectionAssetIndex = dragAnchorAssetIndex; - endSectionIndex = index.sectionIndex; - endSectionAssetIndex = index.rowIndex; - } else { - startSectionIndex = dragAnchorSectionIndex; - endSectionIndex = dragAnchorSectionIndex; - - // If same section, assign proper start / end asset Index - if (dragAnchorAssetIndex < index.rowIndex) { - startSectionAssetIndex = dragAnchorAssetIndex; - endSectionAssetIndex = index.rowIndex; - } else { - startSectionAssetIndex = index.rowIndex; - endSectionAssetIndex = dragAnchorAssetIndex; - } - } - - final selectedAssets = {}; - var currentSectionIndex = startSectionIndex; - while (currentSectionIndex < endSectionIndex) { - final section = widget.renderList.elements.elementAtOrNull(currentSectionIndex); - if (section == null) continue; - - final sectionAssets = widget.renderList.loadAssets(section.offset, section.count); - - if (currentSectionIndex == startSectionIndex) { - selectedAssets.addAll(sectionAssets.slice(startSectionAssetIndex, sectionAssets.length)); - } else { - selectedAssets.addAll(sectionAssets); - } - - currentSectionIndex += 1; - } - - final section = widget.renderList.elements.elementAtOrNull(endSectionIndex); - if (section != null) { - final sectionAssets = widget.renderList.loadAssets(section.offset, section.count); - if (startSectionIndex == endSectionIndex) { - selectedAssets.addAll(sectionAssets.slice(startSectionAssetIndex, endSectionAssetIndex + 1)); - } else { - selectedAssets.addAll(sectionAssets.slice(0, endSectionAssetIndex + 1)); - } - } - - _deselectAssets(_draggedAssets.toList()); - _draggedAssets.clear(); - _draggedAssets.addAll(selectedAssets); - _selectAssets(_draggedAssets.toList()); - } - - @override - Widget build(BuildContext context) { - return PopScope( - canPop: !(widget.selectionActive && _selectedAssets.isNotEmpty), - onPopInvokedWithResult: (didPop, _) { - if (didPop) { - return; - } else { - /// `preselectedAssets` is only present when opening the asset grid from the - /// "add to album" button. - /// - /// `_selectedAssets` includes `preselectedAssets` on initialization. - if (_selectedAssets.length > (widget.preselectedAssets?.length ?? 0)) { - /// `_deselectAll` only deselects the selected assets, - /// doesn't affect the preselected ones. - _deselectAll(); - return; - } else { - Navigator.of(context).canPop() ? Navigator.of(context).pop() : null; - } - } - }, - child: Stack( - children: [ - AssetDragRegion( - onStart: _setDragStartIndex, - onAssetEnter: _handleDragAssetEnter, - onEnd: _stopDrag, - onScroll: _dragDragScroll, - onScrollStart: () => - WidgetsBinding.instance.addPostFrameCallback((_) => controlBottomAppBarNotifier.minimize()), - child: _buildAssetGrid(), - ), - if (widget.showMultiSelectIndicator && widget.selectionActive) _buildMultiSelectIndicator(), - ], - ), - ); - } -} - -/// A single row of all placeholder widgets -class _PlaceholderRow extends StatelessWidget { - final int number; - final double width; - final double height; - final double margin; - - const _PlaceholderRow({ - super.key, - required this.number, - required this.width, - required this.height, - required this.margin, - }); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - for (int i = 0; i < number; i++) - ThumbnailPlaceholder( - key: ValueKey(i), - width: width, - height: height, - margin: EdgeInsets.only(bottom: margin, right: i + 1 == number ? 0.0 : margin), - ), - ], - ); - } -} - -/// A section for the render grid -class _Section extends StatelessWidget { - final RenderAssetGridElement section; - final int sectionIndex; - final Set selectedAssets; - final bool scrolling; - final double margin; - final int assetsPerRow; - final RenderList renderList; - final bool selectionActive; - final bool dynamicLayout; - final void Function(List) selectAssets; - final void Function(List) deselectAssets; - final bool Function(List) allAssetsSelected; - final bool showStack; - final int heroOffset; - final bool showStorageIndicator; - final void Function(Asset) onAssetTap; - - const _Section({ - required this.section, - required this.sectionIndex, - required this.scrolling, - required this.margin, - required this.assetsPerRow, - required this.renderList, - required this.selectionActive, - required this.dynamicLayout, - required this.selectAssets, - required this.deselectAssets, - required this.allAssetsSelected, - required this.selectedAssets, - required this.showStack, - required this.heroOffset, - required this.showStorageIndicator, - required this.onAssetTap, - }); - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - final width = constraints.maxWidth / assetsPerRow - margin * (assetsPerRow - 1) / assetsPerRow; - final rows = (section.count + assetsPerRow - 1) ~/ assetsPerRow; - final List assetsToRender = scrolling ? [] : renderList.loadAssets(section.offset, section.count); - return Column( - key: ValueKey(section.offset), - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (section.type == RenderAssetGridElementType.monthTitle) _MonthTitle(date: section.date), - if (section.type == RenderAssetGridElementType.groupDividerTitle || - section.type == RenderAssetGridElementType.monthTitle) - _Title( - selectionActive: selectionActive, - title: section.title!, - assets: scrolling ? [] : renderList.loadAssets(section.offset, section.totalCount), - allAssetsSelected: allAssetsSelected, - selectAssets: selectAssets, - deselectAssets: deselectAssets, - ), - for (int i = 0; i < rows; i++) - scrolling - ? _PlaceholderRow( - key: ValueKey(i), - number: i + 1 == rows ? section.count - i * assetsPerRow : assetsPerRow, - width: width, - height: width, - margin: margin, - ) - : _AssetRow( - key: ValueKey(i), - rowStartIndex: i * assetsPerRow, - sectionIndex: sectionIndex, - assets: assetsToRender.nestedSlice(i * assetsPerRow, min((i + 1) * assetsPerRow, section.count)), - absoluteOffset: section.offset + i * assetsPerRow, - width: width, - assetsPerRow: assetsPerRow, - margin: margin, - dynamicLayout: dynamicLayout, - renderList: renderList, - selectedAssets: selectedAssets, - isSelectionActive: selectionActive, - showStack: showStack, - heroOffset: heroOffset, - showStorageIndicator: showStorageIndicator, - selectionActive: selectionActive, - onSelect: (asset) => selectAssets([asset]), - onDeselect: (asset) => deselectAssets([asset]), - onAssetTap: onAssetTap, - ), - ], - ); - }, - ); - } -} - -/// The month title row for a section -class _MonthTitle extends StatelessWidget { - final DateTime date; - - const _MonthTitle({required this.date}); - - @override - Widget build(BuildContext context) { - final monthFormat = DateTime.now().year == date.year ? DateFormat.MMMM() : DateFormat.yMMMM(); - final String title = monthFormat.format(date); - return Padding( - key: Key("month-$title"), - padding: const EdgeInsets.only(left: 12.0, top: 24.0), - child: Text( - toBeginningOfSentenceCase(title, context.locale.languageCode), - style: const TextStyle(fontSize: 26, fontWeight: FontWeight.w500), - ), - ); - } -} - -/// A title row -class _Title extends StatelessWidget { - final String title; - final List assets; - final bool selectionActive; - final void Function(List) selectAssets; - final void Function(List) deselectAssets; - final bool Function(List) allAssetsSelected; - - const _Title({ - required this.title, - required this.assets, - required this.selectionActive, - required this.selectAssets, - required this.deselectAssets, - required this.allAssetsSelected, - }); - - @override - Widget build(BuildContext context) { - return GroupDividerTitle( - text: toBeginningOfSentenceCase(title, context.locale.languageCode), - multiselectEnabled: selectionActive, - onSelect: () => selectAssets(assets), - onDeselect: () => deselectAssets(assets), - selected: allAssetsSelected(assets), - ); - } -} - -/// The row of assets -class _AssetRow extends StatelessWidget { - final List assets; - final int rowStartIndex; - final int sectionIndex; - final Set selectedAssets; - final int absoluteOffset; - final double width; - final bool dynamicLayout; - final double margin; - final int assetsPerRow; - final RenderList renderList; - final bool selectionActive; - final bool showStorageIndicator; - final int heroOffset; - final bool showStack; - final void Function(Asset) onAssetTap; - final void Function(Asset)? onSelect; - final void Function(Asset)? onDeselect; - final bool isSelectionActive; - - const _AssetRow({ - super.key, - required this.rowStartIndex, - required this.sectionIndex, - required this.assets, - required this.absoluteOffset, - required this.width, - required this.dynamicLayout, - required this.margin, - required this.assetsPerRow, - required this.renderList, - required this.selectionActive, - required this.showStorageIndicator, - required this.heroOffset, - required this.showStack, - required this.isSelectionActive, - required this.selectedAssets, - required this.onAssetTap, - this.onSelect, - this.onDeselect, - }); - - @override - Widget build(BuildContext context) { - // Default: All assets have the same width - final widthDistribution = List.filled(assets.length, 1.0); - - if (dynamicLayout) { - final aspectRatios = assets.map((e) => (e.width ?? 1) / (e.height ?? 1)).toList(); - final meanAspectRatio = aspectRatios.sum / assets.length; - - // 1: mean width - // 0.5: width < mean - threshold - // 1.5: width > mean + threshold - final arConfiguration = aspectRatios.map((e) { - if (e - meanAspectRatio > 0.3) return 1.5; - if (e - meanAspectRatio < -0.3) return 0.5; - return 1.0; - }); - - // Normalize: - final sum = arConfiguration.sum; - widthDistribution.setRange(0, widthDistribution.length, arConfiguration.map((e) => (e * assets.length) / sum)); - } - return Row( - key: key, - children: assets.mapIndexed((int index, Asset asset) { - final bool last = index + 1 == assetsPerRow; - final isSelected = isSelectionActive && selectedAssets.contains(asset); - return Container( - width: width * widthDistribution[index], - height: width, - margin: EdgeInsets.only(bottom: margin, right: last ? 0.0 : margin), - child: GestureDetector( - onTap: () { - if (selectionActive) { - if (isSelected) { - onDeselect?.call(asset); - } else { - onSelect?.call(asset); - } - } else { - final asset = renderList.loadAsset(absoluteOffset + index); - onAssetTap(asset); - context.pushRoute( - GalleryViewerRoute( - renderList: renderList, - initialIndex: absoluteOffset + index, - heroOffset: heroOffset, - showStack: showStack, - ), - ); - } - }, - onLongPress: () { - onSelect?.call(asset); - HapticFeedback.heavyImpact(); - }, - child: AssetIndexWrapper( - rowIndex: rowStartIndex + index, - sectionIndex: sectionIndex, - child: ThumbnailImage( - asset: asset, - multiselectEnabled: selectionActive, - isSelected: isSelectionActive && selectedAssets.contains(asset), - showStorageIndicator: showStorageIndicator, - heroOffset: heroOffset, - showStack: showStack, - ), - ), - ), - ); - }).toList(), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/multiselect_grid.dart b/mobile/lib/widgets/asset_grid/multiselect_grid.dart deleted file mode 100644 index c0d8a6bea2..0000000000 --- a/mobile/lib/widgets/asset_grid/multiselect_grid.dart +++ /dev/null @@ -1,458 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/collection_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/models/asset_selection_state.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/routes.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/services/stack.service.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/utils/selection_handlers.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_grid/control_bottom_app_bar.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class MultiselectGrid extends HookConsumerWidget { - const MultiselectGrid({ - super.key, - required this.renderListProvider, - this.onRefresh, - this.buildLoadingIndicator, - this.onRemoveFromAlbum, - this.topWidget, - this.stackEnabled = false, - this.dragScrollLabelEnabled = true, - this.archiveEnabled = false, - this.deleteEnabled = true, - this.favoriteEnabled = true, - this.editEnabled = false, - this.unarchive = false, - this.unfavorite = false, - this.downloadEnabled = true, - this.emptyIndicator, - }); - - final ProviderListenable> renderListProvider; - final Future Function()? onRefresh; - final Widget Function()? buildLoadingIndicator; - final Future Function(Iterable)? onRemoveFromAlbum; - final Widget? topWidget; - final bool stackEnabled; - final bool dragScrollLabelEnabled; - final bool archiveEnabled; - final bool unarchive; - final bool deleteEnabled; - final bool downloadEnabled; - final bool favoriteEnabled; - final bool unfavorite; - final bool editEnabled; - final Widget? emptyIndicator; - Widget buildDefaultLoadingIndicator() => const Center(child: CircularProgressIndicator()); - - Widget buildEmptyIndicator() => emptyIndicator ?? Center(child: const Text("no_assets_to_show").tr()); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final multiselectEnabled = ref.watch(multiselectProvider.notifier); - final selectionEnabledHook = useState(false); - final selectionAssetState = useState(const AssetSelectionState()); - - final selection = useState({}); - final currentUser = ref.watch(currentUserProvider); - final processing = useProcessingOverlay(); - - useEffect(() { - selectionEnabledHook.addListener(() { - multiselectEnabled.state = selectionEnabledHook.value; - }); - - return () { - // This does not work in tests - if (kReleaseMode) { - selectionEnabledHook.dispose(); - } - }; - }, []); - - void selectionListener(bool multiselect, Set selectedAssets) { - selectionEnabledHook.value = multiselect; - selection.value = selectedAssets; - selectionAssetState.value = AssetSelectionState.fromSelection(selectedAssets); - } - - errorBuilder(String? msg) => msg != null && msg.isNotEmpty - ? () => ImmichToast.show(context: context, msg: msg, gravity: ToastGravity.BOTTOM) - : null; - - Iterable ownedRemoteSelection({String? localErrorMessage, String? ownerErrorMessage}) { - final assets = selection.value; - return assets - .remoteOnly(errorCallback: errorBuilder(localErrorMessage)) - .ownedOnly(currentUser, errorCallback: errorBuilder(ownerErrorMessage)); - } - - Iterable remoteSelection({String? errorMessage}) => - selection.value.remoteOnly(errorCallback: errorBuilder(errorMessage)); - - void onShareAssets(bool shareLocal) { - processing.value = true; - if (shareLocal) { - // Share = Download + Send to OS specific share sheet - handleShareAssets(ref, context, selection.value); - } else { - final ids = remoteSelection(errorMessage: "home_page_share_err_local".tr()).map((e) => e.remoteId!); - context.pushRoute(SharedLinkEditRoute(assetsList: ids.toList())); - } - processing.value = false; - selectionEnabledHook.value = false; - } - - void onFavoriteAssets() async { - processing.value = true; - try { - final remoteAssets = ownedRemoteSelection( - localErrorMessage: 'home_page_favorite_err_local'.tr(), - ownerErrorMessage: 'home_page_favorite_err_partner'.tr(), - ); - if (remoteAssets.isNotEmpty) { - await handleFavoriteAssets(ref, context, remoteAssets.toList()); - } - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - void onArchiveAsset() async { - processing.value = true; - try { - final remoteAssets = ownedRemoteSelection( - localErrorMessage: 'home_page_archive_err_local'.tr(), - ownerErrorMessage: 'home_page_archive_err_partner'.tr(), - ); - await handleArchiveAssets(ref, context, remoteAssets.toList()); - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - void onDelete([bool force = false]) async { - processing.value = true; - try { - final toDelete = selection.value - .ownedOnly(currentUser, errorCallback: errorBuilder('home_page_delete_err_partner'.tr())) - .toList(); - final isDeleted = await ref.read(assetProvider.notifier).deleteAssets(toDelete, force: force); - - if (isDeleted) { - ImmichToast.show( - context: context, - msg: force - ? 'assets_deleted_permanently'.tr(namedArgs: {'count': "${selection.value.length}"}) - : 'assets_trashed'.tr(namedArgs: {'count': "${selection.value.length}"}), - gravity: ToastGravity.BOTTOM, - ); - selectionEnabledHook.value = false; - } - } finally { - processing.value = false; - } - } - - void onDeleteLocal(bool isMergedAsset) async { - processing.value = true; - try { - final localAssets = selection.value.where((a) => a.isLocal).toList(); - - final toDelete = isMergedAsset ? localAssets.where((e) => e.storage == AssetState.merged) : localAssets; - - if (toDelete.isEmpty) { - return; - } - - final isDeleted = await ref.read(assetProvider.notifier).deleteLocalAssets(toDelete.toList()); - - if (isDeleted) { - final deletedCount = localAssets.where((e) => !isMergedAsset || e.isRemote).length; - - ImmichToast.show( - context: context, - msg: 'assets_removed_permanently_from_device'.tr(namedArgs: {'count': "$deletedCount"}), - gravity: ToastGravity.BOTTOM, - ); - - selectionEnabledHook.value = false; - } - } finally { - processing.value = false; - } - } - - void onDownload() async { - processing.value = true; - try { - final toDownload = selection.value.toList(); - - final results = await ref.read(downloadStateProvider.notifier).downloadAllAsset(toDownload); - - final totalCount = toDownload.length; - final successCount = results.where((e) => e).length; - final failedCount = totalCount - successCount; - - final msg = failedCount > 0 - ? 'assets_downloaded_failed'.t(context: context, args: {'count': successCount, 'error': failedCount}) - : 'assets_downloaded_successfully'.t(context: context, args: {'count': successCount}); - - ImmichToast.show(context: context, msg: msg, gravity: ToastGravity.BOTTOM); - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - void onDeleteRemote([bool shouldDeletePermanently = false]) async { - processing.value = true; - try { - final toDelete = ownedRemoteSelection( - localErrorMessage: 'home_page_delete_remote_err_local'.tr(), - ownerErrorMessage: 'home_page_delete_err_partner'.tr(), - ).toList(); - - final isDeleted = await ref - .read(assetProvider.notifier) - .deleteRemoteAssets(toDelete, shouldDeletePermanently: shouldDeletePermanently); - if (isDeleted) { - ImmichToast.show( - context: context, - msg: shouldDeletePermanently - ? 'assets_deleted_permanently_from_server'.tr(namedArgs: {'count': "${toDelete.length}"}) - : 'assets_trashed_from_server'.tr(namedArgs: {'count': "${toDelete.length}"}), - gravity: ToastGravity.BOTTOM, - ); - } - } finally { - selectionEnabledHook.value = false; - processing.value = false; - } - } - - void onUpload() { - processing.value = true; - selectionEnabledHook.value = false; - try { - ref - .read(manualUploadProvider.notifier) - .uploadAssets(context, selection.value.where((a) => a.storage == AssetState.local)); - } finally { - processing.value = false; - } - } - - void onAddToAlbum(Album album) async { - processing.value = true; - try { - final Iterable assets = remoteSelection(errorMessage: "home_page_add_to_album_err_local".tr()); - if (assets.isEmpty) { - return; - } - final result = await ref.read(albumServiceProvider).addAssets(album, assets); - - if (result != null) { - if (result.alreadyInAlbum.isNotEmpty) { - ImmichToast.show( - context: context, - msg: "home_page_add_to_album_conflicts".tr( - namedArgs: { - "album": album.name, - "added": result.successfullyAdded.toString(), - "failed": result.alreadyInAlbum.length.toString(), - }, - ), - ); - } else { - ImmichToast.show( - context: context, - msg: "home_page_add_to_album_success".tr( - namedArgs: {"album": album.name, "added": result.successfullyAdded.toString()}, - ), - toastType: ToastType.success, - ); - } - } - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - void onCreateNewAlbum() async { - processing.value = true; - try { - final Iterable assets = remoteSelection(errorMessage: "home_page_add_to_album_err_local".tr()); - if (assets.isEmpty) { - return; - } - final result = await ref.read(albumServiceProvider).createAlbumWithGeneratedName(assets); - - if (result != null) { - unawaited(ref.watch(albumProvider.notifier).refreshRemoteAlbums()); - selectionEnabledHook.value = false; - - unawaited(context.pushRoute(AlbumViewerRoute(albumId: result.id))); - } - } finally { - processing.value = false; - } - } - - void onStack() async { - try { - processing.value = true; - if (!selectionEnabledHook.value || selection.value.length < 2) { - return; - } - - await ref.read(stackServiceProvider).createStack(selection.value.map((e) => e.remoteId!).toList()); - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - void onEditTime() async { - try { - final remoteAssets = ownedRemoteSelection( - localErrorMessage: 'home_page_favorite_err_local'.tr(), - ownerErrorMessage: 'home_page_favorite_err_partner'.tr(), - ); - - if (remoteAssets.isNotEmpty) { - unawaited(handleEditDateTime(ref, context, remoteAssets.toList())); - } - } finally { - selectionEnabledHook.value = false; - } - } - - void onEditLocation() async { - try { - final remoteAssets = ownedRemoteSelection( - localErrorMessage: 'home_page_favorite_err_local'.tr(), - ownerErrorMessage: 'home_page_favorite_err_partner'.tr(), - ); - - if (remoteAssets.isNotEmpty) { - unawaited(handleEditLocation(ref, context, remoteAssets.toList())); - } - } finally { - selectionEnabledHook.value = false; - } - } - - void onToggleLockedVisibility() async { - processing.value = true; - try { - final remoteAssets = ownedRemoteSelection( - localErrorMessage: 'home_page_locked_error_local'.tr(), - ownerErrorMessage: 'home_page_locked_error_partner'.tr(), - ); - if (remoteAssets.isNotEmpty) { - final isInLockedView = ref.read(inLockedViewProvider); - final visibility = isInLockedView ? AssetVisibilityEnum.timeline : AssetVisibilityEnum.locked; - - await handleSetAssetsVisibility(ref, context, visibility, remoteAssets.toList()); - } - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - Future Function() wrapLongRunningFun(Future Function() fun, {bool showOverlay = true}) => () async { - if (showOverlay) processing.value = true; - try { - final result = await fun(); - if (result.runtimeType != bool || result == true) { - selectionEnabledHook.value = false; - } - return result; - } finally { - if (showOverlay) processing.value = false; - } - }; - - return SafeArea( - top: true, - bottom: false, - child: Stack( - children: [ - ref - .watch(renderListProvider) - .when( - data: (data) => data.isEmpty && (buildLoadingIndicator != null || topWidget == null) - ? (buildLoadingIndicator ?? buildEmptyIndicator)() - : ImmichAssetGrid( - renderList: data, - listener: selectionListener, - selectionActive: selectionEnabledHook.value, - onRefresh: onRefresh == null ? null : wrapLongRunningFun(onRefresh!, showOverlay: false), - topWidget: topWidget, - showStack: stackEnabled, - showDragScrollLabel: dragScrollLabelEnabled, - ), - error: (error, _) => Center(child: Text(error.toString())), - loading: buildLoadingIndicator ?? buildDefaultLoadingIndicator, - ), - if (selectionEnabledHook.value) - ControlBottomAppBar( - key: const ValueKey("controlBottomAppBar"), - onShare: onShareAssets, - onFavorite: favoriteEnabled ? onFavoriteAssets : null, - onArchive: archiveEnabled ? onArchiveAsset : null, - onDelete: deleteEnabled ? onDelete : null, - onDeleteServer: deleteEnabled ? onDeleteRemote : null, - onDownload: downloadEnabled ? onDownload : null, - - /// local file deletion is allowed irrespective of [deleteEnabled] since it has - /// nothing to do with the state of the asset in the Immich server - onDeleteLocal: onDeleteLocal, - onAddToAlbum: onAddToAlbum, - onCreateNewAlbum: onCreateNewAlbum, - onUpload: onUpload, - enabled: !processing.value, - selectionAssetState: selectionAssetState.value, - selectedAssets: selection.value.toList(), - onStack: stackEnabled ? onStack : null, - onEditTime: editEnabled ? onEditTime : null, - onEditLocation: editEnabled ? onEditLocation : null, - unfavorite: unfavorite, - unarchive: unarchive, - onToggleLocked: onToggleLockedVisibility, - onRemoveFromAlbum: onRemoveFromAlbum != null - ? wrapLongRunningFun(() => onRemoveFromAlbum!(selection.value)) - : null, - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/multiselect_grid_status_indicator.dart b/mobile/lib/widgets/asset_grid/multiselect_grid_status_indicator.dart deleted file mode 100644 index 3a1fa82a28..0000000000 --- a/mobile/lib/widgets/asset_grid/multiselect_grid_status_indicator.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/asset_viewer/render_list_status_provider.dart'; -import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart'; - -class MultiselectGridStatusIndicator extends HookConsumerWidget { - const MultiselectGridStatusIndicator({super.key, this.buildLoadingIndicator, this.emptyIndicator}); - - final Widget Function()? buildLoadingIndicator; - final Widget? emptyIndicator; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final renderListStatus = ref.watch(renderListStatusProvider); - return switch (renderListStatus) { - RenderListStatusEnum.loading => - buildLoadingIndicator == null - ? const Center(child: DelayedLoadingIndicator(delay: Duration(milliseconds: 500))) - : buildLoadingIndicator!(), - RenderListStatusEnum.empty => emptyIndicator ?? Center(child: const Text("no_assets_to_show").tr()), - RenderListStatusEnum.error => Center(child: const Text("error_loading_assets").tr()), - RenderListStatusEnum.complete => const SizedBox(), - }; - } -} diff --git a/mobile/lib/widgets/asset_grid/thumbnail_image.dart b/mobile/lib/widgets/asset_grid/thumbnail_image.dart deleted file mode 100644 index 93385b88b3..0000000000 --- a/mobile/lib/widgets/asset_grid/thumbnail_image.dart +++ /dev/null @@ -1,259 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/duration_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; - -class ThumbnailImage extends StatelessWidget { - /// The asset to show the thumbnail image for - final Asset asset; - - /// Whether to show the storage indicator icont over the image or not - final bool showStorageIndicator; - - /// Whether to show the show stack icon over the image or not - final bool showStack; - - /// Whether to show the checkmark indicating that this image is selected - final bool isSelected; - - /// Can override [isSelected] and never show the selection indicator - final bool multiselectEnabled; - - /// If we are allowed to deselect this image - final bool canDeselect; - - /// The offset index to apply to this hero tag for animation - final int heroOffset; - - const ThumbnailImage({ - super.key, - required this.asset, - this.showStorageIndicator = true, - this.showStack = false, - this.isSelected = false, - this.multiselectEnabled = false, - this.heroOffset = 0, - this.canDeselect = true, - }); - - @override - Widget build(BuildContext context) { - final assetContainerColor = context.isDarkTheme - ? context.primaryColor.darken(amount: 0.6) - : context.primaryColor.lighten(amount: 0.8); - - return Stack( - children: [ - AnimatedContainer( - duration: const Duration(milliseconds: 300), - curve: Curves.decelerate, - decoration: BoxDecoration( - border: multiselectEnabled && isSelected - ? canDeselect - ? Border.all(color: assetContainerColor, width: 8) - : const Border( - top: BorderSide(color: Colors.grey, width: 8), - right: BorderSide(color: Colors.grey, width: 8), - bottom: BorderSide(color: Colors.grey, width: 8), - left: BorderSide(color: Colors.grey, width: 8), - ) - : const Border(), - ), - child: Stack( - children: [ - _ImageIcon( - heroOffset: heroOffset, - asset: asset, - assetContainerColor: assetContainerColor, - multiselectEnabled: multiselectEnabled, - canDeselect: canDeselect, - isSelected: isSelected, - ), - if (showStorageIndicator) _StorageIcon(storage: asset.storage), - if (asset.isFavorite) - const Positioned(left: 8, bottom: 5, child: Icon(Icons.favorite, color: Colors.white, size: 16)), - if (asset.isVideo) _VideoIcon(duration: asset.duration), - if (asset.stackCount > 0) _StackIcon(isVideo: asset.isVideo, stackCount: asset.stackCount), - ], - ), - ), - if (multiselectEnabled) - isSelected - ? const Padding( - padding: EdgeInsets.all(3.0), - child: Align(alignment: Alignment.topLeft, child: _SelectedIcon()), - ) - : const Icon(Icons.circle_outlined, color: Colors.white), - ], - ); - } -} - -class _SelectedIcon extends StatelessWidget { - const _SelectedIcon(); - - @override - Widget build(BuildContext context) { - final assetContainerColor = context.isDarkTheme - ? context.primaryColor.darken(amount: 0.6) - : context.primaryColor.lighten(amount: 0.8); - - return DecoratedBox( - decoration: BoxDecoration(shape: BoxShape.circle, color: assetContainerColor), - child: Icon(Icons.check_circle_rounded, color: context.primaryColor), - ); - } -} - -class _VideoIcon extends StatelessWidget { - final Duration duration; - - const _VideoIcon({required this.duration}); - - @override - Widget build(BuildContext context) { - return Positioned( - top: 5, - right: 8, - child: Row( - children: [ - Text( - duration.format(), - style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold), - ), - const SizedBox(width: 3), - const Icon(Icons.play_circle_fill_rounded, color: Colors.white, size: 18), - ], - ), - ); - } -} - -class _StackIcon extends StatelessWidget { - final bool isVideo; - final int stackCount; - - const _StackIcon({required this.isVideo, required this.stackCount}); - - @override - Widget build(BuildContext context) { - return Positioned( - top: isVideo ? 28 : 5, - right: 8, - child: Row( - children: [ - if (stackCount > 1) - Text( - "$stackCount", - style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold), - ), - if (stackCount > 1) const SizedBox(width: 3), - const Icon(Icons.burst_mode_rounded, color: Colors.white, size: 18), - ], - ), - ); - } -} - -class _StorageIcon extends StatelessWidget { - final AssetState storage; - - const _StorageIcon({required this.storage}); - - @override - Widget build(BuildContext context) { - return switch (storage) { - AssetState.local => const Positioned( - right: 8, - bottom: 5, - child: Icon( - Icons.cloud_off_outlined, - color: Color.fromRGBO(255, 255, 255, 0.8), - size: 16, - shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))], - ), - ), - AssetState.remote => const Positioned( - right: 8, - bottom: 5, - child: Icon( - Icons.cloud_outlined, - color: Color.fromRGBO(255, 255, 255, 0.8), - size: 16, - shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))], - ), - ), - AssetState.merged => const Positioned( - right: 8, - bottom: 5, - child: Icon( - Icons.cloud_done_outlined, - color: Color.fromRGBO(255, 255, 255, 0.8), - size: 16, - shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))], - ), - ), - }; - } -} - -class _ImageIcon extends StatelessWidget { - final int heroOffset; - final Asset asset; - final Color assetContainerColor; - final bool multiselectEnabled; - final bool canDeselect; - final bool isSelected; - - const _ImageIcon({ - required this.heroOffset, - required this.asset, - required this.assetContainerColor, - required this.multiselectEnabled, - required this.canDeselect, - required this.isSelected, - }); - - @override - Widget build(BuildContext context) { - // Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id - final isDto = asset.id == noDbId; - final image = SizedBox.expand( - child: Hero( - tag: isDto ? '${asset.remoteId}-$heroOffset' : asset.id + heroOffset, - child: Stack( - children: [ - SizedBox.expand(child: ImmichThumbnail(asset: asset, height: 250, width: 250)), - const DecoratedBox( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Color.fromRGBO(0, 0, 0, 0.1), - Colors.transparent, - Colors.transparent, - Color.fromRGBO(0, 0, 0, 0.1), - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - stops: [0, 0.3, 0.6, 1], - ), - ), - ), - ], - ), - ), - ); - - if (!multiselectEnabled || !isSelected) { - return image; - } - - return DecoratedBox( - decoration: canDeselect ? BoxDecoration(color: assetContainerColor) : const BoxDecoration(color: Colors.grey), - child: ClipRRect(borderRadius: const BorderRadius.all(Radius.circular(15.0)), child: image), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/upload_dialog.dart b/mobile/lib/widgets/asset_grid/upload_dialog.dart deleted file mode 100644 index 86e2759566..0000000000 --- a/mobile/lib/widgets/asset_grid/upload_dialog.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; - -class UploadDialog extends ConfirmDialog { - final Function onUpload; - - const UploadDialog({super.key, required this.onUpload}) - : super( - title: 'upload_dialog_title', - content: 'upload_dialog_info', - cancel: 'cancel', - ok: 'upload', - onOk: onUpload, - ); -} diff --git a/mobile/lib/widgets/asset_viewer/advanced_bottom_sheet.dart b/mobile/lib/widgets/asset_viewer/advanced_bottom_sheet.dart deleted file mode 100644 index 1a3ef3eac3..0000000000 --- a/mobile/lib/widgets/asset_viewer/advanced_bottom_sheet.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; - -class AdvancedBottomSheet extends HookConsumerWidget { - final Asset assetDetail; - final ScrollController? scrollController; - - const AdvancedBottomSheet({super.key, required this.assetDetail, this.scrollController}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return SingleChildScrollView( - controller: scrollController, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 8.0), - child: LayoutBuilder( - builder: (context, constraints) { - // One column - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Align(child: Text("ADVANCED INFO", style: TextStyle(fontSize: 12.0))), - const SizedBox(height: 32.0), - Container( - decoration: BoxDecoration( - color: context.isDarkTheme ? Colors.grey[900] : Colors.grey[200], - borderRadius: const BorderRadius.all(Radius.circular(15.0)), - ), - child: Padding( - padding: const EdgeInsets.only(right: 16.0, left: 16, top: 8, bottom: 16), - child: ListView( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - children: [ - Align( - alignment: Alignment.centerRight, - child: IconButton( - onPressed: () { - Clipboard.setData(ClipboardData(text: assetDetail.toString())).then((_) { - context.scaffoldMessenger.showSnackBar( - SnackBar( - content: Text( - "Copied to clipboard", - style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), - ), - ), - ); - }); - }, - icon: Icon(Icons.copy, size: 16.0, color: context.primaryColor), - ), - ), - SelectableText( - assetDetail.toString(), - style: const TextStyle( - fontSize: 12.0, - fontWeight: FontWeight.bold, - fontFamily: "GoogleSansCode", - ), - showCursor: true, - ), - ], - ), - ), - ), - const SizedBox(height: 32.0), - ], - ); - }, - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart deleted file mode 100644 index 22a7deffff..0000000000 --- a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart +++ /dev/null @@ -1,362 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/pages/editing/edit.page.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/providers/routes.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/stack.service.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; -import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class BottomGalleryBar extends ConsumerWidget { - final ValueNotifier assetIndex; - final bool showStack; - final ValueNotifier stackIndex; - final ValueNotifier totalAssets; - final PageController controller; - final RenderList renderList; - - const BottomGalleryBar({ - super.key, - required this.showStack, - required this.stackIndex, - required this.assetIndex, - required this.controller, - required this.totalAssets, - required this.renderList, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isInLockedView = ref.watch(inLockedViewProvider); - final asset = ref.watch(currentAssetProvider); - if (asset == null) { - return const SizedBox(); - } - final isOwner = asset.ownerId == fastHash(ref.watch(currentUserProvider)?.id ?? ''); - final showControls = ref.watch(showControlsProvider); - final stackId = asset.stackId; - - final stackItems = showStack && stackId != null ? ref.watch(assetStackStateProvider(stackId)) : []; - bool isStackPrimaryAsset = asset.stackPrimaryAssetId == null; - final navStack = AutoRouter.of(context).stackData; - final isTrashEnabled = ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); - final isFromTrash = - isTrashEnabled && navStack.length > 2 && navStack.elementAt(navStack.length - 2).name == TrashRoute.name; - final isInAlbum = ref.watch(currentAlbumProvider)?.isRemote ?? false; - - void removeAssetFromStack() { - if (stackIndex.value > 0 && showStack && stackId != null) { - ref.read(assetStackStateProvider(stackId).notifier).removeChild(stackIndex.value - 1); - } - } - - void handleDelete() async { - Future onDelete(bool force) async { - final isDeleted = await ref.read(assetProvider.notifier).deleteAssets({asset}, force: force); - if (isDeleted && isStackPrimaryAsset) { - // Workaround for asset remaining in the gallery - renderList.deleteAsset(asset); - - // `assetIndex == totalAssets.value - 1` handle the case of removing the last asset - // to not throw the error when the next preCache index is called - if (totalAssets.value == 1 || assetIndex.value == totalAssets.value - 1) { - // Handle only one asset - await context.maybePop(); - } - - totalAssets.value -= 1; - } - if (isDeleted) { - ref.read(currentAssetProvider.notifier).set(renderList.loadAsset(assetIndex.value)); - } - return isDeleted; - } - - // Asset is trashed - if (isTrashEnabled && !isFromTrash) { - final isDeleted = await onDelete(false); - if (isDeleted) { - // Can only trash assets stored in server. Local assets are always permanently removed for now - if (context.mounted && asset.isRemote && isStackPrimaryAsset) { - ImmichToast.show( - durationInSecond: 1, - context: context, - msg: 'asset_trashed'.tr(), - gravity: ToastGravity.BOTTOM, - ); - } - removeAssetFromStack(); - } - return; - } - - // Asset is permanently removed - unawaited( - showDialog( - context: context, - builder: (BuildContext _) { - return DeleteDialog( - onDelete: () async { - final isDeleted = await onDelete(true); - if (isDeleted) { - removeAssetFromStack(); - } - }, - ); - }, - ), - ); - } - - unStack() async { - if (asset.stackId == null) { - return; - } - - await ref.read(stackServiceProvider).deleteStack(asset.stackId!, stackItems); - } - - void showStackActionItems() { - showModalBottomSheet( - context: context, - enableDrag: false, - builder: (BuildContext ctx) { - return SafeArea( - child: Padding( - padding: const EdgeInsets.only(top: 24.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: const Icon(Icons.filter_none_outlined, size: 18), - onTap: () async { - await unStack(); - ctx.pop(); - await context.maybePop(); - }, - title: const Text("viewer_unstack", style: TextStyle(fontWeight: FontWeight.bold)).tr(), - ), - ], - ), - ), - ); - }, - ); - } - - shareAsset() { - if (asset.isOffline) { - ImmichToast.show( - durationInSecond: 1, - context: context, - msg: 'asset_action_share_err_offline'.tr(), - gravity: ToastGravity.BOTTOM, - ); - return; - } - ref.read(downloadStateProvider.notifier).shareAsset(asset, context); - } - - void handleEdit() async { - final image = Image(image: ImmichImage.imageProvider(asset: asset)); - - unawaited( - context.navigator.push( - MaterialPageRoute( - builder: (context) => EditImagePage(asset: asset, image: image, isEdited: false), - ), - ), - ); - } - - handleArchive() { - ref.read(assetProvider.notifier).toggleArchive([asset]); - if (isStackPrimaryAsset) { - context.maybePop(); - return; - } - removeAssetFromStack(); - } - - handleDownload() { - if (asset.isLocal) { - return; - } - if (asset.isOffline) { - ImmichToast.show( - durationInSecond: 1, - context: context, - msg: 'asset_action_share_err_offline'.tr(), - gravity: ToastGravity.BOTTOM, - ); - return; - } - - ref.read(downloadStateProvider.notifier).downloadAsset(asset); - } - - handleRemoveFromAlbum() async { - final album = ref.read(currentAlbumProvider); - final bool isSuccess = album != null && await ref.read(albumProvider.notifier).removeAsset(album, [asset]); - - if (isSuccess) { - // Workaround for asset remaining in the gallery - renderList.deleteAsset(asset); - - if (totalAssets.value == 1) { - // Handle empty viewer - await context.maybePop(); - } else { - // changing this also for the last asset causes the parent to rebuild with an error - totalAssets.value -= 1; - } - if (assetIndex.value == totalAssets.value && assetIndex.value > 0) { - // handle the case of removing the last asset in the list - assetIndex.value -= 1; - } - } else { - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_remove".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - } - - final List> albumActions = [ - { - BottomNavigationBarItem( - icon: Icon(Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded), - label: 'share'.tr(), - tooltip: 'share'.tr(), - ): (_) => - shareAsset(), - }, - if (asset.isImage && !isInLockedView) - { - BottomNavigationBarItem( - icon: const Icon(Icons.tune_outlined), - label: 'edit'.tr(), - tooltip: 'edit'.tr(), - ): (_) => - handleEdit(), - }, - if (isOwner && !isInLockedView) - { - asset.isArchived - ? BottomNavigationBarItem( - icon: const Icon(Icons.unarchive_rounded), - label: 'unarchive'.tr(), - tooltip: 'unarchive'.tr(), - ) - : BottomNavigationBarItem( - icon: const Icon(Icons.archive_outlined), - label: 'archive'.tr(), - tooltip: 'archive'.tr(), - ): (_) => - handleArchive(), - }, - if (isOwner && asset.stackCount > 0 && !isInLockedView) - { - BottomNavigationBarItem( - icon: const Icon(Icons.burst_mode_outlined), - label: 'stack'.tr(), - tooltip: 'stack'.tr(), - ): (_) => - showStackActionItems(), - }, - if (isOwner && !isInAlbum) - { - BottomNavigationBarItem( - icon: const Icon(Icons.delete_outline), - label: 'delete'.tr(), - tooltip: 'delete'.tr(), - ): (_) => - handleDelete(), - }, - if (!isOwner) - { - BottomNavigationBarItem( - icon: const Icon(Icons.download_outlined), - label: 'download'.tr(), - tooltip: 'download'.tr(), - ): (_) => - handleDownload(), - }, - if (isInAlbum) - { - BottomNavigationBarItem( - icon: const Icon(Icons.remove_circle_outline), - label: 'remove_from_album'.tr(), - tooltip: 'remove_from_album'.tr(), - ): (_) => - handleRemoveFromAlbum(), - }, - ]; - return IgnorePointer( - ignoring: !showControls, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 100), - opacity: showControls ? 1.0 : 0.0, - child: DecoratedBox( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - colors: [Colors.black, Colors.transparent], - ), - ), - position: DecorationPosition.background, - child: Padding( - padding: const EdgeInsets.only(top: 40.0), - child: Column( - children: [ - if (asset.isVideo) VideoControls(videoPlayerName: asset.id.toString()), - BottomNavigationBar( - elevation: 0.0, - backgroundColor: Colors.transparent, - unselectedIconTheme: const IconThemeData(color: Colors.white), - selectedIconTheme: const IconThemeData(color: Colors.white), - unselectedLabelStyle: const TextStyle(color: Colors.white, fontWeight: FontWeight.w500, height: 2.3), - selectedLabelStyle: const TextStyle(color: Colors.white, fontWeight: FontWeight.w500, height: 2.3), - unselectedFontSize: 14, - selectedFontSize: 14, - selectedItemColor: Colors.white, - unselectedItemColor: Colors.white, - showSelectedLabels: true, - showUnselectedLabels: true, - items: albumActions.map((e) => e.keys.first).toList(growable: false), - onTap: (index) { - albumActions[index].values.first.call(index); - }, - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/center_play_button.dart b/mobile/lib/widgets/asset_viewer/center_play_button.dart deleted file mode 100644 index 55d8be8095..0000000000 --- a/mobile/lib/widgets/asset_viewer/center_play_button.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/widgets/asset_viewer/animated_play_pause.dart'; - -class CenterPlayButton extends StatelessWidget { - const CenterPlayButton({ - super.key, - required this.backgroundColor, - this.iconColor, - required this.show, - required this.isPlaying, - required this.isFinished, - this.onPressed, - }); - - final Color backgroundColor; - final Color? iconColor; - final bool show; - final bool isPlaying; - final bool isFinished; - final VoidCallback? onPressed; - - @override - Widget build(BuildContext context) { - return Center( - child: UnconstrainedBox( - child: AnimatedOpacity( - opacity: show ? 1.0 : 0.0, - duration: const Duration(milliseconds: 100), - child: DecoratedBox( - decoration: BoxDecoration(color: backgroundColor, shape: BoxShape.circle), - child: IconButton( - iconSize: 32, - padding: const EdgeInsets.all(12.0), - icon: isFinished - ? Icon(Icons.replay, color: iconColor) - : AnimatedPlayPause(color: iconColor, playing: isPlaying), - onPressed: onPressed, - ), - ), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart b/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart deleted file mode 100644 index 09c0e9d091..0000000000 --- a/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/cast/cast_manager_state.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart'; -import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/utils/hooks/timer_hook.dart'; -import 'package:immich_mobile/widgets/asset_viewer/center_play_button.dart'; -import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart'; - -class CustomVideoPlayerControls extends HookConsumerWidget { - final String videoId; - final Duration hideTimerDuration; - - const CustomVideoPlayerControls({ - super.key, - required this.videoId, - this.hideTimerDuration = const Duration(seconds: 5), - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final assetIsVideo = ref.watch(currentAssetProvider.select((asset) => asset != null && asset.isVideo)); - final showControls = ref.watch(showControlsProvider); - final status = ref.watch(videoPlayerProvider(videoId).select((value) => value.status)); - - final cast = ref.watch(castProvider); - - // A timer to hide the controls - final hideTimer = useTimer(hideTimerDuration, () { - if (!context.mounted) { - return; - } - final s = ref.read(videoPlayerProvider(videoId)).status; - - // Do not hide on paused - if (s != VideoPlaybackStatus.paused && s != VideoPlaybackStatus.completed && assetIsVideo) { - ref.read(showControlsProvider.notifier).show = false; - } - }); - final showBuffering = status == VideoPlaybackStatus.buffering && !cast.isCasting; - - /// Shows the controls and starts the timer to hide them - void showControlsAndStartHideTimer() { - hideTimer.reset(); - ref.read(showControlsProvider.notifier).show = true; - } - - // When playback starts, reset the hide timer - ref.listen(videoPlayerProvider(videoId).select((v) => v.status), (previous, next) { - if (next == VideoPlaybackStatus.playing) { - hideTimer.reset(); - } - }); - - /// Toggles between playing and pausing depending on the state of the video - void togglePlay() { - showControlsAndStartHideTimer(); - - if (cast.isCasting) { - if (cast.castState == CastState.playing) { - ref.read(castProvider.notifier).pause(); - } else if (cast.castState == CastState.paused) { - ref.read(castProvider.notifier).play(); - } else if (cast.castState == CastState.idle) { - // resend the play command since its finished - final asset = ref.read(currentAssetProvider); - if (asset == null) { - return; - } - ref.read(castProvider.notifier).loadMediaOld(asset, true); - } - return; - } - - final notifier = ref.read(videoPlayerProvider(videoId).notifier); - if (status == VideoPlaybackStatus.playing) { - notifier.pause(); - } else if (status == VideoPlaybackStatus.completed) { - notifier.restart(); - } else { - notifier.play(); - } - } - - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: showControlsAndStartHideTimer, - child: AbsorbPointer( - absorbing: !showControls, - child: Stack( - children: [ - if (showBuffering) - const Center(child: DelayedLoadingIndicator(fadeInDuration: Duration(milliseconds: 400))) - else - GestureDetector( - onTap: () => ref.read(showControlsProvider.notifier).show = false, - child: CenterPlayButton( - backgroundColor: Colors.black54, - iconColor: Colors.white, - isFinished: status == VideoPlaybackStatus.completed, - isPlaying: - status == VideoPlaybackStatus.playing || (cast.isCasting && cast.castState == CastState.playing), - show: assetIsVideo && showControls, - onPressed: togglePlay, - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/description_input.dart b/mobile/lib/widgets/asset_viewer/description_input.dart deleted file mode 100644 index b0cefd63fa..0000000000 --- a/mobile/lib/widgets/asset_viewer/description_input.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:logging/logging.dart'; - -class DescriptionInput extends HookConsumerWidget { - DescriptionInput({super.key, required this.asset, this.exifInfo}); - - final Asset asset; - final ExifInfo? exifInfo; - final Logger _log = Logger('DescriptionInput'); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final controller = useTextEditingController(); - final focusNode = useFocusNode(); - final isFocus = useState(false); - final isTextEmpty = useState(controller.text.isEmpty); - final assetService = ref.watch(assetServiceProvider); - final owner = ref.watch(currentUserProvider); - final hasError = useState(false); - final assetWithExif = ref.watch(assetDetailProvider(asset)); - final hasDescription = useState(false); - final isOwner = fastHash(owner?.id ?? '') == asset.ownerId; - - useEffect(() { - assetService.getDescription(asset).then((value) { - controller.text = value; - hasDescription.value = value.isNotEmpty; - }); - return null; - }, [assetWithExif.value]); - - if (!isOwner && !hasDescription.value) { - return const SizedBox.shrink(); - } - - submitDescription(String description) async { - hasError.value = false; - try { - await assetService.setDescription(asset, description); - controller.text = description; - } catch (error, stack) { - hasError.value = true; - _log.severe("Error updating description", error, stack); - ImmichToast.show(context: context, msg: "description_input_submit_error".tr(), toastType: ToastType.error); - } - } - - Widget? suffixIcon; - if (hasError.value) { - suffixIcon = const Icon(Icons.warning_outlined); - } else if (!isTextEmpty.value && isFocus.value) { - suffixIcon = IconButton( - onPressed: () { - controller.clear(); - isTextEmpty.value = true; - }, - icon: Icon(Icons.cancel_rounded, color: context.colorScheme.onSurfaceSecondary), - splashRadius: 10, - ); - } - - return TextField( - enabled: isOwner, - focusNode: focusNode, - onTap: () => isFocus.value = true, - onChanged: (value) { - isTextEmpty.value = false; - }, - onTapOutside: (a) async { - isFocus.value = false; - focusNode.unfocus(); - - if (exifInfo?.description != controller.text) { - await submitDescription(controller.text); - } - }, - autofocus: false, - maxLines: null, - keyboardType: TextInputType.multiline, - controller: controller, - style: context.textTheme.labelLarge, - decoration: InputDecoration( - hintText: 'description_input_hint_text'.tr(), - border: InputBorder.none, - suffixIcon: suffixIcon, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/asset_date_time.dart b/mobile/lib/widgets/asset_viewer/detail_panel/asset_date_time.dart deleted file mode 100644 index df8f6593df..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/asset_date_time.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asset_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/duration_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/utils/selection_handlers.dart'; - -class AssetDateTime extends ConsumerWidget { - final Asset asset; - - const AssetDateTime({super.key, required this.asset}); - - String getDateTimeString(Asset a) { - final (deltaTime, timeZone) = a.getTZAdjustedTimeAndOffset(); - final date = DateFormat.yMMMEd().format(deltaTime); - final time = DateFormat.jm().format(deltaTime); - return '$date • $time GMT${timeZone.formatAsOffset()}'; - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - final watchedAsset = ref.watch(assetDetailProvider(asset)); - String formattedDateTime = getDateTimeString(asset); - - void editDateTime() async { - await handleEditDateTime(ref, context, [asset]); - - if (watchedAsset.value != null) { - formattedDateTime = getDateTimeString(watchedAsset.value!); - } - } - - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(formattedDateTime, style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600)), - if (asset.isRemote) IconButton(onPressed: editDateTime, icon: const Icon(Icons.edit_outlined), iconSize: 20), - ], - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/asset_details.dart b/mobile/lib/widgets/asset_viewer/detail_panel/asset_details.dart deleted file mode 100644 index f0f9a2efcb..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/asset_details.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/camera_info.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/file_info.dart'; - -class AssetDetails extends ConsumerWidget { - final Asset asset; - final ExifInfo? exifInfo; - - const AssetDetails({super.key, required this.asset, this.exifInfo}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final assetWithExif = ref.watch(assetDetailProvider(asset)); - final ExifInfo? exifInfo = (assetWithExif.value ?? asset).exifInfo; - - return Padding( - padding: const EdgeInsets.only(top: 24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "exif_bottom_sheet_details", - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), - ).tr(), - FileInfo(asset: asset), - if (exifInfo?.make != null) CameraInfo(exifInfo: exifInfo!), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/asset_location.dart b/mobile/lib/widgets/asset_viewer/detail_panel/asset_location.dart deleted file mode 100644 index 6edf226e8b..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/asset_location.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/utils/selection_handlers.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/exif_map.dart'; - -class AssetLocation extends HookConsumerWidget { - final Asset asset; - - const AssetLocation({super.key, required this.asset}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final assetWithExif = ref.watch(assetDetailProvider(asset)); - final ExifInfo? exifInfo = (assetWithExif.value ?? asset).exifInfo; - final hasCoordinates = exifInfo?.hasCoordinates ?? false; - - void editLocation() { - handleEditLocation(ref, context, [assetWithExif.value ?? asset]); - } - - // Guard no lat/lng - if (!hasCoordinates) { - return asset.isRemote - ? ListTile( - minLeadingWidth: 0, - contentPadding: const EdgeInsets.all(0), - leading: const Icon(Icons.location_on), - title: Text( - "add_a_location", - style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600, color: context.primaryColor), - ).tr(), - onTap: editLocation, - ) - : const SizedBox.shrink(); - } - - Widget getLocationName() { - if (exifInfo == null) { - return const SizedBox.shrink(); - } - - final cityName = exifInfo.city; - final stateName = exifInfo.state; - - bool hasLocationName = (cityName != null && stateName != null); - - return hasLocationName - ? Text("$cityName, $stateName", style: context.textTheme.labelLarge) - : const SizedBox.shrink(); - } - - return Padding( - padding: EdgeInsets.only(top: asset.isRemote ? 0 : 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "exif_bottom_sheet_location", - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), - ).tr(), - if (asset.isRemote) - IconButton(onPressed: editLocation, icon: const Icon(Icons.edit_outlined), iconSize: 20), - ], - ), - asset.isRemote ? const SizedBox.shrink() : const SizedBox(height: 16), - ExifMap(exifInfo: exifInfo!, markerId: asset.remoteId, markerAssetThumbhash: asset.thumbhash), - const SizedBox(height: 16), - getLocationName(), - Text( - "${exifInfo.latitude!.toStringAsFixed(4)}, ${exifInfo.longitude!.toStringAsFixed(4)}", - style: context.textTheme.labelMedium?.copyWith(color: context.textTheme.labelMedium?.color?.withAlpha(150)), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/camera_info.dart b/mobile/lib/widgets/asset_viewer/detail_panel/camera_info.dart deleted file mode 100644 index 5ae29d32c7..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/camera_info.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; - -class CameraInfo extends StatelessWidget { - final ExifInfo exifInfo; - - const CameraInfo({super.key, required this.exifInfo}); - - @override - Widget build(BuildContext context) { - final textColor = context.isDarkTheme ? Colors.white : Colors.black; - return ListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - leading: Icon(Icons.camera, color: textColor.withAlpha(200)), - title: Text("${exifInfo.make} ${exifInfo.model}", style: context.textTheme.labelLarge), - subtitle: exifInfo.f != null || exifInfo.exposureSeconds != null || exifInfo.mm != null || exifInfo.iso != null - ? Text( - "ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO ${exifInfo.iso ?? ''} ", - style: context.textTheme.bodySmall, - ) - : null, - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart b/mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart deleted file mode 100644 index 97c9477c97..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/widgets/asset_viewer/description_input.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/asset_date_time.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/asset_details.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/asset_location.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/people_info.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; - -class DetailPanel extends HookConsumerWidget { - final Asset asset; - final ScrollController? scrollController; - - const DetailPanel({super.key, required this.asset, this.scrollController}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return ListView( - controller: scrollController, - shrinkWrap: true, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - children: [ - AssetDateTime(asset: asset), - asset.isRemote ? DescriptionInput(asset: asset) : const SizedBox.shrink(), - PeopleInfo(asset: asset), - AssetLocation(asset: asset), - AssetDetails(asset: asset), - ], - ), - ), - ], - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart b/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart deleted file mode 100644 index 78d9ac1776..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/utils/bytes_units.dart'; - -class FileInfo extends StatelessWidget { - final Asset asset; - - const FileInfo({super.key, required this.asset}); - - @override - Widget build(BuildContext context) { - final textColor = context.isDarkTheme ? Colors.white : Colors.black; - - final height = asset.orientatedHeight ?? asset.height; - final width = asset.orientatedWidth ?? asset.width; - String resolution = height != null && width != null ? "$width x $height " : ""; - String fileSize = asset.exifInfo?.fileSize != null ? formatBytes(asset.exifInfo!.fileSize!) : ""; - String text = resolution + fileSize; - final imgSizeString = text.isNotEmpty ? text : null; - - String? title; - String? subtitle; - - if (imgSizeString == null && asset.fileName.isNotEmpty) { - // There is only filename - title = asset.fileName; - } else if (imgSizeString != null && asset.fileName.isNotEmpty) { - // There is both filename and size information - title = asset.fileName; - subtitle = imgSizeString; - } else if (imgSizeString != null && asset.fileName.isEmpty) { - title = imgSizeString; - } else { - return const SizedBox.shrink(); - } - - return ListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - leading: Icon(Icons.image, color: textColor.withAlpha(200)), - titleAlignment: ListTileTitleAlignment.center, - title: Text(title, style: context.textTheme.labelLarge), - subtitle: subtitle == null ? null : Text(subtitle), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart b/mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart deleted file mode 100644 index b96cbc777d..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_people.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/people.utils.dart'; -import 'package:immich_mobile/widgets/search/curated_people_row.dart'; -import 'package:immich_mobile/widgets/search/person_name_edit_form.dart'; - -class PeopleInfo extends ConsumerWidget { - final Asset asset; - final EdgeInsets? padding; - - const PeopleInfo({super.key, required this.asset, this.padding}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final peopleProvider = ref.watch(assetPeopleNotifierProvider(asset).notifier); - final people = ref.watch(assetPeopleNotifierProvider(asset)).value?.where((p) => !p.isHidden); - - showPersonNameEditModel(String personId, String personName) { - return showDialog( - context: context, - useRootNavigator: false, - builder: (BuildContext context) { - return PersonNameEditForm(personId: personId, personName: personName); - }, - ).then((_) { - // ensure the people list is up-to-date. - peopleProvider.refresh(); - }); - } - - final curatedPeople = - people - ?.map( - (p) => SearchCuratedContent( - id: p.id, - label: p.name, - subtitle: p.birthDate != null && p.birthDate!.isBefore(asset.fileCreatedAt) - ? formatAge(p.birthDate!, asset.fileCreatedAt) - : null, - ), - ) - .toList() ?? - []; - - return AnimatedCrossFade( - crossFadeState: (people?.isEmpty ?? true) ? CrossFadeState.showFirst : CrossFadeState.showSecond, - duration: const Duration(milliseconds: 200), - firstChild: Container(), - secondChild: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Column( - children: [ - Padding( - padding: padding ?? EdgeInsets.zero, - child: Align( - alignment: Alignment.topLeft, - child: Text( - "exif_bottom_sheet_people", - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), - ).tr(), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: CuratedPeopleRow( - padding: padding, - content: curatedPeople, - onTap: (content, index) { - context - .pushRoute(PersonResultRoute(personId: content.id, personName: content.label)) - .then((_) => peopleProvider.refresh()); - }, - onNameTap: (person, index) => {showPersonNameEditModel(person.id, person.label)}, - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart deleted file mode 100644 index dcb0334801..0000000000 --- a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart +++ /dev/null @@ -1,119 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/scroll_to_date_notifier.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; -import 'package:immich_mobile/providers/partner.provider.dart'; -import 'package:immich_mobile/providers/tab.provider.dart'; -import 'package:immich_mobile/providers/trash.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:immich_mobile/widgets/album/add_to_album_bottom_sheet.dart'; -import 'package:immich_mobile/widgets/asset_grid/upload_dialog.dart'; -import 'package:immich_mobile/widgets/asset_viewer/top_control_app_bar.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class GalleryAppBar extends ConsumerWidget { - final void Function() showInfo; - - const GalleryAppBar({super.key, required this.showInfo}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final asset = ref.watch(currentAssetProvider); - if (asset == null) { - return const SizedBox(); - } - final album = ref.watch(currentAlbumProvider); - final isOwner = asset.ownerId == fastHash(ref.watch(currentUserProvider)?.id ?? ''); - final showControls = ref.watch(showControlsProvider); - - final isPartner = ref.watch(partnerSharedWithProvider).map((e) => fastHash(e.id)).contains(asset.ownerId); - - toggleFavorite(Asset asset) => ref.read(assetProvider.notifier).toggleFavorite([asset]); - - handleActivities() { - if (album != null && album.shared && album.remoteId != null) { - context.pushRoute(const ActivitiesRoute()); - } - } - - handleRestore(Asset asset) async { - final result = await ref.read(trashProvider.notifier).restoreAssets([asset]); - - if (result && context.mounted) { - ImmichToast.show(context: context, msg: 'asset_restored_successfully'.tr(), gravity: ToastGravity.BOTTOM); - } - } - - handleUpload(Asset asset) { - showDialog( - context: context, - builder: (BuildContext _) { - return UploadDialog( - onUpload: () { - ref.read(manualUploadProvider.notifier).uploadAssets(context, [asset]); - }, - ); - }, - ); - } - - addToAlbum(Asset addToAlbumAsset) { - showModalBottomSheet( - elevation: 0, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(15.0))), - context: context, - builder: (BuildContext _) { - return AddToAlbumBottomSheet(assets: [addToAlbumAsset]); - }, - ); - } - - handleDownloadAsset() { - ref.read(downloadStateProvider.notifier).downloadAsset(asset); - } - - handleLocateAsset() async { - // Go back to the gallery - await context.maybePop(); - await context.navigateTo(const TabControllerRoute(children: [PhotosRoute()])); - ref.read(tabProvider.notifier).update((state) => state = TabEnum.home); - // Scroll to the asset's date - scrollToDateNotifierProvider.scrollToDate(asset.fileCreatedAt); - } - - return IgnorePointer( - ignoring: !showControls, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 100), - opacity: showControls ? 1.0 : 0.0, - child: Container( - color: Colors.black.withValues(alpha: 0.4), - child: TopControlAppBar( - isOwner: isOwner, - isPartner: isPartner, - asset: asset, - onMoreInfoPressed: showInfo, - onLocatePressed: handleLocateAsset, - onFavorite: toggleFavorite, - onRestorePressed: () => handleRestore(asset), - onUploadPressed: asset.isLocal ? () => handleUpload(asset) : null, - onDownloadPressed: asset.isLocal ? null : handleDownloadAsset, - onAddToAlbumPressed: () => addToAlbum(asset), - onActivitiesPressed: handleActivities, - ), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/motion_photo_button.dart b/mobile/lib/widgets/asset_viewer/motion_photo_button.dart deleted file mode 100644 index f5479ab86e..0000000000 --- a/mobile/lib/widgets/asset_viewer/motion_photo_button.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/colors.dart'; -import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; - -class MotionPhotoButton extends ConsumerWidget { - const MotionPhotoButton({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isPlaying = ref.watch(isPlayingMotionVideoProvider); - - return IconButton( - onPressed: () { - ref.read(isPlayingMotionVideoProvider.notifier).toggle(); - }, - icon: isPlaying - ? const Icon(Icons.motion_photos_pause_outlined, color: grey200) - : const Icon(Icons.play_circle_outline_rounded, color: grey200), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart b/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart deleted file mode 100644 index 35f3840797..0000000000 --- a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart +++ /dev/null @@ -1,182 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/activity_statistics.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/routes.provider.dart'; -import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/providers/tab.provider.dart'; -import 'package:immich_mobile/providers/websocket.provider.dart'; -import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart'; -import 'package:immich_mobile/widgets/asset_viewer/motion_photo_button.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; - -class TopControlAppBar extends HookConsumerWidget { - const TopControlAppBar({ - super.key, - required this.asset, - required this.onMoreInfoPressed, - required this.onDownloadPressed, - required this.onLocatePressed, - required this.onAddToAlbumPressed, - required this.onRestorePressed, - required this.onFavorite, - required this.onUploadPressed, - required this.isOwner, - required this.onActivitiesPressed, - required this.isPartner, - }); - - final Asset asset; - final Function onMoreInfoPressed; - final VoidCallback? onUploadPressed; - final VoidCallback? onDownloadPressed; - final VoidCallback onLocatePressed; - final VoidCallback onAddToAlbumPressed; - final VoidCallback onRestorePressed; - final VoidCallback onActivitiesPressed; - final Function(Asset) onFavorite; - final bool isOwner; - final bool isPartner; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isInLockedView = ref.watch(inLockedViewProvider); - const double iconSize = 22.0; - final a = ref.watch(assetWatcher(asset)).value ?? asset; - final album = ref.watch(currentAlbumProvider); - final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); - final websocketConnected = ref.watch(websocketProvider.select((c) => c.isConnected)); - - final comments = album != null && album.remoteId != null && asset.remoteId != null - ? ref.watch(activityStatisticsProvider(album.remoteId!, asset.remoteId)) - : 0; - - Widget buildFavoriteButton(a) { - return IconButton( - onPressed: () => onFavorite(a), - icon: Icon(a.isFavorite ? Icons.favorite : Icons.favorite_border, color: Colors.grey[200]), - ); - } - - Widget buildLocateButton() { - return IconButton( - onPressed: () { - onLocatePressed(); - }, - icon: Icon(Icons.image_search, color: Colors.grey[200]), - ); - } - - Widget buildMoreInfoButton() { - return IconButton( - onPressed: () { - onMoreInfoPressed(); - }, - icon: Icon(Icons.info_outline_rounded, color: Colors.grey[200]), - ); - } - - Widget buildDownloadButton() { - return IconButton( - onPressed: onDownloadPressed, - icon: Icon(Icons.cloud_download_outlined, color: Colors.grey[200]), - ); - } - - Widget buildAddToAlbumButton() { - return IconButton( - onPressed: () { - onAddToAlbumPressed(); - }, - icon: Icon(Icons.add, color: Colors.grey[200]), - ); - } - - Widget buildRestoreButton() { - return IconButton( - onPressed: () { - onRestorePressed(); - }, - icon: Icon(Icons.history_rounded, color: Colors.grey[200]), - ); - } - - Widget buildActivitiesButton() { - return IconButton( - onPressed: () { - onActivitiesPressed(); - }, - icon: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon(Icons.mode_comment_outlined, color: Colors.grey[200]), - if (comments != 0) - Padding( - padding: const EdgeInsets.only(left: 5), - child: Text( - comments.toString(), - style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey[200]), - ), - ), - ], - ), - ); - } - - Widget buildUploadButton() { - return IconButton( - onPressed: onUploadPressed, - icon: Icon(Icons.backup_outlined, color: Colors.grey[200]), - ); - } - - Widget buildBackButton() { - return IconButton( - onPressed: () { - context.maybePop(); - }, - icon: Icon(Icons.arrow_back_ios_new_rounded, size: 20.0, color: Colors.grey[200]), - ); - } - - Widget buildCastButton() { - return IconButton( - onPressed: () { - showDialog(context: context, builder: (context) => const CastDialog()); - }, - icon: Icon( - isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded, - size: 20.0, - color: isCasting ? context.primaryColor : Colors.grey[200], - ), - ); - } - - bool isInHomePage = ref.read(tabProvider.notifier).state == TabEnum.home; - bool? isInTrash = ref.read(currentAssetProvider)?.isTrashed; - - return AppBar( - foregroundColor: Colors.grey[100], - backgroundColor: Colors.transparent, - leading: buildBackButton(), - actionsIconTheme: const IconThemeData(size: iconSize), - shape: const Border(), - actions: [ - if (asset.isRemote && isOwner) buildFavoriteButton(a), - if (isOwner && !isInHomePage && !(isInTrash ?? false) && !isInLockedView) buildLocateButton(), - if (asset.livePhotoVideoId != null) const MotionPhotoButton(), - if (asset.isLocal && !asset.isRemote) buildUploadButton(), - if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(), - if (asset.isRemote && (isOwner || isPartner) && !asset.isTrashed && !isInLockedView) buildAddToAlbumButton(), - if (isCasting || (asset.isRemote && websocketConnected)) buildCastButton(), - if (asset.isTrashed) buildRestoreButton(), - if (album != null && album.shared && !isInLockedView) buildActivitiesButton(), - buildMoreInfoButton(), - ], - ); - } -} diff --git a/mobile/lib/widgets/backup/album_info_card.dart b/mobile/lib/widgets/backup/album_info_card.dart deleted file mode 100644 index d635e136bc..0000000000 --- a/mobile/lib/widgets/backup/album_info_card.dart +++ /dev/null @@ -1,185 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/backup/available_album.model.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class AlbumInfoCard extends HookConsumerWidget { - final AvailableAlbum album; - - const AlbumInfoCard({super.key, required this.album}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final bool isSelected = ref.watch(backupProvider).selectedBackupAlbums.contains(album); - final bool isExcluded = ref.watch(backupProvider).excludedBackupAlbums.contains(album); - final syncAlbum = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums); - - final isDarkTheme = context.isDarkTheme; - - ColorFilter selectedFilter = ColorFilter.mode(context.primaryColor.withAlpha(100), BlendMode.darken); - ColorFilter excludedFilter = ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken); - ColorFilter unselectedFilter = const ColorFilter.mode(Colors.black, BlendMode.color); - - buildSelectedTextBox() { - if (isSelected) { - return Chip( - visualDensity: VisualDensity.compact, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))), - label: Text( - "album_info_card_backup_album_included", - style: TextStyle( - fontSize: 10, - color: isDarkTheme ? Colors.black : Colors.white, - fontWeight: FontWeight.bold, - ), - ).tr(), - backgroundColor: context.primaryColor, - ); - } else if (isExcluded) { - return Chip( - visualDensity: VisualDensity.compact, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))), - label: Text( - "album_info_card_backup_album_excluded", - style: TextStyle( - fontSize: 10, - color: isDarkTheme ? Colors.black : Colors.white, - fontWeight: FontWeight.bold, - ), - ).tr(), - backgroundColor: Colors.red[300], - ); - } - - return const SizedBox(); - } - - buildImageFilter() { - if (isSelected) { - return selectedFilter; - } else if (isExcluded) { - return excludedFilter; - } else { - return unselectedFilter; - } - } - - return GestureDetector( - onTap: () { - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - - if (isSelected) { - ref.read(backupProvider.notifier).removeAlbumForBackup(album); - } else { - ref.read(backupProvider.notifier).addAlbumForBackup(album); - if (syncAlbum) { - ref.read(albumProvider.notifier).createSyncAlbum(album.name); - } - } - }, - onDoubleTap: () { - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - - if (isExcluded) { - // Remove from exclude album list - ref.read(backupProvider.notifier).removeExcludedAlbumForBackup(album); - } else { - // Add to exclude album list - - if (album.id == 'isAll' || album.name == 'Recents') { - ImmichToast.show( - context: context, - msg: 'Cannot exclude album contains all assets', - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - return; - } - - ref.read(backupProvider.notifier).addExcludedAlbumForBackup(album); - } - }, - child: Card( - clipBehavior: Clip.hardEdge, - margin: const EdgeInsets.all(1), - shape: RoundedRectangleBorder( - borderRadius: const BorderRadius.all( - Radius.circular(12), // if you need this - ), - side: BorderSide( - color: isDarkTheme ? const Color.fromARGB(255, 37, 35, 35) : const Color(0xFFC9C9C9), - width: 1, - ), - ), - elevation: 0, - borderOnForeground: false, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: Stack( - clipBehavior: Clip.hardEdge, - children: [ - ColorFiltered( - colorFilter: buildImageFilter(), - child: const Image( - width: double.infinity, - height: double.infinity, - image: AssetImage('assets/immich-logo.png'), - fit: BoxFit.cover, - ), - ), - Positioned(bottom: 10, right: 25, child: buildSelectedTextBox()), - ], - ), - ), - Padding( - padding: const EdgeInsets.only(left: 25), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - album.name, - style: TextStyle(fontSize: 14, color: context.primaryColor, fontWeight: FontWeight.bold), - ), - Padding( - padding: const EdgeInsets.only(top: 2.0), - child: Text( - album.assetCount.toString() + (album.isAll ? " (${'all'.tr()})" : ""), - style: TextStyle(fontSize: 12, color: Colors.grey[600]), - ), - ), - ], - ), - ), - IconButton( - onPressed: () { - context.pushRoute(AlbumPreviewRoute(album: album.album)); - }, - icon: Icon(Icons.image_outlined, color: context.primaryColor, size: 24), - splashRadius: 25, - ), - ], - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/backup/album_info_list_tile.dart b/mobile/lib/widgets/backup/album_info_list_tile.dart deleted file mode 100644 index 9796f45e8b..0000000000 --- a/mobile/lib/widgets/backup/album_info_list_tile.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/backup/available_album.model.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class AlbumInfoListTile extends HookConsumerWidget { - final AvailableAlbum album; - - const AlbumInfoListTile({super.key, required this.album}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final bool isSelected = ref.watch(backupProvider).selectedBackupAlbums.contains(album); - final bool isExcluded = ref.watch(backupProvider).excludedBackupAlbums.contains(album); - final syncAlbum = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums); - - buildTileColor() { - if (isSelected) { - return context.isDarkTheme ? context.primaryColor.withAlpha(100) : context.primaryColor.withAlpha(25); - } else if (isExcluded) { - return context.isDarkTheme ? Colors.red[300]?.withAlpha(150) : Colors.red[100]?.withAlpha(150); - } else { - return Colors.transparent; - } - } - - buildIcon() { - if (isSelected) { - return Icon(Icons.check_circle_rounded, color: context.colorScheme.primary); - } - - if (isExcluded) { - return Icon(Icons.remove_circle_rounded, color: context.colorScheme.error); - } - - return Icon(Icons.circle, color: context.colorScheme.surfaceContainerHighest); - } - - return GestureDetector( - onDoubleTap: () { - ref.watch(hapticFeedbackProvider.notifier).selectionClick(); - - if (isExcluded) { - // Remove from exclude album list - ref.read(backupProvider.notifier).removeExcludedAlbumForBackup(album); - } else { - // Add to exclude album list - - if (album.id == 'isAll' || album.name == 'Recents') { - ImmichToast.show( - context: context, - msg: 'Cannot exclude album contains all assets', - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - return; - } - - ref.read(backupProvider.notifier).addExcludedAlbumForBackup(album); - } - }, - child: ListTile( - tileColor: buildTileColor(), - contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - onTap: () { - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - if (isSelected) { - ref.read(backupProvider.notifier).removeAlbumForBackup(album); - } else { - ref.read(backupProvider.notifier).addAlbumForBackup(album); - if (syncAlbum) { - ref.read(albumProvider.notifier).createSyncAlbum(album.name); - } - } - }, - leading: buildIcon(), - title: Text(album.name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - subtitle: Text(album.assetCount.toString()), - trailing: IconButton( - onPressed: () { - context.pushRoute(AlbumPreviewRoute(album: album.album)); - }, - icon: Icon(Icons.image_outlined, color: context.primaryColor, size: 24), - splashRadius: 25, - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/backup/asset_info_table.dart b/mobile/lib/widgets/backup/asset_info_table.dart deleted file mode 100644 index 2cccded2bb..0000000000 --- a/mobile/lib/widgets/backup/asset_info_table.dart +++ /dev/null @@ -1,105 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; - -class BackupAssetInfoTable extends ConsumerWidget { - const BackupAssetInfoTable({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isManualUpload = ref.watch( - backupProvider.select((value) => value.backupProgress == BackUpProgressEnum.manualInProgress), - ); - - final isUploadInProgress = ref.watch( - backupProvider.select( - (value) => - value.backupProgress == BackUpProgressEnum.inProgress || - value.backupProgress == BackUpProgressEnum.inBackground || - value.backupProgress == BackUpProgressEnum.manualInProgress, - ), - ); - - final asset = isManualUpload - ? ref.watch(manualUploadProvider.select((value) => value.currentUploadAsset)) - : ref.watch(backupProvider.select((value) => value.currentUploadAsset)); - - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Table( - border: TableBorder.all(color: context.colorScheme.outlineVariant, width: 1), - children: [ - TableRow( - children: [ - TableCell( - verticalAlignment: TableCellVerticalAlignment.middle, - child: Padding( - padding: const EdgeInsets.all(6.0), - child: - Text( - 'backup_controller_page_filename', - style: TextStyle( - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - fontSize: 10.0, - ), - ).tr( - namedArgs: isUploadInProgress - ? {'filename': asset.fileName, 'size': asset.fileType.toLowerCase()} - : {'filename': "-", 'size': "-"}, - ), - ), - ), - ], - ), - TableRow( - children: [ - TableCell( - verticalAlignment: TableCellVerticalAlignment.middle, - child: Padding( - padding: const EdgeInsets.all(6.0), - child: Text( - "backup_controller_page_created", - style: TextStyle( - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - fontSize: 10.0, - ), - ).tr(namedArgs: {'date': isUploadInProgress ? _getAssetCreationDate(asset) : "-"}), - ), - ), - ], - ), - TableRow( - children: [ - TableCell( - child: Padding( - padding: const EdgeInsets.all(6.0), - child: Text( - "backup_controller_page_id", - style: TextStyle( - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - fontSize: 10.0, - ), - ).tr(namedArgs: {'id': isUploadInProgress ? asset.id : "-"}), - ), - ), - ], - ), - ], - ), - ); - } - - @pragma('vm:prefer-inline') - String _getAssetCreationDate(CurrentUploadAsset asset) { - return DateFormat.yMMMMd().format(asset.fileCreatedAt.toLocal()); - } -} diff --git a/mobile/lib/widgets/backup/current_backup_asset_info_box.dart b/mobile/lib/widgets/backup/current_backup_asset_info_box.dart deleted file mode 100644 index c2f94e706a..0000000000 --- a/mobile/lib/widgets/backup/current_backup_asset_info_box.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'dart:io'; - -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/widgets/backup/asset_info_table.dart'; -import 'package:immich_mobile/widgets/backup/error_chip.dart'; -import 'package:immich_mobile/widgets/backup/icloud_download_progress_bar.dart'; -import 'package:immich_mobile/widgets/backup/upload_progress_bar.dart'; -import 'package:immich_mobile/widgets/backup/upload_stats.dart'; - -class CurrentUploadingAssetInfoBox extends StatelessWidget { - const CurrentUploadingAssetInfoBox({super.key}); - - @override - Widget build(BuildContext context) { - return ListTile( - isThreeLine: true, - leading: Icon(Icons.image_outlined, color: context.primaryColor, size: 30), - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("backup_controller_page_uploading_file_info", style: context.textTheme.titleSmall).tr(), - const BackupErrorChip(), - ], - ), - subtitle: Column( - children: [ - if (Platform.isIOS) const IcloudDownloadProgressBar(), - const BackupUploadProgressBar(), - const BackupUploadStats(), - const BackupAssetInfoTable(), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/backup/error_chip.dart b/mobile/lib/widgets/backup/error_chip.dart deleted file mode 100644 index 191049cd75..0000000000 --- a/mobile/lib/widgets/backup/error_chip.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/colors.dart'; -import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/backup/error_chip_text.dart'; - -class BackupErrorChip extends ConsumerWidget { - const BackupErrorChip({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final hasErrors = ref.watch(errorBackupListProvider.select((value) => value.isNotEmpty)); - if (!hasErrors) { - return const SizedBox(); - } - - return ActionChip( - avatar: const Icon(Icons.info, color: red400), - elevation: 1, - visualDensity: VisualDensity.compact, - label: const BackupErrorChipText(), - backgroundColor: Colors.white, - onPressed: () => context.pushRoute(const FailedBackupStatusRoute()), - ); - } -} diff --git a/mobile/lib/widgets/backup/error_chip_text.dart b/mobile/lib/widgets/backup/error_chip_text.dart deleted file mode 100644 index c987dfd331..0000000000 --- a/mobile/lib/widgets/backup/error_chip_text.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/colors.dart'; -import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; - -class BackupErrorChipText extends ConsumerWidget { - const BackupErrorChipText({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final count = ref.watch(errorBackupListProvider).length; - if (count == 0) { - return const SizedBox(); - } - - return const Text( - "backup_controller_page_failed", - style: TextStyle(color: red400, fontWeight: FontWeight.bold, fontSize: 11), - ).tr(namedArgs: {'count': count.toString()}); - } -} diff --git a/mobile/lib/widgets/backup/icloud_download_progress_bar.dart b/mobile/lib/widgets/backup/icloud_download_progress_bar.dart deleted file mode 100644 index 9f0f7ec3eb..0000000000 --- a/mobile/lib/widgets/backup/icloud_download_progress_bar.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; - -class IcloudDownloadProgressBar extends ConsumerWidget { - const IcloudDownloadProgressBar({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final isManualUpload = ref.watch( - backupProvider.select((value) => value.backupProgress == BackUpProgressEnum.manualInProgress), - ); - - final isIcloudAsset = isManualUpload - ? ref.watch(manualUploadProvider.select((value) => value.currentUploadAsset.isIcloudAsset)) - : ref.watch(backupProvider.select((value) => value.currentUploadAsset.isIcloudAsset)); - - if (!isIcloudAsset) { - return const SizedBox(); - } - - final iCloudDownloadProgress = ref.watch(backupProvider.select((value) => value.iCloudDownloadProgress)); - - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Row( - children: [ - SizedBox(width: 110, child: Text("iCloud Download", style: context.textTheme.labelSmall)), - Expanded( - child: LinearProgressIndicator( - minHeight: 10.0, - value: iCloudDownloadProgress / 100.0, - borderRadius: const BorderRadius.all(Radius.circular(10.0)), - ), - ), - Text(" ${iCloudDownloadProgress ~/ 1}%", style: const TextStyle(fontSize: 12)), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/backup/ios_debug_info_tile.dart b/mobile/lib/widgets/backup/ios_debug_info_tile.dart deleted file mode 100644 index be333c6460..0000000000 --- a/mobile/lib/widgets/backup/ios_debug_info_tile.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:intl/intl.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart'; - -/// This is a simple debug widget which should be removed later on when we are -/// more confident about background sync -class IosDebugInfoTile extends HookConsumerWidget { - final IOSBackgroundSettings settings; - const IosDebugInfoTile({super.key, required this.settings}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final fetch = settings.timeOfLastFetch; - final processing = settings.timeOfLastProcessing; - final processes = settings.numberOfBackgroundTasksQueued; - - final String title; - if (processes == 0) { - title = 'ios_debug_info_no_processes_queued'.t(context: context); - } else { - title = 'ios_debug_info_processes_queued'.t(context: context, args: {'count': processes}); - } - - final df = DateFormat.yMd().add_jm(); - final String subtitle; - if (fetch == null && processing == null) { - subtitle = 'ios_debug_info_no_sync_yet'.t(context: context); - } else if (fetch != null && processing == null) { - subtitle = 'ios_debug_info_fetch_ran_at'.t(context: context, args: {'dateTime': df.format(fetch)}); - } else if (processing != null && fetch == null) { - subtitle = 'ios_debug_info_processing_ran_at'.t(context: context, args: {'dateTime': df.format(processing)}); - } else { - final fetchOrProcessing = fetch!.isAfter(processing!) ? fetch : processing; - subtitle = 'ios_debug_info_last_sync_at'.t(context: context, args: {'dateTime': df.format(fetchOrProcessing)}); - } - - return ListTile( - title: Text( - title, - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: context.primaryColor), - ), - subtitle: Text(subtitle, style: const TextStyle(fontSize: 14)), - leading: Icon(Icons.bug_report, color: context.primaryColor), - ); - } -} diff --git a/mobile/lib/widgets/backup/upload_progress_bar.dart b/mobile/lib/widgets/backup/upload_progress_bar.dart deleted file mode 100644 index 641ed14878..0000000000 --- a/mobile/lib/widgets/backup/upload_progress_bar.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; - -class BackupUploadProgressBar extends ConsumerWidget { - const BackupUploadProgressBar({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isManualUpload = ref.watch( - backupProvider.select((value) => value.backupProgress == BackUpProgressEnum.manualInProgress), - ); - - final isIcloudAsset = isManualUpload - ? ref.watch(manualUploadProvider.select((value) => value.currentUploadAsset.isIcloudAsset)) - : ref.watch(backupProvider.select((value) => value.currentUploadAsset.isIcloudAsset)); - - final uploadProgress = isManualUpload - ? ref.watch(manualUploadProvider.select((value) => value.progressInPercentage)) - : ref.watch(backupProvider.select((value) => value.progressInPercentage)); - - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Row( - children: [ - if (isIcloudAsset) SizedBox(width: 110, child: Text("Immich Upload", style: context.textTheme.labelSmall)), - Expanded( - child: LinearProgressIndicator( - minHeight: 10.0, - value: uploadProgress / 100.0, - borderRadius: const BorderRadius.all(Radius.circular(10.0)), - ), - ), - Text( - " ${uploadProgress.toStringAsFixed(0)}%", - style: const TextStyle(fontSize: 12, fontFamily: "GoogleSansCode"), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/backup/upload_stats.dart b/mobile/lib/widgets/backup/upload_stats.dart deleted file mode 100644 index 38f99e53fc..0000000000 --- a/mobile/lib/widgets/backup/upload_stats.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; - -class BackupUploadStats extends ConsumerWidget { - const BackupUploadStats({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isManualUpload = ref.watch( - backupProvider.select((value) => value.backupProgress == BackUpProgressEnum.manualInProgress), - ); - - final uploadFileProgress = isManualUpload - ? ref.watch(manualUploadProvider.select((value) => value.progressInFileSize)) - : ref.watch(backupProvider.select((value) => value.progressInFileSize)); - - final uploadFileSpeed = isManualUpload - ? ref.watch(manualUploadProvider.select((value) => value.progressInFileSpeed)) - : ref.watch(backupProvider.select((value) => value.progressInFileSpeed)); - - return Padding( - padding: const EdgeInsets.only(top: 2.0, bottom: 2.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(uploadFileProgress, style: const TextStyle(fontSize: 10, fontFamily: "GoogleSansCode")), - Text( - _formatUploadFileSpeed(uploadFileSpeed), - style: const TextStyle(fontSize: 10, fontFamily: "GoogleSansCode"), - ), - ], - ), - ); - } - - @pragma('vm:prefer-inline') - String _formatUploadFileSpeed(double uploadFileSpeed) { - if (uploadFileSpeed < 1024) { - return '${uploadFileSpeed.toStringAsFixed(2)} B/s'; - } else if (uploadFileSpeed < 1024 * 1024) { - return '${(uploadFileSpeed / 1024).toStringAsFixed(2)} KB/s'; - } else if (uploadFileSpeed < 1024 * 1024 * 1024) { - return '${(uploadFileSpeed / (1024 * 1024)).toStringAsFixed(2)} MB/s'; - } else { - return '${(uploadFileSpeed / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB/s'; - } - } -} diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart index c330fb4649..c6c6b2cff1 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart @@ -5,18 +5,15 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; +import 'package:immich_mobile/models/server_info/server_disk_info.model.dart'; +import 'package:immich_mobile/pages/common/settings.page.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; -import 'package:immich_mobile/pages/common/settings.page.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; import 'package:immich_mobile/widgets/common/app_bar_dialog/app_bar_profile_info.dart'; @@ -32,7 +29,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { ref.watch(localeProvider); - BackUpState backupState = ref.watch(backupProvider); + ServerDiskInfo backupState = ref.watch(backupProvider); final theme = context.themeData; bool isHorizontal = !context.isMobile; final horizontalPadding = isHorizontal ? 100.0 : 20.0; @@ -128,9 +125,6 @@ class ImmichAppBarDialog extends HookConsumerWidget { isLoggingOut.value = true; await ref.read(authProvider.notifier).logout().whenComplete(() => isLoggingOut.value = false); - ref.read(manualUploadProvider.notifier).cancelBackup(); - ref.read(backupProvider.notifier).cancelBackup(); - unawaited(ref.read(assetProvider.notifier).clearAllAssets()); ref.read(websocketProvider.notifier).disconnect(); unawaited(context.replaceRoute(const LoginRoute())); }, @@ -146,9 +140,9 @@ class ImmichAppBarDialog extends HookConsumerWidget { } Widget buildStorageInformation() { - var percentage = backupState.serverInfo.diskUsagePercentage / 100; - var usedDiskSpace = backupState.serverInfo.diskUse; - var totalDiskSpace = backupState.serverInfo.diskSize; + var percentage = backupState.diskUsagePercentage / 100; + var usedDiskSpace = backupState.diskUse; + var totalDiskSpace = backupState.diskSize; if (user != null && user.hasQuota) { usedDiskSpace = formatBytes(user.quotaUsageInBytes); @@ -275,7 +269,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { ], ), ), - if (Store.isBetaTimelineEnabled && isReadonlyModeEnabled) buildReadonlyMessage(), + if (isReadonlyModeEnabled) buildReadonlyMessage(), buildAppLogButton(), buildFreeUpSpaceButton(), buildSettingButton(), diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart index a9fdb9a43f..d6881f519a 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart @@ -4,7 +4,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; @@ -62,10 +61,6 @@ class AppBarProfileInfoBox extends HookConsumerWidget { } void toggleReadonlyMode() { - // read only mode is only supported int he beta experience - // TODO: remove this check when the beta UI goes stable - if (!Store.isBetaTimelineEnabled) return; - final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); ref.read(readonlyModeProvider.notifier).toggleReadonlyMode(); diff --git a/mobile/lib/widgets/common/drag_sheet.dart b/mobile/lib/widgets/common/drag_sheet.dart deleted file mode 100644 index 5d1fda1beb..0000000000 --- a/mobile/lib/widgets/common/drag_sheet.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; - -class CustomDraggingHandle extends StatelessWidget { - const CustomDraggingHandle({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - height: 4, - width: 30, - decoration: BoxDecoration( - color: context.themeData.dividerColor, - borderRadius: const BorderRadius.all(Radius.circular(20)), - ), - ); - } -} - -class ControlBoxButton extends StatelessWidget { - const ControlBoxButton({super.key, required this.label, required this.iconData, this.onPressed, this.onLongPressed}); - - final String label; - final IconData iconData; - final void Function()? onPressed; - final void Function()? onLongPressed; - - @override - Widget build(BuildContext context) { - final minWidth = context.isMobile ? MediaQuery.sizeOf(context).width / 4.5 : 75.0; - - return MaterialButton( - padding: const EdgeInsets.all(10), - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(20))), - onPressed: onPressed, - onLongPress: onLongPressed, - minWidth: minWidth, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon(iconData, size: 24), - const SizedBox(height: 8), - Text( - label, - style: const TextStyle(fontSize: 14.0, fontWeight: FontWeight.w400), - maxLines: 3, - textAlign: TextAlign.center, - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart deleted file mode 100644 index 56b7e91eec..0000000000 --- a/mobile/lib/widgets/common/immich_app_bar.dart +++ /dev/null @@ -1,170 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart'; -import 'package:immich_mobile/widgets/common/app_bar_dialog/app_bar_dialog.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); - final List? actions; - final bool showUploadButton; - - const ImmichAppBar({super.key, this.actions, this.showUploadButton = true}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final BackUpState backupState = ref.watch(backupProvider); - final bool isEnableAutoBackup = backupState.backgroundBackup || backupState.autoBackup; - final user = ref.watch(currentUserProvider); - final bool versionWarningPresent = ref.watch(versionWarningPresentProvider(user)); - final isDarkTheme = context.isDarkTheme; - const widgetSize = 30.0; - final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); - - buildProfileIndicator() { - return InkWell( - onTap: () => - showDialog(context: context, useRootNavigator: false, builder: (ctx) => const ImmichAppBarDialog()), - borderRadius: const BorderRadius.all(Radius.circular(12)), - child: Badge( - label: Container( - decoration: BoxDecoration(color: Colors.black, borderRadius: BorderRadius.circular(widgetSize / 2)), - child: const Icon(Icons.info, color: Color.fromARGB(255, 243, 188, 106), size: widgetSize / 2), - ), - backgroundColor: Colors.transparent, - alignment: Alignment.bottomRight, - isLabelVisible: versionWarningPresent, - offset: const Offset(-2, -12), - child: user == null - ? const Icon(Icons.face_outlined, size: widgetSize) - : Semantics( - label: "logged_in_as".tr(namedArgs: {"user": user.name}), - child: UserCircleAvatar(size: 32, user: user), - ), - ), - ); - } - - getBackupBadgeIcon() { - final iconColor = isDarkTheme ? Colors.white : Colors.black; - - if (isEnableAutoBackup) { - if (backupState.backupProgress == BackUpProgressEnum.inProgress) { - return Container( - padding: const EdgeInsets.all(3.5), - child: CircularProgressIndicator( - strokeWidth: 2, - strokeCap: StrokeCap.round, - valueColor: AlwaysStoppedAnimation(iconColor), - semanticsLabel: 'backup_controller_page_backup'.tr(), - ), - ); - } else if (backupState.backupProgress != BackUpProgressEnum.inBackground && - backupState.backupProgress != BackUpProgressEnum.manualInProgress) { - return Icon( - Icons.check_outlined, - size: 9, - color: iconColor, - semanticLabel: 'backup_controller_page_backup'.tr(), - ); - } - } - - if (!isEnableAutoBackup) { - return Icon( - Icons.cloud_off_rounded, - size: 9, - color: iconColor, - semanticLabel: 'backup_controller_page_backup'.tr(), - ); - } - } - - buildBackupIndicator() { - final indicatorIcon = getBackupBadgeIcon(); - final badgeBackground = context.colorScheme.surfaceContainer; - - return InkWell( - onTap: () => context.pushRoute(const BackupControllerRoute()), - borderRadius: const BorderRadius.all(Radius.circular(12)), - child: Badge( - label: Container( - width: widgetSize / 2, - height: widgetSize / 2, - decoration: BoxDecoration( - color: badgeBackground, - border: Border.all(color: context.colorScheme.outline.withValues(alpha: .3)), - borderRadius: BorderRadius.circular(widgetSize / 2), - ), - child: indicatorIcon, - ), - backgroundColor: Colors.transparent, - alignment: Alignment.bottomRight, - isLabelVisible: indicatorIcon != null, - offset: const Offset(-2, -12), - child: Icon(Icons.backup_rounded, size: widgetSize, color: context.primaryColor), - ), - ); - } - - return AppBar( - backgroundColor: context.themeData.appBarTheme.backgroundColor, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))), - automaticallyImplyLeading: false, - centerTitle: false, - title: Builder( - builder: (BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only(top: 3.0), - child: SvgPicture.asset( - context.isDarkTheme ? 'assets/immich-logo-inline-dark.svg' : 'assets/immich-logo-inline-light.svg', - height: 40, - ), - ), - const Tooltip( - triggerMode: TooltipTriggerMode.tap, - showDuration: Duration(seconds: 4), - message: - "The old timeline is deprecated and will be removed in a future release. Kindly switch to the new timeline under Advanced Settings.", - child: Padding( - padding: EdgeInsets.only(top: 3.0), - child: Icon(Icons.error_rounded, fill: 1, color: Colors.amber, size: 20), - ), - ), - ], - ); - }, - ), - actions: [ - if (actions != null) - ...actions!.map((action) => Padding(padding: const EdgeInsets.only(right: 16), child: action)), - if (isCasting) - Padding( - padding: const EdgeInsets.only(right: 12), - child: IconButton( - onPressed: () { - showDialog(context: context, builder: (context) => const CastDialog()); - }, - icon: Icon(isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded), - ), - ), - if (showUploadButton) Padding(padding: const EdgeInsets.only(right: 20), child: buildBackupIndicator()), - Padding(padding: const EdgeInsets.only(right: 20), child: buildProfileIndicator()), - ], - ); - } -} diff --git a/mobile/lib/widgets/common/immich_image.dart b/mobile/lib/widgets/common/immich_image.dart deleted file mode 100644 index 57978e83ff..0000000000 --- a/mobile/lib/widgets/common/immich_image.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as base_asset; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart'; -import 'package:octo_image/octo_image.dart'; - -class ImmichImage extends StatelessWidget { - const ImmichImage( - this.asset, { - this.width, - this.height, - this.fit = BoxFit.cover, - this.placeholder = const ThumbnailPlaceholder(), - super.key, - }); - - final Asset? asset; - final Widget? placeholder; - final double? width; - final double? height; - final BoxFit fit; - - // Helper function to return the image provider for the asset - // either by using the asset ID or the asset itself - /// [asset] is the Asset to request, or else use [assetId] to get a remote - /// image provider - static ImageProvider imageProvider({Asset? asset, String? assetId, double width = 1080, double height = 1920}) { - if (asset == null && assetId == null) { - throw Exception('Must supply either asset or assetId'); - } - - if (asset == null) { - return RemoteFullImageProvider( - assetId: assetId!, - thumbhash: '', - assetType: base_asset.AssetType.video, - isAnimated: false, - ); - } - - if (useLocal(asset)) { - return LocalFullImageProvider( - id: asset.localId!, - assetType: base_asset.AssetType.video, - size: Size(width, height), - isAnimated: false, - ); - } else { - return RemoteFullImageProvider( - assetId: asset.remoteId!, - thumbhash: asset.thumbhash ?? '', - assetType: base_asset.AssetType.video, - isAnimated: false, - ); - } - } - - // Whether to use the local asset image provider or a remote one - static bool useLocal(Asset asset) => - !asset.isRemote || asset.isLocal && !Store.get(StoreKey.preferRemoteImage, false); - - @override - Widget build(BuildContext context) { - if (asset == null) { - return Container( - color: Colors.grey, - width: width, - height: height, - child: const Center(child: Icon(Icons.no_photography)), - ); - } - - final imageProviderInstance = ImmichImage.imageProvider(asset: asset, width: context.width, height: context.height); - - return OctoImage( - fadeInDuration: const Duration(milliseconds: 0), - fadeOutDuration: const Duration(milliseconds: 100), - placeholderBuilder: (context) { - if (placeholder != null) { - return placeholder!; - } - return const SizedBox(); - }, - image: imageProviderInstance, - width: width, - height: height, - fit: fit, - errorBuilder: (context, error, stackTrace) { - imageProviderInstance.evict(); - - return Icon(Icons.image_not_supported_outlined, size: 32, color: Colors.red[200]); - }, - ); - } -} diff --git a/mobile/lib/widgets/common/immich_thumbnail.dart b/mobile/lib/widgets/common/immich_thumbnail.dart deleted file mode 100644 index f17353c3aa..0000000000 --- a/mobile/lib/widgets/common/immich_thumbnail.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'dart:typed_data'; - -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/utils/hooks/blurhash_hook.dart'; -import 'package:immich_mobile/utils/thumbnail_utils.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; -import 'package:immich_mobile/widgets/common/thumbhash_placeholder.dart'; -import 'package:octo_image/octo_image.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as base_asset; - -class ImmichThumbnail extends HookConsumerWidget { - const ImmichThumbnail({this.asset, this.width = 250, this.height = 250, this.fit = BoxFit.cover, super.key}); - - final Asset? asset; - final double width; - final double height; - final BoxFit fit; - - /// Helper function to return the image provider for the asset thumbnail - /// either by using the asset ID or the asset itself - /// [asset] is the Asset to request, or else use [assetId] to get a remote - /// image provider - static ImageProvider imageProvider({Asset? asset, String? assetId, int thumbnailSize = 256}) { - if (asset == null && assetId == null) { - throw Exception('Must supply either asset or assetId'); - } - - if (asset == null) { - return RemoteImageProvider.thumbnail(assetId: assetId!, thumbhash: ""); - } - - if (ImmichImage.useLocal(asset)) { - return LocalThumbProvider( - id: asset.localId!, - assetType: base_asset.AssetType.video, - size: Size(thumbnailSize.toDouble(), thumbnailSize.toDouble()), - ); - } else { - return RemoteImageProvider.thumbnail(assetId: asset.remoteId!, thumbhash: asset.thumbhash ?? ""); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - Uint8List? blurhash = useBlurHashRef(asset).value; - - if (asset == null) { - return Container( - color: Colors.grey, - width: width, - height: height, - child: const Center(child: Icon(Icons.no_photography)), - ); - } - - final assetAltText = getAltText(asset!.exifInfo, asset!.fileCreatedAt, asset!.type, []); - - final thumbnailProviderInstance = ImmichThumbnail.imageProvider(asset: asset); - - customErrorBuilder(BuildContext ctx, Object error, StackTrace? stackTrace) { - thumbnailProviderInstance.evict(); - - final originalErrorWidgetBuilder = blurHashErrorBuilder(blurhash, fit: fit); - return originalErrorWidgetBuilder(ctx, error, stackTrace); - } - - return Semantics( - label: assetAltText, - child: OctoImage.fromSet( - placeholderFadeInDuration: Duration.zero, - fadeInDuration: Duration.zero, - fadeOutDuration: const Duration(milliseconds: 100), - octoSet: OctoSet( - placeholderBuilder: blurHashPlaceholderBuilder(blurhash, fit: fit), - errorBuilder: customErrorBuilder, - ), - image: thumbnailProviderInstance, - width: width, - height: height, - fit: fit, - ), - ); - } -} diff --git a/mobile/lib/widgets/common/share_dialog.dart b/mobile/lib/widgets/common/share_dialog.dart deleted file mode 100644 index 625390c4b7..0000000000 --- a/mobile/lib/widgets/common/share_dialog.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -class ShareDialog extends StatelessWidget { - const ShareDialog({super.key}); - - @override - Widget build(BuildContext context) { - return AlertDialog( - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const CircularProgressIndicator(), - Container(margin: const EdgeInsets.only(top: 12), child: const Text('share_dialog_preparing').tr()), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/common/thumbhash_placeholder.dart b/mobile/lib/widgets/common/thumbhash_placeholder.dart index 0cb1222989..8a9c2eb928 100644 --- a/mobile/lib/widgets/common/thumbhash_placeholder.dart +++ b/mobile/lib/widgets/common/thumbhash_placeholder.dart @@ -4,15 +4,6 @@ import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart'; import 'package:immich_mobile/widgets/common/fade_in_placeholder_image.dart'; import 'package:octo_image/octo_image.dart'; -/// Simple set to show [OctoPlaceholder.circularProgressIndicator] as -/// placeholder and [OctoError.icon] as error. -OctoSet blurHashOrPlaceholder(Uint8List? blurhash, {BoxFit? fit, Text? errorMessage}) { - return OctoSet( - placeholderBuilder: blurHashPlaceholderBuilder(blurhash, fit: fit), - errorBuilder: blurHashErrorBuilder(blurhash, fit: fit, message: errorMessage), - ); -} - OctoPlaceholderBuilder blurHashPlaceholderBuilder(Uint8List? blurhash, {BoxFit? fit}) { return (context) => blurhash == null ? const ThumbnailPlaceholder() diff --git a/mobile/lib/widgets/forms/change_password_form.dart b/mobile/lib/widgets/forms/change_password_form.dart index 179b05a712..7ed9fa5f1c 100644 --- a/mobile/lib/widgets/forms/change_password_form.dart +++ b/mobile/lib/widgets/forms/change_password_form.dart @@ -1,14 +1,11 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -64,10 +61,6 @@ class ChangePasswordForm extends HookConsumerWidget { if (isSuccess) { await ref.read(authProvider.notifier).logout(); - - ref.read(manualUploadProvider.notifier).cancelBackup(); - ref.read(backupProvider.notifier).cancelBackup(); - await ref.read(assetProvider.notifier).clearAllAssets(); ref.read(websocketProvider.notifier).disconnect(); AutoRouter.of(context).back(); diff --git a/mobile/lib/widgets/forms/login/email_input.dart b/mobile/lib/widgets/forms/login/email_input.dart deleted file mode 100644 index 4d90d918ac..0000000000 --- a/mobile/lib/widgets/forms/login/email_input.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -class EmailInput extends StatelessWidget { - final TextEditingController controller; - final FocusNode? focusNode; - final Function()? onSubmit; - - const EmailInput({super.key, required this.controller, this.focusNode, this.onSubmit}); - - String? _validateInput(String? email) { - if (email == null || email == '') return null; - if (email.endsWith(' ')) return 'login_form_err_trailing_whitespace'.tr(); - if (email.startsWith(' ')) return 'login_form_err_leading_whitespace'.tr(); - if (email.contains(' ') || !email.contains('@')) { - return 'login_form_err_invalid_email'.tr(); - } - return null; - } - - @override - Widget build(BuildContext context) { - return TextFormField( - autofocus: true, - controller: controller, - decoration: InputDecoration( - labelText: 'email'.tr(), - border: const OutlineInputBorder(), - hintText: 'login_form_email_hint'.tr(), - hintStyle: const TextStyle(fontWeight: FontWeight.normal, fontSize: 14), - ), - validator: _validateInput, - autovalidateMode: AutovalidateMode.always, - autofillHints: const [AutofillHints.email], - keyboardType: TextInputType.emailAddress, - onFieldSubmitted: (_) => onSubmit?.call(), - focusNode: focusNode, - textInputAction: TextInputAction.next, - ); - } -} diff --git a/mobile/lib/widgets/forms/login/loading_icon.dart b/mobile/lib/widgets/forms/login/loading_icon.dart deleted file mode 100644 index 052ce43ac7..0000000000 --- a/mobile/lib/widgets/forms/login/loading_icon.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:flutter/material.dart'; - -class LoadingIcon extends StatelessWidget { - const LoadingIcon({super.key}); - - @override - Widget build(BuildContext context) { - return const Padding( - padding: EdgeInsets.only(top: 18.0), - child: SizedBox(width: 24, height: 24, child: FittedBox(child: CircularProgressIndicator(strokeWidth: 2))), - ); - } -} diff --git a/mobile/lib/widgets/forms/login/login_button.dart b/mobile/lib/widgets/forms/login/login_button.dart deleted file mode 100644 index 0f9fb21d8f..0000000000 --- a/mobile/lib/widgets/forms/login/login_button.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -class LoginButton extends ConsumerWidget { - final Function() onPressed; - - const LoginButton({super.key, required this.onPressed}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return ElevatedButton.icon( - style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 12)), - onPressed: onPressed, - icon: const Icon(Icons.login_rounded), - label: const Text("login", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)).tr(), - ); - } -} diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index 2aa770f104..fb3b9c5977 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -17,7 +17,6 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/oauth.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; @@ -34,7 +33,6 @@ import 'package:immich_ui/immich_ui.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:permission_handler/permission_handler.dart'; class LoginForm extends HookConsumerWidget { LoginForm({super.key}); @@ -246,18 +244,14 @@ class LoginForm extends HookConsumerWidget { if (result.shouldChangePassword && !result.isAdmin) { unawaited(context.pushRoute(const ChangePasswordRoute())); } else { - final isBeta = Store.isBetaTimelineEnabled; - if (isBeta) { - await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); - if (isSyncRemoteDeletionsMode()) { - await getManageMediaPermission(); - } - unawaited(handleSyncFlow()); - ref.read(websocketProvider.notifier).connect(); - unawaited(context.replaceRoute(const TabShellRoute())); - return; + await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); + if (isSyncRemoteDeletionsMode()) { + await getManageMediaPermission(); } - unawaited(context.replaceRoute(const TabControllerRoute())); + unawaited(handleSyncFlow()); + ref.read(websocketProvider.notifier).connect(); + unawaited(context.replaceRoute(const TabShellRoute())); + return; } } catch (error) { ImmichToast.show( @@ -338,21 +332,13 @@ class LoginForm extends HookConsumerWidget { .saveAuthInfo(accessToken: loginResponseDto.accessToken); if (isSuccess) { - final permission = ref.watch(galleryPermissionNotifier); - final isBeta = Store.isBetaTimelineEnabled; - if (!isBeta && (permission.isGranted || permission.isLimited)) { - unawaited(ref.watch(backupProvider.notifier).resumeBackup()); + await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); + if (isSyncRemoteDeletionsMode()) { + await getManageMediaPermission(); } - if (isBeta) { - await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); - if (isSyncRemoteDeletionsMode()) { - await getManageMediaPermission(); - } - unawaited(handleSyncFlow()); - unawaited(context.replaceRoute(const TabShellRoute())); - return; - } - unawaited(context.replaceRoute(const TabControllerRoute())); + unawaited(handleSyncFlow()); + unawaited(context.replaceRoute(const TabShellRoute())); + return; } } catch (error, stack) { log.severe('Error logging in with OAuth: $error', stack); diff --git a/mobile/lib/widgets/map/map_app_bar.dart b/mobile/lib/widgets/map/map_app_bar.dart deleted file mode 100644 index 73706c7661..0000000000 --- a/mobile/lib/widgets/map/map_app_bar.dart +++ /dev/null @@ -1,128 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/map/map_state.provider.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/utils/selection_handlers.dart'; -import 'package:immich_mobile/widgets/map/map_settings_sheet.dart'; - -class MapAppBar extends HookWidget implements PreferredSizeWidget { - final ValueNotifier> selectedAssets; - - const MapAppBar({super.key, required this.selectedAssets}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.only(top: context.padding.top + 25), - child: ValueListenableBuilder( - valueListenable: selectedAssets, - builder: (ctx, value, child) => - value.isNotEmpty ? _SelectionRow(selectedAssets: selectedAssets) : const _NonSelectionRow(), - ), - ); - } - - @override - Size get preferredSize => const Size.fromHeight(100); -} - -class _NonSelectionRow extends StatelessWidget { - const _NonSelectionRow(); - - @override - Widget build(BuildContext context) { - void onSettingsPressed() { - showModalBottomSheet( - elevation: 0.0, - showDragHandle: true, - isScrollControlled: true, - context: context, - builder: (_) => const MapSettingsSheet(), - ); - } - - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ElevatedButton( - onPressed: () => context.maybePop(), - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.arrow_back_ios_new_rounded), - ), - ElevatedButton( - onPressed: onSettingsPressed, - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.more_vert_rounded), - ), - ], - ); - } -} - -class _SelectionRow extends HookConsumerWidget { - final ValueNotifier> selectedAssets; - - const _SelectionRow({required this.selectedAssets}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isProcessing = useProcessingOverlay(); - - Future handleProcessing(FutureOr Function() action, [bool reloadMarkers = false]) async { - isProcessing.value = true; - await action(); - // Reset state - selectedAssets.value = {}; - isProcessing.value = false; - if (reloadMarkers) { - ref.read(mapStateNotifierProvider.notifier).setRefetchMarkers(true); - } - } - - return Row( - children: [ - Padding( - padding: const EdgeInsets.only(left: 20), - child: ElevatedButton.icon( - onPressed: () => selectedAssets.value = {}, - icon: const Icon(Icons.close_rounded), - label: Text( - '${selectedAssets.value.length}', - style: context.textTheme.titleMedium?.copyWith(color: context.colorScheme.onPrimary), - ), - ), - ), - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ElevatedButton( - onPressed: () => handleProcessing(() => handleShareAssets(ref, context, selectedAssets.value.toList())), - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.ios_share_rounded), - ), - ElevatedButton( - onPressed: () => - handleProcessing(() => handleFavoriteAssets(ref, context, selectedAssets.value.toList())), - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.favorite), - ), - ElevatedButton( - onPressed: () => - handleProcessing(() => handleArchiveAssets(ref, context, selectedAssets.value.toList()), true), - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.archive), - ), - ], - ), - ), - ], - ); - } -} diff --git a/mobile/lib/widgets/map/map_asset_grid.dart b/mobile/lib/widgets/map/map_asset_grid.dart deleted file mode 100644 index b6c1e708a7..0000000000 --- a/mobile/lib/widgets/map/map_asset_grid.dart +++ /dev/null @@ -1,289 +0,0 @@ -import 'dart:math' as math; - -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/collection_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/models/map/map_event.model.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/utils/color_filter_generator.dart'; -import 'package:immich_mobile/utils/throttle.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; -import 'package:immich_mobile/widgets/common/drag_sheet.dart'; -import 'package:logging/logging.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; - -class MapAssetGrid extends HookConsumerWidget { - final Stream mapEventStream; - final Function(String)? onGridAssetChanged; - final Function(String)? onZoomToAsset; - final Function(bool, Set)? onAssetsSelected; - final ValueNotifier> selectedAssets; - final ScrollController controller; - - const MapAssetGrid({ - required this.mapEventStream, - this.onGridAssetChanged, - this.onZoomToAsset, - this.onAssetsSelected, - required this.selectedAssets, - required this.controller, - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final log = Logger("MapAssetGrid"); - final assetsInBounds = useState>([]); - final cachedRenderList = useRef(null); - final lastRenderElementIndex = useRef(null); - final assetInSheet = useValueNotifier(null); - final gridScrollThrottler = useThrottler(interval: const Duration(milliseconds: 300)); - - // Add a cache for assets we've already loaded - final assetCache = useRef>({}); - - void handleMapEvents(MapEvent event) async { - if (event is MapAssetsInBoundsUpdated) { - final assetIds = event.assetRemoteIds; - final missingIds = []; - final currentAssets = []; - - for (final id in assetIds) { - final asset = assetCache.value[id]; - if (asset != null) { - currentAssets.add(asset); - } else { - missingIds.add(id); - } - } - - // Only fetch missing assets - if (missingIds.isNotEmpty) { - final newAssets = await ref.read(dbProvider).assets.getAllByRemoteId(missingIds); - - // Add new assets to cache and current list - for (final asset in newAssets) { - if (asset.remoteId != null) { - assetCache.value[asset.remoteId!] = asset; - currentAssets.add(asset); - } - } - } - - assetsInBounds.value = currentAssets; - return; - } - } - - useOnStreamChange(mapEventStream, onData: handleMapEvents); - - // Hard-restrict to 4 assets / row in portrait mode - const assetsPerRow = 4; - - void handleVisibleItems(Iterable positions) { - final orderedPos = positions.sortedByField((p) => p.index); - // Index of row where the items are mostly visible - const partialOffset = 0.20; - final item = orderedPos.firstWhereOrNull((p) => p.itemTrailingEdge > partialOffset); - - // Guard no elements, reset state - // Also fail fast when the sheet is just opened and the user is yet to scroll (i.e leading = 0) - if (item == null || item.itemLeadingEdge == 0) { - lastRenderElementIndex.value = null; - return; - } - - final renderElement = cachedRenderList.value?.elements.elementAtOrNull(item.index); - // Guard no render list or render element - if (renderElement == null) { - return; - } - // Reset index - lastRenderElementIndex.value == item.index; - - // - // | 1 | 2 | 3 | 4 | 5 | 6 | - // - // | 7 | 8 | 9 | - // - // | 10 | - - // Skip through the assets from the previous row - final rowOffset = renderElement.offset; - // Column offset = (total trailingEdge - trailingEdge crossed) / offset for each asset - final totalOffset = item.itemTrailingEdge - item.itemLeadingEdge; - final edgeOffset = - (totalOffset - partialOffset) / - // Round the total count to the next multiple of [assetsPerRow] - ((renderElement.totalCount / assetsPerRow) * assetsPerRow).floor(); - - // trailing should never be above the totalOffset - final columnOffset = (totalOffset - math.min(item.itemTrailingEdge, totalOffset)) ~/ edgeOffset; - final assetOffset = rowOffset + columnOffset; - final selectedAsset = cachedRenderList.value?.allAssets?.elementAtOrNull(assetOffset)?.remoteId; - - if (selectedAsset != null) { - onGridAssetChanged?.call(selectedAsset); - assetInSheet.value = selectedAsset; - } - } - - return Card( - margin: EdgeInsets.zero, - child: Stack( - children: [ - /// The Align and FractionallySizedBox are to prevent the Asset Grid from going behind the - /// _MapSheetDragRegion and thereby displaying content behind the top right and top left curves - Align( - alignment: Alignment.bottomCenter, - child: FractionallySizedBox( - // Place it just below the drag handle - heightFactor: 0.87, - child: assetsInBounds.value.isNotEmpty - ? ref - .watch(assetsTimelineProvider(assetsInBounds.value)) - .when( - data: (renderList) { - // Cache render list here to use it back during visibleItemsListener - cachedRenderList.value = renderList; - return ValueListenableBuilder( - valueListenable: selectedAssets, - builder: (_, value, __) => ImmichAssetGrid( - shrinkWrap: true, - renderList: renderList, - showDragScroll: false, - assetsPerRow: assetsPerRow, - showMultiSelectIndicator: false, - selectionActive: value.isNotEmpty, - listener: onAssetsSelected, - visibleItemsListener: (pos) => gridScrollThrottler.run(() => handleVisibleItems(pos)), - ), - ); - }, - error: (error, stackTrace) { - log.warning("Cannot get assets in the current map bounds", error, stackTrace); - return const SizedBox.shrink(); - }, - loading: () => const SizedBox.shrink(), - ) - : const _MapNoAssetsInSheet(), - ), - ), - _MapSheetDragRegion( - controller: controller, - assetsInBoundCount: assetsInBounds.value.length, - assetInSheet: assetInSheet, - onZoomToAsset: onZoomToAsset, - ), - ], - ), - ); - } -} - -class _MapNoAssetsInSheet extends StatelessWidget { - const _MapNoAssetsInSheet(); - - @override - Widget build(BuildContext context) { - const image = Image(height: 150, width: 150, image: AssetImage('assets/lighthouse.png')); - - return Center( - child: ListView( - shrinkWrap: true, - children: [ - context.isDarkTheme - ? const InvertionFilter( - child: SaturationFilter(saturation: -1, child: BrightnessFilter(brightness: -5, child: image)), - ) - : image, - const SizedBox(height: 20), - Center( - child: Text("map_zoom_to_see_photos".tr(), style: context.textTheme.displayLarge?.copyWith(fontSize: 18)), - ), - ], - ), - ); - } -} - -class _MapSheetDragRegion extends StatelessWidget { - final ScrollController controller; - final int assetsInBoundCount; - final ValueNotifier assetInSheet; - final Function(String)? onZoomToAsset; - - const _MapSheetDragRegion({ - required this.controller, - required this.assetsInBoundCount, - required this.assetInSheet, - this.onZoomToAsset, - }); - - @override - Widget build(BuildContext context) { - final assetsInBoundsText = "map_assets_in_bounds".t(context: context, args: {'count': assetsInBoundCount}); - - return SingleChildScrollView( - controller: controller, - physics: const ClampingScrollPhysics(), - child: Card( - margin: EdgeInsets.zero, - shape: context.isMobile - ? const RoundedRectangleBorder( - borderRadius: BorderRadius.only(topRight: Radius.circular(20), topLeft: Radius.circular(20)), - ) - : const BeveledRectangleBorder(), - elevation: 0.0, - child: Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(height: 15), - const CustomDraggingHandle(), - const SizedBox(height: 15), - Center( - child: Text( - assetsInBoundsText, - style: TextStyle( - fontSize: 20, - color: context.textTheme.displayLarge?.color?.withValues(alpha: 0.75), - fontWeight: FontWeight.w500, - ), - ), - ), - const SizedBox(height: 8), - ], - ), - ValueListenableBuilder( - valueListenable: assetInSheet, - builder: (_, value, __) => Visibility( - visible: value != null, - child: Positioned( - right: 18, - top: 24, - child: IconButton( - icon: Icon(Icons.map_outlined, color: context.textTheme.displayLarge?.color), - iconSize: 24, - tooltip: 'zoom_to_bounds'.tr(), - onPressed: () => onZoomToAsset?.call(value!), - ), - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/map/map_bottom_sheet.dart b/mobile/lib/widgets/map/map_bottom_sheet.dart deleted file mode 100644 index fba9e9a041..0000000000 --- a/mobile/lib/widgets/map/map_bottom_sheet.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/map/map_event.model.dart'; -import 'package:immich_mobile/utils/draggable_scroll_controller.dart'; -import 'package:immich_mobile/widgets/map/map_asset_grid.dart'; - -class MapBottomSheet extends HookConsumerWidget { - final Stream mapEventStream; - final Function(String)? onGridAssetChanged; - final Function(String)? onZoomToAsset; - final Function()? onZoomToLocation; - final Function(bool, Set)? onAssetsSelected; - final ValueNotifier> selectedAssets; - - const MapBottomSheet({ - required this.mapEventStream, - this.onGridAssetChanged, - this.onZoomToAsset, - this.onAssetsSelected, - this.onZoomToLocation, - required this.selectedAssets, - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - const sheetMinExtent = 0.1; - final sheetController = useDraggableScrollController(); - final bottomSheetOffset = useValueNotifier(sheetMinExtent); - final isBottomSheetOpened = useRef(false); - - void handleMapEvents(MapEvent event) async { - if (event is MapCloseBottomSheet) { - await sheetController.animateTo( - 0.1, - duration: const Duration(milliseconds: 200), - curve: Curves.linearToEaseOut, - ); - } - } - - useOnStreamChange(mapEventStream, onData: handleMapEvents); - - bool onScrollNotification(DraggableScrollableNotification notification) { - isBottomSheetOpened.value = notification.extent > (notification.maxExtent * 0.9); - bottomSheetOffset.value = notification.extent; - // do not bubble - return true; - } - - return Stack( - children: [ - NotificationListener( - onNotification: onScrollNotification, - child: DraggableScrollableSheet( - controller: sheetController, - minChildSize: sheetMinExtent, - maxChildSize: 0.8, - initialChildSize: sheetMinExtent, - snap: true, - snapSizes: [sheetMinExtent, 0.5, 0.8], - shouldCloseOnMinExtent: false, - builder: (ctx, scrollController) => MapAssetGrid( - controller: scrollController, - mapEventStream: mapEventStream, - selectedAssets: selectedAssets, - onAssetsSelected: onAssetsSelected, - // Do not bother with the event if the bottom sheet is not user scrolled - onGridAssetChanged: (assetId) => isBottomSheetOpened.value ? onGridAssetChanged?.call(assetId) : null, - onZoomToAsset: onZoomToAsset, - ), - ), - ), - ValueListenableBuilder( - valueListenable: bottomSheetOffset, - builder: (context, value, child) { - return Positioned( - right: 0, - bottom: context.height * (value + 0.02), - child: AnimatedOpacity( - opacity: value < 0.8 ? 1 : 0, - duration: const Duration(milliseconds: 150), - child: ElevatedButton( - onPressed: onZoomToLocation, - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.my_location), - ), - ), - ); - }, - ), - ], - ); - } -} diff --git a/mobile/lib/widgets/map/map_settings_sheet.dart b/mobile/lib/widgets/map/map_settings_sheet.dart deleted file mode 100644 index 644056d153..0000000000 --- a/mobile/lib/widgets/map/map_settings_sheet.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/map/map_state.provider.dart'; -import 'package:immich_mobile/widgets/map/map_settings/map_settings_list_tile.dart'; -import 'package:immich_mobile/widgets/map/map_settings/map_settings_time_dropdown.dart'; -import 'package:immich_mobile/widgets/map/map_settings/map_theme_picker.dart'; - -class MapSettingsSheet extends HookConsumerWidget { - const MapSettingsSheet({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final mapState = ref.watch(mapStateNotifierProvider); - - return DraggableScrollableSheet( - expand: false, - initialChildSize: 0.6, - builder: (ctx, scrollController) => SingleChildScrollView( - controller: scrollController, - child: Card( - elevation: 0.0, - shadowColor: Colors.transparent, - margin: EdgeInsets.zero, - child: Column( - mainAxisSize: MainAxisSize.max, - children: [ - MapThemePicker( - themeMode: mapState.themeMode, - onThemeChange: (mode) => ref.read(mapStateNotifierProvider.notifier).switchTheme(mode), - ), - const Divider(height: 30, thickness: 2), - MapSettingsListTile( - title: "map_settings_only_show_favorites", - selected: mapState.showFavoriteOnly, - onChanged: (favoriteOnly) => - ref.read(mapStateNotifierProvider.notifier).switchFavoriteOnly(favoriteOnly), - ), - MapSettingsListTile( - title: "map_settings_include_show_archived", - selected: mapState.includeArchived, - onChanged: (includeArchive) => - ref.read(mapStateNotifierProvider.notifier).switchIncludeArchived(includeArchive), - ), - MapSettingsListTile( - title: "map_settings_include_show_partners", - selected: mapState.withPartners, - onChanged: (withPartners) => - ref.read(mapStateNotifierProvider.notifier).switchWithPartners(withPartners), - ), - MapTimeDropDown( - relativeTime: mapState.relativeTime, - onTimeChange: (time) => ref.read(mapStateNotifierProvider.notifier).setRelativeTime(time), - ), - const SizedBox(height: 20), - ], - ), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/map/positioned_asset_marker_icon.dart b/mobile/lib/widgets/map/positioned_asset_marker_icon.dart deleted file mode 100644 index b6d7241cf4..0000000000 --- a/mobile/lib/widgets/map/positioned_asset_marker_icon.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'dart:io'; -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/widgets/map/asset_marker_icon.dart'; - -class PositionedAssetMarkerIcon extends StatelessWidget { - final Point point; - final String assetRemoteId; - final String assetThumbhash; - final double size; - final int durationInMilliseconds; - - final Function()? onTap; - - const PositionedAssetMarkerIcon({ - required this.point, - required this.assetRemoteId, - required this.assetThumbhash, - this.size = 100, - this.durationInMilliseconds = 100, - this.onTap, - super.key, - }); - - @override - Widget build(BuildContext context) { - final ratio = Platform.isIOS ? 1.0 : context.devicePixelRatio; - return AnimatedPositioned( - left: point.x / ratio - size / 2, - top: point.y / ratio - size, - duration: Duration(milliseconds: durationInMilliseconds), - child: GestureDetector( - onTap: () => onTap?.call(), - child: SizedBox.square( - dimension: size, - child: AssetMarkerIcon(id: assetRemoteId, thumbhash: assetThumbhash, key: Key(assetRemoteId)), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/memories/memory_bottom_info.dart b/mobile/lib/widgets/memories/memory_bottom_info.dart deleted file mode 100644 index 4b43821782..0000000000 --- a/mobile/lib/widgets/memories/memory_bottom_info.dart +++ /dev/null @@ -1,50 +0,0 @@ -// ignore_for_file: require_trailing_commas - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/models/memories/memory.model.dart'; -import 'package:immich_mobile/providers/asset_viewer/scroll_to_date_notifier.provider.dart'; - -class MemoryBottomInfo extends StatelessWidget { - final Memory memory; - - const MemoryBottomInfo({super.key, required this.memory}); - - @override - Widget build(BuildContext context) { - final df = DateFormat.yMMMMd(); - return Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - memory.title, - style: TextStyle(color: Colors.grey[400], fontSize: 13.0, fontWeight: FontWeight.w500), - ), - Text( - df.format(memory.assets[0].fileCreatedAt), - style: const TextStyle(color: Colors.white, fontSize: 15.0, fontWeight: FontWeight.w500), - ), - ], - ), - MaterialButton( - minWidth: 0, - onPressed: () { - context.maybePop(); - scrollToDateNotifierProvider.scrollToDate(memory.assets[0].fileCreatedAt); - }, - shape: const CircleBorder(), - color: Colors.white.withValues(alpha: 0.2), - elevation: 0, - child: const Icon(Icons.open_in_new, color: Colors.white), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/memories/memory_card.dart b/mobile/lib/widgets/memories/memory_card.dart deleted file mode 100644 index 189cc67428..0000000000 --- a/mobile/lib/widgets/memories/memory_card.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'dart:ui'; - -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/pages/common/native_video_viewer.page.dart'; -import 'package:immich_mobile/utils/hooks/blurhash_hook.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; - -class MemoryCard extends StatelessWidget { - final Asset asset; - final String title; - final bool showTitle; - final Function()? onVideoEnded; - - const MemoryCard({required this.asset, required this.title, required this.showTitle, this.onVideoEnded, super.key}); - - @override - Widget build(BuildContext context) { - return Card( - color: Colors.black, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(25.0)), - side: BorderSide(color: Colors.black, width: 1.0), - ), - clipBehavior: Clip.hardEdge, - child: Stack( - children: [ - SizedBox.expand(child: _BlurredBackdrop(asset: asset)), - LayoutBuilder( - builder: (context, constraints) { - // Determine the fit using the aspect ratio - BoxFit fit = BoxFit.contain; - if (asset.width != null && asset.height != null) { - final aspectRatio = asset.width! / asset.height!; - final phoneAspectRatio = constraints.maxWidth / constraints.maxHeight; - // Look for a 25% difference in either direction - if (phoneAspectRatio * .75 < aspectRatio && phoneAspectRatio * 1.25 > aspectRatio) { - // Cover to look nice if we have nearly the same aspect ratio - fit = BoxFit.cover; - } - } - - if (asset.isImage) { - return Hero( - tag: 'memory-${asset.id}', - child: ImmichImage(asset, fit: fit, height: double.infinity, width: double.infinity), - ); - } else { - return Hero( - tag: 'memory-${asset.id}', - child: SizedBox( - width: context.width, - height: context.height, - child: NativeVideoViewerPage( - key: ValueKey(asset.id), - asset: asset, - showControls: false, - playbackDelayFactor: 2, - image: ImmichImage(asset, width: context.width, height: context.height, fit: BoxFit.contain), - ), - ), - ); - } - }, - ), - if (showTitle) - Positioned( - left: 18.0, - bottom: 18.0, - child: Text( - title, - style: context.textTheme.headlineMedium?.copyWith(color: Colors.white, fontWeight: FontWeight.w500), - ), - ), - ], - ), - ); - } -} - -class _BlurredBackdrop extends HookWidget { - final Asset asset; - - const _BlurredBackdrop({required this.asset}); - - @override - Widget build(BuildContext context) { - final blurhash = useBlurHashRef(asset).value; - if (blurhash != null) { - // Use a nice cheap blur hash image decoration - return Container( - decoration: BoxDecoration( - image: DecorationImage(image: MemoryImage(blurhash), fit: BoxFit.cover), - ), - child: Container(color: Colors.black.withValues(alpha: 0.2)), - ); - } else { - // Fall back to using a more expensive image filtered - // Since the ImmichImage is already precached, we can - // safely use that as the image provider - return ImageFiltered( - imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), - child: Container( - decoration: BoxDecoration( - image: DecorationImage( - image: ImmichImage.imageProvider(asset: asset, height: context.height, width: context.width), - fit: BoxFit.cover, - ), - ), - child: Container(color: Colors.black.withValues(alpha: 0.2)), - ), - ); - } - } -} diff --git a/mobile/lib/widgets/memories/memory_lane.dart b/mobile/lib/widgets/memories/memory_lane.dart deleted file mode 100644 index 4cba83bea7..0000000000 --- a/mobile/lib/widgets/memories/memory_lane.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/memories/memory.model.dart'; -import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/memory.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; - -class MemoryLane extends HookConsumerWidget { - const MemoryLane({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final memoryLaneFutureProvider = ref.watch(memoryFutureProvider); - - final memoryLane = memoryLaneFutureProvider - .whenData( - (memories) => memories != null - ? ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 200), - child: CarouselView( - itemExtent: 145.0, - shrinkExtent: 1.0, - elevation: 2, - backgroundColor: Colors.black, - overlayColor: WidgetStateProperty.all(Colors.white.withValues(alpha: 0.1)), - onTap: (memoryIndex) { - ref.read(hapticFeedbackProvider.notifier).heavyImpact(); - if (memories[memoryIndex].assets.isNotEmpty) { - final asset = memories[memoryIndex].assets[0]; - ref.read(currentAssetProvider.notifier).set(asset); - } - context.pushRoute(MemoryRoute(memories: memories, memoryIndex: memoryIndex)); - }, - children: memories - .mapIndexed((index, memory) => MemoryCard(index: index, memory: memory)) - .toList(), - ), - ) - : const SizedBox(), - ) - .value; - - return memoryLane ?? const SizedBox(); - } -} - -class MemoryCard extends ConsumerWidget { - const MemoryCard({super.key, required this.index, required this.memory}); - - final int index; - final Memory memory; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Center( - child: Stack( - children: [ - ColorFiltered( - colorFilter: ColorFilter.mode(Colors.black.withValues(alpha: 0.2), BlendMode.darken), - child: Hero( - tag: 'memory-${memory.assets[0].id}', - child: ImmichImage( - memory.assets[0], - fit: BoxFit.cover, - width: 205, - height: 200, - placeholder: const ThumbnailPlaceholder(width: 105, height: 200), - ), - ), - ), - Positioned( - bottom: 16, - left: 16, - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 114), - child: Text( - memory.title, - style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.white, fontSize: 15), - ), - ), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/search/curated_people_row.dart b/mobile/lib/widgets/search/curated_people_row.dart deleted file mode 100644 index 9155de2131..0000000000 --- a/mobile/lib/widgets/search/curated_people_row.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; - -class CuratedPeopleRow extends StatelessWidget { - static const double imageSize = 60.0; - - final List content; - final EdgeInsets? padding; - - /// Callback with the content and the index when tapped - final Function(SearchCuratedContent, int)? onTap; - final Function(SearchCuratedContent, int)? onNameTap; - - const CuratedPeopleRow({super.key, required this.content, this.onTap, this.padding, required this.onNameTap}); - - @override - Widget build(BuildContext context) { - return SizedBox( - width: double.infinity, - child: SingleChildScrollView( - padding: padding, - scrollDirection: Axis.horizontal, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: List.generate(content.length, (index) { - final person = content[index]; - return Padding( - padding: const EdgeInsets.only(right: 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - GestureDetector( - onTap: () => onTap?.call(person, index), - child: SizedBox( - height: imageSize, - child: Material( - shape: const CircleBorder(side: BorderSide.none), - elevation: 3, - child: CircleAvatar( - maxRadius: imageSize / 2, - backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(person.id)), - ), - ), - ), - ), - const SizedBox(height: 8), - SizedBox(width: imageSize, child: _buildPersonLabel(context, person, index)), - ], - ), - ); - }), - ), - ), - ); - } - - Widget _buildPersonLabel(BuildContext context, SearchCuratedContent person, int index) { - if (person.label.isEmpty) { - return GestureDetector( - onTap: () => onNameTap?.call(person, index), - child: Text( - "exif_bottom_sheet_person_add_person", - style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor), - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - ).tr(), - ); - } - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - person.label, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - style: context.textTheme.labelLarge, - maxLines: 2, - ), - if (person.subtitle != null) Text(person.subtitle!, textAlign: TextAlign.center), - ], - ); - } -} diff --git a/mobile/lib/widgets/search/curated_places_row.dart b/mobile/lib/widgets/search/curated_places_row.dart deleted file mode 100644 index 9d21292bde..0000000000 --- a/mobile/lib/widgets/search/curated_places_row.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/widgets/search/search_map_thumbnail.dart'; -import 'package:immich_mobile/widgets/search/thumbnail_with_info.dart'; - -class CuratedPlacesRow extends StatelessWidget { - const CuratedPlacesRow({ - super.key, - required this.content, - required this.imageSize, - this.isMapEnabled = true, - this.onTap, - }); - - final bool isMapEnabled; - final List content; - final double imageSize; - - /// Callback with the content and the index when tapped - final Function(SearchCuratedContent, int)? onTap; - - @override - Widget build(BuildContext context) { - // Calculating the actual index of the content based on the whether map is enabled or not. - // If enabled, inject map as the first item in the list (index 0) and so the actual content will start from index 1 - final int actualContentIndex = isMapEnabled ? 1 : 0; - - return SizedBox( - height: imageSize, - child: ListView.separated( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 16), - separatorBuilder: (context, index) => const SizedBox(width: 10), - itemBuilder: (context, index) { - // Injecting Map thumbnail as the first element - if (isMapEnabled && index == 0) { - return SizedBox.square( - dimension: imageSize, - child: SearchMapThumbnail(size: imageSize), - ); - } - final actualIndex = index - actualContentIndex; - final object = content[actualIndex]; - final thumbnailRequestUrl = '${Store.get(StoreKey.serverEndpoint)}/assets/${object.id}/thumbnail'; - return SizedBox.square( - dimension: imageSize, - child: ThumbnailWithInfo( - imageUrl: thumbnailRequestUrl, - textInfo: object.label, - onTap: () => onTap?.call(object, actualIndex), - ), - ); - }, - itemCount: content.length + actualContentIndex, - ), - ); - } -} diff --git a/mobile/lib/widgets/search/explore_grid.dart b/mobile/lib/widgets/search/explore_grid.dart deleted file mode 100644 index 6af20df029..0000000000 --- a/mobile/lib/widgets/search/explore_grid.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/models/search/search_filter.model.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; -import 'package:immich_mobile/widgets/search/thumbnail_with_info.dart'; - -class ExploreGrid extends StatelessWidget { - final List curatedContent; - final bool isPeople; - - const ExploreGrid({super.key, required this.curatedContent, this.isPeople = false}); - - @override - Widget build(BuildContext context) { - if (curatedContent.isEmpty) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: SizedBox( - height: 100, - width: 100, - child: ThumbnailWithInfo(textInfo: '', onTap: () {}), - ), - ); - } - - return GridView.builder( - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 140, - mainAxisSpacing: 4, - crossAxisSpacing: 4, - ), - itemBuilder: (context, index) { - final content = curatedContent[index]; - final thumbnailRequestUrl = isPeople - ? getFaceThumbnailUrl(content.id) - : '${Store.get(StoreKey.serverEndpoint)}/assets/${content.id}/thumbnail'; - - return ThumbnailWithInfo( - imageUrl: thumbnailRequestUrl, - textInfo: content.label, - borderRadius: 0, - onTap: () { - isPeople - ? context.pushRoute(PersonResultRoute(personId: content.id, personName: content.label)) - : context.pushRoute( - SearchRoute( - prefilter: SearchFilter( - people: {}, - location: SearchLocationFilter(city: content.label), - camera: SearchCameraFilter(), - date: SearchDateFilter(), - display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), - rating: SearchRatingFilter(), - mediaType: AssetType.other, - ), - ), - ); - }, - ); - }, - itemCount: curatedContent.length, - ); - } -} diff --git a/mobile/lib/widgets/search/person_name_edit_form.dart b/mobile/lib/widgets/search/person_name_edit_form.dart deleted file mode 100644 index 3fa443121a..0000000000 --- a/mobile/lib/widgets/search/person_name_edit_form.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/search/people.provider.dart'; - -class PersonNameEditFormResult { - final bool success; - final String updatedName; - - const PersonNameEditFormResult(this.success, this.updatedName); -} - -class PersonNameEditForm extends HookConsumerWidget { - final String personId; - final String personName; - - const PersonNameEditForm({super.key, required this.personId, required this.personName}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final controller = useTextEditingController(text: personName); - final isError = useState(false); - - return AlertDialog( - title: const Text("add_a_name", style: TextStyle(fontWeight: FontWeight.bold)).tr(), - content: SingleChildScrollView( - child: TextFormField( - controller: controller, - textCapitalization: TextCapitalization.words, - autofocus: true, - decoration: InputDecoration( - hintText: 'name'.tr(), - border: const OutlineInputBorder(), - errorText: isError.value ? 'Error occurred' : null, - ), - ), - ), - actions: [ - TextButton( - onPressed: () => context.pop(const PersonNameEditFormResult(false, '')), - child: Text( - "cancel", - style: TextStyle(color: Colors.red[300], fontWeight: FontWeight.bold), - ).tr(), - ), - TextButton( - onPressed: () async { - isError.value = false; - final result = await ref.read(updatePersonNameProvider(personId, controller.text).future); - isError.value = !result; - if (result) { - context.pop(PersonNameEditFormResult(true, controller.text)); - } - }, - child: Text( - "save", - style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold), - ).tr(), - ), - ], - ); - } -} diff --git a/mobile/lib/widgets/search/search_filter/media_type_picker.dart b/mobile/lib/widgets/search/search_filter/media_type_picker.dart index e0e34b654e..ac89de8190 100644 --- a/mobile/lib/widgets/search/search_filter/media_type_picker.dart +++ b/mobile/lib/widgets/search/search_filter/media_type_picker.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; class MediaTypePicker extends HookWidget { const MediaTypePicker({super.key, required this.onSelect, this.filter}); diff --git a/mobile/lib/widgets/search/search_filter/people_picker.dart b/mobile/lib/widgets/search/search_filter/people_picker.dart index 978b70239c..a7b0286df3 100644 --- a/mobile/lib/widgets/search/search_filter/people_picker.dart +++ b/mobile/lib/widgets/search/search_filter/people_picker.dart @@ -57,6 +57,7 @@ class PeoplePicker extends HookConsumerWidget { final isSelected = selectedPeople.value.contains(person); return Padding( + key: ValueKey(person.id), padding: const EdgeInsets.only(bottom: 2.0), child: LargeLeadingTile( title: Text( @@ -73,6 +74,7 @@ class PeoplePicker extends HookConsumerWidget { shape: const CircleBorder(side: BorderSide.none), elevation: 3, child: CircleAvatar( + key: ValueKey(person.id), maxRadius: imageSize / 2, backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(person.id)), ), diff --git a/mobile/lib/widgets/search/search_map_thumbnail.dart b/mobile/lib/widgets/search/search_map_thumbnail.dart deleted file mode 100644 index 7533e46f1a..0000000000 --- a/mobile/lib/widgets/search/search_map_thumbnail.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; -import 'package:immich_mobile/widgets/search/thumbnail_with_info_container.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; - -class SearchMapThumbnail extends StatelessWidget { - const SearchMapThumbnail({super.key, this.size = 60.0}); - - final double size; - final bool showTitle = true; - - @override - Widget build(BuildContext context) { - return ThumbnailWithInfoContainer( - label: 'search_page_your_map'.tr(), - onTap: () { - context.pushRoute(MapRoute()); - }, - child: IgnorePointer( - child: MapThumbnail(zoom: 2, centre: const LatLng(47, 5), height: size, width: size, showAttribution: false), - ), - ); - } -} diff --git a/mobile/lib/widgets/search/search_row_section.dart b/mobile/lib/widgets/search/search_row_section.dart deleted file mode 100644 index b8584fefef..0000000000 --- a/mobile/lib/widgets/search/search_row_section.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/widgets/search/search_row_title.dart'; - -class SearchRowSection extends StatelessWidget { - const SearchRowSection({ - super.key, - required this.onViewAllPressed, - required this.title, - this.isEmpty = false, - required this.child, - }); - - final Function() onViewAllPressed; - final String title; - final bool isEmpty; - final Widget child; - - @override - Widget build(BuildContext context) { - if (isEmpty) { - return const SizedBox.shrink(); - } - - return Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: SearchRowTitle(onViewAllPressed: onViewAllPressed, title: title), - ), - child, - ], - ); - } -} diff --git a/mobile/lib/widgets/settings/advanced_settings.dart b/mobile/lib/widgets/settings/advanced_settings.dart index d5905a246c..a38ccd3556 100644 --- a/mobile/lib/widgets/settings/advanced_settings.dart +++ b/mobile/lib/widgets/settings/advanced_settings.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; @@ -14,9 +13,7 @@ import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; -import 'package:immich_mobile/widgets/settings/beta_timeline_list_tile.dart'; import 'package:immich_mobile/widgets/settings/custom_proxy_headers_settings/custom_proxy_headers_settings.dart'; -import 'package:immich_mobile/widgets/settings/local_storage_settings.dart'; import 'package:immich_mobile/widgets/settings/settings_action_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart'; @@ -35,7 +32,6 @@ class AdvancedSettings extends HookConsumerWidget { final manageMediaAndroidPermission = useState(false); final levelId = useAppSettingsState(AppSettingsEnum.logLevel); final preferRemote = useAppSettingsState(AppSettingsEnum.preferRemoteImage); - final useAlternatePMFilter = useAppSettingsState(AppSettingsEnum.photoManagerCustomFilter); final readonlyModeEnabled = useAppSettingsState(AppSettingsEnum.readonlyModeEnabled); final logLevel = Level.LEVELS[levelId.value].name; @@ -114,35 +110,26 @@ class AdvancedSettings extends HookConsumerWidget { title: "advanced_settings_prefer_remote_title".tr(), subtitle: "advanced_settings_prefer_remote_subtitle".tr(), ), - if (!Store.isBetaTimelineEnabled) const LocalStorageSettings(), const CustomProxyHeaderSettings(), const SslClientCertSettings(), - if (!Store.isBetaTimelineEnabled) - SettingsSwitchListTile( - valueNotifier: useAlternatePMFilter, - title: "advanced_settings_enable_alternate_media_filter_title".tr(), - subtitle: "advanced_settings_enable_alternate_media_filter_subtitle".tr(), - ), - if (!Store.isBetaTimelineEnabled) const BetaTimelineListTile(), - if (Store.isBetaTimelineEnabled) - SettingsSwitchListTile( - valueNotifier: readonlyModeEnabled, - title: "advanced_settings_readonly_mode_title".tr(), - subtitle: "advanced_settings_readonly_mode_subtitle".tr(), - onChanged: (value) { - readonlyModeEnabled.value = value; - ref.read(readonlyModeProvider.notifier).setReadonlyMode(value); - context.scaffoldMessenger.showSnackBar( - SnackBar( - duration: const Duration(seconds: 2), - content: Text( - (value ? "readonly_mode_enabled" : "readonly_mode_disabled").tr(), - style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), - ), + SettingsSwitchListTile( + valueNotifier: readonlyModeEnabled, + title: "advanced_settings_readonly_mode_title".tr(), + subtitle: "advanced_settings_readonly_mode_subtitle".tr(), + onChanged: (value) { + readonlyModeEnabled.value = value; + ref.read(readonlyModeProvider.notifier).setReadonlyMode(value); + context.scaffoldMessenger.showSnackBar( + SnackBar( + duration: const Duration(seconds: 2), + content: Text( + (value ? "readonly_mode_enabled" : "readonly_mode_disabled").tr(), + style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), ), - ); - }, - ), + ), + ); + }, + ), ListTile( title: Text("advanced_settings_clear_image_cache".tr(), style: const TextStyle(fontWeight: FontWeight.w500)), leading: const Icon(Icons.playlist_remove_rounded), diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart index 08e66df48d..42ea3acfc0 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart @@ -2,11 +2,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_radio_list_tile.dart'; diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart index 2d5c9f06eb..55c8195947 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart @@ -1,21 +1,18 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; class LayoutSettings extends HookConsumerWidget { const LayoutSettings({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final useDynamicLayout = useAppSettingsState(AppSettingsEnum.dynamicLayout); final tilesPerRow = useAppSettingsState(AppSettingsEnum.tilesPerRow); return Column( @@ -25,12 +22,6 @@ class LayoutSettings extends HookConsumerWidget { title: "asset_list_layout_sub_title".t(context: context), icon: Icons.view_module_outlined, ), - if (!Store.isBetaTimelineEnabled) - SettingsSwitchListTile( - valueNotifier: useDynamicLayout, - title: "asset_list_layout_settings_dynamic_layout_title".t(context: context), - onChanged: (_) => ref.invalidate(appSettingsServiceProvider), - ), SettingsSliderListTile( valueNotifier: tilesPerRow, text: 'theme_setting_asset_list_tiles_per_row_title'.tr(namedArgs: {'count': "${tilesPerRow.value}"}), diff --git a/mobile/lib/widgets/settings/backup_settings/background_settings.dart b/mobile/lib/widgets/settings/backup_settings/background_settings.dart deleted file mode 100644 index 038a567dc2..0000000000 --- a/mobile/lib/widgets/settings/backup_settings/background_settings.dart +++ /dev/null @@ -1,204 +0,0 @@ -import 'dart:io'; - -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart'; -import 'package:immich_mobile/widgets/backup/ios_debug_info_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_button_list_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class BackgroundBackupSettings extends ConsumerWidget { - const BackgroundBackupSettings({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isBackgroundEnabled = ref.watch(backupProvider.select((s) => s.backgroundBackup)); - final iosSettings = ref.watch(iOSBackgroundSettingsProvider); - - void showErrorToUser(String msg) { - final snackBar = SnackBar( - content: Text(msg.tr(), style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor)), - backgroundColor: Colors.red, - ); - context.scaffoldMessenger.showSnackBar(snackBar); - } - - void showBatteryOptimizationInfoToUser() { - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext ctx) { - return AlertDialog( - title: const Text('backup_controller_page_background_battery_info_title').tr(), - content: SingleChildScrollView( - child: const Text('backup_controller_page_background_battery_info_message').tr(), - ), - actions: [ - ElevatedButton( - onPressed: () => - launchUrl(Uri.parse('https://dontkillmyapp.com'), mode: LaunchMode.externalApplication), - child: const Text( - "backup_controller_page_background_battery_info_link", - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12), - ).tr(), - ), - ElevatedButton( - child: const Text( - 'backup_controller_page_background_battery_info_ok', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12), - ).tr(), - onPressed: () => ctx.pop(), - ), - ], - ); - }, - ); - } - - if (!isBackgroundEnabled) { - return SettingsButtonListTile( - icon: Icons.cloud_sync_outlined, - title: 'backup_controller_page_background_is_off'.tr(), - subtileText: 'backup_controller_page_background_description'.tr(), - buttonText: 'backup_controller_page_background_turn_on'.tr(), - onButtonTap: () => ref - .read(backupProvider.notifier) - .configureBackgroundBackup( - enabled: true, - onError: showErrorToUser, - onBatteryInfo: showBatteryOptimizationInfoToUser, - ), - ); - } - - return Column( - children: [ - if (!Platform.isIOS || iosSettings?.appRefreshEnabled == true) - _BackgroundSettingsEnabled(onError: showErrorToUser, onBatteryInfo: showBatteryOptimizationInfoToUser), - if (Platform.isIOS && iosSettings?.appRefreshEnabled != true) const _IOSBackgroundRefreshDisabled(), - if (Platform.isIOS && iosSettings != null) IosDebugInfoTile(settings: iosSettings), - ], - ); - } -} - -class _IOSBackgroundRefreshDisabled extends StatelessWidget { - const _IOSBackgroundRefreshDisabled(); - - @override - Widget build(BuildContext context) { - return SettingsButtonListTile( - icon: Icons.task_outlined, - title: 'backup_controller_page_background_app_refresh_disabled_title'.tr(), - subtileText: 'backup_controller_page_background_app_refresh_disabled_content'.tr(), - buttonText: 'backup_controller_page_background_app_refresh_enable_button_text'.tr(), - onButtonTap: () => openAppSettings(), - ); - } -} - -class _BackgroundSettingsEnabled extends HookConsumerWidget { - final void Function(String msg) onError; - final void Function() onBatteryInfo; - - const _BackgroundSettingsEnabled({required this.onError, required this.onBatteryInfo}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isWifiRequired = ref.watch(backupProvider.select((s) => s.backupRequireWifi)); - final isWifiRequiredNotifier = useValueNotifier(isWifiRequired); - useValueChanged( - isWifiRequired, - (_, __) => WidgetsBinding.instance.addPostFrameCallback((_) => isWifiRequiredNotifier.value = isWifiRequired), - ); - - final isChargingRequired = ref.watch(backupProvider.select((s) => s.backupRequireCharging)); - final isChargingRequiredNotifier = useValueNotifier(isChargingRequired); - useValueChanged( - isChargingRequired, - (_, __) => - WidgetsBinding.instance.addPostFrameCallback((_) => isChargingRequiredNotifier.value = isChargingRequired), - ); - - int backupDelayToSliderValue(int ms) => switch (ms) { - 5000 => 0, - 30000 => 1, - 120000 => 2, - _ => 3, - }; - - int backupDelayToMilliseconds(int v) => switch (v) { - 0 => 5000, - 1 => 30000, - 2 => 120000, - _ => 600000, - }; - - String formatBackupDelaySliderValue(int v) => switch (v) { - 0 => 'setting_notifications_notify_seconds'.tr(namedArgs: {'count': '5'}), - 1 => 'setting_notifications_notify_seconds'.tr(namedArgs: {'count': '30'}), - 2 => 'setting_notifications_notify_minutes'.tr(namedArgs: {'count': '2'}), - _ => 'setting_notifications_notify_minutes'.tr(namedArgs: {'count': '10'}), - }; - - final backupTriggerDelay = ref.watch(backupProvider.select((s) => s.backupTriggerDelay)); - final triggerDelay = useState(backupDelayToSliderValue(backupTriggerDelay)); - useValueChanged( - triggerDelay.value, - (_, __) => ref - .read(backupProvider.notifier) - .configureBackgroundBackup( - triggerDelay: backupDelayToMilliseconds(triggerDelay.value), - onError: onError, - onBatteryInfo: onBatteryInfo, - ), - ); - - return SettingsButtonListTile( - icon: Icons.cloud_sync_rounded, - iconColor: context.primaryColor, - title: 'backup_controller_page_background_is_on'.tr(), - buttonText: 'backup_controller_page_background_turn_off'.tr(), - onButtonTap: () => ref - .read(backupProvider.notifier) - .configureBackgroundBackup(enabled: false, onError: onError, onBatteryInfo: onBatteryInfo), - subtitle: Column( - children: [ - SettingsSwitchListTile( - valueNotifier: isWifiRequiredNotifier, - title: 'backup_controller_page_background_wifi'.tr(), - icon: Icons.wifi, - onChanged: (enabled) => ref - .read(backupProvider.notifier) - .configureBackgroundBackup(requireWifi: enabled, onError: onError, onBatteryInfo: onBatteryInfo), - ), - SettingsSwitchListTile( - valueNotifier: isChargingRequiredNotifier, - title: 'backup_controller_page_background_charging'.tr(), - icon: Icons.charging_station, - onChanged: (enabled) => ref - .read(backupProvider.notifier) - .configureBackgroundBackup(requireCharging: enabled, onError: onError, onBatteryInfo: onBatteryInfo), - ), - if (Platform.isAndroid) - SettingsSliderListTile( - valueNotifier: triggerDelay, - text: 'backup_controller_page_background_delay'.tr( - namedArgs: {'duration': formatBackupDelaySliderValue(triggerDelay.value)}, - ), - maxValue: 3.0, - noDivisons: 3, - label: formatBackupDelaySliderValue(triggerDelay.value), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/settings/backup_settings/backup_settings.dart b/mobile/lib/widgets/settings/backup_settings/backup_settings.dart deleted file mode 100644 index 50aa57da9f..0000000000 --- a/mobile/lib/widgets/settings/backup_settings/backup_settings.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'dart:io'; - -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/backup/backup_verification.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:immich_mobile/widgets/settings/backup_settings/background_settings.dart'; -import 'package:immich_mobile/widgets/settings/backup_settings/foreground_settings.dart'; -import 'package:immich_mobile/widgets/settings/settings_button_list_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart'; -import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; -import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; - -class BackupSettings extends HookConsumerWidget { - const BackupSettings({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final ignoreIcloudAssets = useAppSettingsState(AppSettingsEnum.ignoreIcloudAssets); - final isAdvancedTroubleshooting = useAppSettingsState(AppSettingsEnum.advancedTroubleshooting); - final albumSync = useAppSettingsState(AppSettingsEnum.syncAlbums); - final isCorruptCheckInProgress = ref.watch(backupVerificationProvider); - final isAlbumSyncInProgress = useState(false); - - syncAlbums() async { - isAlbumSyncInProgress.value = true; - try { - await ref.read(assetServiceProvider).syncUploadedAssetToAlbums(); - } catch (_) { - } finally { - Future.delayed(const Duration(seconds: 1), () { - isAlbumSyncInProgress.value = false; - }); - } - } - - final backupSettings = [ - const ForegroundBackupSettings(), - const BackgroundBackupSettings(), - if (Platform.isIOS) - SettingsSwitchListTile( - valueNotifier: ignoreIcloudAssets, - title: 'ignore_icloud_photos'.tr(), - subtitle: 'ignore_icloud_photos_description'.tr(), - ), - if (Platform.isAndroid && isAdvancedTroubleshooting.value) - SettingsButtonListTile( - icon: Icons.warning_rounded, - title: 'check_corrupt_asset_backup'.tr(), - subtitle: isCorruptCheckInProgress - ? const Column( - children: [ - SizedBox(height: 20), - Center(child: CircularProgressIndicator()), - SizedBox(height: 20), - ], - ) - : null, - subtileText: !isCorruptCheckInProgress ? 'check_corrupt_asset_backup_description'.tr() : null, - buttonText: 'check_corrupt_asset_backup_button'.tr(), - onButtonTap: !isCorruptCheckInProgress - ? () => ref.read(backupVerificationProvider.notifier).performBackupCheck(context) - : null, - ), - if (albumSync.value) - SettingsButtonListTile( - icon: Icons.photo_album_outlined, - title: 'sync_albums'.tr(), - subtitle: Text("sync_albums_manual_subtitle".tr()), - buttonText: 'sync_albums'.tr(), - child: isAlbumSyncInProgress.value - ? const CircularProgressIndicator() - : ElevatedButton(onPressed: syncAlbums, child: Text('sync'.tr())), - ), - ]; - - return SettingsSubPageScaffold(settings: backupSettings, showDivider: true); - } -} diff --git a/mobile/lib/widgets/settings/backup_settings/foreground_settings.dart b/mobile/lib/widgets/settings/backup_settings/foreground_settings.dart deleted file mode 100644 index a2ff00fe45..0000000000 --- a/mobile/lib/widgets/settings/backup_settings/foreground_settings.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/widgets/settings/settings_button_list_tile.dart'; - -class ForegroundBackupSettings extends ConsumerWidget { - const ForegroundBackupSettings({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isAutoBackup = ref.watch(backupProvider.select((s) => s.autoBackup)); - - void onButtonTap() => ref.read(backupProvider.notifier).setAutoBackup(!isAutoBackup); - - if (isAutoBackup) { - return SettingsButtonListTile( - icon: Icons.cloud_done_rounded, - iconColor: context.primaryColor, - title: 'backup_controller_page_status_on'.tr(), - buttonText: 'backup_controller_page_turn_off'.tr(), - onButtonTap: onButtonTap, - ); - } - - return SettingsButtonListTile( - icon: Icons.cloud_off_rounded, - title: 'backup_controller_page_status_off'.tr(), - subtileText: 'backup_controller_page_desc_backup'.tr(), - buttonText: 'backup_controller_page_turn_on'.tr(), - onButtonTap: onButtonTap, - ); - } -} diff --git a/mobile/lib/widgets/settings/beta_timeline_list_tile.dart b/mobile/lib/widgets/settings/beta_timeline_list_tile.dart deleted file mode 100644 index 21e0edb34c..0000000000 --- a/mobile/lib/widgets/settings/beta_timeline_list_tile.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/settings/setting_list_tile.dart'; - -class BetaTimelineListTile extends ConsumerWidget { - const BetaTimelineListTile({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final betaTimelineValue = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.betaTimeline); - final auth = ref.watch(authProvider); - - if (!auth.isAuthenticated) { - return const SizedBox.shrink(); - } - - void onSwitchChanged(bool value) { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: value ? const Text("Enable New Timeline") : const Text("Disable New Timeline"), - content: value - ? const Text("Are you sure you want to enable the new timeline?") - : const Text("Are you sure you want to disable the new timeline?"), - actions: [ - TextButton( - onPressed: () { - context.pop(); - }, - child: Text( - "cancel".t(context: context), - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: context.colorScheme.outline), - ), - ), - ElevatedButton( - onPressed: () async { - Navigator.of(context).pop(); - unawaited(context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: value)])); - }, - child: Text("ok".t(context: context)), - ), - ], - ); - }, - ); - } - - return Padding( - padding: const EdgeInsets.only(left: 4.0), - child: SettingListTile( - title: "new_timeline".t(context: context), - trailing: Switch.adaptive( - value: betaTimelineValue, - onChanged: onSwitchChanged, - activeThumbColor: context.primaryColor, - ), - onTap: () => onSwitchChanged(!betaTimelineValue), - ), - ); - } -} diff --git a/mobile/lib/widgets/settings/local_storage_settings.dart b/mobile/lib/widgets/settings/local_storage_settings.dart deleted file mode 100644 index af9e4079bb..0000000000 --- a/mobile/lib/widgets/settings/local_storage_settings.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' show useEffect, useState; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; - -class LocalStorageSettings extends HookConsumerWidget { - const LocalStorageSettings({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final isarDb = ref.watch(dbProvider); - final cacheItemCount = useState(0); - - useEffect(() { - cacheItemCount.value = isarDb.duplicatedAssets.countSync(); - return null; - }, []); - - void clearCache() async { - await isarDb.writeTxn(() => isarDb.duplicatedAssets.clear()); - cacheItemCount.value = await isarDb.duplicatedAssets.count(); - } - - return ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 20), - dense: true, - title: Text( - "cache_settings_duplicated_assets_title", - style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500), - ).tr(namedArgs: {'count': "${cacheItemCount.value}"}), - subtitle: Text( - "cache_settings_duplicated_assets_subtitle", - style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ).tr(), - trailing: TextButton( - onPressed: cacheItemCount.value > 0 ? clearCache : null, - child: Text( - "cache_settings_duplicated_assets_clear_button", - style: TextStyle( - fontSize: 12, - color: cacheItemCount.value > 0 ? Colors.red : Colors.grey, - fontWeight: FontWeight.bold, - ), - ).tr(), - ), - ); - } -} diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index d01e743a3f..df469cee3e 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -56,10 +56,10 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); final api_instance = APIKeysApi(); -final aPIKeyCreateDto = APIKeyCreateDto(); // APIKeyCreateDto | +final apiKeyCreateDto = ApiKeyCreateDto(); // ApiKeyCreateDto | try { - final result = api_instance.createApiKey(aPIKeyCreateDto); + final result = api_instance.createApiKey(apiKeyCreateDto); print(result); } catch (e) { print('Exception when calling APIKeysApi->createApiKey: $e\n'); @@ -89,6 +89,7 @@ Class | Method | HTTP request | Description *AlbumsApi* | [**createAlbum**](doc//AlbumsApi.md#createalbum) | **POST** /albums | Create an album *AlbumsApi* | [**deleteAlbum**](doc//AlbumsApi.md#deletealbum) | **DELETE** /albums/{id} | Delete an album *AlbumsApi* | [**getAlbumInfo**](doc//AlbumsApi.md#getalbuminfo) | **GET** /albums/{id} | Retrieve an album +*AlbumsApi* | [**getAlbumMapMarkers**](doc//AlbumsApi.md#getalbummapmarkers) | **GET** /albums/{id}/map-markers | Retrieve album map markers *AlbumsApi* | [**getAlbumStatistics**](doc//AlbumsApi.md#getalbumstatistics) | **GET** /albums/statistics | Retrieve album statistics *AlbumsApi* | [**getAllAlbums**](doc//AlbumsApi.md#getallalbums) | **GET** /albums | List all albums *AlbumsApi* | [**removeAssetFromAlbum**](doc//AlbumsApi.md#removeassetfromalbum) | **DELETE** /albums/{id}/assets | Remove assets from an album @@ -96,24 +97,20 @@ Class | Method | HTTP request | Description *AlbumsApi* | [**updateAlbumInfo**](doc//AlbumsApi.md#updatealbuminfo) | **PATCH** /albums/{id} | Update an album *AlbumsApi* | [**updateAlbumUser**](doc//AlbumsApi.md#updatealbumuser) | **PUT** /albums/{id}/user/{userId} | Update user role *AssetsApi* | [**checkBulkUpload**](doc//AssetsApi.md#checkbulkupload) | **POST** /assets/bulk-upload-check | Check bulk upload -*AssetsApi* | [**checkExistingAssets**](doc//AssetsApi.md#checkexistingassets) | **POST** /assets/exist | Check existing assets *AssetsApi* | [**copyAsset**](doc//AssetsApi.md#copyasset) | **PUT** /assets/copy | Copy asset *AssetsApi* | [**deleteAssetMetadata**](doc//AssetsApi.md#deleteassetmetadata) | **DELETE** /assets/{id}/metadata/{key} | Delete asset metadata by key *AssetsApi* | [**deleteAssets**](doc//AssetsApi.md#deleteassets) | **DELETE** /assets | Delete assets *AssetsApi* | [**deleteBulkAssetMetadata**](doc//AssetsApi.md#deletebulkassetmetadata) | **DELETE** /assets/metadata | Delete asset metadata *AssetsApi* | [**downloadAsset**](doc//AssetsApi.md#downloadasset) | **GET** /assets/{id}/original | Download original asset *AssetsApi* | [**editAsset**](doc//AssetsApi.md#editasset) | **PUT** /assets/{id}/edits | Apply edits to an existing asset -*AssetsApi* | [**getAllUserAssetsByDeviceId**](doc//AssetsApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | Retrieve assets by device ID *AssetsApi* | [**getAssetEdits**](doc//AssetsApi.md#getassetedits) | **GET** /assets/{id}/edits | Retrieve edits for an existing asset *AssetsApi* | [**getAssetInfo**](doc//AssetsApi.md#getassetinfo) | **GET** /assets/{id} | Retrieve an asset *AssetsApi* | [**getAssetMetadata**](doc//AssetsApi.md#getassetmetadata) | **GET** /assets/{id}/metadata | Get asset metadata *AssetsApi* | [**getAssetMetadataByKey**](doc//AssetsApi.md#getassetmetadatabykey) | **GET** /assets/{id}/metadata/{key} | Retrieve asset metadata by key *AssetsApi* | [**getAssetOcr**](doc//AssetsApi.md#getassetocr) | **GET** /assets/{id}/ocr | Retrieve asset OCR data *AssetsApi* | [**getAssetStatistics**](doc//AssetsApi.md#getassetstatistics) | **GET** /assets/statistics | Get asset statistics -*AssetsApi* | [**getRandom**](doc//AssetsApi.md#getrandom) | **GET** /assets/random | Get random assets *AssetsApi* | [**playAssetVideo**](doc//AssetsApi.md#playassetvideo) | **GET** /assets/{id}/video/playback | Play asset video *AssetsApi* | [**removeAssetEdits**](doc//AssetsApi.md#removeassetedits) | **DELETE** /assets/{id}/edits | Remove edits from an existing asset -*AssetsApi* | [**replaceAsset**](doc//AssetsApi.md#replaceasset) | **PUT** /assets/{id}/original | Replace asset *AssetsApi* | [**runAssetJobs**](doc//AssetsApi.md#runassetjobs) | **POST** /assets/jobs | Run an asset job *AssetsApi* | [**updateAsset**](doc//AssetsApi.md#updateasset) | **PUT** /assets/{id} | Update an asset *AssetsApi* | [**updateAssetMetadata**](doc//AssetsApi.md#updateassetmetadata) | **PUT** /assets/{id}/metadata | Update asset metadata @@ -144,12 +141,7 @@ Class | Method | HTTP request | Description *DatabaseBackupsAdminApi* | [**startDatabaseRestoreFlow**](doc//DatabaseBackupsAdminApi.md#startdatabaserestoreflow) | **POST** /admin/database-backups/start-restore | Start database backup restore flow *DatabaseBackupsAdminApi* | [**uploadDatabaseBackup**](doc//DatabaseBackupsAdminApi.md#uploaddatabasebackup) | **POST** /admin/database-backups/upload | Upload database backup *DeprecatedApi* | [**createPartnerDeprecated**](doc//DeprecatedApi.md#createpartnerdeprecated) | **POST** /partners/{id} | Create a partner -*DeprecatedApi* | [**getAllUserAssetsByDeviceId**](doc//DeprecatedApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | Retrieve assets by device ID -*DeprecatedApi* | [**getDeltaSync**](doc//DeprecatedApi.md#getdeltasync) | **POST** /sync/delta-sync | Get delta sync for user -*DeprecatedApi* | [**getFullSyncForUser**](doc//DeprecatedApi.md#getfullsyncforuser) | **POST** /sync/full-sync | Get full sync for user *DeprecatedApi* | [**getQueuesLegacy**](doc//DeprecatedApi.md#getqueueslegacy) | **GET** /jobs | Retrieve queue counts and status -*DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random | Get random assets -*DeprecatedApi* | [**replaceAsset**](doc//DeprecatedApi.md#replaceasset) | **PUT** /assets/{id}/original | Replace asset *DeprecatedApi* | [**runQueueCommandLegacy**](doc//DeprecatedApi.md#runqueuecommandlegacy) | **PUT** /jobs/{name} | Run jobs *DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | Download asset archive *DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | Retrieve download information @@ -267,8 +259,6 @@ Class | Method | HTTP request | Description *StacksApi* | [**searchStacks**](doc//StacksApi.md#searchstacks) | **GET** /stacks | Retrieve stacks *StacksApi* | [**updateStack**](doc//StacksApi.md#updatestack) | **PUT** /stacks/{id} | Update a stack *SyncApi* | [**deleteSyncAck**](doc//SyncApi.md#deletesyncack) | **DELETE** /sync/ack | Delete acknowledgements -*SyncApi* | [**getDeltaSync**](doc//SyncApi.md#getdeltasync) | **POST** /sync/delta-sync | Get delta sync for user -*SyncApi* | [**getFullSyncForUser**](doc//SyncApi.md#getfullsyncforuser) | **POST** /sync/full-sync | Get full sync for user *SyncApi* | [**getSyncAck**](doc//SyncApi.md#getsyncack) | **GET** /sync/ack | Retrieve acknowledgements *SyncApi* | [**getSyncStream**](doc//SyncApi.md#getsyncstream) | **POST** /sync/stream | Stream sync changes *SyncApi* | [**sendSyncAck**](doc//SyncApi.md#sendsyncack) | **POST** /sync/ack | Acknowledge changes @@ -330,10 +320,6 @@ Class | Method | HTTP request | Description ## Documentation For Models - - [APIKeyCreateDto](doc//APIKeyCreateDto.md) - - [APIKeyCreateResponseDto](doc//APIKeyCreateResponseDto.md) - - [APIKeyResponseDto](doc//APIKeyResponseDto.md) - - [APIKeyUpdateDto](doc//APIKeyUpdateDto.md) - [ActivityCreateDto](doc//ActivityCreateDto.md) - [ActivityResponseDto](doc//ActivityResponseDto.md) - [ActivityStatisticsResponseDto](doc//ActivityStatisticsResponseDto.md) @@ -349,6 +335,10 @@ Class | Method | HTTP request | Description - [AlbumsAddAssetsResponseDto](doc//AlbumsAddAssetsResponseDto.md) - [AlbumsResponse](doc//AlbumsResponse.md) - [AlbumsUpdate](doc//AlbumsUpdate.md) + - [ApiKeyCreateDto](doc//ApiKeyCreateDto.md) + - [ApiKeyCreateResponseDto](doc//ApiKeyCreateResponseDto.md) + - [ApiKeyResponseDto](doc//ApiKeyResponseDto.md) + - [ApiKeyUpdateDto](doc//ApiKeyUpdateDto.md) - [AssetBulkDeleteDto](doc//AssetBulkDeleteDto.md) - [AssetBulkUpdateDto](doc//AssetBulkUpdateDto.md) - [AssetBulkUploadCheckDto](doc//AssetBulkUploadCheckDto.md) @@ -356,8 +346,6 @@ Class | Method | HTTP request | Description - [AssetBulkUploadCheckResponseDto](doc//AssetBulkUploadCheckResponseDto.md) - [AssetBulkUploadCheckResult](doc//AssetBulkUploadCheckResult.md) - [AssetCopyDto](doc//AssetCopyDto.md) - - [AssetDeltaSyncDto](doc//AssetDeltaSyncDto.md) - - [AssetDeltaSyncResponseDto](doc//AssetDeltaSyncResponseDto.md) - [AssetEditAction](doc//AssetEditAction.md) - [AssetEditActionItemDto](doc//AssetEditActionItemDto.md) - [AssetEditActionItemDtoParameters](doc//AssetEditActionItemDtoParameters.md) @@ -370,7 +358,7 @@ Class | Method | HTTP request | Description - [AssetFaceUpdateDto](doc//AssetFaceUpdateDto.md) - [AssetFaceUpdateItem](doc//AssetFaceUpdateItem.md) - [AssetFaceWithoutPersonResponseDto](doc//AssetFaceWithoutPersonResponseDto.md) - - [AssetFullSyncDto](doc//AssetFullSyncDto.md) + - [AssetIdErrorReason](doc//AssetIdErrorReason.md) - [AssetIdsDto](doc//AssetIdsDto.md) - [AssetIdsResponseDto](doc//AssetIdsResponseDto.md) - [AssetJobName](doc//AssetJobName.md) @@ -388,10 +376,12 @@ Class | Method | HTTP request | Description - [AssetMetadataUpsertItemDto](doc//AssetMetadataUpsertItemDto.md) - [AssetOcrResponseDto](doc//AssetOcrResponseDto.md) - [AssetOrder](doc//AssetOrder.md) + - [AssetRejectReason](doc//AssetRejectReason.md) - [AssetResponseDto](doc//AssetResponseDto.md) - [AssetStackResponseDto](doc//AssetStackResponseDto.md) - [AssetStatsResponseDto](doc//AssetStatsResponseDto.md) - [AssetTypeEnum](doc//AssetTypeEnum.md) + - [AssetUploadAction](doc//AssetUploadAction.md) - [AssetVisibility](doc//AssetVisibility.md) - [AudioCodec](doc//AudioCodec.md) - [AuthStatusResponseDto](doc//AuthStatusResponseDto.md) @@ -404,8 +394,6 @@ Class | Method | HTTP request | Description - [CastResponse](doc//CastResponse.md) - [CastUpdate](doc//CastUpdate.md) - [ChangePasswordDto](doc//ChangePasswordDto.md) - - [CheckExistingAssetsDto](doc//CheckExistingAssetsDto.md) - - [CheckExistingAssetsResponseDto](doc//CheckExistingAssetsResponseDto.md) - [Colorspace](doc//Colorspace.md) - [ContributorCountResponseDto](doc//ContributorCountResponseDto.md) - [CreateAlbumDto](doc//CreateAlbumDto.md) @@ -440,7 +428,6 @@ Class | Method | HTTP request | Description - [LibraryResponseDto](doc//LibraryResponseDto.md) - [LibraryStatsResponseDto](doc//LibraryStatsResponseDto.md) - [LicenseKeyDto](doc//LicenseKeyDto.md) - - [LicenseResponseDto](doc//LicenseResponseDto.md) - [LogLevel](doc//LogLevel.md) - [LoginCredentialDto](doc//LoginCredentialDto.md) - [LoginResponseDto](doc//LoginResponseDto.md) @@ -504,6 +491,10 @@ Class | Method | HTTP request | Description - [PluginActionResponseDto](doc//PluginActionResponseDto.md) - [PluginContextType](doc//PluginContextType.md) - [PluginFilterResponseDto](doc//PluginFilterResponseDto.md) + - [PluginJsonSchema](doc//PluginJsonSchema.md) + - [PluginJsonSchemaProperty](doc//PluginJsonSchemaProperty.md) + - [PluginJsonSchemaPropertyAdditionalProperties](doc//PluginJsonSchemaPropertyAdditionalProperties.md) + - [PluginJsonSchemaType](doc//PluginJsonSchemaType.md) - [PluginResponseDto](doc//PluginResponseDto.md) - [PluginTriggerResponseDto](doc//PluginTriggerResponseDto.md) - [PluginTriggerType](doc//PluginTriggerType.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 6b554fb644..ec1ea6e6b2 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -68,10 +68,6 @@ part 'api/users_admin_api.dart'; part 'api/views_api.dart'; part 'api/workflows_api.dart'; -part 'model/api_key_create_dto.dart'; -part 'model/api_key_create_response_dto.dart'; -part 'model/api_key_response_dto.dart'; -part 'model/api_key_update_dto.dart'; part 'model/activity_create_dto.dart'; part 'model/activity_response_dto.dart'; part 'model/activity_statistics_response_dto.dart'; @@ -87,6 +83,10 @@ part 'model/albums_add_assets_dto.dart'; part 'model/albums_add_assets_response_dto.dart'; part 'model/albums_response.dart'; part 'model/albums_update.dart'; +part 'model/api_key_create_dto.dart'; +part 'model/api_key_create_response_dto.dart'; +part 'model/api_key_response_dto.dart'; +part 'model/api_key_update_dto.dart'; part 'model/asset_bulk_delete_dto.dart'; part 'model/asset_bulk_update_dto.dart'; part 'model/asset_bulk_upload_check_dto.dart'; @@ -94,8 +94,6 @@ part 'model/asset_bulk_upload_check_item.dart'; part 'model/asset_bulk_upload_check_response_dto.dart'; part 'model/asset_bulk_upload_check_result.dart'; part 'model/asset_copy_dto.dart'; -part 'model/asset_delta_sync_dto.dart'; -part 'model/asset_delta_sync_response_dto.dart'; part 'model/asset_edit_action.dart'; part 'model/asset_edit_action_item_dto.dart'; part 'model/asset_edit_action_item_dto_parameters.dart'; @@ -108,7 +106,7 @@ part 'model/asset_face_response_dto.dart'; part 'model/asset_face_update_dto.dart'; part 'model/asset_face_update_item.dart'; part 'model/asset_face_without_person_response_dto.dart'; -part 'model/asset_full_sync_dto.dart'; +part 'model/asset_id_error_reason.dart'; part 'model/asset_ids_dto.dart'; part 'model/asset_ids_response_dto.dart'; part 'model/asset_job_name.dart'; @@ -126,10 +124,12 @@ part 'model/asset_metadata_upsert_dto.dart'; part 'model/asset_metadata_upsert_item_dto.dart'; part 'model/asset_ocr_response_dto.dart'; part 'model/asset_order.dart'; +part 'model/asset_reject_reason.dart'; part 'model/asset_response_dto.dart'; part 'model/asset_stack_response_dto.dart'; part 'model/asset_stats_response_dto.dart'; part 'model/asset_type_enum.dart'; +part 'model/asset_upload_action.dart'; part 'model/asset_visibility.dart'; part 'model/audio_codec.dart'; part 'model/auth_status_response_dto.dart'; @@ -142,8 +142,6 @@ part 'model/cq_mode.dart'; part 'model/cast_response.dart'; part 'model/cast_update.dart'; part 'model/change_password_dto.dart'; -part 'model/check_existing_assets_dto.dart'; -part 'model/check_existing_assets_response_dto.dart'; part 'model/colorspace.dart'; part 'model/contributor_count_response_dto.dart'; part 'model/create_album_dto.dart'; @@ -178,7 +176,6 @@ part 'model/job_settings_dto.dart'; part 'model/library_response_dto.dart'; part 'model/library_stats_response_dto.dart'; part 'model/license_key_dto.dart'; -part 'model/license_response_dto.dart'; part 'model/log_level.dart'; part 'model/login_credential_dto.dart'; part 'model/login_response_dto.dart'; @@ -242,6 +239,10 @@ part 'model/places_response_dto.dart'; part 'model/plugin_action_response_dto.dart'; part 'model/plugin_context_type.dart'; part 'model/plugin_filter_response_dto.dart'; +part 'model/plugin_json_schema.dart'; +part 'model/plugin_json_schema_property.dart'; +part 'model/plugin_json_schema_property_additional_properties.dart'; +part 'model/plugin_json_schema_type.dart'; part 'model/plugin_response_dto.dart'; part 'model/plugin_trigger_response_dto.dart'; part 'model/plugin_trigger_type.dart'; diff --git a/mobile/openapi/lib/api/activities_api.dart b/mobile/openapi/lib/api/activities_api.dart index 697598ac97..e0a393948c 100644 --- a/mobile/openapi/lib/api/activities_api.dart +++ b/mobile/openapi/lib/api/activities_api.dart @@ -136,10 +136,8 @@ class ActivitiesApi { /// Asset ID (if activity is for an asset) /// /// * [ReactionLevel] level: - /// Filter by activity level /// /// * [ReactionType] type: - /// Filter by activity type /// /// * [String] userId: /// Filter by user ID @@ -195,10 +193,8 @@ class ActivitiesApi { /// Asset ID (if activity is for an asset) /// /// * [ReactionLevel] level: - /// Filter by activity level /// /// * [ReactionType] type: - /// Filter by activity type /// /// * [String] userId: /// Filter by user ID diff --git a/mobile/openapi/lib/api/albums_api.dart b/mobile/openapi/lib/api/albums_api.dart index e2db95b9e0..d08d1cba9d 100644 --- a/mobile/openapi/lib/api/albums_api.dart +++ b/mobile/openapi/lib/api/albums_api.dart @@ -27,11 +27,7 @@ class AlbumsApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - /// - /// * [String] key: - /// - /// * [String] slug: - Future addAssetsToAlbumWithHttpInfo(String id, BulkIdsDto bulkIdsDto, { String? key, String? slug, }) async { + Future addAssetsToAlbumWithHttpInfo(String id, BulkIdsDto bulkIdsDto,) async { // ignore: prefer_const_declarations final apiPath = r'/albums/{id}/assets' .replaceAll('{id}', id); @@ -43,13 +39,6 @@ class AlbumsApi { final headerParams = {}; final formParams = {}; - if (key != null) { - queryParams.addAll(_queryParams('', 'key', key)); - } - if (slug != null) { - queryParams.addAll(_queryParams('', 'slug', slug)); - } - const contentTypes = ['application/json']; @@ -73,12 +62,8 @@ class AlbumsApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - /// - /// * [String] key: - /// - /// * [String] slug: - Future?> addAssetsToAlbum(String id, BulkIdsDto bulkIdsDto, { String? key, String? slug, }) async { - final response = await addAssetsToAlbumWithHttpInfo(id, bulkIdsDto, key: key, slug: slug, ); + Future?> addAssetsToAlbum(String id, BulkIdsDto bulkIdsDto,) async { + final response = await addAssetsToAlbumWithHttpInfo(id, bulkIdsDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -104,11 +89,7 @@ class AlbumsApi { /// Parameters: /// /// * [AlbumsAddAssetsDto] albumsAddAssetsDto (required): - /// - /// * [String] key: - /// - /// * [String] slug: - Future addAssetsToAlbumsWithHttpInfo(AlbumsAddAssetsDto albumsAddAssetsDto, { String? key, String? slug, }) async { + Future addAssetsToAlbumsWithHttpInfo(AlbumsAddAssetsDto albumsAddAssetsDto,) async { // ignore: prefer_const_declarations final apiPath = r'/albums/assets'; @@ -119,13 +100,6 @@ class AlbumsApi { final headerParams = {}; final formParams = {}; - if (key != null) { - queryParams.addAll(_queryParams('', 'key', key)); - } - if (slug != null) { - queryParams.addAll(_queryParams('', 'slug', slug)); - } - const contentTypes = ['application/json']; @@ -147,12 +121,8 @@ class AlbumsApi { /// Parameters: /// /// * [AlbumsAddAssetsDto] albumsAddAssetsDto (required): - /// - /// * [String] key: - /// - /// * [String] slug: - Future addAssetsToAlbums(AlbumsAddAssetsDto albumsAddAssetsDto, { String? key, String? slug, }) async { - final response = await addAssetsToAlbumsWithHttpInfo(albumsAddAssetsDto, key: key, slug: slug, ); + Future addAssetsToAlbums(AlbumsAddAssetsDto albumsAddAssetsDto,) async { + final response = await addAssetsToAlbumsWithHttpInfo(albumsAddAssetsDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -345,10 +315,7 @@ class AlbumsApi { /// * [String] key: /// /// * [String] slug: - /// - /// * [bool] withoutAssets: - /// Exclude assets from response - Future getAlbumInfoWithHttpInfo(String id, { String? key, String? slug, bool? withoutAssets, }) async { + Future getAlbumInfoWithHttpInfo(String id, { String? key, String? slug, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums/{id}' .replaceAll('{id}', id); @@ -366,9 +333,6 @@ class AlbumsApi { if (slug != null) { queryParams.addAll(_queryParams('', 'slug', slug)); } - if (withoutAssets != null) { - queryParams.addAll(_queryParams('', 'withoutAssets', withoutAssets)); - } const contentTypes = []; @@ -395,11 +359,8 @@ class AlbumsApi { /// * [String] key: /// /// * [String] slug: - /// - /// * [bool] withoutAssets: - /// Exclude assets from response - Future getAlbumInfo(String id, { String? key, String? slug, bool? withoutAssets, }) async { - final response = await getAlbumInfoWithHttpInfo(id, key: key, slug: slug, withoutAssets: withoutAssets, ); + Future getAlbumInfo(String id, { String? key, String? slug, }) async { + final response = await getAlbumInfoWithHttpInfo(id, key: key, slug: slug, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -413,6 +374,81 @@ class AlbumsApi { return null; } + /// Retrieve album map markers + /// + /// Retrieve map marker information for a specific album by its ID. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [String] key: + /// + /// * [String] slug: + Future getAlbumMapMarkersWithHttpInfo(String id, { String? key, String? slug, }) async { + // ignore: prefer_const_declarations + final apiPath = r'/albums/{id}/map-markers' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (key != null) { + queryParams.addAll(_queryParams('', 'key', key)); + } + if (slug != null) { + queryParams.addAll(_queryParams('', 'slug', slug)); + } + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Retrieve album map markers + /// + /// Retrieve map marker information for a specific album by its ID. + /// + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [String] key: + /// + /// * [String] slug: + Future?> getAlbumMapMarkers(String id, { String? key, String? slug, }) async { + final response = await getAlbumMapMarkersWithHttpInfo(id, key: key, slug: slug, ); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } + /// Retrieve album statistics /// /// Returns statistics about the albums available to the authenticated user. diff --git a/mobile/openapi/lib/api/api_keys_api.dart b/mobile/openapi/lib/api/api_keys_api.dart index 0bd26575c6..3ca85265c4 100644 --- a/mobile/openapi/lib/api/api_keys_api.dart +++ b/mobile/openapi/lib/api/api_keys_api.dart @@ -24,13 +24,13 @@ class APIKeysApi { /// /// Parameters: /// - /// * [APIKeyCreateDto] aPIKeyCreateDto (required): - Future createApiKeyWithHttpInfo(APIKeyCreateDto aPIKeyCreateDto,) async { + /// * [ApiKeyCreateDto] apiKeyCreateDto (required): + Future createApiKeyWithHttpInfo(ApiKeyCreateDto apiKeyCreateDto,) async { // ignore: prefer_const_declarations final apiPath = r'/api-keys'; // ignore: prefer_final_locals - Object? postBody = aPIKeyCreateDto; + Object? postBody = apiKeyCreateDto; final queryParams = []; final headerParams = {}; @@ -56,9 +56,9 @@ class APIKeysApi { /// /// Parameters: /// - /// * [APIKeyCreateDto] aPIKeyCreateDto (required): - Future createApiKey(APIKeyCreateDto aPIKeyCreateDto,) async { - final response = await createApiKeyWithHttpInfo(aPIKeyCreateDto,); + /// * [ApiKeyCreateDto] apiKeyCreateDto (required): + Future createApiKey(ApiKeyCreateDto apiKeyCreateDto,) async { + final response = await createApiKeyWithHttpInfo(apiKeyCreateDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -66,7 +66,7 @@ class APIKeysApi { // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'APIKeyCreateResponseDto',) as APIKeyCreateResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ApiKeyCreateResponseDto',) as ApiKeyCreateResponseDto; } return null; @@ -163,7 +163,7 @@ class APIKeysApi { /// Parameters: /// /// * [String] id (required): - Future getApiKey(String id,) async { + Future getApiKey(String id,) async { final response = await getApiKeyWithHttpInfo(id,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -172,7 +172,7 @@ class APIKeysApi { // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'APIKeyResponseDto',) as APIKeyResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ApiKeyResponseDto',) as ApiKeyResponseDto; } return null; @@ -211,7 +211,7 @@ class APIKeysApi { /// List all API keys /// /// Retrieve all API keys of the current user. - Future?> getApiKeys() async { + Future?> getApiKeys() async { final response = await getApiKeysWithHttpInfo(); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -221,8 +221,8 @@ class APIKeysApi { // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() .toList(growable: false); } @@ -262,7 +262,7 @@ class APIKeysApi { /// Retrieve the current API key /// /// Retrieve the API key that is used to access this endpoint. - Future getMyApiKey() async { + Future getMyApiKey() async { final response = await getMyApiKeyWithHttpInfo(); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -271,7 +271,7 @@ class APIKeysApi { // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'APIKeyResponseDto',) as APIKeyResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ApiKeyResponseDto',) as ApiKeyResponseDto; } return null; @@ -287,14 +287,14 @@ class APIKeysApi { /// /// * [String] id (required): /// - /// * [APIKeyUpdateDto] aPIKeyUpdateDto (required): - Future updateApiKeyWithHttpInfo(String id, APIKeyUpdateDto aPIKeyUpdateDto,) async { + /// * [ApiKeyUpdateDto] apiKeyUpdateDto (required): + Future updateApiKeyWithHttpInfo(String id, ApiKeyUpdateDto apiKeyUpdateDto,) async { // ignore: prefer_const_declarations final apiPath = r'/api-keys/{id}' .replaceAll('{id}', id); // ignore: prefer_final_locals - Object? postBody = aPIKeyUpdateDto; + Object? postBody = apiKeyUpdateDto; final queryParams = []; final headerParams = {}; @@ -322,9 +322,9 @@ class APIKeysApi { /// /// * [String] id (required): /// - /// * [APIKeyUpdateDto] aPIKeyUpdateDto (required): - Future updateApiKey(String id, APIKeyUpdateDto aPIKeyUpdateDto,) async { - final response = await updateApiKeyWithHttpInfo(id, aPIKeyUpdateDto,); + /// * [ApiKeyUpdateDto] apiKeyUpdateDto (required): + Future updateApiKey(String id, ApiKeyUpdateDto apiKeyUpdateDto,) async { + final response = await updateApiKeyWithHttpInfo(id, apiKeyUpdateDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -332,7 +332,7 @@ class APIKeysApi { // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'APIKeyResponseDto',) as APIKeyResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ApiKeyResponseDto',) as ApiKeyResponseDto; } return null; diff --git a/mobile/openapi/lib/api/assets_api.dart b/mobile/openapi/lib/api/assets_api.dart index a026b99028..5046376168 100644 --- a/mobile/openapi/lib/api/assets_api.dart +++ b/mobile/openapi/lib/api/assets_api.dart @@ -72,62 +72,6 @@ class AssetsApi { return null; } - /// Check existing assets - /// - /// Checks if multiple assets exist on the server and returns all existing - used by background backup - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [CheckExistingAssetsDto] checkExistingAssetsDto (required): - Future checkExistingAssetsWithHttpInfo(CheckExistingAssetsDto checkExistingAssetsDto,) async { - // ignore: prefer_const_declarations - final apiPath = r'/assets/exist'; - - // ignore: prefer_final_locals - Object? postBody = checkExistingAssetsDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - apiPath, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Check existing assets - /// - /// Checks if multiple assets exist on the server and returns all existing - used by background backup - /// - /// Parameters: - /// - /// * [CheckExistingAssetsDto] checkExistingAssetsDto (required): - Future checkExistingAssets(CheckExistingAssetsDto checkExistingAssetsDto,) async { - final response = await checkExistingAssetsWithHttpInfo(checkExistingAssetsDto,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'CheckExistingAssetsResponseDto',) as CheckExistingAssetsResponseDto; - - } - return null; - } - /// Copy asset /// /// Copy asset information like albums, tags, etc. from one asset to another. @@ -472,68 +416,6 @@ class AssetsApi { return null; } - /// Retrieve assets by device ID - /// - /// Get all asset of a device that are in the database, ID only. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [String] deviceId (required): - /// Device ID - Future getAllUserAssetsByDeviceIdWithHttpInfo(String deviceId,) async { - // ignore: prefer_const_declarations - final apiPath = r'/assets/device/{deviceId}' - .replaceAll('{deviceId}', deviceId); - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = []; - - - return apiClient.invokeAPI( - apiPath, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Retrieve assets by device ID - /// - /// Get all asset of a device that are in the database, ID only. - /// - /// Parameters: - /// - /// * [String] deviceId (required): - /// Device ID - Future?> getAllUserAssetsByDeviceId(String deviceId,) async { - final response = await getAllUserAssetsByDeviceIdWithHttpInfo(deviceId,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } - /// Retrieve edits for an existing asset /// /// Retrieve a series of edit actions (crop, rotate, mirror) associated with the specified asset. @@ -864,7 +746,6 @@ class AssetsApi { /// Filter by trash status /// /// * [AssetVisibility] visibility: - /// Filter by visibility Future getAssetStatisticsWithHttpInfo({ bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/statistics'; @@ -913,7 +794,6 @@ class AssetsApi { /// Filter by trash status /// /// * [AssetVisibility] visibility: - /// Filter by visibility Future getAssetStatistics({ bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { final response = await getAssetStatisticsWithHttpInfo( isFavorite: isFavorite, isTrashed: isTrashed, visibility: visibility, ); if (response.statusCode >= HttpStatus.badRequest) { @@ -929,71 +809,6 @@ class AssetsApi { return null; } - /// Get random assets - /// - /// Retrieve a specified number of random assets for the authenticated user. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [num] count: - /// Number of random assets to return - Future getRandomWithHttpInfo({ num? count, }) async { - // ignore: prefer_const_declarations - final apiPath = r'/assets/random'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - if (count != null) { - queryParams.addAll(_queryParams('', 'count', count)); - } - - const contentTypes = []; - - - return apiClient.invokeAPI( - apiPath, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Get random assets - /// - /// Retrieve a specified number of random assets for the authenticated user. - /// - /// Parameters: - /// - /// * [num] count: - /// Number of random assets to return - Future?> getRandom({ num? count, }) async { - final response = await getRandomWithHttpInfo( count: count, ); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } - /// Play asset video /// /// Streams the video file for the specified asset. This endpoint also supports byte range requests. @@ -1115,154 +930,6 @@ class AssetsApi { } } - /// Replace asset - /// - /// Replace the asset with new file, without changing its id. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [String] id (required): - /// - /// * [MultipartFile] assetData (required): - /// Asset file data - /// - /// * [String] deviceAssetId (required): - /// Device asset ID - /// - /// * [String] deviceId (required): - /// Device ID - /// - /// * [DateTime] fileCreatedAt (required): - /// File creation date - /// - /// * [DateTime] fileModifiedAt (required): - /// File modification date - /// - /// * [String] key: - /// - /// * [String] slug: - /// - /// * [String] duration: - /// Duration (for videos) - /// - /// * [String] filename: - /// Filename - Future replaceAssetWithHttpInfo(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? duration, String? filename, }) async { - // ignore: prefer_const_declarations - final apiPath = r'/assets/{id}/original' - .replaceAll('{id}', id); - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - if (key != null) { - queryParams.addAll(_queryParams('', 'key', key)); - } - if (slug != null) { - queryParams.addAll(_queryParams('', 'slug', slug)); - } - - const contentTypes = ['multipart/form-data']; - - bool hasFields = false; - final mp = MultipartRequest('PUT', Uri.parse(apiPath)); - if (assetData != null) { - hasFields = true; - mp.fields[r'assetData'] = assetData.field; - mp.files.add(assetData); - } - if (deviceAssetId != null) { - hasFields = true; - mp.fields[r'deviceAssetId'] = parameterToString(deviceAssetId); - } - if (deviceId != null) { - hasFields = true; - mp.fields[r'deviceId'] = parameterToString(deviceId); - } - if (duration != null) { - hasFields = true; - mp.fields[r'duration'] = parameterToString(duration); - } - if (fileCreatedAt != null) { - hasFields = true; - mp.fields[r'fileCreatedAt'] = parameterToString(fileCreatedAt); - } - if (fileModifiedAt != null) { - hasFields = true; - mp.fields[r'fileModifiedAt'] = parameterToString(fileModifiedAt); - } - if (filename != null) { - hasFields = true; - mp.fields[r'filename'] = parameterToString(filename); - } - if (hasFields) { - postBody = mp; - } - - return apiClient.invokeAPI( - apiPath, - 'PUT', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Replace asset - /// - /// Replace the asset with new file, without changing its id. - /// - /// Parameters: - /// - /// * [String] id (required): - /// - /// * [MultipartFile] assetData (required): - /// Asset file data - /// - /// * [String] deviceAssetId (required): - /// Device asset ID - /// - /// * [String] deviceId (required): - /// Device ID - /// - /// * [DateTime] fileCreatedAt (required): - /// File creation date - /// - /// * [DateTime] fileModifiedAt (required): - /// File modification date - /// - /// * [String] key: - /// - /// * [String] slug: - /// - /// * [String] duration: - /// Duration (for videos) - /// - /// * [String] filename: - /// Filename - Future replaceAsset(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? duration, String? filename, }) async { - final response = await replaceAssetWithHttpInfo(id, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, slug: slug, duration: duration, filename: filename, ); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetMediaResponseDto',) as AssetMediaResponseDto; - - } - return null; - } - /// Run an asset job /// /// Run a specific job on a set of assets. @@ -1554,12 +1221,6 @@ class AssetsApi { /// * [MultipartFile] assetData (required): /// Asset file data /// - /// * [String] deviceAssetId (required): - /// Device asset ID - /// - /// * [String] deviceId (required): - /// Device ID - /// /// * [DateTime] fileCreatedAt (required): /// File creation date /// @@ -1592,8 +1253,7 @@ class AssetsApi { /// Sidecar file data /// /// * [AssetVisibility] visibility: - /// Asset visibility - Future uploadAssetWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { + Future uploadAssetWithHttpInfo(MultipartFile assetData, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets'; @@ -1624,14 +1284,6 @@ class AssetsApi { mp.fields[r'assetData'] = assetData.field; mp.files.add(assetData); } - if (deviceAssetId != null) { - hasFields = true; - mp.fields[r'deviceAssetId'] = parameterToString(deviceAssetId); - } - if (deviceId != null) { - hasFields = true; - mp.fields[r'deviceId'] = parameterToString(deviceId); - } if (duration != null) { hasFields = true; mp.fields[r'duration'] = parameterToString(duration); @@ -1693,12 +1345,6 @@ class AssetsApi { /// * [MultipartFile] assetData (required): /// Asset file data /// - /// * [String] deviceAssetId (required): - /// Device asset ID - /// - /// * [String] deviceId (required): - /// Device ID - /// /// * [DateTime] fileCreatedAt (required): /// File creation date /// @@ -1731,9 +1377,8 @@ class AssetsApi { /// Sidecar file data /// /// * [AssetVisibility] visibility: - /// Asset visibility - Future uploadAsset(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { - final response = await uploadAssetWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, slug: slug, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, metadata: metadata, sidecarData: sidecarData, visibility: visibility, ); + Future uploadAsset(MultipartFile assetData, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List? 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, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -1763,7 +1408,6 @@ class AssetsApi { /// * [String] key: /// /// * [AssetMediaSize] size: - /// Asset media size /// /// * [String] slug: Future viewAssetWithHttpInfo(String id, { bool? edited, String? key, AssetMediaSize? size, String? slug, }) async { @@ -1819,7 +1463,6 @@ class AssetsApi { /// * [String] key: /// /// * [AssetMediaSize] size: - /// Asset media size /// /// * [String] slug: Future viewAsset(String id, { bool? edited, String? key, AssetMediaSize? size, String? slug, }) async { diff --git a/mobile/openapi/lib/api/database_backups_admin_api.dart b/mobile/openapi/lib/api/database_backups_admin_api.dart index fbd485f86f..768185db1e 100644 --- a/mobile/openapi/lib/api/database_backups_admin_api.dart +++ b/mobile/openapi/lib/api/database_backups_admin_api.dart @@ -218,6 +218,7 @@ class DatabaseBackupsAdminApi { /// Parameters: /// /// * [MultipartFile] file: + /// Database backup file Future uploadDatabaseBackupWithHttpInfo({ MultipartFile? file, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/database-backups/upload'; @@ -260,6 +261,7 @@ class DatabaseBackupsAdminApi { /// Parameters: /// /// * [MultipartFile] file: + /// Database backup file Future uploadDatabaseBackup({ MultipartFile? file, }) async { final response = await uploadDatabaseBackupWithHttpInfo( file: file, ); if (response.statusCode >= HttpStatus.badRequest) { diff --git a/mobile/openapi/lib/api/deprecated_api.dart b/mobile/openapi/lib/api/deprecated_api.dart index 33bcaf062c..a437cd5837 100644 --- a/mobile/openapi/lib/api/deprecated_api.dart +++ b/mobile/openapi/lib/api/deprecated_api.dart @@ -73,183 +73,6 @@ class DeprecatedApi { return null; } - /// Retrieve assets by device ID - /// - /// Get all asset of a device that are in the database, ID only. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [String] deviceId (required): - /// Device ID - Future getAllUserAssetsByDeviceIdWithHttpInfo(String deviceId,) async { - // ignore: prefer_const_declarations - final apiPath = r'/assets/device/{deviceId}' - .replaceAll('{deviceId}', deviceId); - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = []; - - - return apiClient.invokeAPI( - apiPath, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Retrieve assets by device ID - /// - /// Get all asset of a device that are in the database, ID only. - /// - /// Parameters: - /// - /// * [String] deviceId (required): - /// Device ID - Future?> getAllUserAssetsByDeviceId(String deviceId,) async { - final response = await getAllUserAssetsByDeviceIdWithHttpInfo(deviceId,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } - - /// Get delta sync for user - /// - /// Retrieve changed assets since the last sync for the authenticated user. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [AssetDeltaSyncDto] assetDeltaSyncDto (required): - Future getDeltaSyncWithHttpInfo(AssetDeltaSyncDto assetDeltaSyncDto,) async { - // ignore: prefer_const_declarations - final apiPath = r'/sync/delta-sync'; - - // ignore: prefer_final_locals - Object? postBody = assetDeltaSyncDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - apiPath, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Get delta sync for user - /// - /// Retrieve changed assets since the last sync for the authenticated user. - /// - /// Parameters: - /// - /// * [AssetDeltaSyncDto] assetDeltaSyncDto (required): - Future getDeltaSync(AssetDeltaSyncDto assetDeltaSyncDto,) async { - final response = await getDeltaSyncWithHttpInfo(assetDeltaSyncDto,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetDeltaSyncResponseDto',) as AssetDeltaSyncResponseDto; - - } - return null; - } - - /// Get full sync for user - /// - /// Retrieve all assets for a full synchronization for the authenticated user. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [AssetFullSyncDto] assetFullSyncDto (required): - Future getFullSyncForUserWithHttpInfo(AssetFullSyncDto assetFullSyncDto,) async { - // ignore: prefer_const_declarations - final apiPath = r'/sync/full-sync'; - - // ignore: prefer_final_locals - Object? postBody = assetFullSyncDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - apiPath, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Get full sync for user - /// - /// Retrieve all assets for a full synchronization for the authenticated user. - /// - /// Parameters: - /// - /// * [AssetFullSyncDto] assetFullSyncDto (required): - Future?> getFullSyncForUser(AssetFullSyncDto assetFullSyncDto,) async { - final response = await getFullSyncForUserWithHttpInfo(assetFullSyncDto,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } - /// Retrieve queue counts and status /// /// Retrieve the counts of the current queue, as well as the current status. @@ -298,219 +121,6 @@ class DeprecatedApi { return null; } - /// Get random assets - /// - /// Retrieve a specified number of random assets for the authenticated user. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [num] count: - /// Number of random assets to return - Future getRandomWithHttpInfo({ num? count, }) async { - // ignore: prefer_const_declarations - final apiPath = r'/assets/random'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - if (count != null) { - queryParams.addAll(_queryParams('', 'count', count)); - } - - const contentTypes = []; - - - return apiClient.invokeAPI( - apiPath, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Get random assets - /// - /// Retrieve a specified number of random assets for the authenticated user. - /// - /// Parameters: - /// - /// * [num] count: - /// Number of random assets to return - Future?> getRandom({ num? count, }) async { - final response = await getRandomWithHttpInfo( count: count, ); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } - - /// Replace asset - /// - /// Replace the asset with new file, without changing its id. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [String] id (required): - /// - /// * [MultipartFile] assetData (required): - /// Asset file data - /// - /// * [String] deviceAssetId (required): - /// Device asset ID - /// - /// * [String] deviceId (required): - /// Device ID - /// - /// * [DateTime] fileCreatedAt (required): - /// File creation date - /// - /// * [DateTime] fileModifiedAt (required): - /// File modification date - /// - /// * [String] key: - /// - /// * [String] slug: - /// - /// * [String] duration: - /// Duration (for videos) - /// - /// * [String] filename: - /// Filename - Future replaceAssetWithHttpInfo(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? duration, String? filename, }) async { - // ignore: prefer_const_declarations - final apiPath = r'/assets/{id}/original' - .replaceAll('{id}', id); - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - if (key != null) { - queryParams.addAll(_queryParams('', 'key', key)); - } - if (slug != null) { - queryParams.addAll(_queryParams('', 'slug', slug)); - } - - const contentTypes = ['multipart/form-data']; - - bool hasFields = false; - final mp = MultipartRequest('PUT', Uri.parse(apiPath)); - if (assetData != null) { - hasFields = true; - mp.fields[r'assetData'] = assetData.field; - mp.files.add(assetData); - } - if (deviceAssetId != null) { - hasFields = true; - mp.fields[r'deviceAssetId'] = parameterToString(deviceAssetId); - } - if (deviceId != null) { - hasFields = true; - mp.fields[r'deviceId'] = parameterToString(deviceId); - } - if (duration != null) { - hasFields = true; - mp.fields[r'duration'] = parameterToString(duration); - } - if (fileCreatedAt != null) { - hasFields = true; - mp.fields[r'fileCreatedAt'] = parameterToString(fileCreatedAt); - } - if (fileModifiedAt != null) { - hasFields = true; - mp.fields[r'fileModifiedAt'] = parameterToString(fileModifiedAt); - } - if (filename != null) { - hasFields = true; - mp.fields[r'filename'] = parameterToString(filename); - } - if (hasFields) { - postBody = mp; - } - - return apiClient.invokeAPI( - apiPath, - 'PUT', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Replace asset - /// - /// Replace the asset with new file, without changing its id. - /// - /// Parameters: - /// - /// * [String] id (required): - /// - /// * [MultipartFile] assetData (required): - /// Asset file data - /// - /// * [String] deviceAssetId (required): - /// Device asset ID - /// - /// * [String] deviceId (required): - /// Device ID - /// - /// * [DateTime] fileCreatedAt (required): - /// File creation date - /// - /// * [DateTime] fileModifiedAt (required): - /// File modification date - /// - /// * [String] key: - /// - /// * [String] slug: - /// - /// * [String] duration: - /// Duration (for videos) - /// - /// * [String] filename: - /// Filename - Future replaceAsset(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? duration, String? filename, }) async { - final response = await replaceAssetWithHttpInfo(id, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, slug: slug, duration: duration, filename: filename, ); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetMediaResponseDto',) as AssetMediaResponseDto; - - } - return null; - } - /// Run jobs /// /// Queue all assets for a specific job type. Defaults to only queueing assets that have not yet been processed, but the force command can be used to re-process all assets. @@ -520,7 +130,6 @@ class DeprecatedApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueCommandDto] queueCommandDto (required): Future runQueueCommandLegacyWithHttpInfo(QueueName name, QueueCommandDto queueCommandDto,) async { @@ -556,7 +165,6 @@ class DeprecatedApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueCommandDto] queueCommandDto (required): Future runQueueCommandLegacy(QueueName name, QueueCommandDto queueCommandDto,) async { diff --git a/mobile/openapi/lib/api/jobs_api.dart b/mobile/openapi/lib/api/jobs_api.dart index 41517f8144..9dda59a883 100644 --- a/mobile/openapi/lib/api/jobs_api.dart +++ b/mobile/openapi/lib/api/jobs_api.dart @@ -121,7 +121,6 @@ class JobsApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueCommandDto] queueCommandDto (required): Future runQueueCommandLegacyWithHttpInfo(QueueName name, QueueCommandDto queueCommandDto,) async { @@ -157,7 +156,6 @@ class JobsApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueCommandDto] queueCommandDto (required): Future runQueueCommandLegacy(QueueName name, QueueCommandDto queueCommandDto,) async { diff --git a/mobile/openapi/lib/api/memories_api.dart b/mobile/openapi/lib/api/memories_api.dart index 913205428e..0cd96ac442 100644 --- a/mobile/openapi/lib/api/memories_api.dart +++ b/mobile/openapi/lib/api/memories_api.dart @@ -260,13 +260,11 @@ class MemoriesApi { /// Include trashed memories /// /// * [MemorySearchOrder] order: - /// Sort order /// /// * [int] size: /// Number of memories to return /// /// * [MemoryType] type: - /// Memory type Future memoriesStatisticsWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { // ignore: prefer_const_declarations final apiPath = r'/memories/statistics'; @@ -327,13 +325,11 @@ class MemoriesApi { /// Include trashed memories /// /// * [MemorySearchOrder] order: - /// Sort order /// /// * [int] size: /// Number of memories to return /// /// * [MemoryType] type: - /// Memory type Future memoriesStatistics({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { final response = await memoriesStatisticsWithHttpInfo( for_: for_, isSaved: isSaved, isTrashed: isTrashed, order: order, size: size, type: type, ); if (response.statusCode >= HttpStatus.badRequest) { @@ -431,13 +427,11 @@ class MemoriesApi { /// Include trashed memories /// /// * [MemorySearchOrder] order: - /// Sort order /// /// * [int] size: /// Number of memories to return /// /// * [MemoryType] type: - /// Memory type Future searchMemoriesWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { // ignore: prefer_const_declarations final apiPath = r'/memories'; @@ -498,13 +492,11 @@ class MemoriesApi { /// Include trashed memories /// /// * [MemorySearchOrder] order: - /// Sort order /// /// * [int] size: /// Number of memories to return /// /// * [MemoryType] type: - /// Memory type Future?> searchMemories({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { final response = await searchMemoriesWithHttpInfo( for_: for_, isSaved: isSaved, isTrashed: isTrashed, order: order, size: size, type: type, ); if (response.statusCode >= HttpStatus.badRequest) { diff --git a/mobile/openapi/lib/api/notifications_api.dart b/mobile/openapi/lib/api/notifications_api.dart index d4e2b1d80f..ab0be3e8f3 100644 --- a/mobile/openapi/lib/api/notifications_api.dart +++ b/mobile/openapi/lib/api/notifications_api.dart @@ -182,10 +182,8 @@ class NotificationsApi { /// Filter by notification ID /// /// * [NotificationLevel] level: - /// Filter by notification level /// /// * [NotificationType] type: - /// Filter by notification type /// /// * [bool] unread: /// Filter by unread status @@ -237,10 +235,8 @@ class NotificationsApi { /// Filter by notification ID /// /// * [NotificationLevel] level: - /// Filter by notification level /// /// * [NotificationType] type: - /// Filter by notification type /// /// * [bool] unread: /// Filter by unread status diff --git a/mobile/openapi/lib/api/partners_api.dart b/mobile/openapi/lib/api/partners_api.dart index 3b15b90909..7d18f6d867 100644 --- a/mobile/openapi/lib/api/partners_api.dart +++ b/mobile/openapi/lib/api/partners_api.dart @@ -138,7 +138,6 @@ class PartnersApi { /// Parameters: /// /// * [PartnerDirection] direction (required): - /// Partner direction Future getPartnersWithHttpInfo(PartnerDirection direction,) async { // ignore: prefer_const_declarations final apiPath = r'/partners'; @@ -173,7 +172,6 @@ class PartnersApi { /// Parameters: /// /// * [PartnerDirection] direction (required): - /// Partner direction Future?> getPartners(PartnerDirection direction,) async { final response = await getPartnersWithHttpInfo(direction,); if (response.statusCode >= HttpStatus.badRequest) { diff --git a/mobile/openapi/lib/api/queues_api.dart b/mobile/openapi/lib/api/queues_api.dart index ecb556e434..1312cb5952 100644 --- a/mobile/openapi/lib/api/queues_api.dart +++ b/mobile/openapi/lib/api/queues_api.dart @@ -25,7 +25,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueDeleteDto] queueDeleteDto (required): Future emptyQueueWithHttpInfo(QueueName name, QueueDeleteDto queueDeleteDto,) async { @@ -61,7 +60,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueDeleteDto] queueDeleteDto (required): Future emptyQueue(QueueName name, QueueDeleteDto queueDeleteDto,) async { @@ -80,7 +78,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name Future getQueueWithHttpInfo(QueueName name,) async { // ignore: prefer_const_declarations final apiPath = r'/queues/{name}' @@ -114,7 +111,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name Future getQueue(QueueName name,) async { final response = await getQueueWithHttpInfo(name,); if (response.statusCode >= HttpStatus.badRequest) { @@ -139,7 +135,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [List] status: /// Filter jobs by status @@ -180,7 +175,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [List] status: /// Filter jobs by status @@ -262,7 +256,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueUpdateDto] queueUpdateDto (required): Future updateQueueWithHttpInfo(QueueName name, QueueUpdateDto queueUpdateDto,) async { @@ -298,7 +291,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueUpdateDto] queueUpdateDto (required): Future updateQueue(QueueName name, QueueUpdateDto queueUpdateDto,) async { diff --git a/mobile/openapi/lib/api/search_api.dart b/mobile/openapi/lib/api/search_api.dart index 085958de66..730627d4a1 100644 --- a/mobile/openapi/lib/api/search_api.dart +++ b/mobile/openapi/lib/api/search_api.dart @@ -127,7 +127,6 @@ class SearchApi { /// Parameters: /// /// * [SearchSuggestionType] type (required): - /// Suggestion type /// /// * [String] country: /// Filter by country @@ -198,7 +197,6 @@ class SearchApi { /// Parameters: /// /// * [SearchSuggestionType] type (required): - /// Suggestion type /// /// * [String] country: /// Filter by country @@ -370,9 +368,6 @@ class SearchApi { /// * [DateTime] createdBefore: /// Filter by creation date (before) /// - /// * [String] deviceId: - /// Device ID to filter by - /// /// * [bool] isEncoded: /// Filter by encoded status /// @@ -434,7 +429,6 @@ class SearchApi { /// Filter by trash date (before) /// /// * [AssetTypeEnum] type: - /// Asset type filter /// /// * [DateTime] updatedAfter: /// Filter by update date (after) @@ -443,14 +437,13 @@ class SearchApi { /// Filter by update date (before) /// /// * [AssetVisibility] visibility: - /// Filter by visibility /// /// * [bool] withDeleted: /// Include deleted assets /// /// * [bool] withExif: /// Include EXIF data in response - Future searchLargeAssetsWithHttpInfo({ List? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceId, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List? personIds, num? rating, num? size, String? state, List? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async { + Future searchLargeAssetsWithHttpInfo({ List? 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? personIds, num? rating, num? size, String? state, List? 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 final apiPath = r'/search/large-assets'; @@ -476,9 +469,6 @@ class SearchApi { if (createdBefore != null) { queryParams.addAll(_queryParams('', 'createdBefore', createdBefore)); } - if (deviceId != null) { - queryParams.addAll(_queryParams('', 'deviceId', deviceId)); - } if (isEncoded != null) { queryParams.addAll(_queryParams('', 'isEncoded', isEncoded)); } @@ -593,9 +583,6 @@ class SearchApi { /// * [DateTime] createdBefore: /// Filter by creation date (before) /// - /// * [String] deviceId: - /// Device ID to filter by - /// /// * [bool] isEncoded: /// Filter by encoded status /// @@ -657,7 +644,6 @@ class SearchApi { /// Filter by trash date (before) /// /// * [AssetTypeEnum] type: - /// Asset type filter /// /// * [DateTime] updatedAfter: /// Filter by update date (after) @@ -666,15 +652,14 @@ class SearchApi { /// Filter by update date (before) /// /// * [AssetVisibility] visibility: - /// Filter by visibility /// /// * [bool] withDeleted: /// Include deleted assets /// /// * [bool] withExif: /// Include EXIF data in response - Future?> searchLargeAssets({ List? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceId, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List? personIds, num? rating, num? size, String? state, List? 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, deviceId: deviceId, 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, ); + Future?> searchLargeAssets({ List? 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? personIds, num? rating, num? size, String? state, List? 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, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/server_api.dart b/mobile/openapi/lib/api/server_api.dart index f5b70a9ea4..4e43ec28eb 100644 --- a/mobile/openapi/lib/api/server_api.dart +++ b/mobile/openapi/lib/api/server_api.dart @@ -281,7 +281,7 @@ class ServerApi { /// Get product key /// /// Retrieve information about whether the server currently has a product key registered. - Future getServerLicense() async { + Future getServerLicense() async { final response = await getServerLicenseWithHttpInfo(); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -290,7 +290,7 @@ class ServerApi { // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'LicenseResponseDto',) as LicenseResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserLicense',) as UserLicense; } return null; @@ -724,7 +724,7 @@ class ServerApi { /// Parameters: /// /// * [LicenseKeyDto] licenseKeyDto (required): - Future setServerLicense(LicenseKeyDto licenseKeyDto,) async { + Future setServerLicense(LicenseKeyDto licenseKeyDto,) async { final response = await setServerLicenseWithHttpInfo(licenseKeyDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -733,7 +733,7 @@ class ServerApi { // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'LicenseResponseDto',) as LicenseResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserLicense',) as UserLicense; } return null; diff --git a/mobile/openapi/lib/api/shared_links_api.dart b/mobile/openapi/lib/api/shared_links_api.dart index 084662ace8..4750442287 100644 --- a/mobile/openapi/lib/api/shared_links_api.dart +++ b/mobile/openapi/lib/api/shared_links_api.dart @@ -27,11 +27,7 @@ class SharedLinksApi { /// * [String] id (required): /// /// * [AssetIdsDto] assetIdsDto (required): - /// - /// * [String] key: - /// - /// * [String] slug: - Future addSharedLinkAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto, { String? key, String? slug, }) async { + Future addSharedLinkAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto,) async { // ignore: prefer_const_declarations final apiPath = r'/shared-links/{id}/assets' .replaceAll('{id}', id); @@ -43,13 +39,6 @@ class SharedLinksApi { final headerParams = {}; final formParams = {}; - if (key != null) { - queryParams.addAll(_queryParams('', 'key', key)); - } - if (slug != null) { - queryParams.addAll(_queryParams('', 'slug', slug)); - } - const contentTypes = ['application/json']; @@ -73,12 +62,8 @@ class SharedLinksApi { /// * [String] id (required): /// /// * [AssetIdsDto] assetIdsDto (required): - /// - /// * [String] key: - /// - /// * [String] slug: - Future?> addSharedLinkAssets(String id, AssetIdsDto assetIdsDto, { String? key, String? slug, }) async { - final response = await addSharedLinkAssetsWithHttpInfo(id, assetIdsDto, key: key, slug: slug, ); + Future?> addSharedLinkAssets(String id, AssetIdsDto assetIdsDto,) async { + final response = await addSharedLinkAssetsWithHttpInfo(id, assetIdsDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -235,14 +220,8 @@ class SharedLinksApi { /// /// * [String] key: /// - /// * [String] password: - /// Link password - /// /// * [String] slug: - /// - /// * [String] token: - /// Access token - Future getMySharedLinkWithHttpInfo({ String? key, String? password, String? slug, String? token, }) async { + Future getMySharedLinkWithHttpInfo({ String? key, String? slug, }) async { // ignore: prefer_const_declarations final apiPath = r'/shared-links/me'; @@ -256,15 +235,9 @@ class SharedLinksApi { if (key != null) { queryParams.addAll(_queryParams('', 'key', key)); } - if (password != null) { - queryParams.addAll(_queryParams('', 'password', password)); - } if (slug != null) { queryParams.addAll(_queryParams('', 'slug', slug)); } - if (token != null) { - queryParams.addAll(_queryParams('', 'token', token)); - } const contentTypes = []; @@ -288,15 +261,9 @@ class SharedLinksApi { /// /// * [String] key: /// - /// * [String] password: - /// Link password - /// /// * [String] slug: - /// - /// * [String] token: - /// Access token - Future getMySharedLink({ String? key, String? password, String? slug, String? token, }) async { - final response = await getMySharedLinkWithHttpInfo( key: key, password: password, slug: slug, token: token, ); + Future getMySharedLink({ String? key, String? slug, }) async { + final response = await getMySharedLinkWithHttpInfo( key: key, slug: slug, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/sync_api.dart b/mobile/openapi/lib/api/sync_api.dart index 6194fd0f89..e7bc822ace 100644 --- a/mobile/openapi/lib/api/sync_api.dart +++ b/mobile/openapi/lib/api/sync_api.dart @@ -64,121 +64,6 @@ class SyncApi { } } - /// Get delta sync for user - /// - /// Retrieve changed assets since the last sync for the authenticated user. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [AssetDeltaSyncDto] assetDeltaSyncDto (required): - Future getDeltaSyncWithHttpInfo(AssetDeltaSyncDto assetDeltaSyncDto,) async { - // ignore: prefer_const_declarations - final apiPath = r'/sync/delta-sync'; - - // ignore: prefer_final_locals - Object? postBody = assetDeltaSyncDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - apiPath, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Get delta sync for user - /// - /// Retrieve changed assets since the last sync for the authenticated user. - /// - /// Parameters: - /// - /// * [AssetDeltaSyncDto] assetDeltaSyncDto (required): - Future getDeltaSync(AssetDeltaSyncDto assetDeltaSyncDto,) async { - final response = await getDeltaSyncWithHttpInfo(assetDeltaSyncDto,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetDeltaSyncResponseDto',) as AssetDeltaSyncResponseDto; - - } - return null; - } - - /// Get full sync for user - /// - /// Retrieve all assets for a full synchronization for the authenticated user. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [AssetFullSyncDto] assetFullSyncDto (required): - Future getFullSyncForUserWithHttpInfo(AssetFullSyncDto assetFullSyncDto,) async { - // ignore: prefer_const_declarations - final apiPath = r'/sync/full-sync'; - - // ignore: prefer_final_locals - Object? postBody = assetFullSyncDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - apiPath, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Get full sync for user - /// - /// Retrieve all assets for a full synchronization for the authenticated user. - /// - /// Parameters: - /// - /// * [AssetFullSyncDto] assetFullSyncDto (required): - Future?> getFullSyncForUser(AssetFullSyncDto assetFullSyncDto,) async { - final response = await getFullSyncForUserWithHttpInfo(assetFullSyncDto,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } - /// Retrieve acknowledgements /// /// Retrieve the synchronization acknowledgments for the current session. diff --git a/mobile/openapi/lib/api/timeline_api.dart b/mobile/openapi/lib/api/timeline_api.dart index f82c362ff7..30a4c123f1 100644 --- a/mobile/openapi/lib/api/timeline_api.dart +++ b/mobile/openapi/lib/api/timeline_api.dart @@ -25,7 +25,7 @@ class TimelineApi { /// Parameters: /// /// * [String] timeBucket (required): - /// Time bucket identifier in YYYY-MM-DD format (e.g., \"2024-01-01\" for January 2024) + /// Time bucket identifier in YYYY-MM-DD format /// /// * [String] albumId: /// Filter assets belonging to a specific album @@ -142,7 +142,7 @@ class TimelineApi { /// Parameters: /// /// * [String] timeBucket (required): - /// Time bucket identifier in YYYY-MM-DD format (e.g., \"2024-01-01\" for January 2024) + /// Time bucket identifier in YYYY-MM-DD format /// /// * [String] albumId: /// Filter assets belonging to a specific album diff --git a/mobile/openapi/lib/api/users_admin_api.dart b/mobile/openapi/lib/api/users_admin_api.dart index 59a4b60096..5e165ffd5d 100644 --- a/mobile/openapi/lib/api/users_admin_api.dart +++ b/mobile/openapi/lib/api/users_admin_api.dart @@ -324,7 +324,6 @@ class UsersAdminApi { /// Filter by trash status /// /// * [AssetVisibility] visibility: - /// Filter by visibility Future getUserStatisticsAdminWithHttpInfo(String id, { bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/users/{id}/statistics' @@ -376,7 +375,6 @@ class UsersAdminApi { /// Filter by trash status /// /// * [AssetVisibility] visibility: - /// Filter by visibility Future getUserStatisticsAdmin(String id, { bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { final response = await getUserStatisticsAdminWithHttpInfo(id, isFavorite: isFavorite, isTrashed: isTrashed, visibility: visibility, ); if (response.statusCode >= HttpStatus.badRequest) { diff --git a/mobile/openapi/lib/api/users_api.dart b/mobile/openapi/lib/api/users_api.dart index 7ccae02c76..401cf4e94b 100644 --- a/mobile/openapi/lib/api/users_api.dart +++ b/mobile/openapi/lib/api/users_api.dart @@ -447,7 +447,7 @@ class UsersApi { /// Retrieve user product key /// /// Retrieve information about whether the current user has a registered product key. - Future getUserLicense() async { + Future getUserLicense() async { final response = await getUserLicenseWithHttpInfo(); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -456,7 +456,7 @@ class UsersApi { // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'LicenseResponseDto',) as LicenseResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserLicense',) as UserLicense; } return null; @@ -602,7 +602,7 @@ class UsersApi { /// Parameters: /// /// * [LicenseKeyDto] licenseKeyDto (required): - Future setUserLicense(LicenseKeyDto licenseKeyDto,) async { + Future setUserLicense(LicenseKeyDto licenseKeyDto,) async { final response = await setUserLicenseWithHttpInfo(licenseKeyDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -611,7 +611,7 @@ class UsersApi { // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'LicenseResponseDto',) as LicenseResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserLicense',) as UserLicense; } return null; @@ -731,7 +731,7 @@ class UsersApi { /// Update current user /// - /// Update the current user making teh API request. + /// Update the current user making the API request. /// /// Note: This method returns the HTTP [Response]. /// @@ -765,7 +765,7 @@ class UsersApi { /// Update current user /// - /// Update the current user making teh API request. + /// Update the current user making the API request. /// /// Parameters: /// diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 48e5f5874b..3bc85a9070 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -182,14 +182,6 @@ class ApiClient { return valueString == 'true' || valueString == '1'; case 'DateTime': return value is DateTime ? value : DateTime.tryParse(value); - case 'APIKeyCreateDto': - return APIKeyCreateDto.fromJson(value); - case 'APIKeyCreateResponseDto': - return APIKeyCreateResponseDto.fromJson(value); - case 'APIKeyResponseDto': - return APIKeyResponseDto.fromJson(value); - case 'APIKeyUpdateDto': - return APIKeyUpdateDto.fromJson(value); case 'ActivityCreateDto': return ActivityCreateDto.fromJson(value); case 'ActivityResponseDto': @@ -220,6 +212,14 @@ class ApiClient { return AlbumsResponse.fromJson(value); case 'AlbumsUpdate': return AlbumsUpdate.fromJson(value); + case 'ApiKeyCreateDto': + return ApiKeyCreateDto.fromJson(value); + case 'ApiKeyCreateResponseDto': + return ApiKeyCreateResponseDto.fromJson(value); + case 'ApiKeyResponseDto': + return ApiKeyResponseDto.fromJson(value); + case 'ApiKeyUpdateDto': + return ApiKeyUpdateDto.fromJson(value); case 'AssetBulkDeleteDto': return AssetBulkDeleteDto.fromJson(value); case 'AssetBulkUpdateDto': @@ -234,10 +234,6 @@ class ApiClient { return AssetBulkUploadCheckResult.fromJson(value); case 'AssetCopyDto': return AssetCopyDto.fromJson(value); - case 'AssetDeltaSyncDto': - return AssetDeltaSyncDto.fromJson(value); - case 'AssetDeltaSyncResponseDto': - return AssetDeltaSyncResponseDto.fromJson(value); case 'AssetEditAction': return AssetEditActionTypeTransformer().decode(value); case 'AssetEditActionItemDto': @@ -262,8 +258,8 @@ class ApiClient { return AssetFaceUpdateItem.fromJson(value); case 'AssetFaceWithoutPersonResponseDto': return AssetFaceWithoutPersonResponseDto.fromJson(value); - case 'AssetFullSyncDto': - return AssetFullSyncDto.fromJson(value); + case 'AssetIdErrorReason': + return AssetIdErrorReasonTypeTransformer().decode(value); case 'AssetIdsDto': return AssetIdsDto.fromJson(value); case 'AssetIdsResponseDto': @@ -298,6 +294,8 @@ class ApiClient { return AssetOcrResponseDto.fromJson(value); case 'AssetOrder': return AssetOrderTypeTransformer().decode(value); + case 'AssetRejectReason': + return AssetRejectReasonTypeTransformer().decode(value); case 'AssetResponseDto': return AssetResponseDto.fromJson(value); case 'AssetStackResponseDto': @@ -306,6 +304,8 @@ class ApiClient { return AssetStatsResponseDto.fromJson(value); case 'AssetTypeEnum': return AssetTypeEnumTypeTransformer().decode(value); + case 'AssetUploadAction': + return AssetUploadActionTypeTransformer().decode(value); case 'AssetVisibility': return AssetVisibilityTypeTransformer().decode(value); case 'AudioCodec': @@ -330,10 +330,6 @@ class ApiClient { return CastUpdate.fromJson(value); case 'ChangePasswordDto': return ChangePasswordDto.fromJson(value); - case 'CheckExistingAssetsDto': - return CheckExistingAssetsDto.fromJson(value); - case 'CheckExistingAssetsResponseDto': - return CheckExistingAssetsResponseDto.fromJson(value); case 'Colorspace': return ColorspaceTypeTransformer().decode(value); case 'ContributorCountResponseDto': @@ -402,8 +398,6 @@ class ApiClient { return LibraryStatsResponseDto.fromJson(value); case 'LicenseKeyDto': return LicenseKeyDto.fromJson(value); - case 'LicenseResponseDto': - return LicenseResponseDto.fromJson(value); case 'LogLevel': return LogLevelTypeTransformer().decode(value); case 'LoginCredentialDto': @@ -530,6 +524,14 @@ class ApiClient { return PluginContextTypeTypeTransformer().decode(value); case 'PluginFilterResponseDto': return PluginFilterResponseDto.fromJson(value); + case 'PluginJsonSchema': + return PluginJsonSchema.fromJson(value); + case 'PluginJsonSchemaProperty': + return PluginJsonSchemaProperty.fromJson(value); + case 'PluginJsonSchemaPropertyAdditionalProperties': + return PluginJsonSchemaPropertyAdditionalProperties.fromJson(value); + case 'PluginJsonSchemaType': + return PluginJsonSchemaTypeTypeTransformer().decode(value); case 'PluginResponseDto': return PluginResponseDto.fromJson(value); case 'PluginTriggerResponseDto': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index 830325a5b6..3b36b23d6c 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -61,6 +61,9 @@ String parameterToString(dynamic value) { if (value is AssetEditAction) { return AssetEditActionTypeTransformer().encode(value).toString(); } + if (value is AssetIdErrorReason) { + return AssetIdErrorReasonTypeTransformer().encode(value).toString(); + } if (value is AssetJobName) { return AssetJobNameTypeTransformer().encode(value).toString(); } @@ -73,9 +76,15 @@ String parameterToString(dynamic value) { if (value is AssetOrder) { return AssetOrderTypeTransformer().encode(value).toString(); } + if (value is AssetRejectReason) { + return AssetRejectReasonTypeTransformer().encode(value).toString(); + } if (value is AssetTypeEnum) { return AssetTypeEnumTypeTransformer().encode(value).toString(); } + if (value is AssetUploadAction) { + return AssetUploadActionTypeTransformer().encode(value).toString(); + } if (value is AssetVisibility) { return AssetVisibilityTypeTransformer().encode(value).toString(); } @@ -133,6 +142,9 @@ String parameterToString(dynamic value) { if (value is PluginContextType) { return PluginContextTypeTypeTransformer().encode(value).toString(); } + if (value is PluginJsonSchemaType) { + return PluginJsonSchemaTypeTypeTransformer().encode(value).toString(); + } if (value is PluginTriggerType) { return PluginTriggerTypeTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/activity_create_dto.dart b/mobile/openapi/lib/model/activity_create_dto.dart index fb4b6d084e..bc220e64ce 100644 --- a/mobile/openapi/lib/model/activity_create_dto.dart +++ b/mobile/openapi/lib/model/activity_create_dto.dart @@ -40,7 +40,6 @@ class ActivityCreateDto { /// String? comment; - /// Activity type (like or comment) ReactionType type; @override diff --git a/mobile/openapi/lib/model/activity_response_dto.dart b/mobile/openapi/lib/model/activity_response_dto.dart index dadb45d8ac..1b0e279ab7 100644 --- a/mobile/openapi/lib/model/activity_response_dto.dart +++ b/mobile/openapi/lib/model/activity_response_dto.dart @@ -33,7 +33,6 @@ class ActivityResponseDto { /// Activity ID String id; - /// Activity type ReactionType type; UserResponseDto user; @@ -72,7 +71,9 @@ class ActivityResponseDto { } else { // json[r'comment'] = null; } - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _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.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'id'] = this.id; json[r'type'] = this.type; json[r'user'] = this.user; @@ -90,7 +91,7 @@ class ActivityResponseDto { return ActivityResponseDto( assetId: mapValueOfType(json, r'assetId'), comment: mapValueOfType(json, r'comment'), - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', 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))$/')!, id: mapValueOfType(json, r'id')!, type: ReactionType.fromJson(json[r'type'])!, user: UserResponseDto.fromJson(json[r'user'])!, diff --git a/mobile/openapi/lib/model/activity_statistics_response_dto.dart b/mobile/openapi/lib/model/activity_statistics_response_dto.dart index 15ad2a170e..d9ac019ee2 100644 --- a/mobile/openapi/lib/model/activity_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/activity_statistics_response_dto.dart @@ -18,9 +18,15 @@ class ActivityStatisticsResponseDto { }); /// Number of comments + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int comments; /// Number of likes + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int likes; @override diff --git a/mobile/openapi/lib/model/album_response_dto.dart b/mobile/openapi/lib/model/album_response_dto.dart index 43e686fbdc..348e25ddaf 100644 --- a/mobile/openapi/lib/model/album_response_dto.dart +++ b/mobile/openapi/lib/model/album_response_dto.dart @@ -17,7 +17,6 @@ class AlbumResponseDto { required this.albumThumbnailAssetId, this.albumUsers = const [], required this.assetCount, - this.assets = const [], this.contributorCounts = const [], required this.createdAt, required this.description, @@ -43,10 +42,11 @@ class AlbumResponseDto { List albumUsers; /// Number of assets + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int assetCount; - List assets; - List contributorCounts; /// Creation date @@ -82,7 +82,6 @@ class AlbumResponseDto { /// DateTime? lastModifiedAssetTimestamp; - /// Asset sort order /// /// 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 @@ -117,7 +116,6 @@ class AlbumResponseDto { other.albumThumbnailAssetId == albumThumbnailAssetId && _deepEquality.equals(other.albumUsers, albumUsers) && other.assetCount == assetCount && - _deepEquality.equals(other.assets, assets) && _deepEquality.equals(other.contributorCounts, contributorCounts) && other.createdAt == createdAt && other.description == description && @@ -140,7 +138,6 @@ class AlbumResponseDto { (albumThumbnailAssetId == null ? 0 : albumThumbnailAssetId!.hashCode) + (albumUsers.hashCode) + (assetCount.hashCode) + - (assets.hashCode) + (contributorCounts.hashCode) + (createdAt.hashCode) + (description.hashCode) + @@ -157,7 +154,7 @@ class AlbumResponseDto { (updatedAt.hashCode); @override - String toString() => 'AlbumResponseDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, albumUsers=$albumUsers, assetCount=$assetCount, assets=$assets, contributorCounts=$contributorCounts, createdAt=$createdAt, description=$description, endDate=$endDate, hasSharedLink=$hasSharedLink, id=$id, isActivityEnabled=$isActivityEnabled, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp, order=$order, owner=$owner, ownerId=$ownerId, 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, owner=$owner, ownerId=$ownerId, shared=$shared, startDate=$startDate, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -169,7 +166,6 @@ class AlbumResponseDto { } json[r'albumUsers'] = this.albumUsers; json[r'assetCount'] = this.assetCount; - json[r'assets'] = this.assets; json[r'contributorCounts'] = this.contributorCounts; json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); json[r'description'] = this.description; @@ -216,7 +212,6 @@ class AlbumResponseDto { albumThumbnailAssetId: mapValueOfType(json, r'albumThumbnailAssetId'), albumUsers: AlbumUserResponseDto.listFromJson(json[r'albumUsers']), assetCount: mapValueOfType(json, r'assetCount')!, - assets: AssetResponseDto.listFromJson(json[r'assets']), contributorCounts: ContributorCountResponseDto.listFromJson(json[r'contributorCounts']), createdAt: mapDateTime(json, r'createdAt', r'')!, description: mapValueOfType(json, r'description')!, @@ -282,7 +277,6 @@ class AlbumResponseDto { 'albumThumbnailAssetId', 'albumUsers', 'assetCount', - 'assets', 'createdAt', 'description', 'hasSharedLink', diff --git a/mobile/openapi/lib/model/album_statistics_response_dto.dart b/mobile/openapi/lib/model/album_statistics_response_dto.dart index 127334e687..0f440d572d 100644 --- a/mobile/openapi/lib/model/album_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/album_statistics_response_dto.dart @@ -19,12 +19,21 @@ class AlbumStatisticsResponseDto { }); /// Number of non-shared albums + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int notShared; /// Number of owned albums + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int owned; /// Number of shared albums + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int shared; @override diff --git a/mobile/openapi/lib/model/album_user_add_dto.dart b/mobile/openapi/lib/model/album_user_add_dto.dart index c448a0b4b7..ee457905bd 100644 --- a/mobile/openapi/lib/model/album_user_add_dto.dart +++ b/mobile/openapi/lib/model/album_user_add_dto.dart @@ -13,12 +13,17 @@ part of openapi.api; class AlbumUserAddDto { /// Returns a new [AlbumUserAddDto] instance. AlbumUserAddDto({ - this.role = AlbumUserRole.editor, + this.role, required this.userId, }); - /// Album user role - 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. + /// + AlbumUserRole? role; /// User ID String userId; @@ -31,7 +36,7 @@ class AlbumUserAddDto { @override int get hashCode => // ignore: unnecessary_parenthesis - (role.hashCode) + + (role == null ? 0 : role!.hashCode) + (userId.hashCode); @override @@ -39,7 +44,11 @@ class AlbumUserAddDto { Map toJson() { final json = {}; + if (this.role != null) { json[r'role'] = this.role; + } else { + // json[r'role'] = null; + } json[r'userId'] = this.userId; return json; } @@ -53,7 +62,7 @@ class AlbumUserAddDto { final json = value.cast(); return AlbumUserAddDto( - role: AlbumUserRole.fromJson(json[r'role']) ?? AlbumUserRole.editor, + role: AlbumUserRole.fromJson(json[r'role']), userId: mapValueOfType(json, r'userId')!, ); } diff --git a/mobile/openapi/lib/model/album_user_create_dto.dart b/mobile/openapi/lib/model/album_user_create_dto.dart index 8006748341..26aa35ae78 100644 --- a/mobile/openapi/lib/model/album_user_create_dto.dart +++ b/mobile/openapi/lib/model/album_user_create_dto.dart @@ -17,7 +17,6 @@ class AlbumUserCreateDto { required this.userId, }); - /// Album user role AlbumUserRole role; /// User ID diff --git a/mobile/openapi/lib/model/album_user_response_dto.dart b/mobile/openapi/lib/model/album_user_response_dto.dart index 8d0c01cfb8..bbae03fba7 100644 --- a/mobile/openapi/lib/model/album_user_response_dto.dart +++ b/mobile/openapi/lib/model/album_user_response_dto.dart @@ -17,7 +17,6 @@ class AlbumUserResponseDto { required this.user, }); - /// Album user role AlbumUserRole role; UserResponseDto user; diff --git a/mobile/openapi/lib/model/albums_add_assets_response_dto.dart b/mobile/openapi/lib/model/albums_add_assets_response_dto.dart index 743a9f0645..99e679222e 100644 --- a/mobile/openapi/lib/model/albums_add_assets_response_dto.dart +++ b/mobile/openapi/lib/model/albums_add_assets_response_dto.dart @@ -17,7 +17,6 @@ class AlbumsAddAssetsResponseDto { required this.success, }); - /// Error reason /// /// 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 diff --git a/mobile/openapi/lib/model/albums_response.dart b/mobile/openapi/lib/model/albums_response.dart index 520ee171c1..def205de90 100644 --- a/mobile/openapi/lib/model/albums_response.dart +++ b/mobile/openapi/lib/model/albums_response.dart @@ -13,10 +13,9 @@ part of openapi.api; class AlbumsResponse { /// Returns a new [AlbumsResponse] instance. AlbumsResponse({ - this.defaultAssetOrder = AssetOrder.desc, + required this.defaultAssetOrder, }); - /// Default asset order for albums AssetOrder defaultAssetOrder; @override diff --git a/mobile/openapi/lib/model/albums_update.dart b/mobile/openapi/lib/model/albums_update.dart index 107c65dd1e..d61b5c1398 100644 --- a/mobile/openapi/lib/model/albums_update.dart +++ b/mobile/openapi/lib/model/albums_update.dart @@ -16,7 +16,6 @@ class AlbumsUpdate { this.defaultAssetOrder, }); - /// Default asset order for albums /// /// 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 diff --git a/mobile/openapi/lib/model/api_key_create_dto.dart b/mobile/openapi/lib/model/api_key_create_dto.dart index e64b127820..6d3ffc1eb1 100644 --- a/mobile/openapi/lib/model/api_key_create_dto.dart +++ b/mobile/openapi/lib/model/api_key_create_dto.dart @@ -10,9 +10,9 @@ part of openapi.api; -class APIKeyCreateDto { - /// Returns a new [APIKeyCreateDto] instance. - APIKeyCreateDto({ +class ApiKeyCreateDto { + /// Returns a new [ApiKeyCreateDto] instance. + ApiKeyCreateDto({ this.name, this.permissions = const [], }); @@ -30,7 +30,7 @@ class APIKeyCreateDto { List permissions; @override - bool operator ==(Object other) => identical(this, other) || other is APIKeyCreateDto && + bool operator ==(Object other) => identical(this, other) || other is ApiKeyCreateDto && other.name == name && _deepEquality.equals(other.permissions, permissions); @@ -41,7 +41,7 @@ class APIKeyCreateDto { (permissions.hashCode); @override - String toString() => 'APIKeyCreateDto[name=$name, permissions=$permissions]'; + String toString() => 'ApiKeyCreateDto[name=$name, permissions=$permissions]'; Map toJson() { final json = {}; @@ -54,15 +54,15 @@ class APIKeyCreateDto { return json; } - /// Returns a new [APIKeyCreateDto] instance and imports its values from + /// Returns a new [ApiKeyCreateDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static APIKeyCreateDto? fromJson(dynamic value) { - upgradeDto(value, "APIKeyCreateDto"); + static ApiKeyCreateDto? fromJson(dynamic value) { + upgradeDto(value, "ApiKeyCreateDto"); if (value is Map) { final json = value.cast(); - return APIKeyCreateDto( + return ApiKeyCreateDto( name: mapValueOfType(json, r'name'), permissions: Permission.listFromJson(json[r'permissions']), ); @@ -70,11 +70,11 @@ class APIKeyCreateDto { return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = APIKeyCreateDto.fromJson(row); + final value = ApiKeyCreateDto.fromJson(row); if (value != null) { result.add(value); } @@ -83,12 +83,12 @@ class APIKeyCreateDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = APIKeyCreateDto.fromJson(entry.value); + final value = ApiKeyCreateDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -97,14 +97,14 @@ class APIKeyCreateDto { return map; } - // maps a json object with a list of APIKeyCreateDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of ApiKeyCreateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = APIKeyCreateDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = ApiKeyCreateDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/api_key_create_response_dto.dart b/mobile/openapi/lib/model/api_key_create_response_dto.dart index 7540c4bb26..77b19ebfd2 100644 --- a/mobile/openapi/lib/model/api_key_create_response_dto.dart +++ b/mobile/openapi/lib/model/api_key_create_response_dto.dart @@ -10,20 +10,20 @@ part of openapi.api; -class APIKeyCreateResponseDto { - /// Returns a new [APIKeyCreateResponseDto] instance. - APIKeyCreateResponseDto({ +class ApiKeyCreateResponseDto { + /// Returns a new [ApiKeyCreateResponseDto] instance. + ApiKeyCreateResponseDto({ required this.apiKey, required this.secret, }); - APIKeyResponseDto apiKey; + ApiKeyResponseDto apiKey; /// API key secret (only shown once) String secret; @override - bool operator ==(Object other) => identical(this, other) || other is APIKeyCreateResponseDto && + bool operator ==(Object other) => identical(this, other) || other is ApiKeyCreateResponseDto && other.apiKey == apiKey && other.secret == secret; @@ -34,7 +34,7 @@ class APIKeyCreateResponseDto { (secret.hashCode); @override - String toString() => 'APIKeyCreateResponseDto[apiKey=$apiKey, secret=$secret]'; + String toString() => 'ApiKeyCreateResponseDto[apiKey=$apiKey, secret=$secret]'; Map toJson() { final json = {}; @@ -43,27 +43,27 @@ class APIKeyCreateResponseDto { return json; } - /// Returns a new [APIKeyCreateResponseDto] instance and imports its values from + /// Returns a new [ApiKeyCreateResponseDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static APIKeyCreateResponseDto? fromJson(dynamic value) { - upgradeDto(value, "APIKeyCreateResponseDto"); + static ApiKeyCreateResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ApiKeyCreateResponseDto"); if (value is Map) { final json = value.cast(); - return APIKeyCreateResponseDto( - apiKey: APIKeyResponseDto.fromJson(json[r'apiKey'])!, + return ApiKeyCreateResponseDto( + apiKey: ApiKeyResponseDto.fromJson(json[r'apiKey'])!, secret: mapValueOfType(json, r'secret')!, ); } return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = APIKeyCreateResponseDto.fromJson(row); + final value = ApiKeyCreateResponseDto.fromJson(row); if (value != null) { result.add(value); } @@ -72,12 +72,12 @@ class APIKeyCreateResponseDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = APIKeyCreateResponseDto.fromJson(entry.value); + final value = ApiKeyCreateResponseDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -86,14 +86,14 @@ class APIKeyCreateResponseDto { return map; } - // maps a json object with a list of APIKeyCreateResponseDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of ApiKeyCreateResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = APIKeyCreateResponseDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = ApiKeyCreateResponseDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/api_key_response_dto.dart b/mobile/openapi/lib/model/api_key_response_dto.dart index 32ba543342..79099188a3 100644 --- a/mobile/openapi/lib/model/api_key_response_dto.dart +++ b/mobile/openapi/lib/model/api_key_response_dto.dart @@ -10,9 +10,9 @@ part of openapi.api; -class APIKeyResponseDto { - /// Returns a new [APIKeyResponseDto] instance. - APIKeyResponseDto({ +class ApiKeyResponseDto { + /// Returns a new [ApiKeyResponseDto] instance. + ApiKeyResponseDto({ required this.createdAt, required this.id, required this.name, @@ -36,7 +36,7 @@ class APIKeyResponseDto { DateTime updatedAt; @override - bool operator ==(Object other) => identical(this, other) || other is APIKeyResponseDto && + bool operator ==(Object other) => identical(this, other) || other is ApiKeyResponseDto && other.createdAt == createdAt && other.id == id && other.name == name && @@ -53,42 +53,46 @@ class APIKeyResponseDto { (updatedAt.hashCode); @override - String toString() => 'APIKeyResponseDto[createdAt=$createdAt, id=$id, name=$name, permissions=$permissions, updatedAt=$updatedAt]'; + String toString() => 'ApiKeyResponseDto[createdAt=$createdAt, id=$id, name=$name, permissions=$permissions, updatedAt=$updatedAt]'; Map toJson() { final json = {}; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _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.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'id'] = this.id; json[r'name'] = this.name; json[r'permissions'] = this.permissions; - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _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.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } - /// Returns a new [APIKeyResponseDto] instance and imports its values from + /// Returns a new [ApiKeyResponseDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static APIKeyResponseDto? fromJson(dynamic value) { - upgradeDto(value, "APIKeyResponseDto"); + static ApiKeyResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ApiKeyResponseDto"); if (value is Map) { final json = value.cast(); - return APIKeyResponseDto( - createdAt: mapDateTime(json, r'createdAt', r'')!, + return ApiKeyResponseDto( + createdAt: mapDateTime(json, r'createdAt', 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))$/')!, id: mapValueOfType(json, r'id')!, name: mapValueOfType(json, r'name')!, permissions: Permission.listFromJson(json[r'permissions']), - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + updatedAt: mapDateTime(json, r'updatedAt', 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))$/')!, ); } return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = APIKeyResponseDto.fromJson(row); + final value = ApiKeyResponseDto.fromJson(row); if (value != null) { result.add(value); } @@ -97,12 +101,12 @@ class APIKeyResponseDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = APIKeyResponseDto.fromJson(entry.value); + final value = ApiKeyResponseDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -111,14 +115,14 @@ class APIKeyResponseDto { return map; } - // maps a json object with a list of APIKeyResponseDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of ApiKeyResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = APIKeyResponseDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = ApiKeyResponseDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/api_key_update_dto.dart b/mobile/openapi/lib/model/api_key_update_dto.dart index ba107bcda2..c8df4be654 100644 --- a/mobile/openapi/lib/model/api_key_update_dto.dart +++ b/mobile/openapi/lib/model/api_key_update_dto.dart @@ -10,9 +10,9 @@ part of openapi.api; -class APIKeyUpdateDto { - /// Returns a new [APIKeyUpdateDto] instance. - APIKeyUpdateDto({ +class ApiKeyUpdateDto { + /// Returns a new [ApiKeyUpdateDto] instance. + ApiKeyUpdateDto({ this.name, this.permissions = const [], }); @@ -30,7 +30,7 @@ class APIKeyUpdateDto { List permissions; @override - bool operator ==(Object other) => identical(this, other) || other is APIKeyUpdateDto && + bool operator ==(Object other) => identical(this, other) || other is ApiKeyUpdateDto && other.name == name && _deepEquality.equals(other.permissions, permissions); @@ -41,7 +41,7 @@ class APIKeyUpdateDto { (permissions.hashCode); @override - String toString() => 'APIKeyUpdateDto[name=$name, permissions=$permissions]'; + String toString() => 'ApiKeyUpdateDto[name=$name, permissions=$permissions]'; Map toJson() { final json = {}; @@ -54,15 +54,15 @@ class APIKeyUpdateDto { return json; } - /// Returns a new [APIKeyUpdateDto] instance and imports its values from + /// Returns a new [ApiKeyUpdateDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static APIKeyUpdateDto? fromJson(dynamic value) { - upgradeDto(value, "APIKeyUpdateDto"); + static ApiKeyUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "ApiKeyUpdateDto"); if (value is Map) { final json = value.cast(); - return APIKeyUpdateDto( + return ApiKeyUpdateDto( name: mapValueOfType(json, r'name'), permissions: Permission.listFromJson(json[r'permissions']), ); @@ -70,11 +70,11 @@ class APIKeyUpdateDto { return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = APIKeyUpdateDto.fromJson(row); + final value = ApiKeyUpdateDto.fromJson(row); if (value != null) { result.add(value); } @@ -83,12 +83,12 @@ class APIKeyUpdateDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = APIKeyUpdateDto.fromJson(entry.value); + final value = ApiKeyUpdateDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -97,14 +97,14 @@ class APIKeyUpdateDto { return map; } - // maps a json object with a list of APIKeyUpdateDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of ApiKeyUpdateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = APIKeyUpdateDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = ApiKeyUpdateDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/asset_bulk_update_dto.dart b/mobile/openapi/lib/model/asset_bulk_update_dto.dart index 99bac7abfa..f97300b19f 100644 --- a/mobile/openapi/lib/model/asset_bulk_update_dto.dart +++ b/mobile/openapi/lib/model/asset_bulk_update_dto.dart @@ -70,6 +70,9 @@ class AssetBulkUpdateDto { /// Latitude coordinate /// + /// Minimum value: -90 + /// Maximum value: 90 + /// /// 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. @@ -79,6 +82,9 @@ class AssetBulkUpdateDto { /// Longitude coordinate /// + /// Minimum value: -180 + /// Maximum value: 180 + /// /// 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. @@ -90,7 +96,7 @@ class AssetBulkUpdateDto { /// /// Minimum value: -1 /// Maximum value: 5 - num? rating; + int? rating; /// Time zone (IANA timezone) /// @@ -101,7 +107,6 @@ class AssetBulkUpdateDto { /// String? timeZone; - /// Asset visibility /// /// 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 @@ -217,9 +222,7 @@ class AssetBulkUpdateDto { isFavorite: mapValueOfType(json, r'isFavorite'), latitude: num.parse('${json[r'latitude']}'), longitude: num.parse('${json[r'longitude']}'), - rating: json[r'rating'] == null - ? null - : num.parse('${json[r'rating']}'), + rating: mapValueOfType(json, r'rating'), timeZone: mapValueOfType(json, r'timeZone'), visibility: AssetVisibility.fromJson(json[r'visibility']), ); diff --git a/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart b/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart index b56370f689..bf3ee8e244 100644 --- a/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart +++ b/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart @@ -20,8 +20,7 @@ class AssetBulkUploadCheckResult { this.reason, }); - /// Upload action - AssetBulkUploadCheckResultActionEnum action; + AssetUploadAction action; /// Existing asset ID if duplicate /// @@ -44,8 +43,13 @@ class AssetBulkUploadCheckResult { /// bool? isTrashed; - /// Rejection reason if rejected - AssetBulkUploadCheckResultReasonEnum? reason; + /// + /// 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. + /// + AssetRejectReason? reason; @override bool operator ==(Object other) => identical(this, other) || other is AssetBulkUploadCheckResult && @@ -98,11 +102,11 @@ class AssetBulkUploadCheckResult { final json = value.cast(); return AssetBulkUploadCheckResult( - action: AssetBulkUploadCheckResultActionEnum.fromJson(json[r'action'])!, + action: AssetUploadAction.fromJson(json[r'action'])!, assetId: mapValueOfType(json, r'assetId'), id: mapValueOfType(json, r'id')!, isTrashed: mapValueOfType(json, r'isTrashed'), - reason: AssetBulkUploadCheckResultReasonEnum.fromJson(json[r'reason']), + reason: AssetRejectReason.fromJson(json[r'reason']), ); } return null; @@ -155,151 +159,3 @@ class AssetBulkUploadCheckResult { }; } -/// Upload action -class AssetBulkUploadCheckResultActionEnum { - /// Instantiate a new enum with the provided [value]. - const AssetBulkUploadCheckResultActionEnum._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const accept = AssetBulkUploadCheckResultActionEnum._(r'accept'); - static const reject = AssetBulkUploadCheckResultActionEnum._(r'reject'); - - /// List of all possible values in this [enum][AssetBulkUploadCheckResultActionEnum]. - static const values = [ - accept, - reject, - ]; - - static AssetBulkUploadCheckResultActionEnum? fromJson(dynamic value) => AssetBulkUploadCheckResultActionEnumTypeTransformer().decode(value); - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = AssetBulkUploadCheckResultActionEnum.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [AssetBulkUploadCheckResultActionEnum] to String, -/// and [decode] dynamic data back to [AssetBulkUploadCheckResultActionEnum]. -class AssetBulkUploadCheckResultActionEnumTypeTransformer { - factory AssetBulkUploadCheckResultActionEnumTypeTransformer() => _instance ??= const AssetBulkUploadCheckResultActionEnumTypeTransformer._(); - - const AssetBulkUploadCheckResultActionEnumTypeTransformer._(); - - String encode(AssetBulkUploadCheckResultActionEnum data) => data.value; - - /// Decodes a [dynamic value][data] to a AssetBulkUploadCheckResultActionEnum. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - AssetBulkUploadCheckResultActionEnum? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'accept': return AssetBulkUploadCheckResultActionEnum.accept; - case r'reject': return AssetBulkUploadCheckResultActionEnum.reject; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [AssetBulkUploadCheckResultActionEnumTypeTransformer] instance. - static AssetBulkUploadCheckResultActionEnumTypeTransformer? _instance; -} - - -/// Rejection reason if rejected -class AssetBulkUploadCheckResultReasonEnum { - /// Instantiate a new enum with the provided [value]. - const AssetBulkUploadCheckResultReasonEnum._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const duplicate = AssetBulkUploadCheckResultReasonEnum._(r'duplicate'); - static const unsupportedFormat = AssetBulkUploadCheckResultReasonEnum._(r'unsupported-format'); - - /// List of all possible values in this [enum][AssetBulkUploadCheckResultReasonEnum]. - static const values = [ - duplicate, - unsupportedFormat, - ]; - - static AssetBulkUploadCheckResultReasonEnum? fromJson(dynamic value) => AssetBulkUploadCheckResultReasonEnumTypeTransformer().decode(value); - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = AssetBulkUploadCheckResultReasonEnum.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [AssetBulkUploadCheckResultReasonEnum] to String, -/// and [decode] dynamic data back to [AssetBulkUploadCheckResultReasonEnum]. -class AssetBulkUploadCheckResultReasonEnumTypeTransformer { - factory AssetBulkUploadCheckResultReasonEnumTypeTransformer() => _instance ??= const AssetBulkUploadCheckResultReasonEnumTypeTransformer._(); - - const AssetBulkUploadCheckResultReasonEnumTypeTransformer._(); - - String encode(AssetBulkUploadCheckResultReasonEnum data) => data.value; - - /// Decodes a [dynamic value][data] to a AssetBulkUploadCheckResultReasonEnum. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - AssetBulkUploadCheckResultReasonEnum? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'duplicate': return AssetBulkUploadCheckResultReasonEnum.duplicate; - case r'unsupported-format': return AssetBulkUploadCheckResultReasonEnum.unsupportedFormat; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [AssetBulkUploadCheckResultReasonEnumTypeTransformer] instance. - static AssetBulkUploadCheckResultReasonEnumTypeTransformer? _instance; -} - - diff --git a/mobile/openapi/lib/model/asset_delta_sync_dto.dart b/mobile/openapi/lib/model/asset_delta_sync_dto.dart deleted file mode 100644 index 22c09752d2..0000000000 --- a/mobile/openapi/lib/model/asset_delta_sync_dto.dart +++ /dev/null @@ -1,111 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class AssetDeltaSyncDto { - /// Returns a new [AssetDeltaSyncDto] instance. - AssetDeltaSyncDto({ - required this.updatedAfter, - this.userIds = const [], - }); - - /// Sync assets updated after this date - DateTime updatedAfter; - - /// User IDs to sync - List userIds; - - @override - bool operator ==(Object other) => identical(this, other) || other is AssetDeltaSyncDto && - other.updatedAfter == updatedAfter && - _deepEquality.equals(other.userIds, userIds); - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (updatedAfter.hashCode) + - (userIds.hashCode); - - @override - String toString() => 'AssetDeltaSyncDto[updatedAfter=$updatedAfter, userIds=$userIds]'; - - Map toJson() { - final json = {}; - json[r'updatedAfter'] = this.updatedAfter.toUtc().toIso8601String(); - json[r'userIds'] = this.userIds; - return json; - } - - /// Returns a new [AssetDeltaSyncDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static AssetDeltaSyncDto? fromJson(dynamic value) { - upgradeDto(value, "AssetDeltaSyncDto"); - if (value is Map) { - final json = value.cast(); - - return AssetDeltaSyncDto( - updatedAfter: mapDateTime(json, r'updatedAfter', r'')!, - userIds: json[r'userIds'] is Iterable - ? (json[r'userIds'] as Iterable).cast().toList(growable: false) - : const [], - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = AssetDeltaSyncDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = AssetDeltaSyncDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of AssetDeltaSyncDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = AssetDeltaSyncDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'updatedAfter', - 'userIds', - }; -} - diff --git a/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart b/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart deleted file mode 100644 index 7351840b11..0000000000 --- a/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart +++ /dev/null @@ -1,120 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class AssetDeltaSyncResponseDto { - /// Returns a new [AssetDeltaSyncResponseDto] instance. - AssetDeltaSyncResponseDto({ - this.deleted = const [], - required this.needsFullSync, - this.upserted = const [], - }); - - /// Deleted asset IDs - List deleted; - - /// Whether full sync is needed - bool needsFullSync; - - /// Upserted assets - List upserted; - - @override - bool operator ==(Object other) => identical(this, other) || other is AssetDeltaSyncResponseDto && - _deepEquality.equals(other.deleted, deleted) && - other.needsFullSync == needsFullSync && - _deepEquality.equals(other.upserted, upserted); - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (deleted.hashCode) + - (needsFullSync.hashCode) + - (upserted.hashCode); - - @override - String toString() => 'AssetDeltaSyncResponseDto[deleted=$deleted, needsFullSync=$needsFullSync, upserted=$upserted]'; - - Map toJson() { - final json = {}; - json[r'deleted'] = this.deleted; - json[r'needsFullSync'] = this.needsFullSync; - json[r'upserted'] = this.upserted; - return json; - } - - /// Returns a new [AssetDeltaSyncResponseDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static AssetDeltaSyncResponseDto? fromJson(dynamic value) { - upgradeDto(value, "AssetDeltaSyncResponseDto"); - if (value is Map) { - final json = value.cast(); - - return AssetDeltaSyncResponseDto( - deleted: json[r'deleted'] is Iterable - ? (json[r'deleted'] as Iterable).cast().toList(growable: false) - : const [], - needsFullSync: mapValueOfType(json, r'needsFullSync')!, - upserted: AssetResponseDto.listFromJson(json[r'upserted']), - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = AssetDeltaSyncResponseDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = AssetDeltaSyncResponseDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of AssetDeltaSyncResponseDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = AssetDeltaSyncResponseDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'deleted', - 'needsFullSync', - 'upserted', - }; -} - diff --git a/mobile/openapi/lib/model/asset_edit_action_item_dto.dart b/mobile/openapi/lib/model/asset_edit_action_item_dto.dart index 7829de4bd5..1b19612bf3 100644 --- a/mobile/openapi/lib/model/asset_edit_action_item_dto.dart +++ b/mobile/openapi/lib/model/asset_edit_action_item_dto.dart @@ -17,10 +17,9 @@ class AssetEditActionItemDto { required this.parameters, }); - /// Type of edit action to perform AssetEditAction action; - AssetEditActionItemDtoParameters parameters; + Map parameters; @override bool operator ==(Object other) => identical(this, other) || other is AssetEditActionItemDto && @@ -53,7 +52,7 @@ class AssetEditActionItemDto { return AssetEditActionItemDto( action: AssetEditAction.fromJson(json[r'action'])!, - parameters: AssetEditActionItemDtoParameters.fromJson(json[r'parameters'])!, + parameters: json[r'parameters'], ); } return null; diff --git a/mobile/openapi/lib/model/asset_edit_action_item_dto_parameters.dart b/mobile/openapi/lib/model/asset_edit_action_item_dto_parameters.dart index fc67aa022f..2086f72929 100644 --- a/mobile/openapi/lib/model/asset_edit_action_item_dto_parameters.dart +++ b/mobile/openapi/lib/model/asset_edit_action_item_dto_parameters.dart @@ -44,7 +44,6 @@ class AssetEditActionItemDtoParameters { /// Rotation angle in degrees num angle; - /// Axis to mirror along MirrorAxis axis; @override diff --git a/mobile/openapi/lib/model/asset_edit_action_item_response_dto.dart b/mobile/openapi/lib/model/asset_edit_action_item_response_dto.dart index a23a1ef5f3..3315fe8579 100644 --- a/mobile/openapi/lib/model/asset_edit_action_item_response_dto.dart +++ b/mobile/openapi/lib/model/asset_edit_action_item_response_dto.dart @@ -18,9 +18,9 @@ class AssetEditActionItemResponseDto { required this.parameters, }); - /// Type of edit action to perform AssetEditAction action; + /// Asset edit ID String id; AssetEditActionItemDtoParameters parameters; diff --git a/mobile/openapi/lib/model/asset_face_create_dto.dart b/mobile/openapi/lib/model/asset_face_create_dto.dart index 3ecc20c699..29c28175cd 100644 --- a/mobile/openapi/lib/model/asset_face_create_dto.dart +++ b/mobile/openapi/lib/model/asset_face_create_dto.dart @@ -27,24 +27,42 @@ class AssetFaceCreateDto { String assetId; /// Face bounding box height + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int height; /// Image height in pixels + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int imageHeight; /// Image width in pixels + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int imageWidth; /// Person ID String personId; /// Face bounding box width + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int width; /// Face bounding box X coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int x; /// Face bounding box Y coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int y; @override diff --git a/mobile/openapi/lib/model/asset_face_response_dto.dart b/mobile/openapi/lib/model/asset_face_response_dto.dart index 61d972a0c4..21b86dfe4e 100644 --- a/mobile/openapi/lib/model/asset_face_response_dto.dart +++ b/mobile/openapi/lib/model/asset_face_response_dto.dart @@ -25,30 +25,46 @@ class AssetFaceResponseDto { }); /// Bounding box X1 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX1; /// Bounding box X2 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX2; /// Bounding box Y1 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY1; /// Bounding box Y2 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY2; /// Face ID String id; /// Image height in pixels + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int imageHeight; /// Image width in pixels + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int imageWidth; - /// Person associated with face PersonResponseDto? person; - /// Face detection source type /// /// 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 diff --git a/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart b/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart index 1ae5cef07e..4a4a2a658e 100644 --- a/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart +++ b/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart @@ -24,27 +24,44 @@ class AssetFaceWithoutPersonResponseDto { }); /// Bounding box X1 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX1; /// Bounding box X2 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX2; /// Bounding box Y1 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY1; /// Bounding box Y2 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY2; /// Face ID String id; /// Image height in pixels + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int imageHeight; /// Image width in pixels + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int imageWidth; - /// Face detection source type /// /// 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 diff --git a/mobile/openapi/lib/model/asset_full_sync_dto.dart b/mobile/openapi/lib/model/asset_full_sync_dto.dart deleted file mode 100644 index 3fabb1cac6..0000000000 --- a/mobile/openapi/lib/model/asset_full_sync_dto.dart +++ /dev/null @@ -1,147 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class AssetFullSyncDto { - /// Returns a new [AssetFullSyncDto] instance. - AssetFullSyncDto({ - this.lastId, - required this.limit, - required this.updatedUntil, - this.userId, - }); - - /// Last asset ID (pagination) - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - String? lastId; - - /// Maximum number of assets to return - /// - /// Minimum value: 1 - int limit; - - /// Sync assets updated until this date - DateTime updatedUntil; - - /// Filter by user ID - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - String? userId; - - @override - bool operator ==(Object other) => identical(this, other) || other is AssetFullSyncDto && - other.lastId == lastId && - other.limit == limit && - other.updatedUntil == updatedUntil && - other.userId == userId; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (lastId == null ? 0 : lastId!.hashCode) + - (limit.hashCode) + - (updatedUntil.hashCode) + - (userId == null ? 0 : userId!.hashCode); - - @override - String toString() => 'AssetFullSyncDto[lastId=$lastId, limit=$limit, updatedUntil=$updatedUntil, userId=$userId]'; - - Map toJson() { - final json = {}; - if (this.lastId != null) { - json[r'lastId'] = this.lastId; - } else { - // json[r'lastId'] = null; - } - json[r'limit'] = this.limit; - json[r'updatedUntil'] = this.updatedUntil.toUtc().toIso8601String(); - if (this.userId != null) { - json[r'userId'] = this.userId; - } else { - // json[r'userId'] = null; - } - return json; - } - - /// Returns a new [AssetFullSyncDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static AssetFullSyncDto? fromJson(dynamic value) { - upgradeDto(value, "AssetFullSyncDto"); - if (value is Map) { - final json = value.cast(); - - return AssetFullSyncDto( - lastId: mapValueOfType(json, r'lastId'), - limit: mapValueOfType(json, r'limit')!, - updatedUntil: mapDateTime(json, r'updatedUntil', r'')!, - userId: mapValueOfType(json, r'userId'), - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = AssetFullSyncDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = AssetFullSyncDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of AssetFullSyncDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = AssetFullSyncDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'limit', - 'updatedUntil', - }; -} - diff --git a/mobile/openapi/lib/model/asset_id_error_reason.dart b/mobile/openapi/lib/model/asset_id_error_reason.dart new file mode 100644 index 0000000000..c51eab1692 --- /dev/null +++ b/mobile/openapi/lib/model/asset_id_error_reason.dart @@ -0,0 +1,88 @@ +// +// 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; + +/// Error reason if failed +class AssetIdErrorReason { + /// Instantiate a new enum with the provided [value]. + const AssetIdErrorReason._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const duplicate = AssetIdErrorReason._(r'duplicate'); + static const noPermission = AssetIdErrorReason._(r'no_permission'); + static const notFound = AssetIdErrorReason._(r'not_found'); + + /// List of all possible values in this [enum][AssetIdErrorReason]. + static const values = [ + duplicate, + noPermission, + notFound, + ]; + + static AssetIdErrorReason? fromJson(dynamic value) => AssetIdErrorReasonTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetIdErrorReason.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [AssetIdErrorReason] to String, +/// and [decode] dynamic data back to [AssetIdErrorReason]. +class AssetIdErrorReasonTypeTransformer { + factory AssetIdErrorReasonTypeTransformer() => _instance ??= const AssetIdErrorReasonTypeTransformer._(); + + const AssetIdErrorReasonTypeTransformer._(); + + String encode(AssetIdErrorReason data) => data.value; + + /// Decodes a [dynamic value][data] to a AssetIdErrorReason. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + AssetIdErrorReason? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'duplicate': return AssetIdErrorReason.duplicate; + case r'no_permission': return AssetIdErrorReason.noPermission; + case r'not_found': return AssetIdErrorReason.notFound; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [AssetIdErrorReasonTypeTransformer] instance. + static AssetIdErrorReasonTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/asset_ids_response_dto.dart b/mobile/openapi/lib/model/asset_ids_response_dto.dart index 9745283021..cafe1b21b9 100644 --- a/mobile/openapi/lib/model/asset_ids_response_dto.dart +++ b/mobile/openapi/lib/model/asset_ids_response_dto.dart @@ -21,8 +21,13 @@ class AssetIdsResponseDto { /// Asset ID String assetId; - /// Error reason if failed - AssetIdsResponseDtoErrorEnum? error; + /// + /// 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. + /// + AssetIdErrorReason? error; /// Whether operation succeeded bool success; @@ -65,7 +70,7 @@ class AssetIdsResponseDto { return AssetIdsResponseDto( assetId: mapValueOfType(json, r'assetId')!, - error: AssetIdsResponseDtoErrorEnum.fromJson(json[r'error']), + error: AssetIdErrorReason.fromJson(json[r'error']), success: mapValueOfType(json, r'success')!, ); } @@ -119,80 +124,3 @@ class AssetIdsResponseDto { }; } -/// Error reason if failed -class AssetIdsResponseDtoErrorEnum { - /// Instantiate a new enum with the provided [value]. - const AssetIdsResponseDtoErrorEnum._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const duplicate = AssetIdsResponseDtoErrorEnum._(r'duplicate'); - static const noPermission = AssetIdsResponseDtoErrorEnum._(r'no_permission'); - static const notFound = AssetIdsResponseDtoErrorEnum._(r'not_found'); - - /// List of all possible values in this [enum][AssetIdsResponseDtoErrorEnum]. - static const values = [ - duplicate, - noPermission, - notFound, - ]; - - static AssetIdsResponseDtoErrorEnum? fromJson(dynamic value) => AssetIdsResponseDtoErrorEnumTypeTransformer().decode(value); - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = AssetIdsResponseDtoErrorEnum.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [AssetIdsResponseDtoErrorEnum] to String, -/// and [decode] dynamic data back to [AssetIdsResponseDtoErrorEnum]. -class AssetIdsResponseDtoErrorEnumTypeTransformer { - factory AssetIdsResponseDtoErrorEnumTypeTransformer() => _instance ??= const AssetIdsResponseDtoErrorEnumTypeTransformer._(); - - const AssetIdsResponseDtoErrorEnumTypeTransformer._(); - - String encode(AssetIdsResponseDtoErrorEnum data) => data.value; - - /// Decodes a [dynamic value][data] to a AssetIdsResponseDtoErrorEnum. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - AssetIdsResponseDtoErrorEnum? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'duplicate': return AssetIdsResponseDtoErrorEnum.duplicate; - case r'no_permission': return AssetIdsResponseDtoErrorEnum.noPermission; - case r'not_found': return AssetIdsResponseDtoErrorEnum.notFound; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [AssetIdsResponseDtoErrorEnumTypeTransformer] instance. - static AssetIdsResponseDtoErrorEnumTypeTransformer? _instance; -} - - diff --git a/mobile/openapi/lib/model/asset_jobs_dto.dart b/mobile/openapi/lib/model/asset_jobs_dto.dart index 0aa5544a3a..5085e3820c 100644 --- a/mobile/openapi/lib/model/asset_jobs_dto.dart +++ b/mobile/openapi/lib/model/asset_jobs_dto.dart @@ -20,7 +20,6 @@ class AssetJobsDto { /// Asset IDs List assetIds; - /// Job name AssetJobName name; @override diff --git a/mobile/openapi/lib/model/asset_media_response_dto.dart b/mobile/openapi/lib/model/asset_media_response_dto.dart index 905e738b6e..6dc5cd3c92 100644 --- a/mobile/openapi/lib/model/asset_media_response_dto.dart +++ b/mobile/openapi/lib/model/asset_media_response_dto.dart @@ -20,7 +20,6 @@ class AssetMediaResponseDto { /// Asset media ID String id; - /// Upload status AssetMediaStatus status; @override diff --git a/mobile/openapi/lib/model/asset_media_size.dart b/mobile/openapi/lib/model/asset_media_size.dart index 087d19da1f..ed7a72a613 100644 --- a/mobile/openapi/lib/model/asset_media_size.dart +++ b/mobile/openapi/lib/model/asset_media_size.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Asset media size class AssetMediaSize { /// Instantiate a new enum with the provided [value]. const AssetMediaSize._(this.value); diff --git a/mobile/openapi/lib/model/asset_metadata_bulk_response_dto.dart b/mobile/openapi/lib/model/asset_metadata_bulk_response_dto.dart index b79a693726..3e16ed8721 100644 --- a/mobile/openapi/lib/model/asset_metadata_bulk_response_dto.dart +++ b/mobile/openapi/lib/model/asset_metadata_bulk_response_dto.dart @@ -16,7 +16,7 @@ class AssetMetadataBulkResponseDto { required this.assetId, required this.key, required this.updatedAt, - required this.value, + this.value = const {}, }); /// Asset ID @@ -29,14 +29,14 @@ class AssetMetadataBulkResponseDto { DateTime updatedAt; /// Metadata value (object) - Object value; + Map value; @override bool operator ==(Object other) => identical(this, other) || other is AssetMetadataBulkResponseDto && other.assetId == assetId && other.key == key && other.updatedAt == updatedAt && - other.value == value; + _deepEquality.equals(other.value, value); @override int get hashCode => @@ -53,7 +53,9 @@ class AssetMetadataBulkResponseDto { final json = {}; json[r'assetId'] = this.assetId; json[r'key'] = this.key; - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _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.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); json[r'value'] = this.value; return json; } @@ -69,8 +71,8 @@ class AssetMetadataBulkResponseDto { return AssetMetadataBulkResponseDto( assetId: mapValueOfType(json, r'assetId')!, key: mapValueOfType(json, r'key')!, - updatedAt: mapDateTime(json, r'updatedAt', r'')!, - value: mapValueOfType(json, r'value')!, + updatedAt: mapDateTime(json, r'updatedAt', 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))$/')!, + value: mapCastOfType(json, r'value')!, ); } return null; diff --git a/mobile/openapi/lib/model/asset_metadata_bulk_upsert_item_dto.dart b/mobile/openapi/lib/model/asset_metadata_bulk_upsert_item_dto.dart index caaf379b30..e4eab08bf1 100644 --- a/mobile/openapi/lib/model/asset_metadata_bulk_upsert_item_dto.dart +++ b/mobile/openapi/lib/model/asset_metadata_bulk_upsert_item_dto.dart @@ -15,7 +15,7 @@ class AssetMetadataBulkUpsertItemDto { AssetMetadataBulkUpsertItemDto({ required this.assetId, required this.key, - required this.value, + this.value = const {}, }); /// Asset ID @@ -25,13 +25,13 @@ class AssetMetadataBulkUpsertItemDto { String key; /// Metadata value (object) - Object value; + Map value; @override bool operator ==(Object other) => identical(this, other) || other is AssetMetadataBulkUpsertItemDto && other.assetId == assetId && other.key == key && - other.value == value; + _deepEquality.equals(other.value, value); @override int get hashCode => @@ -62,7 +62,7 @@ class AssetMetadataBulkUpsertItemDto { return AssetMetadataBulkUpsertItemDto( assetId: mapValueOfType(json, r'assetId')!, key: mapValueOfType(json, r'key')!, - value: mapValueOfType(json, r'value')!, + value: mapCastOfType(json, r'value')!, ); } return null; diff --git a/mobile/openapi/lib/model/asset_metadata_response_dto.dart b/mobile/openapi/lib/model/asset_metadata_response_dto.dart index 2c3faab178..d3562f5a48 100644 --- a/mobile/openapi/lib/model/asset_metadata_response_dto.dart +++ b/mobile/openapi/lib/model/asset_metadata_response_dto.dart @@ -15,7 +15,7 @@ class AssetMetadataResponseDto { AssetMetadataResponseDto({ required this.key, required this.updatedAt, - required this.value, + this.value = const {}, }); /// Metadata key @@ -25,13 +25,13 @@ class AssetMetadataResponseDto { DateTime updatedAt; /// Metadata value (object) - Object value; + Map value; @override bool operator ==(Object other) => identical(this, other) || other is AssetMetadataResponseDto && other.key == key && other.updatedAt == updatedAt && - other.value == value; + _deepEquality.equals(other.value, value); @override int get hashCode => @@ -46,7 +46,9 @@ class AssetMetadataResponseDto { Map toJson() { final json = {}; json[r'key'] = this.key; - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _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.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); json[r'value'] = this.value; return json; } @@ -61,8 +63,8 @@ class AssetMetadataResponseDto { return AssetMetadataResponseDto( key: mapValueOfType(json, r'key')!, - updatedAt: mapDateTime(json, r'updatedAt', r'')!, - value: mapValueOfType(json, r'value')!, + updatedAt: mapDateTime(json, r'updatedAt', 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))$/')!, + value: mapCastOfType(json, r'value')!, ); } return null; diff --git a/mobile/openapi/lib/model/asset_metadata_upsert_item_dto.dart b/mobile/openapi/lib/model/asset_metadata_upsert_item_dto.dart index 8a6bcb9b01..70de1941f3 100644 --- a/mobile/openapi/lib/model/asset_metadata_upsert_item_dto.dart +++ b/mobile/openapi/lib/model/asset_metadata_upsert_item_dto.dart @@ -14,19 +14,19 @@ class AssetMetadataUpsertItemDto { /// Returns a new [AssetMetadataUpsertItemDto] instance. AssetMetadataUpsertItemDto({ required this.key, - required this.value, + this.value = const {}, }); /// Metadata key String key; /// Metadata value (object) - Object value; + Map value; @override bool operator ==(Object other) => identical(this, other) || other is AssetMetadataUpsertItemDto && other.key == key && - other.value == value; + _deepEquality.equals(other.value, value); @override int get hashCode => @@ -54,7 +54,7 @@ class AssetMetadataUpsertItemDto { return AssetMetadataUpsertItemDto( key: mapValueOfType(json, r'key')!, - value: mapValueOfType(json, r'value')!, + value: mapCastOfType(json, r'value')!, ); } return null; diff --git a/mobile/openapi/lib/model/asset_reject_reason.dart b/mobile/openapi/lib/model/asset_reject_reason.dart new file mode 100644 index 0000000000..a31e1e6117 --- /dev/null +++ b/mobile/openapi/lib/model/asset_reject_reason.dart @@ -0,0 +1,85 @@ +// +// 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; + +/// Rejection reason if rejected +class AssetRejectReason { + /// Instantiate a new enum with the provided [value]. + const AssetRejectReason._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const duplicate = AssetRejectReason._(r'duplicate'); + static const unsupportedFormat = AssetRejectReason._(r'unsupported-format'); + + /// List of all possible values in this [enum][AssetRejectReason]. + static const values = [ + duplicate, + unsupportedFormat, + ]; + + static AssetRejectReason? fromJson(dynamic value) => AssetRejectReasonTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetRejectReason.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [AssetRejectReason] to String, +/// and [decode] dynamic data back to [AssetRejectReason]. +class AssetRejectReasonTypeTransformer { + factory AssetRejectReasonTypeTransformer() => _instance ??= const AssetRejectReasonTypeTransformer._(); + + const AssetRejectReasonTypeTransformer._(); + + String encode(AssetRejectReason data) => data.value; + + /// Decodes a [dynamic value][data] to a AssetRejectReason. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + AssetRejectReason? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'duplicate': return AssetRejectReason.duplicate; + case r'unsupported-format': return AssetRejectReason.unsupportedFormat; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [AssetRejectReasonTypeTransformer] instance. + static AssetRejectReasonTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart index 078dd0bdaf..a9d346b155 100644 --- a/mobile/openapi/lib/model/asset_response_dto.dart +++ b/mobile/openapi/lib/model/asset_response_dto.dart @@ -15,8 +15,6 @@ class AssetResponseDto { AssetResponseDto({ required this.checksum, required this.createdAt, - required this.deviceAssetId, - required this.deviceId, this.duplicateId, required this.duration, this.exifInfo, @@ -56,12 +54,6 @@ class AssetResponseDto { /// The UTC timestamp when the asset was originally uploaded to Immich. DateTime createdAt; - /// Device asset ID - String deviceAssetId; - - /// Device ID - String deviceId; - /// Duplicate group ID String? duplicateId; @@ -86,6 +78,8 @@ class AssetResponseDto { bool hasMetadata; /// Asset height + /// + /// Minimum value: 0 num? height; /// Asset ID @@ -159,7 +153,6 @@ class AssetResponseDto { /// Thumbhash for thumbnail generation (base64) also used as the c query param for thumbnail cache busting. String? thumbhash; - /// Asset type AssetTypeEnum type; List unassignedFaces; @@ -167,18 +160,17 @@ class AssetResponseDto { /// The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified. DateTime updatedAt; - /// Asset visibility AssetVisibility visibility; /// Asset width + /// + /// Minimum value: 0 num? width; @override bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto && other.checksum == checksum && other.createdAt == createdAt && - other.deviceAssetId == deviceAssetId && - other.deviceId == deviceId && other.duplicateId == duplicateId && other.duration == duration && other.exifInfo == exifInfo && @@ -216,8 +208,6 @@ class AssetResponseDto { // ignore: unnecessary_parenthesis (checksum.hashCode) + (createdAt.hashCode) + - (deviceAssetId.hashCode) + - (deviceId.hashCode) + (duplicateId == null ? 0 : duplicateId!.hashCode) + (duration.hashCode) + (exifInfo == null ? 0 : exifInfo!.hashCode) + @@ -251,14 +241,12 @@ class AssetResponseDto { (width == null ? 0 : width!.hashCode); @override - String toString() => 'AssetResponseDto[checksum=$checksum, createdAt=$createdAt, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, height=$height, id=$id, isArchived=$isArchived, isEdited=$isEdited, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt, visibility=$visibility, width=$width]'; + String toString() => 'AssetResponseDto[checksum=$checksum, createdAt=$createdAt, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, height=$height, id=$id, isArchived=$isArchived, isEdited=$isEdited, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt, visibility=$visibility, width=$width]'; Map toJson() { final json = {}; json[r'checksum'] = this.checksum; json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); - json[r'deviceAssetId'] = this.deviceAssetId; - json[r'deviceId'] = this.deviceId; if (this.duplicateId != null) { json[r'duplicateId'] = this.duplicateId; } else { @@ -348,8 +336,6 @@ class AssetResponseDto { return AssetResponseDto( checksum: mapValueOfType(json, r'checksum')!, createdAt: mapDateTime(json, r'createdAt', r'')!, - deviceAssetId: mapValueOfType(json, r'deviceAssetId')!, - deviceId: mapValueOfType(json, r'deviceId')!, duplicateId: mapValueOfType(json, r'duplicateId'), duration: mapValueOfType(json, r'duration')!, exifInfo: ExifResponseDto.fromJson(json[r'exifInfo']), @@ -434,8 +420,6 @@ class AssetResponseDto { static const requiredKeys = { 'checksum', 'createdAt', - 'deviceAssetId', - 'deviceId', 'duration', 'fileCreatedAt', 'fileModifiedAt', diff --git a/mobile/openapi/lib/model/asset_stack_response_dto.dart b/mobile/openapi/lib/model/asset_stack_response_dto.dart index 229e7aa710..96fd66a392 100644 --- a/mobile/openapi/lib/model/asset_stack_response_dto.dart +++ b/mobile/openapi/lib/model/asset_stack_response_dto.dart @@ -19,6 +19,9 @@ class AssetStackResponseDto { }); /// Number of assets in stack + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int assetCount; /// Stack ID diff --git a/mobile/openapi/lib/model/asset_stats_response_dto.dart b/mobile/openapi/lib/model/asset_stats_response_dto.dart index 201550c87f..df2762a2f3 100644 --- a/mobile/openapi/lib/model/asset_stats_response_dto.dart +++ b/mobile/openapi/lib/model/asset_stats_response_dto.dart @@ -19,12 +19,21 @@ class AssetStatsResponseDto { }); /// Number of images + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int images; /// Total number of assets + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int total; /// Number of videos + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int videos; @override diff --git a/mobile/openapi/lib/model/asset_upload_action.dart b/mobile/openapi/lib/model/asset_upload_action.dart new file mode 100644 index 0000000000..b5cdbb0151 --- /dev/null +++ b/mobile/openapi/lib/model/asset_upload_action.dart @@ -0,0 +1,85 @@ +// +// 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; + +/// Upload action +class AssetUploadAction { + /// Instantiate a new enum with the provided [value]. + const AssetUploadAction._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const accept = AssetUploadAction._(r'accept'); + static const reject = AssetUploadAction._(r'reject'); + + /// List of all possible values in this [enum][AssetUploadAction]. + static const values = [ + accept, + reject, + ]; + + static AssetUploadAction? fromJson(dynamic value) => AssetUploadActionTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetUploadAction.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [AssetUploadAction] to String, +/// and [decode] dynamic data back to [AssetUploadAction]. +class AssetUploadActionTypeTransformer { + factory AssetUploadActionTypeTransformer() => _instance ??= const AssetUploadActionTypeTransformer._(); + + const AssetUploadActionTypeTransformer._(); + + String encode(AssetUploadAction data) => data.value; + + /// Decodes a [dynamic value][data] to a AssetUploadAction. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + AssetUploadAction? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'accept': return AssetUploadAction.accept; + case r'reject': return AssetUploadAction.reject; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [AssetUploadActionTypeTransformer] instance. + static AssetUploadActionTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/avatar_update.dart b/mobile/openapi/lib/model/avatar_update.dart index a817832dab..875eb138a8 100644 --- a/mobile/openapi/lib/model/avatar_update.dart +++ b/mobile/openapi/lib/model/avatar_update.dart @@ -16,7 +16,6 @@ class AvatarUpdate { this.color, }); - /// Avatar color /// /// 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 diff --git a/mobile/openapi/lib/model/bulk_id_response_dto.dart b/mobile/openapi/lib/model/bulk_id_response_dto.dart index 1fa8536964..bb3f1d8856 100644 --- a/mobile/openapi/lib/model/bulk_id_response_dto.dart +++ b/mobile/openapi/lib/model/bulk_id_response_dto.dart @@ -19,8 +19,13 @@ class BulkIdResponseDto { required this.success, }); - /// Error reason if failed - BulkIdResponseDtoErrorEnum? error; + /// + /// 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. + /// + BulkIdErrorReason? error; /// /// Please note: This property should have been non-nullable! Since the specification file @@ -80,7 +85,7 @@ class BulkIdResponseDto { final json = value.cast(); return BulkIdResponseDto( - error: BulkIdResponseDtoErrorEnum.fromJson(json[r'error']), + error: BulkIdErrorReason.fromJson(json[r'error']), errorMessage: mapValueOfType(json, r'errorMessage'), id: mapValueOfType(json, r'id')!, success: mapValueOfType(json, r'success')!, @@ -136,86 +141,3 @@ class BulkIdResponseDto { }; } -/// Error reason if failed -class BulkIdResponseDtoErrorEnum { - /// Instantiate a new enum with the provided [value]. - const BulkIdResponseDtoErrorEnum._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const duplicate = BulkIdResponseDtoErrorEnum._(r'duplicate'); - static const noPermission = BulkIdResponseDtoErrorEnum._(r'no_permission'); - static const notFound = BulkIdResponseDtoErrorEnum._(r'not_found'); - static const unknown = BulkIdResponseDtoErrorEnum._(r'unknown'); - static const validation = BulkIdResponseDtoErrorEnum._(r'validation'); - - /// List of all possible values in this [enum][BulkIdResponseDtoErrorEnum]. - static const values = [ - duplicate, - noPermission, - notFound, - unknown, - validation, - ]; - - static BulkIdResponseDtoErrorEnum? fromJson(dynamic value) => BulkIdResponseDtoErrorEnumTypeTransformer().decode(value); - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = BulkIdResponseDtoErrorEnum.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [BulkIdResponseDtoErrorEnum] to String, -/// and [decode] dynamic data back to [BulkIdResponseDtoErrorEnum]. -class BulkIdResponseDtoErrorEnumTypeTransformer { - factory BulkIdResponseDtoErrorEnumTypeTransformer() => _instance ??= const BulkIdResponseDtoErrorEnumTypeTransformer._(); - - const BulkIdResponseDtoErrorEnumTypeTransformer._(); - - String encode(BulkIdResponseDtoErrorEnum data) => data.value; - - /// Decodes a [dynamic value][data] to a BulkIdResponseDtoErrorEnum. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - BulkIdResponseDtoErrorEnum? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'duplicate': return BulkIdResponseDtoErrorEnum.duplicate; - case r'no_permission': return BulkIdResponseDtoErrorEnum.noPermission; - case r'not_found': return BulkIdResponseDtoErrorEnum.notFound; - case r'unknown': return BulkIdResponseDtoErrorEnum.unknown; - case r'validation': return BulkIdResponseDtoErrorEnum.validation; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [BulkIdResponseDtoErrorEnumTypeTransformer] instance. - static BulkIdResponseDtoErrorEnumTypeTransformer? _instance; -} - - diff --git a/mobile/openapi/lib/model/cast_response.dart b/mobile/openapi/lib/model/cast_response.dart index 0b7f0738fe..796138b0bf 100644 --- a/mobile/openapi/lib/model/cast_response.dart +++ b/mobile/openapi/lib/model/cast_response.dart @@ -13,7 +13,7 @@ part of openapi.api; class CastResponse { /// Returns a new [CastResponse] instance. CastResponse({ - this.gCastEnabled = false, + required this.gCastEnabled, }); /// Whether Google Cast is enabled diff --git a/mobile/openapi/lib/model/check_existing_assets_dto.dart b/mobile/openapi/lib/model/check_existing_assets_dto.dart deleted file mode 100644 index 6e4a471092..0000000000 --- a/mobile/openapi/lib/model/check_existing_assets_dto.dart +++ /dev/null @@ -1,111 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class CheckExistingAssetsDto { - /// Returns a new [CheckExistingAssetsDto] instance. - CheckExistingAssetsDto({ - this.deviceAssetIds = const [], - required this.deviceId, - }); - - /// Device asset IDs to check - List deviceAssetIds; - - /// Device ID - String deviceId; - - @override - bool operator ==(Object other) => identical(this, other) || other is CheckExistingAssetsDto && - _deepEquality.equals(other.deviceAssetIds, deviceAssetIds) && - other.deviceId == deviceId; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (deviceAssetIds.hashCode) + - (deviceId.hashCode); - - @override - String toString() => 'CheckExistingAssetsDto[deviceAssetIds=$deviceAssetIds, deviceId=$deviceId]'; - - Map toJson() { - final json = {}; - json[r'deviceAssetIds'] = this.deviceAssetIds; - json[r'deviceId'] = this.deviceId; - return json; - } - - /// Returns a new [CheckExistingAssetsDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static CheckExistingAssetsDto? fromJson(dynamic value) { - upgradeDto(value, "CheckExistingAssetsDto"); - if (value is Map) { - final json = value.cast(); - - return CheckExistingAssetsDto( - deviceAssetIds: json[r'deviceAssetIds'] is Iterable - ? (json[r'deviceAssetIds'] as Iterable).cast().toList(growable: false) - : const [], - deviceId: mapValueOfType(json, r'deviceId')!, - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = CheckExistingAssetsDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = CheckExistingAssetsDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of CheckExistingAssetsDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = CheckExistingAssetsDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'deviceAssetIds', - 'deviceId', - }; -} - diff --git a/mobile/openapi/lib/model/check_existing_assets_response_dto.dart b/mobile/openapi/lib/model/check_existing_assets_response_dto.dart deleted file mode 100644 index 9fb13f100f..0000000000 --- a/mobile/openapi/lib/model/check_existing_assets_response_dto.dart +++ /dev/null @@ -1,102 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class CheckExistingAssetsResponseDto { - /// Returns a new [CheckExistingAssetsResponseDto] instance. - CheckExistingAssetsResponseDto({ - this.existingIds = const [], - }); - - /// Existing asset IDs - List existingIds; - - @override - bool operator ==(Object other) => identical(this, other) || other is CheckExistingAssetsResponseDto && - _deepEquality.equals(other.existingIds, existingIds); - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (existingIds.hashCode); - - @override - String toString() => 'CheckExistingAssetsResponseDto[existingIds=$existingIds]'; - - Map toJson() { - final json = {}; - json[r'existingIds'] = this.existingIds; - return json; - } - - /// Returns a new [CheckExistingAssetsResponseDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static CheckExistingAssetsResponseDto? fromJson(dynamic value) { - upgradeDto(value, "CheckExistingAssetsResponseDto"); - if (value is Map) { - final json = value.cast(); - - return CheckExistingAssetsResponseDto( - existingIds: json[r'existingIds'] is Iterable - ? (json[r'existingIds'] as Iterable).cast().toList(growable: false) - : const [], - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = CheckExistingAssetsResponseDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = CheckExistingAssetsResponseDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of CheckExistingAssetsResponseDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = CheckExistingAssetsResponseDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'existingIds', - }; -} - diff --git a/mobile/openapi/lib/model/contributor_count_response_dto.dart b/mobile/openapi/lib/model/contributor_count_response_dto.dart index 1bef8f29d8..af5b2cbf68 100644 --- a/mobile/openapi/lib/model/contributor_count_response_dto.dart +++ b/mobile/openapi/lib/model/contributor_count_response_dto.dart @@ -18,6 +18,9 @@ class ContributorCountResponseDto { }); /// Number of assets contributed + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int assetCount; /// User ID diff --git a/mobile/openapi/lib/model/create_library_dto.dart b/mobile/openapi/lib/model/create_library_dto.dart index 69942fee5c..ba12c62d76 100644 --- a/mobile/openapi/lib/model/create_library_dto.dart +++ b/mobile/openapi/lib/model/create_library_dto.dart @@ -13,17 +13,17 @@ part of openapi.api; class CreateLibraryDto { /// Returns a new [CreateLibraryDto] instance. CreateLibraryDto({ - this.exclusionPatterns = const {}, - this.importPaths = const {}, + this.exclusionPatterns = const [], + this.importPaths = const [], this.name, required this.ownerId, }); /// Exclusion patterns (max 128) - Set exclusionPatterns; + List exclusionPatterns; /// Import paths (max 128) - Set importPaths; + List importPaths; /// Library name /// @@ -57,8 +57,8 @@ class CreateLibraryDto { Map toJson() { final json = {}; - json[r'exclusionPatterns'] = this.exclusionPatterns.toList(growable: false); - json[r'importPaths'] = this.importPaths.toList(growable: false); + json[r'exclusionPatterns'] = this.exclusionPatterns; + json[r'importPaths'] = this.importPaths; if (this.name != null) { json[r'name'] = this.name; } else { @@ -78,11 +78,11 @@ class CreateLibraryDto { return CreateLibraryDto( exclusionPatterns: json[r'exclusionPatterns'] is Iterable - ? (json[r'exclusionPatterns'] as Iterable).cast().toSet() - : const {}, + ? (json[r'exclusionPatterns'] as Iterable).cast().toList(growable: false) + : const [], importPaths: json[r'importPaths'] is Iterable - ? (json[r'importPaths'] as Iterable).cast().toSet() - : const {}, + ? (json[r'importPaths'] as Iterable).cast().toList(growable: false) + : const [], name: mapValueOfType(json, r'name'), ownerId: mapValueOfType(json, r'ownerId')!, ); diff --git a/mobile/openapi/lib/model/create_profile_image_response_dto.dart b/mobile/openapi/lib/model/create_profile_image_response_dto.dart index 20d7cbd5e7..c6ec0d94a0 100644 --- a/mobile/openapi/lib/model/create_profile_image_response_dto.dart +++ b/mobile/openapi/lib/model/create_profile_image_response_dto.dart @@ -45,7 +45,9 @@ class CreateProfileImageResponseDto { Map toJson() { final json = {}; - json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String(); + json[r'profileChangedAt'] = _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.profileChangedAt.millisecondsSinceEpoch + : this.profileChangedAt.toUtc().toIso8601String(); json[r'profileImagePath'] = this.profileImagePath; json[r'userId'] = this.userId; return json; @@ -60,7 +62,7 @@ class CreateProfileImageResponseDto { final json = value.cast(); return CreateProfileImageResponseDto( - profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!, + profileChangedAt: mapDateTime(json, r'profileChangedAt', 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))$/')!, profileImagePath: mapValueOfType(json, r'profileImagePath')!, userId: mapValueOfType(json, r'userId')!, ); diff --git a/mobile/openapi/lib/model/database_backup_delete_dto.dart b/mobile/openapi/lib/model/database_backup_delete_dto.dart index 8bc33a81dc..c336270b84 100644 --- a/mobile/openapi/lib/model/database_backup_delete_dto.dart +++ b/mobile/openapi/lib/model/database_backup_delete_dto.dart @@ -16,6 +16,7 @@ class DatabaseBackupDeleteDto { this.backups = const [], }); + /// Backup filenames to delete List backups; @override diff --git a/mobile/openapi/lib/model/database_backup_dto.dart b/mobile/openapi/lib/model/database_backup_dto.dart index 34912a55e0..abfa637157 100644 --- a/mobile/openapi/lib/model/database_backup_dto.dart +++ b/mobile/openapi/lib/model/database_backup_dto.dart @@ -18,10 +18,13 @@ class DatabaseBackupDto { required this.timezone, }); + /// Backup filename String filename; + /// Backup file size num filesize; + /// Backup timezone String timezone; @override diff --git a/mobile/openapi/lib/model/database_backup_list_response_dto.dart b/mobile/openapi/lib/model/database_backup_list_response_dto.dart index 16985dd605..de7bf78d5a 100644 --- a/mobile/openapi/lib/model/database_backup_list_response_dto.dart +++ b/mobile/openapi/lib/model/database_backup_list_response_dto.dart @@ -16,6 +16,7 @@ class DatabaseBackupListResponseDto { this.backups = const [], }); + /// List of backups List backups; @override diff --git a/mobile/openapi/lib/model/download_archive_info.dart b/mobile/openapi/lib/model/download_archive_info.dart index 97a3346a67..dcb1258457 100644 --- a/mobile/openapi/lib/model/download_archive_info.dart +++ b/mobile/openapi/lib/model/download_archive_info.dart @@ -21,6 +21,9 @@ class DownloadArchiveInfo { List assetIds; /// Archive size in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int size; @override diff --git a/mobile/openapi/lib/model/download_info_dto.dart b/mobile/openapi/lib/model/download_info_dto.dart index a1ba44920e..8a0cebd945 100644 --- a/mobile/openapi/lib/model/download_info_dto.dart +++ b/mobile/openapi/lib/model/download_info_dto.dart @@ -31,6 +31,7 @@ class DownloadInfoDto { /// Archive size limit in bytes /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 /// /// 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 diff --git a/mobile/openapi/lib/model/download_response.dart b/mobile/openapi/lib/model/download_response.dart index 32e9487475..bc1d7b4047 100644 --- a/mobile/openapi/lib/model/download_response.dart +++ b/mobile/openapi/lib/model/download_response.dart @@ -14,10 +14,13 @@ class DownloadResponse { /// Returns a new [DownloadResponse] instance. DownloadResponse({ required this.archiveSize, - this.includeEmbeddedVideos = false, + required this.includeEmbeddedVideos, }); /// Maximum archive size in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int archiveSize; /// Whether to include embedded videos in downloads diff --git a/mobile/openapi/lib/model/download_response_dto.dart b/mobile/openapi/lib/model/download_response_dto.dart index 81912e1d30..bfe32307fa 100644 --- a/mobile/openapi/lib/model/download_response_dto.dart +++ b/mobile/openapi/lib/model/download_response_dto.dart @@ -21,6 +21,9 @@ class DownloadResponseDto { List archives; /// Total size in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int totalSize; @override diff --git a/mobile/openapi/lib/model/download_update.dart b/mobile/openapi/lib/model/download_update.dart index 4acc1c8bd3..c5feb9df43 100644 --- a/mobile/openapi/lib/model/download_update.dart +++ b/mobile/openapi/lib/model/download_update.dart @@ -20,6 +20,7 @@ class DownloadUpdate { /// Maximum archive size in bytes /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 /// /// 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 diff --git a/mobile/openapi/lib/model/exif_response_dto.dart b/mobile/openapi/lib/model/exif_response_dto.dart index 6bb58a8ab9..64a5a73bed 100644 --- a/mobile/openapi/lib/model/exif_response_dto.dart +++ b/mobile/openapi/lib/model/exif_response_dto.dart @@ -50,9 +50,13 @@ class ExifResponseDto { String? description; /// Image height in pixels + /// + /// Minimum value: 0 num? exifImageHeight; /// Image width in pixels + /// + /// Minimum value: 0 num? exifImageWidth; /// Exposure time @@ -62,6 +66,9 @@ class ExifResponseDto { num? fNumber; /// File size in bytes + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int? fileSizeInByte; /// Focal length in mm diff --git a/mobile/openapi/lib/model/facial_recognition_config.dart b/mobile/openapi/lib/model/facial_recognition_config.dart index 4b9d7a6e9e..66cb542ccf 100644 --- a/mobile/openapi/lib/model/facial_recognition_config.dart +++ b/mobile/openapi/lib/model/facial_recognition_config.dart @@ -32,6 +32,7 @@ class FacialRecognitionConfig { /// Minimum number of faces required for recognition /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 int minFaces; /// Minimum confidence score for face detection diff --git a/mobile/openapi/lib/model/folders_response.dart b/mobile/openapi/lib/model/folders_response.dart index 906a95a83c..873404c786 100644 --- a/mobile/openapi/lib/model/folders_response.dart +++ b/mobile/openapi/lib/model/folders_response.dart @@ -13,8 +13,8 @@ part of openapi.api; class FoldersResponse { /// Returns a new [FoldersResponse] instance. FoldersResponse({ - this.enabled = false, - this.sidebarWeb = false, + required this.enabled, + required this.sidebarWeb, }); /// Whether folders are enabled diff --git a/mobile/openapi/lib/model/job_create_dto.dart b/mobile/openapi/lib/model/job_create_dto.dart index 3a3412384e..fe6743cba0 100644 --- a/mobile/openapi/lib/model/job_create_dto.dart +++ b/mobile/openapi/lib/model/job_create_dto.dart @@ -16,7 +16,6 @@ class JobCreateDto { required this.name, }); - /// Job name ManualJobName name; @override diff --git a/mobile/openapi/lib/model/job_name.dart b/mobile/openapi/lib/model/job_name.dart index 96b9339b7d..08f70569f8 100644 --- a/mobile/openapi/lib/model/job_name.dart +++ b/mobile/openapi/lib/model/job_name.dart @@ -38,7 +38,6 @@ class JobName { static const assetFileMigration = JobName._(r'AssetFileMigration'); static const assetGenerateThumbnailsQueueAll = JobName._(r'AssetGenerateThumbnailsQueueAll'); static const assetGenerateThumbnails = JobName._(r'AssetGenerateThumbnails'); - static const auditLogCleanup = JobName._(r'AuditLogCleanup'); static const auditTableCleanup = JobName._(r'AuditTableCleanup'); static const databaseBackup = JobName._(r'DatabaseBackup'); static const facialRecognitionQueueAll = JobName._(r'FacialRecognitionQueueAll'); @@ -97,7 +96,6 @@ class JobName { assetFileMigration, assetGenerateThumbnailsQueueAll, assetGenerateThumbnails, - auditLogCleanup, auditTableCleanup, databaseBackup, facialRecognitionQueueAll, @@ -191,7 +189,6 @@ class JobNameTypeTransformer { case r'AssetFileMigration': return JobName.assetFileMigration; case r'AssetGenerateThumbnailsQueueAll': return JobName.assetGenerateThumbnailsQueueAll; case r'AssetGenerateThumbnails': return JobName.assetGenerateThumbnails; - case r'AuditLogCleanup': return JobName.auditLogCleanup; case r'AuditTableCleanup': return JobName.auditTableCleanup; case r'DatabaseBackup': return JobName.databaseBackup; case r'FacialRecognitionQueueAll': return JobName.facialRecognitionQueueAll; diff --git a/mobile/openapi/lib/model/job_settings_dto.dart b/mobile/openapi/lib/model/job_settings_dto.dart index 73a0187ddd..98fe3d3536 100644 --- a/mobile/openapi/lib/model/job_settings_dto.dart +++ b/mobile/openapi/lib/model/job_settings_dto.dart @@ -19,6 +19,7 @@ class JobSettingsDto { /// Concurrency /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 int concurrency; @override diff --git a/mobile/openapi/lib/model/library_response_dto.dart b/mobile/openapi/lib/model/library_response_dto.dart index aa9158e591..88ebceae24 100644 --- a/mobile/openapi/lib/model/library_response_dto.dart +++ b/mobile/openapi/lib/model/library_response_dto.dart @@ -25,6 +25,9 @@ class LibraryResponseDto { }); /// Number of assets + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int assetCount; /// Creation date @@ -82,18 +85,24 @@ class LibraryResponseDto { Map toJson() { final json = {}; json[r'assetCount'] = this.assetCount; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _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.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'exclusionPatterns'] = this.exclusionPatterns; json[r'id'] = this.id; json[r'importPaths'] = this.importPaths; json[r'name'] = this.name; json[r'ownerId'] = this.ownerId; if (this.refreshedAt != null) { - json[r'refreshedAt'] = this.refreshedAt!.toUtc().toIso8601String(); + json[r'refreshedAt'] = _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.refreshedAt!.millisecondsSinceEpoch + : this.refreshedAt!.toUtc().toIso8601String(); } else { // json[r'refreshedAt'] = null; } - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _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.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } @@ -107,7 +116,7 @@ class LibraryResponseDto { return LibraryResponseDto( assetCount: mapValueOfType(json, r'assetCount')!, - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', 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))$/')!, exclusionPatterns: json[r'exclusionPatterns'] is Iterable ? (json[r'exclusionPatterns'] as Iterable).cast().toList(growable: false) : const [], @@ -117,8 +126,8 @@ class LibraryResponseDto { : const [], name: mapValueOfType(json, r'name')!, ownerId: mapValueOfType(json, r'ownerId')!, - refreshedAt: mapDateTime(json, r'refreshedAt', r''), - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + refreshedAt: mapDateTime(json, r'refreshedAt', 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))$/'), + updatedAt: mapDateTime(json, r'updatedAt', 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))$/')!, ); } return null; diff --git a/mobile/openapi/lib/model/library_stats_response_dto.dart b/mobile/openapi/lib/model/library_stats_response_dto.dart index 6eec3ae8d7..55adbc2b49 100644 --- a/mobile/openapi/lib/model/library_stats_response_dto.dart +++ b/mobile/openapi/lib/model/library_stats_response_dto.dart @@ -13,22 +13,34 @@ part of openapi.api; class LibraryStatsResponseDto { /// Returns a new [LibraryStatsResponseDto] instance. LibraryStatsResponseDto({ - this.photos = 0, - this.total = 0, - this.usage = 0, - this.videos = 0, + required this.photos, + required this.total, + required this.usage, + required this.videos, }); /// Number of photos + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int photos; /// Total number of assets + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int total; /// Storage usage in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int usage; /// Number of videos + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int videos; @override diff --git a/mobile/openapi/lib/model/license_key_dto.dart b/mobile/openapi/lib/model/license_key_dto.dart index ea1fee9d7a..d1818a2a43 100644 --- a/mobile/openapi/lib/model/license_key_dto.dart +++ b/mobile/openapi/lib/model/license_key_dto.dart @@ -20,7 +20,7 @@ class LicenseKeyDto { /// Activation key String activationKey; - /// License key (format: IM(SV|CL)(-XXXX){8}) + /// License key (format: /^IM(SV|CL)(-[\\dA-Za-z]{4}){8}$/) String licenseKey; @override diff --git a/mobile/openapi/lib/model/license_response_dto.dart b/mobile/openapi/lib/model/license_response_dto.dart deleted file mode 100644 index 84ff72c1eb..0000000000 --- a/mobile/openapi/lib/model/license_response_dto.dart +++ /dev/null @@ -1,118 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class LicenseResponseDto { - /// Returns a new [LicenseResponseDto] instance. - LicenseResponseDto({ - required this.activatedAt, - required this.activationKey, - required this.licenseKey, - }); - - /// Activation date - DateTime activatedAt; - - /// Activation key - String activationKey; - - /// License key (format: IM(SV|CL)(-XXXX){8}) - String licenseKey; - - @override - bool operator ==(Object other) => identical(this, other) || other is LicenseResponseDto && - other.activatedAt == activatedAt && - other.activationKey == activationKey && - other.licenseKey == licenseKey; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (activatedAt.hashCode) + - (activationKey.hashCode) + - (licenseKey.hashCode); - - @override - String toString() => 'LicenseResponseDto[activatedAt=$activatedAt, activationKey=$activationKey, licenseKey=$licenseKey]'; - - Map toJson() { - final json = {}; - json[r'activatedAt'] = this.activatedAt.toUtc().toIso8601String(); - json[r'activationKey'] = this.activationKey; - json[r'licenseKey'] = this.licenseKey; - return json; - } - - /// Returns a new [LicenseResponseDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static LicenseResponseDto? fromJson(dynamic value) { - upgradeDto(value, "LicenseResponseDto"); - if (value is Map) { - final json = value.cast(); - - return LicenseResponseDto( - activatedAt: mapDateTime(json, r'activatedAt', r'')!, - activationKey: mapValueOfType(json, r'activationKey')!, - licenseKey: mapValueOfType(json, r'licenseKey')!, - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = LicenseResponseDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = LicenseResponseDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of LicenseResponseDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = LicenseResponseDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'activatedAt', - 'activationKey', - 'licenseKey', - }; -} - diff --git a/mobile/openapi/lib/model/log_level.dart b/mobile/openapi/lib/model/log_level.dart index 2129096da2..edb6a1ddda 100644 --- a/mobile/openapi/lib/model/log_level.dart +++ b/mobile/openapi/lib/model/log_level.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Log level class LogLevel { /// Instantiate a new enum with the provided [value]. const LogLevel._(this.value); diff --git a/mobile/openapi/lib/model/maintenance_detect_install_storage_folder_dto.dart b/mobile/openapi/lib/model/maintenance_detect_install_storage_folder_dto.dart index ad524914b4..e3f8c0acbe 100644 --- a/mobile/openapi/lib/model/maintenance_detect_install_storage_folder_dto.dart +++ b/mobile/openapi/lib/model/maintenance_detect_install_storage_folder_dto.dart @@ -22,7 +22,6 @@ class MaintenanceDetectInstallStorageFolderDto { /// Number of files in the folder num files; - /// Storage folder StorageFolder folder; /// Whether the folder is readable diff --git a/mobile/openapi/lib/model/maintenance_status_response_dto.dart b/mobile/openapi/lib/model/maintenance_status_response_dto.dart index 52dbb5b95b..124fa674fd 100644 --- a/mobile/openapi/lib/model/maintenance_status_response_dto.dart +++ b/mobile/openapi/lib/model/maintenance_status_response_dto.dart @@ -20,7 +20,6 @@ class MaintenanceStatusResponseDto { this.task, }); - /// Maintenance action MaintenanceAction action; bool active; diff --git a/mobile/openapi/lib/model/manual_job_name.dart b/mobile/openapi/lib/model/manual_job_name.dart index d09790a81a..27753eb9dc 100644 --- a/mobile/openapi/lib/model/manual_job_name.dart +++ b/mobile/openapi/lib/model/manual_job_name.dart @@ -10,7 +10,7 @@ part of openapi.api; -/// Job name +/// Manual job name class ManualJobName { /// Instantiate a new enum with the provided [value]. const ManualJobName._(this.value); diff --git a/mobile/openapi/lib/model/memories_response.dart b/mobile/openapi/lib/model/memories_response.dart index 63d4094cd0..250e214a60 100644 --- a/mobile/openapi/lib/model/memories_response.dart +++ b/mobile/openapi/lib/model/memories_response.dart @@ -13,11 +13,14 @@ part of openapi.api; class MemoriesResponse { /// Returns a new [MemoriesResponse] instance. MemoriesResponse({ - this.duration = 5, - this.enabled = true, + required this.duration, + required this.enabled, }); /// Memory duration in seconds + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int duration; /// Whether memories are enabled diff --git a/mobile/openapi/lib/model/memories_update.dart b/mobile/openapi/lib/model/memories_update.dart index d27cef022d..ede9910d74 100644 --- a/mobile/openapi/lib/model/memories_update.dart +++ b/mobile/openapi/lib/model/memories_update.dart @@ -20,6 +20,7 @@ class MemoriesUpdate { /// Memory duration in seconds /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 /// /// 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 diff --git a/mobile/openapi/lib/model/memory_create_dto.dart b/mobile/openapi/lib/model/memory_create_dto.dart index 5b8eeed8fb..b906f6dd1d 100644 --- a/mobile/openapi/lib/model/memory_create_dto.dart +++ b/mobile/openapi/lib/model/memory_create_dto.dart @@ -67,7 +67,6 @@ class MemoryCreateDto { /// DateTime? showAt; - /// Memory type MemoryType type; @override @@ -101,7 +100,9 @@ class MemoryCreateDto { json[r'assetIds'] = this.assetIds; json[r'data'] = this.data; if (this.hideAt != null) { - json[r'hideAt'] = this.hideAt!.toUtc().toIso8601String(); + json[r'hideAt'] = _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.hideAt!.millisecondsSinceEpoch + : this.hideAt!.toUtc().toIso8601String(); } else { // json[r'hideAt'] = null; } @@ -110,14 +111,20 @@ class MemoryCreateDto { } else { // json[r'isSaved'] = null; } - json[r'memoryAt'] = this.memoryAt.toUtc().toIso8601String(); + json[r'memoryAt'] = _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.memoryAt.millisecondsSinceEpoch + : this.memoryAt.toUtc().toIso8601String(); if (this.seenAt != null) { - json[r'seenAt'] = this.seenAt!.toUtc().toIso8601String(); + json[r'seenAt'] = _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.seenAt!.millisecondsSinceEpoch + : this.seenAt!.toUtc().toIso8601String(); } else { // json[r'seenAt'] = null; } if (this.showAt != null) { - json[r'showAt'] = this.showAt!.toUtc().toIso8601String(); + json[r'showAt'] = _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.showAt!.millisecondsSinceEpoch + : this.showAt!.toUtc().toIso8601String(); } else { // json[r'showAt'] = null; } @@ -138,11 +145,11 @@ class MemoryCreateDto { ? (json[r'assetIds'] as Iterable).cast().toList(growable: false) : const [], data: OnThisDayDto.fromJson(json[r'data'])!, - hideAt: mapDateTime(json, r'hideAt', r''), + hideAt: mapDateTime(json, r'hideAt', 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))$/'), isSaved: mapValueOfType(json, r'isSaved'), - memoryAt: mapDateTime(json, r'memoryAt', r'')!, - seenAt: mapDateTime(json, r'seenAt', r''), - showAt: mapDateTime(json, r'showAt', r''), + memoryAt: mapDateTime(json, r'memoryAt', 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))$/')!, + seenAt: mapDateTime(json, r'seenAt', 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))$/'), + showAt: mapDateTime(json, r'showAt', 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))$/'), type: MemoryType.fromJson(json[r'type'])!, ); } diff --git a/mobile/openapi/lib/model/memory_response_dto.dart b/mobile/openapi/lib/model/memory_response_dto.dart index 1835095cf7..e736667d57 100644 --- a/mobile/openapi/lib/model/memory_response_dto.dart +++ b/mobile/openapi/lib/model/memory_response_dto.dart @@ -83,7 +83,6 @@ class MemoryResponseDto { /// DateTime? showAt; - /// Memory type MemoryType type; /// Last update date @@ -128,34 +127,48 @@ class MemoryResponseDto { Map toJson() { final json = {}; json[r'assets'] = this.assets; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _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.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'data'] = this.data; if (this.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + 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.hideAt != null) { - json[r'hideAt'] = this.hideAt!.toUtc().toIso8601String(); + json[r'hideAt'] = _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.hideAt!.millisecondsSinceEpoch + : this.hideAt!.toUtc().toIso8601String(); } else { // json[r'hideAt'] = null; } json[r'id'] = this.id; json[r'isSaved'] = this.isSaved; - json[r'memoryAt'] = this.memoryAt.toUtc().toIso8601String(); + json[r'memoryAt'] = _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.memoryAt.millisecondsSinceEpoch + : this.memoryAt.toUtc().toIso8601String(); json[r'ownerId'] = this.ownerId; if (this.seenAt != null) { - json[r'seenAt'] = this.seenAt!.toUtc().toIso8601String(); + json[r'seenAt'] = _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.seenAt!.millisecondsSinceEpoch + : this.seenAt!.toUtc().toIso8601String(); } else { // json[r'seenAt'] = null; } if (this.showAt != null) { - json[r'showAt'] = this.showAt!.toUtc().toIso8601String(); + json[r'showAt'] = _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.showAt!.millisecondsSinceEpoch + : this.showAt!.toUtc().toIso8601String(); } else { // json[r'showAt'] = null; } json[r'type'] = this.type; - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _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.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } @@ -169,18 +182,18 @@ class MemoryResponseDto { return MemoryResponseDto( assets: AssetResponseDto.listFromJson(json[r'assets']), - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', 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))$/')!, data: OnThisDayDto.fromJson(json[r'data'])!, - deletedAt: mapDateTime(json, r'deletedAt', r''), - hideAt: mapDateTime(json, r'hideAt', r''), + 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))$/'), + hideAt: mapDateTime(json, r'hideAt', 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))$/'), id: mapValueOfType(json, r'id')!, isSaved: mapValueOfType(json, r'isSaved')!, - memoryAt: mapDateTime(json, r'memoryAt', r'')!, + memoryAt: mapDateTime(json, r'memoryAt', 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))$/')!, ownerId: mapValueOfType(json, r'ownerId')!, - seenAt: mapDateTime(json, r'seenAt', r''), - showAt: mapDateTime(json, r'showAt', r''), + seenAt: mapDateTime(json, r'seenAt', 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))$/'), + showAt: mapDateTime(json, r'showAt', 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))$/'), type: MemoryType.fromJson(json[r'type'])!, - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + updatedAt: mapDateTime(json, r'updatedAt', 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))$/')!, ); } return null; diff --git a/mobile/openapi/lib/model/memory_search_order.dart b/mobile/openapi/lib/model/memory_search_order.dart index bdf5b59894..67d0b69f46 100644 --- a/mobile/openapi/lib/model/memory_search_order.dart +++ b/mobile/openapi/lib/model/memory_search_order.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Sort order class MemorySearchOrder { /// Instantiate a new enum with the provided [value]. const MemorySearchOrder._(this.value); diff --git a/mobile/openapi/lib/model/memory_statistics_response_dto.dart b/mobile/openapi/lib/model/memory_statistics_response_dto.dart index bde78de481..ae542870d9 100644 --- a/mobile/openapi/lib/model/memory_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/memory_statistics_response_dto.dart @@ -17,6 +17,9 @@ class MemoryStatisticsResponseDto { }); /// Total number of memories + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int total; @override diff --git a/mobile/openapi/lib/model/memory_type.dart b/mobile/openapi/lib/model/memory_type.dart index aee7bd1ba1..ecfc93edb0 100644 --- a/mobile/openapi/lib/model/memory_type.dart +++ b/mobile/openapi/lib/model/memory_type.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Memory type class MemoryType { /// Instantiate a new enum with the provided [value]. const MemoryType._(this.value); diff --git a/mobile/openapi/lib/model/memory_update_dto.dart b/mobile/openapi/lib/model/memory_update_dto.dart index 4905b161bf..d8d7e9643b 100644 --- a/mobile/openapi/lib/model/memory_update_dto.dart +++ b/mobile/openapi/lib/model/memory_update_dto.dart @@ -69,12 +69,16 @@ class MemoryUpdateDto { // json[r'isSaved'] = null; } if (this.memoryAt != null) { - json[r'memoryAt'] = this.memoryAt!.toUtc().toIso8601String(); + json[r'memoryAt'] = _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.memoryAt!.millisecondsSinceEpoch + : this.memoryAt!.toUtc().toIso8601String(); } else { // json[r'memoryAt'] = null; } if (this.seenAt != null) { - json[r'seenAt'] = this.seenAt!.toUtc().toIso8601String(); + json[r'seenAt'] = _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.seenAt!.millisecondsSinceEpoch + : this.seenAt!.toUtc().toIso8601String(); } else { // json[r'seenAt'] = null; } @@ -91,8 +95,8 @@ class MemoryUpdateDto { return MemoryUpdateDto( isSaved: mapValueOfType(json, r'isSaved'), - memoryAt: mapDateTime(json, r'memoryAt', r''), - seenAt: mapDateTime(json, r'seenAt', r''), + memoryAt: mapDateTime(json, r'memoryAt', 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))$/'), + seenAt: mapDateTime(json, r'seenAt', 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))$/'), ); } return null; diff --git a/mobile/openapi/lib/model/metadata_search_dto.dart b/mobile/openapi/lib/model/metadata_search_dto.dart index 4dbc90d407..d49ea7a4e5 100644 --- a/mobile/openapi/lib/model/metadata_search_dto.dart +++ b/mobile/openapi/lib/model/metadata_search_dto.dart @@ -20,8 +20,6 @@ class MetadataSearchDto { this.createdAfter, this.createdBefore, this.description, - this.deviceAssetId, - this.deviceId, this.encodedVideoPath, this.id, this.isEncoded, @@ -34,7 +32,7 @@ class MetadataSearchDto { this.make, this.model, this.ocr, - this.order = AssetOrder.desc, + this.order, this.originalFileName, this.originalPath, this.page, @@ -104,24 +102,6 @@ class MetadataSearchDto { /// String? description; - /// Filter by device asset ID - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - String? deviceAssetId; - - /// Device ID to filter by - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - String? deviceId; - /// Filter by encoded video file path /// /// Please note: This property should have been non-nullable! Since the specification file @@ -192,12 +172,6 @@ class MetadataSearchDto { String? libraryId; /// Filter by camera make - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? make; /// Filter by camera model @@ -212,8 +186,13 @@ class MetadataSearchDto { /// String? ocr; - /// Sort order - AssetOrder order; + /// + /// 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. + /// + AssetOrder? order; /// Filter by original file name /// @@ -325,7 +304,6 @@ class MetadataSearchDto { /// DateTime? trashedBefore; - /// Asset type filter /// /// 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 @@ -352,7 +330,6 @@ class MetadataSearchDto { /// DateTime? updatedBefore; - /// Filter by visibility /// /// 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 @@ -406,8 +383,6 @@ class MetadataSearchDto { other.createdAfter == createdAfter && other.createdBefore == createdBefore && other.description == description && - other.deviceAssetId == deviceAssetId && - other.deviceId == deviceId && other.encodedVideoPath == encodedVideoPath && other.id == id && other.isEncoded == isEncoded && @@ -454,8 +429,6 @@ class MetadataSearchDto { (createdAfter == null ? 0 : createdAfter!.hashCode) + (createdBefore == null ? 0 : createdBefore!.hashCode) + (description == null ? 0 : description!.hashCode) + - (deviceAssetId == null ? 0 : deviceAssetId!.hashCode) + - (deviceId == null ? 0 : deviceId!.hashCode) + (encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) + (id == null ? 0 : id!.hashCode) + (isEncoded == null ? 0 : isEncoded!.hashCode) + @@ -468,7 +441,7 @@ class MetadataSearchDto { (make == null ? 0 : make!.hashCode) + (model == null ? 0 : model!.hashCode) + (ocr == null ? 0 : ocr!.hashCode) + - (order.hashCode) + + (order == null ? 0 : order!.hashCode) + (originalFileName == null ? 0 : originalFileName!.hashCode) + (originalPath == null ? 0 : originalPath!.hashCode) + (page == null ? 0 : page!.hashCode) + @@ -493,7 +466,7 @@ class MetadataSearchDto { (withStacked == null ? 0 : withStacked!.hashCode); @override - String toString() => 'MetadataSearchDto[albumIds=$albumIds, checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, deviceAssetId=$deviceAssetId, deviceId=$deviceId, encodedVideoPath=$encodedVideoPath, id=$id, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, order=$order, originalFileName=$originalFileName, originalPath=$originalPath, page=$page, personIds=$personIds, previewPath=$previewPath, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, thumbnailPath=$thumbnailPath, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; + String toString() => 'MetadataSearchDto[albumIds=$albumIds, checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, encodedVideoPath=$encodedVideoPath, id=$id, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, order=$order, originalFileName=$originalFileName, originalPath=$originalPath, page=$page, personIds=$personIds, previewPath=$previewPath, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, thumbnailPath=$thumbnailPath, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; Map toJson() { final json = {}; @@ -514,12 +487,16 @@ class MetadataSearchDto { // json[r'country'] = null; } if (this.createdAfter != null) { - json[r'createdAfter'] = this.createdAfter!.toUtc().toIso8601String(); + json[r'createdAfter'] = _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.createdAfter!.millisecondsSinceEpoch + : this.createdAfter!.toUtc().toIso8601String(); } else { // json[r'createdAfter'] = null; } if (this.createdBefore != null) { - json[r'createdBefore'] = this.createdBefore!.toUtc().toIso8601String(); + json[r'createdBefore'] = _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.createdBefore!.millisecondsSinceEpoch + : this.createdBefore!.toUtc().toIso8601String(); } else { // json[r'createdBefore'] = null; } @@ -528,16 +505,6 @@ class MetadataSearchDto { } else { // json[r'description'] = null; } - if (this.deviceAssetId != null) { - json[r'deviceAssetId'] = this.deviceAssetId; - } else { - // json[r'deviceAssetId'] = null; - } - if (this.deviceId != null) { - json[r'deviceId'] = this.deviceId; - } else { - // json[r'deviceId'] = null; - } if (this.encodedVideoPath != null) { json[r'encodedVideoPath'] = this.encodedVideoPath; } else { @@ -598,7 +565,11 @@ class MetadataSearchDto { } else { // json[r'ocr'] = null; } + if (this.order != null) { json[r'order'] = this.order; + } else { + // json[r'order'] = null; + } if (this.originalFileName != null) { json[r'originalFileName'] = this.originalFileName; } else { @@ -641,12 +612,16 @@ class MetadataSearchDto { // json[r'tagIds'] = null; } if (this.takenAfter != null) { - json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String(); + json[r'takenAfter'] = _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.takenAfter!.millisecondsSinceEpoch + : this.takenAfter!.toUtc().toIso8601String(); } else { // json[r'takenAfter'] = null; } if (this.takenBefore != null) { - json[r'takenBefore'] = this.takenBefore!.toUtc().toIso8601String(); + json[r'takenBefore'] = _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.takenBefore!.millisecondsSinceEpoch + : this.takenBefore!.toUtc().toIso8601String(); } else { // json[r'takenBefore'] = null; } @@ -656,12 +631,16 @@ class MetadataSearchDto { // json[r'thumbnailPath'] = null; } if (this.trashedAfter != null) { - json[r'trashedAfter'] = this.trashedAfter!.toUtc().toIso8601String(); + json[r'trashedAfter'] = _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.trashedAfter!.millisecondsSinceEpoch + : this.trashedAfter!.toUtc().toIso8601String(); } else { // json[r'trashedAfter'] = null; } if (this.trashedBefore != null) { - json[r'trashedBefore'] = this.trashedBefore!.toUtc().toIso8601String(); + json[r'trashedBefore'] = _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.trashedBefore!.millisecondsSinceEpoch + : this.trashedBefore!.toUtc().toIso8601String(); } else { // json[r'trashedBefore'] = null; } @@ -671,12 +650,16 @@ class MetadataSearchDto { // json[r'type'] = null; } if (this.updatedAfter != null) { - json[r'updatedAfter'] = this.updatedAfter!.toUtc().toIso8601String(); + json[r'updatedAfter'] = _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.updatedAfter!.millisecondsSinceEpoch + : this.updatedAfter!.toUtc().toIso8601String(); } else { // json[r'updatedAfter'] = null; } if (this.updatedBefore != null) { - json[r'updatedBefore'] = this.updatedBefore!.toUtc().toIso8601String(); + json[r'updatedBefore'] = _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.updatedBefore!.millisecondsSinceEpoch + : this.updatedBefore!.toUtc().toIso8601String(); } else { // json[r'updatedBefore'] = null; } @@ -723,11 +706,9 @@ class MetadataSearchDto { checksum: mapValueOfType(json, r'checksum'), city: mapValueOfType(json, r'city'), country: mapValueOfType(json, r'country'), - createdAfter: mapDateTime(json, r'createdAfter', r''), - createdBefore: mapDateTime(json, r'createdBefore', r''), + createdAfter: mapDateTime(json, r'createdAfter', 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))$/'), + createdBefore: mapDateTime(json, r'createdBefore', 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))$/'), description: mapValueOfType(json, r'description'), - deviceAssetId: mapValueOfType(json, r'deviceAssetId'), - deviceId: mapValueOfType(json, r'deviceId'), encodedVideoPath: mapValueOfType(json, r'encodedVideoPath'), id: mapValueOfType(json, r'id'), isEncoded: mapValueOfType(json, r'isEncoded'), @@ -740,7 +721,7 @@ class MetadataSearchDto { make: mapValueOfType(json, r'make'), model: mapValueOfType(json, r'model'), ocr: mapValueOfType(json, r'ocr'), - order: AssetOrder.fromJson(json[r'order']) ?? AssetOrder.desc, + order: AssetOrder.fromJson(json[r'order']), originalFileName: mapValueOfType(json, r'originalFileName'), originalPath: mapValueOfType(json, r'originalPath'), page: num.parse('${json[r'page']}'), @@ -756,14 +737,14 @@ class MetadataSearchDto { tagIds: json[r'tagIds'] is Iterable ? (json[r'tagIds'] as Iterable).cast().toList(growable: false) : const [], - takenAfter: mapDateTime(json, r'takenAfter', r''), - takenBefore: mapDateTime(json, r'takenBefore', r''), + takenAfter: mapDateTime(json, r'takenAfter', 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))$/'), + takenBefore: mapDateTime(json, r'takenBefore', 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))$/'), thumbnailPath: mapValueOfType(json, r'thumbnailPath'), - trashedAfter: mapDateTime(json, r'trashedAfter', r''), - trashedBefore: mapDateTime(json, r'trashedBefore', r''), + trashedAfter: mapDateTime(json, r'trashedAfter', 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))$/'), + trashedBefore: mapDateTime(json, r'trashedBefore', 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))$/'), type: AssetTypeEnum.fromJson(json[r'type']), - updatedAfter: mapDateTime(json, r'updatedAfter', r''), - updatedBefore: mapDateTime(json, r'updatedBefore', r''), + updatedAfter: mapDateTime(json, r'updatedAfter', 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))$/'), + updatedBefore: mapDateTime(json, r'updatedBefore', 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))$/'), visibility: AssetVisibility.fromJson(json[r'visibility']), withDeleted: mapValueOfType(json, r'withDeleted'), withExif: mapValueOfType(json, r'withExif'), diff --git a/mobile/openapi/lib/model/mirror_parameters.dart b/mobile/openapi/lib/model/mirror_parameters.dart index e8b8db685b..78c3da786c 100644 --- a/mobile/openapi/lib/model/mirror_parameters.dart +++ b/mobile/openapi/lib/model/mirror_parameters.dart @@ -16,7 +16,6 @@ class MirrorParameters { required this.axis, }); - /// Axis to mirror along MirrorAxis axis; @override diff --git a/mobile/openapi/lib/model/notification_create_dto.dart b/mobile/openapi/lib/model/notification_create_dto.dart index 1288da8670..f9771246f9 100644 --- a/mobile/openapi/lib/model/notification_create_dto.dart +++ b/mobile/openapi/lib/model/notification_create_dto.dart @@ -13,7 +13,7 @@ part of openapi.api; class NotificationCreateDto { /// Returns a new [NotificationCreateDto] instance. NotificationCreateDto({ - this.data, + this.data = const {}, this.description, this.level, this.readAt, @@ -23,18 +23,11 @@ class NotificationCreateDto { }); /// Additional notification data - /// - /// 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. - /// - Object? data; + Map data; /// Notification description String? description; - /// Notification level /// /// 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 @@ -49,7 +42,6 @@ class NotificationCreateDto { /// Notification title String title; - /// Notification type /// /// 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 @@ -63,7 +55,7 @@ class NotificationCreateDto { @override bool operator ==(Object other) => identical(this, other) || other is NotificationCreateDto && - other.data == data && + _deepEquality.equals(other.data, data) && other.description == description && other.level == level && other.readAt == readAt && @@ -74,7 +66,7 @@ class NotificationCreateDto { @override int get hashCode => // ignore: unnecessary_parenthesis - (data == null ? 0 : data!.hashCode) + + (data.hashCode) + (description == null ? 0 : description!.hashCode) + (level == null ? 0 : level!.hashCode) + (readAt == null ? 0 : readAt!.hashCode) + @@ -87,11 +79,7 @@ class NotificationCreateDto { Map toJson() { final json = {}; - if (this.data != null) { json[r'data'] = this.data; - } else { - // json[r'data'] = null; - } if (this.description != null) { json[r'description'] = this.description; } else { @@ -103,7 +91,9 @@ class NotificationCreateDto { // json[r'level'] = null; } if (this.readAt != null) { - json[r'readAt'] = this.readAt!.toUtc().toIso8601String(); + json[r'readAt'] = _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.readAt!.millisecondsSinceEpoch + : this.readAt!.toUtc().toIso8601String(); } else { // json[r'readAt'] = null; } @@ -126,10 +116,10 @@ class NotificationCreateDto { final json = value.cast(); return NotificationCreateDto( - data: mapValueOfType(json, r'data'), + data: mapCastOfType(json, r'data') ?? const {}, description: mapValueOfType(json, r'description'), level: NotificationLevel.fromJson(json[r'level']), - readAt: mapDateTime(json, r'readAt', r''), + readAt: mapDateTime(json, r'readAt', 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))$/'), title: mapValueOfType(json, r'title')!, type: NotificationType.fromJson(json[r'type']), userId: mapValueOfType(json, r'userId')!, diff --git a/mobile/openapi/lib/model/notification_dto.dart b/mobile/openapi/lib/model/notification_dto.dart index 30d43de115..ad0e79cb27 100644 --- a/mobile/openapi/lib/model/notification_dto.dart +++ b/mobile/openapi/lib/model/notification_dto.dart @@ -14,7 +14,7 @@ class NotificationDto { /// Returns a new [NotificationDto] instance. NotificationDto({ required this.createdAt, - this.data, + this.data = const {}, this.description, required this.id, required this.level, @@ -27,13 +27,7 @@ class NotificationDto { DateTime createdAt; /// Additional notification data - /// - /// 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. - /// - Object? data; + Map data; /// Notification description /// @@ -47,7 +41,6 @@ class NotificationDto { /// Notification ID String id; - /// Notification level NotificationLevel level; /// Date when notification was read @@ -62,13 +55,12 @@ class NotificationDto { /// Notification title String title; - /// Notification type NotificationType type; @override bool operator ==(Object other) => identical(this, other) || other is NotificationDto && other.createdAt == createdAt && - other.data == data && + _deepEquality.equals(other.data, data) && other.description == description && other.id == id && other.level == level && @@ -80,7 +72,7 @@ class NotificationDto { int get hashCode => // ignore: unnecessary_parenthesis (createdAt.hashCode) + - (data == null ? 0 : data!.hashCode) + + (data.hashCode) + (description == null ? 0 : description!.hashCode) + (id.hashCode) + (level.hashCode) + @@ -93,12 +85,10 @@ class NotificationDto { Map toJson() { final json = {}; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); - if (this.data != null) { + json[r'createdAt'] = _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.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'data'] = this.data; - } else { - // json[r'data'] = null; - } if (this.description != null) { json[r'description'] = this.description; } else { @@ -107,7 +97,9 @@ class NotificationDto { json[r'id'] = this.id; json[r'level'] = this.level; if (this.readAt != null) { - json[r'readAt'] = this.readAt!.toUtc().toIso8601String(); + json[r'readAt'] = _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.readAt!.millisecondsSinceEpoch + : this.readAt!.toUtc().toIso8601String(); } else { // json[r'readAt'] = null; } @@ -125,12 +117,12 @@ class NotificationDto { final json = value.cast(); return NotificationDto( - createdAt: mapDateTime(json, r'createdAt', r'')!, - data: mapValueOfType(json, r'data'), + createdAt: mapDateTime(json, r'createdAt', 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))$/')!, + data: mapCastOfType(json, r'data') ?? const {}, description: mapValueOfType(json, r'description'), id: mapValueOfType(json, r'id')!, level: NotificationLevel.fromJson(json[r'level'])!, - readAt: mapDateTime(json, r'readAt', r''), + readAt: mapDateTime(json, r'readAt', 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))$/'), title: mapValueOfType(json, r'title')!, type: NotificationType.fromJson(json[r'type'])!, ); diff --git a/mobile/openapi/lib/model/notification_level.dart b/mobile/openapi/lib/model/notification_level.dart index 554863ae4f..4ca4e2bcc8 100644 --- a/mobile/openapi/lib/model/notification_level.dart +++ b/mobile/openapi/lib/model/notification_level.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Notification level class NotificationLevel { /// Instantiate a new enum with the provided [value]. const NotificationLevel._(this.value); diff --git a/mobile/openapi/lib/model/notification_type.dart b/mobile/openapi/lib/model/notification_type.dart index b5885aa441..dbc9c12f84 100644 --- a/mobile/openapi/lib/model/notification_type.dart +++ b/mobile/openapi/lib/model/notification_type.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Notification type class NotificationType { /// Instantiate a new enum with the provided [value]. const NotificationType._(this.value); diff --git a/mobile/openapi/lib/model/notification_update_all_dto.dart b/mobile/openapi/lib/model/notification_update_all_dto.dart index a157058324..5ac61ededc 100644 --- a/mobile/openapi/lib/model/notification_update_all_dto.dart +++ b/mobile/openapi/lib/model/notification_update_all_dto.dart @@ -41,7 +41,9 @@ class NotificationUpdateAllDto { final json = {}; json[r'ids'] = this.ids; if (this.readAt != null) { - json[r'readAt'] = this.readAt!.toUtc().toIso8601String(); + json[r'readAt'] = _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.readAt!.millisecondsSinceEpoch + : this.readAt!.toUtc().toIso8601String(); } else { // json[r'readAt'] = null; } @@ -60,7 +62,7 @@ class NotificationUpdateAllDto { ids: json[r'ids'] is Iterable ? (json[r'ids'] as Iterable).cast().toList(growable: false) : const [], - readAt: mapDateTime(json, r'readAt', r''), + readAt: mapDateTime(json, r'readAt', 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))$/'), ); } return null; diff --git a/mobile/openapi/lib/model/notification_update_dto.dart b/mobile/openapi/lib/model/notification_update_dto.dart index eddf9c7e12..c5d949d7b2 100644 --- a/mobile/openapi/lib/model/notification_update_dto.dart +++ b/mobile/openapi/lib/model/notification_update_dto.dart @@ -34,7 +34,9 @@ class NotificationUpdateDto { Map toJson() { final json = {}; if (this.readAt != null) { - json[r'readAt'] = this.readAt!.toUtc().toIso8601String(); + json[r'readAt'] = _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.readAt!.millisecondsSinceEpoch + : this.readAt!.toUtc().toIso8601String(); } else { // json[r'readAt'] = null; } @@ -50,7 +52,7 @@ class NotificationUpdateDto { final json = value.cast(); return NotificationUpdateDto( - readAt: mapDateTime(json, r'readAt', r''), + readAt: mapDateTime(json, r'readAt', 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))$/'), ); } return null; diff --git a/mobile/openapi/lib/model/o_auth_token_endpoint_auth_method.dart b/mobile/openapi/lib/model/o_auth_token_endpoint_auth_method.dart index 77466d61d9..b63f027af7 100644 --- a/mobile/openapi/lib/model/o_auth_token_endpoint_auth_method.dart +++ b/mobile/openapi/lib/model/o_auth_token_endpoint_auth_method.dart @@ -10,7 +10,7 @@ part of openapi.api; -/// Token endpoint auth method +/// OAuth token endpoint auth method class OAuthTokenEndpointAuthMethod { /// Instantiate a new enum with the provided [value]. const OAuthTokenEndpointAuthMethod._(this.value); diff --git a/mobile/openapi/lib/model/ocr_config.dart b/mobile/openapi/lib/model/ocr_config.dart index d97cd5ffca..2ce5646731 100644 --- a/mobile/openapi/lib/model/ocr_config.dart +++ b/mobile/openapi/lib/model/ocr_config.dart @@ -26,6 +26,7 @@ class OcrConfig { /// Maximum resolution for OCR processing /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 int maxResolution; /// Minimum confidence score for text detection diff --git a/mobile/openapi/lib/model/on_this_day_dto.dart b/mobile/openapi/lib/model/on_this_day_dto.dart index 93ec956f58..77ae96532f 100644 --- a/mobile/openapi/lib/model/on_this_day_dto.dart +++ b/mobile/openapi/lib/model/on_this_day_dto.dart @@ -18,8 +18,9 @@ class OnThisDayDto { /// Year for on this day memory /// - /// Minimum value: 1 - num year; + /// Minimum value: 1000 + /// Maximum value: 9999 + int year; @override bool operator ==(Object other) => identical(this, other) || other is OnThisDayDto && @@ -48,7 +49,7 @@ class OnThisDayDto { final json = value.cast(); return OnThisDayDto( - year: num.parse('${json[r'year']}'), + year: mapValueOfType(json, r'year')!, ); } return null; diff --git a/mobile/openapi/lib/model/partner_direction.dart b/mobile/openapi/lib/model/partner_direction.dart index c43c0df75d..c5e3b308ac 100644 --- a/mobile/openapi/lib/model/partner_direction.dart +++ b/mobile/openapi/lib/model/partner_direction.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Partner direction class PartnerDirection { /// Instantiate a new enum with the provided [value]. const PartnerDirection._(this.value); diff --git a/mobile/openapi/lib/model/partner_response_dto.dart b/mobile/openapi/lib/model/partner_response_dto.dart index 5789938d18..f4612cc98a 100644 --- a/mobile/openapi/lib/model/partner_response_dto.dart +++ b/mobile/openapi/lib/model/partner_response_dto.dart @@ -22,7 +22,6 @@ class PartnerResponseDto { required this.profileImagePath, }); - /// Avatar color UserAvatarColor avatarColor; /// User email diff --git a/mobile/openapi/lib/model/people_response.dart b/mobile/openapi/lib/model/people_response.dart index c09560e08c..9d5d8ec18a 100644 --- a/mobile/openapi/lib/model/people_response.dart +++ b/mobile/openapi/lib/model/people_response.dart @@ -13,8 +13,8 @@ part of openapi.api; class PeopleResponse { /// Returns a new [PeopleResponse] instance. PeopleResponse({ - this.enabled = true, - this.sidebarWeb = false, + required this.enabled, + required this.sidebarWeb, }); /// Whether people are enabled diff --git a/mobile/openapi/lib/model/people_response_dto.dart b/mobile/openapi/lib/model/people_response_dto.dart index f345657e73..87edc6b4a7 100644 --- a/mobile/openapi/lib/model/people_response_dto.dart +++ b/mobile/openapi/lib/model/people_response_dto.dart @@ -29,12 +29,17 @@ class PeopleResponseDto { bool? hasNextPage; /// Number of hidden people + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int hidden; - /// List of people List people; /// Total number of people + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int total; @override diff --git a/mobile/openapi/lib/model/permission.dart b/mobile/openapi/lib/model/permission.dart index 9092ede786..0ac9461027 100644 --- a/mobile/openapi/lib/model/permission.dart +++ b/mobile/openapi/lib/model/permission.dart @@ -41,7 +41,6 @@ class Permission { static const assetPeriodView = Permission._(r'asset.view'); static const assetPeriodDownload = Permission._(r'asset.download'); static const assetPeriodUpload = Permission._(r'asset.upload'); - static const assetPeriodReplace = Permission._(r'asset.replace'); static const assetPeriodCopy = Permission._(r'asset.copy'); static const assetPeriodDerive = Permission._(r'asset.derive'); static const assetPeriodEditPeriodGet = Permission._(r'asset.edit.get'); @@ -200,7 +199,6 @@ class Permission { assetPeriodView, assetPeriodDownload, assetPeriodUpload, - assetPeriodReplace, assetPeriodCopy, assetPeriodDerive, assetPeriodEditPeriodGet, @@ -394,7 +392,6 @@ class PermissionTypeTransformer { case r'asset.view': return Permission.assetPeriodView; case r'asset.download': return Permission.assetPeriodDownload; case r'asset.upload': return Permission.assetPeriodUpload; - case r'asset.replace': return Permission.assetPeriodReplace; case r'asset.copy': return Permission.assetPeriodCopy; case r'asset.derive': return Permission.assetPeriodDerive; case r'asset.edit.get': return Permission.assetPeriodEditPeriodGet; diff --git a/mobile/openapi/lib/model/person_statistics_response_dto.dart b/mobile/openapi/lib/model/person_statistics_response_dto.dart index d2b45c8ccb..aeac16cc8a 100644 --- a/mobile/openapi/lib/model/person_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/person_statistics_response_dto.dart @@ -17,6 +17,9 @@ class PersonStatisticsResponseDto { }); /// Number of assets + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int assets; @override diff --git a/mobile/openapi/lib/model/person_with_faces_response_dto.dart b/mobile/openapi/lib/model/person_with_faces_response_dto.dart index f31c04b69f..f710dff8b9 100644 --- a/mobile/openapi/lib/model/person_with_faces_response_dto.dart +++ b/mobile/openapi/lib/model/person_with_faces_response_dto.dart @@ -36,7 +36,6 @@ class PersonWithFacesResponseDto { /// String? color; - /// Face detections List faces; /// Person ID diff --git a/mobile/openapi/lib/model/plugin_action_response_dto.dart b/mobile/openapi/lib/model/plugin_action_response_dto.dart index 34fa314ba9..cff2dc92f7 100644 --- a/mobile/openapi/lib/model/plugin_action_response_dto.dart +++ b/mobile/openapi/lib/model/plugin_action_response_dto.dart @@ -35,7 +35,7 @@ class PluginActionResponseDto { String pluginId; /// Action schema - Object? schema; + PluginJsonSchema? schema; /// Supported contexts List supportedContexts; @@ -96,7 +96,7 @@ class PluginActionResponseDto { id: mapValueOfType(json, r'id')!, methodName: mapValueOfType(json, r'methodName')!, pluginId: mapValueOfType(json, r'pluginId')!, - schema: mapValueOfType(json, r'schema'), + schema: PluginJsonSchema.fromJson(json[r'schema']), supportedContexts: PluginContextType.listFromJson(json[r'supportedContexts']), title: mapValueOfType(json, r'title')!, ); diff --git a/mobile/openapi/lib/model/plugin_context_type.dart b/mobile/openapi/lib/model/plugin_context_type.dart index 6f4ac91fdb..beda0b0f1a 100644 --- a/mobile/openapi/lib/model/plugin_context_type.dart +++ b/mobile/openapi/lib/model/plugin_context_type.dart @@ -10,7 +10,7 @@ part of openapi.api; -/// Context type +/// Plugin context class PluginContextType { /// Instantiate a new enum with the provided [value]. const PluginContextType._(this.value); diff --git a/mobile/openapi/lib/model/plugin_filter_response_dto.dart b/mobile/openapi/lib/model/plugin_filter_response_dto.dart index ea6411a9c1..d1ab867ff9 100644 --- a/mobile/openapi/lib/model/plugin_filter_response_dto.dart +++ b/mobile/openapi/lib/model/plugin_filter_response_dto.dart @@ -35,7 +35,7 @@ class PluginFilterResponseDto { String pluginId; /// Filter schema - Object? schema; + PluginJsonSchema? schema; /// Supported contexts List supportedContexts; @@ -96,7 +96,7 @@ class PluginFilterResponseDto { id: mapValueOfType(json, r'id')!, methodName: mapValueOfType(json, r'methodName')!, pluginId: mapValueOfType(json, r'pluginId')!, - schema: mapValueOfType(json, r'schema'), + schema: PluginJsonSchema.fromJson(json[r'schema']), supportedContexts: PluginContextType.listFromJson(json[r'supportedContexts']), title: mapValueOfType(json, r'title')!, ); diff --git a/mobile/openapi/lib/model/plugin_json_schema.dart b/mobile/openapi/lib/model/plugin_json_schema.dart new file mode 100644 index 0000000000..f7a2d584d9 --- /dev/null +++ b/mobile/openapi/lib/model/plugin_json_schema.dart @@ -0,0 +1,158 @@ +// +// 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 PluginJsonSchema { + /// Returns a new [PluginJsonSchema] instance. + PluginJsonSchema({ + this.additionalProperties, + this.description, + this.properties = const {}, + this.required_ = const [], + this.type, + }); + + /// + /// 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? additionalProperties; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? description; + + Map properties; + + List required_; + + /// + /// 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. + /// + PluginJsonSchemaType? type; + + @override + bool operator ==(Object other) => identical(this, other) || other is PluginJsonSchema && + other.additionalProperties == additionalProperties && + other.description == description && + _deepEquality.equals(other.properties, properties) && + _deepEquality.equals(other.required_, required_) && + other.type == type; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (additionalProperties == null ? 0 : additionalProperties!.hashCode) + + (description == null ? 0 : description!.hashCode) + + (properties.hashCode) + + (required_.hashCode) + + (type == null ? 0 : type!.hashCode); + + @override + String toString() => 'PluginJsonSchema[additionalProperties=$additionalProperties, description=$description, properties=$properties, required_=$required_, type=$type]'; + + Map toJson() { + final json = {}; + if (this.additionalProperties != null) { + json[r'additionalProperties'] = this.additionalProperties; + } else { + // json[r'additionalProperties'] = null; + } + if (this.description != null) { + json[r'description'] = this.description; + } else { + // json[r'description'] = null; + } + json[r'properties'] = this.properties; + json[r'required'] = this.required_; + if (this.type != null) { + json[r'type'] = this.type; + } else { + // json[r'type'] = null; + } + return json; + } + + /// Returns a new [PluginJsonSchema] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static PluginJsonSchema? fromJson(dynamic value) { + upgradeDto(value, "PluginJsonSchema"); + if (value is Map) { + final json = value.cast(); + + return PluginJsonSchema( + additionalProperties: mapValueOfType(json, r'additionalProperties'), + description: mapValueOfType(json, r'description'), + properties: PluginJsonSchemaProperty.mapFromJson(json[r'properties']), + required_: json[r'required'] is Iterable + ? (json[r'required'] as Iterable).cast().toList(growable: false) + : const [], + type: PluginJsonSchemaType.fromJson(json[r'type']), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = PluginJsonSchema.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = PluginJsonSchema.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of PluginJsonSchema-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = PluginJsonSchema.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/plugin_json_schema_property.dart b/mobile/openapi/lib/model/plugin_json_schema_property.dart new file mode 100644 index 0000000000..65951da0a3 --- /dev/null +++ b/mobile/openapi/lib/model/plugin_json_schema_property.dart @@ -0,0 +1,195 @@ +// +// 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 PluginJsonSchemaProperty { + /// Returns a new [PluginJsonSchemaProperty] instance. + PluginJsonSchemaProperty({ + this.additionalProperties, + this.default_, + this.description, + this.enum_ = const [], + this.items, + this.properties = const {}, + this.required_ = const [], + this.type, + }); + + /// + /// 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. + /// + PluginJsonSchemaPropertyAdditionalProperties? additionalProperties; + + Object? default_; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? description; + + List enum_; + + /// + /// 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. + /// + PluginJsonSchemaProperty? items; + + Map properties; + + List required_; + + /// + /// 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. + /// + PluginJsonSchemaType? type; + + @override + bool operator ==(Object other) => identical(this, other) || other is PluginJsonSchemaProperty && + other.additionalProperties == additionalProperties && + other.default_ == default_ && + other.description == description && + _deepEquality.equals(other.enum_, enum_) && + other.items == items && + _deepEquality.equals(other.properties, properties) && + _deepEquality.equals(other.required_, required_) && + other.type == type; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (additionalProperties == null ? 0 : additionalProperties!.hashCode) + + (default_ == null ? 0 : default_!.hashCode) + + (description == null ? 0 : description!.hashCode) + + (enum_.hashCode) + + (items == null ? 0 : items!.hashCode) + + (properties.hashCode) + + (required_.hashCode) + + (type == null ? 0 : type!.hashCode); + + @override + String toString() => 'PluginJsonSchemaProperty[additionalProperties=$additionalProperties, default_=$default_, description=$description, enum_=$enum_, items=$items, properties=$properties, required_=$required_, type=$type]'; + + Map toJson() { + final json = {}; + if (this.additionalProperties != null) { + json[r'additionalProperties'] = this.additionalProperties; + } else { + // json[r'additionalProperties'] = null; + } + if (this.default_ != null) { + json[r'default'] = this.default_; + } else { + // json[r'default'] = null; + } + if (this.description != null) { + json[r'description'] = this.description; + } else { + // json[r'description'] = null; + } + json[r'enum'] = this.enum_; + if (this.items != null) { + json[r'items'] = this.items; + } else { + // json[r'items'] = null; + } + json[r'properties'] = this.properties; + json[r'required'] = this.required_; + if (this.type != null) { + json[r'type'] = this.type; + } else { + // json[r'type'] = null; + } + return json; + } + + /// Returns a new [PluginJsonSchemaProperty] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static PluginJsonSchemaProperty? fromJson(dynamic value) { + upgradeDto(value, "PluginJsonSchemaProperty"); + if (value is Map) { + final json = value.cast(); + + return PluginJsonSchemaProperty( + additionalProperties: PluginJsonSchemaPropertyAdditionalProperties.fromJson(json[r'additionalProperties']), + default_: mapValueOfType(json, r'default'), + description: mapValueOfType(json, r'description'), + enum_: json[r'enum'] is Iterable + ? (json[r'enum'] as Iterable).cast().toList(growable: false) + : const [], + items: PluginJsonSchemaProperty.fromJson(json[r'items']), + properties: PluginJsonSchemaProperty.mapFromJson(json[r'properties']), + required_: json[r'required'] is Iterable + ? (json[r'required'] as Iterable).cast().toList(growable: false) + : const [], + type: PluginJsonSchemaType.fromJson(json[r'type']), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = PluginJsonSchemaProperty.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = PluginJsonSchemaProperty.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of PluginJsonSchemaProperty-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = PluginJsonSchemaProperty.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/plugin_json_schema_property_additional_properties.dart b/mobile/openapi/lib/model/plugin_json_schema_property_additional_properties.dart new file mode 100644 index 0000000000..169c6be772 --- /dev/null +++ b/mobile/openapi/lib/model/plugin_json_schema_property_additional_properties.dart @@ -0,0 +1,195 @@ +// +// 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 PluginJsonSchemaPropertyAdditionalProperties { + /// Returns a new [PluginJsonSchemaPropertyAdditionalProperties] instance. + PluginJsonSchemaPropertyAdditionalProperties({ + this.additionalProperties, + this.default_, + this.description, + this.enum_ = const [], + this.items, + this.properties = const {}, + this.required_ = const [], + this.type, + }); + + /// + /// 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. + /// + PluginJsonSchemaPropertyAdditionalProperties? additionalProperties; + + Object? default_; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? description; + + List enum_; + + /// + /// 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. + /// + PluginJsonSchemaProperty? items; + + Map properties; + + List required_; + + /// + /// 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. + /// + PluginJsonSchemaType? type; + + @override + bool operator ==(Object other) => identical(this, other) || other is PluginJsonSchemaPropertyAdditionalProperties && + other.additionalProperties == additionalProperties && + other.default_ == default_ && + other.description == description && + _deepEquality.equals(other.enum_, enum_) && + other.items == items && + _deepEquality.equals(other.properties, properties) && + _deepEquality.equals(other.required_, required_) && + other.type == type; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (additionalProperties == null ? 0 : additionalProperties!.hashCode) + + (default_ == null ? 0 : default_!.hashCode) + + (description == null ? 0 : description!.hashCode) + + (enum_.hashCode) + + (items == null ? 0 : items!.hashCode) + + (properties.hashCode) + + (required_.hashCode) + + (type == null ? 0 : type!.hashCode); + + @override + String toString() => 'PluginJsonSchemaPropertyAdditionalProperties[additionalProperties=$additionalProperties, default_=$default_, description=$description, enum_=$enum_, items=$items, properties=$properties, required_=$required_, type=$type]'; + + Map toJson() { + final json = {}; + if (this.additionalProperties != null) { + json[r'additionalProperties'] = this.additionalProperties; + } else { + // json[r'additionalProperties'] = null; + } + if (this.default_ != null) { + json[r'default'] = this.default_; + } else { + // json[r'default'] = null; + } + if (this.description != null) { + json[r'description'] = this.description; + } else { + // json[r'description'] = null; + } + json[r'enum'] = this.enum_; + if (this.items != null) { + json[r'items'] = this.items; + } else { + // json[r'items'] = null; + } + json[r'properties'] = this.properties; + json[r'required'] = this.required_; + if (this.type != null) { + json[r'type'] = this.type; + } else { + // json[r'type'] = null; + } + return json; + } + + /// Returns a new [PluginJsonSchemaPropertyAdditionalProperties] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static PluginJsonSchemaPropertyAdditionalProperties? fromJson(dynamic value) { + upgradeDto(value, "PluginJsonSchemaPropertyAdditionalProperties"); + if (value is Map) { + final json = value.cast(); + + return PluginJsonSchemaPropertyAdditionalProperties( + additionalProperties: PluginJsonSchemaPropertyAdditionalProperties.fromJson(json[r'additionalProperties']), + default_: mapValueOfType(json, r'default'), + description: mapValueOfType(json, r'description'), + enum_: json[r'enum'] is Iterable + ? (json[r'enum'] as Iterable).cast().toList(growable: false) + : const [], + items: PluginJsonSchemaProperty.fromJson(json[r'items']), + properties: PluginJsonSchemaProperty.mapFromJson(json[r'properties']), + required_: json[r'required'] is Iterable + ? (json[r'required'] as Iterable).cast().toList(growable: false) + : const [], + type: PluginJsonSchemaType.fromJson(json[r'type']), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = PluginJsonSchemaPropertyAdditionalProperties.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = PluginJsonSchemaPropertyAdditionalProperties.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of PluginJsonSchemaPropertyAdditionalProperties-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = PluginJsonSchemaPropertyAdditionalProperties.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/plugin_json_schema_type.dart b/mobile/openapi/lib/model/plugin_json_schema_type.dart new file mode 100644 index 0000000000..cabac9b71b --- /dev/null +++ b/mobile/openapi/lib/model/plugin_json_schema_type.dart @@ -0,0 +1,100 @@ +// +// 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 PluginJsonSchemaType { + /// Instantiate a new enum with the provided [value]. + const PluginJsonSchemaType._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const string = PluginJsonSchemaType._(r'string'); + static const number = PluginJsonSchemaType._(r'number'); + static const integer = PluginJsonSchemaType._(r'integer'); + static const boolean = PluginJsonSchemaType._(r'boolean'); + static const object = PluginJsonSchemaType._(r'object'); + static const array = PluginJsonSchemaType._(r'array'); + static const null_ = PluginJsonSchemaType._(r'null'); + + /// List of all possible values in this [enum][PluginJsonSchemaType]. + static const values = [ + string, + number, + integer, + boolean, + object, + array, + null_, + ]; + + static PluginJsonSchemaType? fromJson(dynamic value) => PluginJsonSchemaTypeTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = PluginJsonSchemaType.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [PluginJsonSchemaType] to String, +/// and [decode] dynamic data back to [PluginJsonSchemaType]. +class PluginJsonSchemaTypeTypeTransformer { + factory PluginJsonSchemaTypeTypeTransformer() => _instance ??= const PluginJsonSchemaTypeTypeTransformer._(); + + const PluginJsonSchemaTypeTypeTransformer._(); + + String encode(PluginJsonSchemaType data) => data.value; + + /// Decodes a [dynamic value][data] to a PluginJsonSchemaType. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + PluginJsonSchemaType? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'string': return PluginJsonSchemaType.string; + case r'number': return PluginJsonSchemaType.number; + case r'integer': return PluginJsonSchemaType.integer; + case r'boolean': return PluginJsonSchemaType.boolean; + case r'object': return PluginJsonSchemaType.object; + case r'array': return PluginJsonSchemaType.array; + case r'null': return PluginJsonSchemaType.null_; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [PluginJsonSchemaTypeTypeTransformer] instance. + static PluginJsonSchemaTypeTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/plugin_trigger_response_dto.dart b/mobile/openapi/lib/model/plugin_trigger_response_dto.dart index 16a9604bcd..a6ee1c6b69 100644 --- a/mobile/openapi/lib/model/plugin_trigger_response_dto.dart +++ b/mobile/openapi/lib/model/plugin_trigger_response_dto.dart @@ -17,10 +17,8 @@ class PluginTriggerResponseDto { required this.type, }); - /// Context type PluginContextType contextType; - /// Trigger type PluginTriggerType type; @override diff --git a/mobile/openapi/lib/model/plugin_trigger_type.dart b/mobile/openapi/lib/model/plugin_trigger_type.dart index 9ae64acf6c..3ebcef7a95 100644 --- a/mobile/openapi/lib/model/plugin_trigger_type.dart +++ b/mobile/openapi/lib/model/plugin_trigger_type.dart @@ -10,7 +10,7 @@ part of openapi.api; -/// Trigger type +/// Plugin trigger type class PluginTriggerType { /// Instantiate a new enum with the provided [value]. const PluginTriggerType._(this.value); diff --git a/mobile/openapi/lib/model/queue_command_dto.dart b/mobile/openapi/lib/model/queue_command_dto.dart index 9e1eea15db..fb68d85583 100644 --- a/mobile/openapi/lib/model/queue_command_dto.dart +++ b/mobile/openapi/lib/model/queue_command_dto.dart @@ -17,7 +17,6 @@ class QueueCommandDto { this.force, }); - /// Queue command to execute QueueCommand command; /// Force the command execution (if applicable) diff --git a/mobile/openapi/lib/model/queue_job_response_dto.dart b/mobile/openapi/lib/model/queue_job_response_dto.dart index 2ce63784eb..06d433edad 100644 --- a/mobile/openapi/lib/model/queue_job_response_dto.dart +++ b/mobile/openapi/lib/model/queue_job_response_dto.dart @@ -13,14 +13,14 @@ part of openapi.api; class QueueJobResponseDto { /// Returns a new [QueueJobResponseDto] instance. QueueJobResponseDto({ - required this.data, + this.data = const {}, this.id, required this.name, required this.timestamp, }); /// Job data payload - Object data; + Map data; /// Job ID /// @@ -31,15 +31,17 @@ class QueueJobResponseDto { /// String? id; - /// Job name JobName name; /// Job creation timestamp + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int timestamp; @override bool operator ==(Object other) => identical(this, other) || other is QueueJobResponseDto && - other.data == data && + _deepEquality.equals(other.data, data) && other.id == id && other.name == name && other.timestamp == timestamp; @@ -77,7 +79,7 @@ class QueueJobResponseDto { final json = value.cast(); return QueueJobResponseDto( - data: mapValueOfType(json, r'data')!, + data: mapCastOfType(json, r'data')!, id: mapValueOfType(json, r'id'), name: JobName.fromJson(json[r'name'])!, timestamp: mapValueOfType(json, r'timestamp')!, diff --git a/mobile/openapi/lib/model/queue_job_status.dart b/mobile/openapi/lib/model/queue_job_status.dart index 03a1371cc5..cbd01b11ed 100644 --- a/mobile/openapi/lib/model/queue_job_status.dart +++ b/mobile/openapi/lib/model/queue_job_status.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Queue job status class QueueJobStatus { /// Instantiate a new enum with the provided [value]. const QueueJobStatus._(this.value); diff --git a/mobile/openapi/lib/model/queue_name.dart b/mobile/openapi/lib/model/queue_name.dart index d94304d0d3..eb19d8957f 100644 --- a/mobile/openapi/lib/model/queue_name.dart +++ b/mobile/openapi/lib/model/queue_name.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Queue name class QueueName { /// Instantiate a new enum with the provided [value]. const QueueName._(this.value); diff --git a/mobile/openapi/lib/model/queue_response_dto.dart b/mobile/openapi/lib/model/queue_response_dto.dart index ac9244514c..c88f9fc195 100644 --- a/mobile/openapi/lib/model/queue_response_dto.dart +++ b/mobile/openapi/lib/model/queue_response_dto.dart @@ -21,7 +21,6 @@ class QueueResponseDto { /// Whether the queue is paused bool isPaused; - /// Queue name QueueName name; QueueStatisticsDto statistics; diff --git a/mobile/openapi/lib/model/queue_statistics_dto.dart b/mobile/openapi/lib/model/queue_statistics_dto.dart index c9a37ee30a..86c75f8e7c 100644 --- a/mobile/openapi/lib/model/queue_statistics_dto.dart +++ b/mobile/openapi/lib/model/queue_statistics_dto.dart @@ -22,21 +22,39 @@ class QueueStatisticsDto { }); /// Number of active jobs + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int active; /// Number of completed jobs + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int completed; /// Number of delayed jobs + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int delayed; /// Number of failed jobs + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int failed; /// Number of paused jobs + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int paused; /// Number of waiting jobs + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int waiting; @override diff --git a/mobile/openapi/lib/model/random_search_dto.dart b/mobile/openapi/lib/model/random_search_dto.dart index d5803c9cc7..3f33d8f850 100644 --- a/mobile/openapi/lib/model/random_search_dto.dart +++ b/mobile/openapi/lib/model/random_search_dto.dart @@ -18,7 +18,6 @@ class RandomSearchDto { this.country, this.createdAfter, this.createdBefore, - this.deviceId, this.isEncoded, this.isFavorite, this.isMotion, @@ -75,15 +74,6 @@ class RandomSearchDto { /// DateTime? createdBefore; - /// Device ID to filter by - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - String? deviceId; - /// Filter by encoded status /// /// Please note: This property should have been non-nullable! Since the specification file @@ -136,12 +126,6 @@ class RandomSearchDto { String? libraryId; /// Filter by camera make - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? make; /// Filter by camera model @@ -219,7 +203,6 @@ class RandomSearchDto { /// DateTime? trashedBefore; - /// Asset type filter /// /// 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 @@ -246,7 +229,6 @@ class RandomSearchDto { /// DateTime? updatedBefore; - /// Filter by visibility /// /// 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 @@ -298,7 +280,6 @@ class RandomSearchDto { other.country == country && other.createdAfter == createdAfter && other.createdBefore == createdBefore && - other.deviceId == deviceId && other.isEncoded == isEncoded && other.isFavorite == isFavorite && other.isMotion == isMotion && @@ -335,7 +316,6 @@ class RandomSearchDto { (country == null ? 0 : country!.hashCode) + (createdAfter == null ? 0 : createdAfter!.hashCode) + (createdBefore == null ? 0 : createdBefore!.hashCode) + - (deviceId == null ? 0 : deviceId!.hashCode) + (isEncoded == null ? 0 : isEncoded!.hashCode) + (isFavorite == null ? 0 : isFavorite!.hashCode) + (isMotion == null ? 0 : isMotion!.hashCode) + @@ -365,7 +345,7 @@ class RandomSearchDto { (withStacked == null ? 0 : withStacked!.hashCode); @override - String toString() => 'RandomSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, 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, withPeople=$withPeople, withStacked=$withStacked]'; + String toString() => 'RandomSearchDto[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, 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, withPeople=$withPeople, withStacked=$withStacked]'; Map toJson() { final json = {}; @@ -381,20 +361,19 @@ class RandomSearchDto { // json[r'country'] = null; } if (this.createdAfter != null) { - json[r'createdAfter'] = this.createdAfter!.toUtc().toIso8601String(); + json[r'createdAfter'] = _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.createdAfter!.millisecondsSinceEpoch + : this.createdAfter!.toUtc().toIso8601String(); } else { // json[r'createdAfter'] = null; } if (this.createdBefore != null) { - json[r'createdBefore'] = this.createdBefore!.toUtc().toIso8601String(); + json[r'createdBefore'] = _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.createdBefore!.millisecondsSinceEpoch + : this.createdBefore!.toUtc().toIso8601String(); } else { // json[r'createdBefore'] = null; } - if (this.deviceId != null) { - json[r'deviceId'] = this.deviceId; - } else { - // json[r'deviceId'] = null; - } if (this.isEncoded != null) { json[r'isEncoded'] = this.isEncoded; } else { @@ -467,22 +446,30 @@ class RandomSearchDto { // json[r'tagIds'] = null; } if (this.takenAfter != null) { - json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String(); + json[r'takenAfter'] = _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.takenAfter!.millisecondsSinceEpoch + : this.takenAfter!.toUtc().toIso8601String(); } else { // json[r'takenAfter'] = null; } if (this.takenBefore != null) { - json[r'takenBefore'] = this.takenBefore!.toUtc().toIso8601String(); + json[r'takenBefore'] = _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.takenBefore!.millisecondsSinceEpoch + : this.takenBefore!.toUtc().toIso8601String(); } else { // json[r'takenBefore'] = null; } if (this.trashedAfter != null) { - json[r'trashedAfter'] = this.trashedAfter!.toUtc().toIso8601String(); + json[r'trashedAfter'] = _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.trashedAfter!.millisecondsSinceEpoch + : this.trashedAfter!.toUtc().toIso8601String(); } else { // json[r'trashedAfter'] = null; } if (this.trashedBefore != null) { - json[r'trashedBefore'] = this.trashedBefore!.toUtc().toIso8601String(); + json[r'trashedBefore'] = _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.trashedBefore!.millisecondsSinceEpoch + : this.trashedBefore!.toUtc().toIso8601String(); } else { // json[r'trashedBefore'] = null; } @@ -492,12 +479,16 @@ class RandomSearchDto { // json[r'type'] = null; } if (this.updatedAfter != null) { - json[r'updatedAfter'] = this.updatedAfter!.toUtc().toIso8601String(); + json[r'updatedAfter'] = _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.updatedAfter!.millisecondsSinceEpoch + : this.updatedAfter!.toUtc().toIso8601String(); } else { // json[r'updatedAfter'] = null; } if (this.updatedBefore != null) { - json[r'updatedBefore'] = this.updatedBefore!.toUtc().toIso8601String(); + json[r'updatedBefore'] = _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.updatedBefore!.millisecondsSinceEpoch + : this.updatedBefore!.toUtc().toIso8601String(); } else { // json[r'updatedBefore'] = null; } @@ -543,9 +534,8 @@ class RandomSearchDto { : const [], city: mapValueOfType(json, r'city'), country: mapValueOfType(json, r'country'), - createdAfter: mapDateTime(json, r'createdAfter', r''), - createdBefore: mapDateTime(json, r'createdBefore', r''), - deviceId: mapValueOfType(json, r'deviceId'), + createdAfter: mapDateTime(json, r'createdAfter', 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))$/'), + createdBefore: mapDateTime(json, r'createdBefore', 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))$/'), isEncoded: mapValueOfType(json, r'isEncoded'), isFavorite: mapValueOfType(json, r'isFavorite'), isMotion: mapValueOfType(json, r'isMotion'), @@ -567,13 +557,13 @@ class RandomSearchDto { tagIds: json[r'tagIds'] is Iterable ? (json[r'tagIds'] as Iterable).cast().toList(growable: false) : const [], - takenAfter: mapDateTime(json, r'takenAfter', r''), - takenBefore: mapDateTime(json, r'takenBefore', r''), - trashedAfter: mapDateTime(json, r'trashedAfter', r''), - trashedBefore: mapDateTime(json, r'trashedBefore', r''), + takenAfter: mapDateTime(json, r'takenAfter', 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))$/'), + takenBefore: mapDateTime(json, r'takenBefore', 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))$/'), + trashedAfter: mapDateTime(json, r'trashedAfter', 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))$/'), + trashedBefore: mapDateTime(json, r'trashedBefore', 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))$/'), type: AssetTypeEnum.fromJson(json[r'type']), - updatedAfter: mapDateTime(json, r'updatedAfter', r''), - updatedBefore: mapDateTime(json, r'updatedBefore', r''), + updatedAfter: mapDateTime(json, r'updatedAfter', 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))$/'), + updatedBefore: mapDateTime(json, r'updatedBefore', 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))$/'), visibility: AssetVisibility.fromJson(json[r'visibility']), withDeleted: mapValueOfType(json, r'withDeleted'), withExif: mapValueOfType(json, r'withExif'), diff --git a/mobile/openapi/lib/model/ratings_response.dart b/mobile/openapi/lib/model/ratings_response.dart index 4346fa5c58..7b067412bf 100644 --- a/mobile/openapi/lib/model/ratings_response.dart +++ b/mobile/openapi/lib/model/ratings_response.dart @@ -13,7 +13,7 @@ part of openapi.api; class RatingsResponse { /// Returns a new [RatingsResponse] instance. RatingsResponse({ - this.enabled = false, + required this.enabled, }); /// Whether ratings are enabled diff --git a/mobile/openapi/lib/model/reaction_level.dart b/mobile/openapi/lib/model/reaction_level.dart index 29568b9d11..6060f4c2b7 100644 --- a/mobile/openapi/lib/model/reaction_level.dart +++ b/mobile/openapi/lib/model/reaction_level.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Reaction level class ReactionLevel { /// Instantiate a new enum with the provided [value]. const ReactionLevel._(this.value); diff --git a/mobile/openapi/lib/model/reaction_type.dart b/mobile/openapi/lib/model/reaction_type.dart index 4c788138fb..c4daccad71 100644 --- a/mobile/openapi/lib/model/reaction_type.dart +++ b/mobile/openapi/lib/model/reaction_type.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Reaction type class ReactionType { /// Instantiate a new enum with the provided [value]. const ReactionType._(this.value); diff --git a/mobile/openapi/lib/model/search_album_response_dto.dart b/mobile/openapi/lib/model/search_album_response_dto.dart index 8841251e4a..c21113ee6d 100644 --- a/mobile/openapi/lib/model/search_album_response_dto.dart +++ b/mobile/openapi/lib/model/search_album_response_dto.dart @@ -20,6 +20,9 @@ class SearchAlbumResponseDto { }); /// Number of albums in this page + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int count; List facets; @@ -27,6 +30,9 @@ class SearchAlbumResponseDto { List items; /// Total number of matching albums + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int total; @override diff --git a/mobile/openapi/lib/model/search_asset_response_dto.dart b/mobile/openapi/lib/model/search_asset_response_dto.dart index acb81f28e2..f4ffade26b 100644 --- a/mobile/openapi/lib/model/search_asset_response_dto.dart +++ b/mobile/openapi/lib/model/search_asset_response_dto.dart @@ -21,6 +21,9 @@ class SearchAssetResponseDto { }); /// Number of assets in this page + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int count; List facets; @@ -31,6 +34,9 @@ class SearchAssetResponseDto { String? nextPage; /// Total number of matching assets + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int total; @override diff --git a/mobile/openapi/lib/model/search_facet_count_response_dto.dart b/mobile/openapi/lib/model/search_facet_count_response_dto.dart index 8318fbfb3b..62adfaa74a 100644 --- a/mobile/openapi/lib/model/search_facet_count_response_dto.dart +++ b/mobile/openapi/lib/model/search_facet_count_response_dto.dart @@ -18,6 +18,9 @@ class SearchFacetCountResponseDto { }); /// Number of assets with this facet value + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int count; /// Facet value diff --git a/mobile/openapi/lib/model/search_facet_response_dto.dart b/mobile/openapi/lib/model/search_facet_response_dto.dart index 43b5ac5c81..51124ef1cf 100644 --- a/mobile/openapi/lib/model/search_facet_response_dto.dart +++ b/mobile/openapi/lib/model/search_facet_response_dto.dart @@ -17,7 +17,6 @@ class SearchFacetResponseDto { required this.fieldName, }); - /// Facet counts List counts; /// Facet field name diff --git a/mobile/openapi/lib/model/search_statistics_response_dto.dart b/mobile/openapi/lib/model/search_statistics_response_dto.dart index 5aebe4d6a9..c4d893af05 100644 --- a/mobile/openapi/lib/model/search_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/search_statistics_response_dto.dart @@ -17,6 +17,9 @@ class SearchStatisticsResponseDto { }); /// Total number of matching assets + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int total; @override diff --git a/mobile/openapi/lib/model/search_suggestion_type.dart b/mobile/openapi/lib/model/search_suggestion_type.dart index b18fe687c4..6d44b881bd 100644 --- a/mobile/openapi/lib/model/search_suggestion_type.dart +++ b/mobile/openapi/lib/model/search_suggestion_type.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Suggestion type class SearchSuggestionType { /// Instantiate a new enum with the provided [value]. const SearchSuggestionType._(this.value); diff --git a/mobile/openapi/lib/model/server_config_dto.dart b/mobile/openapi/lib/model/server_config_dto.dart index fec096d51a..316edb609f 100644 --- a/mobile/openapi/lib/model/server_config_dto.dart +++ b/mobile/openapi/lib/model/server_config_dto.dart @@ -54,9 +54,15 @@ class ServerConfigDto { bool publicUsers; /// Number of days before trashed assets are permanently deleted + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int trashDays; /// Delay in days before deleted users are permanently removed + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int userDeleteDelay; @override diff --git a/mobile/openapi/lib/model/server_stats_response_dto.dart b/mobile/openapi/lib/model/server_stats_response_dto.dart index ef2fa458e2..605bd74f41 100644 --- a/mobile/openapi/lib/model/server_stats_response_dto.dart +++ b/mobile/openapi/lib/model/server_stats_response_dto.dart @@ -13,29 +13,45 @@ part of openapi.api; class ServerStatsResponseDto { /// Returns a new [ServerStatsResponseDto] instance. ServerStatsResponseDto({ - this.photos = 0, - this.usage = 0, + required this.photos, + required this.usage, this.usageByUser = const [], - this.usagePhotos = 0, - this.usageVideos = 0, - this.videos = 0, + required this.usagePhotos, + required this.usageVideos, + required this.videos, }); /// Total number of photos + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int photos; /// Total storage usage in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int usage; + /// Array of usage for each user List usageByUser; /// Storage usage for photos in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int usagePhotos; /// Storage usage for videos in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int usageVideos; /// Total number of videos + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int videos; @override diff --git a/mobile/openapi/lib/model/server_storage_response_dto.dart b/mobile/openapi/lib/model/server_storage_response_dto.dart index 476b048b4d..4a66d54e37 100644 --- a/mobile/openapi/lib/model/server_storage_response_dto.dart +++ b/mobile/openapi/lib/model/server_storage_response_dto.dart @@ -26,12 +26,18 @@ class ServerStorageResponseDto { String diskAvailable; /// Available disk space in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int diskAvailableRaw; /// Total disk size (human-readable format) String diskSize; /// Total disk size in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int diskSizeRaw; /// Disk usage percentage (0-100) @@ -41,6 +47,9 @@ class ServerStorageResponseDto { String diskUse; /// Used disk space in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int diskUseRaw; @override diff --git a/mobile/openapi/lib/model/server_version_history_response_dto.dart b/mobile/openapi/lib/model/server_version_history_response_dto.dart index c3b7049016..ae5e060cff 100644 --- a/mobile/openapi/lib/model/server_version_history_response_dto.dart +++ b/mobile/openapi/lib/model/server_version_history_response_dto.dart @@ -45,7 +45,9 @@ class ServerVersionHistoryResponseDto { Map toJson() { final json = {}; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _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.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'id'] = this.id; json[r'version'] = this.version; return json; @@ -60,7 +62,7 @@ class ServerVersionHistoryResponseDto { final json = value.cast(); return ServerVersionHistoryResponseDto( - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', 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))$/')!, id: mapValueOfType(json, r'id')!, version: mapValueOfType(json, r'version')!, ); diff --git a/mobile/openapi/lib/model/server_version_response_dto.dart b/mobile/openapi/lib/model/server_version_response_dto.dart index a13cd81ad7..60161a7458 100644 --- a/mobile/openapi/lib/model/server_version_response_dto.dart +++ b/mobile/openapi/lib/model/server_version_response_dto.dart @@ -19,12 +19,21 @@ class ServerVersionResponseDto { }); /// Major version number + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int major; /// Minor version number + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int minor; /// Patch version number + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int patch_; @override diff --git a/mobile/openapi/lib/model/set_maintenance_mode_dto.dart b/mobile/openapi/lib/model/set_maintenance_mode_dto.dart index 14bf584bb9..e7c9dc0d63 100644 --- a/mobile/openapi/lib/model/set_maintenance_mode_dto.dart +++ b/mobile/openapi/lib/model/set_maintenance_mode_dto.dart @@ -17,7 +17,6 @@ class SetMaintenanceModeDto { this.restoreBackupFilename, }); - /// Maintenance action MaintenanceAction action; /// Restore backup filename diff --git a/mobile/openapi/lib/model/shared_link_create_dto.dart b/mobile/openapi/lib/model/shared_link_create_dto.dart index 2675ad4beb..a32714d556 100644 --- a/mobile/openapi/lib/model/shared_link_create_dto.dart +++ b/mobile/openapi/lib/model/shared_link_create_dto.dart @@ -64,7 +64,6 @@ class SharedLinkCreateDto { /// Custom URL slug String? slug; - /// Shared link type SharedLinkType type; @override @@ -117,7 +116,9 @@ class SharedLinkCreateDto { // json[r'description'] = null; } if (this.expiresAt != null) { - json[r'expiresAt'] = this.expiresAt!.toUtc().toIso8601String(); + json[r'expiresAt'] = _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.expiresAt!.millisecondsSinceEpoch + : this.expiresAt!.toUtc().toIso8601String(); } else { // json[r'expiresAt'] = null; } @@ -152,7 +153,7 @@ class SharedLinkCreateDto { ? (json[r'assetIds'] as Iterable).cast().toList(growable: false) : const [], description: mapValueOfType(json, r'description'), - expiresAt: mapDateTime(json, r'expiresAt', r''), + expiresAt: mapDateTime(json, r'expiresAt', 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))$/'), password: mapValueOfType(json, r'password'), showMetadata: mapValueOfType(json, r'showMetadata') ?? true, slug: mapValueOfType(json, r'slug'), diff --git a/mobile/openapi/lib/model/shared_link_edit_dto.dart b/mobile/openapi/lib/model/shared_link_edit_dto.dart index b22232add6..11d6cdd52e 100644 --- a/mobile/openapi/lib/model/shared_link_edit_dto.dart +++ b/mobile/openapi/lib/model/shared_link_edit_dto.dart @@ -120,7 +120,9 @@ class SharedLinkEditDto { // json[r'description'] = null; } if (this.expiresAt != null) { - json[r'expiresAt'] = this.expiresAt!.toUtc().toIso8601String(); + json[r'expiresAt'] = _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.expiresAt!.millisecondsSinceEpoch + : this.expiresAt!.toUtc().toIso8601String(); } else { // json[r'expiresAt'] = null; } @@ -155,7 +157,7 @@ class SharedLinkEditDto { allowUpload: mapValueOfType(json, r'allowUpload'), changeExpiryTime: mapValueOfType(json, r'changeExpiryTime'), description: mapValueOfType(json, r'description'), - expiresAt: mapDateTime(json, r'expiresAt', r''), + expiresAt: mapDateTime(json, r'expiresAt', 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))$/'), password: mapValueOfType(json, r'password'), showMetadata: mapValueOfType(json, r'showMetadata'), slug: mapValueOfType(json, r'slug'), diff --git a/mobile/openapi/lib/model/shared_link_response_dto.dart b/mobile/openapi/lib/model/shared_link_response_dto.dart index d9aec48c39..bad0966ca2 100644 --- a/mobile/openapi/lib/model/shared_link_response_dto.dart +++ b/mobile/openapi/lib/model/shared_link_response_dto.dart @@ -25,7 +25,6 @@ class SharedLinkResponseDto { required this.password, required this.showMetadata, required this.slug, - this.token, required this.type, required this.userId, }); @@ -70,10 +69,6 @@ class SharedLinkResponseDto { /// Custom URL slug String? slug; - /// Access token - String? token; - - /// Shared link type SharedLinkType type; /// Owner user ID @@ -93,7 +88,6 @@ class SharedLinkResponseDto { other.password == password && other.showMetadata == showMetadata && other.slug == slug && - other.token == token && other.type == type && other.userId == userId; @@ -112,12 +106,11 @@ class SharedLinkResponseDto { (password == null ? 0 : password!.hashCode) + (showMetadata.hashCode) + (slug == null ? 0 : slug!.hashCode) + - (token == null ? 0 : token!.hashCode) + (type.hashCode) + (userId.hashCode); @override - String toString() => 'SharedLinkResponseDto[album=$album, allowDownload=$allowDownload, allowUpload=$allowUpload, assets=$assets, createdAt=$createdAt, description=$description, expiresAt=$expiresAt, id=$id, key=$key, password=$password, showMetadata=$showMetadata, slug=$slug, token=$token, type=$type, userId=$userId]'; + String toString() => 'SharedLinkResponseDto[album=$album, allowDownload=$allowDownload, allowUpload=$allowUpload, assets=$assets, createdAt=$createdAt, description=$description, expiresAt=$expiresAt, id=$id, key=$key, password=$password, showMetadata=$showMetadata, slug=$slug, type=$type, userId=$userId]'; Map toJson() { final json = {}; @@ -129,14 +122,18 @@ class SharedLinkResponseDto { json[r'allowDownload'] = this.allowDownload; json[r'allowUpload'] = this.allowUpload; json[r'assets'] = this.assets; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _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.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); if (this.description != null) { json[r'description'] = this.description; } else { // json[r'description'] = null; } if (this.expiresAt != null) { - json[r'expiresAt'] = this.expiresAt!.toUtc().toIso8601String(); + json[r'expiresAt'] = _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.expiresAt!.millisecondsSinceEpoch + : this.expiresAt!.toUtc().toIso8601String(); } else { // json[r'expiresAt'] = null; } @@ -152,11 +149,6 @@ class SharedLinkResponseDto { json[r'slug'] = this.slug; } else { // json[r'slug'] = null; - } - if (this.token != null) { - json[r'token'] = this.token; - } else { - // json[r'token'] = null; } json[r'type'] = this.type; json[r'userId'] = this.userId; @@ -176,15 +168,14 @@ class SharedLinkResponseDto { allowDownload: mapValueOfType(json, r'allowDownload')!, allowUpload: mapValueOfType(json, r'allowUpload')!, assets: AssetResponseDto.listFromJson(json[r'assets']), - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', 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))$/')!, description: mapValueOfType(json, r'description'), - expiresAt: mapDateTime(json, r'expiresAt', r''), + expiresAt: mapDateTime(json, r'expiresAt', 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))$/'), id: mapValueOfType(json, r'id')!, key: mapValueOfType(json, r'key')!, password: mapValueOfType(json, r'password'), showMetadata: mapValueOfType(json, r'showMetadata')!, slug: mapValueOfType(json, r'slug'), - token: mapValueOfType(json, r'token'), type: SharedLinkType.fromJson(json[r'type'])!, userId: mapValueOfType(json, r'userId')!, ); diff --git a/mobile/openapi/lib/model/shared_links_response.dart b/mobile/openapi/lib/model/shared_links_response.dart index 510e94e43f..2b32a57540 100644 --- a/mobile/openapi/lib/model/shared_links_response.dart +++ b/mobile/openapi/lib/model/shared_links_response.dart @@ -13,8 +13,8 @@ part of openapi.api; class SharedLinksResponse { /// Returns a new [SharedLinksResponse] instance. SharedLinksResponse({ - this.enabled = true, - this.sidebarWeb = false, + required this.enabled, + required this.sidebarWeb, }); /// Whether shared links are enabled diff --git a/mobile/openapi/lib/model/smart_search_dto.dart b/mobile/openapi/lib/model/smart_search_dto.dart index 5f8214467f..bf1465223e 100644 --- a/mobile/openapi/lib/model/smart_search_dto.dart +++ b/mobile/openapi/lib/model/smart_search_dto.dart @@ -18,7 +18,6 @@ class SmartSearchDto { this.country, this.createdAfter, this.createdBefore, - this.deviceId, this.isEncoded, this.isFavorite, this.isMotion, @@ -77,15 +76,6 @@ class SmartSearchDto { /// DateTime? createdBefore; - /// Device ID to filter by - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - String? deviceId; - /// Filter by encoded status /// /// Please note: This property should have been non-nullable! Since the specification file @@ -147,12 +137,6 @@ class SmartSearchDto { String? libraryId; /// Filter by camera make - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? make; /// Filter by camera model @@ -259,7 +243,6 @@ class SmartSearchDto { /// DateTime? trashedBefore; - /// Asset type filter /// /// 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 @@ -286,7 +269,6 @@ class SmartSearchDto { /// DateTime? updatedBefore; - /// Filter by visibility /// /// 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 @@ -320,7 +302,6 @@ class SmartSearchDto { other.country == country && other.createdAfter == createdAfter && other.createdBefore == createdBefore && - other.deviceId == deviceId && other.isEncoded == isEncoded && other.isFavorite == isFavorite && other.isMotion == isMotion && @@ -359,7 +340,6 @@ class SmartSearchDto { (country == null ? 0 : country!.hashCode) + (createdAfter == null ? 0 : createdAfter!.hashCode) + (createdBefore == null ? 0 : createdBefore!.hashCode) + - (deviceId == null ? 0 : deviceId!.hashCode) + (isEncoded == null ? 0 : isEncoded!.hashCode) + (isFavorite == null ? 0 : isFavorite!.hashCode) + (isMotion == null ? 0 : isMotion!.hashCode) + @@ -391,7 +371,7 @@ class SmartSearchDto { (withExif == null ? 0 : withExif!.hashCode); @override - String toString() => 'SmartSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, language=$language, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, page=$page, personIds=$personIds, query=$query, queryAssetId=$queryAssetId, 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]'; + String toString() => 'SmartSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, language=$language, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, page=$page, personIds=$personIds, query=$query, queryAssetId=$queryAssetId, 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]'; Map toJson() { final json = {}; @@ -407,20 +387,19 @@ class SmartSearchDto { // json[r'country'] = null; } if (this.createdAfter != null) { - json[r'createdAfter'] = this.createdAfter!.toUtc().toIso8601String(); + json[r'createdAfter'] = _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.createdAfter!.millisecondsSinceEpoch + : this.createdAfter!.toUtc().toIso8601String(); } else { // json[r'createdAfter'] = null; } if (this.createdBefore != null) { - json[r'createdBefore'] = this.createdBefore!.toUtc().toIso8601String(); + json[r'createdBefore'] = _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.createdBefore!.millisecondsSinceEpoch + : this.createdBefore!.toUtc().toIso8601String(); } else { // json[r'createdBefore'] = null; } - if (this.deviceId != null) { - json[r'deviceId'] = this.deviceId; - } else { - // json[r'deviceId'] = null; - } if (this.isEncoded != null) { json[r'isEncoded'] = this.isEncoded; } else { @@ -513,22 +492,30 @@ class SmartSearchDto { // json[r'tagIds'] = null; } if (this.takenAfter != null) { - json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String(); + json[r'takenAfter'] = _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.takenAfter!.millisecondsSinceEpoch + : this.takenAfter!.toUtc().toIso8601String(); } else { // json[r'takenAfter'] = null; } if (this.takenBefore != null) { - json[r'takenBefore'] = this.takenBefore!.toUtc().toIso8601String(); + json[r'takenBefore'] = _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.takenBefore!.millisecondsSinceEpoch + : this.takenBefore!.toUtc().toIso8601String(); } else { // json[r'takenBefore'] = null; } if (this.trashedAfter != null) { - json[r'trashedAfter'] = this.trashedAfter!.toUtc().toIso8601String(); + json[r'trashedAfter'] = _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.trashedAfter!.millisecondsSinceEpoch + : this.trashedAfter!.toUtc().toIso8601String(); } else { // json[r'trashedAfter'] = null; } if (this.trashedBefore != null) { - json[r'trashedBefore'] = this.trashedBefore!.toUtc().toIso8601String(); + json[r'trashedBefore'] = _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.trashedBefore!.millisecondsSinceEpoch + : this.trashedBefore!.toUtc().toIso8601String(); } else { // json[r'trashedBefore'] = null; } @@ -538,12 +525,16 @@ class SmartSearchDto { // json[r'type'] = null; } if (this.updatedAfter != null) { - json[r'updatedAfter'] = this.updatedAfter!.toUtc().toIso8601String(); + json[r'updatedAfter'] = _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.updatedAfter!.millisecondsSinceEpoch + : this.updatedAfter!.toUtc().toIso8601String(); } else { // json[r'updatedAfter'] = null; } if (this.updatedBefore != null) { - json[r'updatedBefore'] = this.updatedBefore!.toUtc().toIso8601String(); + json[r'updatedBefore'] = _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.updatedBefore!.millisecondsSinceEpoch + : this.updatedBefore!.toUtc().toIso8601String(); } else { // json[r'updatedBefore'] = null; } @@ -579,9 +570,8 @@ class SmartSearchDto { : const [], city: mapValueOfType(json, r'city'), country: mapValueOfType(json, r'country'), - createdAfter: mapDateTime(json, r'createdAfter', r''), - createdBefore: mapDateTime(json, r'createdBefore', r''), - deviceId: mapValueOfType(json, r'deviceId'), + createdAfter: mapDateTime(json, r'createdAfter', 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))$/'), + createdBefore: mapDateTime(json, r'createdBefore', 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))$/'), isEncoded: mapValueOfType(json, r'isEncoded'), isFavorite: mapValueOfType(json, r'isFavorite'), isMotion: mapValueOfType(json, r'isMotion'), @@ -607,13 +597,13 @@ class SmartSearchDto { tagIds: json[r'tagIds'] is Iterable ? (json[r'tagIds'] as Iterable).cast().toList(growable: false) : const [], - takenAfter: mapDateTime(json, r'takenAfter', r''), - takenBefore: mapDateTime(json, r'takenBefore', r''), - trashedAfter: mapDateTime(json, r'trashedAfter', r''), - trashedBefore: mapDateTime(json, r'trashedBefore', r''), + takenAfter: mapDateTime(json, r'takenAfter', 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))$/'), + takenBefore: mapDateTime(json, r'takenBefore', 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))$/'), + trashedAfter: mapDateTime(json, r'trashedAfter', 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))$/'), + trashedBefore: mapDateTime(json, r'trashedBefore', 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))$/'), type: AssetTypeEnum.fromJson(json[r'type']), - updatedAfter: mapDateTime(json, r'updatedAfter', r''), - updatedBefore: mapDateTime(json, r'updatedBefore', r''), + updatedAfter: mapDateTime(json, r'updatedAfter', 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))$/'), + updatedBefore: mapDateTime(json, r'updatedBefore', 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))$/'), visibility: AssetVisibility.fromJson(json[r'visibility']), withDeleted: mapValueOfType(json, r'withDeleted'), withExif: mapValueOfType(json, r'withExif'), diff --git a/mobile/openapi/lib/model/stack_response_dto.dart b/mobile/openapi/lib/model/stack_response_dto.dart index 638dfb5255..326f83a03d 100644 --- a/mobile/openapi/lib/model/stack_response_dto.dart +++ b/mobile/openapi/lib/model/stack_response_dto.dart @@ -18,7 +18,6 @@ class StackResponseDto { required this.primaryAssetId, }); - /// Stack assets List assets; /// Stack ID diff --git a/mobile/openapi/lib/model/statistics_search_dto.dart b/mobile/openapi/lib/model/statistics_search_dto.dart index d5bbf448a3..d0070e8e12 100644 --- a/mobile/openapi/lib/model/statistics_search_dto.dart +++ b/mobile/openapi/lib/model/statistics_search_dto.dart @@ -19,7 +19,6 @@ class StatisticsSearchDto { this.createdAfter, this.createdBefore, this.description, - this.deviceId, this.isEncoded, this.isFavorite, this.isMotion, @@ -80,15 +79,6 @@ class StatisticsSearchDto { /// String? description; - /// Device ID to filter by - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - String? deviceId; - /// Filter by encoded status /// /// Please note: This property should have been non-nullable! Since the specification file @@ -141,12 +131,6 @@ class StatisticsSearchDto { String? libraryId; /// Filter by camera make - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? make; /// Filter by camera model @@ -212,7 +196,6 @@ class StatisticsSearchDto { /// DateTime? trashedBefore; - /// Asset type filter /// /// 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 @@ -239,7 +222,6 @@ class StatisticsSearchDto { /// DateTime? updatedBefore; - /// Filter by visibility /// /// 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 @@ -256,7 +238,6 @@ class StatisticsSearchDto { other.createdAfter == createdAfter && other.createdBefore == createdBefore && other.description == description && - other.deviceId == deviceId && other.isEncoded == isEncoded && other.isFavorite == isFavorite && other.isMotion == isMotion && @@ -289,7 +270,6 @@ class StatisticsSearchDto { (createdAfter == null ? 0 : createdAfter!.hashCode) + (createdBefore == null ? 0 : createdBefore!.hashCode) + (description == null ? 0 : description!.hashCode) + - (deviceId == null ? 0 : deviceId!.hashCode) + (isEncoded == null ? 0 : isEncoded!.hashCode) + (isFavorite == null ? 0 : isFavorite!.hashCode) + (isMotion == null ? 0 : isMotion!.hashCode) + @@ -314,7 +294,7 @@ class StatisticsSearchDto { (visibility == null ? 0 : visibility!.hashCode); @override - String toString() => 'StatisticsSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, personIds=$personIds, rating=$rating, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility]'; + String toString() => 'StatisticsSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, personIds=$personIds, rating=$rating, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility]'; Map toJson() { final json = {}; @@ -330,12 +310,16 @@ class StatisticsSearchDto { // json[r'country'] = null; } if (this.createdAfter != null) { - json[r'createdAfter'] = this.createdAfter!.toUtc().toIso8601String(); + json[r'createdAfter'] = _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.createdAfter!.millisecondsSinceEpoch + : this.createdAfter!.toUtc().toIso8601String(); } else { // json[r'createdAfter'] = null; } if (this.createdBefore != null) { - json[r'createdBefore'] = this.createdBefore!.toUtc().toIso8601String(); + json[r'createdBefore'] = _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.createdBefore!.millisecondsSinceEpoch + : this.createdBefore!.toUtc().toIso8601String(); } else { // json[r'createdBefore'] = null; } @@ -344,11 +328,6 @@ class StatisticsSearchDto { } else { // json[r'description'] = null; } - if (this.deviceId != null) { - json[r'deviceId'] = this.deviceId; - } else { - // json[r'deviceId'] = null; - } if (this.isEncoded != null) { json[r'isEncoded'] = this.isEncoded; } else { @@ -416,22 +395,30 @@ class StatisticsSearchDto { // json[r'tagIds'] = null; } if (this.takenAfter != null) { - json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String(); + json[r'takenAfter'] = _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.takenAfter!.millisecondsSinceEpoch + : this.takenAfter!.toUtc().toIso8601String(); } else { // json[r'takenAfter'] = null; } if (this.takenBefore != null) { - json[r'takenBefore'] = this.takenBefore!.toUtc().toIso8601String(); + json[r'takenBefore'] = _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.takenBefore!.millisecondsSinceEpoch + : this.takenBefore!.toUtc().toIso8601String(); } else { // json[r'takenBefore'] = null; } if (this.trashedAfter != null) { - json[r'trashedAfter'] = this.trashedAfter!.toUtc().toIso8601String(); + json[r'trashedAfter'] = _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.trashedAfter!.millisecondsSinceEpoch + : this.trashedAfter!.toUtc().toIso8601String(); } else { // json[r'trashedAfter'] = null; } if (this.trashedBefore != null) { - json[r'trashedBefore'] = this.trashedBefore!.toUtc().toIso8601String(); + json[r'trashedBefore'] = _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.trashedBefore!.millisecondsSinceEpoch + : this.trashedBefore!.toUtc().toIso8601String(); } else { // json[r'trashedBefore'] = null; } @@ -441,12 +428,16 @@ class StatisticsSearchDto { // json[r'type'] = null; } if (this.updatedAfter != null) { - json[r'updatedAfter'] = this.updatedAfter!.toUtc().toIso8601String(); + json[r'updatedAfter'] = _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.updatedAfter!.millisecondsSinceEpoch + : this.updatedAfter!.toUtc().toIso8601String(); } else { // json[r'updatedAfter'] = null; } if (this.updatedBefore != null) { - json[r'updatedBefore'] = this.updatedBefore!.toUtc().toIso8601String(); + json[r'updatedBefore'] = _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.updatedBefore!.millisecondsSinceEpoch + : this.updatedBefore!.toUtc().toIso8601String(); } else { // json[r'updatedBefore'] = null; } @@ -472,10 +463,9 @@ class StatisticsSearchDto { : const [], city: mapValueOfType(json, r'city'), country: mapValueOfType(json, r'country'), - createdAfter: mapDateTime(json, r'createdAfter', r''), - createdBefore: mapDateTime(json, r'createdBefore', r''), + createdAfter: mapDateTime(json, r'createdAfter', 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))$/'), + createdBefore: mapDateTime(json, r'createdBefore', 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))$/'), description: mapValueOfType(json, r'description'), - deviceId: mapValueOfType(json, r'deviceId'), isEncoded: mapValueOfType(json, r'isEncoded'), isFavorite: mapValueOfType(json, r'isFavorite'), isMotion: mapValueOfType(json, r'isMotion'), @@ -496,13 +486,13 @@ class StatisticsSearchDto { tagIds: json[r'tagIds'] is Iterable ? (json[r'tagIds'] as Iterable).cast().toList(growable: false) : const [], - takenAfter: mapDateTime(json, r'takenAfter', r''), - takenBefore: mapDateTime(json, r'takenBefore', r''), - trashedAfter: mapDateTime(json, r'trashedAfter', r''), - trashedBefore: mapDateTime(json, r'trashedBefore', r''), + takenAfter: mapDateTime(json, r'takenAfter', 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))$/'), + takenBefore: mapDateTime(json, r'takenBefore', 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))$/'), + trashedAfter: mapDateTime(json, r'trashedAfter', 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))$/'), + trashedBefore: mapDateTime(json, r'trashedBefore', 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))$/'), type: AssetTypeEnum.fromJson(json[r'type']), - updatedAfter: mapDateTime(json, r'updatedAfter', r''), - updatedBefore: mapDateTime(json, r'updatedBefore', r''), + updatedAfter: mapDateTime(json, r'updatedAfter', 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))$/'), + updatedBefore: mapDateTime(json, r'updatedBefore', 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))$/'), visibility: AssetVisibility.fromJson(json[r'visibility']), ); } diff --git a/mobile/openapi/lib/model/sync_ack_dto.dart b/mobile/openapi/lib/model/sync_ack_dto.dart index 747f671557..fa7e20a832 100644 --- a/mobile/openapi/lib/model/sync_ack_dto.dart +++ b/mobile/openapi/lib/model/sync_ack_dto.dart @@ -20,7 +20,6 @@ class SyncAckDto { /// Acknowledgment ID String ack; - /// Sync entity type SyncEntityType type; @override diff --git a/mobile/openapi/lib/model/sync_album_user_v1.dart b/mobile/openapi/lib/model/sync_album_user_v1.dart index 3fc8972069..1efe7da029 100644 --- a/mobile/openapi/lib/model/sync_album_user_v1.dart +++ b/mobile/openapi/lib/model/sync_album_user_v1.dart @@ -21,7 +21,6 @@ class SyncAlbumUserV1 { /// Album ID String albumId; - /// Album user role AlbumUserRole role; /// User ID diff --git a/mobile/openapi/lib/model/sync_album_v1.dart b/mobile/openapi/lib/model/sync_album_v1.dart index 6c89d93724..17b2bda02b 100644 --- a/mobile/openapi/lib/model/sync_album_v1.dart +++ b/mobile/openapi/lib/model/sync_album_v1.dart @@ -80,7 +80,9 @@ class SyncAlbumV1 { Map toJson() { final json = {}; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _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.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'description'] = this.description; json[r'id'] = this.id; json[r'isActivityEnabled'] = this.isActivityEnabled; @@ -92,7 +94,9 @@ class SyncAlbumV1 { } else { // json[r'thumbnailAssetId'] = null; } - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _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.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } @@ -105,7 +109,7 @@ class SyncAlbumV1 { final json = value.cast(); return SyncAlbumV1( - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', 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))$/')!, description: mapValueOfType(json, r'description')!, id: mapValueOfType(json, r'id')!, isActivityEnabled: mapValueOfType(json, r'isActivityEnabled')!, @@ -113,7 +117,7 @@ class SyncAlbumV1 { order: AssetOrder.fromJson(json[r'order'])!, ownerId: mapValueOfType(json, r'ownerId')!, thumbnailAssetId: mapValueOfType(json, r'thumbnailAssetId'), - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + updatedAt: mapDateTime(json, r'updatedAt', 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))$/')!, ); } return null; diff --git a/mobile/openapi/lib/model/sync_asset_edit_delete_v1.dart b/mobile/openapi/lib/model/sync_asset_edit_delete_v1.dart index 68af280290..e0c98bfef3 100644 --- a/mobile/openapi/lib/model/sync_asset_edit_delete_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_edit_delete_v1.dart @@ -16,6 +16,7 @@ class SyncAssetEditDeleteV1 { required this.editId, }); + /// Edit ID String editId; @override diff --git a/mobile/openapi/lib/model/sync_asset_edit_v1.dart b/mobile/openapi/lib/model/sync_asset_edit_v1.dart index 3cc2673bfc..8acfad5f6a 100644 --- a/mobile/openapi/lib/model/sync_asset_edit_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_edit_v1.dart @@ -16,18 +16,25 @@ class SyncAssetEditV1 { required this.action, required this.assetId, required this.id, - required this.parameters, + this.parameters = const {}, required this.sequence, }); AssetEditAction action; + /// Asset ID String assetId; + /// Edit ID String id; - Object parameters; + /// Edit parameters + Map parameters; + /// Edit sequence + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int sequence; @override @@ -35,7 +42,7 @@ class SyncAssetEditV1 { other.action == action && other.assetId == assetId && other.id == id && - other.parameters == parameters && + _deepEquality.equals(other.parameters, parameters) && other.sequence == sequence; @override @@ -72,7 +79,7 @@ class SyncAssetEditV1 { action: AssetEditAction.fromJson(json[r'action'])!, assetId: mapValueOfType(json, r'assetId')!, id: mapValueOfType(json, r'id')!, - parameters: mapValueOfType(json, r'parameters')!, + parameters: mapCastOfType(json, r'parameters')!, sequence: mapValueOfType(json, r'sequence')!, ); } diff --git a/mobile/openapi/lib/model/sync_asset_exif_v1.dart b/mobile/openapi/lib/model/sync_asset_exif_v1.dart index ff9efdfea3..caaeed7fb3 100644 --- a/mobile/openapi/lib/model/sync_asset_exif_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_exif_v1.dart @@ -56,9 +56,15 @@ class SyncAssetExifV1 { String? description; /// Exif image height + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? exifImageHeight; /// Exif image width + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? exifImageWidth; /// Exposure time @@ -68,6 +74,9 @@ class SyncAssetExifV1 { double? fNumber; /// File size in byte + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? fileSizeInByte; /// Focal length @@ -77,6 +86,9 @@ class SyncAssetExifV1 { double? fps; /// ISO + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? iso; /// Latitude @@ -107,6 +119,9 @@ class SyncAssetExifV1 { String? projectionType; /// Rating + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? rating; /// State @@ -189,7 +204,9 @@ class SyncAssetExifV1 { // json[r'country'] = null; } if (this.dateTimeOriginal != null) { - json[r'dateTimeOriginal'] = this.dateTimeOriginal!.toUtc().toIso8601String(); + json[r'dateTimeOriginal'] = _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.dateTimeOriginal!.millisecondsSinceEpoch + : this.dateTimeOriginal!.toUtc().toIso8601String(); } else { // json[r'dateTimeOriginal'] = null; } @@ -264,7 +281,9 @@ class SyncAssetExifV1 { // json[r'model'] = null; } if (this.modifyDate != null) { - json[r'modifyDate'] = this.modifyDate!.toUtc().toIso8601String(); + json[r'modifyDate'] = _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.modifyDate!.millisecondsSinceEpoch + : this.modifyDate!.toUtc().toIso8601String(); } else { // json[r'modifyDate'] = null; } @@ -313,7 +332,7 @@ class SyncAssetExifV1 { assetId: mapValueOfType(json, r'assetId')!, city: mapValueOfType(json, r'city'), country: mapValueOfType(json, r'country'), - dateTimeOriginal: mapDateTime(json, r'dateTimeOriginal', r''), + dateTimeOriginal: mapDateTime(json, r'dateTimeOriginal', 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))$/'), description: mapValueOfType(json, r'description'), exifImageHeight: mapValueOfType(json, r'exifImageHeight'), exifImageWidth: mapValueOfType(json, r'exifImageWidth'), @@ -328,7 +347,7 @@ class SyncAssetExifV1 { longitude: (mapValueOfType(json, r'longitude'))?.toDouble(), make: mapValueOfType(json, r'make'), model: mapValueOfType(json, r'model'), - modifyDate: mapDateTime(json, r'modifyDate', r''), + modifyDate: mapDateTime(json, r'modifyDate', 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))$/'), orientation: mapValueOfType(json, r'orientation'), profileDescription: mapValueOfType(json, r'profileDescription'), projectionType: mapValueOfType(json, r'projectionType'), diff --git a/mobile/openapi/lib/model/sync_asset_face_v1.dart b/mobile/openapi/lib/model/sync_asset_face_v1.dart index 647a07d5eb..c3f74ff2cd 100644 --- a/mobile/openapi/lib/model/sync_asset_face_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_face_v1.dart @@ -28,19 +28,43 @@ class SyncAssetFaceV1 { /// Asset ID String assetId; + /// Bounding box X1 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX1; + /// Bounding box X2 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX2; + /// Bounding box Y1 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY1; + /// Bounding box Y2 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY2; /// Asset face ID String id; + /// Image height + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int imageHeight; + /// Image width + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int imageWidth; /// Person ID diff --git a/mobile/openapi/lib/model/sync_asset_face_v2.dart b/mobile/openapi/lib/model/sync_asset_face_v2.dart index 688d71229f..aeefc2ece9 100644 --- a/mobile/openapi/lib/model/sync_asset_face_v2.dart +++ b/mobile/openapi/lib/model/sync_asset_face_v2.dart @@ -30,12 +30,28 @@ class SyncAssetFaceV2 { /// Asset ID String assetId; + /// Bounding box X1 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX1; + /// Bounding box X2 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX2; + /// Bounding box Y1 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY1; + /// Bounding box Y2 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY2; /// Face deleted at @@ -44,8 +60,16 @@ class SyncAssetFaceV2 { /// Asset face ID String id; + /// Image height + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int imageHeight; + /// Image width + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int imageWidth; /// Is the face visible in the asset @@ -99,7 +123,9 @@ class SyncAssetFaceV2 { json[r'boundingBoxY1'] = this.boundingBoxY1; json[r'boundingBoxY2'] = this.boundingBoxY2; if (this.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + 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; } @@ -130,7 +156,7 @@ class SyncAssetFaceV2 { boundingBoxX2: mapValueOfType(json, r'boundingBoxX2')!, boundingBoxY1: mapValueOfType(json, r'boundingBoxY1')!, boundingBoxY2: mapValueOfType(json, r'boundingBoxY2')!, - deletedAt: mapDateTime(json, r'deletedAt', r''), + 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))$/'), id: mapValueOfType(json, r'id')!, imageHeight: mapValueOfType(json, r'imageHeight')!, imageWidth: mapValueOfType(json, r'imageWidth')!, diff --git a/mobile/openapi/lib/model/sync_asset_metadata_v1.dart b/mobile/openapi/lib/model/sync_asset_metadata_v1.dart index 4a66623939..08d7eae49b 100644 --- a/mobile/openapi/lib/model/sync_asset_metadata_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_metadata_v1.dart @@ -15,7 +15,7 @@ class SyncAssetMetadataV1 { SyncAssetMetadataV1({ required this.assetId, required this.key, - required this.value, + this.value = const {}, }); /// Asset ID @@ -25,13 +25,13 @@ class SyncAssetMetadataV1 { String key; /// Value - Object value; + Map value; @override bool operator ==(Object other) => identical(this, other) || other is SyncAssetMetadataV1 && other.assetId == assetId && other.key == key && - other.value == value; + _deepEquality.equals(other.value, value); @override int get hashCode => @@ -62,7 +62,7 @@ class SyncAssetMetadataV1 { return SyncAssetMetadataV1( assetId: mapValueOfType(json, r'assetId')!, key: mapValueOfType(json, r'key')!, - value: mapValueOfType(json, r'value')!, + value: mapCastOfType(json, r'value')!, ); } return null; diff --git a/mobile/openapi/lib/model/sync_asset_v1.dart b/mobile/openapi/lib/model/sync_asset_v1.dart index debde4488e..d08de6ab72 100644 --- a/mobile/openapi/lib/model/sync_asset_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_v1.dart @@ -50,6 +50,9 @@ class SyncAssetV1 { DateTime? fileModifiedAt; /// Asset height + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? height; /// Asset ID @@ -82,13 +85,14 @@ class SyncAssetV1 { /// Thumbhash String? thumbhash; - /// Asset type AssetTypeEnum type; - /// Asset visibility AssetVisibility visibility; /// Asset width + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? width; @override @@ -143,7 +147,9 @@ class SyncAssetV1 { final json = {}; json[r'checksum'] = this.checksum; if (this.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + 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; } @@ -153,12 +159,16 @@ class SyncAssetV1 { // json[r'duration'] = null; } if (this.fileCreatedAt != null) { - json[r'fileCreatedAt'] = this.fileCreatedAt!.toUtc().toIso8601String(); + 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'] = this.fileModifiedAt!.toUtc().toIso8601String(); + 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; } @@ -181,7 +191,9 @@ class SyncAssetV1 { // json[r'livePhotoVideoId'] = null; } if (this.localDateTime != null) { - json[r'localDateTime'] = this.localDateTime!.toUtc().toIso8601String(); + 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; } @@ -217,17 +229,17 @@ class SyncAssetV1 { return SyncAssetV1( checksum: mapValueOfType(json, r'checksum')!, - deletedAt: mapDateTime(json, r'deletedAt', r''), + 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(json, r'duration'), - fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r''), - fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r''), + 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(json, r'height'), id: mapValueOfType(json, r'id')!, isEdited: mapValueOfType(json, r'isEdited')!, isFavorite: mapValueOfType(json, r'isFavorite')!, libraryId: mapValueOfType(json, r'libraryId'), livePhotoVideoId: mapValueOfType(json, r'livePhotoVideoId'), - localDateTime: mapDateTime(json, r'localDateTime', r''), + 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(json, r'originalFileName')!, ownerId: mapValueOfType(json, r'ownerId')!, stackId: mapValueOfType(json, r'stackId'), diff --git a/mobile/openapi/lib/model/sync_auth_user_v1.dart b/mobile/openapi/lib/model/sync_auth_user_v1.dart index 0edd804c6a..c64d82bfbd 100644 --- a/mobile/openapi/lib/model/sync_auth_user_v1.dart +++ b/mobile/openapi/lib/model/sync_auth_user_v1.dart @@ -13,7 +13,7 @@ part of openapi.api; class SyncAuthUserV1 { /// Returns a new [SyncAuthUserV1] instance. SyncAuthUserV1({ - required this.avatarColor, + this.avatarColor, required this.deletedAt, required this.email, required this.hasProfileImage, @@ -28,7 +28,6 @@ class SyncAuthUserV1 { required this.storageLabel, }); - /// User avatar color UserAvatarColor? avatarColor; /// User deleted at @@ -58,8 +57,16 @@ class SyncAuthUserV1 { /// User profile changed at DateTime profileChangedAt; + /// Quota size in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? quotaSizeInBytes; + /// Quota usage in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int quotaUsageInBytes; /// User storage label @@ -109,7 +116,9 @@ class SyncAuthUserV1 { // json[r'avatarColor'] = null; } if (this.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + 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; } @@ -124,7 +133,9 @@ class SyncAuthUserV1 { } else { // json[r'pinCode'] = null; } - json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String(); + json[r'profileChangedAt'] = _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.profileChangedAt.millisecondsSinceEpoch + : this.profileChangedAt.toUtc().toIso8601String(); if (this.quotaSizeInBytes != null) { json[r'quotaSizeInBytes'] = this.quotaSizeInBytes; } else { @@ -149,7 +160,7 @@ class SyncAuthUserV1 { return SyncAuthUserV1( avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']), - deletedAt: mapDateTime(json, r'deletedAt', r''), + 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))$/'), email: mapValueOfType(json, r'email')!, hasProfileImage: mapValueOfType(json, r'hasProfileImage')!, id: mapValueOfType(json, r'id')!, @@ -157,7 +168,7 @@ class SyncAuthUserV1 { name: mapValueOfType(json, r'name')!, oauthId: mapValueOfType(json, r'oauthId')!, pinCode: mapValueOfType(json, r'pinCode'), - profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!, + profileChangedAt: mapDateTime(json, r'profileChangedAt', 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))$/')!, quotaSizeInBytes: mapValueOfType(json, r'quotaSizeInBytes'), quotaUsageInBytes: mapValueOfType(json, r'quotaUsageInBytes')!, storageLabel: mapValueOfType(json, r'storageLabel'), @@ -208,7 +219,6 @@ class SyncAuthUserV1 { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'avatarColor', 'deletedAt', 'email', 'hasProfileImage', diff --git a/mobile/openapi/lib/model/sync_memory_v1.dart b/mobile/openapi/lib/model/sync_memory_v1.dart index c506738d97..855340f4d7 100644 --- a/mobile/openapi/lib/model/sync_memory_v1.dart +++ b/mobile/openapi/lib/model/sync_memory_v1.dart @@ -14,7 +14,7 @@ class SyncMemoryV1 { /// Returns a new [SyncMemoryV1] instance. SyncMemoryV1({ required this.createdAt, - required this.data, + this.data = const {}, required this.deletedAt, required this.hideAt, required this.id, @@ -31,7 +31,7 @@ class SyncMemoryV1 { DateTime createdAt; /// Data - Object data; + Map data; /// Deleted at DateTime? deletedAt; @@ -57,7 +57,6 @@ class SyncMemoryV1 { /// Show at DateTime? showAt; - /// Memory type MemoryType type; /// Updated at @@ -66,7 +65,7 @@ class SyncMemoryV1 { @override bool operator ==(Object other) => identical(this, other) || other is SyncMemoryV1 && other.createdAt == createdAt && - other.data == data && + _deepEquality.equals(other.data, data) && other.deletedAt == deletedAt && other.hideAt == hideAt && other.id == id && @@ -99,34 +98,48 @@ class SyncMemoryV1 { Map toJson() { final json = {}; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _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.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'data'] = this.data; if (this.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + 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.hideAt != null) { - json[r'hideAt'] = this.hideAt!.toUtc().toIso8601String(); + json[r'hideAt'] = _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.hideAt!.millisecondsSinceEpoch + : this.hideAt!.toUtc().toIso8601String(); } else { // json[r'hideAt'] = null; } json[r'id'] = this.id; json[r'isSaved'] = this.isSaved; - json[r'memoryAt'] = this.memoryAt.toUtc().toIso8601String(); + json[r'memoryAt'] = _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.memoryAt.millisecondsSinceEpoch + : this.memoryAt.toUtc().toIso8601String(); json[r'ownerId'] = this.ownerId; if (this.seenAt != null) { - json[r'seenAt'] = this.seenAt!.toUtc().toIso8601String(); + json[r'seenAt'] = _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.seenAt!.millisecondsSinceEpoch + : this.seenAt!.toUtc().toIso8601String(); } else { // json[r'seenAt'] = null; } if (this.showAt != null) { - json[r'showAt'] = this.showAt!.toUtc().toIso8601String(); + json[r'showAt'] = _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.showAt!.millisecondsSinceEpoch + : this.showAt!.toUtc().toIso8601String(); } else { // json[r'showAt'] = null; } json[r'type'] = this.type; - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _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.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } @@ -139,18 +152,18 @@ class SyncMemoryV1 { final json = value.cast(); return SyncMemoryV1( - createdAt: mapDateTime(json, r'createdAt', r'')!, - data: mapValueOfType(json, r'data')!, - deletedAt: mapDateTime(json, r'deletedAt', r''), - hideAt: mapDateTime(json, r'hideAt', r''), + createdAt: mapDateTime(json, r'createdAt', 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))$/')!, + data: mapCastOfType(json, r'data')!, + 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))$/'), + hideAt: mapDateTime(json, r'hideAt', 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))$/'), id: mapValueOfType(json, r'id')!, isSaved: mapValueOfType(json, r'isSaved')!, - memoryAt: mapDateTime(json, r'memoryAt', r'')!, + memoryAt: mapDateTime(json, r'memoryAt', 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))$/')!, ownerId: mapValueOfType(json, r'ownerId')!, - seenAt: mapDateTime(json, r'seenAt', r''), - showAt: mapDateTime(json, r'showAt', r''), + seenAt: mapDateTime(json, r'seenAt', 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))$/'), + showAt: mapDateTime(json, r'showAt', 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))$/'), type: MemoryType.fromJson(json[r'type'])!, - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + updatedAt: mapDateTime(json, r'updatedAt', 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))$/')!, ); } return null; diff --git a/mobile/openapi/lib/model/sync_person_v1.dart b/mobile/openapi/lib/model/sync_person_v1.dart index fc2c36aa8c..1bd6f4a160 100644 --- a/mobile/openapi/lib/model/sync_person_v1.dart +++ b/mobile/openapi/lib/model/sync_person_v1.dart @@ -88,7 +88,9 @@ class SyncPersonV1 { Map toJson() { final json = {}; if (this.birthDate != null) { - json[r'birthDate'] = this.birthDate!.toUtc().toIso8601String(); + json[r'birthDate'] = _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.birthDate!.millisecondsSinceEpoch + : this.birthDate!.toUtc().toIso8601String(); } else { // json[r'birthDate'] = null; } @@ -97,7 +99,9 @@ class SyncPersonV1 { } else { // json[r'color'] = null; } - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _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.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); if (this.faceAssetId != null) { json[r'faceAssetId'] = this.faceAssetId; } else { @@ -108,7 +112,9 @@ class SyncPersonV1 { json[r'isHidden'] = this.isHidden; json[r'name'] = this.name; json[r'ownerId'] = this.ownerId; - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _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.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } @@ -121,16 +127,16 @@ class SyncPersonV1 { final json = value.cast(); return SyncPersonV1( - birthDate: mapDateTime(json, r'birthDate', r''), + birthDate: mapDateTime(json, r'birthDate', 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))$/'), color: mapValueOfType(json, r'color'), - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', 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))$/')!, faceAssetId: mapValueOfType(json, r'faceAssetId'), id: mapValueOfType(json, r'id')!, isFavorite: mapValueOfType(json, r'isFavorite')!, isHidden: mapValueOfType(json, r'isHidden')!, name: mapValueOfType(json, r'name')!, ownerId: mapValueOfType(json, r'ownerId')!, - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + updatedAt: mapDateTime(json, r'updatedAt', 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))$/')!, ); } return null; diff --git a/mobile/openapi/lib/model/sync_request_type.dart b/mobile/openapi/lib/model/sync_request_type.dart index 671081c0a5..f51cc8bde9 100644 --- a/mobile/openapi/lib/model/sync_request_type.dart +++ b/mobile/openapi/lib/model/sync_request_type.dart @@ -10,7 +10,7 @@ part of openapi.api; -/// Sync request types +/// Sync request type class SyncRequestType { /// Instantiate a new enum with the provided [value]. const SyncRequestType._(this.value); diff --git a/mobile/openapi/lib/model/sync_stack_v1.dart b/mobile/openapi/lib/model/sync_stack_v1.dart index e4487ccfaf..3e79a55134 100644 --- a/mobile/openapi/lib/model/sync_stack_v1.dart +++ b/mobile/openapi/lib/model/sync_stack_v1.dart @@ -57,11 +57,15 @@ class SyncStackV1 { Map toJson() { final json = {}; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _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.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'id'] = this.id; json[r'ownerId'] = this.ownerId; json[r'primaryAssetId'] = this.primaryAssetId; - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _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.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } @@ -74,11 +78,11 @@ class SyncStackV1 { final json = value.cast(); return SyncStackV1( - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', 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))$/')!, id: mapValueOfType(json, r'id')!, ownerId: mapValueOfType(json, r'ownerId')!, primaryAssetId: mapValueOfType(json, r'primaryAssetId')!, - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + updatedAt: mapDateTime(json, r'updatedAt', 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))$/')!, ); } return null; diff --git a/mobile/openapi/lib/model/sync_user_metadata_delete_v1.dart b/mobile/openapi/lib/model/sync_user_metadata_delete_v1.dart index 61340a8f82..67976108e1 100644 --- a/mobile/openapi/lib/model/sync_user_metadata_delete_v1.dart +++ b/mobile/openapi/lib/model/sync_user_metadata_delete_v1.dart @@ -17,7 +17,6 @@ class SyncUserMetadataDeleteV1 { required this.userId, }); - /// User metadata key UserMetadataKey key; /// User ID diff --git a/mobile/openapi/lib/model/sync_user_metadata_v1.dart b/mobile/openapi/lib/model/sync_user_metadata_v1.dart index 23803d0be4..ddde7c0513 100644 --- a/mobile/openapi/lib/model/sync_user_metadata_v1.dart +++ b/mobile/openapi/lib/model/sync_user_metadata_v1.dart @@ -15,23 +15,22 @@ class SyncUserMetadataV1 { SyncUserMetadataV1({ required this.key, required this.userId, - required this.value, + this.value = const {}, }); - /// User metadata key UserMetadataKey key; /// User ID String userId; /// User metadata value - Object value; + Map value; @override bool operator ==(Object other) => identical(this, other) || other is SyncUserMetadataV1 && other.key == key && other.userId == userId && - other.value == value; + _deepEquality.equals(other.value, value); @override int get hashCode => @@ -62,7 +61,7 @@ class SyncUserMetadataV1 { return SyncUserMetadataV1( key: UserMetadataKey.fromJson(json[r'key'])!, userId: mapValueOfType(json, r'userId')!, - value: mapValueOfType(json, r'value')!, + value: mapCastOfType(json, r'value')!, ); } return null; diff --git a/mobile/openapi/lib/model/sync_user_v1.dart b/mobile/openapi/lib/model/sync_user_v1.dart index 6d425130a3..0a81593547 100644 --- a/mobile/openapi/lib/model/sync_user_v1.dart +++ b/mobile/openapi/lib/model/sync_user_v1.dart @@ -13,7 +13,7 @@ part of openapi.api; class SyncUserV1 { /// Returns a new [SyncUserV1] instance. SyncUserV1({ - required this.avatarColor, + this.avatarColor, required this.deletedAt, required this.email, required this.hasProfileImage, @@ -22,7 +22,6 @@ class SyncUserV1 { required this.profileChangedAt, }); - /// User avatar color UserAvatarColor? avatarColor; /// User deleted at @@ -75,7 +74,9 @@ class SyncUserV1 { // json[r'avatarColor'] = null; } if (this.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + 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; } @@ -83,7 +84,9 @@ class SyncUserV1 { json[r'hasProfileImage'] = this.hasProfileImage; json[r'id'] = this.id; json[r'name'] = this.name; - json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String(); + json[r'profileChangedAt'] = _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.profileChangedAt.millisecondsSinceEpoch + : this.profileChangedAt.toUtc().toIso8601String(); return json; } @@ -97,12 +100,12 @@ class SyncUserV1 { return SyncUserV1( avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']), - deletedAt: mapDateTime(json, r'deletedAt', r''), + 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))$/'), email: mapValueOfType(json, r'email')!, hasProfileImage: mapValueOfType(json, r'hasProfileImage')!, id: mapValueOfType(json, r'id')!, name: mapValueOfType(json, r'name')!, - profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!, + profileChangedAt: mapDateTime(json, r'profileChangedAt', 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))$/')!, ); } return null; @@ -150,7 +153,6 @@ class SyncUserV1 { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'avatarColor', 'deletedAt', 'email', 'hasProfileImage', diff --git a/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart b/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart index 6c7acbd218..ecf2e5da4a 100644 --- a/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart +++ b/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart @@ -36,7 +36,6 @@ class SystemConfigFFmpegDto { required this.twoPass, }); - /// Transcode hardware acceleration TranscodeHWAccel accel; /// Accelerated decode @@ -57,7 +56,6 @@ class SystemConfigFFmpegDto { /// Maximum value: 16 int bframes; - /// CQ mode CQMode cqMode; /// CRF @@ -69,6 +67,7 @@ class SystemConfigFFmpegDto { /// GOP size /// /// Minimum value: 0 + /// Maximum value: 9007199254740991 int gopSize; /// Max bitrate @@ -86,13 +85,11 @@ class SystemConfigFFmpegDto { /// Maximum value: 6 int refs; - /// Target audio codec AudioCodec targetAudioCodec; /// Target resolution String targetResolution; - /// Target video codec VideoCodec targetVideoCodec; /// Temporal AQ @@ -101,12 +98,11 @@ class SystemConfigFFmpegDto { /// Threads /// /// Minimum value: 0 + /// Maximum value: 9007199254740991 int threads; - /// Tone mapping ToneMapping tonemap; - /// Transcode policy TranscodePolicy transcode; /// Two pass diff --git a/mobile/openapi/lib/model/system_config_generated_fullsize_image_dto.dart b/mobile/openapi/lib/model/system_config_generated_fullsize_image_dto.dart index b5640f82c8..d78f8fadd5 100644 --- a/mobile/openapi/lib/model/system_config_generated_fullsize_image_dto.dart +++ b/mobile/openapi/lib/model/system_config_generated_fullsize_image_dto.dart @@ -15,18 +15,23 @@ class SystemConfigGeneratedFullsizeImageDto { SystemConfigGeneratedFullsizeImageDto({ required this.enabled, required this.format, - this.progressive = false, + this.progressive, required this.quality, }); /// Enabled bool enabled; - /// Image format ImageFormat format; /// Progressive - bool progressive; + /// + /// 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? progressive; /// Quality /// @@ -46,7 +51,7 @@ class SystemConfigGeneratedFullsizeImageDto { // ignore: unnecessary_parenthesis (enabled.hashCode) + (format.hashCode) + - (progressive.hashCode) + + (progressive == null ? 0 : progressive!.hashCode) + (quality.hashCode); @override @@ -56,7 +61,11 @@ class SystemConfigGeneratedFullsizeImageDto { final json = {}; json[r'enabled'] = this.enabled; json[r'format'] = this.format; + if (this.progressive != null) { json[r'progressive'] = this.progressive; + } else { + // json[r'progressive'] = null; + } json[r'quality'] = this.quality; return json; } @@ -72,7 +81,7 @@ class SystemConfigGeneratedFullsizeImageDto { return SystemConfigGeneratedFullsizeImageDto( enabled: mapValueOfType(json, r'enabled')!, format: ImageFormat.fromJson(json[r'format'])!, - progressive: mapValueOfType(json, r'progressive') ?? false, + progressive: mapValueOfType(json, r'progressive'), quality: mapValueOfType(json, r'quality')!, ); } diff --git a/mobile/openapi/lib/model/system_config_generated_image_dto.dart b/mobile/openapi/lib/model/system_config_generated_image_dto.dart index 3e8fed2c68..2571c0cab0 100644 --- a/mobile/openapi/lib/model/system_config_generated_image_dto.dart +++ b/mobile/openapi/lib/model/system_config_generated_image_dto.dart @@ -14,15 +14,21 @@ class SystemConfigGeneratedImageDto { /// Returns a new [SystemConfigGeneratedImageDto] instance. SystemConfigGeneratedImageDto({ required this.format, - this.progressive = false, + this.progressive, required this.quality, required this.size, }); - /// Image format ImageFormat format; - bool progressive; + /// Progressive + /// + /// 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? progressive; /// Quality /// @@ -33,6 +39,7 @@ class SystemConfigGeneratedImageDto { /// Size /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 int size; @override @@ -46,7 +53,7 @@ class SystemConfigGeneratedImageDto { int get hashCode => // ignore: unnecessary_parenthesis (format.hashCode) + - (progressive.hashCode) + + (progressive == null ? 0 : progressive!.hashCode) + (quality.hashCode) + (size.hashCode); @@ -56,7 +63,11 @@ class SystemConfigGeneratedImageDto { Map toJson() { final json = {}; json[r'format'] = this.format; + if (this.progressive != null) { json[r'progressive'] = this.progressive; + } else { + // json[r'progressive'] = null; + } json[r'quality'] = this.quality; json[r'size'] = this.size; return json; @@ -72,7 +83,7 @@ class SystemConfigGeneratedImageDto { return SystemConfigGeneratedImageDto( format: ImageFormat.fromJson(json[r'format'])!, - progressive: mapValueOfType(json, r'progressive') ?? false, + progressive: mapValueOfType(json, r'progressive'), quality: mapValueOfType(json, r'quality')!, size: mapValueOfType(json, r'size')!, ); diff --git a/mobile/openapi/lib/model/system_config_image_dto.dart b/mobile/openapi/lib/model/system_config_image_dto.dart index 217a666a67..668b740872 100644 --- a/mobile/openapi/lib/model/system_config_image_dto.dart +++ b/mobile/openapi/lib/model/system_config_image_dto.dart @@ -20,7 +20,6 @@ class SystemConfigImageDto { required this.thumbnail, }); - /// Colorspace Colorspace colorspace; /// Extract embedded diff --git a/mobile/openapi/lib/model/system_config_library_scan_dto.dart b/mobile/openapi/lib/model/system_config_library_scan_dto.dart index 28ea603c2a..003000d2ec 100644 --- a/mobile/openapi/lib/model/system_config_library_scan_dto.dart +++ b/mobile/openapi/lib/model/system_config_library_scan_dto.dart @@ -17,6 +17,7 @@ class SystemConfigLibraryScanDto { required this.enabled, }); + /// Cron expression String cronExpression; /// Enabled diff --git a/mobile/openapi/lib/model/system_config_machine_learning_dto.dart b/mobile/openapi/lib/model/system_config_machine_learning_dto.dart index 2a0f1ffbc6..6162e72b8f 100644 --- a/mobile/openapi/lib/model/system_config_machine_learning_dto.dart +++ b/mobile/openapi/lib/model/system_config_machine_learning_dto.dart @@ -35,6 +35,7 @@ class SystemConfigMachineLearningDto { OcrConfig ocr; + /// ML service URLs List urls; @override diff --git a/mobile/openapi/lib/model/system_config_map_dto.dart b/mobile/openapi/lib/model/system_config_map_dto.dart index 109babd374..7a2fbb516b 100644 --- a/mobile/openapi/lib/model/system_config_map_dto.dart +++ b/mobile/openapi/lib/model/system_config_map_dto.dart @@ -18,11 +18,13 @@ class SystemConfigMapDto { required this.lightStyle, }); + /// Dark map style URL String darkStyle; /// Enabled bool enabled; + /// Light map style URL String lightStyle; @override diff --git a/mobile/openapi/lib/model/system_config_nightly_tasks_dto.dart b/mobile/openapi/lib/model/system_config_nightly_tasks_dto.dart index cfb18b181e..0db417427f 100644 --- a/mobile/openapi/lib/model/system_config_nightly_tasks_dto.dart +++ b/mobile/openapi/lib/model/system_config_nightly_tasks_dto.dart @@ -33,6 +33,7 @@ class SystemConfigNightlyTasksDto { /// Missing thumbnails bool missingThumbnails; + /// Start time String startTime; /// Sync quota usage diff --git a/mobile/openapi/lib/model/system_config_o_auth_dto.dart b/mobile/openapi/lib/model/system_config_o_auth_dto.dart index 82195e498b..88dddbb4d3 100644 --- a/mobile/openapi/lib/model/system_config_o_auth_dto.dart +++ b/mobile/openapi/lib/model/system_config_o_auth_dto.dart @@ -51,7 +51,7 @@ class SystemConfigOAuthDto { /// Default storage quota /// /// Minimum value: 0 - int? defaultStorageQuota; + num? defaultStorageQuota; /// Enabled bool enabled; @@ -62,7 +62,7 @@ class SystemConfigOAuthDto { /// Mobile override enabled bool mobileOverrideEnabled; - /// Mobile redirect URI + /// Mobile redirect URI (set to empty string to disable) String mobileRedirectUri; /// Profile signing algorithm @@ -74,6 +74,7 @@ class SystemConfigOAuthDto { /// Scope String scope; + /// Signing algorithm String signingAlgorithm; /// Storage label claim @@ -85,9 +86,9 @@ class SystemConfigOAuthDto { /// Timeout /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 int timeout; - /// Token endpoint auth method OAuthTokenEndpointAuthMethod tokenEndpointAuthMethod; @override @@ -177,7 +178,9 @@ class SystemConfigOAuthDto { buttonText: mapValueOfType(json, r'buttonText')!, clientId: mapValueOfType(json, r'clientId')!, clientSecret: mapValueOfType(json, r'clientSecret')!, - defaultStorageQuota: mapValueOfType(json, r'defaultStorageQuota'), + defaultStorageQuota: json[r'defaultStorageQuota'] == null + ? null + : num.parse('${json[r'defaultStorageQuota']}'), enabled: mapValueOfType(json, r'enabled')!, issuerUrl: mapValueOfType(json, r'issuerUrl')!, mobileOverrideEnabled: mapValueOfType(json, r'mobileOverrideEnabled')!, diff --git a/mobile/openapi/lib/model/system_config_template_emails_dto.dart b/mobile/openapi/lib/model/system_config_template_emails_dto.dart index 9db85509f5..d29ca1fac3 100644 --- a/mobile/openapi/lib/model/system_config_template_emails_dto.dart +++ b/mobile/openapi/lib/model/system_config_template_emails_dto.dart @@ -18,10 +18,13 @@ class SystemConfigTemplateEmailsDto { required this.welcomeTemplate, }); + /// Album invite template String albumInviteTemplate; + /// Album update template String albumUpdateTemplate; + /// Welcome template String welcomeTemplate; @override diff --git a/mobile/openapi/lib/model/system_config_trash_dto.dart b/mobile/openapi/lib/model/system_config_trash_dto.dart index 9bdaef92d3..790710751f 100644 --- a/mobile/openapi/lib/model/system_config_trash_dto.dart +++ b/mobile/openapi/lib/model/system_config_trash_dto.dart @@ -20,6 +20,7 @@ class SystemConfigTrashDto { /// Days /// /// Minimum value: 0 + /// Maximum value: 9007199254740991 int days; /// Enabled diff --git a/mobile/openapi/lib/model/system_config_user_dto.dart b/mobile/openapi/lib/model/system_config_user_dto.dart index a7313560e6..dc553e7369 100644 --- a/mobile/openapi/lib/model/system_config_user_dto.dart +++ b/mobile/openapi/lib/model/system_config_user_dto.dart @@ -19,6 +19,7 @@ class SystemConfigUserDto { /// Delete delay /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 int deleteDelay; @override diff --git a/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart b/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart index 5566846e3c..4d689f01a1 100644 --- a/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart +++ b/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart @@ -17,6 +17,9 @@ class TagBulkAssetsResponseDto { }); /// Number of assets tagged + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int count; @override diff --git a/mobile/openapi/lib/model/tag_create_dto.dart b/mobile/openapi/lib/model/tag_create_dto.dart index fd6a10163c..e05b29f1ed 100644 --- a/mobile/openapi/lib/model/tag_create_dto.dart +++ b/mobile/openapi/lib/model/tag_create_dto.dart @@ -19,12 +19,6 @@ class TagCreateDto { }); /// Tag color (hex) - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? color; /// Tag name diff --git a/mobile/openapi/lib/model/tags_response.dart b/mobile/openapi/lib/model/tags_response.dart index 1e4a4bd109..8a3ac17474 100644 --- a/mobile/openapi/lib/model/tags_response.dart +++ b/mobile/openapi/lib/model/tags_response.dart @@ -13,8 +13,8 @@ part of openapi.api; class TagsResponse { /// Returns a new [TagsResponse] instance. TagsResponse({ - this.enabled = true, - this.sidebarWeb = true, + required this.enabled, + required this.sidebarWeb, }); /// Whether tags are enabled diff --git a/mobile/openapi/lib/model/time_buckets_response_dto.dart b/mobile/openapi/lib/model/time_buckets_response_dto.dart index 11faa815e2..8b8da1d37a 100644 --- a/mobile/openapi/lib/model/time_buckets_response_dto.dart +++ b/mobile/openapi/lib/model/time_buckets_response_dto.dart @@ -18,6 +18,9 @@ class TimeBucketsResponseDto { }); /// Number of assets in this time bucket + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int count; /// Time bucket identifier in YYYY-MM-DD format representing the start of the time period diff --git a/mobile/openapi/lib/model/trash_response_dto.dart b/mobile/openapi/lib/model/trash_response_dto.dart index 7edd5d032a..7b43d9ceb7 100644 --- a/mobile/openapi/lib/model/trash_response_dto.dart +++ b/mobile/openapi/lib/model/trash_response_dto.dart @@ -17,6 +17,9 @@ class TrashResponseDto { }); /// Number of items in trash + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int count; @override diff --git a/mobile/openapi/lib/model/update_album_dto.dart b/mobile/openapi/lib/model/update_album_dto.dart index 46ce8b0ecc..ae4a5c1f87 100644 --- a/mobile/openapi/lib/model/update_album_dto.dart +++ b/mobile/openapi/lib/model/update_album_dto.dart @@ -56,7 +56,6 @@ class UpdateAlbumDto { /// bool? isActivityEnabled; - /// Asset sort order /// /// 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 diff --git a/mobile/openapi/lib/model/update_album_user_dto.dart b/mobile/openapi/lib/model/update_album_user_dto.dart index 9d934eb465..43218cae6e 100644 --- a/mobile/openapi/lib/model/update_album_user_dto.dart +++ b/mobile/openapi/lib/model/update_album_user_dto.dart @@ -16,7 +16,6 @@ class UpdateAlbumUserDto { required this.role, }); - /// Album user role AlbumUserRole role; @override diff --git a/mobile/openapi/lib/model/update_asset_dto.dart b/mobile/openapi/lib/model/update_asset_dto.dart index 8526995934..2c4c3352ea 100644 --- a/mobile/openapi/lib/model/update_asset_dto.dart +++ b/mobile/openapi/lib/model/update_asset_dto.dart @@ -52,6 +52,9 @@ class UpdateAssetDto { /// Latitude coordinate /// + /// Minimum value: -90 + /// Maximum value: 90 + /// /// 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. @@ -64,6 +67,9 @@ class UpdateAssetDto { /// Longitude coordinate /// + /// Minimum value: -180 + /// Maximum value: 180 + /// /// 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. @@ -75,9 +81,8 @@ class UpdateAssetDto { /// /// Minimum value: -1 /// Maximum value: 5 - num? rating; + int? rating; - /// Asset visibility /// /// 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 @@ -172,9 +177,7 @@ class UpdateAssetDto { latitude: num.parse('${json[r'latitude']}'), livePhotoVideoId: mapValueOfType(json, r'livePhotoVideoId'), longitude: num.parse('${json[r'longitude']}'), - rating: json[r'rating'] == null - ? null - : num.parse('${json[r'rating']}'), + rating: mapValueOfType(json, r'rating'), visibility: AssetVisibility.fromJson(json[r'visibility']), ); } diff --git a/mobile/openapi/lib/model/update_library_dto.dart b/mobile/openapi/lib/model/update_library_dto.dart index 628bdc0055..276d43ecd9 100644 --- a/mobile/openapi/lib/model/update_library_dto.dart +++ b/mobile/openapi/lib/model/update_library_dto.dart @@ -13,16 +13,16 @@ part of openapi.api; class UpdateLibraryDto { /// Returns a new [UpdateLibraryDto] instance. UpdateLibraryDto({ - this.exclusionPatterns = const {}, - this.importPaths = const {}, + this.exclusionPatterns = const [], + this.importPaths = const [], this.name, }); /// Exclusion patterns (max 128) - Set exclusionPatterns; + List exclusionPatterns; /// Import paths (max 128) - Set importPaths; + List importPaths; /// Library name /// @@ -51,8 +51,8 @@ class UpdateLibraryDto { Map toJson() { final json = {}; - json[r'exclusionPatterns'] = this.exclusionPatterns.toList(growable: false); - json[r'importPaths'] = this.importPaths.toList(growable: false); + json[r'exclusionPatterns'] = this.exclusionPatterns; + json[r'importPaths'] = this.importPaths; if (this.name != null) { json[r'name'] = this.name; } else { @@ -71,11 +71,11 @@ class UpdateLibraryDto { return UpdateLibraryDto( exclusionPatterns: json[r'exclusionPatterns'] is Iterable - ? (json[r'exclusionPatterns'] as Iterable).cast().toSet() - : const {}, + ? (json[r'exclusionPatterns'] as Iterable).cast().toList(growable: false) + : const [], importPaths: json[r'importPaths'] is Iterable - ? (json[r'importPaths'] as Iterable).cast().toSet() - : const {}, + ? (json[r'importPaths'] as Iterable).cast().toList(growable: false) + : const [], name: mapValueOfType(json, r'name'), ); } diff --git a/mobile/openapi/lib/model/usage_by_user_dto.dart b/mobile/openapi/lib/model/usage_by_user_dto.dart index da1fe600a5..462b82c3e0 100644 --- a/mobile/openapi/lib/model/usage_by_user_dto.dart +++ b/mobile/openapi/lib/model/usage_by_user_dto.dart @@ -24,18 +24,33 @@ class UsageByUserDto { }); /// Number of photos + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int photos; /// User quota size in bytes (null if unlimited) + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? quotaSizeInBytes; /// Total storage usage in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int usage; /// Storage usage for photos in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int usagePhotos; /// Storage usage for videos in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int usageVideos; /// User ID @@ -45,6 +60,9 @@ class UsageByUserDto { String userName; /// Number of videos + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int videos; @override diff --git a/mobile/openapi/lib/model/user_admin_create_dto.dart b/mobile/openapi/lib/model/user_admin_create_dto.dart index 485b2e00e5..54da0b0566 100644 --- a/mobile/openapi/lib/model/user_admin_create_dto.dart +++ b/mobile/openapi/lib/model/user_admin_create_dto.dart @@ -25,7 +25,6 @@ class UserAdminCreateDto { this.storageLabel, }); - /// Avatar color UserAvatarColor? avatarColor; /// User email @@ -61,6 +60,7 @@ class UserAdminCreateDto { /// Storage quota in bytes /// /// Minimum value: 0 + /// Maximum value: 9007199254740991 int? quotaSizeInBytes; /// Require password change on next login diff --git a/mobile/openapi/lib/model/user_admin_response_dto.dart b/mobile/openapi/lib/model/user_admin_response_dto.dart index 706f65cf35..09f8cedce4 100644 --- a/mobile/openapi/lib/model/user_admin_response_dto.dart +++ b/mobile/openapi/lib/model/user_admin_response_dto.dart @@ -32,7 +32,6 @@ class UserAdminResponseDto { required this.updatedAt, }); - /// Avatar color UserAvatarColor avatarColor; /// Creation date @@ -50,7 +49,6 @@ class UserAdminResponseDto { /// Is admin user bool isAdmin; - /// User license UserLicense? license; /// User name @@ -66,15 +64,20 @@ class UserAdminResponseDto { String profileImagePath; /// Storage quota in bytes + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int? quotaSizeInBytes; /// Storage usage in bytes + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int? quotaUsageInBytes; /// Require password change on next login bool shouldChangePassword; - /// User status UserStatus status; /// Storage label @@ -130,9 +133,13 @@ class UserAdminResponseDto { Map toJson() { final json = {}; json[r'avatarColor'] = this.avatarColor; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _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.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); if (this.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + 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; } @@ -165,7 +172,9 @@ class UserAdminResponseDto { } else { // json[r'storageLabel'] = null; } - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _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.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } @@ -179,8 +188,8 @@ class UserAdminResponseDto { return UserAdminResponseDto( avatarColor: UserAvatarColor.fromJson(json[r'avatarColor'])!, - createdAt: mapDateTime(json, r'createdAt', r'')!, - deletedAt: mapDateTime(json, r'deletedAt', r''), + createdAt: mapDateTime(json, r'createdAt', 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))$/')!, + 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))$/'), email: mapValueOfType(json, r'email')!, id: mapValueOfType(json, r'id')!, isAdmin: mapValueOfType(json, r'isAdmin')!, @@ -194,7 +203,7 @@ class UserAdminResponseDto { shouldChangePassword: mapValueOfType(json, r'shouldChangePassword')!, status: UserStatus.fromJson(json[r'status'])!, storageLabel: mapValueOfType(json, r'storageLabel'), - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + updatedAt: mapDateTime(json, r'updatedAt', 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))$/')!, ); } return null; diff --git a/mobile/openapi/lib/model/user_admin_update_dto.dart b/mobile/openapi/lib/model/user_admin_update_dto.dart index 3cce65745f..0c33a46139 100644 --- a/mobile/openapi/lib/model/user_admin_update_dto.dart +++ b/mobile/openapi/lib/model/user_admin_update_dto.dart @@ -24,7 +24,6 @@ class UserAdminUpdateDto { this.storageLabel, }); - /// Avatar color UserAvatarColor? avatarColor; /// User email @@ -69,6 +68,7 @@ class UserAdminUpdateDto { /// Storage quota in bytes /// /// Minimum value: 0 + /// Maximum value: 9007199254740991 int? quotaSizeInBytes; /// Require password change on next login diff --git a/mobile/openapi/lib/model/user_avatar_color.dart b/mobile/openapi/lib/model/user_avatar_color.dart index 4fcf518550..719e366899 100644 --- a/mobile/openapi/lib/model/user_avatar_color.dart +++ b/mobile/openapi/lib/model/user_avatar_color.dart @@ -10,7 +10,7 @@ part of openapi.api; -/// Avatar color +/// User avatar color class UserAvatarColor { /// Instantiate a new enum with the provided [value]. const UserAvatarColor._(this.value); diff --git a/mobile/openapi/lib/model/user_license.dart b/mobile/openapi/lib/model/user_license.dart index f02dc73bef..8ef46a0bb5 100644 --- a/mobile/openapi/lib/model/user_license.dart +++ b/mobile/openapi/lib/model/user_license.dart @@ -24,7 +24,7 @@ class UserLicense { /// Activation key String activationKey; - /// License key + /// License key (format: /^IM(SV|CL)(-[\\dA-Za-z]{4}){8}$/) String licenseKey; @override @@ -45,7 +45,9 @@ class UserLicense { Map toJson() { final json = {}; - json[r'activatedAt'] = this.activatedAt.toUtc().toIso8601String(); + json[r'activatedAt'] = _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.activatedAt.millisecondsSinceEpoch + : this.activatedAt.toUtc().toIso8601String(); json[r'activationKey'] = this.activationKey; json[r'licenseKey'] = this.licenseKey; return json; @@ -60,7 +62,7 @@ class UserLicense { final json = value.cast(); return UserLicense( - activatedAt: mapDateTime(json, r'activatedAt', r'')!, + activatedAt: mapDateTime(json, r'activatedAt', 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))$/')!, activationKey: mapValueOfType(json, r'activationKey')!, licenseKey: mapValueOfType(json, r'licenseKey')!, ); diff --git a/mobile/openapi/lib/model/user_response_dto.dart b/mobile/openapi/lib/model/user_response_dto.dart index bf0e2cbf09..f671072c72 100644 --- a/mobile/openapi/lib/model/user_response_dto.dart +++ b/mobile/openapi/lib/model/user_response_dto.dart @@ -21,7 +21,6 @@ class UserResponseDto { required this.profileImagePath, }); - /// Avatar color UserAvatarColor avatarColor; /// User email diff --git a/mobile/openapi/lib/model/user_update_me_dto.dart b/mobile/openapi/lib/model/user_update_me_dto.dart index 066c435eb3..0751d4096b 100644 --- a/mobile/openapi/lib/model/user_update_me_dto.dart +++ b/mobile/openapi/lib/model/user_update_me_dto.dart @@ -19,7 +19,6 @@ class UserUpdateMeDto { this.password, }); - /// Avatar color UserAvatarColor? avatarColor; /// User email diff --git a/mobile/openapi/lib/model/validate_library_dto.dart b/mobile/openapi/lib/model/validate_library_dto.dart index 59c3680782..68fb0e9fe2 100644 --- a/mobile/openapi/lib/model/validate_library_dto.dart +++ b/mobile/openapi/lib/model/validate_library_dto.dart @@ -13,15 +13,15 @@ part of openapi.api; class ValidateLibraryDto { /// Returns a new [ValidateLibraryDto] instance. ValidateLibraryDto({ - this.exclusionPatterns = const {}, - this.importPaths = const {}, + this.exclusionPatterns = const [], + this.importPaths = const [], }); /// Exclusion patterns (max 128) - Set exclusionPatterns; + List exclusionPatterns; /// Import paths to validate (max 128) - Set importPaths; + List importPaths; @override bool operator ==(Object other) => identical(this, other) || other is ValidateLibraryDto && @@ -39,8 +39,8 @@ class ValidateLibraryDto { Map toJson() { final json = {}; - json[r'exclusionPatterns'] = this.exclusionPatterns.toList(growable: false); - json[r'importPaths'] = this.importPaths.toList(growable: false); + json[r'exclusionPatterns'] = this.exclusionPatterns; + json[r'importPaths'] = this.importPaths; return json; } @@ -54,11 +54,11 @@ class ValidateLibraryDto { return ValidateLibraryDto( exclusionPatterns: json[r'exclusionPatterns'] is Iterable - ? (json[r'exclusionPatterns'] as Iterable).cast().toSet() - : const {}, + ? (json[r'exclusionPatterns'] as Iterable).cast().toList(growable: false) + : const [], importPaths: json[r'importPaths'] is Iterable - ? (json[r'importPaths'] as Iterable).cast().toSet() - : const {}, + ? (json[r'importPaths'] as Iterable).cast().toList(growable: false) + : const [], ); } return null; diff --git a/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart b/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart index 78cc03dc94..ebcb881935 100644 --- a/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart +++ b/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart @@ -14,7 +14,7 @@ class ValidateLibraryImportPathResponseDto { /// Returns a new [ValidateLibraryImportPathResponseDto] instance. ValidateLibraryImportPathResponseDto({ required this.importPath, - this.isValid = false, + required this.isValid, this.message, }); diff --git a/mobile/openapi/lib/model/video_container.dart b/mobile/openapi/lib/model/video_container.dart index b1a47c8721..a291fabf6e 100644 --- a/mobile/openapi/lib/model/video_container.dart +++ b/mobile/openapi/lib/model/video_container.dart @@ -10,7 +10,7 @@ part of openapi.api; -/// Accepted containers +/// Accepted video containers class VideoContainer { /// Instantiate a new enum with the provided [value]. const VideoContainer._(this.value); diff --git a/mobile/openapi/lib/model/workflow_action_item_dto.dart b/mobile/openapi/lib/model/workflow_action_item_dto.dart index 9222dd6ba7..1ad70238d8 100644 --- a/mobile/openapi/lib/model/workflow_action_item_dto.dart +++ b/mobile/openapi/lib/model/workflow_action_item_dto.dart @@ -13,31 +13,24 @@ part of openapi.api; class WorkflowActionItemDto { /// Returns a new [WorkflowActionItemDto] instance. WorkflowActionItemDto({ - this.actionConfig, + this.actionConfig = const {}, required this.pluginActionId, }); - /// Action configuration - /// - /// 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. - /// - Object? actionConfig; + Map actionConfig; /// Plugin action ID String pluginActionId; @override bool operator ==(Object other) => identical(this, other) || other is WorkflowActionItemDto && - other.actionConfig == actionConfig && + _deepEquality.equals(other.actionConfig, actionConfig) && other.pluginActionId == pluginActionId; @override int get hashCode => // ignore: unnecessary_parenthesis - (actionConfig == null ? 0 : actionConfig!.hashCode) + + (actionConfig.hashCode) + (pluginActionId.hashCode); @override @@ -45,11 +38,7 @@ class WorkflowActionItemDto { Map toJson() { final json = {}; - if (this.actionConfig != null) { json[r'actionConfig'] = this.actionConfig; - } else { - // json[r'actionConfig'] = null; - } json[r'pluginActionId'] = this.pluginActionId; return json; } @@ -63,7 +52,7 @@ class WorkflowActionItemDto { final json = value.cast(); return WorkflowActionItemDto( - actionConfig: mapValueOfType(json, r'actionConfig'), + actionConfig: mapCastOfType(json, r'actionConfig') ?? const {}, pluginActionId: mapValueOfType(json, r'pluginActionId')!, ); } diff --git a/mobile/openapi/lib/model/workflow_action_response_dto.dart b/mobile/openapi/lib/model/workflow_action_response_dto.dart index 8f77e9cf2b..dcbb5ee8ef 100644 --- a/mobile/openapi/lib/model/workflow_action_response_dto.dart +++ b/mobile/openapi/lib/model/workflow_action_response_dto.dart @@ -20,8 +20,7 @@ class WorkflowActionResponseDto { required this.workflowId, }); - /// Action configuration - Object? actionConfig; + Map? actionConfig; /// Action ID String id; @@ -37,7 +36,7 @@ class WorkflowActionResponseDto { @override bool operator ==(Object other) => identical(this, other) || other is WorkflowActionResponseDto && - other.actionConfig == actionConfig && + _deepEquality.equals(other.actionConfig, actionConfig) && other.id == id && other.order == order && other.pluginActionId == pluginActionId && @@ -78,7 +77,7 @@ class WorkflowActionResponseDto { final json = value.cast(); return WorkflowActionResponseDto( - actionConfig: mapValueOfType(json, r'actionConfig'), + actionConfig: mapCastOfType(json, r'actionConfig'), id: mapValueOfType(json, r'id')!, order: num.parse('${json[r'order']}'), pluginActionId: mapValueOfType(json, r'pluginActionId')!, diff --git a/mobile/openapi/lib/model/workflow_create_dto.dart b/mobile/openapi/lib/model/workflow_create_dto.dart index 38665a1912..143af0ca6c 100644 --- a/mobile/openapi/lib/model/workflow_create_dto.dart +++ b/mobile/openapi/lib/model/workflow_create_dto.dart @@ -48,7 +48,6 @@ class WorkflowCreateDto { /// Workflow name String name; - /// Workflow trigger type PluginTriggerType triggerType; @override diff --git a/mobile/openapi/lib/model/workflow_filter_item_dto.dart b/mobile/openapi/lib/model/workflow_filter_item_dto.dart index 52e29c3e93..92224b9f16 100644 --- a/mobile/openapi/lib/model/workflow_filter_item_dto.dart +++ b/mobile/openapi/lib/model/workflow_filter_item_dto.dart @@ -13,31 +13,24 @@ part of openapi.api; class WorkflowFilterItemDto { /// Returns a new [WorkflowFilterItemDto] instance. WorkflowFilterItemDto({ - this.filterConfig, + this.filterConfig = const {}, required this.pluginFilterId, }); - /// Filter configuration - /// - /// 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. - /// - Object? filterConfig; + Map filterConfig; /// Plugin filter ID String pluginFilterId; @override bool operator ==(Object other) => identical(this, other) || other is WorkflowFilterItemDto && - other.filterConfig == filterConfig && + _deepEquality.equals(other.filterConfig, filterConfig) && other.pluginFilterId == pluginFilterId; @override int get hashCode => // ignore: unnecessary_parenthesis - (filterConfig == null ? 0 : filterConfig!.hashCode) + + (filterConfig.hashCode) + (pluginFilterId.hashCode); @override @@ -45,11 +38,7 @@ class WorkflowFilterItemDto { Map toJson() { final json = {}; - if (this.filterConfig != null) { json[r'filterConfig'] = this.filterConfig; - } else { - // json[r'filterConfig'] = null; - } json[r'pluginFilterId'] = this.pluginFilterId; return json; } @@ -63,7 +52,7 @@ class WorkflowFilterItemDto { final json = value.cast(); return WorkflowFilterItemDto( - filterConfig: mapValueOfType(json, r'filterConfig'), + filterConfig: mapCastOfType(json, r'filterConfig') ?? const {}, pluginFilterId: mapValueOfType(json, r'pluginFilterId')!, ); } diff --git a/mobile/openapi/lib/model/workflow_filter_response_dto.dart b/mobile/openapi/lib/model/workflow_filter_response_dto.dart index 355378adac..932722f5a5 100644 --- a/mobile/openapi/lib/model/workflow_filter_response_dto.dart +++ b/mobile/openapi/lib/model/workflow_filter_response_dto.dart @@ -20,8 +20,7 @@ class WorkflowFilterResponseDto { required this.workflowId, }); - /// Filter configuration - Object? filterConfig; + Map? filterConfig; /// Filter ID String id; @@ -37,7 +36,7 @@ class WorkflowFilterResponseDto { @override bool operator ==(Object other) => identical(this, other) || other is WorkflowFilterResponseDto && - other.filterConfig == filterConfig && + _deepEquality.equals(other.filterConfig, filterConfig) && other.id == id && other.order == order && other.pluginFilterId == pluginFilterId && @@ -78,7 +77,7 @@ class WorkflowFilterResponseDto { final json = value.cast(); return WorkflowFilterResponseDto( - filterConfig: mapValueOfType(json, r'filterConfig'), + filterConfig: mapCastOfType(json, r'filterConfig'), id: mapValueOfType(json, r'id')!, order: num.parse('${json[r'order']}'), pluginFilterId: mapValueOfType(json, r'pluginFilterId')!, diff --git a/mobile/openapi/lib/model/workflow_response_dto.dart b/mobile/openapi/lib/model/workflow_response_dto.dart index ae3e6510aa..6461b62508 100644 --- a/mobile/openapi/lib/model/workflow_response_dto.dart +++ b/mobile/openapi/lib/model/workflow_response_dto.dart @@ -48,7 +48,6 @@ class WorkflowResponseDto { /// Owner user ID String ownerId; - /// Workflow trigger type PluginTriggerType triggerType; @override diff --git a/mobile/openapi/lib/model/workflow_update_dto.dart b/mobile/openapi/lib/model/workflow_update_dto.dart index 9891fff079..9abb45ddd5 100644 --- a/mobile/openapi/lib/model/workflow_update_dto.dart +++ b/mobile/openapi/lib/model/workflow_update_dto.dart @@ -54,7 +54,6 @@ class WorkflowUpdateDto { /// String? name; - /// Workflow trigger type /// /// 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 diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 9d5f431792..f2caf05be8 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -378,14 +378,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" - dartx: - dependency: transitive - description: - name: dartx - sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244" - url: "https://pub.dev" - source: hosted - version: "1.2.0" dbus: dependency: transitive description: @@ -1012,40 +1004,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" - isar: - dependency: "direct main" - description: - path: "packages/isar" - ref: bb1dca40fe87a001122e5d43bc6254718cb49f3a - resolved-ref: bb1dca40fe87a001122e5d43bc6254718cb49f3a - url: "https://github.com/immich-app/isar" - source: git - version: "3.1.8" - isar_community: - dependency: transitive - description: - name: isar_community - sha256: "28f59e54636c45ba0bb1b3b7f2656f1c50325f740cea6efcd101900be3fba546" - url: "https://pub.dev" - source: hosted - version: "3.3.0-dev.3" - isar_community_flutter_libs: - dependency: "direct main" - description: - name: isar_community_flutter_libs - sha256: c2934fe755bb3181cb67602fd5df0d080b3d3eb52799f98623aa4fc5acbea010 - url: "https://pub.dev" - source: hosted - version: "3.3.0-dev.3" - isar_generator: - dependency: "direct dev" - description: - path: "packages/isar_generator" - ref: bb1dca40fe87a001122e5d43bc6254718cb49f3a - resolved-ref: bb1dca40fe87a001122e5d43bc6254718cb49f3a - url: "https://github.com/immich-app/isar" - source: git - version: "3.1.8" jni: dependency: transitive description: @@ -1909,14 +1867,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.0+1" - time: - dependency: transitive - description: - name: time - sha256: "370572cf5d1e58adcb3e354c47515da3f7469dac3a95b447117e728e7be6f461" - url: "https://pub.dev" - source: hosted - version: "2.1.5" timezone: dependency: "direct main" description: @@ -2173,14 +2123,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.6.1" - xxh3: - dependency: transitive - description: - name: xxh3 - sha256: "399a0438f5d426785723c99da6b16e136f4953fb1e9db0bf270bd41dd4619916" - url: "https://pub.dev" - source: hosted - version: "1.2.0" yaml: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 1e03bd6e7f..5eb4deb924 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -43,16 +43,9 @@ dependencies: immich_ui: path: './packages/ui' intl: ^0.20.2 - isar: - git: - url: https://github.com/immich-app/isar - ref: 'bb1dca40fe87a001122e5d43bc6254718cb49f3a' - path: packages/isar/ - isar_community_flutter_libs: 3.3.0-dev.3 local_auth: ^2.3.0 logging: ^1.3.0 maplibre_gl: ^0.22.0 - native_video_player: git: url: https://github.com/immich-app/native_video_player @@ -115,11 +108,6 @@ dev_dependencies: path: './immich_lint' integration_test: sdk: flutter - isar_generator: - git: - url: https://github.com/immich-app/isar - ref: 'bb1dca40fe87a001122e5d43bc6254718cb49f3a' - path: packages/isar_generator/ mocktail: ^1.0.4 # Type safe platform code pigeon: ^26.0.2 diff --git a/mobile/test/api.mocks.dart b/mobile/test/api.mocks.dart index c6a3a90582..e1c32eaaee 100644 --- a/mobile/test/api.mocks.dart +++ b/mobile/test/api.mocks.dart @@ -1,8 +1,6 @@ import 'package:mocktail/mocktail.dart'; import 'package:openapi/api.dart'; -class MockAssetsApi extends Mock implements AssetsApi {} - class MockSyncApi extends Mock implements SyncApi {} class MockServerApi extends Mock implements ServerApi {} diff --git a/mobile/test/domain/service.mock.dart b/mobile/test/domain/service.mock.dart index 56b4802f88..89e85a3794 100644 --- a/mobile/test/domain/service.mock.dart +++ b/mobile/test/domain/service.mock.dart @@ -1,20 +1,13 @@ import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/background_upload.service.dart'; import 'package:mocktail/mocktail.dart'; class MockStoreService extends Mock implements StoreService {} -class MockUserService extends Mock implements UserService {} - class MockBackgroundSyncManager extends Mock implements BackgroundSyncManager {} class MockNativeSyncApi extends Mock implements NativeSyncApi {} class MockAppSettingsService extends Mock implements AppSettingsService {} - -class MockBackgroundUploadService extends Mock implements BackgroundUploadService {} - diff --git a/mobile/test/domain/services/album.service_test.dart b/mobile/test/domain/services/album.service_test.dart deleted file mode 100644 index 9110a09471..0000000000 --- a/mobile/test/domain/services/album.service_test.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/album/album.model.dart'; -import 'package:immich_mobile/domain/services/remote_album.service.dart'; -import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; -import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; -import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../infrastructure/repository.mock.dart'; - -void main() { - late RemoteAlbumService sut; - late DriftRemoteAlbumRepository mockRemoteAlbumRepo; - late DriftAlbumApiRepository mockAlbumApiRepo; - - final albumA = RemoteAlbum( - id: '1', - name: 'Album A', - description: "", - isActivityEnabled: false, - order: AlbumAssetOrder.asc, - assetCount: 1, - createdAt: DateTime(2023, 1, 1), - updatedAt: DateTime(2023, 1, 2), - ownerId: 'owner1', - ownerName: "Test User", - isShared: false, - ); - - final albumB = RemoteAlbum( - id: '2', - name: 'Album B', - description: "", - isActivityEnabled: false, - order: AlbumAssetOrder.desc, - assetCount: 2, - createdAt: DateTime(2023, 2, 1), - updatedAt: DateTime(2023, 2, 2), - ownerId: 'owner2', - ownerName: "Test User", - isShared: false, - ); - - setUp(() { - mockRemoteAlbumRepo = MockRemoteAlbumRepository(); - mockAlbumApiRepo = MockDriftAlbumApiRepository(); - - when( - () => mockRemoteAlbumRepo.getSortedAlbumIds(any(), aggregation: AssetDateAggregation.end), - ).thenAnswer((_) async => ['1', '2']); - - when( - () => mockRemoteAlbumRepo.getSortedAlbumIds(any(), aggregation: AssetDateAggregation.start), - ).thenAnswer((_) async => ['1', '2']); - - sut = RemoteAlbumService(mockRemoteAlbumRepo, mockAlbumApiRepo); - }); - - group('sortAlbums', () { - test('should sort correctly based on name', () async { - final albums = [albumB, albumA]; - - final result = await sut.sortAlbums(albums, AlbumSortMode.title); - expect(result, [albumA, albumB]); - }); - - test('should sort correctly based on createdAt', () async { - final albums = [albumB, albumA]; - - final result = await sut.sortAlbums(albums, AlbumSortMode.created); - expect(result, [albumB, albumA]); - }); - - test('should sort correctly based on updatedAt', () async { - final albums = [albumB, albumA]; - - final result = await sut.sortAlbums(albums, AlbumSortMode.lastModified); - expect(result, [albumB, albumA]); - }); - - test('should sort correctly based on assetCount', () async { - final albums = [albumB, albumA]; - - final result = await sut.sortAlbums(albums, AlbumSortMode.assetCount); - expect(result, [albumB, albumA]); - }); - - test('should sort correctly based on newestAssetTimestamp', () async { - final albums = [albumB, albumA]; - - final result = await sut.sortAlbums(albums, AlbumSortMode.mostRecent); - expect(result, [albumB, albumA]); - }); - - test('should sort correctly based on oldestAssetTimestamp', () async { - final albums = [albumB, albumA]; - - final result = await sut.sortAlbums(albums, AlbumSortMode.mostOldest); - expect(result, [albumA, albumB]); - }); - - test('should flip order when isReverse is true for all modes', () async { - final albums = [albumB, albumA]; - - for (final mode in AlbumSortMode.values) { - final normal = await sut.sortAlbums(albums, mode, isReverse: false); - final reversed = await sut.sortAlbums(albums, mode, isReverse: true); - - // reversed should be the exact inverse of normal - expect(reversed, normal.reversed.toList(), reason: 'Mode: $mode'); - } - }); - }); -} diff --git a/mobile/test/domain/services/asset.service_test.dart b/mobile/test/domain/services/asset.service_test.dart deleted file mode 100644 index 04e49f89f9..0000000000 --- a/mobile/test/domain/services/asset.service_test.dart +++ /dev/null @@ -1,185 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/domain/services/asset.service.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../infrastructure/repository.mock.dart'; -import '../../test_utils.dart'; - -void main() { - late AssetService sut; - late MockRemoteAssetRepository mockRemoteAssetRepository; - late MockDriftLocalAssetRepository mockLocalAssetRepository; - - setUp(() { - mockRemoteAssetRepository = MockRemoteAssetRepository(); - mockLocalAssetRepository = MockDriftLocalAssetRepository(); - sut = AssetService( - remoteAssetRepository: mockRemoteAssetRepository, - localAssetRepository: mockLocalAssetRepository, - ); - }); - - group('getAspectRatio', () { - test('flips dimensions on Android for 90° and 270° orientations', () async { - debugDefaultTargetPlatformOverride = TargetPlatform.android; - addTearDown(() => debugDefaultTargetPlatformOverride = null); - - for (final orientation in [90, 270]) { - final localAsset = TestUtils.createLocalAsset( - id: 'local-$orientation', - width: 1920, - height: 1080, - orientation: orientation, - ); - - final result = await sut.getAspectRatio(localAsset); - - expect(result, 1080 / 1920, reason: 'Orientation $orientation should flip on Android'); - } - }); - - test('does not flip dimensions on iOS regardless of orientation', () async { - debugDefaultTargetPlatformOverride = TargetPlatform.iOS; - addTearDown(() => debugDefaultTargetPlatformOverride = null); - - for (final orientation in [0, 90, 270]) { - final localAsset = TestUtils.createLocalAsset( - id: 'local-$orientation', - width: 1920, - height: 1080, - orientation: orientation, - ); - - final result = await sut.getAspectRatio(localAsset); - - expect(result, 1920 / 1080, reason: 'iOS should never flip dimensions'); - } - }); - - test('fetches dimensions from remote repository when missing from asset', () async { - final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: null, height: null); - - final exif = const ExifInfo(orientation: '1'); - - final fetchedAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: 1920, height: 1080); - - when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif); - when(() => mockRemoteAssetRepository.get('remote-1')).thenAnswer((_) async => fetchedAsset); - - final result = await sut.getAspectRatio(remoteAsset); - - expect(result, 1920 / 1080); - verify(() => mockRemoteAssetRepository.get('remote-1')).called(1); - }); - - test('fetches dimensions from local repository when missing from local asset', () async { - final localAsset = TestUtils.createLocalAsset(id: 'local-1', width: null, height: null, orientation: 0); - - final fetchedAsset = TestUtils.createLocalAsset(id: 'local-1', width: 1920, height: 1080, orientation: 0); - - when(() => mockLocalAssetRepository.get('local-1')).thenAnswer((_) async => fetchedAsset); - - final result = await sut.getAspectRatio(localAsset); - - expect(result, 1920 / 1080); - verify(() => mockLocalAssetRepository.get('local-1')).called(1); - }); - - test('uses fetched asset orientation when dimensions are missing on Android', () async { - debugDefaultTargetPlatformOverride = TargetPlatform.android; - addTearDown(() => debugDefaultTargetPlatformOverride = null); - - // Original asset has default orientation 0, but dimensions are missing - final localAsset = TestUtils.createLocalAsset(id: 'local-1', width: null, height: null, orientation: 0); - - // Fetched asset has 90° orientation and proper dimensions - final fetchedAsset = TestUtils.createLocalAsset(id: 'local-1', width: 1920, height: 1080, orientation: 90); - - when(() => mockLocalAssetRepository.get('local-1')).thenAnswer((_) async => fetchedAsset); - - final result = await sut.getAspectRatio(localAsset); - - // Should flip dimensions since fetched asset has 90° orientation - expect(result, 1080 / 1920); - verify(() => mockLocalAssetRepository.get('local-1')).called(1); - }); - - test('returns 1.0 when dimensions are still unavailable after fetching', () async { - final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: null, height: null); - - final exif = const ExifInfo(orientation: '1'); - - when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif); - when(() => mockRemoteAssetRepository.get('remote-1')).thenAnswer((_) async => null); - - final result = await sut.getAspectRatio(remoteAsset); - - expect(result, 1.0); - }); - - test('returns 1.0 when height is zero', () async { - final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: 1920, height: 0); - - final exif = const ExifInfo(orientation: '1'); - - when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif); - - final result = await sut.getAspectRatio(remoteAsset); - - expect(result, 1.0); - }); - - test('handles local asset with remoteId using local orientation not remote exif', () async { - // When a LocalAsset has a remoteId (merged), we should use local orientation - // because the width/height come from the local asset (pre-corrected on iOS) - final localAsset = TestUtils.createLocalAsset( - id: 'local-1', - remoteId: 'remote-1', - width: 1920, - height: 1080, - orientation: 0, - ); - - final result = await sut.getAspectRatio(localAsset); - - expect(result, 1920 / 1080); - // Should not call remote exif for LocalAsset - verifyNever(() => mockRemoteAssetRepository.getExif(any())); - }); - - test('handles local asset with remoteId and 90 degree rotation on Android', () async { - debugDefaultTargetPlatformOverride = TargetPlatform.android; - addTearDown(() => debugDefaultTargetPlatformOverride = null); - - final localAsset = TestUtils.createLocalAsset( - id: 'local-1', - remoteId: 'remote-1', - width: 1920, - height: 1080, - orientation: 90, - ); - - final result = await sut.getAspectRatio(localAsset); - - expect(result, 1080 / 1920); - }); - - test('should not flip remote asset dimensions', () async { - final flippedOrientations = ['1', '2', '3', '4', '5', '6', '7', '8', '90', '-90']; - - for (final orientation in flippedOrientations) { - final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-$orientation', width: 1920, height: 1080); - - final exif = ExifInfo(orientation: orientation); - - when(() => mockRemoteAssetRepository.getExif('remote-$orientation')).thenAnswer((_) async => exif); - - final result = await sut.getAspectRatio(remoteAsset); - - expect(result, 1920 / 1080, reason: 'Should not flipped remote asset dimensions for orientation $orientation'); - } - }); - }); -} diff --git a/mobile/test/domain/services/log_service_test.dart b/mobile/test/domain/services/log_service_test.dart index 95f677ba98..0ccef393ab 100644 --- a/mobile/test/domain/services/log_service_test.dart +++ b/mobile/test/domain/services/log_service_test.dart @@ -29,11 +29,11 @@ final _kWarnLog = LogMessage( void main() { late LogService sut; late LogRepository mockLogRepo; - late IsarStoreRepository mockStoreRepo; + late DriftStoreRepository mockStoreRepo; setUp(() async { mockLogRepo = MockLogRepository(); - mockStoreRepo = MockStoreRepository(); + mockStoreRepo = MockDriftStoreRepository(); registerFallbackValue(_kInfoLog); diff --git a/mobile/test/domain/services/store_service_test.dart b/mobile/test/domain/services/store_service_test.dart index 996170b518..8ceb1e3c9c 100644 --- a/mobile/test/domain/services/store_service_test.dart +++ b/mobile/test/domain/services/store_service_test.dart @@ -15,13 +15,11 @@ final _kBackupFailedSince = DateTime.utc(2023); void main() { late StoreService sut; - late IsarStoreRepository mockStoreRepo; late DriftStoreRepository mockDriftStoreRepo; late StreamController>> controller; setUp(() async { controller = StreamController>>.broadcast(); - mockStoreRepo = MockStoreRepository(); mockDriftStoreRepo = MockDriftStoreRepository(); // For generics, we need to provide fallback to each concrete type to avoid runtime errors registerFallbackValue(StoreKey.accessToken); @@ -29,16 +27,6 @@ void main() { registerFallbackValue(StoreKey.backgroundBackup); registerFallbackValue(StoreKey.backupFailedSince); - when(() => mockStoreRepo.getAll()).thenAnswer( - (_) async => [ - const StoreDto(StoreKey.accessToken, _kAccessToken), - const StoreDto(StoreKey.backgroundBackup, _kBackgroundBackup), - const StoreDto(StoreKey.groupAssetsBy, _kGroupAssetsBy), - StoreDto(StoreKey.backupFailedSince, _kBackupFailedSince), - ], - ); - when(() => mockStoreRepo.watchAll()).thenAnswer((_) => controller.stream); - when(() => mockDriftStoreRepo.getAll()).thenAnswer( (_) async => [ const StoreDto(StoreKey.accessToken, _kAccessToken), @@ -49,7 +37,7 @@ void main() { ); when(() => mockDriftStoreRepo.watchAll()).thenAnswer((_) => controller.stream); - sut = await StoreService.create(storeRepository: mockStoreRepo); + sut = await StoreService.create(storeRepository: mockDriftStoreRepo); }); tearDown(() async { @@ -59,7 +47,7 @@ void main() { group("Store Service Init:", () { test('Populates the internal cache on init', () { - verify(() => mockStoreRepo.getAll()).called(1); + verify(() => mockDriftStoreRepo.getAll()).called(1); expect(sut.tryGet(StoreKey.accessToken), _kAccessToken); expect(sut.tryGet(StoreKey.backgroundBackup), _kBackgroundBackup); expect(sut.tryGet(StoreKey.groupAssetsBy), _kGroupAssetsBy); @@ -74,7 +62,7 @@ void main() { await pumpEventQueue(); - verify(() => mockStoreRepo.watchAll()).called(1); + verify(() => mockDriftStoreRepo.watchAll()).called(1); expect(sut.tryGet(StoreKey.accessToken), _kAccessToken.toUpperCase()); }); }); @@ -95,19 +83,18 @@ void main() { group('Store Service put:', () { setUp(() { - when(() => mockStoreRepo.upsert(any>(), any())).thenAnswer((_) async => true); when(() => mockDriftStoreRepo.upsert(any>(), any())).thenAnswer((_) async => true); }); test('Skip insert when value is not modified', () async { await sut.put(StoreKey.accessToken, _kAccessToken); - verifyNever(() => mockStoreRepo.upsert(StoreKey.accessToken, any())); + verifyNever(() => mockDriftStoreRepo.upsert(StoreKey.accessToken, any())); }); test('Insert value when modified', () async { final newAccessToken = _kAccessToken.toUpperCase(); await sut.put(StoreKey.accessToken, newAccessToken); - verify(() => mockStoreRepo.upsert(StoreKey.accessToken, newAccessToken)).called(1); + verify(() => mockDriftStoreRepo.upsert(StoreKey.accessToken, newAccessToken)).called(1); expect(sut.tryGet(StoreKey.accessToken), newAccessToken); }); }); @@ -117,7 +104,6 @@ void main() { setUp(() { valueController = StreamController.broadcast(); - when(() => mockStoreRepo.watch(any>())).thenAnswer((_) => valueController.stream); when(() => mockDriftStoreRepo.watch(any>())).thenAnswer((_) => valueController.stream); }); @@ -136,19 +122,18 @@ void main() { } await pumpEventQueue(); - verify(() => mockStoreRepo.watch(StoreKey.accessToken)).called(1); + verify(() => mockDriftStoreRepo.watch(StoreKey.accessToken)).called(1); }); }); group('Store Service delete:', () { setUp(() { - when(() => mockStoreRepo.delete(any>())).thenAnswer((_) async => true); when(() => mockDriftStoreRepo.delete(any>())).thenAnswer((_) async => true); }); test('Removes the value from the DB', () async { await sut.delete(StoreKey.accessToken); - verify(() => mockStoreRepo.delete(StoreKey.accessToken)).called(1); + verify(() => mockDriftStoreRepo.delete(StoreKey.accessToken)).called(1); }); test('Removes the value from the cache', () async { @@ -159,13 +144,12 @@ void main() { group('Store Service clear:', () { setUp(() { - when(() => mockStoreRepo.deleteAll()).thenAnswer((_) async => true); when(() => mockDriftStoreRepo.deleteAll()).thenAnswer((_) async => true); }); test('Clears all values from the store', () async { await sut.clear(); - verify(() => mockStoreRepo.deleteAll()).called(1); + verify(() => mockDriftStoreRepo.deleteAll()).called(1); expect(sut.tryGet(StoreKey.accessToken), isNull); expect(sut.tryGet(StoreKey.backgroundBackup), isNull); expect(sut.tryGet(StoreKey.groupAssetsBy), isNull); diff --git a/mobile/test/domain/services/user_service_test.dart b/mobile/test/domain/services/user_service_test.dart index 395f38a207..80b6d80457 100644 --- a/mobile/test/domain/services/user_service_test.dart +++ b/mobile/test/domain/services/user_service_test.dart @@ -4,7 +4,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; import 'package:mocktail/mocktail.dart'; @@ -14,19 +13,13 @@ import '../service.mock.dart'; void main() { late UserService sut; - late IsarUserRepository mockUserRepo; late UserApiRepository mockUserApiRepo; late StoreService mockStoreService; setUp(() { - mockUserRepo = MockIsarUserRepository(); mockUserApiRepo = MockUserApiRepository(); mockStoreService = MockStoreService(); - sut = UserService( - isarUserRepository: mockUserRepo, - userApiRepository: mockUserApiRepo, - storeService: mockStoreService, - ); + sut = UserService(userApiRepository: mockUserApiRepo, storeService: mockStoreService); registerFallbackValue(UserStub.admin); when(() => mockStoreService.get(StoreKey.currentUser)).thenReturn(UserStub.admin); @@ -77,11 +70,9 @@ void main() { test('should return user from api and store it', () async { when(() => mockUserApiRepo.getMyUser()).thenAnswer((_) async => UserStub.admin); when(() => mockStoreService.put(StoreKey.currentUser, UserStub.admin)).thenAnswer((_) async => true); - when(() => mockUserRepo.update(UserStub.admin)).thenAnswer((_) async => UserStub.admin); final result = await sut.refreshMyUser(); verify(() => mockStoreService.put(StoreKey.currentUser, UserStub.admin)).called(1); - verify(() => mockUserRepo.update(UserStub.admin)).called(1); expect(result, UserStub.admin); }); @@ -90,7 +81,6 @@ void main() { final result = await sut.refreshMyUser(); verifyNever(() => mockStoreService.put(StoreKey.currentUser, UserStub.admin)); - verifyNever(() => mockUserRepo.update(UserStub.admin)); expect(result, isNull); }); }); @@ -104,12 +94,10 @@ void main() { () => mockUserApiRepo.createProfileImage(name: profileImagePath, data: Uint8List(0)), ).thenAnswer((_) async => profileImagePath); when(() => mockStoreService.put(StoreKey.currentUser, updatedUser)).thenAnswer((_) async => true); - when(() => mockUserRepo.update(updatedUser)).thenAnswer((_) async => UserStub.admin); final result = await sut.createProfileImage(profileImagePath, Uint8List(0)); verify(() => mockStoreService.put(StoreKey.currentUser, updatedUser)).called(1); - verify(() => mockUserRepo.update(updatedUser)).called(1); expect(result, profileImagePath); }); @@ -123,7 +111,6 @@ void main() { final result = await sut.createProfileImage(profileImagePath, Uint8List(0)); verifyNever(() => mockStoreService.put(StoreKey.currentUser, updatedUser)); - verifyNever(() => mockUserRepo.update(updatedUser)); expect(result, isNull); }); }); diff --git a/mobile/test/dto.mocks.dart b/mobile/test/dto.mocks.dart deleted file mode 100644 index ed53fcdc90..0000000000 --- a/mobile/test/dto.mocks.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:mocktail/mocktail.dart'; -import 'package:openapi/api.dart'; - -class MockSmartSearchDto extends Mock implements SmartSearchDto {} - -class MockMetadataSearchDto extends Mock implements MetadataSearchDto {} diff --git a/mobile/test/fixtures/album.stub.dart b/mobile/test/fixtures/album.stub.dart index a22a4b72ab..5141540a25 100644 --- a/mobile/test/fixtures/album.stub.dart +++ b/mobile/test/fixtures/album.stub.dart @@ -1,108 +1,4 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; - -import 'asset.stub.dart'; -import 'user.stub.dart'; - -final class AlbumStub { - const AlbumStub._(); - - static final emptyAlbum = Album( - name: "empty-album", - localId: "empty-album-local", - remoteId: "empty-album-remote", - createdAt: DateTime(2000), - modifiedAt: DateTime(2023), - shared: false, - activityEnabled: false, - startDate: DateTime(2020), - ); - - static final sharedWithUser = Album( - name: "empty-album-shared-with-user", - localId: "empty-album-shared-with-user-local", - remoteId: "empty-album-shared-with-user-remote", - createdAt: DateTime(2023), - modifiedAt: DateTime(2023), - shared: true, - activityEnabled: false, - endDate: DateTime(2020), - )..sharedUsers.addAll([User.fromDto(UserStub.admin)]); - - static final oneAsset = Album( - name: "album-with-single-asset", - localId: "album-with-single-asset-local", - remoteId: "album-with-single-asset-remote", - createdAt: DateTime(2022), - modifiedAt: DateTime(2023), - shared: false, - activityEnabled: false, - startDate: DateTime(2020), - endDate: DateTime(2023), - )..assets.addAll([AssetStub.image1]); - - static final twoAsset = - Album( - name: "album-with-two-assets", - localId: "album-with-two-assets-local", - remoteId: "album-with-two-assets-remote", - createdAt: DateTime(2001), - modifiedAt: DateTime(2010), - shared: false, - activityEnabled: false, - startDate: DateTime(2019), - endDate: DateTime(2020), - ) - ..assets.addAll([AssetStub.image1, AssetStub.image2]) - ..activityEnabled = true - ..owner.value = User.fromDto(UserStub.admin); - - static final create2020end2020Album = Album( - name: "create2020update2020Album", - localId: "create2020update2020Album-local", - remoteId: "create2020update2020Album-remote", - createdAt: DateTime(2020), - modifiedAt: DateTime(2020), - shared: false, - activityEnabled: false, - startDate: DateTime(2020), - endDate: DateTime(2020), - ); - static final create2020end2022Album = Album( - name: "create2020update2021Album", - localId: "create2020update2021Album-local", - remoteId: "create2020update2021Album-remote", - createdAt: DateTime(2020), - modifiedAt: DateTime(2022), - shared: false, - activityEnabled: false, - startDate: DateTime(2020), - endDate: DateTime(2022), - ); - static final create2020end2024Album = Album( - name: "create2020update2022Album", - localId: "create2020update2022Album-local", - remoteId: "create2020update2022Album-remote", - createdAt: DateTime(2020), - modifiedAt: DateTime(2024), - shared: false, - activityEnabled: false, - startDate: DateTime(2020), - endDate: DateTime(2024), - ); - static final create2020end2026Album = Album( - name: "create2020update2023Album", - localId: "create2020update2023Album-local", - remoteId: "create2020update2023Album-remote", - createdAt: DateTime(2020), - modifiedAt: DateTime(2026), - shared: false, - activityEnabled: false, - startDate: DateTime(2020), - endDate: DateTime(2026), - ); -} abstract final class LocalAlbumStub { const LocalAlbumStub._(); diff --git a/mobile/test/fixtures/asset.stub.dart b/mobile/test/fixtures/asset.stub.dart index 90a7f11737..473b900271 100644 --- a/mobile/test/fixtures/asset.stub.dart +++ b/mobile/test/fixtures/asset.stub.dart @@ -1,59 +1,4 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart' as old; - -final class AssetStub { - const AssetStub._(); - - static final image1 = old.Asset( - checksum: "image1-checksum", - localId: "image1", - remoteId: 'image1-remote', - ownerId: 1, - fileCreatedAt: DateTime(2019), - fileModifiedAt: DateTime(2020), - updatedAt: DateTime.now(), - durationInSeconds: 0, - type: old.AssetType.image, - fileName: "image1.jpg", - isFavorite: true, - isArchived: false, - isTrashed: false, - exifInfo: const ExifInfo(isFlipped: false), - ); - - static final image2 = old.Asset( - checksum: "image2-checksum", - localId: "image2", - remoteId: 'image2-remote', - ownerId: 1, - fileCreatedAt: DateTime(2000), - fileModifiedAt: DateTime(2010), - updatedAt: DateTime.now(), - durationInSeconds: 60, - type: old.AssetType.video, - fileName: "image2.jpg", - isFavorite: false, - isArchived: false, - isTrashed: false, - exifInfo: const ExifInfo(isFlipped: true), - ); - - static final image3 = old.Asset( - checksum: "image3-checksum", - localId: "image3", - ownerId: 1, - fileCreatedAt: DateTime(2025), - fileModifiedAt: DateTime(2025), - updatedAt: DateTime.now(), - durationInSeconds: 60, - type: old.AssetType.image, - fileName: "image3.jpg", - isFavorite: true, - isArchived: false, - isTrashed: false, - ); -} abstract final class LocalAssetStub { const LocalAssetStub._(); diff --git a/mobile/test/fixtures/exif.stub.dart b/mobile/test/fixtures/exif.stub.dart deleted file mode 100644 index 5ad9a41761..0000000000 --- a/mobile/test/fixtures/exif.stub.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:immich_mobile/domain/models/exif.model.dart'; - -abstract final class ExifStub { - static final size = const ExifInfo(assetId: 1, fileSize: 1000); - - static final gps = const ExifInfo( - assetId: 2, - latitude: 20, - longitude: 20, - city: 'city', - state: 'state', - country: 'country', - ); - - static final rotated90CW = const ExifInfo(assetId: 3, orientation: "90"); - - static final rotated270CW = const ExifInfo(assetId: 4, orientation: "-90"); -} diff --git a/mobile/test/fixtures/user.stub.dart b/mobile/test/fixtures/user.stub.dart index 2ba7177f89..b92ba71e5b 100644 --- a/mobile/test/fixtures/user.stub.dart +++ b/mobile/test/fixtures/user.stub.dart @@ -12,24 +12,4 @@ abstract final class UserStub { profileChangedAt: DateTime(2021), avatarColor: AvatarColor.green, ); - - static final user1 = UserDto( - id: "user1", - email: "user1@test.com", - name: "user1", - isAdmin: false, - updatedAt: DateTime(2022), - profileChangedAt: DateTime(2022), - avatarColor: AvatarColor.red, - ); - - static final user2 = UserDto( - id: "user2", - email: "user2@test.com", - name: "user2", - isAdmin: false, - updatedAt: DateTime(2023), - profileChangedAt: DateTime(2023), - avatarColor: AvatarColor.primary, - ); } diff --git a/mobile/test/infrastructure/repositories/exif_repository_test.dart b/mobile/test/infrastructure/repositories/exif_repository_test.dart deleted file mode 100644 index 4e7ee4d79d..0000000000 --- a/mobile/test/infrastructure/repositories/exif_repository_test.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:isar/isar.dart'; - -import '../../fixtures/exif.stub.dart'; -import '../../test_utils.dart'; - -Future _populateExifTable(Isar db) async { - await db.writeTxn(() async { - await db.exifInfos.putAll([ - ExifInfo.fromDto(ExifStub.size), - ExifInfo.fromDto(ExifStub.gps), - ExifInfo.fromDto(ExifStub.rotated90CW), - ExifInfo.fromDto(ExifStub.rotated270CW), - ]); - }); -} - -void main() { - late Isar db; - late IsarExifRepository sut; - - setUp(() async { - db = await TestUtils.initIsar(); - sut = IsarExifRepository(db); - }); - - group("Return with proper orientation", () { - setUp(() async { - await _populateExifTable(db); - }); - - test("isFlipped true for 90CW", () async { - final exif = await sut.get(ExifStub.rotated90CW.assetId!); - expect(exif!.isFlipped, true); - }); - - test("isFlipped true for 270CW", () async { - final exif = await sut.get(ExifStub.rotated270CW.assetId!); - expect(exif!.isFlipped, true); - }); - - test("isFlipped false for the original non-rotated image", () async { - final exif = await sut.get(ExifStub.size.assetId!); - expect(exif!.isFlipped, false); - }); - }); -} diff --git a/mobile/test/infrastructure/repositories/store_repository_test.dart b/mobile/test/infrastructure/repositories/store_repository_test.dart index 18d41e32e0..4cf1adc6b1 100644 --- a/mobile/test/infrastructure/repositories/store_repository_test.dart +++ b/mobile/test/infrastructure/repositories/store_repository_test.dart @@ -1,14 +1,15 @@ import 'dart:async'; +import 'package:drift/drift.dart' hide isNull; +import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:isar/isar.dart'; import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; const _kTestAccessToken = "#TestToken"; final _kTestBackupFailed = DateTime(2025, 2, 20, 11, 45); @@ -16,30 +17,54 @@ const _kTestVersion = 10; const _kTestColorfulInterface = false; final _kTestUser = UserStub.admin; -Future _addIntStoreValue(Isar db, StoreKey key, int? value) async { - await db.storeValues.put(StoreValue(key.id, intValue: value, strValue: null)); -} - -Future _addStrStoreValue(Isar db, StoreKey key, String? value) async { - await db.storeValues.put(StoreValue(key.id, intValue: null, strValue: value)); -} - -Future _populateStore(Isar db) async { - await db.writeTxn(() async { - await _addIntStoreValue(db, StoreKey.colorfulInterface, _kTestColorfulInterface ? 1 : 0); - await _addIntStoreValue(db, StoreKey.backupFailedSince, _kTestBackupFailed.millisecondsSinceEpoch); - await _addStrStoreValue(db, StoreKey.accessToken, _kTestAccessToken); - await _addIntStoreValue(db, StoreKey.version, _kTestVersion); +Future _populateStore(Drift db) async { + await db.batch((batch) async { + batch.insert( + db.storeEntity, + StoreEntityCompanion( + id: Value(StoreKey.colorfulInterface.id), + intValue: const Value(_kTestColorfulInterface ? 1 : 0), + stringValue: const Value(null), + ), + ); + batch.insert( + db.storeEntity, + StoreEntityCompanion( + id: Value(StoreKey.backupFailedSince.id), + intValue: Value(_kTestBackupFailed.millisecondsSinceEpoch), + stringValue: const Value(null), + ), + ); + batch.insert( + db.storeEntity, + StoreEntityCompanion( + id: Value(StoreKey.accessToken.id), + intValue: const Value(null), + stringValue: const Value(_kTestAccessToken), + ), + ); + batch.insert( + db.storeEntity, + StoreEntityCompanion( + id: Value(StoreKey.version.id), + intValue: const Value(_kTestVersion), + stringValue: const Value(null), + ), + ); }); } void main() { - late Isar db; - late IsarStoreRepository sut; + late Drift db; + late DriftStoreRepository sut; setUp(() async { - db = await TestUtils.initIsar(); - sut = IsarStoreRepository(db); + db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); + sut = DriftStoreRepository(db); + }); + + tearDown(() async { + await db.close(); }); group('Store Repository converters:', () { @@ -98,10 +123,10 @@ void main() { }); test('deleteAll()', () async { - final count = await db.storeValues.count(); + final count = await db.storeEntity.count().getSingle(); expect(count, isNot(isZero)); await sut.deleteAll(); - unawaited(expectLater(await db.storeValues.count(), isZero)); + unawaited(expectLater(await db.storeEntity.count().getSingle(), isZero)); }); }); diff --git a/mobile/test/infrastructure/repositories/sync_api_repository_test.dart b/mobile/test/infrastructure/repositories/sync_api_repository_test.dart index 85eebacb14..d538b567bd 100644 --- a/mobile/test/infrastructure/repositories/sync_api_repository_test.dart +++ b/mobile/test/infrastructure/repositories/sync_api_repository_test.dart @@ -1,10 +1,13 @@ import 'dart:async'; import 'dart:convert'; +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:immich_mobile/domain/models/sync_event.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; import 'package:immich_mobile/utils/semver.dart'; @@ -13,7 +16,6 @@ import 'package:openapi/api.dart'; import '../../api.mocks.dart'; import '../../service.mocks.dart'; -import '../../test_utils.dart'; class MockHttpClient extends Mock implements http.Client {} @@ -38,7 +40,8 @@ void main() { late int testBatchSize = 3; setUpAll(() async { - await StoreService.init(storeRepository: IsarStoreRepository(await TestUtils.initIsar())); + final db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); + await StoreService.init(storeRepository: DriftStoreRepository(db)); }); setUp(() { @@ -137,7 +140,7 @@ void main() { bool abortWasCalledInCallback = false; final Completer firstBatchReceived = Completer(); - Future onDataCallback(List events, Function() abort, Function() _) async { + Future onDataCallback(List _, Function() abort, Function() _) async { onDataCallCount++; if (onDataCallCount == 1) { abort(); @@ -241,7 +244,7 @@ void main() { final streamError = Exception("Network Error"); int onDataCallCount = 0; - Future onDataCallback(List events, Function() _, Function() __) async { + Future onDataCallback(List _, Function() _, Function() __) async { onDataCallCount++; } @@ -267,7 +270,7 @@ void main() { when(() => mockStreamedResponse.stream).thenAnswer((_) => http.ByteStream(errorBodyController.stream)); int onDataCallCount = 0; - Future onDataCallback(List events, Function() _, Function() __) async { + Future onDataCallback(List _, Function() _, Function() __) async { onDataCallCount++; } diff --git a/mobile/test/infrastructure/repository.mock.dart b/mobile/test/infrastructure/repository.mock.dart index 2d4af5b308..b7992c1822 100644 --- a/mobile/test/infrastructure/repository.mock.dart +++ b/mobile/test/infrastructure/repository.mock.dart @@ -1,5 +1,4 @@ import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/device_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; @@ -11,22 +10,15 @@ import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.da import 'package:immich_mobile/infrastructure/repositories/sync_migration.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; import 'package:immich_mobile/repositories/upload.repository.dart'; import 'package:mocktail/mocktail.dart'; -class MockStoreRepository extends Mock implements IsarStoreRepository {} - class MockDriftStoreRepository extends Mock implements DriftStoreRepository {} class MockLogRepository extends Mock implements LogRepository {} -class MockIsarUserRepository extends Mock implements IsarUserRepository {} - -class MockDeviceAssetRepository extends Mock implements IsarDeviceAssetRepository {} - class MockSyncStreamRepository extends Mock implements SyncStreamRepository {} class MockLocalAlbumRepository extends Mock implements DriftLocalAlbumRepository {} diff --git a/mobile/test/modules/activity/activities_page_test.dart b/mobile/test/modules/activity/activities_page_test.dart deleted file mode 100644 index 39350530ea..0000000000 --- a/mobile/test/modules/activity/activities_page_test.dart +++ /dev/null @@ -1,175 +0,0 @@ -@Skip('currently failing due to mock HTTP client to download ISAR binaries') -@Tags(['widget']) -library; - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/pages/common/activities.page.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/activities/activity_text_field.dart'; -import 'package:immich_mobile/widgets/activities/dismissible_activity.dart'; -import 'package:isar/isar.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -import '../../fixtures/album.stub.dart'; -import '../../fixtures/asset.stub.dart'; -import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; -import '../../widget_tester_extensions.dart'; -import '../album/album_mocks.dart'; -import '../asset_viewer/asset_viewer_mocks.dart'; -import '../shared/shared_mocks.dart'; -import 'activity_mocks.dart'; - -final _activities = [ - Activity( - id: '1', - createdAt: DateTime(100), - type: ActivityType.comment, - comment: 'First Activity', - assetId: 'asset-2', - user: UserStub.admin, - ), - Activity( - id: '2', - createdAt: DateTime(200), - type: ActivityType.comment, - comment: 'Second Activity', - user: UserStub.user1, - ), - Activity(id: '3', createdAt: DateTime(300), type: ActivityType.like, assetId: 'asset-1', user: UserStub.user2), - Activity(id: '4', createdAt: DateTime(400), type: ActivityType.like, user: UserStub.user1), -]; - -void main() { - late MockAlbumActivity activityMock; - late MockCurrentAlbumProvider mockCurrentAlbumProvider; - late MockCurrentAssetProvider mockCurrentAssetProvider; - late List overrides; - late Isar db; - - setUpAll(() async { - TestUtils.init(); - db = await TestUtils.initIsar(); - await StoreService.init(storeRepository: IsarStoreRepository(db)); - await Store.put(StoreKey.currentUser, UserStub.admin); - await Store.put(StoreKey.serverEndpoint, ''); - await Store.put(StoreKey.accessToken, ''); - }); - - setUp(() async { - mockCurrentAlbumProvider = MockCurrentAlbumProvider(AlbumStub.twoAsset); - mockCurrentAssetProvider = MockCurrentAssetProvider(AssetStub.image1); - activityMock = MockAlbumActivity(_activities); - overrides = [ - albumActivityProvider(AlbumStub.twoAsset.remoteId!, AssetStub.image1.remoteId!).overrideWith(() => activityMock), - currentAlbumProvider.overrideWith(() => mockCurrentAlbumProvider), - currentAssetProvider.overrideWith(() => mockCurrentAssetProvider), - ]; - - await db.writeTxn(() async { - await db.clear(); - // Save all assets - await db.users.put(User.fromDto(UserStub.admin)); - await db.assets.putAll([AssetStub.image1, AssetStub.image2]); - await db.albums.put(AlbumStub.twoAsset); - await AlbumStub.twoAsset.owner.save(); - await AlbumStub.twoAsset.assets.save(); - }); - expect(db.albums.countSync(), 1); - expect(db.assets.countSync(), 2); - expect(db.users.countSync(), 1); - }); - - group("App bar", () { - testWidgets("No title when currentAsset != null", (tester) async { - await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides); - - final listTile = tester.widget(find.byType(AppBar)); - expect(listTile.title, isNull); - }); - - testWidgets("Album name as title when currentAsset == null", (tester) async { - await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides); - await tester.pumpAndSettle(); - - mockCurrentAssetProvider.state = null; - await tester.pumpAndSettle(); - - expect(find.text(AlbumStub.twoAsset.name), findsOneWidget); - final listTile = tester.widget(find.byType(AppBar)); - expect(listTile.title, isNotNull); - }); - }); - - group("Body", () { - testWidgets("Contains a stack with Activity List and Activity Input", (tester) async { - await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides); - await tester.pumpAndSettle(); - - expect(find.descendant(of: find.byType(Stack), matching: find.byType(ActivityTextField)), findsOneWidget); - - expect(find.descendant(of: find.byType(Stack), matching: find.byType(ListView)), findsOneWidget); - }); - - testWidgets("List Contains all dismissible activities", (tester) async { - await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides); - await tester.pumpAndSettle(); - - final listFinder = find.descendant(of: find.byType(Stack), matching: find.byType(ListView)); - final listChildren = find.descendant(of: listFinder, matching: find.byType(DismissibleActivity)); - expect(listChildren, findsNWidgets(_activities.length)); - }); - - testWidgets("Submitting text input adds a comment with the text", (tester) async { - await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides); - await tester.pumpAndSettle(); - - when(() => activityMock.addComment(any())).thenAnswer((_) => Future.value()); - - final textField = find.byType(TextField); - await tester.enterText(textField, 'Test comment'); - await tester.testTextInput.receiveAction(TextInputAction.done); - - verify(() => activityMock.addComment('Test comment')); - }); - - testWidgets("Owner can remove all activities", (tester) async { - await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides); - await tester.pumpAndSettle(); - - final deletableActivityFinder = find.byWidgetPredicate( - (widget) => widget is DismissibleActivity && widget.onDismiss != null, - ); - expect(deletableActivityFinder, findsNWidgets(_activities.length)); - }); - - testWidgets("Non-Owner can remove only their activities", (tester) async { - final mockCurrentUser = MockCurrentUserProvider(); - - await tester.pumpConsumerWidget( - const ActivitiesPage(), - overrides: [...overrides, currentUserProvider.overrideWith((ref) => mockCurrentUser)], - ); - mockCurrentUser.state = UserStub.user1; - await tester.pumpAndSettle(); - - final deletableActivityFinder = find.byWidgetPredicate( - (widget) => widget is DismissibleActivity && widget.onDismiss != null, - ); - expect(deletableActivityFinder, findsNWidgets(_activities.where((a) => a.user == UserStub.user1).length)); - }); - }); -} diff --git a/mobile/test/modules/activity/activity_mocks.dart b/mobile/test/modules/activity/activity_mocks.dart deleted file mode 100644 index c50810795e..0000000000 --- a/mobile/test/modules/activity/activity_mocks.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/activity_statistics.provider.dart'; -import 'package:immich_mobile/services/activity.service.dart'; -import 'package:mocktail/mocktail.dart'; - -class ActivityServiceMock extends Mock implements ActivityService {} - -class MockAlbumActivity extends AlbumActivityInternal with Mock implements AlbumActivity { - List? initActivities; - MockAlbumActivity([this.initActivities]); - - @override - Future> build(String albumId, [String? assetId]) async { - return initActivities ?? []; - } -} - -class ActivityStatisticsMock extends ActivityStatisticsInternal with Mock implements ActivityStatistics {} diff --git a/mobile/test/modules/activity/activity_provider_test.dart b/mobile/test/modules/activity/activity_provider_test.dart deleted file mode 100644 index 84eba62b70..0000000000 --- a/mobile/test/modules/activity/activity_provider_test.dart +++ /dev/null @@ -1,331 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/activity_service.provider.dart'; -import 'package:immich_mobile/providers/activity_statistics.provider.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; -import 'activity_mocks.dart'; - -final _activities = [ - Activity( - id: '1', - createdAt: DateTime(100), - type: ActivityType.comment, - comment: 'First Activity', - assetId: 'asset-2', - user: UserStub.admin, - ), - Activity( - id: '2', - createdAt: DateTime(200), - type: ActivityType.comment, - comment: 'Second Activity', - user: UserStub.user1, - ), - Activity(id: '3', createdAt: DateTime(300), type: ActivityType.like, assetId: 'asset-1', user: UserStub.admin), - Activity(id: '4', createdAt: DateTime(400), type: ActivityType.like, user: UserStub.user1), -]; - -void main() { - late ActivityServiceMock activityMock; - late ActivityStatisticsMock activityStatisticsMock; - late ActivityStatisticsMock albumActivityStatisticsMock; - late ProviderContainer container; - late AlbumActivityProvider provider; - late ListenerMock>> listener; - - setUpAll(() { - registerFallbackValue(AsyncData>([..._activities])); - }); - - setUp(() async { - activityMock = ActivityServiceMock(); - activityStatisticsMock = ActivityStatisticsMock(); - albumActivityStatisticsMock = ActivityStatisticsMock(); - - container = TestUtils.createContainer( - overrides: [ - activityServiceProvider.overrideWith((ref) => activityMock), - activityStatisticsProvider('test-album', 'test-asset').overrideWith(() => activityStatisticsMock), - activityStatisticsProvider('test-album').overrideWith(() => albumActivityStatisticsMock), - ], - ); - - // Mock values - when(() => activityStatisticsMock.build(any(), any())).thenReturn(0); - when(() => albumActivityStatisticsMock.build(any())).thenReturn(0); - when( - () => activityMock.getAllActivities('test-album', assetId: 'test-asset'), - ).thenAnswer((_) async => [..._activities]); - when(() => activityMock.getAllActivities('test-album')).thenAnswer((_) async => [..._activities]); - - // Init and wait for providers future to complete - provider = albumActivityProvider('test-album', 'test-asset'); - listener = ListenerMock(); - container.listen(provider, listener.call, fireImmediately: true); - - await container.read(provider.future); - }); - - test('Returns a list of activity', () async { - verifyInOrder([ - () => listener.call(null, const AsyncLoading()), - () => listener.call( - const AsyncLoading(), - any( - that: allOf([ - isA>>(), - predicate((AsyncData> ad) => ad.requireValue.every((e) => _activities.contains(e))), - ]), - ), - ), - ]); - - verifyNoMoreInteractions(listener); - }); - - group('addLike()', () { - test('Like successfully added', () async { - final like = Activity(id: '5', createdAt: DateTime(2023), type: ActivityType.like, user: UserStub.admin); - - when( - () => activityMock.addActivity('test-album', ActivityType.like, assetId: 'test-asset'), - ).thenAnswer((_) async => AsyncData(like)); - - final albumProvider = albumActivityProvider('test-album'); - container.read(albumProvider.notifier); - await container.read(albumProvider.future); - - await container.read(provider.notifier).addLike(); - - verify(() => activityMock.addActivity('test-album', ActivityType.like, assetId: 'test-asset')); - - final activities = await container.read(provider.future); - expect(activities, hasLength(5)); - expect(activities, contains(like)); - - // Never bump activity count for new likes - verifyNever(() => activityStatisticsMock.addActivity()); - verifyNever(() => albumActivityStatisticsMock.addActivity()); - - final albumActivities = container.read(albumProvider).requireValue; - expect(albumActivities, hasLength(5)); - expect(albumActivities, contains(like)); - }); - - test('Like failed', () async { - final like = Activity(id: '5', createdAt: DateTime(2023), type: ActivityType.like, user: UserStub.admin); - when( - () => activityMock.addActivity('test-album', ActivityType.like, assetId: 'test-asset'), - ).thenAnswer((_) async => AsyncError(Exception('Mock'), StackTrace.current)); - - final albumProvider = albumActivityProvider('test-album'); - container.read(albumProvider.notifier); - await container.read(albumProvider.future); - - await container.read(provider.notifier).addLike(); - - verify(() => activityMock.addActivity('test-album', ActivityType.like, assetId: 'test-asset')); - - final activities = await container.read(provider.future); - expect(activities, hasLength(4)); - expect(activities, isNot(contains(like))); - - verifyNever(() => albumActivityStatisticsMock.addActivity()); - - final albumActivities = container.read(albumProvider).requireValue; - expect(albumActivities, hasLength(4)); - expect(albumActivities, isNot(contains(like))); - }); - }); - - group('removeActivity()', () { - test('Like successfully removed', () async { - when(() => activityMock.removeActivity('3')).thenAnswer((_) async => true); - - await container.read(provider.notifier).removeActivity('3'); - - verify(() => activityMock.removeActivity('3')); - - final activities = await container.read(provider.future); - expect(activities, hasLength(3)); - expect(activities, isNot(anyElement(predicate((Activity a) => a.id == '3')))); - - verifyNever(() => activityStatisticsMock.removeActivity()); - verifyNever(() => albumActivityStatisticsMock.removeActivity()); - }); - - test('Remove Like failed', () async { - when(() => activityMock.removeActivity('3')).thenAnswer((_) async => false); - - await container.read(provider.notifier).removeActivity('3'); - - final activities = await container.read(provider.future); - expect(activities, hasLength(4)); - expect(activities, anyElement(predicate((Activity a) => a.id == '3'))); - - verifyNever(() => activityStatisticsMock.removeActivity()); - verifyNever(() => albumActivityStatisticsMock.removeActivity()); - }); - - test('Comment successfully removed', () async { - when(() => activityMock.removeActivity('1')).thenAnswer((_) async => true); - - await container.read(provider.notifier).removeActivity('1'); - - final activities = await container.read(provider.future); - expect(activities, isNot(anyElement(predicate((Activity a) => a.id == '1')))); - - verify(() => activityStatisticsMock.removeActivity()); - verify(() => albumActivityStatisticsMock.removeActivity()); - }); - - test('Removes activity from album state when asset scoped', () async { - when(() => activityMock.removeActivity('3')).thenAnswer((_) async => true); - when(() => activityMock.getAllActivities('test-album')).thenAnswer((_) async => [..._activities]); - - final albumProvider = albumActivityProvider('test-album'); - container.read(albumProvider.notifier); - await container.read(albumProvider.future); - - await container.read(provider.notifier).removeActivity('3'); - - final assetActivities = container.read(provider).requireValue; - final albumActivities = container.read(albumProvider).requireValue; - - expect(assetActivities, hasLength(3)); - expect(assetActivities, isNot(anyElement(predicate((Activity a) => a.id == '3')))); - - expect(albumActivities, hasLength(3)); - expect(albumActivities, isNot(anyElement(predicate((Activity a) => a.id == '3')))); - - verify(() => activityMock.removeActivity('3')); - verifyNever(() => activityStatisticsMock.removeActivity()); - verifyNever(() => albumActivityStatisticsMock.removeActivity()); - }); - }); - - group('addComment()', () { - test('Comment successfully added', () async { - final comment = Activity( - id: '5', - createdAt: DateTime(2023), - type: ActivityType.comment, - user: UserStub.admin, - comment: 'Test-Comment', - assetId: 'test-asset', - ); - - final albumProvider = albumActivityProvider('test-album'); - container.read(albumProvider.notifier); - await container.read(albumProvider.future); - - when( - () => activityMock.addActivity( - 'test-album', - ActivityType.comment, - assetId: 'test-asset', - comment: 'Test-Comment', - ), - ).thenAnswer((_) async => AsyncData(comment)); - when(() => activityStatisticsMock.build('test-album', 'test-asset')).thenReturn(4); - when(() => albumActivityStatisticsMock.build('test-album')).thenReturn(2); - - await container.read(provider.notifier).addComment('Test-Comment'); - - verify( - () => activityMock.addActivity( - 'test-album', - ActivityType.comment, - assetId: 'test-asset', - comment: 'Test-Comment', - ), - ); - - final activities = await container.read(provider.future); - expect(activities, hasLength(5)); - expect(activities, contains(comment)); - - verify(() => activityStatisticsMock.addActivity()); - verify(() => albumActivityStatisticsMock.addActivity()); - - final albumActivities = container.read(albumProvider).requireValue; - expect(albumActivities, hasLength(5)); - expect(albumActivities, contains(comment)); - }); - - test('Comment successfully added without assetId', () async { - final comment = Activity( - id: '5', - createdAt: DateTime(2023), - type: ActivityType.comment, - user: UserStub.admin, - assetId: 'test-asset', - comment: 'Test-Comment', - ); - - when( - () => activityMock.addActivity('test-album', ActivityType.comment, comment: 'Test-Comment'), - ).thenAnswer((_) async => AsyncData(comment)); - when(() => albumActivityStatisticsMock.build('test-album')).thenReturn(2); - when(() => activityMock.getAllActivities('test-album')).thenAnswer((_) async => [..._activities]); - - final albumProvider = albumActivityProvider('test-album'); - container.read(albumProvider.notifier); - await container.read(albumProvider.future); - await container.read(albumProvider.notifier).addComment('Test-Comment'); - - verify( - () => activityMock.addActivity('test-album', ActivityType.comment, assetId: null, comment: 'Test-Comment'), - ); - - final activities = await container.read(albumProvider.future); - expect(activities, hasLength(5)); - expect(activities, contains(comment)); - - verifyNever(() => activityStatisticsMock.addActivity()); - verify(() => albumActivityStatisticsMock.addActivity()); - }); - - test('Comment failed', () async { - final comment = Activity( - id: '5', - createdAt: DateTime(2023), - type: ActivityType.comment, - user: UserStub.admin, - comment: 'Test-Comment', - assetId: 'test-asset', - ); - - when( - () => activityMock.addActivity( - 'test-album', - ActivityType.comment, - assetId: 'test-asset', - comment: 'Test-Comment', - ), - ).thenAnswer((_) async => AsyncError(Exception('Error'), StackTrace.current)); - - final albumProvider = albumActivityProvider('test-album'); - container.read(albumProvider.notifier); - await container.read(albumProvider.future); - - await container.read(provider.notifier).addComment('Test-Comment'); - - final activities = await container.read(provider.future); - expect(activities, hasLength(4)); - expect(activities, isNot(contains(comment))); - - verifyNever(() => activityStatisticsMock.addActivity()); - verifyNever(() => albumActivityStatisticsMock.addActivity()); - - final albumActivities = container.read(albumProvider).requireValue; - expect(albumActivities, hasLength(4)); - expect(albumActivities, isNot(contains(comment))); - }); - }); -} diff --git a/mobile/test/modules/activity/activity_statistics_provider_test.dart b/mobile/test/modules/activity/activity_statistics_provider_test.dart deleted file mode 100644 index 7fe73868f5..0000000000 --- a/mobile/test/modules/activity/activity_statistics_provider_test.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/providers/activity_service.provider.dart'; -import 'package:immich_mobile/providers/activity_statistics.provider.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../test_utils.dart'; -import 'activity_mocks.dart'; - -void main() { - late ActivityServiceMock activityMock; - late ProviderContainer container; - late ListenerMock listener; - - setUp(() async { - activityMock = ActivityServiceMock(); - container = TestUtils.createContainer(overrides: [activityServiceProvider.overrideWith((ref) => activityMock)]); - listener = ListenerMock(); - }); - - test('Returns the proper count family', () async { - when( - () => activityMock.getStatistics('test-album', assetId: 'test-asset'), - ).thenAnswer((_) async => const ActivityStats(comments: 5)); - - // Read here to make the getStatistics call - container.read(activityStatisticsProvider('test-album', 'test-asset')); - - container.listen(activityStatisticsProvider('test-album', 'test-asset'), listener.call, fireImmediately: true); - - // Sleep for the getStatistics future to resolve - await Future.delayed(const Duration(milliseconds: 1)); - - verifyInOrder([() => listener.call(null, 0), () => listener.call(0, 5)]); - - verifyNoMoreInteractions(listener); - }); - - test('Adds activity', () async { - when(() => activityMock.getStatistics('test-album')).thenAnswer((_) async => const ActivityStats(comments: 10)); - - final provider = activityStatisticsProvider('test-album'); - container.listen(provider, listener.call, fireImmediately: true); - - // Sleep for the getStatistics future to resolve - await Future.delayed(const Duration(milliseconds: 1)); - - container.read(provider.notifier).addActivity(); - container.read(provider.notifier).addActivity(); - - expect(container.read(provider), 12); - }); - - test('Removes activity', () async { - when( - () => activityMock.getStatistics('new-album', assetId: 'test-asset'), - ).thenAnswer((_) async => const ActivityStats(comments: 10)); - - final provider = activityStatisticsProvider('new-album', 'test-asset'); - container.listen(provider, listener.call, fireImmediately: true); - - // Sleep for the getStatistics future to resolve - await Future.delayed(const Duration(milliseconds: 1)); - - container.read(provider.notifier).removeActivity(); - container.read(provider.notifier).removeActivity(); - - expect(container.read(provider), 8); - }); -} diff --git a/mobile/test/modules/activity/activity_text_field_test.dart b/mobile/test/modules/activity/activity_text_field_test.dart deleted file mode 100644 index 4f4a2c7068..0000000000 --- a/mobile/test/modules/activity/activity_text_field_test.dart +++ /dev/null @@ -1,149 +0,0 @@ -@Skip('currently failing due to mock HTTP client to download ISAR binaries') -@Tags(['widget']) -library; - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/activities/activity_text_field.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; -import 'package:isar/isar.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -import '../../fixtures/album.stub.dart'; -import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; -import '../../widget_tester_extensions.dart'; -import '../album/album_mocks.dart'; -import '../shared/shared_mocks.dart'; -import 'activity_mocks.dart'; - -void main() { - late Isar db; - late MockCurrentAlbumProvider mockCurrentAlbumProvider; - late MockAlbumActivity activityMock; - late List overrides; - - setUpAll(() async { - TestUtils.init(); - db = await TestUtils.initIsar(); - await StoreService.init(storeRepository: IsarStoreRepository(db)); - await Store.put(StoreKey.currentUser, UserStub.admin); - await Store.put(StoreKey.serverEndpoint, ''); - }); - - setUp(() { - mockCurrentAlbumProvider = MockCurrentAlbumProvider(AlbumStub.twoAsset); - activityMock = MockAlbumActivity(); - overrides = [ - currentAlbumProvider.overrideWith(() => mockCurrentAlbumProvider), - albumActivityProvider(AlbumStub.twoAsset.remoteId!).overrideWith(() => activityMock), - ]; - }); - - testWidgets('Returns an Input text field', (tester) async { - await tester.pumpConsumerWidget(ActivityTextField(onSubmit: (_) {}), overrides: overrides); - - expect(find.byType(TextField), findsOneWidget); - }); - - testWidgets('No UserCircleAvatar when user == null', (tester) async { - final userProvider = MockCurrentUserProvider(); - - await tester.pumpConsumerWidget( - ActivityTextField(onSubmit: (_) {}), - overrides: [currentUserProvider.overrideWith((ref) => userProvider), ...overrides], - ); - - expect(find.byType(UserCircleAvatar), findsNothing); - }); - - testWidgets('UserCircleAvatar displayed when user != null', (tester) async { - await tester.pumpConsumerWidget(ActivityTextField(onSubmit: (_) {}), overrides: overrides); - - expect(find.byType(UserCircleAvatar), findsOneWidget); - }); - - testWidgets('Filled icon if likedId != null', (tester) async { - await tester.pumpConsumerWidget( - ActivityTextField(onSubmit: (_) {}, likeId: '1'), - overrides: overrides, - ); - - expect(find.widgetWithIcon(IconButton, Icons.thumb_up), findsOneWidget); - expect(find.widgetWithIcon(IconButton, Icons.thumb_up_off_alt), findsNothing); - }); - - testWidgets('Bordered icon if likedId == null', (tester) async { - await tester.pumpConsumerWidget(ActivityTextField(onSubmit: (_) {}), overrides: overrides); - - expect(find.widgetWithIcon(IconButton, Icons.thumb_up_off_alt), findsOneWidget); - expect(find.widgetWithIcon(IconButton, Icons.thumb_up), findsNothing); - }); - - testWidgets('Adds new like', (tester) async { - await tester.pumpConsumerWidget(ActivityTextField(onSubmit: (_) {}), overrides: overrides); - - when(() => activityMock.addLike()).thenAnswer((_) => Future.value()); - - final suffixIcon = find.byType(IconButton); - await tester.tap(suffixIcon); - - verify(() => activityMock.addLike()); - }); - - testWidgets('Removes like if already liked', (tester) async { - await tester.pumpConsumerWidget( - ActivityTextField(onSubmit: (_) {}, likeId: 'test-suffix'), - overrides: overrides, - ); - - when(() => activityMock.removeActivity(any())).thenAnswer((_) => Future.value()); - - final suffixIcon = find.byType(IconButton); - await tester.tap(suffixIcon); - - verify(() => activityMock.removeActivity('test-suffix')); - }); - - testWidgets('Passes text entered to onSubmit on submit', (tester) async { - String? receivedText; - - await tester.pumpConsumerWidget( - ActivityTextField(onSubmit: (text) => receivedText = text, likeId: 'test-suffix'), - overrides: overrides, - ); - - final textField = find.byType(TextField); - await tester.enterText(textField, 'This is a test comment'); - await tester.testTextInput.receiveAction(TextInputAction.done); - expect(receivedText, 'This is a test comment'); - }); - - testWidgets('Input disabled when isEnabled false', (tester) async { - String? receviedText; - - await tester.pumpConsumerWidget( - ActivityTextField(onSubmit: (text) => receviedText = text, isEnabled: false, likeId: 'test-suffix'), - overrides: overrides, - ); - - final suffixIcon = find.byType(IconButton); - await tester.tap(suffixIcon, warnIfMissed: false); - - final textField = find.byType(TextField); - await tester.enterText(textField, 'This is a test comment'); - await tester.testTextInput.receiveAction(TextInputAction.done); - - expect(receviedText, isNull); - verifyNever(() => activityMock.addLike()); - verifyNever(() => activityMock.removeActivity(any())); - }); -} diff --git a/mobile/test/modules/activity/activity_tile_test.dart b/mobile/test/modules/activity/activity_tile_test.dart deleted file mode 100644 index 538e3c0911..0000000000 --- a/mobile/test/modules/activity/activity_tile_test.dart +++ /dev/null @@ -1,165 +0,0 @@ -@Skip('currently failing due to mock HTTP client to download ISAR binaries') -@Tags(['widget']) -library; - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/widgets/activities/activity_tile.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; -import 'package:isar/isar.dart'; - -import '../../fixtures/asset.stub.dart'; -import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; -import '../../widget_tester_extensions.dart'; -import '../asset_viewer/asset_viewer_mocks.dart'; - -void main() { - late MockCurrentAssetProvider assetProvider; - late List overrides; - late Isar db; - - setUpAll(() async { - TestUtils.init(); - db = await TestUtils.initIsar(); - // For UserCircleAvatar - await StoreService.init(storeRepository: IsarStoreRepository(db)); - await Store.put(StoreKey.currentUser, UserStub.admin); - await Store.put(StoreKey.serverEndpoint, ''); - await Store.put(StoreKey.accessToken, ''); - }); - - setUp(() { - assetProvider = MockCurrentAssetProvider(); - overrides = [currentAssetProvider.overrideWith(() => assetProvider)]; - }); - - testWidgets('Returns a ListTile', (tester) async { - await tester.pumpConsumerWidget( - ActivityTile(Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin)), - overrides: overrides, - ); - - expect(find.byType(ListTile), findsOneWidget); - }); - - testWidgets('No trailing widget when activity assetId == null', (tester) async { - await tester.pumpConsumerWidget( - ActivityTile(Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin)), - overrides: overrides, - ); - - final listTile = tester.widget(find.byType(ListTile)); - expect(listTile.trailing, isNull); - }); - - testWidgets('Asset Thumbanil as trailing widget when activity assetId != null', (tester) async { - await tester.pumpConsumerWidget( - ActivityTile( - Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin, assetId: '1'), - ), - overrides: overrides, - ); - - final listTile = tester.widget(find.byType(ListTile)); - expect(listTile.trailing, isNotNull); - // TODO: Validate this to be the common class after migrating ActivityTile#_ActivityAssetThumbnail to a common class - }); - - testWidgets('No trailing widget when current asset != null', (tester) async { - await tester.pumpConsumerWidget( - ActivityTile( - Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin, assetId: '1'), - ), - overrides: overrides, - ); - - assetProvider.state = AssetStub.image1; - await tester.pumpAndSettle(); - - final listTile = tester.widget(find.byType(ListTile)); - expect(listTile.trailing, isNull); - }); - - group('Like Activity', () { - final activity = Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin); - - testWidgets('Like contains filled thumbs-up as leading', (tester) async { - await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides); - - // Leading widget should not be null - final listTile = tester.widget(find.byType(ListTile)); - expect(listTile.leading, isNotNull); - - // And should have a thumb_up icon - final thumbUpIconFinder = find.widgetWithIcon(listTile.leading!.runtimeType, Icons.thumb_up); - - expect(thumbUpIconFinder, findsOneWidget); - }); - - testWidgets('Like title is center aligned', (tester) async { - await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides); - - final listTile = tester.widget(find.byType(ListTile)); - - expect(listTile.titleAlignment, ListTileTitleAlignment.center); - }); - - testWidgets('No subtitle for likes', (tester) async { - await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides); - - final listTile = tester.widget(find.byType(ListTile)); - - expect(listTile.subtitle, isNull); - }); - }); - - group('Comment Activity', () { - final activity = Activity( - id: '1', - createdAt: DateTime(100), - type: ActivityType.comment, - comment: 'This is a test comment', - user: UserStub.admin, - ); - - testWidgets('Comment contains User Circle Avatar as leading', (tester) async { - await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides); - - final userAvatarFinder = find.byType(UserCircleAvatar); - expect(userAvatarFinder, findsOneWidget); - - // Leading widget should not be null - final listTile = tester.widget(find.byType(ListTile)); - expect(listTile.leading, isNotNull); - - // Make sure that the leading widget is the UserCircleAvatar - final userAvatar = tester.widget(userAvatarFinder); - expect(listTile.leading, userAvatar); - }); - - testWidgets('Comment title is top aligned', (tester) async { - await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides); - - final listTile = tester.widget(find.byType(ListTile)); - - expect(listTile.titleAlignment, ListTileTitleAlignment.top); - }); - - testWidgets('Contains comment text as subtitle', (tester) async { - await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides); - - final listTile = tester.widget(find.byType(ListTile)); - - expect(listTile.subtitle, isNotNull); - expect(find.descendant(of: find.byType(ListTile), matching: find.text(activity.comment!)), findsOneWidget); - }); - }); -} diff --git a/mobile/test/modules/activity/dismissible_activity_test.dart b/mobile/test/modules/activity/dismissible_activity_test.dart deleted file mode 100644 index 32516e73ea..0000000000 --- a/mobile/test/modules/activity/dismissible_activity_test.dart +++ /dev/null @@ -1,99 +0,0 @@ -@Tags(['widget']) -library; - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/widgets/activities/activity_tile.dart'; -import 'package:immich_mobile/widgets/activities/dismissible_activity.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; -import '../../widget_tester_extensions.dart'; -import '../asset_viewer/asset_viewer_mocks.dart'; - -final activity = Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin); - -void main() { - late MockCurrentAssetProvider assetProvider; - late List overrides; - - setUpAll(() => TestUtils.init()); - - setUp(() { - assetProvider = MockCurrentAssetProvider(); - overrides = [currentAssetProvider.overrideWith(() => assetProvider)]; - }); - - testWidgets('Returns a Dismissible', (tester) async { - await tester.pumpConsumerWidget( - DismissibleActivity('1', ActivityTile(activity), onDismiss: (_) {}), - overrides: overrides, - ); - - expect(find.byType(Dismissible), findsOneWidget); - }); - - testWidgets('Dialog displayed when onDismiss is set', (tester) async { - await tester.pumpConsumerWidget( - DismissibleActivity('1', ActivityTile(activity), onDismiss: (_) {}), - overrides: overrides, - ); - - final dismissible = find.byType(Dismissible); - await tester.drag(dismissible, const Offset(500, 0)); - await tester.pumpAndSettle(); - - expect(find.byType(ConfirmDialog), findsOneWidget); - }); - - testWidgets('Ok action in ConfirmDialog should call onDismiss with activityId', (tester) async { - String? receivedActivityId; - await tester.pumpConsumerWidget( - DismissibleActivity('1', ActivityTile(activity), onDismiss: (id) => receivedActivityId = id), - overrides: overrides, - ); - - final dismissible = find.byType(Dismissible); - await tester.drag(dismissible, const Offset(-500, 0)); - await tester.pumpAndSettle(); - - final okButton = find.text('delete'); - await tester.tap(okButton); - await tester.pumpAndSettle(); - - expect(receivedActivityId, '1'); - }); - - testWidgets('Delete icon for background if onDismiss is set', (tester) async { - await tester.pumpConsumerWidget( - DismissibleActivity('1', ActivityTile(activity), onDismiss: (_) {}), - overrides: overrides, - ); - - final dismissible = find.byType(Dismissible); - await tester.drag(dismissible, const Offset(500, 0)); - await tester.pumpAndSettle(); - - expect(find.byIcon(Icons.delete_sweep_rounded), findsOneWidget); - }); - - testWidgets('No delete dialog if onDismiss is not set', (tester) async { - await tester.pumpConsumerWidget(DismissibleActivity('1', ActivityTile(activity)), overrides: overrides); - - // When onDismiss is not set, the widget should not be wrapped by a Dismissible - expect(find.byType(Dismissible), findsNothing); - expect(find.byType(ConfirmDialog), findsNothing); - }); - - testWidgets('No icon for background if onDismiss is not set', (tester) async { - await tester.pumpConsumerWidget(DismissibleActivity('1', ActivityTile(activity)), overrides: overrides); - - // No Dismissible should exist when onDismiss is not provided, so no delete icon either - expect(find.byType(Dismissible), findsNothing); - expect(find.byIcon(Icons.delete_sweep_rounded), findsNothing); - }); -} diff --git a/mobile/test/modules/album/album_mocks.dart b/mobile/test/modules/album/album_mocks.dart deleted file mode 100644 index 7a1b76e0c7..0000000000 --- a/mobile/test/modules/album/album_mocks.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockCurrentAlbumProvider extends CurrentAlbum with Mock implements CurrentAlbumInternal { - Album? initAlbum; - MockCurrentAlbumProvider([this.initAlbum]); - - @override - Album? build() { - return initAlbum; - } -} diff --git a/mobile/test/modules/album/album_sort_by_options_provider_test.dart b/mobile/test/modules/album/album_sort_by_options_provider_test.dart deleted file mode 100644 index a35255bc21..0000000000 --- a/mobile/test/modules/album/album_sort_by_options_provider_test.dart +++ /dev/null @@ -1,270 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:isar/isar.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../fixtures/album.stub.dart'; -import '../../fixtures/asset.stub.dart'; -import '../../test_utils.dart'; -import '../settings/settings_mocks.dart'; - -void main() { - /// Verify the sort modes - group("AlbumSortMode", () { - late final Isar db; - - setUpAll(() async { - db = await TestUtils.initIsar(); - }); - - final albums = [AlbumStub.emptyAlbum, AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.twoAsset]; - - setUp(() { - db.writeTxnSync(() { - db.clearSync(); - // Save all assets - db.assets.putAllSync([AssetStub.image1, AssetStub.image2]); - db.albums.putAllSync(albums); - for (final album in albums) { - album.sharedUsers.saveSync(); - album.assets.saveSync(); - } - }); - expect(db.albums.countSync(), 4); - expect(db.assets.countSync(), 2); - }); - - group("Album sort - Created Time", () { - const created = AlbumSortMode.created; - test("Created time - ASC", () { - final sorted = created.sortFn(albums, false); - final sortedList = [AlbumStub.emptyAlbum, AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser]; - expect(sorted, orderedEquals(sortedList)); - }); - - test("Created time - DESC", () { - final sorted = created.sortFn(albums, true); - final sortedList = [AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.twoAsset, AlbumStub.emptyAlbum]; - expect(sorted, orderedEquals(sortedList)); - }); - }); - - group("Album sort - Asset count", () { - const assetCount = AlbumSortMode.assetCount; - test("Asset Count - ASC", () { - final sorted = assetCount.sortFn(albums, false); - final sortedList = [AlbumStub.emptyAlbum, AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.twoAsset]; - expect(sorted, orderedEquals(sortedList)); - }); - - test("Asset Count - DESC", () { - final sorted = assetCount.sortFn(albums, true); - final sortedList = [AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser, AlbumStub.emptyAlbum]; - expect(sorted, orderedEquals(sortedList)); - }); - }); - - group("Album sort - Last modified", () { - const lastModified = AlbumSortMode.lastModified; - test("Last modified - ASC", () { - final sorted = lastModified.sortFn(albums, false); - final sortedList = [AlbumStub.twoAsset, AlbumStub.emptyAlbum, AlbumStub.sharedWithUser, AlbumStub.oneAsset]; - expect(sorted, orderedEquals(sortedList)); - }); - - test("Last modified - DESC", () { - final sorted = lastModified.sortFn(albums, true); - final sortedList = [AlbumStub.oneAsset, AlbumStub.sharedWithUser, AlbumStub.emptyAlbum, AlbumStub.twoAsset]; - expect(sorted, orderedEquals(sortedList)); - }); - }); - - group("Album sort - Created", () { - const created = AlbumSortMode.created; - test("Created - ASC", () { - final sorted = created.sortFn(albums, false); - final sortedList = [AlbumStub.emptyAlbum, AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser]; - expect(sorted, orderedEquals(sortedList)); - }); - - test("Created - DESC", () { - final sorted = created.sortFn(albums, true); - final sortedList = [AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.twoAsset, AlbumStub.emptyAlbum]; - expect(sorted, orderedEquals(sortedList)); - }); - }); - - group("Album sort - Most Recent", () { - const mostRecent = AlbumSortMode.mostRecent; - - test("Most Recent - DESC", () { - final sorted = mostRecent.sortFn([ - AlbumStub.create2020end2020Album, - AlbumStub.create2020end2022Album, - AlbumStub.create2020end2024Album, - AlbumStub.create2020end2026Album, - ], false); - final sortedList = [ - AlbumStub.create2020end2026Album, - AlbumStub.create2020end2024Album, - AlbumStub.create2020end2022Album, - AlbumStub.create2020end2020Album, - ]; - expect(sorted, orderedEquals(sortedList)); - }); - - test("Most Recent - ASC", () { - final sorted = mostRecent.sortFn([ - AlbumStub.create2020end2020Album, - AlbumStub.create2020end2022Album, - AlbumStub.create2020end2024Album, - AlbumStub.create2020end2026Album, - ], true); - final sortedList = [ - AlbumStub.create2020end2020Album, - AlbumStub.create2020end2022Album, - AlbumStub.create2020end2024Album, - AlbumStub.create2020end2026Album, - ]; - expect(sorted, orderedEquals(sortedList)); - }); - }); - - group("Album sort - Most Oldest", () { - const mostOldest = AlbumSortMode.mostOldest; - - test("Most Oldest - ASC", () { - final sorted = mostOldest.sortFn(albums, false); - final sortedList = [AlbumStub.twoAsset, AlbumStub.emptyAlbum, AlbumStub.oneAsset, AlbumStub.sharedWithUser]; - expect(sorted, orderedEquals(sortedList)); - }); - - test("Most Oldest - DESC", () { - final sorted = mostOldest.sortFn(albums, true); - final sortedList = [AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.emptyAlbum, AlbumStub.twoAsset]; - expect(sorted, orderedEquals(sortedList)); - }); - }); - }); - - /// Verify the sort mode provider - group('AlbumSortByOptions', () { - late AppSettingsService settingsMock; - late ProviderContainer container; - - setUp(() async { - settingsMock = MockAppSettingsService(); - container = TestUtils.createContainer( - overrides: [appSettingsServiceProvider.overrideWith((ref) => settingsMock)], - ); - when( - () => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortReverse, any()), - ).thenAnswer((_) async => {}); - when( - () => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortOrder, any()), - ).thenAnswer((_) async => {}); - }); - - test('Returns the default sort mode when none set', () { - // Returns the default value when nothing is set - when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortOrder)).thenReturn(0); - - expect(container.read(albumSortByOptionsProvider), AlbumSortMode.created); - }); - - test('Returns the correct sort mode with index from Store', () { - // Returns the default value when nothing is set - when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortOrder)).thenReturn(3); - - expect(container.read(albumSortByOptionsProvider), AlbumSortMode.lastModified); - }); - - test('Properly saves the correct store index of sort mode', () { - container.read(albumSortByOptionsProvider.notifier).changeSortMode(AlbumSortMode.mostOldest); - - verify( - () => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortOrder, AlbumSortMode.mostOldest.storeIndex), - ); - }); - - test('Notifies listeners on state change', () { - when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortOrder)).thenReturn(0); - - final listener = ListenerMock(); - container.listen(albumSortByOptionsProvider, listener.call, fireImmediately: true); - - // Created -> Most Oldest - container.read(albumSortByOptionsProvider.notifier).changeSortMode(AlbumSortMode.mostOldest); - - // Most Oldest -> Title - container.read(albumSortByOptionsProvider.notifier).changeSortMode(AlbumSortMode.title); - - verifyInOrder([ - () => listener.call(null, AlbumSortMode.created), - () => listener.call(AlbumSortMode.created, AlbumSortMode.mostOldest), - () => listener.call(AlbumSortMode.mostOldest, AlbumSortMode.title), - ]); - - verifyNoMoreInteractions(listener); - }); - }); - - /// Verify the sort order provider - group('AlbumSortOrder', () { - late AppSettingsService settingsMock; - late ProviderContainer container; - - registerFallbackValue(AppSettingsEnum.selectedAlbumSortReverse); - - setUp(() async { - settingsMock = MockAppSettingsService(); - container = TestUtils.createContainer( - overrides: [appSettingsServiceProvider.overrideWith((ref) => settingsMock)], - ); - when( - () => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortReverse, any()), - ).thenAnswer((_) async => {}); - when( - () => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortOrder, any()), - ).thenAnswer((_) async => {}); - }); - - test('Returns the default sort order when none set - false', () { - when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortReverse)).thenReturn(false); - - expect(container.read(albumSortOrderProvider), isFalse); - }); - - test('Properly saves the correct order', () { - container.read(albumSortOrderProvider.notifier).changeSortDirection(true); - - verify(() => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortReverse, true)); - }); - - test('Notifies listeners on state change', () { - when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortReverse)).thenReturn(false); - - final listener = ListenerMock(); - container.listen(albumSortOrderProvider, listener.call, fireImmediately: true); - - // false -> true - container.read(albumSortOrderProvider.notifier).changeSortDirection(true); - - // true -> false - container.read(albumSortOrderProvider.notifier).changeSortDirection(false); - - verifyInOrder([ - () => listener.call(null, false), - () => listener.call(false, true), - () => listener.call(true, false), - ]); - - verifyNoMoreInteractions(listener); - }); - }); -} diff --git a/mobile/test/modules/asset_viewer/asset_viewer_mocks.dart b/mobile/test/modules/asset_viewer/asset_viewer_mocks.dart deleted file mode 100644 index 89b06d3c09..0000000000 --- a/mobile/test/modules/asset_viewer/asset_viewer_mocks.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockCurrentAssetProvider extends CurrentAssetInternal with Mock implements CurrentAsset { - Asset? initAsset; - MockCurrentAssetProvider([this.initAsset]); - - @override - Asset? build() { - return initAsset; - } -} diff --git a/mobile/test/modules/extensions/asset_extensions_test.dart b/mobile/test/modules/extensions/asset_extensions_test.dart deleted file mode 100644 index 2b9b740ca7..0000000000 --- a/mobile/test/modules/extensions/asset_extensions_test.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asset_extensions.dart'; -import 'package:timezone/data/latest.dart'; -import 'package:timezone/timezone.dart'; - -ExifInfo makeExif({DateTime? dateTimeOriginal, String? timeZone}) { - return ExifInfo(dateTimeOriginal: dateTimeOriginal, timeZone: timeZone); -} - -Asset makeAsset({required String id, required DateTime createdAt, ExifInfo? exifInfo}) { - return Asset( - checksum: '', - localId: id, - remoteId: id, - ownerId: 1, - fileCreatedAt: createdAt, - fileModifiedAt: DateTime.now(), - updatedAt: DateTime.now(), - durationInSeconds: 0, - type: AssetType.image, - fileName: id, - isFavorite: false, - isArchived: false, - isTrashed: false, - exifInfo: exifInfo, - ); -} - -void main() { - // Init Timezone DB - initializeTimeZones(); - - group("Returns local time and offset if no exifInfo", () { - test('returns createdAt directly if in local', () { - final createdAt = DateTime(2023, 12, 12, 12, 12, 12); - final a = makeAsset(id: '1', createdAt: createdAt); - final (dt, tz) = a.getTZAdjustedTimeAndOffset(); - - expect(createdAt, dt); - expect(createdAt.timeZoneOffset, tz); - }); - - test('returns createdAt in local if in utc', () { - final createdAt = DateTime.utc(2023, 12, 12, 12, 12, 12); - final a = makeAsset(id: '1', createdAt: createdAt); - final (dt, tz) = a.getTZAdjustedTimeAndOffset(); - - final localCreatedAt = createdAt.toLocal(); - expect(localCreatedAt, dt); - expect(localCreatedAt.timeZoneOffset, tz); - }); - }); - - group("Returns dateTimeOriginal", () { - test('Returns dateTimeOriginal in UTC from exifInfo without timezone', () { - final createdAt = DateTime.parse("2023-01-27T14:00:00-0500"); - final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530"); - final e = makeExif(dateTimeOriginal: dateTimeOriginal); - final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e); - final (dt, tz) = a.getTZAdjustedTimeAndOffset(); - - final dateTimeInUTC = dateTimeOriginal.toUtc(); - expect(dateTimeInUTC, dt); - expect(dateTimeInUTC.timeZoneOffset, tz); - }); - - test('Returns dateTimeOriginal in UTC from exifInfo with invalid timezone', () { - final createdAt = DateTime.parse("2023-01-27T14:00:00-0500"); - final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530"); - final e = makeExif(dateTimeOriginal: dateTimeOriginal, timeZone: "#_#"); // Invalid timezone - final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e); - final (dt, tz) = a.getTZAdjustedTimeAndOffset(); - - final dateTimeInUTC = dateTimeOriginal.toUtc(); - expect(dateTimeInUTC, dt); - expect(dateTimeInUTC.timeZoneOffset, tz); - }); - }); - - group("Returns adjusted time if timezone available", () { - test('With timezone as location', () { - final createdAt = DateTime.parse("2023-01-27T14:00:00-0500"); - final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530"); - const location = "Asia/Hong_Kong"; - final e = makeExif(dateTimeOriginal: dateTimeOriginal, timeZone: location); - final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e); - final (dt, tz) = a.getTZAdjustedTimeAndOffset(); - - final adjustedTime = TZDateTime.from(dateTimeOriginal.toUtc(), getLocation(location)); - expect(adjustedTime, dt); - expect(adjustedTime.timeZoneOffset, tz); - }); - - test('With timezone as offset', () { - final createdAt = DateTime.parse("2023-01-27T14:00:00-0500"); - final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530"); - const offset = "utc+08:00"; - final e = makeExif(dateTimeOriginal: dateTimeOriginal, timeZone: offset); - final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e); - final (dt, tz) = a.getTZAdjustedTimeAndOffset(); - - final location = getLocation("Asia/Hong_Kong"); - final offsetFromLocation = Duration(milliseconds: location.currentTimeZone.offset); - final adjustedTime = dateTimeOriginal.toUtc().add(offsetFromLocation); - - // Adds the offset to the actual time and returns the offset separately - expect(adjustedTime, dt); - expect(offsetFromLocation, tz); - }); - }); -} diff --git a/mobile/test/modules/home/asset_grid_data_structure_test.dart b/mobile/test/modules/home/asset_grid_data_structure_test.dart deleted file mode 100644 index 3e1fe06c68..0000000000 --- a/mobile/test/modules/home/asset_grid_data_structure_test.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; - -void main() { - final List testAssets = []; - - for (int i = 0; i < 150; i++) { - int month = i ~/ 31; - int day = (i % 31).toInt(); - - DateTime date = DateTime(2022, month, day); - - testAssets.add( - Asset( - checksum: "", - localId: '$i', - ownerId: 1, - fileCreatedAt: date, - fileModifiedAt: date, - updatedAt: date, - durationInSeconds: 0, - type: AssetType.image, - fileName: '', - isFavorite: false, - isArchived: false, - isTrashed: false, - ), - ); - } - - final List assets = []; - - assets.addAll( - testAssets.sublist(0, 5).map((e) { - e.fileCreatedAt = DateTime(2022, 1, 5); - return e; - }).toList(), - ); - assets.addAll( - testAssets.sublist(5, 10).map((e) { - e.fileCreatedAt = DateTime(2022, 1, 10); - return e; - }).toList(), - ); - assets.addAll( - testAssets.sublist(10, 15).map((e) { - e.fileCreatedAt = DateTime(2022, 2, 17); - return e; - }).toList(), - ); - assets.addAll( - testAssets.sublist(15, 30).map((e) { - e.fileCreatedAt = DateTime(2022, 10, 15); - return e; - }).toList(), - ); - - group('Test grouped', () { - test('test grouped check months', () async { - final renderList = await RenderList.fromAssets(assets, GroupAssetsBy.day); - - // Oct - // Day 1 - // 15 Assets => 5 Rows - // Feb - // Day 1 - // 5 Assets => 2 Rows - // Jan - // Day 2 - // 5 Assets => 2 Rows - // Day 1 - // 5 Assets => 2 Rows - expect(renderList.elements, hasLength(4)); - expect(renderList.elements[0].type, RenderAssetGridElementType.monthTitle); - expect(renderList.elements[0].date.month, 1); - expect(renderList.elements[1].type, RenderAssetGridElementType.groupDividerTitle); - expect(renderList.elements[1].date.month, 1); - expect(renderList.elements[2].type, RenderAssetGridElementType.monthTitle); - expect(renderList.elements[2].date.month, 2); - expect(renderList.elements[3].type, RenderAssetGridElementType.monthTitle); - expect(renderList.elements[3].date.month, 10); - }); - - test('test grouped check types', () async { - final renderList = await RenderList.fromAssets(assets, GroupAssetsBy.day); - - // Oct - // Day 1 - // 15 Assets => 3 Rows - // Feb - // Day 1 - // 5 Assets => 1 Row - // Jan - // Day 2 - // 5 Assets => 1 Row - // Day 1 - // 5 Assets => 1 Row - final types = [ - RenderAssetGridElementType.monthTitle, - RenderAssetGridElementType.groupDividerTitle, - RenderAssetGridElementType.monthTitle, - RenderAssetGridElementType.monthTitle, - ]; - - expect(renderList.elements, hasLength(types.length)); - - for (int i = 0; i < renderList.elements.length; i++) { - expect(renderList.elements[i].type, types[i]); - } - }); - }); -} diff --git a/mobile/test/modules/map/map_theme_override_test.dart b/mobile/test/modules/map/map_theme_override_test.dart index de16b7f24f..56efde98dd 100644 --- a/mobile/test/modules/map/map_theme_override_test.dart +++ b/mobile/test/modules/map/map_theme_override_test.dart @@ -2,16 +2,18 @@ @Tags(['widget']) library; +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/models/map/map_state.model.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/map/map_state.provider.dart'; import 'package:immich_mobile/widgets/map/map_theme_override.dart'; -import 'package:isar/isar.dart'; import '../../test_utils.dart'; import '../../widget_tester_extensions.dart'; @@ -21,17 +23,17 @@ void main() { late MockMapStateNotifier mapStateNotifier; late List overrides; late MapState mapState; - late Isar db; + late Drift db; setUpAll(() async { - db = await TestUtils.initIsar(); + db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); TestUtils.init(); }); setUp(() async { mapState = const MapState(themeMode: ThemeMode.dark); mapStateNotifier = MockMapStateNotifier(mapState); - await StoreService.init(storeRepository: IsarStoreRepository(db)); + await StoreService.init(storeRepository: DriftStoreRepository(db)); overrides = [ mapStateNotifierProvider.overrideWith(() => mapStateNotifier), localeProvider.overrideWithValue(const Locale("en")), diff --git a/mobile/test/modules/settings/settings_mocks.dart b/mobile/test/modules/settings/settings_mocks.dart deleted file mode 100644 index 63fd9312b7..0000000000 --- a/mobile/test/modules/settings/settings_mocks.dart +++ /dev/null @@ -1,4 +0,0 @@ -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockAppSettingsService extends Mock implements AppSettingsService {} diff --git a/mobile/test/modules/shared/shared_mocks.dart b/mobile/test/modules/shared/shared_mocks.dart deleted file mode 100644 index 790bbbd815..0000000000 --- a/mobile/test/modules/shared/shared_mocks.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockCurrentUserProvider extends StateNotifier with Mock implements CurrentUserProvider { - MockCurrentUserProvider() : super(null); - - @override - set state(UserDto? user) => super.state = user; -} diff --git a/mobile/test/modules/shared/sync_service_test.dart b/mobile/test/modules/shared/sync_service_test.dart deleted file mode 100644 index 767a52b8d8..0000000000 --- a/mobile/test/modules/shared/sync_service_test.dart +++ /dev/null @@ -1,285 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:drift/native.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/services/log.service.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/partner_api.repository.dart'; -import 'package:immich_mobile/services/sync.service.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../domain/service.mock.dart'; -import '../../fixtures/asset.stub.dart'; -import '../../infrastructure/repository.mock.dart'; -import '../../repository.mocks.dart'; -import '../../service.mocks.dart'; -import '../../test_utils.dart'; - -void main() { - int assetIdCounter = 0; - Asset makeAsset({ - required String checksum, - String? localId, - String? remoteId, - int ownerId = 590700560494856554, // hash of "1" - }) { - final DateTime date = DateTime(2000); - return Asset( - id: assetIdCounter++, - checksum: checksum, - localId: localId, - remoteId: remoteId, - ownerId: ownerId, - fileCreatedAt: date, - fileModifiedAt: date, - updatedAt: date, - durationInSeconds: 0, - type: AssetType.image, - fileName: localId ?? remoteId ?? "", - isFavorite: false, - isArchived: false, - isTrashed: false, - ); - } - - final owner = UserDto( - id: "1", - updatedAt: DateTime.now(), - email: "a@b.c", - name: "first last", - isAdmin: false, - profileChangedAt: DateTime.now(), - ); - - setUpAll(() async { - final loggerDb = DriftLogger(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - final LogRepository logRepository = LogRepository(loggerDb); - - WidgetsFlutterBinding.ensureInitialized(); - final db = await TestUtils.initIsar(); - - db.writeTxnSync(() => db.clearSync()); - await StoreService.init(storeRepository: IsarStoreRepository(db)); - await Store.put(StoreKey.currentUser, owner); - await LogService.init(logRepository: logRepository, storeRepository: IsarStoreRepository(db)); - }); - - group('Test SyncService grouped', () { - final MockHashService hs = MockHashService(); - final MockEntityService entityService = MockEntityService(); - final MockAlbumRepository albumRepository = MockAlbumRepository(); - final MockAssetRepository assetRepository = MockAssetRepository(); - final MockExifInfoRepository exifInfoRepository = MockExifInfoRepository(); - final MockIsarUserRepository userRepository = MockIsarUserRepository(); - final MockETagRepository eTagRepository = MockETagRepository(); - final MockAlbumMediaRepository albumMediaRepository = MockAlbumMediaRepository(); - final MockAlbumApiRepository albumApiRepository = MockAlbumApiRepository(); - final MockAppSettingService appSettingService = MockAppSettingService(); - final MockLocalFilesManagerRepository localFilesManagerRepository = MockLocalFilesManagerRepository(); - final MockPartnerApiRepository partnerApiRepository = MockPartnerApiRepository(); - final MockUserApiRepository userApiRepository = MockUserApiRepository(); - final MockPartnerRepository partnerRepository = MockPartnerRepository(); - final MockUserService userService = MockUserService(); - - final owner = UserDto( - id: "1", - updatedAt: DateTime.now(), - email: "a@b.c", - name: "first last", - isAdmin: false, - profileChangedAt: DateTime(2021), - ); - - late SyncService s; - - final List initialAssets = [ - makeAsset(checksum: "a", remoteId: "0-1"), - makeAsset(checksum: "b", remoteId: "2-1"), - makeAsset(checksum: "c", localId: "1", remoteId: "1-1"), - makeAsset(checksum: "d", localId: "2"), - makeAsset(checksum: "e", localId: "3"), - ]; - setUp(() { - s = SyncService( - hs, - entityService, - albumMediaRepository, - albumApiRepository, - albumRepository, - assetRepository, - exifInfoRepository, - partnerRepository, - userRepository, - userService, - eTagRepository, - appSettingService, - localFilesManagerRepository, - partnerApiRepository, - userApiRepository, - ); - when(() => userService.getMyUser()).thenReturn(owner); - when(() => eTagRepository.get(owner.id)).thenAnswer((_) async => ETag(id: owner.id, time: DateTime.now())); - when(() => eTagRepository.deleteByIds(["1"])).thenAnswer((_) async {}); - when(() => eTagRepository.upsertAll(any())).thenAnswer((_) async {}); - when(() => partnerRepository.getSharedWith()).thenAnswer((_) async => []); - when(() => userRepository.getAll(sortBy: SortUserBy.id)).thenAnswer((_) async => [owner]); - when(() => userRepository.getAll()).thenAnswer((_) async => [owner]); - when( - () => assetRepository.getAll(ownerId: owner.id, sortBy: AssetSort.checksum), - ).thenAnswer((_) async => initialAssets); - when( - () => assetRepository.getAllByOwnerIdChecksum(any(), any()), - ).thenAnswer((_) async => [initialAssets[3], null, null]); - when(() => assetRepository.updateAll(any())).thenAnswer((_) async => []); - when(() => assetRepository.deleteByIds(any())).thenAnswer((_) async {}); - when(() => exifInfoRepository.updateAll(any())).thenAnswer((_) async => []); - when( - () => assetRepository.transaction(any()), - ).thenAnswer((call) => (call.positionalArguments.first as Function).call()); - when( - () => assetRepository.transaction(any()), - ).thenAnswer((call) => (call.positionalArguments.first as Function).call()); - when(() => userApiRepository.getAll()).thenAnswer((_) async => [owner]); - registerFallbackValue(Direction.sharedByMe); - when(() => partnerApiRepository.getAll(any())).thenAnswer((_) async => []); - }); - test('test inserting existing assets', () async { - final List remoteAssets = [ - makeAsset(checksum: "a", remoteId: "0-1"), - makeAsset(checksum: "b", remoteId: "2-1"), - makeAsset(checksum: "c", remoteId: "1-1"), - ]; - final bool c1 = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: _failDiff, - loadAssets: (u, d) => remoteAssets, - ); - expect(c1, isFalse); - verifyNever(() => assetRepository.updateAll(any())); - }); - - test('test inserting new assets', () async { - final List remoteAssets = [ - makeAsset(checksum: "a", remoteId: "0-1"), - makeAsset(checksum: "b", remoteId: "2-1"), - makeAsset(checksum: "c", remoteId: "1-1"), - makeAsset(checksum: "d", remoteId: "1-2"), - makeAsset(checksum: "f", remoteId: "1-4"), - makeAsset(checksum: "g", remoteId: "3-1"), - ]; - final bool c1 = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: _failDiff, - loadAssets: (u, d) => remoteAssets, - ); - expect(c1, isTrue); - final updatedAsset = initialAssets[3].updatedCopy(remoteAssets[3]); - verify(() => assetRepository.updateAll([remoteAssets[4], remoteAssets[5], updatedAsset])); - }); - - test('test syncing duplicate assets', () async { - final List remoteAssets = [ - makeAsset(checksum: "a", remoteId: "0-1"), - makeAsset(checksum: "b", remoteId: "1-1"), - makeAsset(checksum: "c", remoteId: "2-1"), - makeAsset(checksum: "h", remoteId: "2-1b"), - makeAsset(checksum: "i", remoteId: "2-1c"), - makeAsset(checksum: "j", remoteId: "2-1d"), - ]; - final bool c1 = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: _failDiff, - loadAssets: (u, d) => remoteAssets, - ); - expect(c1, isTrue); - when( - () => assetRepository.getAll(ownerId: owner.id, sortBy: AssetSort.checksum), - ).thenAnswer((_) async => remoteAssets); - final bool c2 = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: _failDiff, - loadAssets: (u, d) => remoteAssets, - ); - expect(c2, isFalse); - final currentState = [...remoteAssets]; - when( - () => assetRepository.getAll(ownerId: owner.id, sortBy: AssetSort.checksum), - ).thenAnswer((_) async => currentState); - remoteAssets.removeAt(4); - final bool c3 = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: _failDiff, - loadAssets: (u, d) => remoteAssets, - ); - expect(c3, isTrue); - remoteAssets.add(makeAsset(checksum: "k", remoteId: "2-1e")); - remoteAssets.add(makeAsset(checksum: "l", remoteId: "2-2")); - final bool c4 = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: _failDiff, - loadAssets: (u, d) => remoteAssets, - ); - expect(c4, isTrue); - }); - - test('test efficient sync', () async { - when( - () => assetRepository.deleteAllByRemoteId([ - initialAssets[1].remoteId!, - initialAssets[2].remoteId!, - ], state: AssetState.remote), - ).thenAnswer((_) async { - return; - }); - when( - () => assetRepository.getAllByRemoteId(["2-1", "1-1"], state: AssetState.merged), - ).thenAnswer((_) async => [initialAssets[2]]); - when( - () => assetRepository.getAllByOwnerIdChecksum(any(), any()), - ).thenAnswer((_) async => [initialAssets[0], null, null]); //afg - final List toUpsert = [ - makeAsset(checksum: "a", remoteId: "0-1"), // changed - makeAsset(checksum: "f", remoteId: "0-2"), // new - makeAsset(checksum: "g", remoteId: "0-3"), // new - ]; - toUpsert[0].isFavorite = true; - final List toDelete = ["2-1", "1-1"]; - final expected = [...toUpsert]; - expected[0].id = initialAssets[0].id; - final bool c = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: (user, since) async => (toUpsert, toDelete), - loadAssets: (user, date) => throw Exception(), - ); - expect(c, isTrue); - verify(() => assetRepository.updateAll(expected)); - }); - - group("upsertAssetsWithExif", () { - test('test upsert with EXIF data', () async { - final assets = [AssetStub.image1, AssetStub.image2]; - - expect(assets.map((a) => a.exifInfo?.assetId), List.filled(assets.length, null)); - await s.upsertAssetsWithExif(assets); - verify( - () => exifInfoRepository.updateAll( - any(that: containsAll(assets.map((a) => a.exifInfo!.copyWith(assetId: a.id)))), - ), - ); - expect(assets.map((a) => a.exifInfo?.assetId), assets.map((a) => a.id)); - }); - }); - }); -} - -Future<(List?, List?)> _failDiff(List user, DateTime time) => Future.value((null, null)); diff --git a/mobile/test/modules/utils/migration_test.dart b/mobile/test/modules/utils/migration_test.dart deleted file mode 100644 index 08ab1204a6..0000000000 --- a/mobile/test/modules/utils/migration_test.dart +++ /dev/null @@ -1,131 +0,0 @@ -import 'package:drift/drift.dart' hide isNull; -import 'package:drift/native.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; -import 'package:immich_mobile/utils/migration.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../infrastructure/repository.mock.dart'; - -void main() { - late Drift db; - late SyncStreamRepository mockSyncStreamRepository; - - setUpAll(() async { - db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - await StoreService.init(storeRepository: DriftStoreRepository(db)); - mockSyncStreamRepository = MockSyncStreamRepository(); - when(() => mockSyncStreamRepository.reset()).thenAnswer((_) async => {}); - }); - - tearDown(() async { - await Store.clear(); - }); - - group('handleBetaMigration Tests', () { - group("version < 15", () { - test('already on new timeline', () async { - await Store.put(StoreKey.betaTimeline, true); - - await handleBetaMigration(14, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), true); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - - test('already on old timeline', () async { - await Store.put(StoreKey.betaTimeline, false); - - await handleBetaMigration(14, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.needBetaMigration), true); - }); - - test('fresh install', () async { - await Store.delete(StoreKey.betaTimeline); - await handleBetaMigration(14, true, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), true); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - }); - - group("version == 15", () { - test('already on new timeline', () async { - await Store.put(StoreKey.betaTimeline, true); - - await handleBetaMigration(15, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), true); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - - test('already on old timeline', () async { - await Store.put(StoreKey.betaTimeline, false); - - await handleBetaMigration(15, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.needBetaMigration), true); - }); - - test('fresh install', () async { - await Store.delete(StoreKey.betaTimeline); - await handleBetaMigration(15, true, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), true); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - }); - - group("version > 15", () { - test('already on new timeline', () async { - await Store.put(StoreKey.betaTimeline, true); - - await handleBetaMigration(16, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), true); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - - test('already on old timeline', () async { - await Store.put(StoreKey.betaTimeline, false); - - await handleBetaMigration(16, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), false); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - - test('fresh install', () async { - await Store.delete(StoreKey.betaTimeline); - await handleBetaMigration(16, true, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), true); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - }); - }); - - group('sync reset tests', () { - test('version < 16', () async { - await Store.put(StoreKey.shouldResetSync, false); - - await handleBetaMigration(15, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.shouldResetSync), true); - }); - - test('version >= 16', () async { - await Store.put(StoreKey.shouldResetSync, false); - - await handleBetaMigration(16, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.shouldResetSync), false); - }); - }); -} diff --git a/mobile/test/modules/utils/openapi_patching_test.dart b/mobile/test/modules/utils/openapi_patching_test.dart index a577b0544f..18ab07b3a9 100644 --- a/mobile/test/modules/utils/openapi_patching_test.dart +++ b/mobile/test/modules/utils/openapi_patching_test.dart @@ -21,7 +21,7 @@ void main() { """); upgradeDto(value, targetType); - expect(value['tags'], TagsResponse().toJson()); + expect(value['tags'], TagsResponse(enabled: false, sidebarWeb: false).toJson()); expect(value['download']['includeEmbeddedVideos'], false); }); diff --git a/mobile/test/modules/utils/throttler_test.dart b/mobile/test/modules/utils/throttler_test.dart deleted file mode 100644 index 1757826daf..0000000000 --- a/mobile/test/modules/utils/throttler_test.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/utils/throttle.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; - -class _Counter { - int _count = 0; - _Counter(); - - int get count => _count; - void increment() { - dPrint(() => "Counter inside increment: $count"); - _count = _count + 1; - } -} - -void main() { - test('Executes the method immediately if no calls received previously', () async { - var counter = _Counter(); - final throttler = Throttler(interval: const Duration(milliseconds: 300)); - throttler.run(() => counter.increment()); - expect(counter.count, 1); - }); - - test('Does not execute calls before throttle interval', () async { - var counter = _Counter(); - final throttler = Throttler(interval: const Duration(milliseconds: 100)); - throttler.run(() => counter.increment()); - throttler.run(() => counter.increment()); - throttler.run(() => counter.increment()); - throttler.run(() => counter.increment()); - throttler.run(() => counter.increment()); - await Future.delayed(const Duration(seconds: 1)); - expect(counter.count, 1); - }); - - test('Executes the method if received in intervals', () async { - var counter = _Counter(); - final throttler = Throttler(interval: const Duration(milliseconds: 100)); - for (final _ in Iterable.generate(10)) { - throttler.run(() => counter.increment()); - await Future.delayed(const Duration(milliseconds: 50)); - } - await Future.delayed(const Duration(seconds: 1)); - expect(counter.count, 5); - }); -} diff --git a/mobile/test/modules/utils/thumbnail_utils_test.dart b/mobile/test/modules/utils/thumbnail_utils_test.dart deleted file mode 100644 index dd4588fc80..0000000000 --- a/mobile/test/modules/utils/thumbnail_utils_test.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/utils/thumbnail_utils.dart'; - -void main() { - final dateTime = DateTime(2025, 04, 25, 12, 13, 14); - final dateTimeString = DateFormat.yMMMMd().format(dateTime); - - test('returns description if it has one', () { - final result = getAltText(const ExifInfo(description: 'description'), dateTime, AssetType.image, []); - expect(result, 'description'); - }); - - test('returns image alt text with date if no location', () { - final (template, args) = getAltTextTemplate(const ExifInfo(), dateTime, AssetType.image, []); - expect(template, "image_alt_text_date"); - expect(args["isVideo"], "false"); - expect(args["date"], dateTimeString); - }); - - test('returns image alt text with date and place', () { - final (template, args) = getAltTextTemplate( - const ExifInfo(city: 'city', country: 'country'), - dateTime, - AssetType.video, - [], - ); - expect(template, "image_alt_text_date_place"); - expect(args["isVideo"], "true"); - expect(args["date"], dateTimeString); - expect(args["city"], "city"); - expect(args["country"], "country"); - }); - - test('returns image alt text with date and some people', () { - final (template, args) = getAltTextTemplate(const ExifInfo(), dateTime, AssetType.image, ["Alice", "Bob"]); - expect(template, "image_alt_text_date_2_people"); - expect(args["isVideo"], "false"); - expect(args["date"], dateTimeString); - expect(args["person1"], "Alice"); - expect(args["person2"], "Bob"); - }); - - test('returns image alt text with date and location and many people', () { - final (template, args) = getAltTextTemplate( - const ExifInfo(city: "city", country: 'country'), - dateTime, - AssetType.video, - ["Alice", "Bob", "Carol", "David", "Eve"], - ); - expect(template, "image_alt_text_date_place_4_or_more_people"); - expect(args["isVideo"], "true"); - expect(args["date"], dateTimeString); - expect(args["city"], "city"); - expect(args["country"], "country"); - expect(args["person1"], "Alice"); - expect(args["person2"], "Bob"); - expect(args["person3"], "Carol"); - expect(args["additionalCount"], "2"); - }); -} diff --git a/mobile/test/pages/search/search.page_test.dart b/mobile/test/pages/search/search.page_test.dart deleted file mode 100644 index 9592623a28..0000000000 --- a/mobile/test/pages/search/search.page_test.dart +++ /dev/null @@ -1,98 +0,0 @@ -@Skip('currently failing due to mock HTTP client to download ISAR binaries') -@Tags(['pages']) -library; - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/pages/search/search.page.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:isar/isar.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:openapi/api.dart'; - -import '../../dto.mocks.dart'; -import '../../service.mocks.dart'; -import '../../test_utils.dart'; -import '../../widget_tester_extensions.dart'; - -void main() { - late List overrides; - late Isar db; - late MockApiService mockApiService; - late MockSearchApi mockSearchApi; - - setUpAll(() async { - TestUtils.init(); - db = await TestUtils.initIsar(); - await StoreService.init(storeRepository: IsarStoreRepository(db)); - mockApiService = MockApiService(); - mockSearchApi = MockSearchApi(); - when(() => mockApiService.searchApi).thenReturn(mockSearchApi); - registerFallbackValue(MockSmartSearchDto()); - registerFallbackValue(MockMetadataSearchDto()); - overrides = [ - dbProvider.overrideWithValue(db), - isarProvider.overrideWithValue(db), - apiServiceProvider.overrideWithValue(mockApiService), - ]; - }); - - final emptyTextSearch = isA().having((s) => s.originalFileName, 'originalFileName', null); - - testWidgets('contextual search with/without text', (tester) async { - await tester.pumpConsumerWidget(const SearchPage(), overrides: overrides); - - await tester.pumpAndSettle(); - - expect(find.byIcon(Icons.abc_rounded), findsOneWidget, reason: 'Should have contextual search icon'); - - final searchField = find.byKey(const Key('search_text_field')); - expect(searchField, findsOneWidget); - - await tester.enterText(searchField, 'test'); - await tester.testTextInput.receiveAction(TextInputAction.search); - - var captured = verify(() => mockSearchApi.searchSmart(captureAny())).captured; - - expect(captured.first, isA().having((s) => s.query, 'query', 'test')); - - await tester.enterText(searchField, ''); - await tester.testTextInput.receiveAction(TextInputAction.search); - - captured = verify(() => mockSearchApi.searchAssets(captureAny())).captured; - expect(captured.first, emptyTextSearch); - }); - - testWidgets('not contextual search with/without text', (tester) async { - await tester.pumpConsumerWidget(const SearchPage(), overrides: overrides); - - await tester.pumpAndSettle(); - - await tester.tap(find.byKey(const Key('contextual_search_button'))); - - await tester.pumpAndSettle(); - - expect(find.byIcon(Icons.image_search_rounded), findsOneWidget, reason: 'Should not have contextual search icon'); - - final searchField = find.byKey(const Key('search_text_field')); - expect(searchField, findsOneWidget); - - await tester.enterText(searchField, 'test'); - await tester.testTextInput.receiveAction(TextInputAction.search); - - var captured = verify(() => mockSearchApi.searchAssets(captureAny())).captured; - - expect(captured.first, isA().having((s) => s.originalFileName, 'originalFileName', 'test')); - - await tester.enterText(searchField, ''); - await tester.testTextInput.receiveAction(TextInputAction.search); - - captured = verify(() => mockSearchApi.searchAssets(captureAny())).captured; - expect(captured.first, emptyTextSearch); - }); -} diff --git a/mobile/test/repository.mocks.dart b/mobile/test/repository.mocks.dart index 4b54ec4055..d049626f1d 100644 --- a/mobile/test/repository.mocks.dart +++ b/mobile/test/repository.mocks.dart @@ -1,48 +1,16 @@ -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/repositories/partner_api.repository.dart'; -import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/repositories/album_api.repository.dart'; -import 'package:immich_mobile/repositories/partner.repository.dart'; -import 'package:immich_mobile/repositories/etag.repository.dart'; -import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; +import 'package:immich_mobile/repositories/asset_api.repository.dart'; +import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/repositories/auth.repository.dart'; import 'package:immich_mobile/repositories/auth_api.repository.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/asset_media.repository.dart'; -import 'package:immich_mobile/repositories/album.repository.dart'; -import 'package:immich_mobile/repositories/asset_api.repository.dart'; +import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; import 'package:mocktail/mocktail.dart'; -class MockAlbumRepository extends Mock implements AlbumRepository {} - -class MockAssetRepository extends Mock implements AssetRepository {} - -class MockBackupRepository extends Mock implements BackupAlbumRepository {} - -class MockExifInfoRepository extends Mock implements IsarExifRepository {} - -class MockETagRepository extends Mock implements ETagRepository {} - -class MockAlbumMediaRepository extends Mock implements AlbumMediaRepository {} - -class MockBackupAlbumRepository extends Mock implements BackupAlbumRepository {} - class MockAssetApiRepository extends Mock implements AssetApiRepository {} class MockAssetMediaRepository extends Mock implements AssetMediaRepository {} -class MockFileMediaRepository extends Mock implements FileMediaRepository {} - -class MockAlbumApiRepository extends Mock implements AlbumApiRepository {} - class MockAuthApiRepository extends Mock implements AuthApiRepository {} class MockAuthRepository extends Mock implements AuthRepository {} -class MockPartnerRepository extends Mock implements PartnerRepository {} - -class MockPartnerApiRepository extends Mock implements PartnerApiRepository {} - class MockLocalFilesManagerRepository extends Mock implements LocalFilesManagerRepository {} diff --git a/mobile/test/service.mocks.dart b/mobile/test/service.mocks.dart index 87a8c01cf0..4591dd845d 100644 --- a/mobile/test/service.mocks.dart +++ b/mobile/test/service.mocks.dart @@ -1,31 +1,10 @@ -import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:immich_mobile/services/backup.service.dart'; -import 'package:immich_mobile/services/entity.service.dart'; -import 'package:immich_mobile/services/hash.service.dart'; import 'package:immich_mobile/services/network.service.dart'; -import 'package:immich_mobile/services/sync.service.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:openapi/api.dart'; class MockApiService extends Mock implements ApiService {} -class MockAlbumService extends Mock implements AlbumService {} - -class MockBackupService extends Mock implements BackupService {} - -class MockSyncService extends Mock implements SyncService {} - -class MockHashService extends Mock implements HashService {} - -class MockEntityService extends Mock implements EntityService {} - class MockNetworkService extends Mock implements NetworkService {} -class MockSearchApi extends Mock implements SearchApi {} - class MockAppSettingService extends Mock implements AppSettingsService {} - -class MockBackgroundService extends Mock implements BackgroundService {} diff --git a/mobile/test/services/album.service_test.dart b/mobile/test/services/album.service_test.dart deleted file mode 100644 index 97683cdab1..0000000000 --- a/mobile/test/services/album.service_test.dart +++ /dev/null @@ -1,177 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../domain/service.mock.dart'; -import '../fixtures/album.stub.dart'; -import '../fixtures/asset.stub.dart'; -import '../fixtures/user.stub.dart'; -import '../repository.mocks.dart'; -import '../service.mocks.dart'; - -void main() { - late AlbumService sut; - late MockUserService userService; - late MockSyncService syncService; - late MockEntityService entityService; - late MockAlbumRepository albumRepository; - late MockAssetRepository assetRepository; - late MockBackupRepository backupRepository; - late MockAlbumMediaRepository albumMediaRepository; - late MockAlbumApiRepository albumApiRepository; - - setUp(() { - userService = MockUserService(); - syncService = MockSyncService(); - entityService = MockEntityService(); - albumRepository = MockAlbumRepository(); - assetRepository = MockAssetRepository(); - backupRepository = MockBackupRepository(); - albumMediaRepository = MockAlbumMediaRepository(); - albumApiRepository = MockAlbumApiRepository(); - - when(() => userService.getMyUser()).thenReturn(UserStub.user1); - - when( - () => albumRepository.transaction(any()), - ).thenAnswer((call) => (call.positionalArguments.first as Function).call()); - when( - () => assetRepository.transaction(any()), - ).thenAnswer((call) => (call.positionalArguments.first as Function).call()); - - sut = AlbumService( - syncService, - userService, - entityService, - albumRepository, - assetRepository, - backupRepository, - albumMediaRepository, - albumApiRepository, - ); - }); - - group('refreshDeviceAlbums', () { - test('empty selection with one album in db', () async { - when(() => backupRepository.getIdsBySelection(BackupSelection.exclude)).thenAnswer((_) async => []); - when(() => backupRepository.getIdsBySelection(BackupSelection.select)).thenAnswer((_) async => []); - when(() => albumMediaRepository.getAll()).thenAnswer((_) async => []); - when(() => albumRepository.count(local: true)).thenAnswer((_) async => 1); - when(() => syncService.removeAllLocalAlbumsAndAssets()).thenAnswer((_) async => true); - final result = await sut.refreshDeviceAlbums(); - expect(result, false); - verify(() => syncService.removeAllLocalAlbumsAndAssets()); - }); - - test('one selected albums, two on device', () async { - when(() => backupRepository.getIdsBySelection(BackupSelection.exclude)).thenAnswer((_) async => []); - when( - () => backupRepository.getIdsBySelection(BackupSelection.select), - ).thenAnswer((_) async => [AlbumStub.oneAsset.localId!]); - when(() => albumMediaRepository.getAll()).thenAnswer((_) async => [AlbumStub.oneAsset, AlbumStub.twoAsset]); - when(() => syncService.syncLocalAlbumAssetsToDb(any(), any())).thenAnswer((_) async => true); - final result = await sut.refreshDeviceAlbums(); - expect(result, true); - verify(() => syncService.syncLocalAlbumAssetsToDb([AlbumStub.oneAsset], null)).called(1); - verifyNoMoreInteractions(syncService); - }); - }); - - group('refreshRemoteAlbums', () { - test('is working', () async { - when(() => syncService.getUsersFromServer()).thenAnswer((_) async => []); - when(() => syncService.syncUsersFromServer(any())).thenAnswer((_) async => true); - when(() => albumApiRepository.getAll(shared: true)).thenAnswer((_) async => [AlbumStub.sharedWithUser]); - - when( - () => albumApiRepository.getAll(shared: null), - ).thenAnswer((_) async => [AlbumStub.oneAsset, AlbumStub.twoAsset]); - - when( - () => syncService.syncRemoteAlbumsToDb([AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser]), - ).thenAnswer((_) async => true); - final result = await sut.refreshRemoteAlbums(); - expect(result, true); - verify(() => syncService.getUsersFromServer()).called(1); - verify(() => syncService.syncUsersFromServer([])).called(1); - verify(() => albumApiRepository.getAll(shared: true)).called(1); - verify(() => albumApiRepository.getAll(shared: null)).called(1); - verify( - () => syncService.syncRemoteAlbumsToDb([AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser]), - ).called(1); - verifyNoMoreInteractions(userService); - verifyNoMoreInteractions(albumApiRepository); - verifyNoMoreInteractions(syncService); - }); - }); - - group('createAlbum', () { - test('shared with assets', () async { - when( - () => albumApiRepository.create( - "name", - assetIds: any(named: "assetIds"), - sharedUserIds: any(named: "sharedUserIds"), - ), - ).thenAnswer((_) async => AlbumStub.oneAsset); - - when( - () => entityService.fillAlbumWithDatabaseEntities(AlbumStub.oneAsset), - ).thenAnswer((_) async => AlbumStub.oneAsset); - - when(() => albumRepository.create(AlbumStub.oneAsset)).thenAnswer((_) async => AlbumStub.twoAsset); - - final result = await sut.createAlbum("name", [AssetStub.image1], [UserStub.user1]); - expect(result, AlbumStub.twoAsset); - verify( - () => albumApiRepository.create( - "name", - assetIds: [AssetStub.image1.remoteId!], - sharedUserIds: [UserStub.user1.id], - ), - ).called(1); - verify(() => entityService.fillAlbumWithDatabaseEntities(AlbumStub.oneAsset)).called(1); - }); - }); - - group('addAdditionalAssetToAlbum', () { - test('one added, one duplicate', () async { - when( - () => albumApiRepository.addAssets(AlbumStub.oneAsset.remoteId!, any()), - ).thenAnswer((_) async => (added: [AssetStub.image2.remoteId!], duplicates: [AssetStub.image1.remoteId!])); - when(() => albumRepository.get(AlbumStub.oneAsset.id)).thenAnswer((_) async => AlbumStub.oneAsset); - when(() => albumRepository.addAssets(AlbumStub.oneAsset, [AssetStub.image2])).thenAnswer((_) async {}); - when(() => albumRepository.removeAssets(AlbumStub.oneAsset, [])).thenAnswer((_) async {}); - when(() => albumRepository.recalculateMetadata(AlbumStub.oneAsset)).thenAnswer((_) async => AlbumStub.oneAsset); - when(() => albumRepository.update(AlbumStub.oneAsset)).thenAnswer((_) async => AlbumStub.oneAsset); - - final result = await sut.addAssets(AlbumStub.oneAsset, [AssetStub.image1, AssetStub.image2]); - - expect(result != null, true); - expect(result!.alreadyInAlbum, [AssetStub.image1.remoteId!]); - expect(result.successfullyAdded, 1); - }); - }); - - group('addAdditionalUserToAlbum', () { - test('one added', () async { - when( - () => albumApiRepository.addUsers(AlbumStub.emptyAlbum.remoteId!, any()), - ).thenAnswer((_) async => AlbumStub.sharedWithUser); - - when( - () => albumRepository.addUsers( - AlbumStub.emptyAlbum, - AlbumStub.emptyAlbum.sharedUsers.map((u) => u.toDto()).toList(), - ), - ).thenAnswer((_) async => AlbumStub.emptyAlbum); - - when(() => albumRepository.update(AlbumStub.emptyAlbum)).thenAnswer((_) async => AlbumStub.emptyAlbum); - - final result = await sut.addUsers(AlbumStub.emptyAlbum, [UserStub.user2.id]); - - expect(result, true); - }); - }); -} diff --git a/mobile/test/services/asset.service_test.dart b/mobile/test/services/asset.service_test.dart deleted file mode 100644 index b741150165..0000000000 --- a/mobile/test/services/asset.service_test.dart +++ /dev/null @@ -1,103 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:openapi/api.dart'; - -import '../api.mocks.dart'; -import '../domain/service.mock.dart'; -import '../fixtures/asset.stub.dart'; -import '../infrastructure/repository.mock.dart'; -import '../repository.mocks.dart'; -import '../service.mocks.dart'; - -class FakeAssetBulkUpdateDto extends Fake implements AssetBulkUpdateDto {} - -void main() { - late AssetService sut; - - late MockAssetRepository assetRepository; - late MockAssetApiRepository assetApiRepository; - late MockExifInfoRepository exifInfoRepository; - late MockETagRepository eTagRepository; - late MockBackupAlbumRepository backupAlbumRepository; - late MockIsarUserRepository userRepository; - late MockAssetMediaRepository assetMediaRepository; - late MockApiService apiService; - - late MockSyncService syncService; - late MockAlbumService albumService; - late MockBackupService backupService; - late MockUserService userService; - - setUp(() { - assetRepository = MockAssetRepository(); - assetApiRepository = MockAssetApiRepository(); - exifInfoRepository = MockExifInfoRepository(); - userRepository = MockIsarUserRepository(); - eTagRepository = MockETagRepository(); - backupAlbumRepository = MockBackupAlbumRepository(); - apiService = MockApiService(); - assetMediaRepository = MockAssetMediaRepository(); - - syncService = MockSyncService(); - userService = MockUserService(); - albumService = MockAlbumService(); - backupService = MockBackupService(); - - sut = AssetService( - assetApiRepository, - assetRepository, - exifInfoRepository, - userRepository, - eTagRepository, - backupAlbumRepository, - apiService, - syncService, - backupService, - albumService, - userService, - assetMediaRepository, - ); - - registerFallbackValue(FakeAssetBulkUpdateDto()); - }); - - group("Edit ExifInfo", () { - late AssetsApi assetsApi; - setUp(() { - assetsApi = MockAssetsApi(); - when(() => apiService.assetsApi).thenReturn(assetsApi); - when(() => assetsApi.updateAssets(any())).thenAnswer((_) async => Future.value()); - }); - - test("asset is updated with DateTime", () async { - final assets = [AssetStub.image1, AssetStub.image2]; - final dateTime = DateTime.utc(2025, 6, 4, 2, 57); - await sut.changeDateTime(assets, dateTime.toIso8601String()); - - verify(() => assetsApi.updateAssets(any())).called(1); - final upsertExifCallback = verify(() => syncService.upsertAssetsWithExif(captureAny())); - upsertExifCallback.called(1); - final receivedAssets = upsertExifCallback.captured.firstOrNull as List? ?? []; - final receivedDatetime = receivedAssets.cast().map((a) => a.exifInfo?.dateTimeOriginal ?? DateTime(0)); - expect(receivedDatetime.every((d) => d == dateTime), isTrue); - }); - - test("asset is updated with LatLng", () async { - final assets = [AssetStub.image1, AssetStub.image2]; - final latLng = const LatLng(37.7749, -122.4194); - await sut.changeLocation(assets, latLng); - - verify(() => assetsApi.updateAssets(any())).called(1); - final upsertExifCallback = verify(() => syncService.upsertAssetsWithExif(captureAny())); - upsertExifCallback.called(1); - final receivedAssets = upsertExifCallback.captured.firstOrNull as List? ?? []; - final receivedCoords = receivedAssets.cast().map( - (a) => LatLng(a.exifInfo?.latitude ?? 0, a.exifInfo?.longitude ?? 0), - ); - expect(receivedCoords.every((l) => l == latLng), isTrue); - }); - }); -} diff --git a/mobile/test/services/auth.service_test.dart b/mobile/test/services/auth.service_test.dart index 7c7de3cd0e..f9a6d5e282 100644 --- a/mobile/test/services/auth.service_test.dart +++ b/mobile/test/services/auth.service_test.dart @@ -1,18 +1,19 @@ +import 'package:drift/drift.dart' hide isNull; +import 'package:drift/native.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/auth.service.dart'; -import 'package:isar/isar.dart'; import 'package:mocktail/mocktail.dart'; import 'package:openapi/api.dart'; import '../domain/service.mock.dart'; import '../repository.mocks.dart'; import '../service.mocks.dart'; -import '../test_utils.dart'; void main() { late AuthService sut; @@ -22,7 +23,7 @@ void main() { late MockNetworkService networkService; late MockBackgroundSyncManager backgroundSyncManager; late MockAppSettingService appSettingsService; - late Isar db; + late Drift db; setUp(() async { authApiRepository = MockAuthApiRepository(); @@ -45,19 +46,16 @@ void main() { }); setUpAll(() async { - db = await TestUtils.initIsar(); - db.writeTxnSync(() => db.clearSync()); - await StoreService.init(storeRepository: IsarStoreRepository(db)); + WidgetsFlutterBinding.ensureInitialized(); + db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); + await StoreService.init(storeRepository: DriftStoreRepository(db)); + }); + + tearDownAll(() async { + await db.close(); }); group('validateServerUrl', () { - setUpAll(() async { - WidgetsFlutterBinding.ensureInitialized(); - final db = await TestUtils.initIsar(); - db.writeTxnSync(() => db.clearSync()); - await StoreService.init(storeRepository: IsarStoreRepository(db)); - }); - test('Should resolve HTTP endpoint', () async { const testUrl = 'http://ip:2283'; const resolvedUrl = 'http://ip:2283/api'; diff --git a/mobile/test/services/entity.service_test.dart b/mobile/test/services/entity.service_test.dart deleted file mode 100644 index 64b9fc604b..0000000000 --- a/mobile/test/services/entity.service_test.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/services/entity.service.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../fixtures/asset.stub.dart'; -import '../fixtures/user.stub.dart'; -import '../infrastructure/repository.mock.dart'; -import '../repository.mocks.dart'; - -void main() { - late EntityService sut; - late MockAssetRepository assetRepository; - late MockIsarUserRepository userRepository; - - setUp(() { - assetRepository = MockAssetRepository(); - userRepository = MockIsarUserRepository(); - sut = EntityService(assetRepository, userRepository); - }); - - group('fillAlbumWithDatabaseEntities', () { - test('remote album with owner, thumbnail, sharedUsers and assets', () async { - final Album album = - Album( - name: "album-with-two-assets-and-two-users", - localId: "album-with-two-assets-and-two-users-local", - remoteId: "album-with-two-assets-and-two-users-remote", - createdAt: DateTime(2001), - modifiedAt: DateTime(2010), - shared: true, - activityEnabled: true, - startDate: DateTime(2019), - endDate: DateTime(2020), - ) - ..remoteThumbnailAssetId = AssetStub.image1.remoteId - ..assets.addAll([AssetStub.image1, AssetStub.image1]) - ..owner.value = User.fromDto(UserStub.user1) - ..sharedUsers.addAll([User.fromDto(UserStub.admin), User.fromDto(UserStub.admin)]); - - when(() => userRepository.getByUserId(any())).thenAnswer((_) async => UserStub.admin); - when(() => userRepository.getByUserId(any())).thenAnswer((_) async => UserStub.admin); - - when(() => assetRepository.getByRemoteId(AssetStub.image1.remoteId!)).thenAnswer((_) async => AssetStub.image1); - - when(() => userRepository.getByUserIds(any())).thenAnswer((_) async => [UserStub.user1, UserStub.user2]); - - when(() => assetRepository.getAllByRemoteId(any())).thenAnswer((_) async => [AssetStub.image1, AssetStub.image2]); - - await sut.fillAlbumWithDatabaseEntities(album); - expect(album.owner.value?.toDto(), UserStub.admin); - expect(album.thumbnail.value, AssetStub.image1); - expect(album.remoteUsers.map((u) => u.toDto()).toSet(), {UserStub.user1, UserStub.user2}); - expect(album.remoteAssets.toSet(), {AssetStub.image1, AssetStub.image2}); - }); - - test('remote album without any info', () async { - makeEmptyAlbum() => Album( - name: "album-without-info", - localId: "album-without-info-local", - remoteId: "album-without-info-remote", - createdAt: DateTime(2001), - modifiedAt: DateTime(2010), - shared: false, - activityEnabled: false, - ); - - final album = makeEmptyAlbum(); - await sut.fillAlbumWithDatabaseEntities(album); - verifyNoMoreInteractions(assetRepository); - verifyNoMoreInteractions(userRepository); - expect(album, makeEmptyAlbum()); - }); - }); -} diff --git a/mobile/test/services/hash_service_test.dart b/mobile/test/services/hash_service_test.dart deleted file mode 100644 index 9429d434b0..0000000000 --- a/mobile/test/services/hash_service_test.dart +++ /dev/null @@ -1,349 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; -import 'dart:math'; - -import 'package:collection/collection.dart'; -import 'package:file/memory.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/device_asset.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/device_asset.repository.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:immich_mobile/services/hash.service.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../fixtures/asset.stub.dart'; -import '../infrastructure/repository.mock.dart'; -import '../service.mocks.dart'; -import '../mocks/asset_entity.mock.dart'; - -class MockAsset extends Mock implements Asset {} - -void main() { - late HashService sut; - late BackgroundService mockBackgroundService; - late IsarDeviceAssetRepository mockDeviceAssetRepository; - - setUp(() { - mockBackgroundService = MockBackgroundService(); - mockDeviceAssetRepository = MockDeviceAssetRepository(); - - sut = HashService(deviceAssetRepository: mockDeviceAssetRepository, backgroundService: mockBackgroundService); - - when(() => mockDeviceAssetRepository.transaction(any())).thenAnswer((_) async { - final capturedCallback = verify(() => mockDeviceAssetRepository.transaction(captureAny())).captured; - // Invoke the transaction callback - await (capturedCallback.firstOrNull as Future Function()?)?.call(); - }); - when(() => mockDeviceAssetRepository.updateAll(any())).thenAnswer((_) async => true); - when(() => mockDeviceAssetRepository.deleteIds(any())).thenAnswer((_) async => true); - }); - - group("HashService: No DeviceAsset entry", () { - test("hash successfully", () async { - final (mockAsset, file, deviceAsset, hash) = await _createAssetMock(AssetStub.image1); - - when(() => mockBackgroundService.digestFiles([file.path])).thenAnswer((_) async => [hash]); - // No DB entries for this asset - when(() => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!])).thenAnswer((_) async => []); - - final result = await sut.hashAssets([mockAsset]); - - // Verify we stored the new hash in DB - when(() => mockDeviceAssetRepository.transaction(any())).thenAnswer((_) async { - final capturedCallback = verify(() => mockDeviceAssetRepository.transaction(captureAny())).captured; - // Invoke the transaction callback - await (capturedCallback.firstOrNull as Future Function()?)?.call(); - verify( - () => mockDeviceAssetRepository.updateAll([ - deviceAsset.copyWith(modifiedTime: AssetStub.image1.fileModifiedAt), - ]), - ).called(1); - verify(() => mockDeviceAssetRepository.deleteIds([])).called(1); - }); - expect(result, [AssetStub.image1.copyWith(checksum: base64.encode(hash))]); - }); - }); - - group("HashService: Has DeviceAsset entry", () { - test("when the asset is not modified", () async { - final hash = utf8.encode("image1-hash"); - - when(() => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!])).thenAnswer( - (_) async => [ - DeviceAsset(assetId: AssetStub.image1.localId!, hash: hash, modifiedTime: AssetStub.image1.fileModifiedAt), - ], - ); - final result = await sut.hashAssets([AssetStub.image1]); - - verifyNever(() => mockBackgroundService.digestFiles(any())); - verifyNever(() => mockBackgroundService.digestFile(any())); - verifyNever(() => mockDeviceAssetRepository.updateAll(any())); - verifyNever(() => mockDeviceAssetRepository.deleteIds(any())); - - expect(result, [AssetStub.image1.copyWith(checksum: base64.encode(hash))]); - }); - - test("hashed successful when asset is modified", () async { - final (mockAsset, file, deviceAsset, hash) = await _createAssetMock(AssetStub.image1); - - when(() => mockBackgroundService.digestFiles([file.path])).thenAnswer((_) async => [hash]); - when( - () => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!]), - ).thenAnswer((_) async => [deviceAsset]); - - final result = await sut.hashAssets([mockAsset]); - - when(() => mockDeviceAssetRepository.transaction(any())).thenAnswer((_) async { - final capturedCallback = verify(() => mockDeviceAssetRepository.transaction(captureAny())).captured; - // Invoke the transaction callback - await (capturedCallback.firstOrNull as Future Function()?)?.call(); - verify( - () => mockDeviceAssetRepository.updateAll([ - deviceAsset.copyWith(modifiedTime: AssetStub.image1.fileModifiedAt), - ]), - ).called(1); - verify(() => mockDeviceAssetRepository.deleteIds([])).called(1); - }); - - verify(() => mockBackgroundService.digestFiles([file.path])).called(1); - - expect(result, [AssetStub.image1.copyWith(checksum: base64.encode(hash))]); - }); - }); - - group("HashService: Cleanup", () { - late Asset mockAsset; - late Uint8List hash; - late DeviceAsset deviceAsset; - late File file; - - setUp(() async { - (mockAsset, file, deviceAsset, hash) = await _createAssetMock(AssetStub.image1); - - when(() => mockBackgroundService.digestFiles([file.path])).thenAnswer((_) async => [hash]); - when( - () => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!]), - ).thenAnswer((_) async => [deviceAsset]); - }); - - test("cleanups DeviceAsset when local file cannot be obtained", () async { - when(() => mockAsset.local).thenThrow(Exception("File not found")); - final result = await sut.hashAssets([mockAsset]); - - verifyNever(() => mockBackgroundService.digestFiles(any())); - verifyNever(() => mockBackgroundService.digestFile(any())); - verifyNever(() => mockDeviceAssetRepository.updateAll(any())); - verify(() => mockDeviceAssetRepository.deleteIds([AssetStub.image1.localId!])).called(1); - - expect(result, isEmpty); - }); - - test("cleanups DeviceAsset when hashing failed", () async { - when(() => mockDeviceAssetRepository.transaction(any())).thenAnswer((_) async { - final capturedCallback = verify(() => mockDeviceAssetRepository.transaction(captureAny())).captured; - // Invoke the transaction callback - await (capturedCallback.firstOrNull as Future Function()?)?.call(); - - // Verify the callback inside the transaction because, doing it outside results - // in a small delay before the callback is invoked, resulting in other LOCs getting executed - // resulting in an incorrect state - // - // i.e, consider the following piece of code - // await _deviceAssetRepository.transaction(() async { - // await _deviceAssetRepository.updateAll(toBeAdded); - // await _deviceAssetRepository.deleteIds(toBeDeleted); - // }); - // toBeDeleted.clear(); - // since the transaction method is mocked, the callback is not invoked until it is captured - // and executed manually in the next event loop. However, the toBeDeleted.clear() is executed - // immediately once the transaction stub is executed, resulting in the deleteIds method being - // called with an empty list. - // - // To avoid this, we capture the callback and execute it within the transaction stub itself - // and verify the results inside the transaction stub - verify(() => mockDeviceAssetRepository.updateAll([])).called(1); - verify(() => mockDeviceAssetRepository.deleteIds([AssetStub.image1.localId!])).called(1); - }); - - when(() => mockBackgroundService.digestFiles([file.path])).thenAnswer( - // Invalid hash, length != 20 - (_) async => [Uint8List.fromList(hash.slice(2).toList())], - ); - - final result = await sut.hashAssets([mockAsset]); - - verify(() => mockBackgroundService.digestFiles([file.path])).called(1); - expect(result, isEmpty); - }); - }); - - group("HashService: Batch processing", () { - test("processes assets in batches when size limit is reached", () async { - // Setup multiple assets with large file sizes - final (mock1, mock2, mock3) = await ( - _createAssetMock(AssetStub.image1), - _createAssetMock(AssetStub.image2), - _createAssetMock(AssetStub.image3), - ).wait; - - final (asset1, file1, deviceAsset1, hash1) = mock1; - final (asset2, file2, deviceAsset2, hash2) = mock2; - final (asset3, file3, deviceAsset3, hash3) = mock3; - - when(() => mockDeviceAssetRepository.getByIds(any())).thenAnswer((_) async => []); - - // Setup for multiple batch processing calls - when(() => mockBackgroundService.digestFiles([file1.path, file2.path])).thenAnswer((_) async => [hash1, hash2]); - when(() => mockBackgroundService.digestFiles([file3.path])).thenAnswer((_) async => [hash3]); - - final size = await file1.length() + await file2.length(); - - sut = HashService( - deviceAssetRepository: mockDeviceAssetRepository, - backgroundService: mockBackgroundService, - batchSizeLimit: size, - ); - final result = await sut.hashAssets([asset1, asset2, asset3]); - - // Verify multiple batch process calls - verify(() => mockBackgroundService.digestFiles([file1.path, file2.path])).called(1); - verify(() => mockBackgroundService.digestFiles([file3.path])).called(1); - - expect(result, [ - AssetStub.image1.copyWith(checksum: base64.encode(hash1)), - AssetStub.image2.copyWith(checksum: base64.encode(hash2)), - AssetStub.image3.copyWith(checksum: base64.encode(hash3)), - ]); - }); - - test("processes assets in batches when file limit is reached", () async { - // Setup multiple assets with large file sizes - final (mock1, mock2, mock3) = await ( - _createAssetMock(AssetStub.image1), - _createAssetMock(AssetStub.image2), - _createAssetMock(AssetStub.image3), - ).wait; - - final (asset1, file1, deviceAsset1, hash1) = mock1; - final (asset2, file2, deviceAsset2, hash2) = mock2; - final (asset3, file3, deviceAsset3, hash3) = mock3; - - when(() => mockDeviceAssetRepository.getByIds(any())).thenAnswer((_) async => []); - - when(() => mockBackgroundService.digestFiles([file1.path])).thenAnswer((_) async => [hash1]); - when(() => mockBackgroundService.digestFiles([file2.path])).thenAnswer((_) async => [hash2]); - when(() => mockBackgroundService.digestFiles([file3.path])).thenAnswer((_) async => [hash3]); - - sut = HashService( - deviceAssetRepository: mockDeviceAssetRepository, - backgroundService: mockBackgroundService, - batchFileLimit: 1, - ); - final result = await sut.hashAssets([asset1, asset2, asset3]); - - // Verify multiple batch process calls - verify(() => mockBackgroundService.digestFiles([file1.path])).called(1); - verify(() => mockBackgroundService.digestFiles([file2.path])).called(1); - verify(() => mockBackgroundService.digestFiles([file3.path])).called(1); - - expect(result, [ - AssetStub.image1.copyWith(checksum: base64.encode(hash1)), - AssetStub.image2.copyWith(checksum: base64.encode(hash2)), - AssetStub.image3.copyWith(checksum: base64.encode(hash3)), - ]); - }); - - test("HashService: Sort & Process different states", () async { - final (asset1, file1, deviceAsset1, hash1) = await _createAssetMock(AssetStub.image1); // Will need rehashing - final (asset2, file2, deviceAsset2, hash2) = await _createAssetMock(AssetStub.image2); // Will have matching hash - final (asset3, file3, deviceAsset3, hash3) = await _createAssetMock(AssetStub.image3); // No DB entry - final asset4 = AssetStub.image3.copyWith(localId: "image4"); // Cannot be hashed - - when(() => mockBackgroundService.digestFiles([file1.path, file3.path])).thenAnswer((_) async => [hash1, hash3]); - // DB entries are not sorted and a dummy entry added - when( - () => mockDeviceAssetRepository.getByIds([ - AssetStub.image1.localId!, - AssetStub.image2.localId!, - AssetStub.image3.localId!, - asset4.localId!, - ]), - ).thenAnswer( - (_) async => [ - // Same timestamp to reuse deviceAsset - deviceAsset2.copyWith(modifiedTime: asset2.fileModifiedAt), - deviceAsset1, - deviceAsset3.copyWith(assetId: asset4.localId!), - ], - ); - - final result = await sut.hashAssets([asset1, asset2, asset3, asset4]); - - // Verify correct processing of all assets - verify(() => mockBackgroundService.digestFiles([file1.path, file3.path])).called(1); - expect(result.length, 3); - expect(result, [ - AssetStub.image2.copyWith(checksum: base64.encode(hash2)), - AssetStub.image1.copyWith(checksum: base64.encode(hash1)), - AssetStub.image3.copyWith(checksum: base64.encode(hash3)), - ]); - }); - - group("HashService: Edge cases", () { - test("handles empty list of assets", () async { - when(() => mockDeviceAssetRepository.getByIds(any())).thenAnswer((_) async => []); - - final result = await sut.hashAssets([]); - - verifyNever(() => mockBackgroundService.digestFiles(any())); - verifyNever(() => mockDeviceAssetRepository.updateAll(any())); - verifyNever(() => mockDeviceAssetRepository.deleteIds(any())); - - expect(result, isEmpty); - }); - - test("handles all file access failures", () async { - // No DB entries - when( - () => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!, AssetStub.image2.localId!]), - ).thenAnswer((_) async => []); - - final result = await sut.hashAssets([AssetStub.image1, AssetStub.image2]); - - verifyNever(() => mockBackgroundService.digestFiles(any())); - verifyNever(() => mockDeviceAssetRepository.updateAll(any())); - expect(result, isEmpty); - }); - }); - }); -} - -Future<(Asset, File, DeviceAsset, Uint8List)> _createAssetMock(Asset asset) async { - final random = Random(); - final hash = Uint8List.fromList(List.generate(20, (i) => random.nextInt(255))); - final mockAsset = MockAsset(); - final mockAssetEntity = MockAssetEntity(); - final fs = MemoryFileSystem(); - final deviceAsset = DeviceAsset( - assetId: asset.localId!, - hash: Uint8List.fromList(hash), - modifiedTime: DateTime.now(), - ); - final tmp = await fs.systemTempDirectory.createTemp(); - final file = tmp.childFile("${asset.fileName}-path"); - await file.writeAsString("${asset.fileName}-content"); - - when(() => mockAsset.localId).thenReturn(asset.localId); - when(() => mockAsset.fileName).thenReturn(asset.fileName); - when(() => mockAsset.fileCreatedAt).thenReturn(asset.fileCreatedAt); - when(() => mockAsset.fileModifiedAt).thenReturn(asset.fileModifiedAt); - when( - () => mockAsset.copyWith(checksum: any(named: "checksum")), - ).thenReturn(asset.copyWith(checksum: base64.encode(hash))); - when(() => mockAsset.local).thenAnswer((_) => mockAssetEntity); - when(() => mockAssetEntity.originFile).thenAnswer((_) async => file); - - return (mockAsset, file, deviceAsset, hash); -} diff --git a/mobile/test/test_utils.dart b/mobile/test/test_utils.dart index 30d4e2e6d4..75a41b46fb 100644 --- a/mobile/test/test_utils.dart +++ b/mobile/test/test_utils.dart @@ -4,82 +4,13 @@ import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:fake_async/fake_async.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as domain; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/android_device_asset.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:isar/isar.dart'; -import 'package:mocktail/mocktail.dart'; import 'mock_http_override.dart'; -// Listener Mock to test when a provider notifies its listeners -class ListenerMock extends Mock { - void call(T? previous, T next); -} - abstract final class TestUtils { const TestUtils._(); - /// Downloads Isar binaries (if required) and initializes a new Isar db - static Future initIsar() async { - await Isar.initializeIsarCore(download: true); - - final instance = Isar.getInstance(); - if (instance != null) { - return instance; - } - - final db = await Isar.open( - [ - StoreValueSchema, - ExifInfoSchema, - AssetSchema, - AlbumSchema, - UserSchema, - BackupAlbumSchema, - DuplicatedAssetSchema, - ETagSchema, - AndroidDeviceAssetSchema, - IOSDeviceAssetSchema, - DeviceAssetEntitySchema, - ], - directory: "test/", - maxSizeMiB: 1024, - inspector: false, - ); - - // Clear and close db on test end - addTearDown(() async { - await db.writeTxn(() async => await db.clear()); - await db.close(); - }); - return db; - } - - /// Creates a new ProviderContainer to test Riverpod providers - static ProviderContainer createContainer({ - ProviderContainer? parent, - List overrides = const [], - List? observers, - }) { - final container = ProviderContainer(parent: parent, overrides: overrides, observers: observers); - - // Dispose on test end - addTearDown(container.dispose); - - return container; - } - static void init() { // Turn off easy localization logging EasyLocalization.logger.enableBuildModes = []; diff --git a/mobile/test/test_utils/medium_factory.dart b/mobile/test/test_utils/medium_factory.dart index 50e73e5b5e..c8c41bbf0f 100644 --- a/mobile/test/test_utils/medium_factory.dart +++ b/mobile/test/test_utils/medium_factory.dart @@ -1,7 +1,6 @@ import 'dart:math'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; @@ -10,28 +9,6 @@ class MediumFactory { const MediumFactory(Drift db) : _db = db; - LocalAsset localAsset({ - String? id, - String? name, - AssetType? type, - DateTime? createdAt, - DateTime? updatedAt, - String? checksum, - }) { - final random = Random(); - - return LocalAsset( - id: id ?? '${random.nextInt(1000000)}', - name: name ?? 'Asset ${random.nextInt(1000000)}', - checksum: checksum ?? '${random.nextInt(1000000)}', - type: type ?? AssetType.image, - createdAt: createdAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)), - updatedAt: updatedAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)), - playbackStyle: AssetPlaybackStyle.image, - isEdited: false, - ); - } - LocalAlbum localAlbum({ String? id, String? name, diff --git a/mobile/test/utils/editor_test.dart b/mobile/test/utils/editor_test.dart new file mode 100644 index 0000000000..16f1c08d05 --- /dev/null +++ b/mobile/test/utils/editor_test.dart @@ -0,0 +1,322 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart'; +import 'package:immich_mobile/utils/editor.utils.dart'; +import 'package:openapi/api.dart' show MirrorAxis, MirrorParameters, RotateParameters; + +List normalizedToEdits(NormalizedTransform transform) { + List edits = []; + + if (transform.mirrorHorizontal) { + edits.add(MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal))); + } + + if (transform.mirrorVertical) { + edits.add(MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical))); + } + + if (transform.rotation != 0) { + edits.add(RotateEdit(RotateParameters(angle: transform.rotation))); + } + + return edits; +} + +bool compareEditAffines(List editsA, List editsB) { + final normA = buildAffineFromEdits(editsA); + final normB = buildAffineFromEdits(editsB); + + return ((normA.a - normB.a).abs() < 0.0001 && + (normA.b - normB.b).abs() < 0.0001 && + (normA.c - normB.c).abs() < 0.0001 && + (normA.d - normB.d).abs() < 0.0001); +} + +void main() { + group('normalizeEdits', () { + test('should handle no edits', () { + final edits = []; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle a single 90° rotation', () { + final edits = [ + RotateEdit(RotateParameters(angle: 90)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle a single 180° rotation', () { + final edits = [ + RotateEdit(RotateParameters(angle: 180)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle a single 270° rotation', () { + final edits = [ + RotateEdit(RotateParameters(angle: 270)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle a single horizontal mirror', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle a single vertical mirror', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 90° rotation + horizontal mirror', () { + final edits = [ + RotateEdit(RotateParameters(angle: 90)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 90° rotation + vertical mirror', () { + final edits = [ + RotateEdit(RotateParameters(angle: 90)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 90° rotation + both mirrors', () { + final edits = [ + RotateEdit(RotateParameters(angle: 90)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 180° rotation + horizontal mirror', () { + final edits = [ + RotateEdit(RotateParameters(angle: 180)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 180° rotation + vertical mirror', () { + final edits = [ + RotateEdit(RotateParameters(angle: 180)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 180° rotation + both mirrors', () { + final edits = [ + RotateEdit(RotateParameters(angle: 180)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 270° rotation + horizontal mirror', () { + final edits = [ + RotateEdit(RotateParameters(angle: 270)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 270° rotation + vertical mirror', () { + final edits = [ + RotateEdit(RotateParameters(angle: 270)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 270° rotation + both mirrors', () { + final edits = [ + RotateEdit(RotateParameters(angle: 270)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle horizontal mirror + 90° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + RotateEdit(RotateParameters(angle: 90)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle horizontal mirror + 180° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + RotateEdit(RotateParameters(angle: 180)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle horizontal mirror + 270° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + RotateEdit(RotateParameters(angle: 270)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle vertical mirror + 90° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + RotateEdit(RotateParameters(angle: 90)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle vertical mirror + 180° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + RotateEdit(RotateParameters(angle: 180)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle vertical mirror + 270° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + RotateEdit(RotateParameters(angle: 270)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle both mirrors + 90° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + RotateEdit(RotateParameters(angle: 90)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle both mirrors + 180° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + RotateEdit(RotateParameters(angle: 180)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle both mirrors + 270° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + RotateEdit(RotateParameters(angle: 270)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + }); +} diff --git a/open-api/bin/generate-open-api.sh b/open-api/bin/generate-open-api.sh index 522063185f..99adf9f4a8 100755 --- a/open-api/bin/generate-open-api.sh +++ b/open-api/bin/generate-open-api.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash OPENAPI_GENERATOR_VERSION=v7.12.0 +set -euo pipefail + # usage: ./bin/generate-open-api.sh function dart { @@ -21,6 +23,7 @@ function dart { patch --no-backup-if-mismatch -u ../mobile/openapi/lib/api_client.dart <./patch/api_client.dart.patch patch --no-backup-if-mismatch -u ../mobile/openapi/lib/api.dart <./patch/api.dart.patch patch --no-backup-if-mismatch -u ../mobile/openapi/pubspec.yaml <./patch/pubspec_immich_mobile.yaml.patch + patch --no-backup-if-mismatch -u ../mobile/openapi/lib/model/asset_edit_action_item_dto.dart <./patch/asset_edit_action_item_dto.dart.patch # Don't include analysis_options.yaml for the generated openapi files # so that language servers can properly exclude the mobile/openapi directory rm ../mobile/openapi/analysis_options.yaml @@ -39,9 +42,9 @@ function typescript { pnpm --filter immich sync:open-api ) -if [[ $1 == 'dart' ]]; then +if [[ $# -ge 1 ]] && [[ $1 == 'dart' ]]; then dart -elif [[ $1 == 'typescript' ]]; then +elif [[ $# -ge 1 ]] && [[ $1 == 'typescript' ]]; then typescript else dart diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 90d151a2a3..33a692474a 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -11,8 +11,12 @@ "required": true, "in": "query", "description": "Album ID", + "x-nestjs_zod-parent-metadata": { + "description": "Activity search" + }, "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -21,8 +25,12 @@ "required": false, "in": "query", "description": "Asset ID (if activity is for an asset)", + "x-nestjs_zod-parent-metadata": { + "description": "Activity search" + }, "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -30,7 +38,9 @@ "name": "level", "required": false, "in": "query", - "description": "Filter by activity level", + "x-nestjs_zod-parent-metadata": { + "description": "Activity search" + }, "schema": { "$ref": "#/components/schemas/ReactionLevel" } @@ -39,7 +49,9 @@ "name": "type", "required": false, "in": "query", - "description": "Filter by activity type", + "x-nestjs_zod-parent-metadata": { + "description": "Activity search" + }, "schema": { "$ref": "#/components/schemas/ReactionType" } @@ -49,8 +61,12 @@ "required": false, "in": "query", "description": "Filter by user ID", + "x-nestjs_zod-parent-metadata": { + "description": "Activity search" + }, "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -171,8 +187,12 @@ "required": true, "in": "query", "description": "Album ID", + "x-nestjs_zod-parent-metadata": { + "description": "Activity" + }, "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -181,8 +201,12 @@ "required": false, "in": "query", "description": "Asset ID (if activity is for an asset)", + "x-nestjs_zod-parent-metadata": { + "description": "Activity" + }, "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -243,6 +267,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -512,7 +537,7 @@ "required": true, "in": "path", "schema": { - "format": "string", + "pattern": "^[a-zA-Z0-9_\\-.]+$", "type": "string" } } @@ -936,6 +961,7 @@ "description": "User ID filter", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -1068,6 +1094,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -1137,6 +1164,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -1196,6 +1224,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -1267,6 +1296,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -1326,6 +1356,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -1397,6 +1428,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -1458,6 +1490,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -1522,6 +1555,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -1547,7 +1581,6 @@ "name": "visibility", "required": false, "in": "query", - "description": "Filter by visibility", "schema": { "$ref": "#/components/schemas/AssetVisibility" } @@ -1611,6 +1644,7 @@ "description": "Filter albums containing this asset ID (ignores shared parameter)", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -1734,24 +1768,7 @@ "put": { "description": "Send a list of asset IDs and album IDs to add each asset to each album.", "operationId": "addAssetsToAlbums", - "parameters": [ - { - "name": "key", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "slug", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - } - ], + "parameters": [], "requestBody": { "content": { "application/json": { @@ -1868,6 +1885,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -1919,6 +1937,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -1937,15 +1956,6 @@ "schema": { "type": "string" } - }, - { - "name": "withoutAssets", - "required": false, - "in": "query", - "description": "Exclude assets from response", - "schema": { - "type": "boolean" - } } ], "responses": { @@ -2002,6 +2012,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -2072,6 +2083,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -2143,22 +2155,7 @@ "in": "path", "schema": { "format": "uuid", - "type": "string" - } - }, - { - "name": "key", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "slug", - "required": false, - "in": "query", - "schema": { + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -2221,6 +2218,77 @@ "x-immich-state": "Stable" } }, + "/albums/{id}/map-markers": { + "get": { + "description": "Retrieve map marker information for a specific album by its ID.", + "operationId": "getAlbumMapMarkers", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + }, + { + "name": "key", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "slug", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/MapMarkerResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Retrieve album map markers", + "tags": [ + "Albums" + ], + "x-immich-history": [ + { + "version": "v3", + "state": "Added" + } + ], + "x-immich-permission": "album.read" + } + }, "/albums/{id}/user/{userId}": { "delete": { "description": "Remove a user from an album. Use an ID of \"me\" to leave a shared album.", @@ -2232,6 +2300,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -2291,6 +2360,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -2362,6 +2432,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -2432,7 +2503,7 @@ "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/APIKeyResponseDto" + "$ref": "#/components/schemas/ApiKeyResponseDto" }, "type": "array" } @@ -2481,7 +2552,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/APIKeyCreateDto" + "$ref": "#/components/schemas/ApiKeyCreateDto" } } }, @@ -2492,7 +2563,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/APIKeyCreateResponseDto" + "$ref": "#/components/schemas/ApiKeyCreateResponseDto" } } }, @@ -2542,7 +2613,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/APIKeyResponseDto" + "$ref": "#/components/schemas/ApiKeyResponseDto" } } }, @@ -2592,6 +2663,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -2643,6 +2715,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -2652,7 +2725,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/APIKeyResponseDto" + "$ref": "#/components/schemas/ApiKeyResponseDto" } } }, @@ -2701,6 +2774,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -2709,7 +2783,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/APIKeyUpdateDto" + "$ref": "#/components/schemas/ApiKeyUpdateDto" } } }, @@ -2720,7 +2794,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/APIKeyResponseDto" + "$ref": "#/components/schemas/ApiKeyResponseDto" } } }, @@ -3072,126 +3146,6 @@ "x-immich-state": "Stable" } }, - "/assets/device/{deviceId}": { - "get": { - "deprecated": true, - "description": "Get all asset of a device that are in the database, ID only.", - "operationId": "getAllUserAssetsByDeviceId", - "parameters": [ - { - "name": "deviceId", - "required": true, - "in": "path", - "description": "Device ID", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "type": "string" - }, - "type": "array" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "summary": "Retrieve assets by device ID", - "tags": [ - "Assets", - "Deprecated" - ], - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v2", - "state": "Deprecated" - } - ], - "x-immich-state": "Deprecated" - } - }, - "/assets/exist": { - "post": { - "description": "Checks if multiple assets exist on the server and returns all existing - used by background backup", - "operationId": "checkExistingAssets", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CheckExistingAssetsDto" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CheckExistingAssetsResponseDto" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "summary": "Check existing assets", - "tags": [ - "Assets" - ], - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v1", - "state": "Beta" - }, - { - "version": "v2", - "state": "Stable" - } - ], - "x-immich-permission": "asset.upload", - "x-immich-state": "Stable" - } - }, "/assets/jobs": { "post": { "description": "Run a specific job on a set of assets.", @@ -3351,69 +3305,6 @@ "x-immich-state": "Beta" } }, - "/assets/random": { - "get": { - "deprecated": true, - "description": "Retrieve a specified number of random assets for the authenticated user.", - "operationId": "getRandom", - "parameters": [ - { - "name": "count", - "required": false, - "in": "query", - "description": "Number of random assets to return", - "schema": { - "minimum": 1, - "type": "number" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/AssetResponseDto" - }, - "type": "array" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "summary": "Get random assets", - "tags": [ - "Assets", - "Deprecated" - ], - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v1", - "state": "Deprecated", - "replacementId": "searchAssets" - } - ], - "x-immich-permission": "asset.read", - "x-immich-state": "Deprecated" - } - }, "/assets/statistics": { "get": { "description": "Retrieve various statistics about the assets owned by the authenticated user.", @@ -3441,7 +3332,6 @@ "name": "visibility", "required": false, "in": "query", - "description": "Filter by visibility", "schema": { "$ref": "#/components/schemas/AssetVisibility" } @@ -3503,6 +3393,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -3577,6 +3468,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -3647,6 +3539,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -3694,6 +3587,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -3748,6 +3642,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -3814,6 +3709,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -3875,6 +3771,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -3949,6 +3846,7 @@ "description": "Asset ID", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -4010,6 +3908,7 @@ "description": "Asset ID", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -4079,6 +3978,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -4152,6 +4052,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -4216,89 +4117,6 @@ ], "x-immich-permission": "asset.download", "x-immich-state": "Stable" - }, - "put": { - "deprecated": true, - "description": "Replace the asset with new file, without changing its id.", - "operationId": "replaceAsset", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "name": "key", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "slug", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/AssetMediaReplaceDto" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AssetMediaResponseDto" - } - } - }, - "description": "Asset replaced successfully" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "summary": "Replace asset", - "tags": [ - "Assets", - "Deprecated" - ], - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v1", - "state": "Deprecated", - "replacementId": "copyAsset" - } - ], - "x-immich-permission": "asset.replace", - "x-immich-state": "Deprecated" } }, "/assets/{id}/thumbnail": { @@ -4322,6 +4140,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -4337,7 +4156,13 @@ "name": "size", "required": false, "in": "query", - "description": "Asset media size", + "x-immich-history": [ + { + "version": "v3", + "state": "Updated", + "description": "Specifying 'original' is deprecated. Use the original endpoint directly instead" + } + ], "schema": { "$ref": "#/components/schemas/AssetMediaSize" } @@ -4408,6 +4233,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -5355,6 +5181,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -5409,6 +5236,7 @@ "description": "Face ID", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -5523,6 +5351,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -5584,6 +5413,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -5762,7 +5592,6 @@ "name": "name", "required": true, "in": "path", - "description": "Queue name", "schema": { "$ref": "#/components/schemas/QueueName" } @@ -5953,6 +5782,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -6005,6 +5835,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -6064,6 +5895,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -6135,6 +5967,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -6189,6 +6022,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -6250,6 +6084,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -6321,6 +6156,8 @@ "description": "Filter assets created after this date", "schema": { "format": "date-time", + "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))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -6331,6 +6168,8 @@ "description": "Filter assets created before this date", "schema": { "format": "date-time", + "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))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -6505,6 +6344,8 @@ "description": "Filter by date", "schema": { "format": "date-time", + "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))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -6530,7 +6371,6 @@ "name": "order", "required": false, "in": "query", - "description": "Sort order", "schema": { "$ref": "#/components/schemas/MemorySearchOrder" } @@ -6542,6 +6382,7 @@ "description": "Number of memories to return", "schema": { "minimum": 1, + "maximum": 9007199254740991, "type": "integer" } }, @@ -6549,7 +6390,6 @@ "name": "type", "required": false, "in": "query", - "description": "Memory type", "schema": { "$ref": "#/components/schemas/MemoryType" } @@ -6673,6 +6513,8 @@ "description": "Filter by date", "schema": { "format": "date-time", + "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))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -6698,7 +6540,6 @@ "name": "order", "required": false, "in": "query", - "description": "Sort order", "schema": { "$ref": "#/components/schemas/MemorySearchOrder" } @@ -6710,6 +6551,7 @@ "description": "Number of memories to return", "schema": { "minimum": 1, + "maximum": 9007199254740991, "type": "integer" } }, @@ -6717,7 +6559,6 @@ "name": "type", "required": false, "in": "query", - "description": "Memory type", "schema": { "$ref": "#/components/schemas/MemoryType" } @@ -6779,6 +6620,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -6830,6 +6672,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -6888,6 +6731,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -6958,6 +6802,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -7029,6 +6874,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -7154,6 +7000,7 @@ "description": "Filter by notification ID", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -7161,7 +7008,6 @@ "name": "level", "required": false, "in": "query", - "description": "Filter by notification level", "schema": { "$ref": "#/components/schemas/NotificationLevel" } @@ -7170,7 +7016,6 @@ "name": "type", "required": false, "in": "query", - "description": "Filter by notification type", "schema": { "$ref": "#/components/schemas/NotificationType" } @@ -7295,6 +7140,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -7346,6 +7192,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -7404,6 +7251,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -7707,7 +7555,6 @@ "name": "direction", "required": true, "in": "query", - "description": "Partner direction", "schema": { "$ref": "#/components/schemas/PartnerDirection" } @@ -7830,6 +7677,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -7882,6 +7730,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -7938,6 +7787,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -8060,6 +7910,7 @@ "description": "Closest asset ID for similarity search", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -8070,6 +7921,7 @@ "description": "Closest person ID for similarity search", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -8281,6 +8133,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -8332,6 +8185,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -8390,6 +8244,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -8460,6 +8315,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -8533,6 +8389,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -8606,6 +8463,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -8666,6 +8524,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -8825,6 +8684,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -8929,7 +8789,6 @@ "name": "name", "required": true, "in": "path", - "description": "Queue name", "schema": { "$ref": "#/components/schemas/QueueName" } @@ -8984,7 +8843,6 @@ "name": "name", "required": true, "in": "path", - "description": "Queue name", "schema": { "$ref": "#/components/schemas/QueueName" } @@ -9051,7 +8909,6 @@ "name": "name", "required": true, "in": "path", - "description": "Queue name", "schema": { "$ref": "#/components/schemas/QueueName" } @@ -9109,7 +8966,6 @@ "name": "name", "required": true, "in": "path", - "description": "Queue name", "schema": { "$ref": "#/components/schemas/QueueName" } @@ -9292,7 +9148,8 @@ "type": "array", "items": { "type": "string", - "format": "uuid" + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$" } } }, @@ -9302,8 +9159,8 @@ "in": "query", "description": "Filter by city name", "schema": { - "nullable": true, - "type": "string" + "type": "string", + "nullable": true } }, { @@ -9312,8 +9169,8 @@ "in": "query", "description": "Filter by country name", "schema": { - "nullable": true, - "type": "string" + "type": "string", + "nullable": true } }, { @@ -9323,6 +9180,8 @@ "description": "Filter by creation date (after)", "schema": { "format": "date-time", + "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))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -9333,15 +9192,8 @@ "description": "Filter by creation date (before)", "schema": { "format": "date-time", - "type": "string" - } - }, - { - "name": "deviceId", - "required": false, - "in": "query", - "description": "Device ID to filter by", - "schema": { + "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))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -9396,8 +9248,8 @@ "in": "query", "description": "Filter by lens model", "schema": { - "nullable": true, - "type": "string" + "type": "string", + "nullable": true } }, { @@ -9406,9 +9258,10 @@ "in": "query", "description": "Library ID to filter by", "schema": { + "type": "string", "format": "uuid", - "nullable": true, - "type": "string" + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "nullable": true } }, { @@ -9417,7 +9270,8 @@ "in": "query", "description": "Filter by camera make", "schema": { - "type": "string" + "type": "string", + "nullable": true } }, { @@ -9427,6 +9281,7 @@ "description": "Minimum file size in bytes", "schema": { "minimum": 0, + "maximum": 9007199254740991, "type": "integer" } }, @@ -9436,8 +9291,8 @@ "in": "query", "description": "Filter by camera model", "schema": { - "nullable": true, - "type": "string" + "type": "string", + "nullable": true } }, { @@ -9458,7 +9313,8 @@ "type": "array", "items": { "type": "string", - "format": "uuid" + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$" } } }, @@ -9484,10 +9340,10 @@ ], "x-immich-state": "Stable", "schema": { + "type": "number", "minimum": -1, "maximum": 5, - "nullable": true, - "type": "number" + "nullable": true } }, { @@ -9507,8 +9363,8 @@ "in": "query", "description": "Filter by state/province name", "schema": { - "nullable": true, - "type": "string" + "type": "string", + "nullable": true } }, { @@ -9517,12 +9373,13 @@ "in": "query", "description": "Filter by tag IDs", "schema": { - "nullable": true, "type": "array", "items": { "type": "string", - "format": "uuid" - } + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$" + }, + "nullable": true } }, { @@ -9532,6 +9389,8 @@ "description": "Filter by taken date (after)", "schema": { "format": "date-time", + "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))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -9542,6 +9401,8 @@ "description": "Filter by taken date (before)", "schema": { "format": "date-time", + "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))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -9552,6 +9413,8 @@ "description": "Filter by trash date (after)", "schema": { "format": "date-time", + "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))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -9562,6 +9425,8 @@ "description": "Filter by trash date (before)", "schema": { "format": "date-time", + "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))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -9569,7 +9434,6 @@ "name": "type", "required": false, "in": "query", - "description": "Asset type filter", "schema": { "$ref": "#/components/schemas/AssetTypeEnum" } @@ -9581,6 +9445,8 @@ "description": "Filter by update date (after)", "schema": { "format": "date-time", + "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))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -9591,6 +9457,8 @@ "description": "Filter by update date (before)", "schema": { "format": "date-time", + "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))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -9598,7 +9466,6 @@ "name": "visibility", "required": false, "in": "query", - "description": "Filter by visibility", "schema": { "$ref": "#/components/schemas/AssetVisibility" } @@ -10122,7 +9989,6 @@ "name": "type", "required": true, "in": "query", - "description": "Suggestion type", "schema": { "$ref": "#/components/schemas/SearchSuggestionType" } @@ -11014,6 +10880,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -11065,6 +10932,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -11135,6 +11003,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -11189,6 +11058,7 @@ "description": "Filter by album ID", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -11205,6 +11075,7 @@ ], "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -11400,16 +11271,6 @@ "type": "string" } }, - { - "name": "password", - "required": false, - "in": "query", - "description": "Link password", - "schema": { - "example": "password", - "type": "string" - } - }, { "name": "slug", "required": false, @@ -11417,15 +11278,6 @@ "schema": { "type": "string" } - }, - { - "name": "token", - "required": false, - "in": "query", - "description": "Access token", - "schema": { - "type": "string" - } } ], "responses": { @@ -11483,6 +11335,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -11534,6 +11387,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -11592,6 +11446,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -11662,6 +11517,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -11733,22 +11589,7 @@ "in": "path", "schema": { "format": "uuid", - "type": "string" - } - }, - { - "name": "key", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "slug", - "required": false, - "in": "query", - "schema": { + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -11807,6 +11648,7 @@ "state": "Stable" } ], + "x-immich-permission": "sharedLink.update", "x-immich-state": "Stable" } }, @@ -11873,6 +11715,7 @@ "description": "Filter by primary asset ID", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -11994,6 +11837,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -12045,6 +11889,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -12103,6 +11948,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -12173,6 +12019,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -12182,6 +12029,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -12379,123 +12227,6 @@ "x-immich-state": "Stable" } }, - "/sync/delta-sync": { - "post": { - "deprecated": true, - "description": "Retrieve changed assets since the last sync for the authenticated user.", - "operationId": "getDeltaSync", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AssetDeltaSyncDto" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AssetDeltaSyncResponseDto" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "summary": "Get delta sync for user", - "tags": [ - "Sync", - "Deprecated" - ], - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v2", - "state": "Deprecated" - } - ], - "x-immich-state": "Deprecated" - } - }, - "/sync/full-sync": { - "post": { - "deprecated": true, - "description": "Retrieve all assets for a full synchronization for the authenticated user.", - "operationId": "getFullSyncForUser", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AssetFullSyncDto" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/AssetResponseDto" - }, - "type": "array" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "summary": "Get full sync for user", - "tags": [ - "Sync", - "Deprecated" - ], - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v2", - "state": "Deprecated" - } - ], - "x-immich-state": "Deprecated" - } - }, "/sync/stream": { "post": { "description": "Retrieve a JSON lines streamed response of changes for synchronization. This endpoint is used by the mobile app to efficiently stay up to date with changes.", @@ -13209,6 +12940,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -13260,6 +12992,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -13318,6 +13051,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -13388,6 +13122,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -13459,6 +13194,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -13533,6 +13269,7 @@ "description": "Filter assets belonging to a specific album", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -13588,6 +13325,7 @@ "description": "Filter assets containing a specific person (face recognition)", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -13606,6 +13344,7 @@ "description": "Filter assets with a specific tag", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -13613,7 +13352,7 @@ "name": "timeBucket", "required": true, "in": "query", - "description": "Time bucket identifier in YYYY-MM-DD format (e.g., \"2024-01-01\" for January 2024)", + "description": "Time bucket identifier in YYYY-MM-DD format", "schema": { "example": "2024-01-01", "type": "string" @@ -13626,6 +13365,7 @@ "description": "Filter assets by specific user ID", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -13719,6 +13459,7 @@ "description": "Filter assets belonging to a specific album", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -13774,6 +13515,7 @@ "description": "Filter assets containing a specific person (face recognition)", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -13792,6 +13534,7 @@ "description": "Filter assets with a specific tag", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -13802,6 +13545,7 @@ "description": "Filter assets by specific user ID", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -14149,7 +13893,7 @@ "x-immich-state": "Stable" }, "put": { - "description": "Update the current user making teh API request.", + "description": "Update the current user making the API request.", "operationId": "updateMyUser", "parameters": [], "requestBody": { @@ -14726,6 +14470,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -14786,6 +14531,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -15065,6 +14811,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -15112,6 +14859,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -15166,6 +14914,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -15403,106 +15152,19 @@ } }, "schemas": { - "APIKeyCreateDto": { - "properties": { - "name": { - "description": "API key name", - "type": "string" - }, - "permissions": { - "description": "List of permissions", - "items": { - "$ref": "#/components/schemas/Permission" - }, - "minItems": 1, - "type": "array" - } - }, - "required": [ - "permissions" - ], - "type": "object" - }, - "APIKeyCreateResponseDto": { - "properties": { - "apiKey": { - "$ref": "#/components/schemas/APIKeyResponseDto" - }, - "secret": { - "description": "API key secret (only shown once)", - "type": "string" - } - }, - "required": [ - "apiKey", - "secret" - ], - "type": "object" - }, - "APIKeyResponseDto": { - "properties": { - "createdAt": { - "description": "Creation date", - "format": "date-time", - "type": "string" - }, - "id": { - "description": "API key ID", - "type": "string" - }, - "name": { - "description": "API key name", - "type": "string" - }, - "permissions": { - "description": "List of permissions", - "items": { - "$ref": "#/components/schemas/Permission" - }, - "type": "array" - }, - "updatedAt": { - "description": "Last update date", - "format": "date-time", - "type": "string" - } - }, - "required": [ - "createdAt", - "id", - "name", - "permissions", - "updatedAt" - ], - "type": "object" - }, - "APIKeyUpdateDto": { - "properties": { - "name": { - "description": "API key name", - "type": "string" - }, - "permissions": { - "description": "List of permissions", - "items": { - "$ref": "#/components/schemas/Permission" - }, - "minItems": 1, - "type": "array" - } - }, - "type": "object" - }, "ActivityCreateDto": { + "description": "Activity create", "properties": { "albumId": { "description": "Album ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "assetId": { "description": "Asset ID (if activity is for an asset)", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "comment": { @@ -15510,12 +15172,7 @@ "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/ReactionType" - } - ], - "description": "Activity type (like or comment)" + "$ref": "#/components/schemas/ReactionType" } }, "required": [ @@ -15528,7 +15185,9 @@ "properties": { "assetId": { "description": "Asset ID (if activity is for an asset)", + "format": "uuid", "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "comment": { @@ -15538,20 +15197,19 @@ }, "createdAt": { "description": "Creation date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "id": { "description": "Activity ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/ReactionType" - } - ], - "description": "Activity type" + "$ref": "#/components/schemas/ReactionType" }, "user": { "$ref": "#/components/schemas/UserResponseDto" @@ -15570,10 +15228,14 @@ "properties": { "comments": { "description": "Number of comments", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "likes": { "description": "Number of likes", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" } }, @@ -15630,14 +15292,10 @@ }, "assetCount": { "description": "Number of assets", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, - "assets": { - "items": { - "$ref": "#/components/schemas/AssetResponseDto" - }, - "type": "array" - }, "contributorCounts": { "items": { "$ref": "#/components/schemas/ContributorCountResponseDto" @@ -15676,12 +15334,7 @@ "type": "string" }, "order": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetOrder" - } - ], - "description": "Asset sort order" + "$ref": "#/components/schemas/AssetOrder" }, "owner": { "$ref": "#/components/schemas/UserResponseDto" @@ -15710,7 +15363,6 @@ "albumThumbnailAssetId", "albumUsers", "assetCount", - "assets", "createdAt", "description", "hasSharedLink", @@ -15727,14 +15379,20 @@ "properties": { "notShared": { "description": "Number of non-shared albums", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "owned": { "description": "Number of owned albums", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "shared": { "description": "Number of shared albums", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" } }, @@ -15748,17 +15406,14 @@ "AlbumUserAddDto": { "properties": { "role": { - "allOf": [ - { - "$ref": "#/components/schemas/AlbumUserRole" - } - ], + "$ref": "#/components/schemas/AlbumUserRole", "default": "editor", "description": "Album user role" }, "userId": { "description": "User ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -15770,16 +15425,12 @@ "AlbumUserCreateDto": { "properties": { "role": { - "allOf": [ - { - "$ref": "#/components/schemas/AlbumUserRole" - } - ], - "description": "Album user role" + "$ref": "#/components/schemas/AlbumUserRole" }, "userId": { "description": "User ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -15792,12 +15443,7 @@ "AlbumUserResponseDto": { "properties": { "role": { - "allOf": [ - { - "$ref": "#/components/schemas/AlbumUserRole" - } - ], - "description": "Album user role" + "$ref": "#/components/schemas/AlbumUserRole" }, "user": { "$ref": "#/components/schemas/UserResponseDto" @@ -15823,6 +15469,7 @@ "description": "Album IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -15831,6 +15478,7 @@ "description": "Asset IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -15845,12 +15493,7 @@ "AlbumsAddAssetsResponseDto": { "properties": { "error": { - "allOf": [ - { - "$ref": "#/components/schemas/BulkIdErrorReason" - } - ], - "description": "Error reason" + "$ref": "#/components/schemas/BulkIdErrorReason" }, "success": { "description": "Operation success", @@ -15865,13 +15508,7 @@ "AlbumsResponse": { "properties": { "defaultAssetOrder": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetOrder" - } - ], - "default": "desc", - "description": "Default asset order for albums" + "$ref": "#/components/schemas/AssetOrder" } }, "required": [ @@ -15883,12 +15520,101 @@ "description": "Album preferences", "properties": { "defaultAssetOrder": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetOrder" - } - ], - "description": "Default asset order for albums" + "$ref": "#/components/schemas/AssetOrder" + } + }, + "type": "object" + }, + "ApiKeyCreateDto": { + "properties": { + "name": { + "description": "API key name", + "type": "string" + }, + "permissions": { + "description": "List of permissions", + "items": { + "$ref": "#/components/schemas/Permission" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "permissions" + ], + "type": "object" + }, + "ApiKeyCreateResponseDto": { + "properties": { + "apiKey": { + "$ref": "#/components/schemas/ApiKeyResponseDto" + }, + "secret": { + "description": "API key secret (only shown once)", + "type": "string" + } + }, + "required": [ + "apiKey", + "secret" + ], + "type": "object" + }, + "ApiKeyResponseDto": { + "properties": { + "createdAt": { + "description": "Creation date", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "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" + }, + "id": { + "description": "API key ID", + "type": "string" + }, + "name": { + "description": "API key name", + "type": "string" + }, + "permissions": { + "description": "List of permissions", + "items": { + "$ref": "#/components/schemas/Permission" + }, + "type": "array" + }, + "updatedAt": { + "description": "Last update date", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "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" + } + }, + "required": [ + "createdAt", + "id", + "name", + "permissions", + "updatedAt" + ], + "type": "object" + }, + "ApiKeyUpdateDto": { + "properties": { + "name": { + "description": "API key name", + "type": "string" + }, + "permissions": { + "description": "List of permissions", + "items": { + "$ref": "#/components/schemas/Permission" + }, + "minItems": 1, + "type": "array" } }, "type": "object" @@ -15903,6 +15629,7 @@ "description": "IDs to process", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -15936,6 +15663,7 @@ "description": "Asset IDs to update", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -15946,10 +15674,14 @@ }, "latitude": { "description": "Latitude coordinate", + "maximum": 90, + "minimum": -90, "type": "number" }, "longitude": { "description": "Longitude coordinate", + "maximum": 180, + "minimum": -180, "type": "number" }, "rating": { @@ -15957,7 +15689,7 @@ "maximum": 5, "minimum": -1, "nullable": true, - "type": "number", + "type": "integer", "x-immich-history": [ { "version": "v1", @@ -15980,12 +15712,7 @@ "type": "string" }, "visibility": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetVisibility" - } - ], - "description": "Asset visibility" + "$ref": "#/components/schemas/AssetVisibility" } }, "required": [ @@ -16043,12 +15770,7 @@ "AssetBulkUploadCheckResult": { "properties": { "action": { - "description": "Upload action", - "enum": [ - "accept", - "reject" - ], - "type": "string" + "$ref": "#/components/schemas/AssetUploadAction" }, "assetId": { "description": "Existing asset ID if duplicate", @@ -16063,12 +15785,7 @@ "type": "boolean" }, "reason": { - "description": "Rejection reason if rejected", - "enum": [ - "duplicate", - "unsupported-format" - ], - "type": "string" + "$ref": "#/components/schemas/AssetRejectReason" } }, "required": [ @@ -16102,6 +15819,7 @@ "sourceId": { "description": "Source asset ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "stack": { @@ -16112,6 +15830,7 @@ "targetId": { "description": "Target asset ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -16121,56 +15840,6 @@ ], "type": "object" }, - "AssetDeltaSyncDto": { - "properties": { - "updatedAfter": { - "description": "Sync assets updated after this date", - "format": "date-time", - "type": "string" - }, - "userIds": { - "description": "User IDs to sync", - "items": { - "format": "uuid", - "type": "string" - }, - "type": "array" - } - }, - "required": [ - "updatedAfter", - "userIds" - ], - "type": "object" - }, - "AssetDeltaSyncResponseDto": { - "properties": { - "deleted": { - "description": "Deleted asset IDs", - "items": { - "type": "string" - }, - "type": "array" - }, - "needsFullSync": { - "description": "Whether full sync is needed", - "type": "boolean" - }, - "upserted": { - "description": "Upserted assets", - "items": { - "$ref": "#/components/schemas/AssetResponseDto" - }, - "type": "array" - } - }, - "required": [ - "deleted", - "needsFullSync", - "upserted" - ], - "type": "object" - }, "AssetEditAction": { "description": "Type of edit action to perform", "enum": [ @@ -16183,12 +15852,7 @@ "AssetEditActionItemDto": { "properties": { "action": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetEditAction" - } - ], - "description": "Type of edit action to perform" + "$ref": "#/components/schemas/AssetEditAction" }, "parameters": { "anyOf": [ @@ -16214,15 +15878,12 @@ "AssetEditActionItemResponseDto": { "properties": { "action": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetEditAction" - } - ], - "description": "Type of edit action to perform" + "$ref": "#/components/schemas/AssetEditAction" }, "id": { + "description": "Asset edit ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "parameters": { @@ -16268,6 +15929,7 @@ "assetId": { "description": "Asset ID these edits belong to", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "edits": { @@ -16289,35 +15951,49 @@ "assetId": { "description": "Asset ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "height": { "description": "Face bounding box height", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "imageHeight": { "description": "Image height in pixels", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "imageWidth": { "description": "Image width in pixels", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "personId": { "description": "Person ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "width": { "description": "Face bounding box width", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "x": { "description": "Face bounding box X coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "y": { "description": "Face bounding box Y coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -16349,31 +16025,44 @@ "properties": { "boundingBoxX1": { "description": "Bounding box X1 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxX2": { "description": "Bounding box X2 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxY1": { "description": "Bounding box Y1 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxY2": { "description": "Bounding box Y2 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "id": { "description": "Face ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "imageHeight": { "description": "Image height in pixels", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "imageWidth": { "description": "Image width in pixels", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "person": { @@ -16382,16 +16071,10 @@ "$ref": "#/components/schemas/PersonResponseDto" } ], - "description": "Person associated with face", "nullable": true }, "sourceType": { - "allOf": [ - { - "$ref": "#/components/schemas/SourceType" - } - ], - "description": "Face detection source type" + "$ref": "#/components/schemas/SourceType" } }, "required": [ @@ -16426,11 +16109,13 @@ "assetId": { "description": "Asset ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "personId": { "description": "Person ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -16441,43 +16126,52 @@ "type": "object" }, "AssetFaceWithoutPersonResponseDto": { + "description": "Asset face without person", "properties": { "boundingBoxX1": { "description": "Bounding box X1 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxX2": { "description": "Bounding box X2 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxY1": { "description": "Bounding box Y1 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxY2": { "description": "Bounding box Y2 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "id": { "description": "Face ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "imageHeight": { "description": "Image height in pixels", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "imageWidth": { "description": "Image width in pixels", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "sourceType": { - "allOf": [ - { - "$ref": "#/components/schemas/SourceType" - } - ], - "description": "Face detection source type" + "$ref": "#/components/schemas/SourceType" } }, "required": [ @@ -16491,34 +16185,14 @@ ], "type": "object" }, - "AssetFullSyncDto": { - "properties": { - "lastId": { - "description": "Last asset ID (pagination)", - "format": "uuid", - "type": "string" - }, - "limit": { - "description": "Maximum number of assets to return", - "minimum": 1, - "type": "integer" - }, - "updatedUntil": { - "description": "Sync assets updated until this date", - "format": "date-time", - "type": "string" - }, - "userId": { - "description": "Filter by user ID", - "format": "uuid", - "type": "string" - } - }, - "required": [ - "limit", - "updatedUntil" + "AssetIdErrorReason": { + "description": "Error reason if failed", + "enum": [ + "duplicate", + "no_permission", + "not_found" ], - "type": "object" + "type": "string" }, "AssetIdsDto": { "properties": { @@ -16526,6 +16200,7 @@ "description": "Asset IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -16543,13 +16218,7 @@ "type": "string" }, "error": { - "description": "Error reason if failed", - "enum": [ - "duplicate", - "no_permission", - "not_found" - ], - "type": "string" + "$ref": "#/components/schemas/AssetIdErrorReason" }, "success": { "description": "Whether operation succeeded", @@ -16578,17 +16247,13 @@ "description": "Asset IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" }, "name": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetJobName" - } - ], - "description": "Job name" + "$ref": "#/components/schemas/AssetJobName" } }, "required": [ @@ -16604,26 +16269,22 @@ "format": "binary", "type": "string" }, - "deviceAssetId": { - "description": "Device asset ID", - "type": "string" - }, - "deviceId": { - "description": "Device ID", - "type": "string" - }, "duration": { "description": "Duration (for videos)", "type": "string" }, "fileCreatedAt": { "description": "File creation date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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 modification date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "filename": { @@ -16637,6 +16298,7 @@ "livePhotoVideoId": { "description": "Live photo video ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "metadata": { @@ -16652,61 +16314,11 @@ "type": "string" }, "visibility": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetVisibility" - } - ], - "description": "Asset visibility" + "$ref": "#/components/schemas/AssetVisibility" } }, "required": [ "assetData", - "deviceAssetId", - "deviceId", - "fileCreatedAt", - "fileModifiedAt" - ], - "type": "object" - }, - "AssetMediaReplaceDto": { - "properties": { - "assetData": { - "description": "Asset file data", - "format": "binary", - "type": "string" - }, - "deviceAssetId": { - "description": "Device asset ID", - "type": "string" - }, - "deviceId": { - "description": "Device ID", - "type": "string" - }, - "duration": { - "description": "Duration (for videos)", - "type": "string" - }, - "fileCreatedAt": { - "description": "File creation date", - "format": "date-time", - "type": "string" - }, - "fileModifiedAt": { - "description": "File modification date", - "format": "date-time", - "type": "string" - }, - "filename": { - "description": "Filename", - "type": "string" - } - }, - "required": [ - "assetData", - "deviceAssetId", - "deviceId", "fileCreatedAt", "fileModifiedAt" ], @@ -16719,12 +16331,7 @@ "type": "string" }, "status": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetMediaStatus" - } - ], - "description": "Upload status" + "$ref": "#/components/schemas/AssetMediaStatus" } }, "required": [ @@ -16734,6 +16341,7 @@ "type": "object" }, "AssetMediaSize": { + "description": "Asset media size", "enum": [ "original", "fullsize", @@ -16771,6 +16379,7 @@ "assetId": { "description": "Asset ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "key": { @@ -16796,10 +16405,13 @@ }, "updatedAt": { "description": "Last update date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "value": { + "additionalProperties": {}, "description": "Metadata value (object)", "type": "object" } @@ -16832,6 +16444,7 @@ "assetId": { "description": "Asset ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "key": { @@ -16839,6 +16452,7 @@ "type": "string" }, "value": { + "additionalProperties": {}, "description": "Metadata value (object)", "type": "object" } @@ -16858,10 +16472,13 @@ }, "updatedAt": { "description": "Last update date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "value": { + "additionalProperties": {}, "description": "Metadata value (object)", "type": "object" } @@ -16895,6 +16512,7 @@ "type": "string" }, "value": { + "additionalProperties": {}, "description": "Metadata value (object)", "type": "object" } @@ -16909,6 +16527,7 @@ "properties": { "assetId": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "boxScore": { @@ -16918,6 +16537,7 @@ }, "id": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "text": { @@ -16995,6 +16615,14 @@ ], "type": "string" }, + "AssetRejectReason": { + "description": "Rejection reason if rejected", + "enum": [ + "duplicate", + "unsupported-format" + ], + "type": "string" + }, "AssetResponseDto": { "properties": { "checksum": { @@ -17003,18 +16631,9 @@ }, "createdAt": { "description": "The UTC timestamp when the asset was originally uploaded to Immich.", - "example": "2024-01-15T20:30:00.000Z", "format": "date-time", "type": "string" }, - "deviceAssetId": { - "description": "Device asset ID", - "type": "string" - }, - "deviceId": { - "description": "Device ID", - "type": "string" - }, "duplicateId": { "description": "Duplicate group ID", "nullable": true, @@ -17029,13 +16648,11 @@ }, "fileCreatedAt": { "description": "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.", - "example": "2024-01-15T19:30:00.000Z", "format": "date-time", "type": "string" }, "fileModifiedAt": { "description": "The UTC timestamp when the file was last modified on the filesystem. This reflects the last time the physical file was changed, which may be different from when the photo was originally taken.", - "example": "2024-01-16T10:15:00.000Z", "format": "date-time", "type": "string" }, @@ -17045,6 +16662,7 @@ }, "height": { "description": "Asset height", + "minimum": 0, "nullable": true, "type": "number" }, @@ -17084,10 +16702,10 @@ "type": "boolean" }, "libraryId": { - "deprecated": true, "description": "Library ID", "format": "uuid", "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string", "x-immich-history": [ { @@ -17108,7 +16726,6 @@ }, "localDateTime": { "description": "The local date and time when the photo/video was taken, derived from EXIF metadata. This represents the photographer's local time regardless of timezone, stored as a timezone-agnostic timestamp. Used for timeline grouping by \"local\" days and months.", - "example": "2024-01-15T14:30:00.000Z", "format": "date-time", "type": "string" }, @@ -17138,7 +16755,6 @@ "type": "array" }, "resized": { - "deprecated": true, "description": "Is resized", "type": "boolean", "x-immich-history": [ @@ -17173,12 +16789,7 @@ "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetTypeEnum" - } - ], - "description": "Asset type" + "$ref": "#/components/schemas/AssetTypeEnum" }, "unassignedFaces": { "items": { @@ -17188,20 +16799,15 @@ }, "updatedAt": { "description": "The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified.", - "example": "2024-01-16T12:45:30.000Z", "format": "date-time", "type": "string" }, "visibility": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetVisibility" - } - ], - "description": "Asset visibility" + "$ref": "#/components/schemas/AssetVisibility" }, "width": { "description": "Asset width", + "minimum": 0, "nullable": true, "type": "number" } @@ -17209,8 +16815,6 @@ "required": [ "checksum", "createdAt", - "deviceAssetId", - "deviceId", "duration", "fileCreatedAt", "fileModifiedAt", @@ -17238,6 +16842,8 @@ "properties": { "assetCount": { "description": "Number of assets in stack", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "id": { @@ -17260,14 +16866,20 @@ "properties": { "images": { "description": "Number of images", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "total": { "description": "Total number of assets", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "videos": { "description": "Number of videos", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -17288,6 +16900,14 @@ ], "type": "string" }, + "AssetUploadAction": { + "description": "Upload action", + "enum": [ + "accept", + "reject" + ], + "type": "string" + }, "AssetVisibility": { "description": "Asset visibility", "enum": [ @@ -17342,12 +16962,7 @@ "AvatarUpdate": { "properties": { "color": { - "allOf": [ - { - "$ref": "#/components/schemas/UserAvatarColor" - } - ], - "description": "Avatar color" + "$ref": "#/components/schemas/UserAvatarColor" } }, "type": "object" @@ -17366,15 +16981,7 @@ "BulkIdResponseDto": { "properties": { "error": { - "description": "Error reason if failed", - "enum": [ - "duplicate", - "no_permission", - "not_found", - "unknown", - "validation" - ], - "type": "string" + "$ref": "#/components/schemas/BulkIdErrorReason" }, "errorMessage": { "type": "string" @@ -17400,6 +17007,7 @@ "description": "IDs to process", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -17439,7 +17047,6 @@ "CastResponse": { "properties": { "gCastEnabled": { - "default": false, "description": "Whether Google Cast is enabled", "type": "boolean" } @@ -17483,42 +17090,6 @@ ], "type": "object" }, - "CheckExistingAssetsDto": { - "properties": { - "deviceAssetIds": { - "description": "Device asset IDs to check", - "items": { - "type": "string" - }, - "minItems": 1, - "type": "array" - }, - "deviceId": { - "description": "Device ID", - "type": "string" - } - }, - "required": [ - "deviceAssetIds", - "deviceId" - ], - "type": "object" - }, - "CheckExistingAssetsResponseDto": { - "properties": { - "existingIds": { - "description": "Existing asset IDs", - "items": { - "type": "string" - }, - "type": "array" - } - }, - "required": [ - "existingIds" - ], - "type": "object" - }, "Colorspace": { "description": "Colorspace", "enum": [ @@ -17531,6 +17102,8 @@ "properties": { "assetCount": { "description": "Number of assets contributed", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "userId": { @@ -17561,6 +17134,7 @@ "description": "Initial asset IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -17583,8 +17157,7 @@ "type": "string" }, "maxItems": 128, - "type": "array", - "uniqueItems": true + "type": "array" }, "importPaths": { "description": "Import paths (max 128)", @@ -17592,16 +17165,17 @@ "type": "string" }, "maxItems": 128, - "type": "array", - "uniqueItems": true + "type": "array" }, "name": { "description": "Library name", + "minLength": 1, "type": "string" }, "ownerId": { "description": "Owner user ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -17627,7 +17201,9 @@ "properties": { "profileChangedAt": { "description": "Profile image change date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "profileImagePath": { @@ -17681,6 +17257,7 @@ "properties": { "cronExpression": { "description": "Cron expression", + "pattern": "(((\\d+,)+\\d+|(\\d+(\\/|-)\\d+)|\\d+|\\*) ?){5,7}", "type": "string" }, "enabled": { @@ -17703,6 +17280,7 @@ "DatabaseBackupDeleteDto": { "properties": { "backups": { + "description": "Backup filenames to delete", "items": { "type": "string" }, @@ -17717,12 +17295,15 @@ "DatabaseBackupDto": { "properties": { "filename": { + "description": "Backup filename", "type": "string" }, "filesize": { + "description": "Backup file size", "type": "number" }, "timezone": { + "description": "Backup timezone", "type": "string" } }, @@ -17736,6 +17317,7 @@ "DatabaseBackupListResponseDto": { "properties": { "backups": { + "description": "List of backups", "items": { "$ref": "#/components/schemas/DatabaseBackupDto" }, @@ -17750,6 +17332,7 @@ "DatabaseBackupUploadDto": { "properties": { "file": { + "description": "Database backup file", "format": "binary", "type": "string" } @@ -17762,6 +17345,7 @@ "description": "Asset IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -17787,6 +17371,8 @@ }, "size": { "description": "Archive size in bytes", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -17801,10 +17387,12 @@ "albumId": { "description": "Album ID to download", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "archiveSize": { "description": "Archive size limit in bytes", + "maximum": 9007199254740991, "minimum": 1, "type": "integer" }, @@ -17812,6 +17400,7 @@ "description": "Asset IDs to download", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -17819,6 +17408,7 @@ "userId": { "description": "User ID to download assets from", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -17828,10 +17418,11 @@ "properties": { "archiveSize": { "description": "Maximum archive size in bytes", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "includeEmbeddedVideos": { - "default": false, "description": "Whether to include embedded videos in downloads", "type": "boolean" } @@ -17853,6 +17444,8 @@ }, "totalSize": { "description": "Total size in bytes", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -17866,6 +17459,7 @@ "properties": { "archiveSize": { "description": "Maximum archive size in bytes", + "maximum": 9007199254740991, "minimum": 1, "type": "integer" }, @@ -17916,12 +17510,14 @@ "properties": { "duplicateId": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "keepAssetIds": { "description": "Asset IDs to keep", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -17930,6 +17526,7 @@ "description": "Asset IDs to trash or delete", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -17959,6 +17556,7 @@ "description": "Suggested asset IDs to keep based on file size and EXIF data", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -18011,6 +17609,7 @@ "type": "object" }, "ExifResponseDto": { + "description": "EXIF response", "properties": { "city": { "default": null, @@ -18040,12 +17639,14 @@ "exifImageHeight": { "default": null, "description": "Image height in pixels", + "minimum": 0, "nullable": true, "type": "number" }, "exifImageWidth": { "default": null, "description": "Image width in pixels", + "minimum": 0, "nullable": true, "type": "number" }, @@ -18064,7 +17665,8 @@ "fileSizeInByte": { "default": null, "description": "File size in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": 0, "nullable": true, "type": "integer" }, @@ -18155,6 +17757,7 @@ "id": { "description": "Face ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -18178,6 +17781,7 @@ }, "minFaces": { "description": "Minimum number of faces required for recognition", + "maximum": 9007199254740991, "minimum": 1, "type": "integer" }, @@ -18205,12 +17809,10 @@ "FoldersResponse": { "properties": { "enabled": { - "default": false, "description": "Whether folders are enabled", "type": "boolean" }, "sidebarWeb": { - "default": false, "description": "Whether folders appear in web sidebar", "type": "boolean" } @@ -18245,12 +17847,7 @@ "JobCreateDto": { "properties": { "name": { - "allOf": [ - { - "$ref": "#/components/schemas/ManualJobName" - } - ], - "description": "Job name" + "$ref": "#/components/schemas/ManualJobName" } }, "required": [ @@ -18276,7 +17873,6 @@ "AssetFileMigration", "AssetGenerateThumbnailsQueueAll", "AssetGenerateThumbnails", - "AuditLogCleanup", "AuditTableCleanup", "DatabaseBackup", "FacialRecognitionQueueAll", @@ -18324,6 +17920,7 @@ "properties": { "concurrency": { "description": "Concurrency", + "maximum": 9007199254740991, "minimum": 1, "type": "integer" } @@ -18337,11 +17934,15 @@ "properties": { "assetCount": { "description": "Number of assets", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "createdAt": { "description": "Creation date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "exclusionPatterns": { @@ -18372,13 +17973,17 @@ }, "refreshedAt": { "description": "Last refresh date", + "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" }, "updatedAt": { "description": "Last update date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" } }, @@ -18398,24 +18003,27 @@ "LibraryStatsResponseDto": { "properties": { "photos": { - "default": 0, "description": "Number of photos", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "total": { - "default": 0, "description": "Total number of assets", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "usage": { - "default": 0, "description": "Storage usage in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "videos": { - "default": 0, "description": "Number of videos", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -18434,8 +18042,8 @@ "type": "string" }, "licenseKey": { - "description": "License key (format: IM(SV|CL)(-XXXX){8})", - "pattern": "/IM(SV|CL)(-[\\dA-Za-z]{4}){8}/", + "description": "License key (format: /^IM(SV|CL)(-[\\dA-Za-z]{4}){8}$/)", + "pattern": "^IM(SV|CL)(-[\\dA-Za-z]{4}){8}$", "type": "string" } }, @@ -18446,30 +18054,10 @@ "type": "object" }, "LicenseResponseDto": { - "properties": { - "activatedAt": { - "description": "Activation date", - "format": "date-time", - "type": "string" - }, - "activationKey": { - "description": "Activation key", - "type": "string" - }, - "licenseKey": { - "description": "License key (format: IM(SV|CL)(-XXXX){8})", - "pattern": "/IM(SV|CL)(-[\\dA-Za-z]{4}){8}/", - "type": "string" - } - }, - "required": [ - "activatedAt", - "activationKey", - "licenseKey" - ], - "type": "object" + "$ref": "#/components/schemas/UserLicense" }, "LogLevel": { + "description": "Log level", "enum": [ "verbose", "debug", @@ -18486,6 +18074,7 @@ "description": "User email", "example": "testuser@email.com", "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, "password": { @@ -18528,6 +18117,8 @@ }, "userEmail": { "description": "User email", + "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, "userId": { @@ -18627,12 +18218,7 @@ "type": "number" }, "folder": { - "allOf": [ - { - "$ref": "#/components/schemas/StorageFolder" - } - ], - "description": "Storage folder" + "$ref": "#/components/schemas/StorageFolder" }, "readable": { "description": "Whether the folder is readable", @@ -18663,12 +18249,7 @@ "MaintenanceStatusResponseDto": { "properties": { "action": { - "allOf": [ - { - "$ref": "#/components/schemas/MaintenanceAction" - } - ], - "description": "Maintenance action" + "$ref": "#/components/schemas/MaintenanceAction" }, "active": { "type": "boolean" @@ -18690,7 +18271,7 @@ "type": "object" }, "ManualJobName": { - "description": "Job name", + "description": "Manual job name", "enum": [ "person-cleanup", "tag-cleanup", @@ -18771,12 +18352,12 @@ "MemoriesResponse": { "properties": { "duration": { - "default": 5, "description": "Memory duration in seconds", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "enabled": { - "default": true, "description": "Whether memories are enabled", "type": "boolean" } @@ -18791,6 +18372,7 @@ "properties": { "duration": { "description": "Memory duration in seconds", + "maximum": 9007199254740991, "minimum": 1, "type": "integer" }, @@ -18807,6 +18389,7 @@ "description": "Asset IDs to associate with memory", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -18816,7 +18399,9 @@ }, "hideAt": { "description": "Date when memory should be hidden", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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", "x-immich-history": [ { @@ -18836,17 +18421,23 @@ }, "memoryAt": { "description": "Memory date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "seenAt": { "description": "Date when memory was seen", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "showAt": { "description": "Date when memory should be shown", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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", "x-immich-history": [ { @@ -18861,12 +18452,7 @@ "x-immich-state": "Stable" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/MemoryType" - } - ], - "description": "Memory type" + "$ref": "#/components/schemas/MemoryType" } }, "required": [ @@ -18886,7 +18472,9 @@ }, "createdAt": { "description": "Creation date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "data": { @@ -18894,12 +18482,16 @@ }, "deletedAt": { "description": "Deletion date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "hideAt": { "description": "Date when memory should be hidden", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "id": { @@ -18912,7 +18504,9 @@ }, "memoryAt": { "description": "Memory date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "ownerId": { @@ -18921,25 +18515,26 @@ }, "seenAt": { "description": "Date when memory was seen", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "showAt": { "description": "Date when memory should be shown", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/MemoryType" - } - ], - "description": "Memory type" + "$ref": "#/components/schemas/MemoryType" }, "updatedAt": { "description": "Last update date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" } }, @@ -18957,6 +18552,7 @@ "type": "object" }, "MemorySearchOrder": { + "description": "Sort order", "enum": [ "asc", "desc", @@ -18968,6 +18564,8 @@ "properties": { "total": { "description": "Total number of memories", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -18977,6 +18575,7 @@ "type": "object" }, "MemoryType": { + "description": "Memory type", "enum": [ "on_this_day" ], @@ -18990,12 +18589,16 @@ }, "memoryAt": { "description": "Memory date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "seenAt": { "description": "Date when memory was seen", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" } }, @@ -19007,6 +18610,7 @@ "description": "Person IDs to merge", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -19023,6 +18627,7 @@ "description": "Filter by album IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -19043,26 +18648,22 @@ }, "createdAfter": { "description": "Filter by creation date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "createdBefore": { "description": "Filter by creation date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "description": { "description": "Filter by description text", "type": "string" }, - "deviceAssetId": { - "description": "Filter by device asset ID", - "type": "string" - }, - "deviceId": { - "description": "Device ID to filter by", - "type": "string" - }, "encodedVideoPath": { "description": "Filter by encoded video file path", "type": "string" @@ -19070,6 +18671,7 @@ "id": { "description": "Filter by asset ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "isEncoded": { @@ -19101,10 +18703,12 @@ "description": "Library ID to filter by", "format": "uuid", "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "make": { "description": "Filter by camera make", + "nullable": true, "type": "string" }, "model": { @@ -19117,11 +18721,7 @@ "type": "string" }, "order": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetOrder" - } - ], + "$ref": "#/components/schemas/AssetOrder", "default": "desc", "description": "Sort order" }, @@ -19142,6 +18742,7 @@ "description": "Filter by person IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -19188,6 +18789,7 @@ "description": "Filter by tag IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "nullable": true, @@ -19195,12 +18797,16 @@ }, "takenAfter": { "description": "Filter by taken date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "takenBefore": { "description": "Filter by taken date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "thumbnailPath": { @@ -19209,39 +18815,37 @@ }, "trashedAfter": { "description": "Filter by trash date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "trashedBefore": { "description": "Filter by trash date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetTypeEnum" - } - ], - "description": "Asset type filter" + "$ref": "#/components/schemas/AssetTypeEnum" }, "updatedAfter": { "description": "Filter by update date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "updatedBefore": { "description": "Filter by update date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "visibility": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetVisibility" - } - ], - "description": "Filter by visibility" + "$ref": "#/components/schemas/AssetVisibility" }, "withDeleted": { "description": "Include deleted assets", @@ -19273,12 +18877,7 @@ "MirrorParameters": { "properties": { "axis": { - "allOf": [ - { - "$ref": "#/components/schemas/MirrorAxis" - } - ], - "description": "Axis to mirror along" + "$ref": "#/components/schemas/MirrorAxis" } }, "required": [ @@ -19289,6 +18888,7 @@ "NotificationCreateDto": { "properties": { "data": { + "additionalProperties": {}, "description": "Additional notification data", "type": "object" }, @@ -19298,17 +18898,14 @@ "type": "string" }, "level": { - "allOf": [ - { - "$ref": "#/components/schemas/NotificationLevel" - } - ], - "description": "Notification level" + "$ref": "#/components/schemas/NotificationLevel" }, "readAt": { "description": "Date when notification was read", + "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" }, "title": { @@ -19316,16 +18913,12 @@ "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/NotificationType" - } - ], - "description": "Notification type" + "$ref": "#/components/schemas/NotificationType" }, "userId": { "description": "User ID to send notification to", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -19341,6 +18934,7 @@ "description": "Notification IDs to delete", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "minItems": 1, @@ -19356,10 +18950,13 @@ "properties": { "createdAt": { "description": "Creation date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "data": { + "additionalProperties": {}, "description": "Additional notification data", "type": "object" }, @@ -19372,16 +18969,13 @@ "type": "string" }, "level": { - "allOf": [ - { - "$ref": "#/components/schemas/NotificationLevel" - } - ], - "description": "Notification level" + "$ref": "#/components/schemas/NotificationLevel" }, "readAt": { "description": "Date when notification was read", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "title": { @@ -19389,12 +18983,7 @@ "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/NotificationType" - } - ], - "description": "Notification type" + "$ref": "#/components/schemas/NotificationType" } }, "required": [ @@ -19407,6 +18996,7 @@ "type": "object" }, "NotificationLevel": { + "description": "Notification level", "enum": [ "success", "error", @@ -19416,6 +19006,7 @@ "type": "string" }, "NotificationType": { + "description": "Notification type", "enum": [ "JobFailed", "BackupFailed", @@ -19432,6 +19023,7 @@ "description": "Notification IDs to update", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "minItems": 1, @@ -19439,8 +19031,10 @@ }, "readAt": { "description": "Date when notifications were read", + "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" } }, @@ -19453,8 +19047,10 @@ "properties": { "readAt": { "description": "Date when notification was read", + "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" } }, @@ -19484,6 +19080,7 @@ }, "url": { "description": "OAuth callback URL", + "minLength": 1, "type": "string" } }, @@ -19513,7 +19110,7 @@ "type": "object" }, "OAuthTokenEndpointAuthMethod": { - "description": "Token endpoint auth method", + "description": "OAuth token endpoint auth method", "enum": [ "client_secret_post", "client_secret_basic" @@ -19528,6 +19125,7 @@ }, "maxResolution": { "description": "Maximum resolution for OCR processing", + "maximum": 9007199254740991, "minimum": 1, "type": "integer" }, @@ -19563,8 +19161,9 @@ "properties": { "year": { "description": "Year for on this day memory", - "minimum": 1, - "type": "number" + "maximum": 9999, + "minimum": 1000, + "type": "integer" } }, "required": [ @@ -19601,6 +19200,7 @@ "sharedWithId": { "description": "User ID to share with", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -19610,6 +19210,7 @@ "type": "object" }, "PartnerDirection": { + "description": "Partner direction", "enum": [ "shared-by", "shared-with" @@ -19617,21 +19218,21 @@ "type": "string" }, "PartnerResponseDto": { + "description": "Partner response", "properties": { "avatarColor": { - "allOf": [ - { - "$ref": "#/components/schemas/UserAvatarColor" - } - ], - "description": "Avatar color" + "$ref": "#/components/schemas/UserAvatarColor" }, "email": { "description": "User email", + "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, "id": { "description": "User ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "inTimeline": { @@ -19677,12 +19278,10 @@ "PeopleResponse": { "properties": { "enabled": { - "default": true, "description": "Whether people are enabled", "type": "boolean" }, "sidebarWeb": { - "default": false, "description": "Whether people appear in web sidebar", "type": "boolean" } @@ -19694,6 +19293,7 @@ "type": "object" }, "PeopleResponseDto": { + "description": "People response", "properties": { "hasNextPage": { "description": "Whether there are more pages", @@ -19712,10 +19312,11 @@ }, "hidden": { "description": "Number of hidden people", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "people": { - "description": "List of people", "items": { "$ref": "#/components/schemas/PersonResponseDto" }, @@ -19723,6 +19324,8 @@ }, "total": { "description": "Total number of people", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" } }, @@ -19772,11 +19375,13 @@ "color": { "description": "Person color (hex)", "nullable": true, + "pattern": "^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$", "type": "string" }, "featureFaceAssetId": { "description": "Asset ID used for feature face thumbnail", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "id": { @@ -19822,7 +19427,6 @@ "asset.view", "asset.download", "asset.upload", - "asset.replace", "asset.copy", "asset.derive", "asset.edit.get", @@ -19974,6 +19578,7 @@ "color": { "description": "Person color (hex)", "nullable": true, + "pattern": "^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$", "type": "string" }, "isFavorite": { @@ -20075,6 +19680,8 @@ "properties": { "assets": { "description": "Number of assets", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -20094,11 +19701,13 @@ "color": { "description": "Person color (hex)", "nullable": true, + "pattern": "^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$", "type": "string" }, "featureFaceAssetId": { "description": "Asset ID used for feature face thumbnail", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "isFavorite": { @@ -20140,7 +19749,6 @@ "x-immich-state": "Stable" }, "faces": { - "description": "Face detections", "items": { "$ref": "#/components/schemas/AssetFaceWithoutPersonResponseDto" }, @@ -20208,16 +19816,18 @@ "properties": { "newPinCode": { "description": "New PIN code (4-6 digits)", - "example": "123456", + "pattern": "^\\d{6}$", "type": "string" }, "password": { "description": "User password (required if PIN code is not provided)", + "example": "password", "type": "string" }, "pinCode": { "description": "New PIN code (4-6 digits)", "example": "123456", + "pattern": "^\\d{6}$", "type": "string" } }, @@ -20230,11 +19840,13 @@ "properties": { "password": { "description": "User password (required if PIN code is not provided)", + "example": "password", "type": "string" }, "pinCode": { "description": "New PIN code (4-6 digits)", "example": "123456", + "pattern": "^\\d{6}$", "type": "string" } }, @@ -20245,6 +19857,7 @@ "pinCode": { "description": "PIN code (4-6 digits)", "example": "123456", + "pattern": "^\\d{6}$", "type": "string" } }, @@ -20302,9 +19915,13 @@ "type": "string" }, "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PluginJsonSchema" + } + ], "description": "Action schema", - "nullable": true, - "type": "object" + "nullable": true }, "supportedContexts": { "description": "Supported contexts", @@ -20329,8 +19946,9 @@ ], "type": "object" }, + "PluginConfigValue": {}, "PluginContextType": { - "description": "Context type", + "description": "Plugin context", "enum": [ "asset", "album", @@ -20357,9 +19975,13 @@ "type": "string" }, "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PluginJsonSchema" + } + ], "description": "Filter schema", - "nullable": true, - "type": "object" + "nullable": true }, "supportedContexts": { "description": "Supported contexts", @@ -20384,6 +20006,87 @@ ], "type": "object" }, + "PluginJsonSchema": { + "properties": { + "additionalProperties": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "properties": { + "additionalProperties": { + "$ref": "#/components/schemas/PluginJsonSchemaProperty" + }, + "type": "object" + }, + "required": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "$ref": "#/components/schemas/PluginJsonSchemaType" + } + }, + "type": "object" + }, + "PluginJsonSchemaProperty": { + "properties": { + "additionalProperties": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/components/schemas/PluginJsonSchemaProperty" + } + ] + }, + "default": {}, + "description": { + "type": "string" + }, + "enum": { + "items": { + "type": "string" + }, + "type": "array" + }, + "items": { + "$ref": "#/components/schemas/PluginJsonSchemaProperty" + }, + "properties": { + "additionalProperties": { + "$ref": "#/components/schemas/PluginJsonSchemaProperty" + }, + "type": "object" + }, + "required": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "$ref": "#/components/schemas/PluginJsonSchemaType" + } + }, + "type": "object" + }, + "PluginJsonSchemaType": { + "enum": [ + "string", + "number", + "integer", + "boolean", + "object", + "array", + "null" + ], + "type": "string" + }, "PluginResponseDto": { "properties": { "actions": { @@ -20450,20 +20153,10 @@ "PluginTriggerResponseDto": { "properties": { "contextType": { - "allOf": [ - { - "$ref": "#/components/schemas/PluginContextType" - } - ], - "description": "Context type" + "$ref": "#/components/schemas/PluginContextType" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/PluginTriggerType" - } - ], - "description": "Trigger type" + "$ref": "#/components/schemas/PluginTriggerType" } }, "required": [ @@ -20473,7 +20166,7 @@ "type": "object" }, "PluginTriggerType": { - "description": "Trigger type", + "description": "Plugin trigger type", "enum": [ "AssetCreate", "PersonRecognized" @@ -20524,12 +20217,7 @@ "QueueCommandDto": { "properties": { "command": { - "allOf": [ - { - "$ref": "#/components/schemas/QueueCommand" - } - ], - "description": "Queue command to execute" + "$ref": "#/components/schemas/QueueCommand" }, "force": { "description": "Force the command execution (if applicable)", @@ -20564,6 +20252,7 @@ "QueueJobResponseDto": { "properties": { "data": { + "additionalProperties": {}, "description": "Job data payload", "type": "object" }, @@ -20572,15 +20261,12 @@ "type": "string" }, "name": { - "allOf": [ - { - "$ref": "#/components/schemas/JobName" - } - ], - "description": "Job name" + "$ref": "#/components/schemas/JobName" }, "timestamp": { "description": "Job creation timestamp", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -20592,6 +20278,7 @@ "type": "object" }, "QueueJobStatus": { + "description": "Queue job status", "enum": [ "active", "failed", @@ -20603,6 +20290,7 @@ "type": "string" }, "QueueName": { + "description": "Queue name", "enum": [ "thumbnailGeneration", "metadataExtraction", @@ -20632,12 +20320,7 @@ "type": "boolean" }, "name": { - "allOf": [ - { - "$ref": "#/components/schemas/QueueName" - } - ], - "description": "Queue name" + "$ref": "#/components/schemas/QueueName" }, "statistics": { "$ref": "#/components/schemas/QueueStatisticsDto" @@ -20669,26 +20352,38 @@ "properties": { "active": { "description": "Number of active jobs", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "completed": { "description": "Number of completed jobs", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "delayed": { "description": "Number of delayed jobs", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "failed": { "description": "Number of failed jobs", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "paused": { "description": "Number of paused jobs", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "waiting": { "description": "Number of waiting jobs", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -20813,6 +20508,7 @@ "description": "Filter by album IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -20829,16 +20525,16 @@ }, "createdAfter": { "description": "Filter by creation date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "createdBefore": { "description": "Filter by creation date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", - "type": "string" - }, - "deviceId": { - "description": "Device ID to filter by", + "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" }, "isEncoded": { @@ -20870,10 +20566,12 @@ "description": "Library ID to filter by", "format": "uuid", "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "make": { "description": "Filter by camera make", + "nullable": true, "type": "string" }, "model": { @@ -20889,6 +20587,7 @@ "description": "Filter by person IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -20931,6 +20630,7 @@ "description": "Filter by tag IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "nullable": true, @@ -20938,49 +20638,51 @@ }, "takenAfter": { "description": "Filter by taken date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "takenBefore": { "description": "Filter by taken date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "trashedAfter": { "description": "Filter by trash date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "trashedBefore": { "description": "Filter by trash date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetTypeEnum" - } - ], - "description": "Asset type filter" + "$ref": "#/components/schemas/AssetTypeEnum" }, "updatedAfter": { "description": "Filter by update date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "updatedBefore": { "description": "Filter by update date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "visibility": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetVisibility" - } - ], - "description": "Filter by visibility" + "$ref": "#/components/schemas/AssetVisibility" }, "withDeleted": { "description": "Include deleted assets", @@ -21004,7 +20706,6 @@ "RatingsResponse": { "properties": { "enabled": { - "default": false, "description": "Whether ratings are enabled", "type": "boolean" } @@ -21024,6 +20725,7 @@ "type": "object" }, "ReactionLevel": { + "description": "Reaction level", "enum": [ "album", "asset" @@ -21031,6 +20733,7 @@ "type": "string" }, "ReactionType": { + "description": "Reaction type", "enum": [ "comment", "like" @@ -21072,6 +20775,8 @@ "properties": { "count": { "description": "Number of albums in this page", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "facets": { @@ -21088,6 +20793,8 @@ }, "total": { "description": "Total number of matching albums", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" } }, @@ -21103,6 +20810,8 @@ "properties": { "count": { "description": "Number of assets in this page", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "facets": { @@ -21124,6 +20833,8 @@ }, "total": { "description": "Total number of matching assets", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" } }, @@ -21175,6 +20886,8 @@ "properties": { "count": { "description": "Number of assets with this facet value", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "value": { @@ -21191,7 +20904,6 @@ "SearchFacetResponseDto": { "properties": { "counts": { - "description": "Facet counts", "items": { "$ref": "#/components/schemas/SearchFacetCountResponseDto" }, @@ -21227,6 +20939,8 @@ "properties": { "total": { "description": "Total number of matching assets", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -21236,6 +20950,7 @@ "type": "object" }, "SearchSuggestionType": { + "description": "Suggestion type", "enum": [ "country", "state", @@ -21407,10 +21122,14 @@ }, "trashDays": { "description": "Number of days before trashed assets are permanently deleted", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "userDeleteDelay": { "description": "Delay in days before deleted users are permanently removed", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -21546,7 +21265,6 @@ "properties": { "res": { "example": "pong", - "readOnly": true, "type": "string" } }, @@ -21558,48 +21276,40 @@ "ServerStatsResponseDto": { "properties": { "photos": { - "default": 0, "description": "Total number of photos", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "usage": { - "default": 0, "description": "Total storage usage in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "usageByUser": { - "default": [], - "example": [ - { - "photos": 1, - "videos": 1, - "diskUsageRaw": 2, - "usagePhotos": 1, - "usageVideos": 1 - } - ], + "description": "Array of usage for each user", "items": { "$ref": "#/components/schemas/UsageByUserDto" }, - "title": "Array of usage for each user", "type": "array" }, "usagePhotos": { - "default": 0, "description": "Storage usage for photos in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "usageVideos": { - "default": 0, "description": "Storage usage for videos in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "videos": { - "default": 0, "description": "Total number of videos", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -21621,7 +21331,8 @@ }, "diskAvailableRaw": { "description": "Available disk space in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "diskSize": { @@ -21630,7 +21341,8 @@ }, "diskSizeRaw": { "description": "Total disk size in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "diskUsagePercentage": { @@ -21644,7 +21356,8 @@ }, "diskUseRaw": { "description": "Used disk space in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -21675,7 +21388,9 @@ "properties": { "createdAt": { "description": "When this version was first seen", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "id": { @@ -21698,14 +21413,20 @@ "properties": { "major": { "description": "Major version number", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "minor": { "description": "Minor version number", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "patch": { "description": "Patch version number", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -21847,11 +21568,13 @@ "properties": { "password": { "description": "User password (required if PIN code is not provided)", + "example": "password", "type": "string" }, "pinCode": { "description": "New PIN code (4-6 digits)", "example": "123456", + "pattern": "^\\d{6}$", "type": "string" } }, @@ -21869,12 +21592,7 @@ "SetMaintenanceModeDto": { "properties": { "action": { - "allOf": [ - { - "$ref": "#/components/schemas/MaintenanceAction" - } - ], - "description": "Maintenance action" + "$ref": "#/components/schemas/MaintenanceAction" }, "restoreBackupFilename": { "description": "Restore backup filename", @@ -21891,6 +21609,7 @@ "albumId": { "description": "Album ID (for album sharing)", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "allowDownload": { @@ -21906,6 +21625,7 @@ "description": "Asset IDs (for individual assets)", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -21918,8 +21638,10 @@ "expiresAt": { "default": null, "description": "Expiration date", + "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" }, "password": { @@ -21938,12 +21660,7 @@ "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/SharedLinkType" - } - ], - "description": "Shared link type" + "$ref": "#/components/schemas/SharedLinkType" } }, "required": [ @@ -21972,8 +21689,10 @@ }, "expiresAt": { "description": "Expiration date", + "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" }, "password": { @@ -22007,6 +21726,7 @@ "type": "object" }, "SharedLinkResponseDto": { + "description": "Shared link response", "properties": { "album": { "$ref": "#/components/schemas/AlbumResponseDto" @@ -22027,7 +21747,9 @@ }, "createdAt": { "description": "Creation date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "description": { @@ -22037,8 +21759,10 @@ }, "expiresAt": { "description": "Expiration date", + "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" }, "id": { @@ -22063,34 +21787,8 @@ "nullable": true, "type": "string" }, - "token": { - "deprecated": true, - "description": "Access token", - "nullable": true, - "type": "string", - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v2", - "state": "Stable" - }, - { - "version": "v2.6.0", - "state": "Deprecated" - } - ], - "x-immich-state": "Deprecated" - }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/SharedLinkType" - } - ], - "description": "Shared link type" + "$ref": "#/components/schemas/SharedLinkType" }, "userId": { "description": "Owner user ID", @@ -22125,12 +21823,10 @@ "SharedLinksResponse": { "properties": { "enabled": { - "default": true, "description": "Whether shared links are enabled", "type": "boolean" }, "sidebarWeb": { - "default": false, "description": "Whether shared links appear in web sidebar", "type": "boolean" } @@ -22160,6 +21856,7 @@ "description": "User email", "example": "testuser@email.com", "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, "name": { @@ -22186,6 +21883,7 @@ "description": "Filter by album IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -22202,16 +21900,16 @@ }, "createdAfter": { "description": "Filter by creation date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "createdBefore": { "description": "Filter by creation date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", - "type": "string" - }, - "deviceId": { - "description": "Device ID to filter by", + "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" }, "isEncoded": { @@ -22247,10 +21945,12 @@ "description": "Library ID to filter by", "format": "uuid", "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "make": { "description": "Filter by camera make", + "nullable": true, "type": "string" }, "model": { @@ -22271,6 +21971,7 @@ "description": "Filter by person IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -22282,6 +21983,7 @@ "queryAssetId": { "description": "Asset ID to use as search reference", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "rating": { @@ -22322,6 +22024,7 @@ "description": "Filter by tag IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "nullable": true, @@ -22329,49 +22032,51 @@ }, "takenAfter": { "description": "Filter by taken date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "takenBefore": { "description": "Filter by taken date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "trashedAfter": { "description": "Filter by trash date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "trashedBefore": { "description": "Filter by trash date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetTypeEnum" - } - ], - "description": "Asset type filter" + "$ref": "#/components/schemas/AssetTypeEnum" }, "updatedAfter": { "description": "Filter by update date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "updatedBefore": { "description": "Filter by update date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "visibility": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetVisibility" - } - ], - "description": "Filter by visibility" + "$ref": "#/components/schemas/AssetVisibility" }, "withDeleted": { "description": "Include deleted assets", @@ -22399,6 +22104,7 @@ "description": "Asset IDs (first becomes primary, min 2)", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "minItems": 2, @@ -22411,9 +22117,9 @@ "type": "object" }, "StackResponseDto": { + "description": "Stack response", "properties": { "assets": { - "description": "Stack assets", "items": { "$ref": "#/components/schemas/AssetResponseDto" }, @@ -22440,6 +22146,7 @@ "primaryAssetId": { "description": "Primary asset ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -22451,6 +22158,7 @@ "description": "Filter by album IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -22467,22 +22175,22 @@ }, "createdAfter": { "description": "Filter by creation date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "createdBefore": { "description": "Filter by creation date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "description": { "description": "Filter by description text", "type": "string" }, - "deviceId": { - "description": "Device ID to filter by", - "type": "string" - }, "isEncoded": { "description": "Filter by encoded status", "type": "boolean" @@ -22512,10 +22220,12 @@ "description": "Library ID to filter by", "format": "uuid", "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "make": { "description": "Filter by camera make", + "nullable": true, "type": "string" }, "model": { @@ -22531,6 +22241,7 @@ "description": "Filter by person IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -22567,6 +22278,7 @@ "description": "Filter by tag IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "nullable": true, @@ -22574,49 +22286,51 @@ }, "takenAfter": { "description": "Filter by taken date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "takenBefore": { "description": "Filter by taken date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "trashedAfter": { "description": "Filter by trash date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "trashedBefore": { "description": "Filter by trash date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetTypeEnum" - } - ], - "description": "Asset type filter" + "$ref": "#/components/schemas/AssetTypeEnum" }, "updatedAfter": { "description": "Filter by update date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "updatedBefore": { "description": "Filter by update date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "visibility": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetVisibility" - } - ], - "description": "Filter by visibility" + "$ref": "#/components/schemas/AssetVisibility" } }, "type": "object" @@ -22652,12 +22366,7 @@ "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/SyncEntityType" - } - ], - "description": "Sync entity type" + "$ref": "#/components/schemas/SyncEntityType" } }, "required": [ @@ -22756,12 +22465,7 @@ "type": "string" }, "role": { - "allOf": [ - { - "$ref": "#/components/schemas/AlbumUserRole" - } - ], - "description": "Album user role" + "$ref": "#/components/schemas/AlbumUserRole" }, "userId": { "description": "User ID", @@ -22779,7 +22483,9 @@ "properties": { "createdAt": { "description": "Created at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "description": { @@ -22799,11 +22505,7 @@ "type": "string" }, "order": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetOrder" - } - ] + "$ref": "#/components/schemas/AssetOrder" }, "ownerId": { "description": "Owner ID", @@ -22816,7 +22518,9 @@ }, "updatedAt": { "description": "Updated at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" } }, @@ -22848,6 +22552,7 @@ "SyncAssetEditDeleteV1": { "properties": { "editId": { + "description": "Edit ID", "type": "string" } }, @@ -22859,22 +22564,25 @@ "SyncAssetEditV1": { "properties": { "action": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetEditAction" - } - ] + "$ref": "#/components/schemas/AssetEditAction" }, "assetId": { + "description": "Asset ID", "type": "string" }, "id": { + "description": "Edit ID", "type": "string" }, "parameters": { + "additionalProperties": {}, + "description": "Edit parameters", "type": "object" }, "sequence": { + "description": "Edit sequence", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -22905,8 +22613,10 @@ }, "dateTimeOriginal": { "description": "Date time original", + "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" }, "description": { @@ -22916,11 +22626,15 @@ }, "exifImageHeight": { "description": "Exif image height", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "nullable": true, "type": "integer" }, "exifImageWidth": { "description": "Exif image width", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "nullable": true, "type": "integer" }, @@ -22937,6 +22651,8 @@ }, "fileSizeInByte": { "description": "File size in byte", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "nullable": true, "type": "integer" }, @@ -22954,6 +22670,8 @@ }, "iso": { "description": "ISO", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "nullable": true, "type": "integer" }, @@ -22986,8 +22704,10 @@ }, "modifyDate": { "description": "Modify date", + "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" }, "orientation": { @@ -23007,6 +22727,8 @@ }, "rating": { "description": "Rating", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "nullable": true, "type": "integer" }, @@ -23069,15 +22791,27 @@ "type": "string" }, "boundingBoxX1": { + "description": "Bounding box X1", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxX2": { + "description": "Bounding box X2", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxY1": { + "description": "Bounding box Y1", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxY2": { + "description": "Bounding box Y2", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "id": { @@ -23085,9 +22819,15 @@ "type": "string" }, "imageHeight": { + "description": "Image height", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "imageWidth": { + "description": "Image width", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "personId": { @@ -23121,21 +22861,35 @@ "type": "string" }, "boundingBoxX1": { + "description": "Bounding box X1", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxX2": { + "description": "Bounding box X2", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxY1": { + "description": "Bounding box Y1", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxY2": { + "description": "Bounding box Y2", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "deletedAt": { "description": "Face 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" }, "id": { @@ -23143,9 +22897,15 @@ "type": "string" }, "imageHeight": { + "description": "Image height", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "imageWidth": { + "description": "Image width", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "isVisible": { @@ -23206,6 +22966,7 @@ "type": "string" }, "value": { + "additionalProperties": {}, "description": "Value", "type": "object" } @@ -23225,8 +22986,10 @@ }, "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": { @@ -23236,18 +22999,24 @@ }, "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" }, @@ -23275,8 +23044,10 @@ }, "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": { @@ -23298,23 +23069,15 @@ "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetTypeEnum" - } - ], - "description": "Asset type" + "$ref": "#/components/schemas/AssetTypeEnum" }, "visibility": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetVisibility" - } - ], - "description": "Asset visibility" + "$ref": "#/components/schemas/AssetVisibility" }, "width": { "description": "Asset width", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "nullable": true, "type": "integer" } @@ -23350,13 +23113,14 @@ "$ref": "#/components/schemas/UserAvatarColor" } ], - "description": "User avatar color", "nullable": true }, "deletedAt": { "description": "User 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" }, "email": { @@ -23390,14 +23154,22 @@ }, "profileChangedAt": { "description": "User profile changed at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "quotaSizeInBytes": { + "description": "Quota size in bytes", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "nullable": true, "type": "integer" }, "quotaUsageInBytes": { + "description": "Quota usage in bytes", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "storageLabel": { @@ -23407,7 +23179,6 @@ } }, "required": [ - "avatarColor", "deletedAt", "email", "hasProfileImage", @@ -23533,23 +23304,30 @@ "properties": { "createdAt": { "description": "Created at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "data": { + "additionalProperties": {}, "description": "Data", "type": "object" }, "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" }, "hideAt": { "description": "Hide 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" }, "id": { @@ -23562,7 +23340,9 @@ }, "memoryAt": { "description": "Memory at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "ownerId": { @@ -23571,27 +23351,28 @@ }, "seenAt": { "description": "Seen 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" }, "showAt": { "description": "Show 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" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/MemoryType" - } - ], - "description": "Memory type" + "$ref": "#/components/schemas/MemoryType" }, "updatedAt": { "description": "Updated at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" } }, @@ -23666,8 +23447,10 @@ "properties": { "birthDate": { "description": "Birth date", + "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" }, "color": { @@ -23677,7 +23460,9 @@ }, "createdAt": { "description": "Created at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "faceAssetId": { @@ -23707,7 +23492,9 @@ }, "updatedAt": { "description": "Updated at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" } }, @@ -23726,7 +23513,7 @@ "type": "object" }, "SyncRequestType": { - "description": "Sync request types", + "description": "Sync request type", "enum": [ "AlbumsV1", "AlbumUsersV1", @@ -23773,7 +23560,9 @@ "properties": { "createdAt": { "description": "Created at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "id": { @@ -23790,7 +23579,9 @@ }, "updatedAt": { "description": "Updated at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" } }, @@ -23837,12 +23628,7 @@ "SyncUserMetadataDeleteV1": { "properties": { "key": { - "allOf": [ - { - "$ref": "#/components/schemas/UserMetadataKey" - } - ], - "description": "User metadata key" + "$ref": "#/components/schemas/UserMetadataKey" }, "userId": { "description": "User ID", @@ -23858,18 +23644,14 @@ "SyncUserMetadataV1": { "properties": { "key": { - "allOf": [ - { - "$ref": "#/components/schemas/UserMetadataKey" - } - ], - "description": "User metadata key" + "$ref": "#/components/schemas/UserMetadataKey" }, "userId": { "description": "User ID", "type": "string" }, "value": { + "additionalProperties": {}, "description": "User metadata value", "type": "object" } @@ -23889,13 +23671,14 @@ "$ref": "#/components/schemas/UserAvatarColor" } ], - "description": "User avatar color", "nullable": true }, "deletedAt": { "description": "User 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" }, "email": { @@ -23916,12 +23699,13 @@ }, "profileChangedAt": { "description": "User profile changed at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" } }, "required": [ - "avatarColor", "deletedAt", "email", "hasProfileImage", @@ -23943,6 +23727,7 @@ "type": "object" }, "SystemConfigDto": { + "description": "System configuration", "properties": { "backup": { "$ref": "#/components/schemas/SystemConfigBackupsDto" @@ -24036,12 +23821,7 @@ "SystemConfigFFmpegDto": { "properties": { "accel": { - "allOf": [ - { - "$ref": "#/components/schemas/TranscodeHWAccel" - } - ], - "description": "Transcode hardware acceleration" + "$ref": "#/components/schemas/TranscodeHWAccel" }, "accelDecode": { "description": "Accelerated decode", @@ -24075,12 +23855,7 @@ "type": "integer" }, "cqMode": { - "allOf": [ - { - "$ref": "#/components/schemas/CQMode" - } - ], - "description": "CQ mode" + "$ref": "#/components/schemas/CQMode" }, "crf": { "description": "CRF", @@ -24090,6 +23865,7 @@ }, "gopSize": { "description": "GOP size", + "maximum": 9007199254740991, "minimum": 0, "type": "integer" }, @@ -24112,24 +23888,14 @@ "type": "integer" }, "targetAudioCodec": { - "allOf": [ - { - "$ref": "#/components/schemas/AudioCodec" - } - ], - "description": "Target audio codec" + "$ref": "#/components/schemas/AudioCodec" }, "targetResolution": { "description": "Target resolution", "type": "string" }, "targetVideoCodec": { - "allOf": [ - { - "$ref": "#/components/schemas/VideoCodec" - } - ], - "description": "Target video codec" + "$ref": "#/components/schemas/VideoCodec" }, "temporalAQ": { "description": "Temporal AQ", @@ -24137,24 +23903,15 @@ }, "threads": { "description": "Threads", + "maximum": 9007199254740991, "minimum": 0, "type": "integer" }, "tonemap": { - "allOf": [ - { - "$ref": "#/components/schemas/ToneMapping" - } - ], - "description": "Tone mapping" + "$ref": "#/components/schemas/ToneMapping" }, "transcode": { - "allOf": [ - { - "$ref": "#/components/schemas/TranscodePolicy" - } - ], - "description": "Transcode policy" + "$ref": "#/components/schemas/TranscodePolicy" }, "twoPass": { "description": "Two pass", @@ -24205,15 +23962,9 @@ "type": "boolean" }, "format": { - "allOf": [ - { - "$ref": "#/components/schemas/ImageFormat" - } - ], - "description": "Image format" + "$ref": "#/components/schemas/ImageFormat" }, "progressive": { - "default": false, "description": "Progressive", "type": "boolean" }, @@ -24234,15 +23985,10 @@ "SystemConfigGeneratedImageDto": { "properties": { "format": { - "allOf": [ - { - "$ref": "#/components/schemas/ImageFormat" - } - ], - "description": "Image format" + "$ref": "#/components/schemas/ImageFormat" }, "progressive": { - "default": false, + "description": "Progressive", "type": "boolean" }, "quality": { @@ -24253,6 +23999,7 @@ }, "size": { "description": "Size", + "maximum": 9007199254740991, "minimum": 1, "type": "integer" } @@ -24267,12 +24014,7 @@ "SystemConfigImageDto": { "properties": { "colorspace": { - "allOf": [ - { - "$ref": "#/components/schemas/Colorspace" - } - ], - "description": "Colorspace" + "$ref": "#/components/schemas/Colorspace" }, "extractEmbedded": { "description": "Extract embedded", @@ -24378,6 +24120,8 @@ "SystemConfigLibraryScanDto": { "properties": { "cronExpression": { + "description": "Cron expression", + "pattern": "(((\\d+,)+\\d+|(\\d+(\\/|-)\\d+)|\\d+|\\*) ?){5,7}", "type": "string" }, "enabled": { @@ -24410,11 +24154,7 @@ "type": "boolean" }, "level": { - "allOf": [ - { - "$ref": "#/components/schemas/LogLevel" - } - ] + "$ref": "#/components/schemas/LogLevel" } }, "required": [ @@ -24445,9 +24185,8 @@ "$ref": "#/components/schemas/OcrConfig" }, "urls": { - "format": "uri", + "description": "ML service URLs", "items": { - "format": "uri", "type": "string" }, "minItems": 1, @@ -24468,6 +24207,7 @@ "SystemConfigMapDto": { "properties": { "darkStyle": { + "description": "Dark map style URL", "format": "uri", "type": "string" }, @@ -24476,6 +24216,7 @@ "type": "boolean" }, "lightStyle": { + "description": "Light map style URL", "format": "uri", "type": "string" } @@ -24529,6 +24270,8 @@ "type": "boolean" }, "startTime": { + "description": "Start time", + "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$", "type": "string" }, "syncQuotaUsage": { @@ -24581,10 +24324,9 @@ }, "defaultStorageQuota": { "description": "Default storage quota", - "format": "int64", "minimum": 0, "nullable": true, - "type": "integer" + "type": "number" }, "enabled": { "description": "Enabled", @@ -24599,8 +24341,7 @@ "type": "boolean" }, "mobileRedirectUri": { - "description": "Mobile redirect URI", - "format": "uri", + "description": "Mobile redirect URI (set to empty string to disable)", "type": "string" }, "profileSigningAlgorithm": { @@ -24616,6 +24357,7 @@ "type": "string" }, "signingAlgorithm": { + "description": "Signing algorithm", "type": "string" }, "storageLabelClaim": { @@ -24628,16 +24370,12 @@ }, "timeout": { "description": "Timeout", + "maximum": 9007199254740991, "minimum": 1, "type": "integer" }, "tokenEndpointAuthMethod": { - "allOf": [ - { - "$ref": "#/components/schemas/OAuthTokenEndpointAuthMethod" - } - ], - "description": "Token endpoint auth method" + "$ref": "#/components/schemas/OAuthTokenEndpointAuthMethod" } }, "required": [ @@ -24690,7 +24428,6 @@ "properties": { "externalDomain": { "description": "External domain", - "format": "uri", "type": "string" }, "loginPageMessage": { @@ -24799,12 +24536,15 @@ "SystemConfigTemplateEmailsDto": { "properties": { "albumInviteTemplate": { + "description": "Album invite template", "type": "string" }, "albumUpdateTemplate": { + "description": "Album update template", "type": "string" }, "welcomeTemplate": { + "description": "Welcome template", "type": "string" } }, @@ -24913,6 +24653,7 @@ "properties": { "days": { "description": "Days", + "maximum": 9007199254740991, "minimum": 0, "type": "integer" }, @@ -24931,6 +24672,7 @@ "properties": { "deleteDelay": { "description": "Delete delay", + "maximum": 9007199254740991, "minimum": 1, "type": "integer" } @@ -24946,6 +24688,7 @@ "description": "Asset IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -24954,6 +24697,7 @@ "description": "Tag IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -24969,6 +24713,8 @@ "properties": { "count": { "description": "Number of assets tagged", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -24981,7 +24727,8 @@ "properties": { "color": { "description": "Tag color (hex)", - "pattern": "^#?([0-9A-F]{3}|[0-9A-F]{4}|[0-9A-F]{6}|[0-9A-F]{8})$", + "nullable": true, + "pattern": "^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$", "type": "string" }, "name": { @@ -24992,6 +24739,7 @@ "description": "Parent tag ID", "format": "uuid", "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -25047,6 +24795,7 @@ "color": { "description": "Tag color (hex)", "nullable": true, + "pattern": "^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$", "type": "string" } }, @@ -25070,12 +24819,10 @@ "TagsResponse": { "properties": { "enabled": { - "default": true, "description": "Whether tags are enabled", "type": "boolean" }, "sidebarWeb": { - "default": true, "description": "Whether tags appear in web sidebar", "type": "boolean" } @@ -25307,6 +25054,8 @@ "count": { "description": "Number of assets in this time bucket", "example": 42, + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "timeBucket": { @@ -25357,6 +25106,8 @@ "properties": { "count": { "description": "Number of items in trash", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -25374,6 +25125,7 @@ "albumThumbnailAssetId": { "description": "Album thumbnail asset ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "description": { @@ -25385,12 +25137,7 @@ "type": "boolean" }, "order": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetOrder" - } - ], - "description": "Asset sort order" + "$ref": "#/components/schemas/AssetOrder" } }, "type": "object" @@ -25398,12 +25145,7 @@ "UpdateAlbumUserDto": { "properties": { "role": { - "allOf": [ - { - "$ref": "#/components/schemas/AlbumUserRole" - } - ], - "description": "Album user role" + "$ref": "#/components/schemas/AlbumUserRole" } }, "required": [ @@ -25427,16 +25169,21 @@ }, "latitude": { "description": "Latitude coordinate", + "maximum": 90, + "minimum": -90, "type": "number" }, "livePhotoVideoId": { "description": "Live photo video ID", "format": "uuid", "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "longitude": { "description": "Longitude coordinate", + "maximum": 180, + "minimum": -180, "type": "number" }, "rating": { @@ -25444,7 +25191,7 @@ "maximum": 5, "minimum": -1, "nullable": true, - "type": "number", + "type": "integer", "x-immich-history": [ { "version": "v1", @@ -25463,12 +25210,7 @@ "x-immich-state": "Stable" }, "visibility": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetVisibility" - } - ], - "description": "Asset visibility" + "$ref": "#/components/schemas/AssetVisibility" } }, "type": "object" @@ -25481,8 +25223,7 @@ "type": "string" }, "maxItems": 128, - "type": "array", - "uniqueItems": true + "type": "array" }, "importPaths": { "description": "Import paths (max 128)", @@ -25490,11 +25231,11 @@ "type": "string" }, "maxItems": 128, - "type": "array", - "uniqueItems": true + "type": "array" }, "name": { "description": "Library name", + "minLength": 1, "type": "string" } }, @@ -25504,27 +25245,33 @@ "properties": { "photos": { "description": "Number of photos", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "quotaSizeInBytes": { "description": "User quota size in bytes (null if unlimited)", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "nullable": true, "type": "integer" }, "usage": { "description": "Total storage usage in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "usagePhotos": { "description": "Storage usage for photos in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "usageVideos": { "description": "Storage usage for videos in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "userId": { @@ -25537,6 +25284,8 @@ }, "videos": { "description": "Number of videos", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -25560,12 +25309,12 @@ "$ref": "#/components/schemas/UserAvatarColor" } ], - "description": "Avatar color", "nullable": true }, "email": { "description": "User email", "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, "isAdmin": { @@ -25588,11 +25337,12 @@ "description": "PIN code", "example": "123456", "nullable": true, + "pattern": "^\\d{6}$", "type": "string" }, "quotaSizeInBytes": { "description": "Storage quota in bytes", - "format": "int64", + "maximum": 9007199254740991, "minimum": 0, "nullable": true, "type": "integer" @@ -25626,30 +25376,33 @@ "UserAdminResponseDto": { "properties": { "avatarColor": { - "allOf": [ - { - "$ref": "#/components/schemas/UserAvatarColor" - } - ], - "description": "Avatar color" + "$ref": "#/components/schemas/UserAvatarColor" }, "createdAt": { "description": "Creation date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "deletedAt": { "description": "Deletion date", + "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" }, "email": { "description": "User email", + "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, "id": { "description": "User ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "isAdmin": { @@ -25662,7 +25415,6 @@ "$ref": "#/components/schemas/UserLicense" } ], - "description": "User license", "nullable": true }, "name": { @@ -25684,13 +25436,15 @@ }, "quotaSizeInBytes": { "description": "Storage quota in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": 0, "nullable": true, "type": "integer" }, "quotaUsageInBytes": { "description": "Storage usage in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": 0, "nullable": true, "type": "integer" }, @@ -25699,12 +25453,7 @@ "type": "boolean" }, "status": { - "allOf": [ - { - "$ref": "#/components/schemas/UserStatus" - } - ], - "description": "User status" + "$ref": "#/components/schemas/UserStatus" }, "storageLabel": { "description": "Storage label", @@ -25713,7 +25462,9 @@ }, "updatedAt": { "description": "Last update date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" } }, @@ -25746,12 +25497,12 @@ "$ref": "#/components/schemas/UserAvatarColor" } ], - "description": "Avatar color", "nullable": true }, "email": { "description": "User email", "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, "isAdmin": { @@ -25770,11 +25521,12 @@ "description": "PIN code", "example": "123456", "nullable": true, + "pattern": "^\\d{6}$", "type": "string" }, "quotaSizeInBytes": { "description": "Storage quota in bytes", - "format": "int64", + "maximum": 9007199254740991, "minimum": 0, "nullable": true, "type": "integer" @@ -25792,7 +25544,7 @@ "type": "object" }, "UserAvatarColor": { - "description": "Avatar color", + "description": "User avatar color", "enum": [ "primary", "pink", @@ -25811,7 +25563,9 @@ "properties": { "activatedAt": { "description": "Activation date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "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" }, "activationKey": { @@ -25819,7 +25573,8 @@ "type": "string" }, "licenseKey": { - "description": "License key", + "description": "License key (format: /^IM(SV|CL)(-[\\dA-Za-z]{4}){8}$/)", + "pattern": "^IM(SV|CL)(-[\\dA-Za-z]{4}){8}$", "type": "string" } }, @@ -25934,19 +25689,18 @@ "UserResponseDto": { "properties": { "avatarColor": { - "allOf": [ - { - "$ref": "#/components/schemas/UserAvatarColor" - } - ], - "description": "Avatar color" + "$ref": "#/components/schemas/UserAvatarColor" }, "email": { "description": "User email", + "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, "id": { "description": "User ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "name": { @@ -25990,12 +25744,12 @@ "$ref": "#/components/schemas/UserAvatarColor" } ], - "description": "Avatar color", "nullable": true }, "email": { "description": "User email", "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, "name": { @@ -26003,6 +25757,7 @@ "type": "string" }, "password": { + "deprecated": true, "description": "User password (deprecated, use change password endpoint)", "type": "string" } @@ -26029,8 +25784,7 @@ "type": "string" }, "maxItems": 128, - "type": "array", - "uniqueItems": true + "type": "array" }, "importPaths": { "description": "Import paths to validate (max 128)", @@ -26038,8 +25792,7 @@ "type": "string" }, "maxItems": 128, - "type": "array", - "uniqueItems": true + "type": "array" } }, "type": "object" @@ -26051,7 +25804,6 @@ "type": "string" }, "isValid": { - "default": false, "description": "Is valid", "type": "boolean" }, @@ -26108,7 +25860,7 @@ "type": "string" }, "VideoContainer": { - "description": "Accepted containers", + "description": "Accepted video containers", "enum": [ "mov", "mp4", @@ -26117,15 +25869,21 @@ ], "type": "string" }, + "WorkflowActionConfig": { + "additionalProperties": { + "$ref": "#/components/schemas/PluginConfigValue" + }, + "type": "object" + }, "WorkflowActionItemDto": { "properties": { "actionConfig": { - "description": "Action configuration", - "type": "object" + "$ref": "#/components/schemas/WorkflowActionConfig" }, "pluginActionId": { "description": "Plugin action ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -26137,9 +25895,12 @@ "WorkflowActionResponseDto": { "properties": { "actionConfig": { - "description": "Action configuration", - "nullable": true, - "type": "object" + "allOf": [ + { + "$ref": "#/components/schemas/WorkflowActionConfig" + } + ], + "nullable": true }, "id": { "description": "Action ID", @@ -26196,12 +25957,7 @@ "type": "string" }, "triggerType": { - "allOf": [ - { - "$ref": "#/components/schemas/PluginTriggerType" - } - ], - "description": "Workflow trigger type" + "$ref": "#/components/schemas/PluginTriggerType" } }, "required": [ @@ -26212,15 +25968,21 @@ ], "type": "object" }, + "WorkflowFilterConfig": { + "additionalProperties": { + "$ref": "#/components/schemas/PluginConfigValue" + }, + "type": "object" + }, "WorkflowFilterItemDto": { "properties": { "filterConfig": { - "description": "Filter configuration", - "type": "object" + "$ref": "#/components/schemas/WorkflowFilterConfig" }, "pluginFilterId": { "description": "Plugin filter ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -26232,9 +25994,12 @@ "WorkflowFilterResponseDto": { "properties": { "filterConfig": { - "description": "Filter configuration", - "nullable": true, - "type": "object" + "allOf": [ + { + "$ref": "#/components/schemas/WorkflowFilterConfig" + } + ], + "nullable": true }, "id": { "description": "Filter ID", @@ -26304,12 +26069,7 @@ "type": "string" }, "triggerType": { - "allOf": [ - { - "$ref": "#/components/schemas/PluginTriggerType" - } - ], - "description": "Workflow trigger type" + "$ref": "#/components/schemas/PluginTriggerType" } }, "required": [ @@ -26354,12 +26114,7 @@ "type": "string" }, "triggerType": { - "allOf": [ - { - "$ref": "#/components/schemas/PluginTriggerType" - } - ], - "description": "Workflow trigger type" + "$ref": "#/components/schemas/PluginTriggerType" } }, "type": "object" diff --git a/open-api/patch/asset_edit_action_item_dto.dart.patch b/open-api/patch/asset_edit_action_item_dto.dart.patch new file mode 100644 index 0000000000..b825795bf4 --- /dev/null +++ b/open-api/patch/asset_edit_action_item_dto.dart.patch @@ -0,0 +1,18 @@ +@@ -20,7 +20,7 @@ class AssetEditActionItemDto { + /// Type of edit action to perform + AssetEditAction action; + +- AssetEditActionItemDtoParameters parameters; ++ Map parameters; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetEditActionItemDto && +@@ -53,7 +53,7 @@ class AssetEditActionItemDto { + + return AssetEditActionItemDto( + action: AssetEditAction.fromJson(json[r'action'])!, +- parameters: AssetEditActionItemDtoParameters.fromJson(json[r'parameters'])!, ++ parameters: json[r'parameters'], + ); + } + return null; \ No newline at end of file diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index d74c2dd3e2..0875715beb 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -15,7 +15,6 @@ export const servers = { server1: "/api" }; export type UserResponseDto = { - /** Avatar color */ avatarColor: UserAvatarColor; /** User email */ email: string; @@ -37,7 +36,6 @@ export type ActivityResponseDto = { createdAt: string; /** Activity ID */ id: string; - /** Activity type */ "type": ReactionType; user: UserResponseDto; }; @@ -48,7 +46,6 @@ export type ActivityCreateDto = { assetId?: string; /** Comment text (required if type is comment) */ comment?: string; - /** Activity type (like or comment) */ "type": ReactionType; }; export type ActivityStatisticsResponseDto = { @@ -58,21 +55,26 @@ export type ActivityStatisticsResponseDto = { likes: number; }; export type DatabaseBackupDeleteDto = { + /** Backup filenames to delete */ backups: string[]; }; export type DatabaseBackupDto = { + /** Backup filename */ filename: string; + /** Backup file size */ filesize: number; + /** Backup timezone */ timezone: string; }; export type DatabaseBackupListResponseDto = { + /** List of backups */ backups: DatabaseBackupDto[]; }; export type DatabaseBackupUploadDto = { + /** Database backup file */ file?: Blob; }; export type SetMaintenanceModeDto = { - /** Maintenance action */ action: MaintenanceAction; /** Restore backup filename */ restoreBackupFilename?: string; @@ -80,7 +82,6 @@ export type SetMaintenanceModeDto = { export type MaintenanceDetectInstallStorageFolderDto = { /** Number of files in the folder */ files: number; - /** Storage folder */ folder: StorageFolder; /** Whether the folder is readable */ readable: boolean; @@ -99,7 +100,6 @@ export type MaintenanceAuthDto = { username: string; }; export type MaintenanceStatusResponseDto = { - /** Maintenance action */ action: MaintenanceAction; active: boolean; error?: string; @@ -108,16 +108,16 @@ export type MaintenanceStatusResponseDto = { }; export type NotificationCreateDto = { /** Additional notification data */ - data?: object; + data?: { + [key: string]: any; + }; /** Notification description */ description?: string | null; - /** Notification level */ level?: NotificationLevel; /** Date when notification was read */ readAt?: string | null; /** Notification title */ title: string; - /** Notification type */ "type"?: NotificationType; /** User ID to send notification to */ userId: string; @@ -126,18 +126,18 @@ export type NotificationDto = { /** Creation date */ createdAt: string; /** Additional notification data */ - data?: object; + data?: { + [key: string]: any; + }; /** Notification description */ description?: string; /** Notification ID */ id: string; - /** Notification level */ level: NotificationLevel; /** Date when notification was read */ readAt?: string; /** Notification title */ title: string; - /** Notification type */ "type": NotificationType; }; export type TemplateDto = { @@ -182,11 +182,10 @@ export type UserLicense = { activatedAt: string; /** Activation key */ activationKey: string; - /** License key */ + /** License key (format: /^IM(SV|CL)(-[\dA-Za-z]{4}){8}$/) */ licenseKey: string; }; export type UserAdminResponseDto = { - /** Avatar color */ avatarColor: UserAvatarColor; /** Creation date */ createdAt: string; @@ -198,7 +197,6 @@ export type UserAdminResponseDto = { id: string; /** Is admin user */ isAdmin: boolean; - /** User license */ license: (UserLicense) | null; /** User name */ name: string; @@ -214,7 +212,6 @@ export type UserAdminResponseDto = { quotaUsageInBytes: number | null; /** Require password change on next login */ shouldChangePassword: boolean; - /** User status */ status: UserStatus; /** Storage label */ storageLabel: string | null; @@ -222,7 +219,6 @@ export type UserAdminResponseDto = { updatedAt: string; }; export type UserAdminCreateDto = { - /** Avatar color */ avatarColor?: (UserAvatarColor) | null; /** User email */ email: string; @@ -248,7 +244,6 @@ export type UserAdminDeleteDto = { force?: boolean; }; export type UserAdminUpdateDto = { - /** Avatar color */ avatarColor?: (UserAvatarColor) | null; /** User email */ email?: string; @@ -268,7 +263,6 @@ export type UserAdminUpdateDto = { storageLabel?: string | null; }; export type AlbumsResponse = { - /** Default asset order for albums */ defaultAssetOrder: AssetOrder; }; export type CastResponse = { @@ -343,11 +337,9 @@ export type UserPreferencesResponseDto = { tags: TagsResponse; }; export type AlbumsUpdate = { - /** Default asset order for albums */ defaultAssetOrder?: AssetOrder; }; export type AvatarUpdate = { - /** Avatar color */ color?: UserAvatarColor; }; export type CastUpdate = { @@ -451,10 +443,306 @@ export type AssetStatsResponseDto = { videos: number; }; export type AlbumUserResponseDto = { - /** Album user role */ role: AlbumUserRole; user: UserResponseDto; }; +export type ContributorCountResponseDto = { + /** Number of assets contributed */ + assetCount: number; + /** User ID */ + userId: string; +}; +export type AlbumResponseDto = { + /** Album name */ + albumName: string; + /** Thumbnail asset ID */ + albumThumbnailAssetId: string | null; + albumUsers: AlbumUserResponseDto[]; + /** Number of assets */ + assetCount: number; + contributorCounts?: ContributorCountResponseDto[]; + /** Creation date */ + createdAt: string; + /** Album description */ + description: string; + /** End date (latest asset) */ + endDate?: string; + /** Has shared link */ + hasSharedLink: boolean; + /** Album ID */ + id: string; + /** Activity feed enabled */ + isActivityEnabled: boolean; + /** Last modified asset timestamp */ + lastModifiedAssetTimestamp?: string; + order?: AssetOrder; + owner: UserResponseDto; + /** Owner user ID */ + ownerId: string; + /** Is shared album */ + shared: boolean; + /** Start date (earliest asset) */ + startDate?: string; + /** Last update date */ + updatedAt: string; +}; +export type AlbumUserCreateDto = { + role: AlbumUserRole; + /** User ID */ + userId: string; +}; +export type CreateAlbumDto = { + /** Album name */ + albumName: string; + /** Album users */ + albumUsers?: AlbumUserCreateDto[]; + /** Initial asset IDs */ + assetIds?: string[]; + /** Album description */ + description?: string; +}; +export type AlbumsAddAssetsDto = { + /** Album IDs */ + albumIds: string[]; + /** Asset IDs */ + assetIds: string[]; +}; +export type AlbumsAddAssetsResponseDto = { + error?: BulkIdErrorReason; + /** Operation success */ + success: boolean; +}; +export type AlbumStatisticsResponseDto = { + /** Number of non-shared albums */ + notShared: number; + /** Number of owned albums */ + owned: number; + /** Number of shared albums */ + shared: number; +}; +export type UpdateAlbumDto = { + /** Album name */ + albumName?: string; + /** Album thumbnail asset ID */ + albumThumbnailAssetId?: string; + /** Album description */ + description?: string; + /** Enable activity feed */ + isActivityEnabled?: boolean; + order?: AssetOrder; +}; +export type BulkIdsDto = { + /** IDs to process */ + ids: string[]; +}; +export type BulkIdResponseDto = { + error?: BulkIdErrorReason; + errorMessage?: string; + /** ID */ + id: string; + /** Whether operation succeeded */ + success: boolean; +}; +export type MapMarkerResponseDto = { + /** City name */ + city: string | null; + /** Country name */ + country: string | null; + /** Asset ID */ + id: string; + /** Latitude */ + lat: number; + /** Longitude */ + lon: number; + /** State/Province name */ + state: string | null; +}; +export type UpdateAlbumUserDto = { + role: AlbumUserRole; +}; +export type AlbumUserAddDto = { + /** Album user role */ + role?: AlbumUserRole; + /** User ID */ + userId: string; +}; +export type AddUsersDto = { + /** Album users to add */ + albumUsers: AlbumUserAddDto[]; +}; +export type ApiKeyResponseDto = { + /** Creation date */ + createdAt: string; + /** API key ID */ + id: string; + /** API key name */ + name: string; + /** List of permissions */ + permissions: Permission[]; + /** Last update date */ + updatedAt: string; +}; +export type ApiKeyCreateDto = { + /** API key name */ + name?: string; + /** List of permissions */ + permissions: Permission[]; +}; +export type ApiKeyCreateResponseDto = { + apiKey: ApiKeyResponseDto; + /** API key secret (only shown once) */ + secret: string; +}; +export type ApiKeyUpdateDto = { + /** API key name */ + name?: string; + /** List of permissions */ + permissions?: Permission[]; +}; +export type AssetBulkDeleteDto = { + /** Force delete even if in use */ + force?: boolean; + /** IDs to process */ + ids: string[]; +}; +export type AssetMetadataUpsertItemDto = { + /** Metadata key */ + key: string; + /** Metadata value (object) */ + value: { + [key: string]: any; + }; +}; +export type AssetMediaCreateDto = { + /** Asset file data */ + assetData: Blob; + /** Duration (for videos) */ + duration?: string; + /** File creation date */ + fileCreatedAt: string; + /** File modification date */ + fileModifiedAt: string; + /** Filename */ + filename?: string; + /** Mark as favorite */ + isFavorite?: boolean; + /** Live photo video ID */ + livePhotoVideoId?: string; + /** Asset metadata items */ + metadata?: AssetMetadataUpsertItemDto[]; + /** Sidecar file data */ + sidecarData?: Blob; + visibility?: AssetVisibility; +}; +export type AssetMediaResponseDto = { + /** Asset media ID */ + id: string; + status: AssetMediaStatus; +}; +export type AssetBulkUpdateDto = { + /** Original date and time */ + dateTimeOriginal?: string; + /** Relative time offset in seconds */ + dateTimeRelative?: number; + /** Asset description */ + description?: string; + /** Duplicate ID */ + duplicateId?: string | null; + /** Asset IDs to update */ + ids: string[]; + /** Mark as favorite */ + isFavorite?: boolean; + /** Latitude coordinate */ + latitude?: number; + /** Longitude coordinate */ + longitude?: number; + /** Rating in range [1-5], or null for unrated */ + rating?: number | null; + /** Time zone (IANA timezone) */ + timeZone?: string; + visibility?: AssetVisibility; +}; +export type AssetBulkUploadCheckItem = { + /** Base64 or hex encoded SHA1 hash */ + checksum: string; + /** Asset ID */ + id: string; +}; +export type AssetBulkUploadCheckDto = { + /** Assets to check */ + assets: AssetBulkUploadCheckItem[]; +}; +export type AssetBulkUploadCheckResult = { + action: AssetUploadAction; + /** Existing asset ID if duplicate */ + assetId?: string; + /** Asset ID */ + id: string; + /** Whether existing asset is trashed */ + isTrashed?: boolean; + reason?: AssetRejectReason; +}; +export type AssetBulkUploadCheckResponseDto = { + /** Upload check results */ + results: AssetBulkUploadCheckResult[]; +}; +export type AssetCopyDto = { + /** Copy album associations */ + albums?: boolean; + /** Copy favorite status */ + favorite?: boolean; + /** Copy shared links */ + sharedLinks?: boolean; + /** Copy sidecar file */ + sidecar?: boolean; + /** Source asset ID */ + sourceId: string; + /** Copy stack association */ + stack?: boolean; + /** Target asset ID */ + targetId: string; +}; +export type AssetJobsDto = { + /** Asset IDs */ + assetIds: string[]; + name: AssetJobName; +}; +export type AssetMetadataBulkDeleteItemDto = { + /** Asset ID */ + assetId: string; + /** Metadata key */ + key: string; +}; +export type AssetMetadataBulkDeleteDto = { + /** Metadata items to delete */ + items: AssetMetadataBulkDeleteItemDto[]; +}; +export type AssetMetadataBulkUpsertItemDto = { + /** Asset ID */ + assetId: string; + /** Metadata key */ + key: string; + /** Metadata value (object) */ + value: { + [key: string]: any; + }; +}; +export type AssetMetadataBulkUpsertDto = { + /** Metadata items to upsert */ + items: AssetMetadataBulkUpsertItemDto[]; +}; +export type AssetMetadataBulkResponseDto = { + /** Asset ID */ + assetId: string; + /** Metadata key */ + key: string; + /** Last update date */ + updatedAt: string; + /** Metadata value (object) */ + value: { + [key: string]: any; + }; +}; export type ExifResponseDto = { /** City name */ city?: string | null; @@ -516,7 +804,6 @@ export type AssetFaceWithoutPersonResponseDto = { imageHeight: number; /** Image width in pixels */ imageWidth: number; - /** Face detection source type */ sourceType?: SourceType; }; export type PersonWithFacesResponseDto = { @@ -524,7 +811,6 @@ export type PersonWithFacesResponseDto = { birthDate: string | null; /** Person color (hex) */ color?: string; - /** Face detections */ faces: AssetFaceWithoutPersonResponseDto[]; /** Person ID */ id: string; @@ -568,10 +854,6 @@ export type AssetResponseDto = { checksum: string; /** The UTC timestamp when the asset was originally uploaded to Immich. */ createdAt: string; - /** Device asset ID */ - deviceAssetId: string; - /** Device ID */ - deviceId: string; /** Duplicate group ID */ duplicateId?: string | null; /** Video duration (for videos) */ @@ -619,320 +901,14 @@ export type AssetResponseDto = { tags?: TagResponseDto[]; /** Thumbhash for thumbnail generation (base64) also used as the c query param for thumbnail cache busting. */ thumbhash: string | null; - /** Asset type */ "type": AssetTypeEnum; unassignedFaces?: AssetFaceWithoutPersonResponseDto[]; /** The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified. */ updatedAt: string; - /** Asset visibility */ visibility: AssetVisibility; /** Asset width */ width: number | null; }; -export type ContributorCountResponseDto = { - /** Number of assets contributed */ - assetCount: number; - /** User ID */ - userId: string; -}; -export type AlbumResponseDto = { - /** Album name */ - albumName: string; - /** Thumbnail asset ID */ - albumThumbnailAssetId: string | null; - albumUsers: AlbumUserResponseDto[]; - /** Number of assets */ - assetCount: number; - assets: AssetResponseDto[]; - contributorCounts?: ContributorCountResponseDto[]; - /** Creation date */ - createdAt: string; - /** Album description */ - description: string; - /** End date (latest asset) */ - endDate?: string; - /** Has shared link */ - hasSharedLink: boolean; - /** Album ID */ - id: string; - /** Activity feed enabled */ - isActivityEnabled: boolean; - /** Last modified asset timestamp */ - lastModifiedAssetTimestamp?: string; - /** Asset sort order */ - order?: AssetOrder; - owner: UserResponseDto; - /** Owner user ID */ - ownerId: string; - /** Is shared album */ - shared: boolean; - /** Start date (earliest asset) */ - startDate?: string; - /** Last update date */ - updatedAt: string; -}; -export type AlbumUserCreateDto = { - /** Album user role */ - role: AlbumUserRole; - /** User ID */ - userId: string; -}; -export type CreateAlbumDto = { - /** Album name */ - albumName: string; - /** Album users */ - albumUsers?: AlbumUserCreateDto[]; - /** Initial asset IDs */ - assetIds?: string[]; - /** Album description */ - description?: string; -}; -export type AlbumsAddAssetsDto = { - /** Album IDs */ - albumIds: string[]; - /** Asset IDs */ - assetIds: string[]; -}; -export type AlbumsAddAssetsResponseDto = { - /** Error reason */ - error?: BulkIdErrorReason; - /** Operation success */ - success: boolean; -}; -export type AlbumStatisticsResponseDto = { - /** Number of non-shared albums */ - notShared: number; - /** Number of owned albums */ - owned: number; - /** Number of shared albums */ - shared: number; -}; -export type UpdateAlbumDto = { - /** Album name */ - albumName?: string; - /** Album thumbnail asset ID */ - albumThumbnailAssetId?: string; - /** Album description */ - description?: string; - /** Enable activity feed */ - isActivityEnabled?: boolean; - /** Asset sort order */ - order?: AssetOrder; -}; -export type BulkIdsDto = { - /** IDs to process */ - ids: string[]; -}; -export type BulkIdResponseDto = { - /** Error reason if failed */ - error?: Error; - errorMessage?: string; - /** ID */ - id: string; - /** Whether operation succeeded */ - success: boolean; -}; -export type UpdateAlbumUserDto = { - /** Album user role */ - role: AlbumUserRole; -}; -export type AlbumUserAddDto = { - /** Album user role */ - role?: AlbumUserRole; - /** User ID */ - userId: string; -}; -export type AddUsersDto = { - /** Album users to add */ - albumUsers: AlbumUserAddDto[]; -}; -export type ApiKeyResponseDto = { - /** Creation date */ - createdAt: string; - /** API key ID */ - id: string; - /** API key name */ - name: string; - /** List of permissions */ - permissions: Permission[]; - /** Last update date */ - updatedAt: string; -}; -export type ApiKeyCreateDto = { - /** API key name */ - name?: string; - /** List of permissions */ - permissions: Permission[]; -}; -export type ApiKeyCreateResponseDto = { - apiKey: ApiKeyResponseDto; - /** API key secret (only shown once) */ - secret: string; -}; -export type ApiKeyUpdateDto = { - /** API key name */ - name?: string; - /** List of permissions */ - permissions?: Permission[]; -}; -export type AssetBulkDeleteDto = { - /** Force delete even if in use */ - force?: boolean; - /** IDs to process */ - ids: string[]; -}; -export type AssetMetadataUpsertItemDto = { - /** Metadata key */ - key: string; - /** Metadata value (object) */ - value: object; -}; -export type AssetMediaCreateDto = { - /** Asset file data */ - assetData: Blob; - /** Device asset ID */ - deviceAssetId: string; - /** Device ID */ - deviceId: string; - /** Duration (for videos) */ - duration?: string; - /** File creation date */ - fileCreatedAt: string; - /** File modification date */ - fileModifiedAt: string; - /** Filename */ - filename?: string; - /** Mark as favorite */ - isFavorite?: boolean; - /** Live photo video ID */ - livePhotoVideoId?: string; - /** Asset metadata items */ - metadata?: AssetMetadataUpsertItemDto[]; - /** Sidecar file data */ - sidecarData?: Blob; - /** Asset visibility */ - visibility?: AssetVisibility; -}; -export type AssetMediaResponseDto = { - /** Asset media ID */ - id: string; - /** Upload status */ - status: AssetMediaStatus; -}; -export type AssetBulkUpdateDto = { - /** Original date and time */ - dateTimeOriginal?: string; - /** Relative time offset in seconds */ - dateTimeRelative?: number; - /** Asset description */ - description?: string; - /** Duplicate ID */ - duplicateId?: string | null; - /** Asset IDs to update */ - ids: string[]; - /** Mark as favorite */ - isFavorite?: boolean; - /** Latitude coordinate */ - latitude?: number; - /** Longitude coordinate */ - longitude?: number; - /** Rating in range [1-5], or null for unrated */ - rating?: number | null; - /** Time zone (IANA timezone) */ - timeZone?: string; - /** Asset visibility */ - visibility?: AssetVisibility; -}; -export type AssetBulkUploadCheckItem = { - /** Base64 or hex encoded SHA1 hash */ - checksum: string; - /** Asset ID */ - id: string; -}; -export type AssetBulkUploadCheckDto = { - /** Assets to check */ - assets: AssetBulkUploadCheckItem[]; -}; -export type AssetBulkUploadCheckResult = { - /** Upload action */ - action: Action; - /** Existing asset ID if duplicate */ - assetId?: string; - /** Asset ID */ - id: string; - /** Whether existing asset is trashed */ - isTrashed?: boolean; - /** Rejection reason if rejected */ - reason?: Reason; -}; -export type AssetBulkUploadCheckResponseDto = { - /** Upload check results */ - results: AssetBulkUploadCheckResult[]; -}; -export type AssetCopyDto = { - /** Copy album associations */ - albums?: boolean; - /** Copy favorite status */ - favorite?: boolean; - /** Copy shared links */ - sharedLinks?: boolean; - /** Copy sidecar file */ - sidecar?: boolean; - /** Source asset ID */ - sourceId: string; - /** Copy stack association */ - stack?: boolean; - /** Target asset ID */ - targetId: string; -}; -export type CheckExistingAssetsDto = { - /** Device asset IDs to check */ - deviceAssetIds: string[]; - /** Device ID */ - deviceId: string; -}; -export type CheckExistingAssetsResponseDto = { - /** Existing asset IDs */ - existingIds: string[]; -}; -export type AssetJobsDto = { - /** Asset IDs */ - assetIds: string[]; - /** Job name */ - name: AssetJobName; -}; -export type AssetMetadataBulkDeleteItemDto = { - /** Asset ID */ - assetId: string; - /** Metadata key */ - key: string; -}; -export type AssetMetadataBulkDeleteDto = { - /** Metadata items to delete */ - items: AssetMetadataBulkDeleteItemDto[]; -}; -export type AssetMetadataBulkUpsertItemDto = { - /** Asset ID */ - assetId: string; - /** Metadata key */ - key: string; - /** Metadata value (object) */ - value: object; -}; -export type AssetMetadataBulkUpsertDto = { - /** Metadata items to upsert */ - items: AssetMetadataBulkUpsertItemDto[]; -}; -export type AssetMetadataBulkResponseDto = { - /** Asset ID */ - assetId: string; - /** Metadata key */ - key: string; - /** Last update date */ - updatedAt: string; - /** Metadata value (object) */ - value: object; -}; export type UpdateAssetDto = { /** Original date and time */ dateTimeOriginal?: string; @@ -948,7 +924,6 @@ export type UpdateAssetDto = { longitude?: number; /** Rating in range [1-5], or null for unrated */ rating?: number | null; - /** Asset visibility */ visibility?: AssetVisibility; }; export type CropParameters = { @@ -966,12 +941,11 @@ export type RotateParameters = { angle: number; }; export type MirrorParameters = { - /** Axis to mirror along */ axis: MirrorAxis; }; export type AssetEditActionItemResponseDto = { - /** Type of edit action to perform */ action: AssetEditAction; + /** Asset edit ID */ id: string; /** List of edit actions to apply (crop, rotate, or mirror) */ parameters: CropParameters | RotateParameters | MirrorParameters; @@ -983,7 +957,6 @@ export type AssetEditsResponseDto = { edits: AssetEditActionItemResponseDto[]; }; export type AssetEditActionItemDto = { - /** Type of edit action to perform */ action: AssetEditAction; /** List of edit actions to apply (crop, rotate, or mirror) */ parameters: CropParameters | RotateParameters | MirrorParameters; @@ -998,7 +971,9 @@ export type AssetMetadataResponseDto = { /** Last update date */ updatedAt: string; /** Metadata value (object) */ - value: object; + value: { + [key: string]: any; + }; }; export type AssetMetadataUpsertDto = { /** Metadata items to upsert */ @@ -1030,22 +1005,6 @@ export type AssetOcrResponseDto = { /** Normalized y coordinate of box corner 4 (0-1) */ y4: number; }; -export type AssetMediaReplaceDto = { - /** Asset file data */ - assetData: Blob; - /** Device asset ID */ - deviceAssetId: string; - /** Device ID */ - deviceId: string; - /** Duration (for videos) */ - duration?: string; - /** File creation date */ - fileCreatedAt: string; - /** File modification date */ - fileModifiedAt: string; - /** Filename */ - filename?: string; -}; export type SignUpDto = { /** User email */ email: string; @@ -1212,9 +1171,7 @@ export type AssetFaceResponseDto = { imageHeight: number; /** Image width in pixels */ imageWidth: number; - /** Person associated with face */ person: (PersonResponseDto) | null; - /** Face detection source type */ sourceType?: SourceType; }; export type AssetFaceCreateDto = { @@ -1288,11 +1245,9 @@ export type QueuesResponseLegacyDto = { workflow: QueueResponseLegacyDto; }; export type JobCreateDto = { - /** Job name */ name: ManualJobName; }; export type QueueCommandDto = { - /** Queue command to execute */ command: QueueCommand; /** Force the command execution (if applicable) */ force?: boolean; @@ -1363,20 +1318,6 @@ export type ValidateLibraryResponseDto = { /** Validation results for import paths */ importPaths?: ValidateLibraryImportPathResponseDto[]; }; -export type MapMarkerResponseDto = { - /** City name */ - city: string | null; - /** Country name */ - country: string | null; - /** Asset ID */ - id: string; - /** Latitude */ - lat: number; - /** Longitude */ - lon: number; - /** State/Province name */ - state: string | null; -}; export type MapReverseGeocodeResponseDto = { /** City name */ city: string | null; @@ -1410,7 +1351,6 @@ export type MemoryResponseDto = { seenAt?: string; /** Date when memory should be shown */ showAt?: string; - /** Memory type */ "type": MemoryType; /** Last update date */ updatedAt: string; @@ -1429,7 +1369,6 @@ export type MemoryCreateDto = { seenAt?: string; /** Date when memory should be shown */ showAt?: string; - /** Memory type */ "type": MemoryType; }; export type MemoryStatisticsResponseDto = { @@ -1479,7 +1418,6 @@ export type OAuthCallbackDto = { url: string; }; export type PartnerResponseDto = { - /** Avatar color */ avatarColor: UserAvatarColor; /** User email */ email: string; @@ -1507,7 +1445,6 @@ export type PeopleResponseDto = { hasNextPage?: boolean; /** Number of hidden people */ hidden: number; - /** List of people */ people: PersonResponseDto[]; /** Total number of people */ total: number; @@ -1576,6 +1513,27 @@ export type PersonStatisticsResponseDto = { /** Number of assets */ assets: number; }; +export type PluginJsonSchemaProperty = { + additionalProperties?: boolean | PluginJsonSchemaProperty; + "default"?: any; + description?: string; + "enum"?: string[]; + items?: PluginJsonSchemaProperty; + properties?: { + [key: string]: PluginJsonSchemaProperty; + }; + required?: string[]; + "type"?: PluginJsonSchemaType; +}; +export type PluginJsonSchema = { + additionalProperties?: boolean; + description?: string; + properties?: { + [key: string]: PluginJsonSchemaProperty; + }; + required?: string[]; + "type"?: PluginJsonSchemaType; +}; export type PluginActionResponseDto = { /** Action description */ description: string; @@ -1586,7 +1544,7 @@ export type PluginActionResponseDto = { /** Plugin ID */ pluginId: string; /** Action schema */ - schema: object | null; + schema: (PluginJsonSchema) | null; /** Supported contexts */ supportedContexts: PluginContextType[]; /** Action title */ @@ -1602,7 +1560,7 @@ export type PluginFilterResponseDto = { /** Plugin ID */ pluginId: string; /** Filter schema */ - schema: object | null; + schema: (PluginJsonSchema) | null; /** Supported contexts */ supportedContexts: PluginContextType[]; /** Filter title */ @@ -1631,15 +1589,12 @@ export type PluginResponseDto = { version: string; }; export type PluginTriggerResponseDto = { - /** Context type */ contextType: PluginContextType; - /** Trigger type */ "type": PluginTriggerType; }; export type QueueResponseDto = { /** Whether the queue is paused */ isPaused: boolean; - /** Queue name */ name: QueueName; statistics: QueueStatisticsDto; }; @@ -1653,10 +1608,11 @@ export type QueueDeleteDto = { }; export type QueueJobResponseDto = { /** Job data payload */ - data: object; + data: { + [key: string]: any; + }; /** Job ID */ id?: string; - /** Job name */ name: JobName; /** Job creation timestamp */ timestamp: number; @@ -1686,10 +1642,6 @@ export type MetadataSearchDto = { createdBefore?: string; /** Filter by description text */ description?: string; - /** Filter by device asset ID */ - deviceAssetId?: string; - /** Device ID to filter by */ - deviceId?: string; /** Filter by encoded video file path */ encodedVideoPath?: string; /** Filter by asset ID */ @@ -1709,7 +1661,7 @@ export type MetadataSearchDto = { /** Library ID to filter by */ libraryId?: string | null; /** Filter by camera make */ - make?: string; + make?: string | null; /** Filter by camera model */ model?: string | null; /** Filter by OCR text content */ @@ -1744,13 +1696,11 @@ export type MetadataSearchDto = { trashedAfter?: string; /** Filter by trash date (before) */ trashedBefore?: string; - /** Asset type filter */ "type"?: AssetTypeEnum; /** Filter by update date (after) */ updatedAfter?: string; /** Filter by update date (before) */ updatedBefore?: string; - /** Filter by visibility */ visibility?: AssetVisibility; /** Include deleted assets */ withDeleted?: boolean; @@ -1768,7 +1718,6 @@ export type SearchFacetCountResponseDto = { value: string; }; export type SearchFacetResponseDto = { - /** Facet counts */ counts: SearchFacetCountResponseDto[]; /** Facet field name */ fieldName: string; @@ -1818,8 +1767,6 @@ export type RandomSearchDto = { createdAfter?: string; /** Filter by creation date (before) */ createdBefore?: string; - /** Device ID to filter by */ - deviceId?: string; /** Filter by encoded status */ isEncoded?: boolean; /** Filter by favorite status */ @@ -1835,7 +1782,7 @@ export type RandomSearchDto = { /** Library ID to filter by */ libraryId?: string | null; /** Filter by camera make */ - make?: string; + make?: string | null; /** Filter by camera model */ model?: string | null; /** Filter by OCR text content */ @@ -1858,13 +1805,11 @@ export type RandomSearchDto = { trashedAfter?: string; /** Filter by trash date (before) */ trashedBefore?: string; - /** Asset type filter */ "type"?: AssetTypeEnum; /** Filter by update date (after) */ updatedAfter?: string; /** Filter by update date (before) */ updatedBefore?: string; - /** Filter by visibility */ visibility?: AssetVisibility; /** Include deleted assets */ withDeleted?: boolean; @@ -1886,8 +1831,6 @@ export type SmartSearchDto = { createdAfter?: string; /** Filter by creation date (before) */ createdBefore?: string; - /** Device ID to filter by */ - deviceId?: string; /** Filter by encoded status */ isEncoded?: boolean; /** Filter by favorite status */ @@ -1905,7 +1848,7 @@ export type SmartSearchDto = { /** Library ID to filter by */ libraryId?: string | null; /** Filter by camera make */ - make?: string; + make?: string | null; /** Filter by camera model */ model?: string | null; /** Filter by OCR text content */ @@ -1934,13 +1877,11 @@ export type SmartSearchDto = { trashedAfter?: string; /** Filter by trash date (before) */ trashedBefore?: string; - /** Asset type filter */ "type"?: AssetTypeEnum; /** Filter by update date (after) */ updatedAfter?: string; /** Filter by update date (before) */ updatedBefore?: string; - /** Filter by visibility */ visibility?: AssetVisibility; /** Include deleted assets */ withDeleted?: boolean; @@ -1960,8 +1901,6 @@ export type StatisticsSearchDto = { createdBefore?: string; /** Filter by description text */ description?: string; - /** Device ID to filter by */ - deviceId?: string; /** Filter by encoded status */ isEncoded?: boolean; /** Filter by favorite status */ @@ -1977,7 +1916,7 @@ export type StatisticsSearchDto = { /** Library ID to filter by */ libraryId?: string | null; /** Filter by camera make */ - make?: string; + make?: string | null; /** Filter by camera model */ model?: string | null; /** Filter by OCR text content */ @@ -1998,13 +1937,11 @@ export type StatisticsSearchDto = { trashedAfter?: string; /** Filter by trash date (before) */ trashedBefore?: string; - /** Asset type filter */ "type"?: AssetTypeEnum; /** Filter by update date (after) */ updatedAfter?: string; /** Filter by update date (before) */ updatedBefore?: string; - /** Filter by visibility */ visibility?: AssetVisibility; }; export type SearchStatisticsResponseDto = { @@ -2121,18 +2058,10 @@ export type ServerFeaturesDto = { /** Whether trash feature is enabled */ trash: boolean; }; -export type LicenseResponseDto = { - /** Activation date */ - activatedAt: string; - /** Activation key */ - activationKey: string; - /** License key (format: IM(SV|CL)(-XXXX){8}) */ - licenseKey: string; -}; export type LicenseKeyDto = { /** Activation key */ activationKey: string; - /** License key (format: IM(SV|CL)(-XXXX){8}) */ + /** License key (format: /^IM(SV|CL)(-[\dA-Za-z]{4}){8}$/) */ licenseKey: string; }; export type ServerMediaTypesResponseDto = { @@ -2143,8 +2072,7 @@ export type ServerMediaTypesResponseDto = { /** Supported video MIME types */ video: string[]; }; -export type ServerPingResponse = {}; -export type ServerPingResponseRead = { +export type ServerPingResponse = { res: string; }; export type UsageByUserDto = { @@ -2170,6 +2098,7 @@ export type ServerStatsResponseDto = { photos: number; /** Total storage usage in bytes */ usage: number; + /** Array of usage for each user */ usageByUser: UsageByUserDto[]; /** Storage usage for photos in bytes */ usagePhotos: number; @@ -2277,9 +2206,6 @@ export type SharedLinkResponseDto = { showMetadata: boolean; /** Custom URL slug */ slug: string | null; - /** Access token */ - token?: string | null; - /** Shared link type */ "type": SharedLinkType; /** Owner user ID */ userId: string; @@ -2303,7 +2229,6 @@ export type SharedLinkCreateDto = { showMetadata?: boolean; /** Custom URL slug */ slug?: string | null; - /** Shared link type */ "type": SharedLinkType; }; export type SharedLinkLoginDto = { @@ -2335,13 +2260,11 @@ export type AssetIdsDto = { export type AssetIdsResponseDto = { /** Asset ID */ assetId: string; - /** Error reason if failed */ - error?: Error2; + error?: AssetIdErrorReason; /** Whether operation succeeded */ success: boolean; }; export type StackResponseDto = { - /** Stack assets */ assets: AssetResponseDto[]; /** Stack ID */ id: string; @@ -2363,37 +2286,12 @@ export type SyncAckDeleteDto = { export type SyncAckDto = { /** Acknowledgment ID */ ack: string; - /** Sync entity type */ "type": SyncEntityType; }; export type SyncAckSetDto = { /** Acknowledgment IDs (max 1000) */ acks: string[]; }; -export type AssetDeltaSyncDto = { - /** Sync assets updated after this date */ - updatedAfter: string; - /** User IDs to sync */ - userIds: string[]; -}; -export type AssetDeltaSyncResponseDto = { - /** Deleted asset IDs */ - deleted: string[]; - /** Whether full sync is needed */ - needsFullSync: boolean; - /** Upserted assets */ - upserted: AssetResponseDto[]; -}; -export type AssetFullSyncDto = { - /** Last asset ID (pagination) */ - lastId?: string; - /** Maximum number of assets to return */ - limit: number; - /** Sync assets updated until this date */ - updatedUntil: string; - /** Filter by user ID */ - userId?: string; -}; export type SyncStreamDto = { /** Reset sync state */ reset?: boolean; @@ -2412,7 +2310,6 @@ export type SystemConfigBackupsDto = { database: DatabaseBackupConfig; }; export type SystemConfigFFmpegDto = { - /** Transcode hardware acceleration */ accel: TranscodeHWAccel; /** Accelerated decode */ accelDecode: boolean; @@ -2424,7 +2321,6 @@ export type SystemConfigFFmpegDto = { acceptedVideoCodecs: VideoCodec[]; /** B-frames */ bframes: number; - /** CQ mode */ cqMode: CQMode; /** CRF */ crf: number; @@ -2438,19 +2334,15 @@ export type SystemConfigFFmpegDto = { preset: string; /** References */ refs: number; - /** Target audio codec */ targetAudioCodec: AudioCodec; /** Target resolution */ targetResolution: string; - /** Target video codec */ targetVideoCodec: VideoCodec; /** Temporal AQ */ temporalAQ: boolean; /** Threads */ threads: number; - /** Tone mapping */ tonemap: ToneMapping; - /** Transcode policy */ transcode: TranscodePolicy; /** Two pass */ twoPass: boolean; @@ -2458,7 +2350,6 @@ export type SystemConfigFFmpegDto = { export type SystemConfigGeneratedFullsizeImageDto = { /** Enabled */ enabled: boolean; - /** Image format */ format: ImageFormat; /** Progressive */ progressive?: boolean; @@ -2466,8 +2357,8 @@ export type SystemConfigGeneratedFullsizeImageDto = { quality: number; }; export type SystemConfigGeneratedImageDto = { - /** Image format */ format: ImageFormat; + /** Progressive */ progressive?: boolean; /** Quality */ quality: number; @@ -2475,7 +2366,6 @@ export type SystemConfigGeneratedImageDto = { size: number; }; export type SystemConfigImageDto = { - /** Colorspace */ colorspace: Colorspace; /** Extract embedded */ extractEmbedded: boolean; @@ -2504,6 +2394,7 @@ export type SystemConfigJobDto = { workflow: JobSettingsDto; }; export type SystemConfigLibraryScanDto = { + /** Cron expression */ cronExpression: string; /** Enabled */ enabled: boolean; @@ -2571,12 +2462,15 @@ export type SystemConfigMachineLearningDto = { enabled: boolean; facialRecognition: FacialRecognitionConfig; ocr: OcrConfig; + /** ML service URLs */ urls: string[]; }; export type SystemConfigMapDto = { + /** Dark map style URL */ darkStyle: string; /** Enabled */ enabled: boolean; + /** Light map style URL */ lightStyle: string; }; export type SystemConfigFacesDto = { @@ -2599,6 +2493,7 @@ export type SystemConfigNightlyTasksDto = { generateMemories: boolean; /** Missing thumbnails */ missingThumbnails: boolean; + /** Start time */ startTime: string; /** Sync quota usage */ syncQuotaUsage: boolean; @@ -2625,7 +2520,7 @@ export type SystemConfigOAuthDto = { issuerUrl: string; /** Mobile override enabled */ mobileOverrideEnabled: boolean; - /** Mobile redirect URI */ + /** Mobile redirect URI (set to empty string to disable) */ mobileRedirectUri: string; /** Profile signing algorithm */ profileSigningAlgorithm: string; @@ -2633,6 +2528,7 @@ export type SystemConfigOAuthDto = { roleClaim: string; /** Scope */ scope: string; + /** Signing algorithm */ signingAlgorithm: string; /** Storage label claim */ storageLabelClaim: string; @@ -2640,7 +2536,6 @@ export type SystemConfigOAuthDto = { storageQuotaClaim: string; /** Timeout */ timeout: number; - /** Token endpoint auth method */ tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod; }; export type SystemConfigPasswordLoginDto = { @@ -2668,8 +2563,11 @@ export type SystemConfigStorageTemplateDto = { template: string; }; export type SystemConfigTemplateEmailsDto = { + /** Album invite template */ albumInviteTemplate: string; + /** Album update template */ albumUpdateTemplate: string; + /** Welcome template */ welcomeTemplate: string; }; export type SystemConfigTemplatesDto = { @@ -2742,7 +2640,7 @@ export type ReverseGeocodingStateResponseDto = { }; export type TagCreateDto = { /** Tag color (hex) */ - color?: string; + color?: string | null; /** Tag name */ name: string; /** Parent tag ID */ @@ -2815,7 +2713,6 @@ export type TrashResponseDto = { count: number; }; export type UserUpdateMeDto = { - /** Avatar color */ avatarColor?: (UserAvatarColor) | null; /** User email */ email?: string; @@ -2844,9 +2741,12 @@ export type CreateProfileImageResponseDto = { /** User ID */ userId: string; }; +export type PluginConfigValue = any; +export type WorkflowActionConfig = { + [key: string]: PluginConfigValue; +}; export type WorkflowActionResponseDto = { - /** Action configuration */ - actionConfig: object | null; + actionConfig: (WorkflowActionConfig) | null; /** Action ID */ id: string; /** Action order */ @@ -2856,9 +2756,11 @@ export type WorkflowActionResponseDto = { /** Workflow ID */ workflowId: string; }; +export type WorkflowFilterConfig = { + [key: string]: PluginConfigValue; +}; export type WorkflowFilterResponseDto = { - /** Filter configuration */ - filterConfig: object | null; + filterConfig: (WorkflowFilterConfig) | null; /** Filter ID */ id: string; /** Filter order */ @@ -2885,18 +2787,15 @@ export type WorkflowResponseDto = { name: string | null; /** Owner user ID */ ownerId: string; - /** Workflow trigger type */ triggerType: PluginTriggerType; }; export type WorkflowActionItemDto = { - /** Action configuration */ - actionConfig?: object; + actionConfig?: WorkflowActionConfig; /** Plugin action ID */ pluginActionId: string; }; export type WorkflowFilterItemDto = { - /** Filter configuration */ - filterConfig?: object; + filterConfig?: WorkflowFilterConfig; /** Plugin filter ID */ pluginFilterId: string; }; @@ -2911,7 +2810,6 @@ export type WorkflowCreateDto = { filters: WorkflowFilterItemDto[]; /** Workflow name */ name: string; - /** Workflow trigger type */ triggerType: PluginTriggerType; }; export type WorkflowUpdateDto = { @@ -2925,9 +2823,9 @@ export type WorkflowUpdateDto = { filters?: WorkflowFilterItemDto[]; /** Workflow name */ name?: string; - /** Workflow trigger type */ triggerType?: PluginTriggerType; }; +export type LicenseResponseDto = UserLicense; export type SyncAckV1 = {}; export type SyncAlbumDeleteV1 = { /** Album ID */ @@ -2954,7 +2852,6 @@ export type SyncAlbumUserDeleteV1 = { export type SyncAlbumUserV1 = { /** Album ID */ albumId: string; - /** Album user role */ role: AlbumUserRole; /** User ID */ userId: string; @@ -2983,13 +2880,20 @@ export type SyncAssetDeleteV1 = { assetId: string; }; export type SyncAssetEditDeleteV1 = { + /** Edit ID */ editId: string; }; export type SyncAssetEditV1 = { action: AssetEditAction; + /** Asset ID */ assetId: string; + /** Edit ID */ id: string; - parameters: object; + /** Edit parameters */ + parameters: { + [key: string]: any; + }; + /** Edit sequence */ sequence: number; }; export type SyncAssetExifV1 = { @@ -3051,13 +2955,19 @@ export type SyncAssetFaceDeleteV1 = { export type SyncAssetFaceV1 = { /** Asset ID */ assetId: string; + /** Bounding box X1 */ boundingBoxX1: number; + /** Bounding box X2 */ boundingBoxX2: number; + /** Bounding box Y1 */ boundingBoxY1: number; + /** Bounding box Y2 */ boundingBoxY2: number; /** Asset face ID */ id: string; + /** Image height */ imageHeight: number; + /** Image width */ imageWidth: number; /** Person ID */ personId: string | null; @@ -3067,15 +2977,21 @@ export type SyncAssetFaceV1 = { export type SyncAssetFaceV2 = { /** Asset ID */ assetId: string; + /** Bounding box X1 */ boundingBoxX1: number; + /** Bounding box X2 */ boundingBoxX2: number; + /** Bounding box Y1 */ boundingBoxY1: number; + /** Bounding box Y2 */ boundingBoxY2: number; /** Face deleted at */ deletedAt: string | null; /** Asset face ID */ id: string; + /** Image height */ imageHeight: number; + /** Image width */ imageWidth: number; /** Is the face visible in the asset */ isVisible: boolean; @@ -3096,7 +3012,9 @@ export type SyncAssetMetadataV1 = { /** Key */ key: string; /** Value */ - value: object; + value: { + [key: string]: any; + }; }; export type SyncAssetV1 = { /** Checksum */ @@ -3131,16 +3049,13 @@ export type SyncAssetV1 = { stackId: string | null; /** Thumbhash */ thumbhash: string | null; - /** Asset type */ "type": AssetTypeEnum; - /** Asset visibility */ visibility: AssetVisibility; /** Asset width */ width: number | null; }; export type SyncAuthUserV1 = { - /** User avatar color */ - avatarColor: (UserAvatarColor) | null; + avatarColor?: (UserAvatarColor) | null; /** User deleted at */ deletedAt: string | null; /** User email */ @@ -3159,7 +3074,9 @@ export type SyncAuthUserV1 = { pinCode: string | null; /** User profile changed at */ profileChangedAt: string; + /** Quota size in bytes */ quotaSizeInBytes: number | null; + /** Quota usage in bytes */ quotaUsageInBytes: number; /** User storage label */ storageLabel: string | null; @@ -3185,7 +3102,9 @@ export type SyncMemoryV1 = { /** Created at */ createdAt: string; /** Data */ - data: object; + data: { + [key: string]: any; + }; /** Deleted at */ deletedAt: string | null; /** Hide at */ @@ -3202,7 +3121,6 @@ export type SyncMemoryV1 = { seenAt: string | null; /** Show at */ showAt: string | null; - /** Memory type */ "type": MemoryType; /** Updated at */ updatedAt: string; @@ -3269,22 +3187,21 @@ export type SyncUserDeleteV1 = { userId: string; }; export type SyncUserMetadataDeleteV1 = { - /** User metadata key */ key: UserMetadataKey; /** User ID */ userId: string; }; export type SyncUserMetadataV1 = { - /** User metadata key */ key: UserMetadataKey; /** User ID */ userId: string; /** User metadata value */ - value: object; + value: { + [key: string]: any; + }; }; export type SyncUserV1 = { - /** User avatar color */ - avatarColor: (UserAvatarColor) | null; + avatarColor?: (UserAvatarColor) | null; /** User deleted at */ deletedAt: string | null; /** User email */ @@ -3713,18 +3630,13 @@ export function createAlbum({ createAlbumDto }: { /** * Add assets to albums */ -export function addAssetsToAlbums({ key, slug, albumsAddAssetsDto }: { - key?: string; - slug?: string; +export function addAssetsToAlbums({ albumsAddAssetsDto }: { albumsAddAssetsDto: AlbumsAddAssetsDto; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: AlbumsAddAssetsResponseDto; - }>(`/albums/assets${QS.query(QS.explode({ - key, - slug - }))}`, oazapfts.json({ + }>("/albums/assets", oazapfts.json({ ...opts, method: "PUT", body: albumsAddAssetsDto @@ -3755,19 +3667,17 @@ export function deleteAlbum({ id }: { /** * Retrieve an album */ -export function getAlbumInfo({ id, key, slug, withoutAssets }: { +export function getAlbumInfo({ id, key, slug }: { id: string; key?: string; slug?: string; - withoutAssets?: boolean; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: AlbumResponseDto; }>(`/albums/${encodeURIComponent(id)}${QS.query(QS.explode({ key, - slug, - withoutAssets + slug }))}`, { ...opts })); @@ -3807,24 +3717,37 @@ export function removeAssetFromAlbum({ id, bulkIdsDto }: { /** * Add assets to an album */ -export function addAssetsToAlbum({ id, key, slug, bulkIdsDto }: { +export function addAssetsToAlbum({ id, bulkIdsDto }: { id: string; - key?: string; - slug?: string; bulkIdsDto: BulkIdsDto; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: BulkIdResponseDto[]; - }>(`/albums/${encodeURIComponent(id)}/assets${QS.query(QS.explode({ - key, - slug - }))}`, oazapfts.json({ + }>(`/albums/${encodeURIComponent(id)}/assets`, oazapfts.json({ ...opts, method: "PUT", body: bulkIdsDto }))); } +/** + * Retrieve album map markers + */ +export function getAlbumMapMarkers({ id, key, slug }: { + id: string; + key?: string; + slug?: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: MapMarkerResponseDto[]; + }>(`/albums/${encodeURIComponent(id)}/map-markers${QS.query(QS.explode({ + key, + slug + }))}`, { + ...opts + })); +} /** * Remove user from album */ @@ -4022,34 +3945,6 @@ export function copyAsset({ assetCopyDto }: { body: assetCopyDto }))); } -/** - * Retrieve assets by device ID - */ -export function getAllUserAssetsByDeviceId({ deviceId }: { - deviceId: string; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: string[]; - }>(`/assets/device/${encodeURIComponent(deviceId)}`, { - ...opts - })); -} -/** - * Check existing assets - */ -export function checkExistingAssets({ checkExistingAssetsDto }: { - checkExistingAssetsDto: CheckExistingAssetsDto; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: CheckExistingAssetsResponseDto; - }>("/assets/exist", oazapfts.json({ - ...opts, - method: "POST", - body: checkExistingAssetsDto - }))); -} /** * Run an asset job */ @@ -4089,21 +3984,6 @@ export function updateBulkAssetMetadata({ assetMetadataBulkUpsertDto }: { body: assetMetadataBulkUpsertDto }))); } -/** - * Get random assets - */ -export function getRandom({ count }: { - count?: number; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: AssetResponseDto[]; - }>(`/assets/random${QS.query(QS.explode({ - count - }))}`, { - ...opts - })); -} /** * Get asset statistics */ @@ -4285,27 +4165,6 @@ export function downloadAsset({ edited, id, key, slug }: { ...opts })); } -/** - * Replace asset - */ -export function replaceAsset({ id, key, slug, assetMediaReplaceDto }: { - id: string; - key?: string; - slug?: string; - assetMediaReplaceDto: AssetMediaReplaceDto; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: AssetMediaResponseDto; - }>(`/assets/${encodeURIComponent(id)}/original${QS.query(QS.explode({ - key, - slug - }))}`, oazapfts.multipart({ - ...opts, - method: "PUT", - body: assetMediaReplaceDto - }))); -} /** * View asset thumbnail */ @@ -5465,13 +5324,12 @@ export function getExploreData(opts?: Oazapfts.RequestOpts) { /** * Search large assets */ -export function searchLargeAssets({ albumIds, city, country, createdAfter, createdBefore, deviceId, isEncoded, isFavorite, isMotion, isNotInAlbum, isOffline, lensModel, libraryId, make, minFileSize, model, ocr, personIds, rating, size, state, tagIds, takenAfter, takenBefore, trashedAfter, trashedBefore, $type, updatedAfter, updatedBefore, visibility, withDeleted, withExif }: { +export function searchLargeAssets({ albumIds, city, country, createdAfter, createdBefore, isEncoded, isFavorite, isMotion, isNotInAlbum, isOffline, lensModel, libraryId, make, minFileSize, model, ocr, personIds, rating, size, state, tagIds, takenAfter, takenBefore, trashedAfter, trashedBefore, $type, updatedAfter, updatedBefore, visibility, withDeleted, withExif }: { albumIds?: string[]; city?: string | null; country?: string | null; createdAfter?: string; createdBefore?: string; - deviceId?: string; isEncoded?: boolean; isFavorite?: boolean; isMotion?: boolean; @@ -5479,7 +5337,7 @@ export function searchLargeAssets({ albumIds, city, country, createdAfter, creat isOffline?: boolean; lensModel?: string | null; libraryId?: string | null; - make?: string; + make?: string | null; minFileSize?: number; model?: string | null; ocr?: string; @@ -5508,7 +5366,6 @@ export function searchLargeAssets({ albumIds, city, country, createdAfter, creat country, createdAfter, createdBefore, - deviceId, isEncoded, isFavorite, isMotion, @@ -5718,7 +5575,7 @@ export function deleteServerLicense(opts?: Oazapfts.RequestOpts) { export function getServerLicense(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: LicenseResponseDto; + data: UserLicense; } | { status: 404; }>("/server/license", { @@ -5733,7 +5590,7 @@ export function setServerLicense({ licenseKeyDto }: { }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: LicenseResponseDto; + data: UserLicense; }>("/server/license", oazapfts.json({ ...opts, method: "PUT", @@ -5757,7 +5614,7 @@ export function getSupportedMediaTypes(opts?: Oazapfts.RequestOpts) { export function pingServer(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: ServerPingResponseRead; + data: ServerPingResponse; }>("/server/ping", { ...opts })); @@ -5956,20 +5813,16 @@ export function sharedLinkLogin({ key, slug, sharedLinkLoginDto }: { /** * Retrieve current shared link */ -export function getMySharedLink({ key, password, slug, token }: { +export function getMySharedLink({ key, slug }: { key?: string; - password?: string; slug?: string; - token?: string; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: SharedLinkResponseDto; }>(`/shared-links/me${QS.query(QS.explode({ key, - password, - slug, - token + slug }))}`, { ...opts })); @@ -6033,19 +5886,14 @@ export function removeSharedLinkAssets({ id, assetIdsDto }: { /** * Add assets to a shared link */ -export function addSharedLinkAssets({ id, key, slug, assetIdsDto }: { +export function addSharedLinkAssets({ id, assetIdsDto }: { id: string; - key?: string; - slug?: string; assetIdsDto: AssetIdsDto; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: AssetIdsResponseDto[]; - }>(`/shared-links/${encodeURIComponent(id)}/assets${QS.query(QS.explode({ - key, - slug - }))}`, oazapfts.json({ + }>(`/shared-links/${encodeURIComponent(id)}/assets`, oazapfts.json({ ...opts, method: "PUT", body: assetIdsDto @@ -6180,36 +6028,6 @@ export function sendSyncAck({ syncAckSetDto }: { body: syncAckSetDto }))); } -/** - * Get delta sync for user - */ -export function getDeltaSync({ assetDeltaSyncDto }: { - assetDeltaSyncDto: AssetDeltaSyncDto; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: AssetDeltaSyncResponseDto; - }>("/sync/delta-sync", oazapfts.json({ - ...opts, - method: "POST", - body: assetDeltaSyncDto - }))); -} -/** - * Get full sync for user - */ -export function getFullSyncForUser({ assetFullSyncDto }: { - assetFullSyncDto: AssetFullSyncDto; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: AssetResponseDto[]; - }>("/sync/full-sync", oazapfts.json({ - ...opts, - method: "POST", - body: assetFullSyncDto - }))); -} /** * Stream sync changes */ @@ -6618,7 +6436,7 @@ export function deleteUserLicense(opts?: Oazapfts.RequestOpts) { export function getUserLicense(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: LicenseResponseDto; + data: UserLicense; }>("/users/me/license", { ...opts })); @@ -6631,7 +6449,7 @@ export function setUserLicense({ licenseKeyDto }: { }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: LicenseResponseDto; + data: UserLicense; }>("/users/me/license", oazapfts.json({ ...opts, method: "PUT", @@ -6908,17 +6726,6 @@ export enum AlbumUserRole { Editor = "editor", Viewer = "viewer" } -export enum SourceType { - MachineLearning = "machine-learning", - Exif = "exif", - Manual = "manual" -} -export enum AssetTypeEnum { - Image = "IMAGE", - Video = "VIDEO", - Audio = "AUDIO", - Other = "OTHER" -} export enum BulkIdErrorReason { Duplicate = "duplicate", NoPermission = "no_permission", @@ -6926,13 +6733,6 @@ export enum BulkIdErrorReason { Unknown = "unknown", Validation = "validation" } -export enum Error { - Duplicate = "duplicate", - NoPermission = "no_permission", - NotFound = "not_found", - Unknown = "unknown", - Validation = "validation" -} export enum Permission { All = "all", ActivityCreate = "activity.create", @@ -6952,7 +6752,6 @@ export enum Permission { AssetView = "asset.view", AssetDownload = "asset.download", AssetUpload = "asset.upload", - AssetReplace = "asset.replace", AssetCopy = "asset.copy", AssetDerive = "asset.derive", AssetEditGet = "asset.edit.get", @@ -7096,11 +6895,11 @@ export enum AssetMediaStatus { Replaced = "replaced", Duplicate = "duplicate" } -export enum Action { +export enum AssetUploadAction { Accept = "accept", Reject = "reject" } -export enum Reason { +export enum AssetRejectReason { Duplicate = "duplicate", UnsupportedFormat = "unsupported-format" } @@ -7110,6 +6909,17 @@ export enum AssetJobName { RegenerateThumbnail = "regenerate-thumbnail", TranscodeVideo = "transcode-video" } +export enum SourceType { + MachineLearning = "machine-learning", + Exif = "exif", + Manual = "manual" +} +export enum AssetTypeEnum { + Image = "IMAGE", + Video = "VIDEO", + Audio = "AUDIO", + Other = "OTHER" +} export enum AssetEditAction { Crop = "crop", Rotate = "rotate", @@ -7172,6 +6982,15 @@ export enum PartnerDirection { SharedBy = "shared-by", SharedWith = "shared-with" } +export enum PluginJsonSchemaType { + String = "string", + Number = "number", + Integer = "integer", + Boolean = "boolean", + Object = "object", + Array = "array", + Null = "null" +} export enum PluginContextType { Asset = "asset", Album = "album", @@ -7205,7 +7024,6 @@ export enum JobName { AssetFileMigration = "AssetFileMigration", AssetGenerateThumbnailsQueueAll = "AssetGenerateThumbnailsQueueAll", AssetGenerateThumbnails = "AssetGenerateThumbnails", - AuditLogCleanup = "AuditLogCleanup", AuditTableCleanup = "AuditTableCleanup", DatabaseBackup = "DatabaseBackup", FacialRecognitionQueueAll = "FacialRecognitionQueueAll", @@ -7259,7 +7077,7 @@ export enum SharedLinkType { Album = "ALBUM", Individual = "INDIVIDUAL" } -export enum Error2 { +export enum AssetIdErrorReason { Duplicate = "duplicate", NoPermission = "no_permission", NotFound = "not_found" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9b8b49159..077f7a6785 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -433,12 +433,6 @@ importers: chokidar: specifier: ^4.0.3 version: 4.0.3 - class-transformer: - specifier: ^0.5.1 - version: 0.5.1 - class-validator: - specifier: ^0.15.0 - version: 0.15.1 compression: specifier: ^1.8.0 version: 1.8.1 @@ -517,6 +511,9 @@ importers: nestjs-otel: specifier: ^7.0.0 version: 7.0.1(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) + nestjs-zod: + specifier: ^5.3.0 + version: 5.3.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.2.6(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6) nodemailer: specifier: ^8.0.0 version: 8.0.5 @@ -583,6 +580,9 @@ importers: validator: specifier: ^13.12.0 version: 13.15.26 + zod: + specifier: ^4.3.6 + version: 4.3.6 devDependencies: '@eslint/js': specifier: ^10.0.0 @@ -741,8 +741,8 @@ importers: specifier: workspace:* version: link:../open-api/typescript-sdk '@immich/ui': - specifier: ^0.69.0 - version: 0.69.0(@sveltejs/kit@2.57.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.1)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@6.0.2)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1) + specifier: ^0.76.0 + version: 0.76.0(@sveltejs/kit@2.57.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.1)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@6.0.2)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1) '@mapbox/mapbox-gl-rtl-text': specifier: 0.3.0 version: 0.3.0 @@ -3042,13 +3042,13 @@ packages: resolution: {integrity: sha512-UWhy/+Lf8C1dJip5wPfFytI3Vq/9UyDKQE1ROjXwVhT6E/CPgBkRLwHPetjYGPJ4o1JVVpRLnEEJCXdvzqVpGw==} hasBin: true - '@immich/svelte-markdown-preprocess@0.2.1': - resolution: {integrity: sha512-mbr/g75lO8Zh+ELCuYrZP0XB4gf2UbK8rJcGYMYxFJJzMMunV+sm9FqtV1dbwW2dpXzCZGz1XPCEZ6oo526TbA==} + '@immich/svelte-markdown-preprocess@0.4.1': + resolution: {integrity: sha512-/N5dhu3fnRZUoZ+Z9hrIV61o9wi6Uf70TDxqiinXNYlXfqP81p1o77Z5mhbxtNigTNcp6GwpGeHAXRHQrU9JAQ==} peerDependencies: svelte: ^5.0.0 - '@immich/ui@0.69.0': - resolution: {integrity: sha512-YQ+27pGQhzdRBOo/7cHcbXnax5BUrrJeYjUc+VdRYp6KMS8SlGWAKQhvZPdcqiPB332fxJMmpHjV+VqXJJjrqg==} + '@immich/ui@0.76.0': + resolution: {integrity: sha512-ghxfbC47UPMwQJ65maOUYdduQ/G/zo87Oc2ZUKe6o8KgoHsWxLVjQUw44T3dZdFOhvyS8SsIlkGLuagVcrM9Bg==} peerDependencies: svelte: ^5.0.0 @@ -9390,6 +9390,17 @@ packages: '@nestjs/common': '>= 11 < 12' '@nestjs/core': '>= 11 < 12' + nestjs-zod@5.3.0: + resolution: {integrity: sha512-QY6imXm9heMOpWigjFHgMWPvc1ZQHeNQ7pdogo9Q5xj5F8HpqZ972vKlVdkaTyzYlOXJP/yVy3wlF1EjubDQPg==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/swagger': ^7.4.2 || ^8.0.0 || ^11.0.0 + rxjs: ^7.0.0 + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + '@nestjs/swagger': + optional: true + next-tick@1.1.0: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} @@ -12532,8 +12543,8 @@ packages: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} - zod@4.2.1: - resolution: {integrity: sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} zwitch@1.0.5: resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==} @@ -12545,33 +12556,33 @@ snapshots: '@adobe/css-tools@4.4.4': {} - '@ai-sdk/gateway@2.0.21(zod@4.2.1)': + '@ai-sdk/gateway@2.0.21(zod@4.3.6)': dependencies: '@ai-sdk/provider': 2.0.0 - '@ai-sdk/provider-utils': 3.0.19(zod@4.2.1) + '@ai-sdk/provider-utils': 3.0.19(zod@4.3.6) '@vercel/oidc': 3.0.5 - zod: 4.2.1 + zod: 4.3.6 - '@ai-sdk/provider-utils@3.0.19(zod@4.2.1)': + '@ai-sdk/provider-utils@3.0.19(zod@4.3.6)': dependencies: '@ai-sdk/provider': 2.0.0 '@standard-schema/spec': 1.1.0 eventsource-parser: 3.0.6 - zod: 4.2.1 + zod: 4.3.6 '@ai-sdk/provider@2.0.0': dependencies: json-schema: 0.4.0 - '@ai-sdk/react@2.0.115(react@19.2.4)(zod@4.2.1)': + '@ai-sdk/react@2.0.115(react@19.2.4)(zod@4.3.6)': dependencies: - '@ai-sdk/provider-utils': 3.0.19(zod@4.2.1) - ai: 5.0.113(zod@4.2.1) + '@ai-sdk/provider-utils': 3.0.19(zod@4.3.6) + ai: 5.0.113(zod@4.3.6) react: 19.2.4 swr: 2.3.8(react@19.2.4) throttleit: 2.1.0 optionalDependencies: - zod: 4.2.1 + zod: 4.3.6 '@algolia/abtesting@1.12.0': dependencies: @@ -13914,14 +13925,14 @@ snapshots: '@docsearch/react@4.3.2(@algolia/client-search@5.46.0)(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(search-insights@2.17.3)': dependencies: - '@ai-sdk/react': 2.0.115(react@19.2.4)(zod@4.2.1) + '@ai-sdk/react': 2.0.115(react@19.2.4)(zod@4.3.6) '@algolia/autocomplete-core': 1.19.2(@algolia/client-search@5.46.0)(algoliasearch@5.46.0)(search-insights@2.17.3) '@docsearch/core': 4.3.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@docsearch/css': 4.3.2 - ai: 5.0.113(zod@4.2.1) + ai: 5.0.113(zod@4.3.6) algoliasearch: 5.46.0 marked: 16.4.2 - zod: 4.2.1 + zod: 4.3.6 optionalDependencies: '@types/react': 19.2.14 react: 19.2.4 @@ -15225,16 +15236,16 @@ snapshots: pg-connection-string: 2.12.0 postgres: 3.4.8 - '@immich/svelte-markdown-preprocess@0.2.1(svelte@5.55.1)': + '@immich/svelte-markdown-preprocess@0.4.1(svelte@5.55.1)': dependencies: front-matter: 4.0.2 marked: 17.0.5 node-emoji: 2.2.0 svelte: 5.55.1 - '@immich/ui@0.69.0(@sveltejs/kit@2.57.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.1)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@6.0.2)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)': + '@immich/ui@0.76.0(@sveltejs/kit@2.57.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.1)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@6.0.2)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)': dependencies: - '@immich/svelte-markdown-preprocess': 0.2.1(svelte@5.55.1) + '@immich/svelte-markdown-preprocess': 0.4.1(svelte@5.55.1) '@internationalized/date': 3.12.0 '@mdi/js': 7.4.47 bits-ui: 2.16.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.1)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@6.0.2)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1) @@ -17935,13 +17946,13 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 - ai@5.0.113(zod@4.2.1): + ai@5.0.113(zod@4.3.6): dependencies: - '@ai-sdk/gateway': 2.0.21(zod@4.2.1) + '@ai-sdk/gateway': 2.0.21(zod@4.3.6) '@ai-sdk/provider': 2.0.0 - '@ai-sdk/provider-utils': 3.0.19(zod@4.2.1) + '@ai-sdk/provider-utils': 3.0.19(zod@4.3.6) '@opentelemetry/api': 1.9.0 - zod: 4.2.1 + zod: 4.3.6 ajv-formats@2.1.1(ajv@8.18.0): optionalDependencies: @@ -18600,13 +18611,15 @@ snapshots: cjs-module-lexer@2.2.0: {} - class-transformer@0.5.1: {} + class-transformer@0.5.1: + optional: true class-validator@0.15.1: dependencies: '@types/validator': 13.15.10 libphonenumber-js: 1.12.38 validator: 13.15.26 + optional: true clean-css@5.3.3: dependencies: @@ -21483,7 +21496,8 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - libphonenumber-js@1.12.38: {} + libphonenumber-js@1.12.38: + optional: true lightningcss-android-arm64@1.32.0: optional: true @@ -22533,6 +22547,15 @@ snapshots: response-time: 2.3.4 tslib: 2.8.1 + nestjs-zod@5.3.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.2.6(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6): + dependencies: + '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + deepmerge: 4.3.1 + rxjs: 7.8.2 + zod: 4.3.6 + optionalDependencies: + '@nestjs/swagger': 11.2.6(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2) + next-tick@1.1.0: {} no-case@3.0.4: @@ -26252,7 +26275,7 @@ snapshots: compress-commons: 6.0.2 readable-stream: 4.7.0 - zod@4.2.1: {} + zod@4.3.6: {} zwitch@1.0.5: {} diff --git a/server/Dockerfile b/server/Dockerfile index 476d58b983..5951baa96c 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -23,6 +23,9 @@ RUN --mount=type=cache,id=pnpm-server,target=/buildcache/pnpm-store \ FROM builder AS web +ARG BUILD_ID +ENV IMMICH_BUILD=${BUILD_ID} + WORKDIR /usr/src/app COPY ./web ./web/ COPY ./i18n ./i18n/ diff --git a/server/package.json b/server/package.json index bd3f5b0d69..73ea7f6f45 100644 --- a/server/package.json +++ b/server/package.json @@ -70,8 +70,6 @@ "body-parser": "^2.2.0", "bullmq": "^5.51.0", "chokidar": "^4.0.3", - "class-transformer": "^0.5.1", - "class-validator": "^0.15.0", "compression": "^1.8.0", "cookie": "^1.0.2", "cookie-parser": "^1.4.7", @@ -99,6 +97,7 @@ "nestjs-kysely": "3.1.2", "nestjs-otel": "^7.0.0", "nodemailer": "^8.0.0", + "nestjs-zod": "^5.3.0", "openid-client": "^6.3.3", "pg": "^8.11.3", "pg-connection-string": "^2.9.1", @@ -119,7 +118,8 @@ "transformation-matrix": "^3.1.0", "ua-parser-js": "^2.0.0", "uuid": "^11.1.0", - "validator": "^13.12.0" + "validator": "^13.12.0", + "zod": "^4.3.6" }, "devDependencies": { "@eslint/js": "^10.0.0", diff --git a/server/src/app.module.ts b/server/src/app.module.ts index f2b6a7e805..ae930762d0 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -1,10 +1,11 @@ import { BullModule } from '@nestjs/bullmq'; -import { Inject, Module, OnModuleDestroy, OnModuleInit, ValidationPipe } from '@nestjs/common'; +import { Inject, Module, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule'; import { ClsModule } from 'nestjs-cls'; import { KyselyModule } from 'nestjs-kysely'; import { OpenTelemetryModule } from 'nestjs-otel'; +import { ZodSerializerInterceptor, ZodValidationPipe } from 'nestjs-zod'; import { commandsAndQuestions } from 'src/commands'; import { IWorker } from 'src/constants'; import { controllers } from 'src/controllers'; @@ -43,7 +44,8 @@ const common = [...repositories, ...services, GlobalExceptionFilter]; const commonMiddleware = [ { provide: APP_FILTER, useClass: GlobalExceptionFilter }, - { provide: APP_PIPE, useValue: new ValidationPipe({ transform: true, whitelist: true }) }, + { provide: APP_PIPE, useClass: ZodValidationPipe }, + { provide: APP_INTERCEPTOR, useClass: ZodSerializerInterceptor }, { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor }, { provide: APP_INTERCEPTOR, useClass: ErrorInterceptor }, ]; diff --git a/server/src/bin/sync-sql.ts b/server/src/bin/sync-sql.ts index b632332069..5be9ae29b9 100644 --- a/server/src/bin/sync-sql.ts +++ b/server/src/bin/sync-sql.ts @@ -3,7 +3,6 @@ import { INestApplication } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { SchedulerRegistry } from '@nestjs/schedule'; import { Test } from '@nestjs/testing'; -import { ClassConstructor } from 'class-transformer'; import { ClsModule } from 'nestjs-cls'; import { KyselyModule } from 'nestjs-kysely'; import { OpenTelemetryModule } from 'nestjs-otel'; @@ -44,7 +43,7 @@ export class SqlLogger { const reflector = new Reflector(); -type Repository = ClassConstructor; +type Repository = new (...args: any[]) => any; type SqlGeneratorOptions = { targetDir: string }; class SqlGenerator { diff --git a/server/src/config.ts b/server/src/config.ts index e6134df477..b4feeca4dc 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -247,7 +247,7 @@ export const defaults = Object.freeze({ urls: [process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003'], availabilityChecks: { enabled: true, - timeout: Number(process.env.IMMICH_MACHINE_LEARNING_PING_TIMEOUT) || 2000, + timeout: 2000, interval: 30_000, }, clip: { diff --git a/server/src/controllers/activity.controller.spec.ts b/server/src/controllers/activity.controller.spec.ts index bf2038048f..7ac6e051f6 100644 --- a/server/src/controllers/activity.controller.spec.ts +++ b/server/src/controllers/activity.controller.spec.ts @@ -27,13 +27,15 @@ describe(ActivityController.name, () => { it('should require an albumId', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/activities'); expect(status).toEqual(400); - expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['albumId must be a UUID']))); + expect(body).toEqual( + factory.responses.badRequest(['[albumId] Invalid input: expected string, received undefined']), + ); }); it('should reject an invalid albumId', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/activities').query({ albumId: '123' }); expect(status).toEqual(400); - expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['albumId must be a UUID']))); + expect(body).toEqual(factory.responses.badRequest(['[albumId] Invalid UUID'])); }); it('should reject an invalid assetId', async () => { @@ -41,7 +43,7 @@ describe(ActivityController.name, () => { .get('/activities') .query({ albumId: factory.uuid(), assetId: '123' }); expect(status).toEqual(400); - expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['assetId must be a UUID']))); + expect(body).toEqual(factory.responses.badRequest(['[assetId] Invalid UUID'])); }); }); @@ -52,9 +54,11 @@ describe(ActivityController.name, () => { }); it('should require an albumId', async () => { - const { status, body } = await request(ctx.getHttpServer()).post('/activities').send({ albumId: '123' }); + const { status, body } = await request(ctx.getHttpServer()) + .post('/activities') + .send({ albumId: '123', type: 'like' }); expect(status).toEqual(400); - expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['albumId must be a UUID']))); + expect(body).toEqual(factory.responses.badRequest(['[albumId] Invalid UUID'])); }); it('should require a comment when type is comment', async () => { @@ -62,7 +66,7 @@ describe(ActivityController.name, () => { .post('/activities') .send({ albumId: factory.uuid(), type: 'comment', comment: null }); expect(status).toEqual(400); - expect(body).toEqual(factory.responses.badRequest(['comment must be a string', 'comment should not be empty'])); + expect(body).toEqual(factory.responses.badRequest(['[comment] Invalid input: expected string, received null'])); }); }); @@ -75,7 +79,7 @@ describe(ActivityController.name, () => { it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).delete(`/activities/123`); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); + expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID'])); }); }); }); diff --git a/server/src/controllers/album.controller.spec.ts b/server/src/controllers/album.controller.spec.ts index d13227555b..fadc5103eb 100644 --- a/server/src/controllers/album.controller.spec.ts +++ b/server/src/controllers/album.controller.spec.ts @@ -27,13 +27,13 @@ describe(AlbumController.name, () => { it('should reject an invalid shared param', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/albums?shared=invalid'); expect(status).toEqual(400); - expect(body).toEqual(factory.responses.badRequest(['shared must be a boolean value'])); + expect(body).toEqual(factory.responses.badRequest(['[shared] Invalid option: expected one of "true"|"false"'])); }); it('should reject an invalid assetId param', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/albums?assetId=invalid'); expect(status).toEqual(400); - expect(body).toEqual(factory.responses.badRequest(['assetId must be a UUID'])); + expect(body).toEqual(factory.responses.badRequest(['[assetId] Invalid UUID'])); }); }); diff --git a/server/src/controllers/album.controller.ts b/server/src/controllers/album.controller.ts index dad70257a7..90a8fa5a25 100644 --- a/server/src/controllers/album.controller.ts +++ b/server/src/controllers/album.controller.ts @@ -3,7 +3,6 @@ import { ApiTags } from '@nestjs/swagger'; import { Endpoint, HistoryBuilder } from 'src/decorators'; import { AddUsersDto, - AlbumInfoDto, AlbumResponseDto, AlbumsAddAssetsDto, AlbumsAddAssetsResponseDto, @@ -15,6 +14,7 @@ import { } from 'src/dtos/album.dto'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; +import { MapMarkerResponseDto } from 'src/dtos/map.dto'; import { ApiTag, Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { AlbumService } from 'src/services/album.service'; @@ -65,12 +65,8 @@ export class AlbumController { description: 'Retrieve information about a specific album by its ID.', history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), }) - getAlbumInfo( - @Auth() auth: AuthDto, - @Param() { id }: UUIDParamDto, - @Query() dto: AlbumInfoDto, - ): Promise { - return this.service.get(auth, id, dto); + getAlbumInfo(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.get(auth, id); } @Patch(':id') @@ -102,8 +98,19 @@ export class AlbumController { return this.service.delete(auth, id); } + @Authenticated({ permission: Permission.AlbumRead, sharedLink: true }) + @Get(':id/map-markers') + @Endpoint({ + summary: 'Retrieve album map markers', + description: 'Retrieve map marker information for a specific album by its ID.', + history: new HistoryBuilder().added('v3'), + }) + getAlbumMapMarkers(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.getMapMarkers(auth, id); + } + @Put(':id/assets') - @Authenticated({ permission: Permission.AlbumAssetCreate, sharedLink: true }) + @Authenticated({ permission: Permission.AlbumAssetCreate }) @Endpoint({ summary: 'Add assets to an album', description: 'Add multiple assets to a specific album by its ID.', @@ -118,7 +125,7 @@ export class AlbumController { } @Put('assets') - @Authenticated({ permission: Permission.AlbumAssetCreate, sharedLink: true }) + @Authenticated({ permission: Permission.AlbumAssetCreate }) @Endpoint({ summary: 'Add assets to albums', description: 'Send a list of asset IDs and album IDs to add each asset to each album.', diff --git a/server/src/controllers/api-key.controller.spec.ts b/server/src/controllers/api-key.controller.spec.ts index c6dab09a3c..23a1f8b98c 100644 --- a/server/src/controllers/api-key.controller.spec.ts +++ b/server/src/controllers/api-key.controller.spec.ts @@ -49,7 +49,7 @@ describe(ApiKeyController.name, () => { it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).get(`/api-keys/123`); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); + expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID'])); }); }); @@ -64,7 +64,7 @@ describe(ApiKeyController.name, () => { .put(`/api-keys/123`) .send({ name: 'new name', permissions: [Permission.All] }); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); + expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID'])); }); it('should allow updating just the name', async () => { @@ -84,7 +84,7 @@ describe(ApiKeyController.name, () => { it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).delete(`/api-keys/123`); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); + expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID'])); }); }); }); diff --git a/server/src/controllers/api-key.controller.ts b/server/src/controllers/api-key.controller.ts index 61ad203331..9238a8b57c 100644 --- a/server/src/controllers/api-key.controller.ts +++ b/server/src/controllers/api-key.controller.ts @@ -1,7 +1,7 @@ import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Endpoint, HistoryBuilder } from 'src/decorators'; -import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto, APIKeyUpdateDto } from 'src/dtos/api-key.dto'; +import { ApiKeyCreateDto, ApiKeyCreateResponseDto, ApiKeyResponseDto, ApiKeyUpdateDto } from 'src/dtos/api-key.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { ApiTag, Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; @@ -20,7 +20,7 @@ export class ApiKeyController { description: 'Creates a new API key. It will be limited to the permissions specified.', history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), }) - createApiKey(@Auth() auth: AuthDto, @Body() dto: APIKeyCreateDto): Promise { + createApiKey(@Auth() auth: AuthDto, @Body() dto: ApiKeyCreateDto): Promise { return this.service.create(auth, dto); } @@ -31,7 +31,7 @@ export class ApiKeyController { description: 'Retrieve all API keys of the current user.', history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), }) - getApiKeys(@Auth() auth: AuthDto): Promise { + getApiKeys(@Auth() auth: AuthDto): Promise { return this.service.getAll(auth); } @@ -42,7 +42,7 @@ export class ApiKeyController { description: 'Retrieve the API key that is used to access this endpoint.', history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), }) - async getMyApiKey(@Auth() auth: AuthDto): Promise { + async getMyApiKey(@Auth() auth: AuthDto): Promise { return this.service.getMine(auth); } @@ -53,7 +53,7 @@ export class ApiKeyController { description: 'Retrieve an API key by its ID. The current user must own this API key.', history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), }) - getApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + getApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.getById(auth, id); } @@ -67,8 +67,8 @@ export class ApiKeyController { updateApiKey( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, - @Body() dto: APIKeyUpdateDto, - ): Promise { + @Body() dto: ApiKeyUpdateDto, + ): Promise { return this.service.update(auth, id, dto); } diff --git a/server/src/controllers/asset-media.controller.spec.ts b/server/src/controllers/asset-media.controller.spec.ts index c2f6aeacef..7ccd0d644d 100644 --- a/server/src/controllers/asset-media.controller.spec.ts +++ b/server/src/controllers/asset-media.controller.spec.ts @@ -9,8 +9,6 @@ import { automock, ControllerContext, controllerSetup, mockBaseService } from 't const makeUploadDto = (options?: { omit: string }): Record => { const dto: Record = { - deviceAssetId: 'example-image', - deviceId: 'TEST', fileCreatedAt: new Date().toISOString(), fileModifiedAt: new Date().toISOString(), isFavorite: 'false', @@ -82,29 +80,11 @@ describe(AssetMediaController.name, () => { }); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['metadata must be valid JSON'])); - }); - - it('should require `deviceAssetId`', async () => { - const { status, body } = await request(ctx.getHttpServer()) - .post('/assets') - .attach('assetData', assetData, filename) - .field({ ...makeUploadDto({ omit: 'deviceAssetId' }) }); - expect(status).toBe(400); expect(body).toEqual( - factory.responses.badRequest(['deviceAssetId must be a string', 'deviceAssetId should not be empty']), + factory.responses.badRequest(['[metadata] Invalid input: expected JSON string, received string']), ); }); - it('should require `deviceId`', async () => { - const { status, body } = await request(ctx.getHttpServer()) - .post('/assets') - .attach('assetData', assetData, filename) - .field({ ...makeUploadDto({ omit: 'deviceId' }) }); - expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['deviceId must be a string', 'deviceId should not be empty'])); - }); - it('should require `fileCreatedAt`', async () => { const { status, body } = await request(ctx.getHttpServer()) .post('/assets') @@ -112,7 +92,9 @@ describe(AssetMediaController.name, () => { .field({ ...makeUploadDto({ omit: 'fileCreatedAt' }) }); expect(status).toBe(400); expect(body).toEqual( - factory.responses.badRequest(['fileCreatedAt must be a Date instance', 'fileCreatedAt should not be empty']), + factory.responses.badRequest([ + '[fileCreatedAt] Invalid input: expected ISO 8601 datetime string, received undefined', + ]), ); }); @@ -123,7 +105,9 @@ describe(AssetMediaController.name, () => { .field(makeUploadDto({ omit: 'fileModifiedAt' })); expect(status).toBe(400); expect(body).toEqual( - factory.responses.badRequest(['fileModifiedAt must be a Date instance', 'fileModifiedAt should not be empty']), + factory.responses.badRequest([ + '[fileModifiedAt] Invalid input: expected ISO 8601 datetime string, received undefined', + ]), ); }); @@ -133,7 +117,9 @@ describe(AssetMediaController.name, () => { .attach('assetData', assetData, filename) .field({ ...makeUploadDto(), isFavorite: 'not-a-boolean' }); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['isFavorite must be a boolean value'])); + expect(body).toEqual( + factory.responses.badRequest(['[isFavorite] Invalid option: expected one of "true"|"false"']), + ); }); it('should throw if `visibility` is not an enum', async () => { @@ -143,7 +129,7 @@ describe(AssetMediaController.name, () => { .field({ ...makeUploadDto(), visibility: 'not-an-option' }); expect(status).toBe(400); expect(body).toEqual( - factory.responses.badRequest([expect.stringContaining('visibility must be one of the following values:')]), + factory.responses.badRequest([expect.stringContaining('[visibility] Invalid option: expected one of')]), ); }); @@ -156,11 +142,16 @@ describe(AssetMediaController.name, () => { }); // TODO figure out how to deal with `sendFile` - describe.skip('GET /assets/:id/thumbnail', () => { - it('should be an authenticated route', async () => { + describe('GET /assets/:id/thumbnail', () => { + it.skip('should be an authenticated route', async () => { await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/thumbnail`); expect(ctx.authenticate).toHaveBeenCalled(); }); + + it('should redirect if size=original is requested', async () => { + const { status } = await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/thumbnail?size=original`); + expect(status).toBe(302); + }); }); }); }); diff --git a/server/src/controllers/asset-media.controller.ts b/server/src/controllers/asset-media.controller.ts index ec6083cfa8..240e5b8b3c 100644 --- a/server/src/controllers/asset-media.controller.ts +++ b/server/src/controllers/asset-media.controller.ts @@ -8,7 +8,6 @@ import { Param, ParseFilePipe, Post, - Put, Query, Req, Res, @@ -22,16 +21,12 @@ import { AssetBulkUploadCheckResponseDto, AssetMediaResponseDto, AssetMediaStatus, - CheckExistingAssetsResponseDto, } from 'src/dtos/asset-media-response.dto'; import { AssetBulkUploadCheckDto, AssetMediaCreateDto, AssetMediaOptionsDto, - AssetMediaReplaceDto, AssetMediaSize, - CheckExistingAssetsDto, - UploadFieldName, } from 'src/dtos/asset-media.dto'; import { AssetDownloadOriginalDto } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; @@ -112,36 +107,6 @@ export class AssetMediaController { await sendFile(res, next, () => this.service.downloadOriginal(auth, id, dto), this.logger); } - @Put(':id/original') - @UseInterceptors(FileUploadInterceptor) - @ApiConsumes('multipart/form-data') - @ApiResponse({ - status: 200, - description: 'Asset replaced successfully', - type: AssetMediaResponseDto, - }) - @Endpoint({ - summary: 'Replace asset', - description: 'Replace the asset with new file, without changing its id.', - history: new HistoryBuilder().added('v1').deprecated('v1', { replacementId: 'copyAsset' }), - }) - @Authenticated({ permission: Permission.AssetReplace, sharedLink: true }) - async replaceAsset( - @Auth() auth: AuthDto, - @Param() { id }: UUIDParamDto, - @UploadedFiles(new ParseFilePipe({ validators: [new FileNotEmptyValidator([UploadFieldName.ASSET_DATA])] })) - files: UploadFiles, - @Body() dto: AssetMediaReplaceDto, - @Res({ passthrough: true }) res: Response, - ): Promise { - const { file } = getFiles(files); - const responseDto = await this.service.replaceAsset(auth, id, dto, file); - if (responseDto.status === AssetMediaStatus.DUPLICATE) { - res.status(HttpStatus.OK); - } - return responseDto; - } - @Get(':id/thumbnail') @FileResponse() @Authenticated({ permission: Permission.AssetView, sharedLink: true }) @@ -159,6 +124,16 @@ export class AssetMediaController { @Res() res: Response, @Next() next: NextFunction, ) { + if (dto.size === AssetMediaSize.Original) { + this.logger.deprecate( + 'Calling the thumbnail endpoint with size=original is deprecated. Use the :id/original endpoint instead', + ); + const [_, reqSearch] = req.url.split('?'); + const redirSearchParams = new URLSearchParams(reqSearch); + redirSearchParams.delete('size'); + return res.redirect('original' + '?' + redirSearchParams.toString()); + } + const viewThumbnailRes = await this.service.viewThumbnail(auth, id, dto); if (viewThumbnailRes instanceof ImmichFileResponse) { @@ -202,21 +177,6 @@ export class AssetMediaController { await sendFile(res, next, () => this.service.playbackVideo(auth, id), this.logger); } - @Post('exist') - @Authenticated({ permission: Permission.AssetUpload }) - @Endpoint({ - summary: 'Check existing assets', - description: 'Checks if multiple assets exist on the server and returns all existing - used by background backup', - history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), - }) - @HttpCode(HttpStatus.OK) - checkExistingAssets( - @Auth() auth: AuthDto, - @Body() dto: CheckExistingAssetsDto, - ): Promise { - return this.service.checkExistingAssets(auth, dto); - } - @Post('bulk-upload-check') @Authenticated({ permission: Permission.AssetUpload }) @Endpoint({ diff --git a/server/src/controllers/asset.controller.spec.ts b/server/src/controllers/asset.controller.spec.ts index 69bf1f6443..3c01e3d0a9 100644 --- a/server/src/controllers/asset.controller.spec.ts +++ b/server/src/controllers/asset.controller.spec.ts @@ -31,7 +31,7 @@ describe(AssetController.name, () => { .send({ ids: ['123'] }); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['each value in ids must be a UUID'])); + expect(body).toEqual(factory.responses.badRequest(['[ids.0] Invalid UUID'])); }); it('should require duplicateId to be a string', async () => { @@ -41,7 +41,9 @@ describe(AssetController.name, () => { .send({ ids: [id], duplicateId: true }); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['duplicateId must be a string'])); + expect(body).toEqual( + factory.responses.badRequest(['[duplicateId] Invalid input: expected string, received boolean']), + ); }); it('should accept a null duplicateId', async () => { @@ -68,7 +70,7 @@ describe(AssetController.name, () => { .send({ ids: ['123'] }); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['each value in ids must be a UUID'])); + expect(body).toEqual(factory.responses.badRequest(['[ids.0] Invalid UUID'])); }); }); @@ -81,7 +83,7 @@ describe(AssetController.name, () => { it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).get(`/assets/123`); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); + expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID'])); }); }); @@ -95,7 +97,12 @@ describe(AssetController.name, () => { const { status, body } = await request(ctx.getHttpServer()).put('/assets/copy').send({}); expect(status).toBe(400); expect(body).toEqual( - factory.responses.badRequest(expect.arrayContaining(['sourceId must be a UUID', 'targetId must be a UUID'])), + factory.responses.badRequest( + expect.arrayContaining([ + '[sourceId] Invalid input: expected string, received undefined', + '[targetId] Invalid input: expected string, received undefined', + ]), + ), ); }); @@ -118,7 +125,7 @@ describe(AssetController.name, () => { .put('/assets/metadata') .send({ items: [{ assetId: '123', key: 'test', value: {} }] }); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['items.0.assetId must be a UUID']))); + expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['[items.0.assetId] Invalid UUID']))); }); it('should require a key', async () => { @@ -128,7 +135,7 @@ describe(AssetController.name, () => { expect(status).toBe(400); expect(body).toEqual( factory.responses.badRequest( - expect.arrayContaining(['items.0.key must be a string', 'items.0.key should not be empty']), + expect.arrayContaining(['[items.0.key] Invalid input: expected string, received undefined']), ), ); }); @@ -152,7 +159,7 @@ describe(AssetController.name, () => { .delete('/assets/metadata') .send({ items: [{ assetId: '123', key: 'test' }] }); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['items.0.assetId must be a UUID']))); + expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['[items.0.assetId] Invalid UUID']))); }); it('should require a key', async () => { @@ -162,7 +169,7 @@ describe(AssetController.name, () => { expect(status).toBe(400); expect(body).toEqual( factory.responses.badRequest( - expect.arrayContaining(['items.0.key must be a string', 'items.0.key should not be empty']), + expect.arrayContaining(['[items.0.key] Invalid input: expected string, received undefined']), ), ); }); @@ -184,7 +191,7 @@ describe(AssetController.name, () => { it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).put(`/assets/123`); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); + expect(body).toEqual(factory.responses.badRequest(['Invalid input: expected object, received undefined'])); }); it('should reject invalid gps coordinates', async () => { @@ -238,21 +245,6 @@ describe(AssetController.name, () => { }); }); - describe('GET /assets/random', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/assets/random`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - - it('should not allow count to be a string', async () => { - const { status, body } = await request(ctx.getHttpServer()).get('/assets/random?count=ABC'); - expect(status).toBe(400); - expect(body).toEqual( - factory.responses.badRequest(['count must be a positive number', 'count must be an integer number']), - ); - }); - }); - describe('GET /assets/:id/metadata', () => { it('should be an authenticated route', async () => { await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/metadata`); @@ -269,13 +261,13 @@ describe(AssetController.name, () => { it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).put(`/assets/123/metadata`).send({ items: [] }); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['id must be a UUID']))); + expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['[id] Invalid UUID']))); }); it('should require items to be an array', async () => { const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}/metadata`).send({}); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['items must be an array'])); + expect(body).toEqual(factory.responses.badRequest(['[items] Invalid input: expected array, received undefined'])); }); it('should require each item to have a valid key', async () => { @@ -284,7 +276,7 @@ describe(AssetController.name, () => { .send({ items: [{ value: { some: 'value' } }] }); expect(status).toBe(400); expect(body).toEqual( - factory.responses.badRequest(['items.0.key must be a string', 'items.0.key should not be empty']), + factory.responses.badRequest(['[items.0.key] Invalid input: expected string, received undefined']), ); }); @@ -294,7 +286,9 @@ describe(AssetController.name, () => { .send({ items: [{ key: 'mobile-app', value: null }] }); expect(status).toBe(400); expect(body).toEqual( - factory.responses.badRequest(expect.arrayContaining([expect.stringContaining('value must be an object')])), + factory.responses.badRequest( + expect.arrayContaining(['[items.0.value] Invalid input: expected record, received null']), + ), ); }); @@ -332,7 +326,7 @@ describe(AssetController.name, () => { it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).get(`/assets/123/metadata/mobile-app`); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['id must be a UUID']))); + expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['[id] Invalid UUID']))); }); }); @@ -382,7 +376,7 @@ describe(AssetController.name, () => { }); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['id must be a UUID']))); + expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['[id] Invalid UUID']))); }); it('should check the action and parameters discriminator', async () => { @@ -405,7 +399,11 @@ describe(AssetController.name, () => { expect(status).toBe(400); expect(body).toEqual( factory.responses.badRequest( - expect.arrayContaining([expect.stringContaining('parameters.angle must be one of the following values')]), + expect.arrayContaining([ + expect.stringContaining( + "[edits.0.parameters] Invalid parameters for action 'rotate', expecting keys: angle", + ), + ]), ), ); }); @@ -415,7 +413,7 @@ describe(AssetController.name, () => { .put(`/assets/${factory.uuid()}/edits`) .send({ edits: [] }); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['edits must contain at least 1 elements'])); + expect(body).toEqual(factory.responses.badRequest(['[edits] Too small: expected array to have >=1 items'])); }); }); @@ -428,7 +426,7 @@ describe(AssetController.name, () => { it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).delete(`/assets/123/metadata/mobile-app`); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); + expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID'])); }); }); }); diff --git a/server/src/controllers/asset.controller.ts b/server/src/controllers/asset.controller.ts index 2024760975..1c9afebc1b 100644 --- a/server/src/controllers/asset.controller.ts +++ b/server/src/controllers/asset.controller.ts @@ -15,8 +15,6 @@ import { AssetMetadataUpsertDto, AssetStatsDto, AssetStatsResponseDto, - DeviceIdDto, - RandomAssetsDto, UpdateAssetDto, } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; @@ -32,28 +30,6 @@ import { UUIDParamDto } from 'src/validation'; export class AssetController { constructor(private service: AssetService) {} - @Get('random') - @Authenticated({ permission: Permission.AssetRead }) - @Endpoint({ - summary: 'Get random assets', - description: 'Retrieve a specified number of random assets for the authenticated user.', - history: new HistoryBuilder().added('v1').deprecated('v1', { replacementId: 'searchAssets' }), - }) - getRandom(@Auth() auth: AuthDto, @Query() dto: RandomAssetsDto): Promise { - return this.service.getRandom(auth, dto.count ?? 1); - } - - @Get('/device/:deviceId') - @Endpoint({ - summary: 'Retrieve assets by device ID', - description: 'Get all asset of a device that are in the database, ID only.', - history: new HistoryBuilder().added('v1').deprecated('v2'), - }) - @Authenticated() - getAllUserAssetsByDeviceId(@Auth() auth: AuthDto, @Param() { deviceId }: DeviceIdDto) { - return this.service.getUserAssetsByDeviceId(auth, deviceId); - } - @Get('statistics') @Authenticated({ permission: Permission.AssetStatistics }) @Endpoint({ diff --git a/server/src/controllers/auth.controller.spec.ts b/server/src/controllers/auth.controller.spec.ts index 7dd145ff5c..a61397e75c 100644 --- a/server/src/controllers/auth.controller.spec.ts +++ b/server/src/controllers/auth.controller.spec.ts @@ -74,10 +74,8 @@ describe(AuthController.name, () => { expect(status).toBe(400); expect(body).toEqual( errorDto.badRequest([ - 'email should not be empty', - 'email must be an email', - 'password should not be empty', - 'password must be a string', + '[email] Invalid input: expected email, received undefined', + '[password] Invalid input: expected string, received undefined', ]), ); }); @@ -87,7 +85,7 @@ describe(AuthController.name, () => { .post('/auth/login') .send({ name: 'admin', email: null, password: 'password' }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['email should not be empty', 'email must be an email'])); + expect(body).toEqual(errorDto.badRequest(['[email] Invalid input: expected email, received object'])); }); it(`should not allow null password`, async () => { @@ -95,7 +93,7 @@ describe(AuthController.name, () => { .post('/auth/login') .send({ name: 'admin', email: 'admin@immich.cloud', password: null }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['password should not be empty', 'password must be a string'])); + expect(body).toEqual(errorDto.badRequest(['[password] Invalid input: expected string, received null'])); }); it('should reject an invalid email', async () => { @@ -106,7 +104,7 @@ describe(AuthController.name, () => { .send({ name: 'admin', email: [], password: 'password' }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['email must be an email'])); + expect(body).toEqual(errorDto.badRequest(['[email] Invalid input: expected email, received object'])); }); it('should transform the email to all lowercase', async () => { @@ -197,19 +195,19 @@ describe(AuthController.name, () => { it('should reject 5 digits', async () => { const { status, body } = await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: '12345' }); expect(status).toEqual(400); - expect(body).toEqual(errorDto.badRequest(['pinCode must be a 6-digit numeric string'])); + expect(body).toEqual(errorDto.badRequest([String.raw`[pinCode] Invalid string: must match pattern /^\d{6}$/`])); }); it('should reject 7 digits', async () => { const { status, body } = await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: '1234567' }); expect(status).toEqual(400); - expect(body).toEqual(errorDto.badRequest(['pinCode must be a 6-digit numeric string'])); + expect(body).toEqual(errorDto.badRequest([String.raw`[pinCode] Invalid string: must match pattern /^\d{6}$/`])); }); it('should reject non-numbers', async () => { const { status, body } = await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: 'A12345' }); expect(status).toEqual(400); - expect(body).toEqual(errorDto.badRequest(['pinCode must be a 6-digit numeric string'])); + expect(body).toEqual(errorDto.badRequest([String.raw`[pinCode] Invalid string: must match pattern /^\d{6}$/`])); }); }); diff --git a/server/src/controllers/duplicate.controller.spec.ts b/server/src/controllers/duplicate.controller.spec.ts index 66598b9920..3e11b628e3 100644 --- a/server/src/controllers/duplicate.controller.spec.ts +++ b/server/src/controllers/duplicate.controller.spec.ts @@ -41,7 +41,7 @@ describe(DuplicateController.name, () => { it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).delete(`/duplicates/123`); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); + expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID'])); }); }); }); diff --git a/server/src/controllers/maintenance.controller.spec.ts b/server/src/controllers/maintenance.controller.spec.ts index 094028687e..07c0149463 100644 --- a/server/src/controllers/maintenance.controller.spec.ts +++ b/server/src/controllers/maintenance.controller.spec.ts @@ -31,7 +31,7 @@ describe(MaintenanceController.name, () => { }); expect(status).toBe(400); expect(body).toEqual( - errorDto.badRequest(['restoreBackupFilename must be a string', 'restoreBackupFilename should not be empty']), + errorDto.badRequest(['[restoreBackupFilename] Backup filename is required when action is restore_database']), ); expect(ctx.authenticate).toHaveBeenCalled(); }); diff --git a/server/src/controllers/memory.controller.spec.ts b/server/src/controllers/memory.controller.spec.ts index 820819ee6e..4ed32ee271 100644 --- a/server/src/controllers/memory.controller.spec.ts +++ b/server/src/controllers/memory.controller.spec.ts @@ -47,9 +47,7 @@ describe(MemoryController.name, () => { }); expect(status).toBe(400); - expect(body).toEqual( - errorDto.badRequest(['data.year must be a positive number', 'data.year must be an integer number']), - ); + expect(body).toEqual(errorDto.badRequest(['[data.year] Invalid input: expected number, received undefined'])); }); it('should accept showAt and hideAt', async () => { @@ -83,7 +81,7 @@ describe(MemoryController.name, () => { it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).get(`/memories/invalid`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID'])); }); }); @@ -96,7 +94,7 @@ describe(MemoryController.name, () => { it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).put(`/memories/invalid`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + expect(body).toEqual(errorDto.badRequest(['Invalid input: expected object, received undefined'])); }); }); @@ -116,7 +114,7 @@ describe(MemoryController.name, () => { it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).put(`/memories/invalid/assets`).send({ ids: [] }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID'])); }); it('should require a valid asset id', async () => { @@ -124,7 +122,7 @@ describe(MemoryController.name, () => { .put(`/memories/${factory.uuid()}/assets`) .send({ ids: ['invalid'] }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID'])); + expect(body).toEqual(errorDto.badRequest(['[ids.0] Invalid UUID'])); }); }); @@ -137,7 +135,7 @@ describe(MemoryController.name, () => { it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).delete(`/memories/invalid/assets`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID'])); }); it('should require a valid asset id', async () => { @@ -145,7 +143,7 @@ describe(MemoryController.name, () => { .delete(`/memories/${factory.uuid()}/assets`) .send({ ids: ['invalid'] }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID'])); + expect(body).toEqual(errorDto.badRequest(['[ids.0] Invalid UUID'])); }); }); }); diff --git a/server/src/controllers/notification.controller.spec.ts b/server/src/controllers/notification.controller.spec.ts index a64aee2912..e9886ebb07 100644 --- a/server/src/controllers/notification.controller.spec.ts +++ b/server/src/controllers/notification.controller.spec.ts @@ -31,7 +31,7 @@ describe(NotificationController.name, () => { .query({ level: 'invalid' }) .set('Authorization', `Bearer token`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest([expect.stringContaining('level must be one of the following values')])); + expect(body).toEqual(errorDto.badRequest([expect.stringContaining('[level] Invalid option: expected one of')])); }); }); @@ -45,7 +45,7 @@ describe(NotificationController.name, () => { it('should require a list', async () => { const { status, body } = await request(ctx.getHttpServer()).put(`/notifications`).send({ ids: true }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['ids must be an array']))); + expect(body).toEqual(errorDto.badRequest(['[ids] Invalid input: expected array, received boolean'])); }); it('should require uuids', async () => { @@ -53,7 +53,7 @@ describe(NotificationController.name, () => { .put(`/notifications`) .send({ ids: [true] }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID'])); + expect(body).toEqual(errorDto.badRequest(['[ids.0] Invalid input: expected string, received boolean'])); }); it('should accept valid uuids', async () => { @@ -75,7 +75,7 @@ describe(NotificationController.name, () => { it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).get(`/notifications/123`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest([expect.stringContaining('id must be a UUID')])); + expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID'])); }); }); diff --git a/server/src/controllers/partner.controller.spec.ts b/server/src/controllers/partner.controller.spec.ts index 2c507a634f..0661e9121b 100644 --- a/server/src/controllers/partner.controller.spec.ts +++ b/server/src/controllers/partner.controller.spec.ts @@ -33,10 +33,7 @@ describe(PartnerController.name, () => { const { status, body } = await request(ctx.getHttpServer()).get(`/partners`).set('Authorization', `Bearer token`); expect(status).toBe(400); expect(body).toEqual( - errorDto.badRequest([ - 'direction should not be empty', - expect.stringContaining('direction must be one of the following values:'), - ]), + errorDto.badRequest([expect.stringContaining('[direction] Invalid option: expected one of')]), ); }); @@ -47,7 +44,7 @@ describe(PartnerController.name, () => { .set('Authorization', `Bearer token`); expect(status).toBe(400); expect(body).toEqual( - errorDto.badRequest([expect.stringContaining('direction must be one of the following values:')]), + errorDto.badRequest([expect.stringContaining('[direction] Invalid option: expected one of')]), ); }); }); @@ -64,7 +61,7 @@ describe(PartnerController.name, () => { .send({ sharedWithId: 'invalid' }) .set('Authorization', `Bearer token`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')])); + expect(body).toEqual(errorDto.badRequest(['[sharedWithId] Invalid UUID'])); }); }); @@ -80,7 +77,7 @@ describe(PartnerController.name, () => { .send({ inTimeline: true }) .set('Authorization', `Bearer token`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')])); + expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID'])); }); }); @@ -95,7 +92,7 @@ describe(PartnerController.name, () => { .delete(`/partners/invalid`) .set('Authorization', `Bearer token`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')])); + expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID'])); }); }); }); diff --git a/server/src/controllers/person.controller.spec.ts b/server/src/controllers/person.controller.spec.ts index a28ac9b659..c6c0a1c91f 100644 --- a/server/src/controllers/person.controller.spec.ts +++ b/server/src/controllers/person.controller.spec.ts @@ -35,7 +35,7 @@ describe(PersonController.name, () => { .query({ closestPersonId: 'invalid' }) .set('Authorization', `Bearer token`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')])); + expect(body).toEqual(errorDto.badRequest(['[closestPersonId] Invalid UUID'])); }); it(`should require closestAssetId to be a uuid`, async () => { @@ -44,7 +44,7 @@ describe(PersonController.name, () => { .query({ closestAssetId: 'invalid' }) .set('Authorization', `Bearer token`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')])); + expect(body).toEqual(errorDto.badRequest(['[closestAssetId] Invalid UUID'])); }); }); @@ -76,7 +76,7 @@ describe(PersonController.name, () => { .delete('/people') .send({ ids: ['invalid'] }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')])); + expect(body).toEqual(errorDto.badRequest(['[ids.0] Invalid UUID'])); }); it('should respond with 204', async () => { @@ -104,7 +104,7 @@ describe(PersonController.name, () => { it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).put(`/people/123`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest([expect.stringContaining('id must be a UUID')])); + expect(body).toEqual(errorDto.badRequest(['Invalid input: expected object, received undefined'])); }); it(`should not allow a null name`, async () => { @@ -113,7 +113,7 @@ describe(PersonController.name, () => { .send({ name: null }) .set('Authorization', `Bearer token`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['name must be a string'])); + expect(body).toEqual(errorDto.badRequest(['[name] Invalid input: expected string, received null'])); }); it(`should require featureFaceAssetId to be a uuid`, async () => { @@ -122,7 +122,7 @@ describe(PersonController.name, () => { .send({ featureFaceAssetId: 'invalid' }) .set('Authorization', `Bearer token`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['featureFaceAssetId must be a UUID'])); + expect(body).toEqual(errorDto.badRequest(['[featureFaceAssetId] Invalid UUID'])); }); it(`should require isFavorite to be a boolean`, async () => { @@ -131,7 +131,7 @@ describe(PersonController.name, () => { .send({ isFavorite: 'invalid' }) .set('Authorization', `Bearer token`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['isFavorite must be a boolean value'])); + expect(body).toEqual(errorDto.badRequest(['[isFavorite] Invalid input: expected boolean, received string'])); }); it(`should require isHidden to be a boolean`, async () => { @@ -140,7 +140,7 @@ describe(PersonController.name, () => { .send({ isHidden: 'invalid' }) .set('Authorization', `Bearer token`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['isHidden must be a boolean value'])); + expect(body).toEqual(errorDto.badRequest(['[isHidden] Invalid input: expected boolean, received string'])); }); it('should map an empty birthDate to null', async () => { @@ -154,12 +154,7 @@ describe(PersonController.name, () => { .put(`/people/${factory.uuid()}`) .send({ birthDate: false }); expect(status).toBe(400); - expect(body).toEqual( - errorDto.badRequest([ - 'birthDate must be a string in the format yyyy-MM-dd', - 'Birth date cannot be in the future', - ]), - ); + expect(body).toEqual(errorDto.badRequest(['[birthDate] Invalid input: expected string, received boolean'])); }); it('should not accept an invalid birth date (number)', async () => { @@ -167,12 +162,7 @@ describe(PersonController.name, () => { .put(`/people/${factory.uuid()}`) .send({ birthDate: 123_456 }); expect(status).toBe(400); - expect(body).toEqual( - errorDto.badRequest([ - 'birthDate must be a string in the format yyyy-MM-dd', - 'Birth date cannot be in the future', - ]), - ); + expect(body).toEqual(errorDto.badRequest(['[birthDate] Invalid input: expected string, received number'])); }); it('should not accept a birth date in the future)', async () => { @@ -180,7 +170,7 @@ describe(PersonController.name, () => { .put(`/people/${factory.uuid()}`) .send({ birthDate: '9999-01-01' }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['Birth date cannot be in the future'])); + expect(body).toEqual(errorDto.badRequest(['[birthDate] Birth date cannot be in the future'])); }); }); @@ -193,7 +183,7 @@ describe(PersonController.name, () => { it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).delete(`/people/invalid`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')])); + expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID'])); }); it('should respond with 204', async () => { diff --git a/server/src/controllers/search.controller.spec.ts b/server/src/controllers/search.controller.spec.ts index adbc8be0f3..4df247031a 100644 --- a/server/src/controllers/search.controller.spec.ts +++ b/server/src/controllers/search.controller.spec.ts @@ -27,37 +27,31 @@ describe(SearchController.name, () => { it('should reject page as a string', async () => { const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ page: 'abc' }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['page must not be less than 1', 'page must be an integer number'])); + expect(body).toEqual(errorDto.badRequest(['[page] Invalid input: expected number, received string'])); }); it('should reject page as a negative number', async () => { const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ page: -10 }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['page must not be less than 1'])); + expect(body).toEqual(errorDto.badRequest(['[page] Too small: expected number to be >=1'])); }); it('should reject page as 0', async () => { const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ page: 0 }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['page must not be less than 1'])); + expect(body).toEqual(errorDto.badRequest(['[page] Too small: expected number to be >=1'])); }); it('should reject size as a string', async () => { const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ size: 'abc' }); expect(status).toBe(400); - expect(body).toEqual( - errorDto.badRequest([ - 'size must not be greater than 1000', - 'size must not be less than 1', - 'size must be an integer number', - ]), - ); + expect(body).toEqual(errorDto.badRequest(['[size] Invalid input: expected number, received string'])); }); it('should reject an invalid size', async () => { const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ size: -1.5 }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['size must not be less than 1', 'size must be an integer number'])); + expect(body).toEqual(errorDto.badRequest(['[size] Too small: expected number to be >=1'])); }); it('should reject an visibility as not an enum', async () => { @@ -66,7 +60,7 @@ describe(SearchController.name, () => { .send({ visibility: 'immich' }); expect(status).toBe(400); expect(body).toEqual( - errorDto.badRequest(['visibility must be one of the following values: archive, timeline, hidden, locked']), + errorDto.badRequest([expect.stringContaining('[visibility] Invalid option: expected one of')]), ); }); @@ -75,7 +69,7 @@ describe(SearchController.name, () => { .post('/search/metadata') .send({ isFavorite: 'immich' }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['isFavorite must be a boolean value'])); + expect(body).toEqual(errorDto.badRequest(['[isFavorite] Invalid input: expected boolean, received string'])); }); it('should reject an isEncoded as not a boolean', async () => { @@ -83,7 +77,7 @@ describe(SearchController.name, () => { .post('/search/metadata') .send({ isEncoded: 'immich' }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['isEncoded must be a boolean value'])); + expect(body).toEqual(errorDto.badRequest(['[isEncoded] Invalid input: expected boolean, received string'])); }); it('should reject an isOffline as not a boolean', async () => { @@ -91,13 +85,13 @@ describe(SearchController.name, () => { .post('/search/metadata') .send({ isOffline: 'immich' }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['isOffline must be a boolean value'])); + expect(body).toEqual(errorDto.badRequest(['[isOffline] Invalid input: expected boolean, received string'])); }); it('should reject an isMotion as not a boolean', async () => { const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ isMotion: 'immich' }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['isMotion must be a boolean value'])); + expect(body).toEqual(errorDto.badRequest(['[isMotion] Invalid input: expected boolean, received string'])); }); describe('POST /search/random', () => { @@ -111,7 +105,7 @@ describe(SearchController.name, () => { .post('/search/random') .send({ withStacked: 'immich' }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['withStacked must be a boolean value'])); + expect(body).toEqual(errorDto.badRequest(['[withStacked] Invalid input: expected boolean, received string'])); }); it('should reject if withPeople is not a boolean', async () => { @@ -119,7 +113,7 @@ describe(SearchController.name, () => { .post('/search/random') .send({ withPeople: 'immich' }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['withPeople must be a boolean value'])); + expect(body).toEqual(errorDto.badRequest(['[withPeople] Invalid input: expected boolean, received string'])); }); }); @@ -146,7 +140,7 @@ describe(SearchController.name, () => { it('should require a name', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/search/person').send({}); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['name should not be empty', 'name must be a string'])); + expect(body).toEqual(errorDto.badRequest(['[name] Invalid input: expected string, received undefined'])); }); }); @@ -159,7 +153,7 @@ describe(SearchController.name, () => { it('should require a name', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/search/places').send({}); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['name should not be empty', 'name must be a string'])); + expect(body).toEqual(errorDto.badRequest(['[name] Invalid input: expected string, received undefined'])); }); }); @@ -179,12 +173,7 @@ describe(SearchController.name, () => { it('should require a type', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/search/suggestions').send({}); expect(status).toBe(400); - expect(body).toEqual( - errorDto.badRequest([ - 'type should not be empty', - expect.stringContaining('type must be one of the following values:'), - ]), - ); + expect(body).toEqual(errorDto.badRequest([expect.stringContaining('[type] Invalid option: expected one of')])); }); }); }); diff --git a/server/src/controllers/shared-link.controller.ts b/server/src/controllers/shared-link.controller.ts index c7ba589a9f..b3a8dd9f59 100644 --- a/server/src/controllers/shared-link.controller.ts +++ b/server/src/controllers/shared-link.controller.ts @@ -23,7 +23,6 @@ import { SharedLinkCreateDto, SharedLinkEditDto, SharedLinkLoginDto, - SharedLinkPasswordDto, SharedLinkResponseDto, SharedLinkSearchDto, } from 'src/dtos/shared-link.dto'; @@ -96,21 +95,7 @@ export class SharedLinkController { description: 'Retrieve the current shared link associated with authentication method.', history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), }) - async getMySharedLink( - @Auth() auth: AuthDto, - @Query() dto: SharedLinkPasswordDto, - @Req() req: Request, - @Res({ passthrough: true }) res: Response, - @GetLoginDetails() loginDetails: LoginDetails, - ): Promise { - if (dto.password) { - this.logger.deprecate( - 'Passing shared link password via query parameters is deprecated and will be removed in the next major release. Please use POST /shared-links/login instead.', - ); - - return this.sharedLinkLogin(auth, { password: dto.password }, req, res, loginDetails); - } - + getMySharedLink(@Auth() auth: AuthDto, @Req() req: Request): Promise { return this.service.getMine(auth, getAuthTokens(req.cookies)); } @@ -164,7 +149,7 @@ export class SharedLinkController { } @Put(':id/assets') - @Authenticated({ sharedLink: true }) + @Authenticated({ permission: Permission.SharedLinkUpdate }) @Endpoint({ summary: 'Add assets to a shared link', description: diff --git a/server/src/controllers/sync.controller.spec.ts b/server/src/controllers/sync.controller.spec.ts index c1f19ddd66..07b0d7199f 100644 --- a/server/src/controllers/sync.controller.spec.ts +++ b/server/src/controllers/sync.controller.spec.ts @@ -35,9 +35,7 @@ describe(SyncController.name, () => { .post('/sync/stream') .send({ types: ['invalid'] }); expect(status).toBe(400); - expect(body).toEqual( - errorDto.badRequest([expect.stringContaining('each value in types must be one of the following values')]), - ); + expect(body).toEqual(errorDto.badRequest([expect.stringContaining('[types.0] Invalid option: expected one of')])); expect(ctx.authenticate).toHaveBeenCalled(); }); }); @@ -59,7 +57,7 @@ describe(SyncController.name, () => { const acks = Array.from({ length: 1001 }, (_, i) => `ack-${i}`); const { status, body } = await request(ctx.getHttpServer()).post('/sync/ack').send({ acks }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['acks must contain no more than 1000 elements'])); + expect(body).toEqual(errorDto.badRequest(['[acks] Too big: expected array to have <=1000 items'])); expect(ctx.authenticate).toHaveBeenCalled(); }); }); @@ -75,9 +73,7 @@ describe(SyncController.name, () => { .delete('/sync/ack') .send({ types: ['invalid'] }); expect(status).toBe(400); - expect(body).toEqual( - errorDto.badRequest([expect.stringContaining('each value in types must be one of the following values')]), - ); + expect(body).toEqual(errorDto.badRequest([expect.stringContaining('[types.0] Invalid option: expected one of')])); expect(ctx.authenticate).toHaveBeenCalled(); }); }); diff --git a/server/src/controllers/sync.controller.ts b/server/src/controllers/sync.controller.ts index de94738f73..c9f3fa7825 100644 --- a/server/src/controllers/sync.controller.ts +++ b/server/src/controllers/sync.controller.ts @@ -2,17 +2,8 @@ import { Body, Controller, Delete, Get, Header, HttpCode, HttpStatus, Post, Res import { ApiTags } from '@nestjs/swagger'; import { Response } from 'express'; import { Endpoint, HistoryBuilder } from 'src/decorators'; -import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { - AssetDeltaSyncDto, - AssetDeltaSyncResponseDto, - AssetFullSyncDto, - SyncAckDeleteDto, - SyncAckDto, - SyncAckSetDto, - SyncStreamDto, -} from 'src/dtos/sync.dto'; +import { SyncAckDeleteDto, SyncAckDto, SyncAckSetDto, SyncStreamDto } from 'src/dtos/sync.dto'; import { ApiTag, Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { GlobalExceptionFilter } from 'src/middleware/global-exception.filter'; @@ -26,30 +17,6 @@ export class SyncController { private errorService: GlobalExceptionFilter, ) {} - @Post('full-sync') - @Authenticated() - @HttpCode(HttpStatus.OK) - @Endpoint({ - summary: 'Get full sync for user', - description: 'Retrieve all assets for a full synchronization for the authenticated user.', - history: new HistoryBuilder().added('v1').deprecated('v2'), - }) - getFullSyncForUser(@Auth() auth: AuthDto, @Body() dto: AssetFullSyncDto): Promise { - return this.service.getFullSync(auth, dto); - } - - @Post('delta-sync') - @Authenticated() - @HttpCode(HttpStatus.OK) - @Endpoint({ - summary: 'Get delta sync for user', - description: 'Retrieve changed assets since the last sync for the authenticated user.', - history: new HistoryBuilder().added('v1').deprecated('v2'), - }) - getDeltaSync(@Auth() auth: AuthDto, @Body() dto: AssetDeltaSyncDto): Promise { - return this.service.getDeltaSync(auth, dto); - } - @Post('stream') @Authenticated({ permission: Permission.SyncStream }) @Header('Content-Type', 'application/jsonlines+json') diff --git a/server/src/controllers/system-config.controller.spec.ts b/server/src/controllers/system-config.controller.spec.ts index bbd1241dc5..a07dee64ad 100644 --- a/server/src/controllers/system-config.controller.spec.ts +++ b/server/src/controllers/system-config.controller.spec.ts @@ -7,6 +7,20 @@ import request from 'supertest'; import { errorDto } from 'test/medium/responses'; import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; +/** Returns a full config that passes Zod validation (required URLs and min lengths). */ +function validConfig() { + const config = _.cloneDeep(defaults) as typeof defaults & { + oauth: { mobileRedirectUri: string }; + notifications: { smtp: { from: string; transport: { host: string } } }; + server: { externalDomain: string }; + }; + config.oauth.mobileRedirectUri = config.oauth.mobileRedirectUri || 'https://example.com'; + config.server.externalDomain = config.server.externalDomain || 'https://example.com'; + config.notifications.smtp.from = config.notifications.smtp.from || 'noreply@example.com'; + config.notifications.smtp.transport.host = config.notifications.smtp.transport.host || 'localhost'; + return config; +} + describe(SystemConfigController.name, () => { let ctx: ControllerContext; const systemConfigService = mockBaseService(SystemConfigService); @@ -48,32 +62,38 @@ describe(SystemConfigController.name, () => { describe('nightlyTasks', () => { it('should validate nightly jobs start time', async () => { - const config = _.cloneDeep(defaults); + const config = validConfig(); config.nightlyTasks.startTime = 'invalid'; const { status, body } = await request(ctx.getHttpServer()).put('/system-config').send(config); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['nightlyTasks.startTime must be in HH:mm format'])); + expect(body).toEqual( + errorDto.badRequest([ + '[nightlyTasks.startTime] Invalid input: expected string in HH:mm format, received string', + ]), + ); }); it('should accept a valid time', async () => { - const config = _.cloneDeep(defaults); + const config = validConfig(); config.nightlyTasks.startTime = '05:05'; const { status } = await request(ctx.getHttpServer()).put('/system-config').send(config); expect(status).toBe(200); }); it('should validate a boolean field', async () => { - const config = _.cloneDeep(defaults); + const config = validConfig(); (config.nightlyTasks.databaseCleanup as any) = 'invalid'; const { status, body } = await request(ctx.getHttpServer()).put('/system-config').send(config); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['nightlyTasks.databaseCleanup must be a boolean value'])); + expect(body).toEqual( + errorDto.badRequest(['[nightlyTasks.databaseCleanup] Invalid input: expected boolean, received string']), + ); }); }); describe('image', () => { it('should accept config without optional progressive property', async () => { - const config = _.cloneDeep(defaults); + const config = validConfig(); delete config.image.thumbnail.progressive; delete config.image.preview.progressive; delete config.image.fullsize.progressive; @@ -82,7 +102,7 @@ describe(SystemConfigController.name, () => { }); it('should accept config with progressive set to true', async () => { - const config = _.cloneDeep(defaults); + const config = validConfig(); config.image.thumbnail.progressive = true; config.image.preview.progressive = true; config.image.fullsize.progressive = true; @@ -91,11 +111,13 @@ describe(SystemConfigController.name, () => { }); it('should reject invalid progressive value', async () => { - const config = _.cloneDeep(defaults); + const config = validConfig(); (config.image.thumbnail.progressive as any) = 'invalid'; const { status, body } = await request(ctx.getHttpServer()).put('/system-config').send(config); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['image.thumbnail.progressive must be a boolean value'])); + expect(body).toEqual( + errorDto.badRequest(['[image.thumbnail.progressive] Invalid input: expected boolean, received string']), + ); }); }); }); diff --git a/server/src/controllers/tag.controller.spec.ts b/server/src/controllers/tag.controller.spec.ts index 60fc3d65ae..edd0f27980 100644 --- a/server/src/controllers/tag.controller.spec.ts +++ b/server/src/controllers/tag.controller.spec.ts @@ -54,7 +54,7 @@ describe(TagController.name, () => { it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).get(`/tags/123`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest([expect.stringContaining('id must be a UUID')])); + expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID'])); }); }); diff --git a/server/src/controllers/timeline.controller.spec.ts b/server/src/controllers/timeline.controller.spec.ts index 6d0276c6a3..f4c18235e4 100644 --- a/server/src/controllers/timeline.controller.spec.ts +++ b/server/src/controllers/timeline.controller.spec.ts @@ -23,6 +23,36 @@ describe(TimelineController.name, () => { await request(ctx.getHttpServer()).get('/timeline/buckets'); expect(ctx.authenticate).toHaveBeenCalled(); }); + + it('should parse bbox query string into an object', async () => { + const { status } = await request(ctx.getHttpServer()) + .get('/timeline/buckets') + .query({ bbox: '11.075683,49.416711,11.117589,49.454875' }); + + expect(status).toBe(200); + expect(service.getTimeBuckets).toHaveBeenCalledWith( + undefined, + expect.objectContaining({ + bbox: { west: 11.075_683, south: 49.416_711, east: 11.117_589, north: 49.454_875 }, + }), + ); + }); + + it('should reject incomplete bbox query string', async () => { + const { status, body } = await request(ctx.getHttpServer()).get('/timeline/buckets').query({ bbox: '1,2,3' }); + expect(status).toBe(400); + expect(body).toEqual( + errorDto.badRequest(['[bbox] bbox must have 4 comma-separated numbers: west,south,east,north'] as any), + ); + }); + + it('should reject invalid bbox query string', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .get('/timeline/buckets') + .query({ bbox: '1,2,3,invalid' }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['[bbox] bbox parts must be valid numbers'] as any)); + }); }); describe('GET /timeline/bucket', () => { diff --git a/server/src/controllers/user-admin.controller.spec.ts b/server/src/controllers/user-admin.controller.spec.ts index edda974476..048f94df5a 100644 --- a/server/src/controllers/user-admin.controller.spec.ts +++ b/server/src/controllers/user-admin.controller.spec.ts @@ -77,7 +77,11 @@ describe(UserAdminController.name, () => { .set('Authorization', `Bearer token`) .send(dto); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['quotaSizeInBytes must be an integer number']))); + expect(body).toEqual( + errorDto.badRequest( + expect.arrayContaining(['[quotaSizeInBytes] Invalid input: expected int, received number']), + ), + ); }); it(`should not allow decimal quota`, async () => { @@ -93,7 +97,11 @@ describe(UserAdminController.name, () => { .set('Authorization', `Bearer token`) .send(dto); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['quotaSizeInBytes must be an integer number']))); + expect(body).toEqual( + errorDto.badRequest( + expect.arrayContaining(['[quotaSizeInBytes] Invalid input: expected int, received number']), + ), + ); }); }); @@ -116,7 +124,11 @@ describe(UserAdminController.name, () => { .set('Authorization', `Bearer token`) .send({ quotaSizeInBytes: 1.2 }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['quotaSizeInBytes must be an integer number']))); + expect(body).toEqual( + errorDto.badRequest( + expect.arrayContaining(['[quotaSizeInBytes] Invalid input: expected int, received number']), + ), + ); }); it('should allow a null pinCode', async () => { diff --git a/server/src/controllers/user.controller.ts b/server/src/controllers/user.controller.ts index 9c0dd3db7a..2db0ca182b 100644 --- a/server/src/controllers/user.controller.ts +++ b/server/src/controllers/user.controller.ts @@ -64,7 +64,7 @@ export class UserController { @Authenticated({ permission: Permission.UserUpdate }) @Endpoint({ summary: 'Update current user', - description: 'Update the current user making teh API request.', + description: 'Update the current user making the API request.', history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), }) updateMyUser(@Auth() auth: AuthDto, @Body() dto: UserUpdateMeDto): Promise { diff --git a/server/src/database.ts b/server/src/database.ts index 4f339624e6..7235af46fa 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -104,7 +104,7 @@ export type Memory = { showAt: Date | null; hideAt: Date | null; type: MemoryType; - data: object; + data: Record; ownerId: string; isSaved: boolean; assets: ShallowDehydrateObject[]; @@ -114,8 +114,6 @@ export type Asset = { id: string; checksum: Buffer; checksumAlgorithm: ChecksumAlgorithm; - deviceAssetId: string; - deviceId: string; fileCreatedAt: Date; fileModifiedAt: Date; isExternal: boolean; @@ -333,8 +331,6 @@ export const columns = { 'asset.id', 'asset.checksum', 'asset.checksumAlgorithm', - 'asset.deviceAssetId', - 'asset.deviceId', 'asset.fileCreatedAt', 'asset.fileModifiedAt', 'asset.isExternal', diff --git a/server/src/dtos/activity.dto.ts b/server/src/dtos/activity.dto.ts index 6464d88508..7b8ba34c91 100644 --- a/server/src/dtos/activity.dto.ts +++ b/server/src/dtos/activity.dto.ts @@ -1,76 +1,68 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsNotEmpty, IsString, ValidateIf } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { Activity } from 'src/database'; -import { mapUser, UserResponseDto } from 'src/dtos/user.dto'; -import { ValidateEnum, ValidateUUID } from 'src/validation'; - -export enum ReactionType { - COMMENT = 'comment', - LIKE = 'like', -} +import { mapUser, UserResponseSchema } from 'src/dtos/user.dto'; +import { isoDatetimeToDate } from 'src/validation'; +import z from 'zod'; export enum ReactionLevel { ALBUM = 'album', ASSET = 'asset', } +const ReactionLevelSchema = z.enum(ReactionLevel).describe('Reaction level').meta({ id: 'ReactionLevel' }); + +export enum ReactionType { + COMMENT = 'comment', + LIKE = 'like', +} +const ReactionTypeSchema = z.enum(ReactionType).describe('Reaction type').meta({ id: 'ReactionType' }); export type MaybeDuplicate = { duplicate: boolean; value: T }; -export class ActivityResponseDto { - @ApiProperty({ description: 'Activity ID' }) - id!: string; - @ApiProperty({ description: 'Creation date', format: 'date-time' }) - createdAt!: Date; - @ValidateEnum({ enum: ReactionType, name: 'ReactionType', description: 'Activity type' }) - type!: ReactionType; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - user!: UserResponseDto; - @ApiProperty({ description: 'Asset ID (if activity is for an asset)' }) - assetId!: string | null; - @ApiPropertyOptional({ description: 'Comment text (for comment activities)' }) - comment?: string | null; -} +const ActivityResponseSchema = z + .object({ + id: z.uuidv4().describe('Activity ID'), + createdAt: isoDatetimeToDate.describe('Creation date'), + user: UserResponseSchema, + assetId: z.uuidv4().nullable().describe('Asset ID (if activity is for an asset)'), + type: ReactionTypeSchema, + comment: z.string().nullish().describe('Comment text (for comment activities)'), + }) + .meta({ id: 'ActivityResponseDto' }); -export class ActivityStatisticsResponseDto { - @ApiProperty({ type: 'integer', description: 'Number of comments' }) - comments!: number; +const ActivityStatisticsResponseSchema = z + .object({ + comments: z.int().min(0).describe('Number of comments'), + likes: z.int().min(0).describe('Number of likes'), + }) + .meta({ id: 'ActivityStatisticsResponseDto' }); - @ApiProperty({ type: 'integer', description: 'Number of likes' }) - likes!: number; -} +const ActivitySchema = z + .object({ + albumId: z.uuidv4().describe('Album ID'), + assetId: z.uuidv4().optional().describe('Asset ID (if activity is for an asset)'), + }) + .describe('Activity'); -export class ActivityDto { - @ValidateUUID({ description: 'Album ID' }) - albumId!: string; +const ActivitySearchSchema = ActivitySchema.extend({ + type: ReactionTypeSchema.optional(), + level: ReactionLevelSchema.optional(), + userId: z.uuidv4().optional().describe('Filter by user ID'), +}).describe('Activity search'); - @ValidateUUID({ optional: true, description: 'Asset ID (if activity is for an asset)' }) - assetId?: string; -} - -export class ActivitySearchDto extends ActivityDto { - @ValidateEnum({ enum: ReactionType, name: 'ReactionType', description: 'Filter by activity type', optional: true }) - type?: ReactionType; - - @ValidateEnum({ enum: ReactionLevel, name: 'ReactionLevel', description: 'Filter by activity level', optional: true }) - level?: ReactionLevel; - - @ValidateUUID({ optional: true, description: 'Filter by user ID' }) - userId?: string; -} - -const isComment = (dto: ActivityCreateDto) => dto.type === ReactionType.COMMENT; - -export class ActivityCreateDto extends ActivityDto { - @ValidateEnum({ enum: ReactionType, name: 'ReactionType', description: 'Activity type (like or comment)' }) - type!: ReactionType; - - @ApiPropertyOptional({ description: 'Comment text (required if type is comment)' }) - @ValidateIf(isComment) - @IsNotEmpty() - @IsString() - comment?: string; -} +const ActivityCreateSchema = ActivitySchema.extend({ + type: ReactionTypeSchema, + assetId: z.uuidv4().optional().describe('Asset ID (if activity is for an asset)'), + comment: z.string().optional().describe('Comment text (required if type is comment)'), +}) + .refine((data) => data.type !== ReactionType.COMMENT || (data.comment && data.comment.trim() !== ''), { + error: 'Comment is required when type is COMMENT', + path: ['comment'], + }) + .refine((data) => data.type === ReactionType.COMMENT || !data.comment, { + error: 'Comment must not be provided when type is not COMMENT', + path: ['comment'], + }) + .describe('Activity create'); export const mapActivity = (activity: Activity): ActivityResponseDto => { return { @@ -82,3 +74,9 @@ export const mapActivity = (activity: Activity): ActivityResponseDto => { user: mapUser(activity.user), }; }; + +export class ActivityResponseDto extends createZodDto(ActivityResponseSchema) {} +export class ActivityCreateDto extends createZodDto(ActivityCreateSchema) {} +export class ActivityDto extends createZodDto(ActivitySchema) {} +export class ActivitySearchDto extends createZodDto(ActivitySearchSchema) {} +export class ActivityStatisticsResponseDto extends createZodDto(ActivityStatisticsResponseSchema) {} diff --git a/server/src/dtos/album-response.dto.spec.ts b/server/src/dtos/album-response.dto.spec.ts index e82067580b..c03662288a 100644 --- a/server/src/dtos/album-response.dto.spec.ts +++ b/server/src/dtos/album-response.dto.spec.ts @@ -10,13 +10,13 @@ describe('mapAlbum', () => { .asset({ localDateTime: endDate }, (builder) => builder.exif()) .asset({ localDateTime: startDate }, (builder) => builder.exif()) .build(); - const dto = mapAlbum(getForAlbum(album), false); + const dto = mapAlbum(getForAlbum(album)); expect(dto.startDate).toEqual(startDate.toISOString()); expect(dto.endDate).toEqual(endDate.toISOString()); }); it('should not set start and end dates for empty assets', () => { - const dto = mapAlbum(getForAlbum(AlbumFactory.create()), false); + const dto = mapAlbum(getForAlbum(AlbumFactory.create())); expect(dto.startDate).toBeUndefined(); expect(dto.endDate).toBeUndefined(); }); diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts index b270125b36..519094cd94 100644 --- a/server/src/dtos/album.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -1,196 +1,149 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { ArrayNotEmpty, IsArray, IsString, ValidateNested } from 'class-validator'; import { ShallowDehydrateObject } from 'kysely'; import _ from 'lodash'; +import { createZodDto } from 'nestjs-zod'; import { AlbumUser, AuthSharedLink, User } from 'src/database'; -import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto'; -import { AssetResponseDto, MapAsset, mapAsset } from 'src/dtos/asset-response.dto'; -import { AuthDto } from 'src/dtos/auth.dto'; -import { mapUser, UserResponseDto } from 'src/dtos/user.dto'; -import { AlbumUserRole, AssetOrder } from 'src/enum'; +import { BulkIdErrorReasonSchema } from 'src/dtos/asset-ids.response.dto'; +import { MapAsset } from 'src/dtos/asset-response.dto'; +import { UserResponseSchema, mapUser } from 'src/dtos/user.dto'; +import { AlbumUserRole, AlbumUserRoleSchema, AssetOrder, AssetOrderSchema } from 'src/enum'; import { MaybeDehydrated } from 'src/types'; import { asDateString } from 'src/utils/date'; -import { Optional, ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation'; +import { stringToBool } from 'src/validation'; +import z from 'zod'; -export class AlbumInfoDto { - @ValidateBoolean({ optional: true, description: 'Exclude assets from response' }) - withoutAssets?: boolean; -} - -export class AlbumUserAddDto { - @ValidateUUID({ description: 'User ID' }) - userId!: string; - - @ValidateEnum({ - enum: AlbumUserRole, - name: 'AlbumUserRole', - description: 'Album user role', - default: AlbumUserRole.Editor, +const AlbumUserAddSchema = z + .object({ + userId: z.uuidv4().describe('User ID'), + role: AlbumUserRoleSchema.default(AlbumUserRole.Editor).optional().describe('Album user role'), }) - role?: AlbumUserRole; -} + .meta({ id: 'AlbumUserAddDto' }); -export class AddUsersDto { - @ApiProperty({ description: 'Album users to add' }) - @ArrayNotEmpty() - albumUsers!: AlbumUserAddDto[]; -} - -export class AlbumUserCreateDto { - @ValidateUUID({ description: 'User ID' }) - userId!: string; - - @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', description: 'Album user role' }) - role!: AlbumUserRole; -} - -export class CreateAlbumDto { - @ApiProperty({ description: 'Album name' }) - @IsString() - albumName!: string; - - @ApiPropertyOptional({ description: 'Album description' }) - @IsString() - @Optional() - description?: string; - - @ApiPropertyOptional({ description: 'Album users' }) - @Optional() - @IsArray() - @ValidateNested({ each: true }) - @Type(() => AlbumUserCreateDto) - albumUsers?: AlbumUserCreateDto[]; - - @ValidateUUID({ optional: true, each: true, description: 'Initial asset IDs' }) - assetIds?: string[]; -} - -export class AlbumsAddAssetsDto { - @ValidateUUID({ each: true, description: 'Album IDs' }) - albumIds!: string[]; - - @ValidateUUID({ each: true, description: 'Asset IDs' }) - assetIds!: string[]; -} - -export class AlbumsAddAssetsResponseDto { - @ApiProperty({ description: 'Operation success' }) - success!: boolean; - @ValidateEnum({ enum: BulkIdErrorReason, name: 'BulkIdErrorReason', description: 'Error reason', optional: true }) - error?: BulkIdErrorReason; -} - -export class UpdateAlbumDto { - @ApiPropertyOptional({ description: 'Album name' }) - @Optional() - @IsString() - albumName?: string; - - @ApiPropertyOptional({ description: 'Album description' }) - @Optional() - @IsString() - description?: string; - - @ValidateUUID({ optional: true, description: 'Album thumbnail asset ID' }) - albumThumbnailAssetId?: string; - - @ValidateBoolean({ optional: true, description: 'Enable activity feed' }) - isActivityEnabled?: boolean; - - @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', description: 'Asset sort order', optional: true }) - order?: AssetOrder; -} - -export class GetAlbumsDto { - @ValidateBoolean({ - optional: true, - description: 'Filter by shared status: true = only shared, false = not shared, undefined = all owned albums', +const AddUsersSchema = z + .object({ + albumUsers: z.array(AlbumUserAddSchema).min(1).describe('Album users to add'), }) - shared?: boolean; + .meta({ id: 'AddUsersDto' }); - @ValidateUUID({ optional: true, description: 'Filter albums containing this asset ID (ignores shared parameter)' }) - assetId?: string; -} +const AlbumUserCreateSchema = z + .object({ + userId: z.uuidv4().describe('User ID'), + role: AlbumUserRoleSchema, + }) + .meta({ id: 'AlbumUserCreateDto' }); -export class AlbumStatisticsResponseDto { - @ApiProperty({ type: 'integer', description: 'Number of owned albums' }) - owned!: number; +const CreateAlbumSchema = z + .object({ + albumName: z.string().describe('Album name'), + description: z.string().optional().describe('Album description'), + albumUsers: z.array(AlbumUserCreateSchema).optional().describe('Album users'), + assetIds: z.array(z.uuidv4()).optional().describe('Initial asset IDs'), + }) + .meta({ id: 'CreateAlbumDto' }); - @ApiProperty({ type: 'integer', description: 'Number of shared albums' }) - shared!: number; +const AlbumsAddAssetsSchema = z + .object({ + albumIds: z.array(z.uuidv4()).describe('Album IDs'), + assetIds: z.array(z.uuidv4()).describe('Asset IDs'), + }) + .meta({ id: 'AlbumsAddAssetsDto' }); - @ApiProperty({ type: 'integer', description: 'Number of non-shared albums' }) - notShared!: number; -} +const AlbumsAddAssetsResponseSchema = z + .object({ + success: z.boolean().describe('Operation success'), + error: BulkIdErrorReasonSchema.optional(), + }) + .meta({ id: 'AlbumsAddAssetsResponseDto' }); -export class UpdateAlbumUserDto { - @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', description: 'Album user role' }) - role!: AlbumUserRole; -} +const UpdateAlbumSchema = z + .object({ + albumName: z.string().optional().describe('Album name'), + description: z.string().optional().describe('Album description'), + albumThumbnailAssetId: z.uuidv4().optional().describe('Album thumbnail asset ID'), + isActivityEnabled: z.boolean().optional().describe('Enable activity feed'), + order: AssetOrderSchema.optional(), + }) + .meta({ id: 'UpdateAlbumDto' }); -export class AlbumUserResponseDto { - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - user!: UserResponseDto; - @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', description: 'Album user role' }) - role!: AlbumUserRole; -} +const GetAlbumsSchema = z + .object({ + shared: stringToBool + .optional() + .describe('Filter by shared status: true = only shared, false = not shared, undefined = all owned albums'), + assetId: z.uuidv4().optional().describe('Filter albums containing this asset ID (ignores shared parameter)'), + }) + .meta({ id: 'GetAlbumsDto' }); -export class ContributorCountResponseDto { - @ApiProperty({ description: 'User ID' }) - userId!: string; +const AlbumStatisticsResponseSchema = z + .object({ + owned: z.int().min(0).describe('Number of owned albums'), + shared: z.int().min(0).describe('Number of shared albums'), + notShared: z.int().min(0).describe('Number of non-shared albums'), + }) + .meta({ id: 'AlbumStatisticsResponseDto' }); - @ApiProperty({ type: 'integer', description: 'Number of assets contributed' }) - assetCount!: number; -} +const UpdateAlbumUserSchema = z + .object({ + role: AlbumUserRoleSchema, + }) + .meta({ id: 'UpdateAlbumUserDto' }); -export class AlbumResponseDto { - @ApiProperty({ description: 'Album ID' }) - id!: string; - @ApiProperty({ description: 'Owner user ID' }) - ownerId!: string; - @ApiProperty({ description: 'Album name' }) - albumName!: string; - @ApiProperty({ description: 'Album description' }) - description!: string; - @ApiProperty({ description: 'Creation date', format: 'date-time' }) - createdAt!: string; - @ApiProperty({ description: 'Last update date', format: 'date-time' }) - updatedAt!: string; - @ApiProperty({ description: 'Thumbnail asset ID' }) - albumThumbnailAssetId!: string | null; - @ApiProperty({ description: 'Is shared album' }) - shared!: boolean; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - albumUsers!: AlbumUserResponseDto[]; - @ApiProperty({ description: 'Has shared link' }) - hasSharedLink!: boolean; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - assets!: AssetResponseDto[]; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - owner!: UserResponseDto; - @ApiProperty({ type: 'integer', description: 'Number of assets' }) - assetCount!: number; - @ApiPropertyOptional({ description: 'Last modified asset timestamp', format: 'date-time' }) - lastModifiedAssetTimestamp?: string; - @ApiPropertyOptional({ description: 'Start date (earliest asset)', format: 'date-time' }) - startDate?: string; - @ApiPropertyOptional({ description: 'End date (latest asset)', format: 'date-time' }) - endDate?: string; - @ApiProperty({ description: 'Activity feed enabled' }) - isActivityEnabled!: boolean; - @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', description: 'Asset sort order', optional: true }) - order?: AssetOrder; +const AlbumUserResponseSchema = z + .object({ + user: UserResponseSchema, + role: AlbumUserRoleSchema, + }) + .meta({ id: 'AlbumUserResponseDto' }); - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Type(() => ContributorCountResponseDto) - contributorCounts?: ContributorCountResponseDto[]; -} +const ContributorCountResponseSchema = z + .object({ + userId: z.string().describe('User ID'), + assetCount: z.int().min(0).describe('Number of assets contributed'), + }) + .meta({ id: 'ContributorCountResponseDto' }); + +export const AlbumResponseSchema = z + .object({ + id: z.string().describe('Album ID'), + ownerId: z.string().describe('Owner user ID'), + albumName: z.string().describe('Album name'), + description: z.string().describe('Album description'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + createdAt: z.string().meta({ format: 'date-time' }).describe('Creation date'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + updatedAt: z.string().meta({ format: 'date-time' }).describe('Last update date'), + albumThumbnailAssetId: z.string().nullable().describe('Thumbnail asset ID'), + shared: z.boolean().describe('Is shared album'), + albumUsers: z.array(AlbumUserResponseSchema), + hasSharedLink: z.boolean().describe('Has shared link'), + owner: UserResponseSchema, + assetCount: z.int().min(0).describe('Number of assets'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + lastModifiedAssetTimestamp: z + .string() + .meta({ format: 'date-time' }) + .optional() + .describe('Last modified asset timestamp'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + startDate: z.string().meta({ format: 'date-time' }).optional().describe('Start date (earliest asset)'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + endDate: z.string().meta({ format: 'date-time' }).optional().describe('End date (latest asset)'), + isActivityEnabled: z.boolean().describe('Activity feed enabled'), + order: AssetOrderSchema.optional(), + contributorCounts: z.array(ContributorCountResponseSchema).optional(), + }) + .meta({ id: 'AlbumResponseDto' }); + +export class AddUsersDto extends createZodDto(AddUsersSchema) {} +export class AlbumUserCreateDto extends createZodDto(AlbumUserCreateSchema) {} +export class CreateAlbumDto extends createZodDto(CreateAlbumSchema) {} +export class AlbumsAddAssetsDto extends createZodDto(AlbumsAddAssetsSchema) {} +export class AlbumsAddAssetsResponseDto extends createZodDto(AlbumsAddAssetsResponseSchema) {} +export class UpdateAlbumDto extends createZodDto(UpdateAlbumSchema) {} +export class GetAlbumsDto extends createZodDto(GetAlbumsSchema) {} +export class AlbumStatisticsResponseDto extends createZodDto(AlbumStatisticsResponseSchema) {} +export class UpdateAlbumUserDto extends createZodDto(UpdateAlbumUserSchema) {} +export class AlbumResponseDto extends createZodDto(AlbumResponseSchema) {} +class AlbumUserResponseDto extends createZodDto(AlbumUserResponseSchema) {} export type MapAlbumDto = { albumUsers?: AlbumUser[]; @@ -208,11 +161,7 @@ export type MapAlbumDto = { order: AssetOrder; }; -export const mapAlbum = ( - entity: MaybeDehydrated, - withAssets: boolean, - auth?: AuthDto, -): AlbumResponseDto => { +export const mapAlbum = (entity: MaybeDehydrated): AlbumResponseDto => { const albumUsers: AlbumUserResponseDto[] = []; if (entity.albumUsers) { @@ -253,12 +202,8 @@ export const mapAlbum = ( hasSharedLink, startDate: asDateString(startDate), endDate: asDateString(endDate), - assets: (withAssets ? assets : []).map((asset) => mapAsset(asset, { auth })), assetCount: entity.assets?.length || 0, isActivityEnabled: entity.isActivityEnabled, order: entity.order, }; }; - -export const mapAlbumWithAssets = (entity: MaybeDehydrated) => mapAlbum(entity, true); -export const mapAlbumWithoutAssets = (entity: MaybeDehydrated) => mapAlbum(entity, false); diff --git a/server/src/dtos/api-key.dto.ts b/server/src/dtos/api-key.dto.ts index 273082c41b..176812cc4f 100644 --- a/server/src/dtos/api-key.dto.ts +++ b/server/src/dtos/api-key.dto.ts @@ -1,55 +1,42 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { ArrayMinSize, IsNotEmpty, IsString } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { Permission } from 'src/enum'; -import { Optional, ValidateEnum } from 'src/validation'; +import { isoDatetimeToDate } from 'src/validation'; +import z from 'zod'; -export class APIKeyCreateDto { - @ApiPropertyOptional({ description: 'API key name' }) - @IsString() - @IsNotEmpty() - @Optional() - name?: string; +const PermissionSchema = z.enum(Permission).describe('List of permissions').meta({ id: 'Permission' }); - @ValidateEnum({ enum: Permission, name: 'Permission', each: true, description: 'List of permissions' }) - @ArrayMinSize(1) - permissions!: Permission[]; -} - -export class APIKeyUpdateDto { - @ApiPropertyOptional({ description: 'API key name' }) - @Optional() - @IsString() - @IsNotEmpty() - name?: string; - - @ValidateEnum({ - enum: Permission, - name: 'Permission', - description: 'List of permissions', - each: true, - optional: true, +const ApiKeyCreateSchema = z + .object({ + name: z.string().optional().describe('API key name'), + permissions: z.array(PermissionSchema).min(1).describe('List of permissions'), }) - @ArrayMinSize(1) - permissions?: Permission[]; -} + .meta({ id: 'ApiKeyCreateDto' }); -export class APIKeyResponseDto { - @ApiProperty({ description: 'API key ID' }) - id!: string; - @ApiProperty({ description: 'API key name' }) - name!: string; - @ApiProperty({ description: 'Creation date' }) - createdAt!: Date; - @ApiProperty({ description: 'Last update date' }) - updatedAt!: Date; - @ValidateEnum({ enum: Permission, name: 'Permission', each: true, description: 'List of permissions' }) - permissions!: Permission[]; -} +const ApiKeyUpdateSchema = z + .object({ + name: z.string().optional().describe('API key name'), + permissions: z.array(PermissionSchema).min(1).optional().describe('List of permissions'), + }) + .meta({ id: 'ApiKeyUpdateDto' }); -export class APIKeyCreateResponseDto { - @ApiProperty({ description: 'API key secret (only shown once)' }) - secret!: string; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - apiKey!: APIKeyResponseDto; -} +const ApiKeyResponseSchema = z + .object({ + id: z.string().describe('API key ID'), + name: z.string().describe('API key name'), + createdAt: isoDatetimeToDate.describe('Creation date'), + updatedAt: isoDatetimeToDate.describe('Last update date'), + permissions: z.array(PermissionSchema).describe('List of permissions'), + }) + .meta({ id: 'ApiKeyResponseDto' }); + +const ApiKeyCreateResponseSchema = z + .object({ + secret: z.string().describe('API key secret (only shown once)'), + apiKey: ApiKeyResponseSchema, + }) + .meta({ id: 'ApiKeyCreateResponseDto' }); + +export class ApiKeyCreateDto extends createZodDto(ApiKeyCreateSchema) {} +export class ApiKeyUpdateDto extends createZodDto(ApiKeyUpdateSchema) {} +export class ApiKeyResponseDto extends createZodDto(ApiKeyResponseSchema) {} +export class ApiKeyCreateResponseDto extends createZodDto(ApiKeyCreateResponseSchema) {} diff --git a/server/src/dtos/asset-ids.response.dto.ts b/server/src/dtos/asset-ids.response.dto.ts index 1065d8485e..346829e644 100644 --- a/server/src/dtos/asset-ids.response.dto.ts +++ b/server/src/dtos/asset-ids.response.dto.ts @@ -1,5 +1,5 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { ValidateUUID } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import z from 'zod'; /** @deprecated Use `BulkIdResponseDto` instead */ export enum AssetIdErrorReason { @@ -8,15 +8,19 @@ export enum AssetIdErrorReason { NOT_FOUND = 'not_found', } +const AssetIdErrorReasonSchema = z + .enum(AssetIdErrorReason) + .describe('Error reason if failed') + .meta({ id: 'AssetIdErrorReason' }); + /** @deprecated Use `BulkIdResponseDto` instead */ -export class AssetIdsResponseDto { - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; - @ApiProperty({ description: 'Whether operation succeeded' }) - success!: boolean; - @ApiPropertyOptional({ description: 'Error reason if failed', enum: AssetIdErrorReason }) - error?: AssetIdErrorReason; -} +const AssetIdsResponseSchema = z + .object({ + assetId: z.string().describe('Asset ID'), + success: z.boolean().describe('Whether operation succeeded'), + error: AssetIdErrorReasonSchema.optional(), + }) + .meta({ id: 'AssetIdsResponseDto' }); export enum BulkIdErrorReason { DUPLICATE = 'duplicate', @@ -26,17 +30,27 @@ export enum BulkIdErrorReason { VALIDATION = 'validation', } -export class BulkIdsDto { - @ValidateUUID({ each: true, description: 'IDs to process' }) - ids!: string[]; -} +export const BulkIdErrorReasonSchema = z + .enum(BulkIdErrorReason) + .describe('Error reason') + .meta({ id: 'BulkIdErrorReason' }); -export class BulkIdResponseDto { - @ApiProperty({ description: 'ID' }) - id!: string; - @ApiProperty({ description: 'Whether operation succeeded' }) - success!: boolean; - @ApiPropertyOptional({ description: 'Error reason if failed', enum: BulkIdErrorReason }) - error?: BulkIdErrorReason; - errorMessage?: string; -} +export const BulkIdsSchema = z + .object({ + ids: z.array(z.uuidv4()).describe('IDs to process'), + }) + .meta({ id: 'BulkIdsDto' }); + +const BulkIdResponseSchema = z + .object({ + id: z.string().describe('ID'), + success: z.boolean().describe('Whether operation succeeded'), + error: BulkIdErrorReasonSchema.optional(), + errorMessage: z.string().optional(), + }) + .meta({ id: 'BulkIdResponseDto' }); + +/** @deprecated Use `BulkIdResponseDto` instead */ +export class AssetIdsResponseDto extends createZodDto(AssetIdsResponseSchema) {} +export class BulkIdsDto extends createZodDto(BulkIdsSchema) {} +export class BulkIdResponseDto extends createZodDto(BulkIdResponseSchema) {} diff --git a/server/src/dtos/asset-media-response.dto.ts b/server/src/dtos/asset-media-response.dto.ts index 345c1bf418..ab79b8428c 100644 --- a/server/src/dtos/asset-media-response.dto.ts +++ b/server/src/dtos/asset-media-response.dto.ts @@ -1,47 +1,53 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { ValidateEnum } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import z from 'zod'; export enum AssetMediaStatus { CREATED = 'created', REPLACED = 'replaced', DUPLICATE = 'duplicate', } -export class AssetMediaResponseDto { - @ValidateEnum({ enum: AssetMediaStatus, name: 'AssetMediaStatus', description: 'Upload status' }) - status!: AssetMediaStatus; - @ApiProperty({ description: 'Asset media ID' }) - id!: string; -} + +const AssetMediaStatusSchema = z.enum(AssetMediaStatus).describe('Upload status').meta({ id: 'AssetMediaStatus' }); + +const AssetMediaResponseSchema = z + .object({ + status: AssetMediaStatusSchema, + id: z.string().describe('Asset media ID'), + }) + .meta({ id: 'AssetMediaResponseDto' }); export enum AssetUploadAction { ACCEPT = 'accept', REJECT = 'reject', } +const AssetUploadActionSchema = z.enum(AssetUploadAction).describe('Upload action').meta({ id: 'AssetUploadAction' }); + export enum AssetRejectReason { DUPLICATE = 'duplicate', UNSUPPORTED_FORMAT = 'unsupported-format', } -export class AssetBulkUploadCheckResult { - @ApiProperty({ description: 'Asset ID' }) - id!: string; - @ApiProperty({ description: 'Upload action', enum: AssetUploadAction }) - action!: AssetUploadAction; - @ApiPropertyOptional({ description: 'Rejection reason if rejected', enum: AssetRejectReason }) - reason?: AssetRejectReason; - @ApiPropertyOptional({ description: 'Existing asset ID if duplicate' }) - assetId?: string; - @ApiPropertyOptional({ description: 'Whether existing asset is trashed' }) - isTrashed?: boolean; -} +const AssetRejectReasonSchema = z + .enum(AssetRejectReason) + .describe('Rejection reason if rejected') + .meta({ id: 'AssetRejectReason' }); -export class AssetBulkUploadCheckResponseDto { - @ApiProperty({ description: 'Upload check results' }) - results!: AssetBulkUploadCheckResult[]; -} +const AssetBulkUploadCheckResultSchema = z + .object({ + id: z.string().describe('Asset ID'), + action: AssetUploadActionSchema, + reason: AssetRejectReasonSchema.optional(), + assetId: z.string().optional().describe('Existing asset ID if duplicate'), + isTrashed: z.boolean().optional().describe('Whether existing asset is trashed'), + }) + .meta({ id: 'AssetBulkUploadCheckResult' }); -export class CheckExistingAssetsResponseDto { - @ApiProperty({ description: 'Existing asset IDs' }) - existingIds!: string[]; -} +const AssetBulkUploadCheckResponseSchema = z + .object({ + results: z.array(AssetBulkUploadCheckResultSchema).describe('Upload check results'), + }) + .meta({ id: 'AssetBulkUploadCheckResponseDto' }); + +export class AssetMediaResponseDto extends createZodDto(AssetMediaResponseSchema) {} +export class AssetBulkUploadCheckResponseDto extends createZodDto(AssetBulkUploadCheckResponseSchema) {} diff --git a/server/src/dtos/asset-media.dto.ts b/server/src/dtos/asset-media.dto.ts index 4655850379..cd9c7de641 100644 --- a/server/src/dtos/asset-media.dto.ts +++ b/server/src/dtos/asset-media.dto.ts @@ -1,10 +1,9 @@ -import { BadRequestException } from '@nestjs/common'; -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { plainToInstance, Transform, Type } from 'class-transformer'; -import { ArrayNotEmpty, IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; -import { AssetMetadataUpsertItemDto } from 'src/dtos/asset.dto'; -import { AssetVisibility } from 'src/enum'; -import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import { HistoryBuilder } from 'src/decorators'; +import { AssetMetadataUpsertItemSchema } from 'src/dtos/asset.dto'; +import { AssetVisibilitySchema } from 'src/enum'; +import { isoDatetimeToDate, JsonParsed, stringToBool } from 'src/validation'; +import z from 'zod'; export enum AssetMediaSize { Original = 'original', @@ -17,13 +16,18 @@ export enum AssetMediaSize { THUMBNAIL = 'thumbnail', } -export class AssetMediaOptionsDto { - @ValidateEnum({ enum: AssetMediaSize, name: 'AssetMediaSize', description: 'Asset media size', optional: true }) - size?: AssetMediaSize; +const AssetMediaSizeSchema = z.enum(AssetMediaSize).describe('Asset media size').meta({ id: 'AssetMediaSize' }); - @ValidateBoolean({ optional: true, description: 'Return edited asset if available', default: false }) - edited?: boolean; -} +const AssetMediaOptionsSchema = z + .object({ + size: AssetMediaSizeSchema.optional().meta( + new HistoryBuilder() + .updated('v3', "Specifying 'original' is deprecated. Use the original endpoint directly instead") + .getExtensions(), + ), + edited: stringToBool.default(false).optional().describe('Return edited asset if available'), + }) + .meta({ id: 'AssetMediaOptionsDto' }); export enum UploadFieldName { ASSET_DATA = 'assetData', @@ -31,98 +35,40 @@ export enum UploadFieldName { PROFILE_DATA = 'file', } -class AssetMediaBase { - @ApiProperty({ description: 'Device asset ID' }) - @IsNotEmpty() - @IsString() - deviceAssetId!: string; +const AssetMediaBaseSchema = z.object({ + fileCreatedAt: isoDatetimeToDate.describe('File creation date'), + fileModifiedAt: isoDatetimeToDate.describe('File modification date'), + duration: z.string().optional().describe('Duration (for videos)'), + filename: z.string().optional().describe('Filename'), + /** The properties below are added to correctly generate the API docs and client SDKs. Validation should be handled in the controller. */ + [UploadFieldName.ASSET_DATA]: z.any().describe('Asset file data').meta({ type: 'string', format: 'binary' }), +}); - @ApiProperty({ description: 'Device ID' }) - @IsNotEmpty() - @IsString() - deviceId!: string; +const AssetMediaCreateSchema = AssetMediaBaseSchema.extend({ + isFavorite: stringToBool.optional().describe('Mark as favorite'), + visibility: AssetVisibilitySchema.optional(), + livePhotoVideoId: z.uuidv4().optional().describe('Live photo video ID'), + metadata: JsonParsed.pipe(z.array(AssetMetadataUpsertItemSchema)).optional().describe('Asset metadata items'), + [UploadFieldName.SIDECAR_DATA]: z + .any() + .optional() + .describe('Sidecar file data') + .meta({ type: 'string', format: 'binary' }), +}).meta({ id: 'AssetMediaCreateDto' }); - @ValidateDate({ description: 'File creation date' }) - fileCreatedAt!: Date; - - @ValidateDate({ description: 'File modification date' }) - fileModifiedAt!: Date; - - @ApiPropertyOptional({ description: 'Duration (for videos)' }) - @Optional() - @IsString() - duration?: string; - - @ApiPropertyOptional({ description: 'Filename' }) - @Optional() - @IsString() - filename?: string; - - // The properties below are added to correctly generate the API docs - // and client SDKs. Validation should be handled in the controller. - @ApiProperty({ type: 'string', format: 'binary', description: 'Asset file data' }) - [UploadFieldName.ASSET_DATA]!: any; -} - -export class AssetMediaCreateDto extends AssetMediaBase { - @ValidateBoolean({ optional: true, description: 'Mark as favorite' }) - isFavorite?: boolean; - - @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', description: 'Asset visibility', optional: true }) - visibility?: AssetVisibility; - - @ValidateUUID({ optional: true, description: 'Live photo video ID' }) - livePhotoVideoId?: string; - - @ApiPropertyOptional({ description: 'Asset metadata items' }) - @Transform(({ value }) => { - try { - const json = JSON.parse(value); - const items = Array.isArray(json) ? json : [json]; - return items.map((item) => plainToInstance(AssetMetadataUpsertItemDto, item)); - } catch { - throw new BadRequestException(['metadata must be valid JSON']); - } +const AssetBulkUploadCheckItemSchema = z + .object({ + id: z.string().describe('Asset ID'), + checksum: z.string().describe('Base64 or hex encoded SHA1 hash'), }) - @Optional() - @ValidateNested({ each: true }) - @IsArray() - metadata?: AssetMetadataUpsertItemDto[]; + .meta({ id: 'AssetBulkUploadCheckItem' }); - @ApiProperty({ type: 'string', format: 'binary', required: false, description: 'Sidecar file data' }) - [UploadFieldName.SIDECAR_DATA]?: any; -} +const AssetBulkUploadCheckSchema = z + .object({ + assets: z.array(AssetBulkUploadCheckItemSchema).describe('Assets to check'), + }) + .meta({ id: 'AssetBulkUploadCheckDto' }); -export class AssetMediaReplaceDto extends AssetMediaBase {} - -export class AssetBulkUploadCheckItem { - @ApiProperty({ description: 'Asset ID' }) - @IsString() - @IsNotEmpty() - id!: string; - - @ApiProperty({ description: 'Base64 or hex encoded SHA1 hash' }) - @IsString() - @IsNotEmpty() - checksum!: string; -} - -export class AssetBulkUploadCheckDto { - @ApiProperty({ description: 'Assets to check' }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => AssetBulkUploadCheckItem) - assets!: AssetBulkUploadCheckItem[]; -} - -export class CheckExistingAssetsDto { - @ApiProperty({ description: 'Device asset IDs to check' }) - @ArrayNotEmpty() - @IsString({ each: true }) - @IsNotEmpty({ each: true }) - deviceAssetIds!: string[]; - - @ApiProperty({ description: 'Device ID' }) - @IsNotEmpty() - deviceId!: string; -} +export class AssetMediaOptionsDto extends createZodDto(AssetMediaOptionsSchema) {} +export class AssetMediaCreateDto extends createZodDto(AssetMediaCreateSchema) {} +export class AssetBulkUploadCheckDto extends createZodDto(AssetBulkUploadCheckSchema) {} diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index 2c2f57bbb2..b4a398b573 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -1,144 +1,130 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Selectable, ShallowDehydrateObject } from 'kysely'; +import { createZodDto } from 'nestjs-zod'; import { AssetFace, AssetFile, Exif, Stack, Tag, User } from 'src/database'; -import { HistoryBuilder, Property } from 'src/decorators'; +import { HistoryBuilder } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetEditActionItem } from 'src/dtos/editing.dto'; -import { ExifResponseDto, mapExif } from 'src/dtos/exif.dto'; +import { ExifResponseSchema, mapExif } from 'src/dtos/exif.dto'; import { - AssetFaceWithoutPersonResponseDto, + AssetFaceWithoutPersonResponseSchema, PersonWithFacesResponseDto, + PersonWithFacesResponseSchema, mapFacesWithoutPerson, mapPerson, } from 'src/dtos/person.dto'; -import { TagResponseDto, mapTag } from 'src/dtos/tag.dto'; -import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; -import { AssetStatus, AssetType, AssetVisibility, ChecksumAlgorithm } from 'src/enum'; +import { TagResponseSchema, mapTag } from 'src/dtos/tag.dto'; +import { UserResponseSchema, mapUser } from 'src/dtos/user.dto'; +import { + AssetStatus, + AssetType, + AssetTypeSchema, + AssetVisibility, + AssetVisibilitySchema, + ChecksumAlgorithm, +} from 'src/enum'; import { ImageDimensions, MaybeDehydrated } from 'src/types'; import { getDimensions } from 'src/utils/asset.util'; import { hexOrBufferToBase64 } from 'src/utils/bytes'; import { asDateString } from 'src/utils/date'; import { mimeTypes } from 'src/utils/mime-types'; -import { ValidateEnum, ValidateUUID } from 'src/validation'; +import z from 'zod'; -export class SanitizedAssetResponseDto { - @ApiProperty({ description: 'Asset ID' }) - id!: string; - @ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum', description: 'Asset type' }) - type!: AssetType; - @ApiProperty({ - description: - 'Thumbhash for thumbnail generation (base64) also used as the c query param for thumbnail cache busting.', +const SanitizedAssetResponseSchema = z + .object({ + id: z.string().describe('Asset ID'), + type: AssetTypeSchema, + thumbhash: z + .string() + .describe( + 'Thumbhash for thumbnail generation (base64) also used as the c query param for thumbnail cache busting.', + ) + .nullable(), + originalMimeType: z.string().optional().describe('Original MIME type'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + localDateTime: z + .string() + .meta({ format: 'date-time' }) + .describe( + 'The local date and time when the photo/video was taken, derived from EXIF metadata. This represents the photographer\'s local time regardless of timezone, stored as a timezone-agnostic timestamp. Used for timeline grouping by "local" days and months.', + ), + duration: z.string().describe('Video duration (for videos)'), + livePhotoVideoId: z.string().nullish().describe('Live photo video ID'), + hasMetadata: z.boolean().describe('Whether asset has metadata'), + width: z.number().min(0).nullable().describe('Asset width'), + height: z.number().min(0).nullable().describe('Asset height'), }) - thumbhash!: string | null; - @ApiPropertyOptional({ description: 'Original MIME type' }) - originalMimeType?: string; - @ApiProperty({ - type: 'string', - format: 'date-time', - description: - 'The local date and time when the photo/video was taken, derived from EXIF metadata. This represents the photographer\'s local time regardless of timezone, stored as a timezone-agnostic timestamp. Used for timeline grouping by "local" days and months.', - example: '2024-01-15T14:30:00.000Z', - }) - localDateTime!: string; - @ApiProperty({ description: 'Video duration (for videos)' }) - duration!: string; - @ApiPropertyOptional({ description: 'Live photo video ID' }) - livePhotoVideoId?: string | null; - @ApiProperty({ description: 'Whether asset has metadata' }) - hasMetadata!: boolean; - @ApiProperty({ description: 'Asset width' }) - width!: number | null; - @ApiProperty({ description: 'Asset height' }) - height!: number | null; -} + .meta({ id: 'SanitizedAssetResponseDto' }); -export class AssetResponseDto extends SanitizedAssetResponseDto { - @ApiProperty({ - type: 'string', - format: 'date-time', - description: 'The UTC timestamp when the asset was originally uploaded to Immich.', - example: '2024-01-15T20:30:00.000Z', - }) - createdAt!: string; - @ApiProperty({ description: 'Device asset ID' }) - deviceAssetId!: string; - @ApiProperty({ description: 'Device ID' }) - deviceId!: string; - @ApiProperty({ description: 'Owner user ID' }) - ownerId!: string; - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - owner?: UserResponseDto; - @ValidateUUID({ - nullable: true, - description: 'Library ID', - history: new HistoryBuilder().added('v1').deprecated('v1'), - }) - libraryId?: string | null; - @ApiProperty({ description: 'Original file path' }) - originalPath!: string; - @ApiProperty({ description: 'Original file name' }) - originalFileName!: string; - @ApiProperty({ - type: 'string', - format: 'date-time', - description: - '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.', - example: '2024-01-15T19:30:00.000Z', - }) - fileCreatedAt!: string; - @ApiProperty({ - type: 'string', - format: 'date-time', - description: - 'The UTC timestamp when the file was last modified on the filesystem. This reflects the last time the physical file was changed, which may be different from when the photo was originally taken.', - example: '2024-01-16T10:15:00.000Z', - }) - fileModifiedAt!: string; - @ApiProperty({ - type: 'string', - format: 'date-time', - description: - 'The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified.', - example: '2024-01-16T12:45:30.000Z', - }) - updatedAt!: string; - @ApiProperty({ description: 'Is favorite' }) - isFavorite!: boolean; - @ApiProperty({ description: 'Is archived' }) - isArchived!: boolean; - @ApiProperty({ description: 'Is trashed' }) - isTrashed!: boolean; - @ApiProperty({ description: 'Is offline' }) - isOffline!: boolean; - @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', description: 'Asset visibility' }) - visibility!: AssetVisibility; - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - exifInfo?: ExifResponseDto; - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - tags?: TagResponseDto[]; - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - people?: PersonWithFacesResponseDto[]; - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - unassignedFaces?: AssetFaceWithoutPersonResponseDto[]; - @ApiProperty({ description: 'Base64 encoded SHA1 hash' }) - checksum!: string; - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - stack?: AssetStackResponseDto | null; - @ApiPropertyOptional({ description: 'Duplicate group ID' }) - duplicateId?: string | null; +export class SanitizedAssetResponseDto extends createZodDto(SanitizedAssetResponseSchema) {} - @Property({ description: 'Is resized', history: new HistoryBuilder().added('v1').deprecated('v1.113.0') }) - resized?: boolean; - @Property({ description: 'Is edited', history: new HistoryBuilder().added('v2.5.0').beta('v2.5.0') }) - isEdited!: boolean; -} +const AssetStackResponseSchema = z + .object({ + id: z.string().describe('Stack ID'), + primaryAssetId: z.string().describe('Primary asset ID'), + assetCount: z.int().min(0).describe('Number of assets in stack'), + }) + .meta({ id: 'AssetStackResponseDto' }); + +export const AssetResponseSchema = SanitizedAssetResponseSchema.extend( + z.object({ + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + createdAt: z + .string() + .meta({ format: 'date-time' }) + .describe('The UTC timestamp when the asset was originally uploaded to Immich.'), + ownerId: z.string().describe('Owner user ID'), + owner: UserResponseSchema.optional(), + libraryId: z + .uuidv4() + .nullish() + .describe('Library ID') + .meta(new HistoryBuilder().added('v1').deprecated('v1').getExtensions()), + originalPath: z.string().describe('Original file path'), + originalFileName: z.string().describe('Original file name'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + fileCreatedAt: z + .string() + .meta({ format: 'date-time' }) + .describe( + '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.', + ), + fileModifiedAt: z + .string() + .meta({ format: 'date-time' }) + .describe( + 'The UTC timestamp when the file was last modified on the filesystem. This reflects the last time the physical file was changed, which may be different from when the photo was originally taken.', + ), + updatedAt: z + .string() + .meta({ format: 'date-time' }) + .describe( + 'The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified.', + ), + isFavorite: z.boolean().describe('Is favorite'), + isArchived: z.boolean().describe('Is archived'), + isTrashed: z.boolean().describe('Is trashed'), + isOffline: z.boolean().describe('Is offline'), + visibility: AssetVisibilitySchema, + exifInfo: ExifResponseSchema.optional(), + tags: z.array(TagResponseSchema).optional(), + people: z.array(PersonWithFacesResponseSchema).optional(), + unassignedFaces: z.array(AssetFaceWithoutPersonResponseSchema).optional(), + checksum: z.string().describe('Base64 encoded SHA1 hash'), + stack: AssetStackResponseSchema.nullish(), + duplicateId: z.string().nullish().describe('Duplicate group ID'), + resized: z + .boolean() + .optional() + .describe('Is resized') + .meta(new HistoryBuilder().added('v1').deprecated('v1.113.0').getExtensions()), + isEdited: z + .boolean() + .describe('Is edited') + .meta(new HistoryBuilder().added('v2.5.0').beta('v2.5.0').getExtensions()), + }).shape, +).meta({ id: 'AssetResponseDto' }); + +export class AssetResponseDto extends createZodDto(AssetResponseSchema) {} export type MapAsset = { createdAt: Date; @@ -149,8 +135,6 @@ export type MapAsset = { status: AssetStatus; checksum: Buffer; checksumAlgorithm: ChecksumAlgorithm; - deviceAssetId: string; - deviceId: string; duplicateId: string | null; duration: string | null; edits?: ShallowDehydrateObject[]; @@ -180,17 +164,6 @@ export type MapAsset = { isEdited: boolean; }; -export class AssetStackResponseDto { - @ApiProperty({ description: 'Stack ID' }) - id!: string; - - @ApiProperty({ description: 'Primary asset ID' }) - primaryAssetId!: string; - - @ApiProperty({ type: 'integer', description: 'Number of assets in stack' }) - assetCount!: number; -} - export type AssetMapOptions = { stripMetadata?: boolean; withStack?: boolean; @@ -262,10 +235,8 @@ export function mapAsset(entity: MaybeDehydrated, options: AssetMapOpt return { id: entity.id, createdAt: asDateString(entity.createdAt), - deviceAssetId: entity.deviceAssetId, ownerId: entity.ownerId, owner: entity.owner ? mapUser(entity.owner) : undefined, - deviceId: entity.deviceId, libraryId: entity.libraryId, type: entity.type, originalPath: entity.originalPath, diff --git a/server/src/dtos/asset.dto.ts b/server/src/dtos/asset.dto.ts index b7bd7a18e8..1362a86ed7 100644 --- a/server/src/dtos/asset.dto.ts +++ b/server/src/dtos/asset.dto.ts @@ -1,125 +1,66 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Transform, Type } from 'class-transformer'; -import { - IsArray, - IsDateString, - IsInt, - IsLatitude, - IsLongitude, - IsNotEmpty, - IsObject, - IsPositive, - IsString, - IsTimeZone, - Max, - Min, - ValidateIf, - ValidateNested, -} from 'class-validator'; -import { HistoryBuilder, Property } from 'src/decorators'; -import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; -import { AssetType, AssetVisibility } from 'src/enum'; +import { createZodDto } from 'nestjs-zod'; +import { HistoryBuilder } from 'src/decorators'; +import { BulkIdsSchema } from 'src/dtos/asset-ids.response.dto'; +import { AssetType, AssetVisibilitySchema } from 'src/enum'; import { AssetStats } from 'src/repositories/asset.repository'; -import { IsNotSiblingOf, Optional, ValidateBoolean, ValidateEnum, ValidateString, ValidateUUID } from 'src/validation'; +import { IsNotSiblingOf, isoDatetimeToDate, latitudeSchema, longitudeSchema, stringToBool } from 'src/validation'; +import z from 'zod'; -export class DeviceIdDto { - @ApiProperty({ description: 'Device ID' }) - @IsNotEmpty() - @IsString() - deviceId!: string; -} - -const hasGPS = (o: { latitude: undefined; longitude: undefined }) => - o.latitude !== undefined || o.longitude !== undefined; -const ValidateGPS = () => ValidateIf(hasGPS); - -export class UpdateAssetBase { - @ValidateBoolean({ optional: true, description: 'Mark as favorite' }) - isFavorite?: boolean; - - @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true, description: 'Asset visibility' }) - visibility?: AssetVisibility; - - @ApiProperty({ description: 'Original date and time' }) - @Optional() - @IsDateString() - dateTimeOriginal?: string; - - @ApiProperty({ description: 'Latitude coordinate' }) - @ValidateGPS() - @IsLatitude() - @IsNotEmpty() - latitude?: number; - - @ApiProperty({ description: 'Longitude coordinate' }) - @ValidateGPS() - @IsLongitude() - @IsNotEmpty() - longitude?: number; - - @Property({ - description: 'Rating in range [1-5], or null for unrated', - history: new HistoryBuilder() - .added('v1') - .stable('v2') - .updated('v2.6.0', 'Using -1 as a rating is deprecated and will be removed in the next major version.'), +const UpdateAssetBaseSchema = z + .object({ + isFavorite: z.boolean().optional().describe('Mark as favorite'), + visibility: AssetVisibilitySchema.optional(), + dateTimeOriginal: z.string().optional().describe('Original date and time'), + latitude: latitudeSchema.optional().describe('Latitude coordinate'), + longitude: longitudeSchema.optional().describe('Longitude coordinate'), + rating: z + .number() + .int() + .min(-1) + .max(5) + .transform((value) => (value === 0 ? null : value)) + .nullish() + .describe('Rating in range [1-5], or null for unrated') + .meta({ + ...new HistoryBuilder() + .added('v1') + .stable('v2') + .updated('v2.6.0', 'Using -1 as a rating is deprecated and will be removed in the next major version.') + .getExtensions(), + }), + description: z.string().optional().describe('Asset description'), }) - @Optional({ nullable: true }) - @IsInt() - @Max(5) - @Min(-1) - @Transform(({ value }) => (value === 0 ? null : value)) - rating?: number | null; + .refine( + (data) => + (data.latitude === undefined && data.longitude === undefined) || + (data.latitude !== undefined && data.longitude !== undefined), + { message: 'Latitude and longitude must be provided together' }, + ); - @ApiProperty({ description: 'Asset description' }) - @Optional() - @IsString() - description?: string; -} +const AssetBulkUpdateBaseSchema = UpdateAssetBaseSchema.extend({ + ids: z.array(z.uuidv4()).describe('Asset IDs to update'), + duplicateId: z.string().nullish().describe('Duplicate ID'), + dateTimeRelative: z.number().optional().describe('Relative time offset in seconds'), + timeZone: z.string().optional().describe('Time zone (IANA timezone)'), +}); -export class AssetBulkUpdateDto extends UpdateAssetBase { - @ValidateUUID({ each: true, description: 'Asset IDs to update' }) - ids!: string[]; +const AssetBulkUpdateSchema = AssetBulkUpdateBaseSchema.pipe( + IsNotSiblingOf(AssetBulkUpdateBaseSchema, 'dateTimeRelative', ['dateTimeOriginal']), +).meta({ id: 'AssetBulkUpdateDto' }); - @ValidateString({ optional: true, nullable: true, description: 'Duplicate ID' }) - duplicateId?: string | null; +const UpdateAssetSchema = UpdateAssetBaseSchema.extend({ + livePhotoVideoId: z.uuidv4().nullish().describe('Live photo video ID'), +}).meta({ id: 'UpdateAssetDto' }); - @ApiProperty({ description: 'Relative time offset in seconds' }) - @IsNotSiblingOf(['dateTimeOriginal']) - @Optional() - @IsInt() - dateTimeRelative?: number; +const AssetBulkDeleteSchema = BulkIdsSchema.extend({ + force: z.boolean().optional().describe('Force delete even if in use'), +}).meta({ id: 'AssetBulkDeleteDto' }); - @ApiProperty({ description: 'Time zone (IANA timezone)' }) - @IsNotSiblingOf(['dateTimeOriginal']) - @IsTimeZone() - @Optional() - timeZone?: string; -} - -export class UpdateAssetDto extends UpdateAssetBase { - @ValidateUUID({ optional: true, nullable: true, description: 'Live photo video ID' }) - livePhotoVideoId?: string | null; -} - -export class RandomAssetsDto { - @ApiProperty({ description: 'Number of random assets to return' }) - @Optional() - @IsInt() - @IsPositive() - @Type(() => Number) - count?: number; -} - -export class AssetBulkDeleteDto extends BulkIdsDto { - @ValidateBoolean({ optional: true, description: 'Force delete even if in use' }) - force?: boolean; -} - -export class AssetIdsDto { - @ValidateUUID({ each: true, description: 'Asset IDs' }) - assetIds!: string[]; -} +export const AssetIdsSchema = z + .object({ + assetIds: z.array(z.uuidv4()).describe('Asset IDs'), + }) + .meta({ id: 'AssetIdsDto' }); export enum AssetJobName { REFRESH_FACES = 'refresh-faces', @@ -128,137 +69,104 @@ export enum AssetJobName { TRANSCODE_VIDEO = 'transcode-video', } -export class AssetJobsDto extends AssetIdsDto { - @ValidateEnum({ enum: AssetJobName, name: 'AssetJobName', description: 'Job name' }) - name!: AssetJobName; -} +const AssetJobNameSchema = z.enum(AssetJobName).describe('Job name').meta({ id: 'AssetJobName' }); -export class AssetStatsDto { - @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', description: 'Filter by visibility', optional: true }) - visibility?: AssetVisibility; +const AssetJobsSchema = AssetIdsSchema.extend({ + name: AssetJobNameSchema, +}).meta({ id: 'AssetJobsDto' }); - @ValidateBoolean({ optional: true, description: 'Filter by favorite status' }) - isFavorite?: boolean; +const AssetStatsSchema = z + .object({ + visibility: AssetVisibilitySchema.optional(), + isFavorite: stringToBool.optional().describe('Filter by favorite status'), + isTrashed: stringToBool.optional().describe('Filter by trash status'), + }) + .meta({ id: 'AssetStatsDto' }); - @ValidateBoolean({ optional: true, description: 'Filter by trash status' }) - isTrashed?: boolean; -} +const AssetStatsResponseSchema = z + .object({ + images: z.int().describe('Number of images'), + videos: z.int().describe('Number of videos'), + total: z.int().describe('Total number of assets'), + }) + .meta({ id: 'AssetStatsResponseDto' }); -export class AssetStatsResponseDto { - @ApiProperty({ description: 'Number of images', type: 'integer' }) - images!: number; +const AssetMetadataRouteParamsSchema = z + .object({ + id: z.uuidv4().describe('Asset ID'), + key: z.string().describe('Metadata key'), + }) + .meta({ id: 'AssetMetadataRouteParams' }); - @ApiProperty({ description: 'Number of videos', type: 'integer' }) - videos!: number; +export const AssetMetadataUpsertItemSchema = z + .object({ + key: z.string().describe('Metadata key'), + value: z.record(z.string(), z.unknown()).describe('Metadata value (object)'), + }) + .meta({ id: 'AssetMetadataUpsertItemDto' }); - @ApiProperty({ description: 'Total number of assets', type: 'integer' }) - total!: number; -} +const AssetMetadataUpsertSchema = z + .object({ + items: z.array(AssetMetadataUpsertItemSchema).describe('Metadata items to upsert'), + }) + .meta({ id: 'AssetMetadataUpsertDto' }); -export class AssetMetadataRouteParams { - @ValidateUUID({ description: 'Asset ID' }) - id!: string; +const AssetMetadataBulkUpsertItemSchema = z + .object({ + assetId: z.uuidv4().describe('Asset ID'), + key: z.string().describe('Metadata key'), + value: z.record(z.string(), z.unknown()).describe('Metadata value (object)'), + }) + .meta({ id: 'AssetMetadataBulkUpsertItemDto' }); - @ValidateString({ description: 'Metadata key' }) - key!: string; -} +const AssetMetadataBulkUpsertSchema = z + .object({ + items: z.array(AssetMetadataBulkUpsertItemSchema).describe('Metadata items to upsert'), + }) + .meta({ id: 'AssetMetadataBulkUpsertDto' }); -export class AssetMetadataUpsertDto { - @ApiProperty({ description: 'Metadata items to upsert' }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => AssetMetadataUpsertItemDto) - items!: AssetMetadataUpsertItemDto[]; -} +const AssetMetadataBulkDeleteItemSchema = z + .object({ + assetId: z.uuidv4().describe('Asset ID'), + key: z.string().describe('Metadata key'), + }) + .meta({ id: 'AssetMetadataBulkDeleteItemDto' }); -export class AssetMetadataUpsertItemDto { - @ValidateString({ description: 'Metadata key' }) - key!: string; +const AssetMetadataBulkDeleteSchema = z + .object({ + items: z.array(AssetMetadataBulkDeleteItemSchema).describe('Metadata items to delete'), + }) + .meta({ id: 'AssetMetadataBulkDeleteDto' }); - @ApiProperty({ description: 'Metadata value (object)' }) - @IsObject() - value!: object; -} +const AssetMetadataResponseSchema = z + .object({ + key: z.string().describe('Metadata key'), + value: z.record(z.string(), z.unknown()).describe('Metadata value (object)'), + updatedAt: isoDatetimeToDate.describe('Last update date'), + }) + .meta({ id: 'AssetMetadataResponseDto' }); -export class AssetMetadataBulkUpsertDto { - @ApiProperty({ description: 'Metadata items to upsert' }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => AssetMetadataBulkUpsertItemDto) - items!: AssetMetadataBulkUpsertItemDto[]; -} +const AssetMetadataBulkResponseSchema = AssetMetadataResponseSchema.extend({ + assetId: z.string().describe('Asset ID'), +}).meta({ id: 'AssetMetadataBulkResponseDto' }); -export class AssetMetadataBulkUpsertItemDto { - @ValidateUUID({ description: 'Asset ID' }) - assetId!: string; +const AssetCopySchema = z + .object({ + sourceId: z.uuidv4().describe('Source asset ID'), + targetId: z.uuidv4().describe('Target asset ID'), + sharedLinks: z.boolean().default(true).optional().describe('Copy shared links'), + albums: z.boolean().default(true).optional().describe('Copy album associations'), + sidecar: z.boolean().default(true).optional().describe('Copy sidecar file'), + stack: z.boolean().default(true).optional().describe('Copy stack association'), + favorite: z.boolean().default(true).optional().describe('Copy favorite status'), + }) + .meta({ id: 'AssetCopyDto' }); - @ValidateString({ description: 'Metadata key' }) - key!: string; - - @ApiProperty({ description: 'Metadata value (object)' }) - @IsObject() - value!: object; -} - -export class AssetMetadataBulkDeleteDto { - @ApiProperty({ description: 'Metadata items to delete' }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => AssetMetadataBulkDeleteItemDto) - items!: AssetMetadataBulkDeleteItemDto[]; -} - -export class AssetMetadataBulkDeleteItemDto { - @ValidateUUID({ description: 'Asset ID' }) - assetId!: string; - - @ValidateString({ description: 'Metadata key' }) - key!: string; -} - -export class AssetMetadataResponseDto { - @ValidateString({ description: 'Metadata key' }) - key!: string; - - @ApiProperty({ description: 'Metadata value (object)' }) - value!: object; - - @ApiProperty({ description: 'Last update date' }) - updatedAt!: Date; -} - -export class AssetMetadataBulkResponseDto extends AssetMetadataResponseDto { - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; -} - -export class AssetCopyDto { - @ValidateUUID({ description: 'Source asset ID' }) - sourceId!: string; - - @ValidateUUID({ description: 'Target asset ID' }) - targetId!: string; - - @ValidateBoolean({ optional: true, description: 'Copy shared links', default: true }) - sharedLinks?: boolean; - - @ValidateBoolean({ optional: true, description: 'Copy album associations', default: true }) - albums?: boolean; - - @ValidateBoolean({ optional: true, description: 'Copy sidecar file', default: true }) - sidecar?: boolean; - - @ValidateBoolean({ optional: true, description: 'Copy stack association', default: true }) - stack?: boolean; - - @ValidateBoolean({ optional: true, description: 'Copy favorite status', default: true }) - favorite?: boolean; -} - -export class AssetDownloadOriginalDto { - @ValidateBoolean({ optional: true, description: 'Return edited asset if available', default: false }) - edited?: boolean; -} +const AssetDownloadOriginalSchema = z + .object({ + edited: stringToBool.default(false).optional().describe('Return edited asset if available'), + }) + .meta({ id: 'AssetDownloadOriginalDto' }); export const mapStats = (stats: AssetStats): AssetStatsResponseDto => { return { @@ -267,3 +175,19 @@ export const mapStats = (stats: AssetStats): AssetStatsResponseDto => { total: Object.values(stats).reduce((total, value) => total + value, 0), }; }; + +export class AssetBulkUpdateDto extends createZodDto(AssetBulkUpdateSchema) {} +export class UpdateAssetDto extends createZodDto(UpdateAssetSchema) {} +export class AssetBulkDeleteDto extends createZodDto(AssetBulkDeleteSchema) {} +export class AssetIdsDto extends createZodDto(AssetIdsSchema) {} +export class AssetJobsDto extends createZodDto(AssetJobsSchema) {} +export class AssetStatsDto extends createZodDto(AssetStatsSchema) {} +export class AssetStatsResponseDto extends createZodDto(AssetStatsResponseSchema) {} +export class AssetMetadataRouteParams extends createZodDto(AssetMetadataRouteParamsSchema) {} +export class AssetMetadataUpsertDto extends createZodDto(AssetMetadataUpsertSchema) {} +export class AssetMetadataBulkUpsertDto extends createZodDto(AssetMetadataBulkUpsertSchema) {} +export class AssetMetadataBulkDeleteDto extends createZodDto(AssetMetadataBulkDeleteSchema) {} +export class AssetMetadataResponseDto extends createZodDto(AssetMetadataResponseSchema) {} +export class AssetMetadataBulkResponseDto extends createZodDto(AssetMetadataBulkResponseSchema) {} +export class AssetCopyDto extends createZodDto(AssetCopySchema) {} +export class AssetDownloadOriginalDto extends createZodDto(AssetDownloadOriginalSchema) {} diff --git a/server/src/dtos/auth.dto.ts b/server/src/dtos/auth.dto.ts index 3df82f4ef4..95d2bb126a 100644 --- a/server/src/dtos/auth.dto.ts +++ b/server/src/dtos/auth.dto.ts @@ -1,59 +1,43 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; -import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { AuthApiKey, AuthSession, AuthSharedLink, AuthUser, UserAdmin } from 'src/database'; import { ImmichCookie, UserMetadataKey } from 'src/enum'; import { UserMetadataItem } from 'src/types'; -import { Optional, PinCode, toEmail, ValidateBoolean } from 'src/validation'; +import { toEmail } from 'src/validation'; +import z from 'zod'; export type CookieResponse = { isSecure: boolean; values: Array<{ key: ImmichCookie; value: string | null }>; }; -export class AuthDto { - @ApiProperty({ description: 'Authenticated user' }) - user!: AuthUser; +export const pinCodeRegex = /^\d{6}$/; - @ApiPropertyOptional({ description: 'API key (if authenticated via API key)' }) +export type AuthDto = { + user: AuthUser; apiKey?: AuthApiKey; - @ApiPropertyOptional({ description: 'Shared link (if authenticated via shared link)' }) sharedLink?: AuthSharedLink; - @ApiPropertyOptional({ description: 'Session (if authenticated via session)' }) session?: AuthSession; -} +}; -export class LoginCredentialDto { - @ApiProperty({ example: 'testuser@email.com', description: 'User email' }) - @IsEmail({ require_tld: false }) - @Transform(toEmail) - @IsNotEmpty() - email!: string; +const LoginCredentialSchema = z + .object({ + email: toEmail.describe('User email').meta({ example: 'testuser@email.com' }), + password: z.string().describe('User password').meta({ example: 'password' }), + }) + .meta({ id: 'LoginCredentialDto' }); - @ApiProperty({ example: 'password', description: 'User password' }) - @IsString() - @IsNotEmpty() - password!: string; -} - -export class LoginResponseDto { - @ApiProperty({ description: 'Access token' }) - accessToken!: string; - @ApiProperty({ description: 'User ID' }) - userId!: string; - @ApiProperty({ description: 'User email' }) - userEmail!: string; - @ApiProperty({ description: 'User name' }) - name!: string; - @ApiProperty({ description: 'Profile image path' }) - profileImagePath!: string; - @ApiProperty({ description: 'Is admin user' }) - isAdmin!: boolean; - @ApiProperty({ description: 'Should change password' }) - shouldChangePassword!: boolean; - @ApiProperty({ description: 'Is onboarded' }) - isOnboarded!: boolean; -} +const LoginResponseSchema = z + .object({ + accessToken: z.string().describe('Access token'), + userId: z.string().describe('User ID'), + userEmail: toEmail.describe('User email'), + name: z.string().describe('User name'), + profileImagePath: z.string().describe('Profile image path'), + isAdmin: z.boolean().describe('Is admin user'), + shouldChangePassword: z.boolean().describe('Should change password'), + isOnboarded: z.boolean().describe('Is onboarded'), + }) + .meta({ id: 'LoginResponseDto' }); export function mapLoginResponse(entity: UserAdmin, accessToken: string): LoginResponseDto { const onboardingMetadata = entity.metadata.find( @@ -72,115 +56,95 @@ export function mapLoginResponse(entity: UserAdmin, accessToken: string): LoginR }; } -export class LogoutResponseDto { - @ApiProperty({ description: 'Logout successful' }) - successful!: boolean; - @ApiProperty({ description: 'Redirect URI' }) - redirectUri!: string; -} +const LogoutResponseSchema = z + .object({ + successful: z.boolean().describe('Logout successful'), + redirectUri: z.string().describe('Redirect URI'), + }) + .meta({ id: 'LogoutResponseDto' }); -export class SignUpDto extends LoginCredentialDto { - @ApiProperty({ example: 'Admin', description: 'User name' }) - @IsString() - @IsNotEmpty() - name!: string; -} +const SignUpSchema = LoginCredentialSchema.extend({ + name: z.string().describe('User name').meta({ example: 'Admin' }), +}).meta({ id: 'SignUpDto' }); -export class ChangePasswordDto { - @ApiProperty({ example: 'password', description: 'Current password' }) - @IsString() - @IsNotEmpty() - password!: string; +const ChangePasswordSchema = z + .object({ + password: z.string().describe('Current password').meta({ example: 'password' }), + newPassword: z.string().min(8).describe('New password (min 8 characters)').meta({ example: 'password' }), + invalidateSessions: z.boolean().default(false).optional().describe('Invalidate all other sessions'), + }) + .meta({ id: 'ChangePasswordDto' }); - @ApiProperty({ example: 'password', description: 'New password (min 8 characters)' }) - @IsString() - @IsNotEmpty() - @MinLength(8) - newPassword!: string; +const PinCodeSetupSchema = z + .object({ + pinCode: z.string().regex(pinCodeRegex).describe('PIN code (4-6 digits)').meta({ example: '123456' }), + }) + .meta({ id: 'PinCodeSetupDto' }); - @ValidateBoolean({ optional: true, default: false, description: 'Invalidate all other sessions' }) - invalidateSessions?: boolean; -} +const PinCodeResetSchema = z.object({ + pinCode: z.string().regex(pinCodeRegex).optional().describe('New PIN code (4-6 digits)').meta({ example: '123456' }), + password: z + .string() + .optional() + .describe('User password (required if PIN code is not provided)') + .meta({ example: 'password' }), +}); -export class PinCodeSetupDto { - @ApiProperty({ description: 'PIN code (4-6 digits)' }) - @PinCode() - pinCode!: string; -} +const SessionUnlockSchema = PinCodeResetSchema.meta({ id: 'SessionUnlockDto' }); -export class PinCodeResetDto { - @ApiPropertyOptional({ description: 'New PIN code (4-6 digits)' }) - @PinCode({ optional: true }) - pinCode?: string; +const PinCodeChangeSchema = PinCodeResetSchema.extend({ + newPinCode: z.string().regex(pinCodeRegex).describe('New PIN code (4-6 digits)'), +}).meta({ id: 'PinCodeChangeDto' }); - @ApiPropertyOptional({ description: 'User password (required if PIN code is not provided)' }) - @Optional() - @IsString() - @IsNotEmpty() - password?: string; -} +const ValidateAccessTokenResponseSchema = z + .object({ + authStatus: z.boolean().describe('Authentication status'), + }) + .meta({ id: 'ValidateAccessTokenResponseDto' }); -export class SessionUnlockDto extends PinCodeResetDto {} +const OAuthCallbackSchema = z + .object({ + url: z.string().min(1).describe('OAuth callback URL'), + state: z.string().optional().describe('OAuth state parameter'), + codeVerifier: z.string().optional().describe('OAuth code verifier (PKCE)'), + }) + .meta({ id: 'OAuthCallbackDto' }); -export class PinCodeChangeDto extends PinCodeResetDto { - @ApiProperty({ description: 'New PIN code (4-6 digits)' }) - @PinCode() - newPinCode!: string; -} +const OAuthConfigSchema = z + .object({ + redirectUri: z.string().describe('OAuth redirect URI'), + state: z.string().optional().describe('OAuth state parameter'), + codeChallenge: z.string().optional().describe('OAuth code challenge (PKCE)'), + }) + .meta({ id: 'OAuthConfigDto' }); -export class ValidateAccessTokenResponseDto { - @ApiProperty({ description: 'Authentication status' }) - authStatus!: boolean; -} +const OAuthAuthorizeResponseSchema = z + .object({ + url: z.string().describe('OAuth authorization URL'), + }) + .meta({ id: 'OAuthAuthorizeResponseDto' }); -export class OAuthCallbackDto { - @ApiProperty({ description: 'OAuth callback URL' }) - @IsNotEmpty() - @IsString() - url!: string; +const AuthStatusResponseSchema = z + .object({ + pinCode: z.boolean().describe('Has PIN code set'), + password: z.boolean().describe('Has password set'), + isElevated: z.boolean().describe('Is elevated session'), + expiresAt: z.string().optional().describe('Session expiration date'), + pinExpiresAt: z.string().optional().describe('PIN expiration date'), + }) + .meta({ id: 'AuthStatusResponseDto' }); - @ApiPropertyOptional({ description: 'OAuth state parameter' }) - @Optional() - @IsString() - state?: string; - - @ApiPropertyOptional({ description: 'OAuth code verifier (PKCE)' }) - @Optional() - @IsString() - codeVerifier?: string; -} - -export class OAuthConfigDto { - @ApiProperty({ description: 'OAuth redirect URI' }) - @IsNotEmpty() - @IsString() - redirectUri!: string; - - @ApiPropertyOptional({ description: 'OAuth state parameter' }) - @Optional() - @IsString() - state?: string; - - @ApiPropertyOptional({ description: 'OAuth code challenge (PKCE)' }) - @Optional() - @IsString() - codeChallenge?: string; -} - -export class OAuthAuthorizeResponseDto { - @ApiProperty({ description: 'OAuth authorization URL' }) - url!: string; -} - -export class AuthStatusResponseDto { - @ApiProperty({ description: 'Has PIN code set' }) - pinCode!: boolean; - @ApiProperty({ description: 'Has password set' }) - password!: boolean; - @ApiProperty({ description: 'Is elevated session' }) - isElevated!: boolean; - @ApiPropertyOptional({ description: 'Session expiration date' }) - expiresAt?: string; - @ApiPropertyOptional({ description: 'PIN expiration date' }) - pinExpiresAt?: string; -} +export class LoginCredentialDto extends createZodDto(LoginCredentialSchema) {} +export class LoginResponseDto extends createZodDto(LoginResponseSchema) {} +export class LogoutResponseDto extends createZodDto(LogoutResponseSchema) {} +export class SignUpDto extends createZodDto(SignUpSchema) {} +export class ChangePasswordDto extends createZodDto(ChangePasswordSchema) {} +export class PinCodeSetupDto extends createZodDto(PinCodeSetupSchema) {} +export class PinCodeResetDto extends createZodDto(PinCodeResetSchema) {} +export class SessionUnlockDto extends createZodDto(SessionUnlockSchema) {} +export class PinCodeChangeDto extends createZodDto(PinCodeChangeSchema) {} +export class ValidateAccessTokenResponseDto extends createZodDto(ValidateAccessTokenResponseSchema) {} +export class OAuthCallbackDto extends createZodDto(OAuthCallbackSchema) {} +export class OAuthConfigDto extends createZodDto(OAuthConfigSchema) {} +export class OAuthAuthorizeResponseDto extends createZodDto(OAuthAuthorizeResponseSchema) {} +export class AuthStatusResponseDto extends createZodDto(AuthStatusResponseSchema) {} diff --git a/server/src/dtos/bbox.dto.ts b/server/src/dtos/bbox.dto.ts index 1afe9f53ba..8c24173791 100644 --- a/server/src/dtos/bbox.dto.ts +++ b/server/src/dtos/bbox.dto.ts @@ -1,25 +1,17 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsLatitude, IsLongitude } from 'class-validator'; -import { IsGreaterThanOrEqualTo } from 'src/validation'; +import { latitudeSchema, longitudeSchema } from 'src/validation'; +import z from 'zod'; -export class BBoxDto { - @ApiProperty({ format: 'double', description: 'West longitude (-180 to 180)' }) - @IsLongitude() - west!: number; - - @ApiProperty({ format: 'double', description: 'South latitude (-90 to 90)' }) - @IsLatitude() - south!: number; - - @ApiProperty({ - format: 'double', - description: 'East longitude (-180 to 180). May be less than west when crossing the antimeridian.', +export const BBoxSchema = z + .object({ + west: longitudeSchema.describe('West longitude (-180 to 180)'), + south: latitudeSchema.describe('South latitude (-90 to 90)'), + east: longitudeSchema.describe( + 'East longitude (-180 to 180). May be less than west when crossing the antimeridian.', + ), + north: latitudeSchema.describe('North latitude (-90 to 90). Must be >= south.'), }) - @IsLongitude() - east!: number; - - @ApiProperty({ format: 'double', description: 'North latitude (-90 to 90). Must be >= south.' }) - @IsLatitude() - @IsGreaterThanOrEqualTo('south') - north!: number; -} + .refine(({ north, south }) => north >= south, { + path: ['north'], + error: 'North latitude must be greater than or equal to south latitude', + }) + .meta({ id: 'BBoxDto' }); diff --git a/server/src/dtos/database-backup.dto.ts b/server/src/dtos/database-backup.dto.ts index c0554f83b7..34dd8f2a62 100644 --- a/server/src/dtos/database-backup.dto.ts +++ b/server/src/dtos/database-backup.dto.ts @@ -1,22 +1,32 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; +import z from 'zod'; -export class DatabaseBackupDto { - filename!: string; - filesize!: number; - timezone!: string; -} +const DatabaseBackupSchema = z + .object({ + filename: z.string().describe('Backup filename'), + filesize: z.number().describe('Backup file size'), + timezone: z.string().describe('Backup timezone'), + }) + .meta({ id: 'DatabaseBackupDto' }); -export class DatabaseBackupListResponseDto { - backups!: DatabaseBackupDto[]; -} +const DatabaseBackupListResponseSchema = z + .object({ + backups: z.array(DatabaseBackupSchema).describe('List of backups'), + }) + .meta({ id: 'DatabaseBackupListResponseDto' }); -export class DatabaseBackupUploadDto { - @ApiProperty({ type: 'string', format: 'binary', required: false }) - file?: any; -} +const DatabaseBackupUploadSchema = z + .object({ + file: z.file().optional().describe('Database backup file'), + }) + .meta({ id: 'DatabaseBackupUploadDto' }); -export class DatabaseBackupDeleteDto { - @IsString({ each: true }) - backups!: string[]; -} +const DatabaseBackupDeleteSchema = z + .object({ + backups: z.array(z.string()).describe('Backup filenames to delete'), + }) + .meta({ id: 'DatabaseBackupDeleteDto' }); + +export class DatabaseBackupListResponseDto extends createZodDto(DatabaseBackupListResponseSchema) {} +export class DatabaseBackupUploadDto extends createZodDto(DatabaseBackupUploadSchema) {} +export class DatabaseBackupDeleteDto extends createZodDto(DatabaseBackupDeleteSchema) {} diff --git a/server/src/dtos/download.dto.ts b/server/src/dtos/download.dto.ts index ef52a72bd0..b44a6a7afc 100644 --- a/server/src/dtos/download.dto.ts +++ b/server/src/dtos/download.dto.ts @@ -1,40 +1,35 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsInt, IsPositive } from 'class-validator'; -import { AssetIdsDto } from 'src/dtos/asset.dto'; -import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import { AssetIdsSchema } from 'src/dtos/asset.dto'; +import z from 'zod'; -export class DownloadInfoDto { - @ValidateUUID({ each: true, optional: true, description: 'Asset IDs to download' }) - assetIds?: string[]; +const DownloadInfoSchema = z + .object({ + assetIds: z.array(z.uuidv4()).optional().describe('Asset IDs to download'), + albumId: z.uuidv4().optional().describe('Album ID to download'), + userId: z.uuidv4().optional().describe('User ID to download assets from'), + archiveSize: z.int().min(1).optional().describe('Archive size limit in bytes'), + }) + .meta({ id: 'DownloadInfoDto' }); - @ValidateUUID({ optional: true, description: 'Album ID to download' }) - albumId?: string; +const DownloadArchiveInfoSchema = z + .object({ + size: z.int().describe('Archive size in bytes'), + assetIds: z.array(z.string()).describe('Asset IDs in this archive'), + }) + .meta({ id: 'DownloadArchiveInfo' }); - @ValidateUUID({ optional: true, description: 'User ID to download assets from' }) - userId?: string; +const DownloadResponseSchema = z + .object({ + totalSize: z.int().describe('Total size in bytes'), + archives: z.array(DownloadArchiveInfoSchema).describe('Archive information'), + }) + .meta({ id: 'DownloadResponseDto' }); - @ApiPropertyOptional({ type: 'integer', description: 'Archive size limit in bytes' }) - @IsInt() - @IsPositive() - @Optional() - archiveSize?: number; -} +const DownloadArchiveSchema = AssetIdsSchema.extend({ + edited: z.boolean().optional().describe('Download edited asset if available'), +}).meta({ id: 'DownloadArchiveDto' }); -export class DownloadResponseDto { - @ApiProperty({ type: 'integer', description: 'Total size in bytes' }) - totalSize!: number; - @ApiProperty({ description: 'Archive information' }) - archives!: DownloadArchiveInfo[]; -} - -export class DownloadArchiveInfo { - @ApiProperty({ type: 'integer', description: 'Archive size in bytes' }) - size!: number; - @ApiProperty({ description: 'Asset IDs in this archive' }) - assetIds!: string[]; -} - -export class DownloadArchiveDto extends AssetIdsDto { - @ValidateBoolean({ optional: true, description: 'Download edited asset if available' }) - edited?: boolean; -} +export class DownloadInfoDto extends createZodDto(DownloadInfoSchema) {} +export class DownloadResponseDto extends createZodDto(DownloadResponseSchema) {} +export class DownloadArchiveInfo extends createZodDto(DownloadArchiveInfoSchema) {} +export class DownloadArchiveDto extends createZodDto(DownloadArchiveSchema) {} diff --git a/server/src/dtos/duplicate.dto.ts b/server/src/dtos/duplicate.dto.ts index 40b1b74c70..55427e36aa 100644 --- a/server/src/dtos/duplicate.dto.ts +++ b/server/src/dtos/duplicate.dto.ts @@ -1,35 +1,29 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { ArrayMinSize, IsArray, ValidateNested } from 'class-validator'; -import { AssetResponseDto } from 'src/dtos/asset-response.dto'; -import { ValidateUUID } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import { AssetResponseSchema } from 'src/dtos/asset-response.dto'; +import z from 'zod'; -export class DuplicateResponseDto { - @ApiProperty({ description: 'Duplicate group ID' }) - duplicateId!: string; - @ApiProperty({ description: 'Duplicate assets' }) - assets!: AssetResponseDto[]; +const DuplicateResponseSchema = z + .object({ + duplicateId: z.string().describe('Duplicate group ID'), + assets: z.array(AssetResponseSchema).describe('Duplicate assets'), + suggestedKeepAssetIds: z.array(z.uuidv4()).describe('Suggested asset IDs to keep based on file size and EXIF data'), + }) + .meta({ id: 'DuplicateResponseDto' }); - @ValidateUUID({ each: true, description: 'Suggested asset IDs to keep based on file size and EXIF data' }) - suggestedKeepAssetIds!: string[]; -} +const DuplicateResolveGroupSchema = z + .object({ + duplicateId: z.uuidv4(), + keepAssetIds: z.array(z.uuidv4()).describe('Asset IDs to keep'), + trashAssetIds: z.array(z.uuidv4()).describe('Asset IDs to trash or delete'), + }) + .meta({ id: 'DuplicateResolveGroupDto' }); -export class DuplicateResolveGroupDto { - @ValidateUUID() - duplicateId!: string; +const DuplicateResolveSchema = z + .object({ + groups: z.array(DuplicateResolveGroupSchema).min(1).describe('List of duplicate groups to resolve'), + }) + .meta({ id: 'DuplicateResolveDto' }); - @ValidateUUID({ each: true, description: 'Asset IDs to keep' }) - keepAssetIds!: string[]; - - @ValidateUUID({ each: true, description: 'Asset IDs to trash or delete' }) - trashAssetIds!: string[]; -} - -export class DuplicateResolveDto { - @ApiProperty({ description: 'List of duplicate groups to resolve' }) - @ValidateNested({ each: true }) - @IsArray() - @Type(() => DuplicateResolveGroupDto) - @ArrayMinSize(1) - groups!: DuplicateResolveGroupDto[]; -} +export class DuplicateResponseDto extends createZodDto(DuplicateResponseSchema) {} +export class DuplicateResolveGroupDto extends createZodDto(DuplicateResolveGroupSchema) {} +export class DuplicateResolveDto extends createZodDto(DuplicateResolveSchema) {} diff --git a/server/src/dtos/editing.dto.ts b/server/src/dtos/editing.dto.ts index 8217fec41c..9f5b352195 100644 --- a/server/src/dtos/editing.dto.ts +++ b/server/src/dtos/editing.dto.ts @@ -1,7 +1,5 @@ -import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { ArrayMinSize, IsEnum, IsInt, Min, ValidateNested } from 'class-validator'; -import { IsAxisAlignedRotation, IsUniqueEditActions, ValidateEnum, ValidateUUID } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import z from 'zod'; export enum AssetEditAction { Crop = 'crop', @@ -9,103 +7,128 @@ export enum AssetEditAction { Mirror = 'mirror', } +export const AssetEditActionSchema = z + .enum(AssetEditAction) + .describe('Type of edit action to perform') + .meta({ id: 'AssetEditAction' }); + export enum MirrorAxis { Horizontal = 'horizontal', Vertical = 'vertical', } -export class CropParameters { - @IsInt() - @Min(0) - @ApiProperty({ description: 'Top-Left X coordinate of crop' }) - x!: number; +const MirrorAxisSchema = z.enum(['horizontal', 'vertical']).describe('Axis to mirror along').meta({ id: 'MirrorAxis' }); - @IsInt() - @Min(0) - @ApiProperty({ description: 'Top-Left Y coordinate of crop' }) - y!: number; - - @IsInt() - @Min(1) - @ApiProperty({ description: 'Width of the crop' }) - width!: number; - - @IsInt() - @Min(1) - @ApiProperty({ description: 'Height of the crop' }) - height!: number; -} - -export class RotateParameters { - @IsAxisAlignedRotation() - @ApiProperty({ description: 'Rotation angle in degrees' }) - angle!: number; -} - -export class MirrorParameters { - @IsEnum(MirrorAxis) - @ApiProperty({ enum: MirrorAxis, enumName: 'MirrorAxis', description: 'Axis to mirror along' }) - axis!: MirrorAxis; -} - -export type AssetEditParameters = CropParameters | RotateParameters | MirrorParameters; -export type AssetEditActionItem = - | { - action: AssetEditAction.Crop; - parameters: CropParameters; - } - | { - action: AssetEditAction.Rotate; - parameters: RotateParameters; - } - | { - action: AssetEditAction.Mirror; - parameters: MirrorParameters; - }; - -@ApiExtraModels(CropParameters, RotateParameters, MirrorParameters) -export class AssetEditActionItemDto { - @ValidateEnum({ name: 'AssetEditAction', enum: AssetEditAction, description: 'Type of edit action to perform' }) - action!: AssetEditAction; - - @ApiProperty({ - description: 'List of edit actions to apply (crop, rotate, or mirror)', - anyOf: [CropParameters, RotateParameters, MirrorParameters].map((type) => ({ - $ref: getSchemaPath(type), - })), +const CropParametersSchema = z + .object({ + x: z.number().min(0).describe('Top-Left X coordinate of crop'), + y: z.number().min(0).describe('Top-Left Y coordinate of crop'), + width: z.number().min(1).describe('Width of the crop'), + height: z.number().min(1).describe('Height of the crop'), }) - @ValidateNested() - @Type((options) => actionParameterMap[options?.object.action as keyof AssetEditActionParameter]) - parameters!: AssetEditActionItem['parameters']; -} + .meta({ id: 'CropParameters' }); -export class AssetEditActionItemResponseDto extends AssetEditActionItemDto { - @ValidateUUID() - id!: string; -} +const RotateParametersSchema = z + .object({ + angle: z + .number() + .refine((v) => [0, 90, 180, 270].includes(v), { + error: 'Angle must be one of the following values: 0, 90, 180, 270', + }) + .describe('Rotation angle in degrees'), + }) + .meta({ id: 'RotateParameters' }); + +const MirrorParametersSchema = z + .object({ + axis: MirrorAxisSchema, + }) + .meta({ id: 'MirrorParameters' }); + +// TODO: ideally we would use the discriminated union directly in the future not only for type support but also for validation and openapi generation +const __AssetEditActionItemSchema = z.discriminatedUnion('action', [ + z.object({ action: AssetEditActionSchema.extract(['Crop']), parameters: CropParametersSchema }), + z.object({ action: AssetEditActionSchema.extract(['Rotate']), parameters: RotateParametersSchema }), + z.object({ action: AssetEditActionSchema.extract(['Mirror']), parameters: MirrorParametersSchema }), +]); + +const AssetEditParametersSchema = z + .union([CropParametersSchema, RotateParametersSchema, MirrorParametersSchema], { + error: getExpectedKeysByActionMessage, + }) + .describe('List of edit actions to apply (crop, rotate, or mirror)'); -export type AssetEditActionParameter = typeof actionParameterMap; const actionParameterMap = { - [AssetEditAction.Crop]: CropParameters, - [AssetEditAction.Rotate]: RotateParameters, - [AssetEditAction.Mirror]: MirrorParameters, -}; + [AssetEditAction.Crop]: CropParametersSchema, + [AssetEditAction.Rotate]: RotateParametersSchema, + [AssetEditAction.Mirror]: MirrorParametersSchema, +} as const; -export class AssetEditsCreateDto { - @ArrayMinSize(1) - @IsUniqueEditActions() - @ValidateNested({ each: true }) - @Type(() => AssetEditActionItemDto) - @ApiProperty({ description: 'List of edit actions to apply (crop, rotate, or mirror)' }) - edits!: AssetEditActionItemDto[]; +function getExpectedKeysByActionMessage(): string { + const expectedByAction = Object.entries(actionParameterMap) + .map(([action, schema]) => `${action}: [${Object.keys(schema.shape).join(', ')}]`) + .join('; '); + + return `Invalid parameters for action, expected keys by action: ${expectedByAction}`; } -export class AssetEditsResponseDto { - @ValidateUUID({ description: 'Asset ID these edits belong to' }) - assetId!: string; +function isParametersValidForAction(edit: z.infer): boolean { + return actionParameterMap[edit.action].safeParse(edit.parameters).success; +} - @ApiProperty({ - description: 'List of edit actions applied to the asset', +const AssetEditActionItemSchema = z + .object({ + action: AssetEditActionSchema, + parameters: AssetEditParametersSchema, }) - edits!: AssetEditActionItemResponseDto[]; + .superRefine((edit, ctx) => { + if (!isParametersValidForAction(edit)) { + ctx.addIssue({ + code: 'custom', + path: ['parameters'], + message: `Invalid parameters for action '${edit.action}', expecting keys: ${Object.keys(actionParameterMap[edit.action].shape).join(', ')}`, + }); + } + }) + .meta({ id: 'AssetEditActionItemDto' }); + +export type AssetEditActionItem = z.infer; +export type AssetEditParameters = AssetEditActionItem['parameters']; + +function uniqueEditActions(edits: z.infer[]): boolean { + const keys = new Set(); + for (const edit of edits) { + const key = edit.action === 'mirror' ? `mirror-${JSON.stringify(edit.parameters)}` : edit.action; + if (keys.has(key)) { + return false; + } + keys.add(key); + } + return true; } + +const AssetEditsCreateSchema = z + .object({ + edits: z + .array(AssetEditActionItemSchema) + .min(1) + .describe('List of edit actions to apply (crop, rotate, or mirror)') + .refine(uniqueEditActions, { error: 'Duplicate edit actions are not allowed' }), + }) + .meta({ id: 'AssetEditsCreateDto' }); + +const AssetEditActionItemResponseSchema = AssetEditActionItemSchema.extend({ + id: z.uuidv4().describe('Asset edit ID'), +}).meta({ id: 'AssetEditActionItemResponseDto' }); + +const AssetEditsResponseSchema = z + .object({ + assetId: z.uuidv4().describe('Asset ID these edits belong to'), + edits: z.array(AssetEditActionItemResponseSchema).describe('List of edit actions applied to the asset'), + }) + .meta({ id: 'AssetEditsResponseDto' }); + +export class AssetEditActionItemResponseDto extends createZodDto(AssetEditActionItemResponseSchema) {} +export class AssetEditsCreateDto extends createZodDto(AssetEditsCreateSchema) {} +export class AssetEditsResponseDto extends createZodDto(AssetEditsResponseSchema) {} +export type CropParameters = z.infer; diff --git a/server/src/dtos/env.dto.ts b/server/src/dtos/env.dto.ts index bdcf3614fd..fc30875b5a 100644 --- a/server/src/dtos/env.dto.ts +++ b/server/src/dtos/env.dto.ts @@ -1,7 +1,6 @@ -import { Transform, Type } from 'class-transformer'; -import { IsEnum, IsInt, IsString, Matches } from 'class-validator'; -import { ImmichEnvironment, LogFormat, LogLevel } from 'src/enum'; -import { IsIPRange, Optional, ValidateBoolean } from 'src/validation'; +import { ImmichEnvironmentSchema, LogFormatSchema, LogLevelSchema } from 'src/enum'; +import { IsIPRange } from 'src/validation'; +import z from 'zod'; // TODO import from sql-tools once the swagger plugin supports external enums enum DatabaseSslMode { @@ -12,214 +11,80 @@ enum DatabaseSslMode { VerifyFull = 'verify-full', } -export class EnvDto { - @IsInt() - @Optional() - @Type(() => Number) - IMMICH_API_METRICS_PORT?: number; +const DatabaseSslModeSchema = z.enum(DatabaseSslMode).describe('Database SSL mode').meta({ id: 'DatabaseSslMode' }); +const absolutePath = z.string().regex(/^\//, 'Must be an absolute path').optional(); +/** + * Treat certain strings as booleans and coerce them to boolean + * Ideal for environment variables that are strings but should be treated as booleans + * @docs https://zod.dev/api?id=stringbool + */ +const stringBool = z.stringbool(); - @IsString() - @Optional() - IMMICH_BUILD_DATA?: string; - - @IsString() - @Optional() - IMMICH_BUILD?: string; - - @IsString() - @Optional() - IMMICH_BUILD_URL?: string; - - @IsString() - @Optional() - IMMICH_BUILD_IMAGE?: string; - - @IsString() - @Optional() - IMMICH_BUILD_IMAGE_URL?: string; - - @IsString() - @Optional() - IMMICH_CONFIG_FILE?: string; - - @IsString() - @Optional() - IMMICH_HELMET_FILE?: string; - - @IsEnum(ImmichEnvironment) - @Optional() - IMMICH_ENV?: ImmichEnvironment; - - @IsString() - @Optional() - IMMICH_HOST?: string; - - @ValidateBoolean({ optional: true }) - IMMICH_IGNORE_MOUNT_CHECK_ERRORS?: boolean; - - @IsEnum(LogLevel) - @Optional() - IMMICH_LOG_LEVEL?: LogLevel; - - @IsEnum(LogFormat) - @Optional() - IMMICH_LOG_FORMAT?: LogFormat; - - @Optional() - @Matches(/^\//, { message: 'IMMICH_MEDIA_LOCATION must be an absolute path' }) - IMMICH_MEDIA_LOCATION?: string; - - @IsInt() - @Optional() - @Type(() => Number) - IMMICH_MICROSERVICES_METRICS_PORT?: number; - - @ValidateBoolean({ optional: true }) - IMMICH_ALLOW_EXTERNAL_PLUGINS?: boolean; - - @Optional() - @Matches(/^\//, { message: 'IMMICH_PLUGINS_INSTALL_FOLDER must be an absolute path' }) - IMMICH_PLUGINS_INSTALL_FOLDER?: string; - - @IsInt() - @Optional() - @Type(() => Number) - IMMICH_PORT?: number; - - @IsString() - @Optional() - IMMICH_REPOSITORY?: string; - - @IsString() - @Optional() - IMMICH_REPOSITORY_URL?: string; - - @IsString() - @Optional() - IMMICH_SOURCE_REF?: string; - - @IsString() - @Optional() - IMMICH_SOURCE_COMMIT?: string; - - @IsString() - @Optional() - IMMICH_SOURCE_URL?: string; - - @IsString() - @Optional() - IMMICH_TELEMETRY_INCLUDE?: string; - - @IsString() - @Optional() - IMMICH_TELEMETRY_EXCLUDE?: string; - - @IsString() - @Optional() - IMMICH_THIRD_PARTY_SOURCE_URL?: string; - - @IsString() - @Optional() - IMMICH_THIRD_PARTY_BUG_FEATURE_URL?: string; - - @IsString() - @Optional() - IMMICH_THIRD_PARTY_DOCUMENTATION_URL?: string; - - @IsString() - @Optional() - IMMICH_THIRD_PARTY_SUPPORT_URL?: string; - - @ValidateBoolean({ optional: true }) - IMMICH_ALLOW_SETUP?: boolean; - - @IsIPRange({ requireCIDR: false }, { each: true }) - @Transform(({ value }) => - value && typeof value === 'string' - ? value +const trustedProxiesSchema = z + .string() + .optional() + .transform((s) => + s + ? s .split(',') - .map((value) => value.trim()) + .map((x) => x.trim()) .filter(Boolean) - : value, + : undefined, ) - @Optional() - IMMICH_TRUSTED_PROXIES?: string[]; - @IsString() - @Optional() - IMMICH_WORKERS_INCLUDE?: string; + .pipe(z.union([z.undefined(), IsIPRange({ requireCIDR: false })])); - @IsString() - @Optional() - IMMICH_WORKERS_EXCLUDE?: string; - - @IsString() - @Optional() - DB_DATABASE_NAME?: string; - - @IsString() - @Optional() - DB_HOSTNAME?: string; - - @IsString() - @Optional() - DB_PASSWORD?: string; - - @IsInt() - @Optional() - @Type(() => Number) - DB_PORT?: number; - - @ValidateBoolean({ optional: true }) - DB_SKIP_MIGRATIONS?: boolean; - - @IsEnum(DatabaseSslMode) - @Optional() - DB_SSL_MODE?: DatabaseSslMode; - - @IsString() - @Optional() - DB_URL?: string; - - @IsString() - @Optional() - DB_USERNAME?: string; - - @IsEnum(['pgvector', 'pgvecto.rs', 'vectorchord']) - @Optional() - DB_VECTOR_EXTENSION?: 'pgvector' | 'pgvecto.rs' | 'vectorchord'; - - @IsString() - @Optional() - NO_COLOR?: string; - - @IsString() - @Optional() - REDIS_HOSTNAME?: string; - - @IsInt() - @Optional() - @Type(() => Number) - REDIS_PORT?: number; - - @IsInt() - @Optional() - @Type(() => Number) - REDIS_DBINDEX?: number; - - @IsString() - @Optional() - REDIS_USERNAME?: string; - - @IsString() - @Optional() - REDIS_PASSWORD?: string; - - @IsString() - @Optional() - REDIS_SOCKET?: string; - - @IsString() - @Optional() - REDIS_URL?: string; -} +export const EnvSchema = z + .object({ + IMMICH_API_METRICS_PORT: z.coerce.number().int().optional(), + IMMICH_BUILD_DATA: z.string().optional(), + IMMICH_BUILD: z.string().optional(), + IMMICH_BUILD_URL: z.string().optional(), + IMMICH_BUILD_IMAGE: z.string().optional(), + IMMICH_BUILD_IMAGE_URL: z.string().optional(), + IMMICH_CONFIG_FILE: z.string().optional(), + IMMICH_HELMET_FILE: z.string().optional(), + IMMICH_ENV: ImmichEnvironmentSchema.optional(), + IMMICH_HOST: z.string().optional(), + IMMICH_IGNORE_MOUNT_CHECK_ERRORS: stringBool.optional(), + IMMICH_LOG_LEVEL: LogLevelSchema.optional(), + IMMICH_LOG_FORMAT: LogFormatSchema.optional(), + IMMICH_MEDIA_LOCATION: absolutePath, + IMMICH_MICROSERVICES_METRICS_PORT: z.coerce.number().int().optional(), + IMMICH_ALLOW_EXTERNAL_PLUGINS: stringBool.optional(), + IMMICH_PLUGINS_INSTALL_FOLDER: absolutePath, + IMMICH_PORT: z.coerce.number().int().optional(), + IMMICH_REPOSITORY: z.string().optional(), + IMMICH_REPOSITORY_URL: z.string().optional(), + IMMICH_SOURCE_REF: z.string().optional(), + IMMICH_SOURCE_COMMIT: z.string().optional(), + IMMICH_SOURCE_URL: z.string().optional(), + IMMICH_TELEMETRY_INCLUDE: z.string().optional(), + IMMICH_TELEMETRY_EXCLUDE: z.string().optional(), + IMMICH_THIRD_PARTY_SOURCE_URL: z.string().optional(), + IMMICH_THIRD_PARTY_BUG_FEATURE_URL: z.string().optional(), + IMMICH_THIRD_PARTY_DOCUMENTATION_URL: z.string().optional(), + IMMICH_THIRD_PARTY_SUPPORT_URL: z.string().optional(), + IMMICH_ALLOW_SETUP: stringBool.optional(), + IMMICH_TRUSTED_PROXIES: trustedProxiesSchema, + IMMICH_WORKERS_INCLUDE: z.string().optional(), + IMMICH_WORKERS_EXCLUDE: z.string().optional(), + DB_DATABASE_NAME: z.string().optional(), + DB_HOSTNAME: z.string().optional(), + DB_PASSWORD: z.string().optional(), + DB_PORT: z.coerce.number().int().optional(), + DB_SKIP_MIGRATIONS: stringBool.optional(), + DB_SSL_MODE: DatabaseSslModeSchema.optional(), + DB_URL: z.string().optional(), + DB_USERNAME: z.string().optional(), + DB_VECTOR_EXTENSION: z.enum(['pgvector', 'pgvecto.rs', 'vectorchord']).optional(), + NO_COLOR: z.string().optional(), + REDIS_HOSTNAME: z.string().optional(), + REDIS_PORT: z.coerce.number().int().optional(), + REDIS_DBINDEX: z.coerce.number().int().optional(), + REDIS_USERNAME: z.string().optional(), + REDIS_PASSWORD: z.string().optional(), + REDIS_SOCKET: z.string().optional(), + REDIS_URL: z.string().optional(), + }) + .meta({ id: 'EnvDto' }); diff --git a/server/src/dtos/exif.dto.ts b/server/src/dtos/exif.dto.ts index 165ecde4db..c3e1ab36c8 100644 --- a/server/src/dtos/exif.dto.ts +++ b/server/src/dtos/exif.dto.ts @@ -1,55 +1,40 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { createZodDto } from 'nestjs-zod'; import { Exif } from 'src/database'; import { MaybeDehydrated } from 'src/types'; import { asDateString } from 'src/utils/date'; +import z from 'zod'; -export class ExifResponseDto { - @ApiPropertyOptional({ description: 'Camera make' }) - make?: string | null = null; - @ApiPropertyOptional({ description: 'Camera model' }) - model?: string | null = null; - @ApiPropertyOptional({ type: 'number', description: 'Image width in pixels' }) - exifImageWidth?: number | null = null; - @ApiPropertyOptional({ type: 'number', description: 'Image height in pixels' }) - exifImageHeight?: number | null = null; +export const ExifResponseSchema = z + .object({ + make: z.string().nullish().default(null).describe('Camera make'), + model: z.string().nullish().default(null).describe('Camera model'), + exifImageWidth: z.number().min(0).nullish().default(null).describe('Image width in pixels'), + exifImageHeight: z.number().min(0).nullish().default(null).describe('Image height in pixels'), + fileSizeInByte: z.int().min(0).nullish().default(null).describe('File size in bytes'), + orientation: z.string().nullish().default(null).describe('Image orientation'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + dateTimeOriginal: z.string().meta({ format: 'date-time' }).nullish().default(null).describe('Original date/time'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + modifyDate: z.string().meta({ format: 'date-time' }).nullish().default(null).describe('Modification date/time'), + timeZone: z.string().nullish().default(null).describe('Time zone'), + lensModel: z.string().nullish().default(null).describe('Lens model'), + fNumber: z.number().nullish().default(null).describe('F-number (aperture)'), + focalLength: z.number().nullish().default(null).describe('Focal length in mm'), + iso: z.number().nullish().default(null).describe('ISO sensitivity'), + exposureTime: z.string().nullish().default(null).describe('Exposure time'), + latitude: z.number().nullish().default(null).describe('GPS latitude'), + longitude: z.number().nullish().default(null).describe('GPS longitude'), + city: z.string().nullish().default(null).describe('City name'), + state: z.string().nullish().default(null).describe('State/province name'), + country: z.string().nullish().default(null).describe('Country name'), + description: z.string().nullish().default(null).describe('Image description'), + projectionType: z.string().nullish().default(null).describe('Projection type'), + rating: z.number().nullish().default(null).describe('Rating'), + }) + .describe('EXIF response') + .meta({ id: 'ExifResponseDto' }); - @ApiProperty({ type: 'integer', format: 'int64', description: 'File size in bytes' }) - fileSizeInByte?: number | null = null; - @ApiPropertyOptional({ description: 'Image orientation' }) - orientation?: string | null = null; - @ApiPropertyOptional({ description: 'Original date/time', format: 'date-time' }) - dateTimeOriginal?: string | null = null; - @ApiPropertyOptional({ description: 'Modification date/time', format: 'date-time' }) - modifyDate?: string | null = null; - @ApiPropertyOptional({ description: 'Time zone' }) - timeZone?: string | null = null; - @ApiPropertyOptional({ description: 'Lens model' }) - lensModel?: string | null = null; - @ApiPropertyOptional({ type: 'number', description: 'F-number (aperture)' }) - fNumber?: number | null = null; - @ApiPropertyOptional({ type: 'number', description: 'Focal length in mm' }) - focalLength?: number | null = null; - @ApiPropertyOptional({ type: 'number', description: 'ISO sensitivity' }) - iso?: number | null = null; - @ApiPropertyOptional({ description: 'Exposure time' }) - exposureTime?: string | null = null; - @ApiPropertyOptional({ type: 'number', description: 'GPS latitude' }) - latitude?: number | null = null; - @ApiPropertyOptional({ type: 'number', description: 'GPS longitude' }) - longitude?: number | null = null; - @ApiPropertyOptional({ description: 'City name' }) - city?: string | null = null; - @ApiPropertyOptional({ description: 'State/province name' }) - state?: string | null = null; - @ApiPropertyOptional({ description: 'Country name' }) - country?: string | null = null; - @ApiPropertyOptional({ description: 'Image description' }) - description?: string | null = null; - @ApiPropertyOptional({ description: 'Projection type' }) - projectionType?: string | null = null; - @ApiPropertyOptional({ type: 'number', description: 'Rating' }) - rating?: number | null = null; -} +class ExifResponseDto extends createZodDto(ExifResponseSchema) {} export function mapExif(entity: MaybeDehydrated): ExifResponseDto { return { @@ -77,16 +62,3 @@ export function mapExif(entity: MaybeDehydrated): ExifResponseDto { rating: entity.rating, }; } - -export function mapSanitizedExif(entity: Exif): ExifResponseDto { - return { - fileSizeInByte: entity.fileSizeInByte ? Number.parseInt(entity.fileSizeInByte.toString()) : null, - orientation: entity.orientation, - dateTimeOriginal: asDateString(entity.dateTimeOriginal), - timeZone: entity.timeZone, - projectionType: entity.projectionType, - exifImageWidth: entity.exifImageWidth, - exifImageHeight: entity.exifImageHeight, - rating: entity.rating, - }; -} diff --git a/server/src/dtos/job.dto.ts b/server/src/dtos/job.dto.ts index ef34a41720..325dae4d2e 100644 --- a/server/src/dtos/job.dto.ts +++ b/server/src/dtos/job.dto.ts @@ -1,7 +1,11 @@ -import { ManualJobName } from 'src/enum'; -import { ValidateEnum } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import { ManualJobNameSchema } from 'src/enum'; +import z from 'zod'; -export class JobCreateDto { - @ValidateEnum({ enum: ManualJobName, name: 'ManualJobName', description: 'Job name' }) - name!: ManualJobName; -} +const JobCreateSchema = z + .object({ + name: ManualJobNameSchema, + }) + .meta({ id: 'JobCreateDto' }); + +export class JobCreateDto extends createZodDto(JobCreateSchema) {} diff --git a/server/src/dtos/library.dto.ts b/server/src/dtos/library.dto.ts index 3f71b8a0ed..aafdd9f793 100644 --- a/server/src/dtos/library.dto.ts +++ b/server/src/dtos/library.dto.ts @@ -1,58 +1,30 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { ArrayMaxSize, ArrayUnique, IsNotEmpty, IsString } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { Library } from 'src/database'; -import { Optional, ValidateUUID } from 'src/validation'; +import { isoDatetimeToDate } from 'src/validation'; +import z from 'zod'; -export class CreateLibraryDto { - @ValidateUUID({ description: 'Owner user ID' }) - ownerId!: string; +const stringArrayMax128 = z + .array(z.string()) + .max(128) + .refine((arr) => arr.every((s) => s.trim() !== ''), 'Array items must not be empty') + .refine((arr) => new Set(arr).size === arr.length, 'Array must have unique items'); - @ApiPropertyOptional({ description: 'Library name' }) - @IsString() - @Optional() - @IsNotEmpty() - name?: string; +const CreateLibrarySchema = z + .object({ + ownerId: z.uuidv4().describe('Owner user ID'), + name: z.string().min(1).optional().describe('Library name'), + importPaths: stringArrayMax128.optional().describe('Import paths (max 128)'), + exclusionPatterns: stringArrayMax128.optional().describe('Exclusion patterns (max 128)'), + }) + .meta({ id: 'CreateLibraryDto' }); - @ApiPropertyOptional({ description: 'Import paths (max 128)' }) - @Optional() - @IsString({ each: true }) - @IsNotEmpty({ each: true }) - @ArrayUnique() - @ArrayMaxSize(128) - importPaths?: string[]; - - @ApiPropertyOptional({ description: 'Exclusion patterns (max 128)' }) - @Optional() - @IsString({ each: true }) - @IsNotEmpty({ each: true }) - @ArrayUnique() - @ArrayMaxSize(128) - exclusionPatterns?: string[]; -} - -export class UpdateLibraryDto { - @ApiPropertyOptional({ description: 'Library name' }) - @Optional() - @IsString() - @IsNotEmpty() - name?: string; - - @ApiPropertyOptional({ description: 'Import paths (max 128)' }) - @Optional() - @IsString({ each: true }) - @IsNotEmpty({ each: true }) - @ArrayUnique() - @ArrayMaxSize(128) - importPaths?: string[]; - - @ApiPropertyOptional({ description: 'Exclusion patterns (max 128)' }) - @Optional() - @IsNotEmpty({ each: true }) - @IsString({ each: true }) - @ArrayUnique() - @ArrayMaxSize(128) - exclusionPatterns?: string[]; -} +const UpdateLibrarySchema = z + .object({ + name: z.string().min(1).optional().describe('Library name'), + importPaths: stringArrayMax128.optional().describe('Import paths (max 128)'), + exclusionPatterns: stringArrayMax128.optional().describe('Exclusion patterns (max 128)'), + }) + .meta({ id: 'UpdateLibraryDto' }); export interface CrawlOptionsDto { pathsToCrawl: string[]; @@ -64,81 +36,60 @@ export interface WalkOptionsDto extends CrawlOptionsDto { take: number; } -export class ValidateLibraryDto { - @ApiPropertyOptional({ description: 'Import paths to validate (max 128)' }) - @Optional() - @IsString({ each: true }) - @IsNotEmpty({ each: true }) - @ArrayUnique() - @ArrayMaxSize(128) - importPaths?: string[]; +const ValidateLibrarySchema = z + .object({ + importPaths: stringArrayMax128.optional().describe('Import paths to validate (max 128)'), + exclusionPatterns: stringArrayMax128.optional().describe('Exclusion patterns (max 128)'), + }) + .meta({ id: 'ValidateLibraryDto' }); - @ApiPropertyOptional({ description: 'Exclusion patterns (max 128)' }) - @Optional() - @IsNotEmpty({ each: true }) - @IsString({ each: true }) - @ArrayUnique() - @ArrayMaxSize(128) - exclusionPatterns?: string[]; -} +const ValidateLibraryImportPathResponseSchema = z + .object({ + importPath: z.string().describe('Import path'), + isValid: z.boolean().describe('Is valid'), + message: z.string().optional().describe('Validation message'), + }) + .meta({ id: 'ValidateLibraryImportPathResponseDto' }); -export class ValidateLibraryResponseDto { - @ApiPropertyOptional({ description: 'Validation results for import paths' }) - importPaths?: ValidateLibraryImportPathResponseDto[]; -} +const ValidateLibraryResponseSchema = z + .object({ + importPaths: z + .array(ValidateLibraryImportPathResponseSchema) + .optional() + .describe('Validation results for import paths'), + }) + .meta({ id: 'ValidateLibraryResponseDto' }); -export class ValidateLibraryImportPathResponseDto { - @ApiProperty({ description: 'Import path' }) - importPath!: string; - @ApiProperty({ description: 'Is valid' }) - isValid: boolean = false; - @ApiPropertyOptional({ description: 'Validation message' }) - message?: string; -} +const LibraryResponseSchema = z + .object({ + id: z.string().describe('Library ID'), + ownerId: z.string().describe('Owner user ID'), + name: z.string().describe('Library name'), + assetCount: z.int().describe('Number of assets'), + importPaths: z.array(z.string()).describe('Import paths'), + exclusionPatterns: z.array(z.string()).describe('Exclusion patterns'), + createdAt: isoDatetimeToDate.describe('Creation date'), + updatedAt: isoDatetimeToDate.describe('Last update date'), + refreshedAt: isoDatetimeToDate.nullable().describe('Last refresh date'), + }) + .meta({ id: 'LibraryResponseDto' }); -export class LibrarySearchDto { - @ValidateUUID({ optional: true, description: 'Filter by user ID' }) - userId?: string; -} +const LibraryStatsResponseSchema = z + .object({ + photos: z.int().describe('Number of photos'), + videos: z.int().describe('Number of videos'), + total: z.int().describe('Total number of assets'), + usage: z.int().describe('Storage usage in bytes'), + }) + .meta({ id: 'LibraryStatsResponseDto' }); -export class LibraryResponseDto { - @ApiProperty({ description: 'Library ID' }) - id!: string; - @ApiProperty({ description: 'Owner user ID' }) - ownerId!: string; - @ApiProperty({ description: 'Library name' }) - name!: string; - - @ApiProperty({ type: 'integer', description: 'Number of assets' }) - assetCount!: number; - - @ApiProperty({ description: 'Import paths' }) - importPaths!: string[]; - - @ApiProperty({ description: 'Exclusion patterns' }) - exclusionPatterns!: string[]; - - @ApiProperty({ description: 'Creation date' }) - createdAt!: Date; - @ApiProperty({ description: 'Last update date' }) - updatedAt!: Date; - @ApiProperty({ description: 'Last refresh date' }) - refreshedAt!: Date | null; -} - -export class LibraryStatsResponseDto { - @ApiProperty({ type: 'integer', description: 'Number of photos' }) - photos = 0; - - @ApiProperty({ type: 'integer', description: 'Number of videos' }) - videos = 0; - - @ApiProperty({ type: 'integer', description: 'Total number of assets' }) - total = 0; - - @ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage in bytes' }) - usage = 0; -} +export class CreateLibraryDto extends createZodDto(CreateLibrarySchema) {} +export class UpdateLibraryDto extends createZodDto(UpdateLibrarySchema) {} +export class ValidateLibraryDto extends createZodDto(ValidateLibrarySchema) {} +export class ValidateLibraryResponseDto extends createZodDto(ValidateLibraryResponseSchema) {} +export class ValidateLibraryImportPathResponseDto extends createZodDto(ValidateLibraryImportPathResponseSchema) {} +export class LibraryResponseDto extends createZodDto(LibraryResponseSchema) {} +export class LibraryStatsResponseDto extends createZodDto(LibraryStatsResponseSchema) {} export function mapLibrary(entity: Library): LibraryResponseDto { let assetCount = 0; diff --git a/server/src/dtos/license.dto.ts b/server/src/dtos/license.dto.ts index 14232940b6..a68905fb47 100644 --- a/server/src/dtos/license.dto.ts +++ b/server/src/dtos/license.dto.ts @@ -1,20 +1,12 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString, Matches } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; +import { UserLicenseSchema } from 'src/dtos/user.dto'; -export class LicenseKeyDto { - @ApiProperty({ description: 'License key (format: IM(SV|CL)(-XXXX){8})' }) - @IsString() - @IsNotEmpty() - @Matches(/IM(SV|CL)(-[\dA-Za-z]{4}){8}/) - licenseKey!: string; +const LicenseKeySchema = UserLicenseSchema.pick({ + licenseKey: true, + activationKey: true, +}).meta({ id: 'LicenseKeyDto' }); - @ApiProperty({ description: 'Activation key' }) - @IsString() - @IsNotEmpty() - activationKey!: string; -} +const LicenseResponseSchema = UserLicenseSchema.meta({ id: 'LicenseResponseDto' }); -export class LicenseResponseDto extends LicenseKeyDto { - @ApiProperty({ description: 'Activation date' }) - activatedAt!: Date; -} +export class LicenseKeyDto extends createZodDto(LicenseKeySchema) {} +export class LicenseResponseDto extends createZodDto(LicenseResponseSchema) {} diff --git a/server/src/dtos/maintenance.dto.ts b/server/src/dtos/maintenance.dto.ts index f31d9ffa23..9b1c0b63c0 100644 --- a/server/src/dtos/maintenance.dto.ts +++ b/server/src/dtos/maintenance.dto.ts @@ -1,49 +1,57 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { ValidateIf } from 'class-validator'; -import { MaintenanceAction, StorageFolder } from 'src/enum'; -import { ValidateBoolean, ValidateEnum, ValidateString } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import { MaintenanceAction, MaintenanceActionSchema, StorageFolderSchema } from 'src/enum'; +import z from 'zod'; -export class SetMaintenanceModeDto { - @ValidateEnum({ enum: MaintenanceAction, name: 'MaintenanceAction', description: 'Maintenance action' }) - action!: MaintenanceAction; +const SetMaintenanceModeSchema = z + .object({ + action: MaintenanceActionSchema, + restoreBackupFilename: z.string().optional().describe('Restore backup filename'), + }) + .refine( + (data) => data.action !== MaintenanceAction.RestoreDatabase || (data.restoreBackupFilename?.length ?? 0) > 0, + { error: 'Backup filename is required when action is restore_database', path: ['restoreBackupFilename'] }, + ) + .meta({ id: 'SetMaintenanceModeDto' }); - @ValidateIf((o) => o.action === MaintenanceAction.RestoreDatabase) - @ValidateString({ description: 'Restore backup filename' }) - restoreBackupFilename?: string; -} +const MaintenanceLoginSchema = z + .object({ + token: z.string().optional().describe('Maintenance token'), + }) + .meta({ id: 'MaintenanceLoginDto' }); -export class MaintenanceLoginDto { - @ValidateString({ optional: true, description: 'Maintenance token' }) - token?: string; -} +const MaintenanceAuthSchema = z + .object({ + username: z.string().describe('Maintenance username'), + }) + .meta({ id: 'MaintenanceAuthDto' }); -export class MaintenanceAuthDto { - @ApiProperty({ description: 'Maintenance username' }) - username!: string; -} +const MaintenanceStatusResponseSchema = z + .object({ + active: z.boolean(), + action: MaintenanceActionSchema, + progress: z.number().optional(), + task: z.string().optional(), + error: z.string().optional(), + }) + .meta({ id: 'MaintenanceStatusResponseDto' }); -export class MaintenanceStatusResponseDto { - active!: boolean; +const MaintenanceDetectInstallStorageFolderSchema = z + .object({ + folder: StorageFolderSchema, + readable: z.boolean().describe('Whether the folder is readable'), + writable: z.boolean().describe('Whether the folder is writable'), + files: z.number().describe('Number of files in the folder'), + }) + .meta({ id: 'MaintenanceDetectInstallStorageFolderDto' }); - @ValidateEnum({ enum: MaintenanceAction, name: 'MaintenanceAction', description: 'Maintenance action' }) - action!: MaintenanceAction; +const MaintenanceDetectInstallResponseSchema = z + .object({ + storage: z.array(MaintenanceDetectInstallStorageFolderSchema), + }) + .meta({ id: 'MaintenanceDetectInstallResponseDto' }); - progress?: number; - task?: string; - error?: string; -} - -export class MaintenanceDetectInstallStorageFolderDto { - @ValidateEnum({ enum: StorageFolder, name: 'StorageFolder', description: 'Storage folder' }) - folder!: StorageFolder; - @ValidateBoolean({ description: 'Whether the folder is readable' }) - readable!: boolean; - @ValidateBoolean({ description: 'Whether the folder is writable' }) - writable!: boolean; - @ApiProperty({ description: 'Number of files in the folder' }) - files!: number; -} - -export class MaintenanceDetectInstallResponseDto { - storage!: MaintenanceDetectInstallStorageFolderDto[]; -} +export class SetMaintenanceModeDto extends createZodDto(SetMaintenanceModeSchema) {} +export class MaintenanceLoginDto extends createZodDto(MaintenanceLoginSchema) {} +export class MaintenanceAuthDto extends createZodDto(MaintenanceAuthSchema) {} +export class MaintenanceStatusResponseDto extends createZodDto(MaintenanceStatusResponseSchema) {} +export class MaintenanceDetectInstallResponseDto extends createZodDto(MaintenanceDetectInstallResponseSchema) {} diff --git a/server/src/dtos/map.dto.ts b/server/src/dtos/map.dto.ts index d8db175c28..6a4776d49d 100644 --- a/server/src/dtos/map.dto.ts +++ b/server/src/dtos/map.dto.ts @@ -1,67 +1,45 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsLatitude, IsLongitude } from 'class-validator'; -import { ValidateBoolean, ValidateDate } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import { isoDatetimeToDate, latitudeSchema, longitudeSchema, stringToBool } from 'src/validation'; +import z from 'zod'; -export class MapReverseGeocodeDto { - @ApiProperty({ format: 'double', description: 'Latitude (-90 to 90)' }) - @Type(() => Number) - @IsLatitude({ message: ({ property }) => `${property} must be a number between -90 and 90` }) - lat!: number; +const MapReverseGeocodeSchema = z + .object({ + lat: z.coerce.number().meta({ format: 'double' }).pipe(latitudeSchema).describe('Latitude (-90 to 90)'), + lon: z.coerce.number().meta({ format: 'double' }).pipe(longitudeSchema).describe('Longitude (-180 to 180)'), + }) + .meta({ id: 'MapReverseGeocodeDto' }); - @ApiProperty({ format: 'double', description: 'Longitude (-180 to 180)' }) - @Type(() => Number) - @IsLongitude({ message: ({ property }) => `${property} must be a number between -180 and 180` }) - lon!: number; -} +const MapReverseGeocodeResponseSchema = z + .object({ + city: z.string().nullable().describe('City name'), + state: z.string().nullable().describe('State/Province name'), + country: z.string().nullable().describe('Country name'), + }) + .meta({ id: 'MapReverseGeocodeResponseDto' }); -export class MapReverseGeocodeResponseDto { - @ApiProperty({ description: 'City name' }) - city!: string | null; +const MapMarkerSchema = z + .object({ + isArchived: stringToBool.optional().describe('Filter by archived status'), + isFavorite: stringToBool.optional().describe('Filter by favorite status'), + fileCreatedAfter: isoDatetimeToDate.optional().describe('Filter assets created after this date'), + fileCreatedBefore: isoDatetimeToDate.optional().describe('Filter assets created before this date'), + withPartners: stringToBool.optional().describe('Include partner assets'), + withSharedAlbums: stringToBool.optional().describe('Include shared album assets'), + }) + .meta({ id: 'MapMarkerDto' }); - @ApiProperty({ description: 'State/Province name' }) - state!: string | null; +const MapMarkerResponseSchema = z + .object({ + id: z.string().describe('Asset ID'), + lat: z.number().meta({ format: 'double' }).describe('Latitude'), + lon: z.number().meta({ format: 'double' }).describe('Longitude'), + city: z.string().nullable().describe('City name'), + state: z.string().nullable().describe('State/Province name'), + country: z.string().nullable().describe('Country name'), + }) + .meta({ id: 'MapMarkerResponseDto' }); - @ApiProperty({ description: 'Country name' }) - country!: string | null; -} - -export class MapMarkerDto { - @ValidateBoolean({ optional: true, description: 'Filter by archived status' }) - isArchived?: boolean; - - @ValidateBoolean({ optional: true, description: 'Filter by favorite status' }) - isFavorite?: boolean; - - @ValidateDate({ optional: true, description: 'Filter assets created after this date' }) - fileCreatedAfter?: Date; - - @ValidateDate({ optional: true, description: 'Filter assets created before this date' }) - fileCreatedBefore?: Date; - - @ValidateBoolean({ optional: true, description: 'Include partner assets' }) - withPartners?: boolean; - - @ValidateBoolean({ optional: true, description: 'Include shared album assets' }) - withSharedAlbums?: boolean; -} - -export class MapMarkerResponseDto { - @ApiProperty({ description: 'Asset ID' }) - id!: string; - - @ApiProperty({ format: 'double', description: 'Latitude' }) - lat!: number; - - @ApiProperty({ format: 'double', description: 'Longitude' }) - lon!: number; - - @ApiProperty({ description: 'City name' }) - city!: string | null; - - @ApiProperty({ description: 'State/Province name' }) - state!: string | null; - - @ApiProperty({ description: 'Country name' }) - country!: string | null; -} +export class MapReverseGeocodeDto extends createZodDto(MapReverseGeocodeSchema) {} +export class MapReverseGeocodeResponseDto extends createZodDto(MapReverseGeocodeResponseSchema) {} +export class MapMarkerDto extends createZodDto(MapMarkerSchema) {} +export class MapMarkerResponseDto extends createZodDto(MapMarkerResponseSchema) {} diff --git a/server/src/dtos/memory.dto.ts b/server/src/dtos/memory.dto.ts index edf65ef583..334520dded 100644 --- a/server/src/dtos/memory.dto.ts +++ b/server/src/dtos/memory.dto.ts @@ -1,136 +1,87 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsInt, IsObject, IsPositive, ValidateNested } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { Memory } from 'src/database'; import { HistoryBuilder } from 'src/decorators'; -import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; +import { AssetResponseSchema, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { AssetOrderWithRandom, MemoryType } from 'src/enum'; -import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; +import { AssetOrderWithRandomSchema, MemoryType, MemoryTypeSchema } from 'src/enum'; +import { isoDatetimeToDate, stringToBool } from 'src/validation'; +import z from 'zod'; -class MemoryBaseDto { - @ValidateBoolean({ optional: true, description: 'Is memory saved' }) - isSaved?: boolean; - - @ValidateDate({ optional: true, description: 'Date when memory was seen' }) - seenAt?: Date; -} - -export class MemorySearchDto { - @ValidateEnum({ enum: MemoryType, name: 'MemoryType', description: 'Memory type', optional: true }) - type?: MemoryType; - - @ValidateDate({ optional: true, description: 'Filter by date' }) - for?: Date; - - @ValidateBoolean({ optional: true, description: 'Include trashed memories' }) - isTrashed?: boolean; - - @ValidateBoolean({ optional: true, description: 'Filter by saved status' }) - isSaved?: boolean; - - @IsInt() - @IsPositive() - @Type(() => Number) - @Optional() - @ApiProperty({ type: 'integer', description: 'Number of memories to return' }) - size?: number; - - @ValidateEnum({ enum: AssetOrderWithRandom, name: 'MemorySearchOrder', description: 'Sort order', optional: true }) - order?: AssetOrderWithRandom; -} - -class OnThisDayDto { - @ApiProperty({ type: 'number', description: 'Year for on this day memory', minimum: 1 }) - @IsInt() - @IsPositive() - year!: number; -} - -type MemoryData = OnThisDayDto; - -export class MemoryUpdateDto extends MemoryBaseDto { - @ValidateDate({ optional: true, description: 'Memory date' }) - memoryAt?: Date; -} - -export class MemoryCreateDto extends MemoryBaseDto { - @ValidateEnum({ enum: MemoryType, name: 'MemoryType', description: 'Memory type' }) - type!: MemoryType; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @IsObject() - @ValidateNested() - @Type((options) => { - switch (options?.object.type) { - case MemoryType.OnThisDay: { - return OnThisDayDto; - } - - default: { - return Object; - } - } +const MemorySearchSchema = z + .object({ + type: MemoryTypeSchema.optional(), + for: isoDatetimeToDate.optional().describe('Filter by date'), + isTrashed: stringToBool.optional().describe('Include trashed memories'), + isSaved: stringToBool.optional().describe('Filter by saved status'), + size: z.coerce.number().int().min(1).optional().describe('Number of memories to return'), + order: AssetOrderWithRandomSchema.optional(), }) - data!: MemoryData; + .meta({ id: 'MemorySearchDto' }); - @ValidateDate({ description: 'Memory date' }) - memoryAt!: Date; - - @ValidateDate({ - optional: true, - description: 'Date when memory should be shown', - history: new HistoryBuilder().added('v2.6.0').stable('v2.6.0'), +const OnThisDaySchema = z + .object({ + year: z.int().min(1000).max(9999).describe('Year for on this day memory'), }) - showAt?: Date; + .meta({ id: 'OnThisDayDto' }); - @ValidateDate({ - optional: true, - description: 'Date when memory should be hidden', - history: new HistoryBuilder().added('v2.6.0').stable('v2.6.0'), +type MemoryData = z.infer; + +const MemoryUpdateSchema = z + .object({ + isSaved: z.boolean().optional().describe('Is memory saved'), + seenAt: isoDatetimeToDate.optional().describe('Date when memory was seen'), + memoryAt: isoDatetimeToDate.optional().describe('Memory date'), }) - hideAt?: Date; + .meta({ id: 'MemoryUpdateDto' }); - @ValidateUUID({ optional: true, each: true, description: 'Asset IDs to associate with memory' }) - assetIds?: string[]; -} +const MemoryCreateSchema = z + .object({ + type: MemoryTypeSchema, + data: OnThisDaySchema, + memoryAt: isoDatetimeToDate.describe('Memory date'), + assetIds: z.array(z.uuidv4()).optional().describe('Asset IDs to associate with memory'), + isSaved: z.boolean().optional().describe('Is memory saved'), + seenAt: isoDatetimeToDate.optional().describe('Date when memory was seen'), + showAt: isoDatetimeToDate + .optional() + .describe('Date when memory should be shown') + .meta(new HistoryBuilder().added('v2.6.0').stable('v2.6.0').getExtensions()), + hideAt: isoDatetimeToDate + .optional() + .describe('Date when memory should be hidden') + .meta(new HistoryBuilder().added('v2.6.0').stable('v2.6.0').getExtensions()), + }) + .meta({ id: 'MemoryCreateDto' }); -export class MemoryStatisticsResponseDto { - @ApiProperty({ type: 'integer', description: 'Total number of memories' }) - total!: number; -} +const MemoryStatisticsResponseSchema = z + .object({ + total: z.int().describe('Total number of memories'), + }) + .meta({ id: 'MemoryStatisticsResponseDto' }); -export class MemoryResponseDto { - @ApiProperty({ description: 'Memory ID' }) - id!: string; - @ValidateDate({ description: 'Creation date' }) - createdAt!: Date; - @ValidateDate({ description: 'Last update date' }) - updatedAt!: Date; - @ValidateDate({ optional: true, description: 'Deletion date' }) - deletedAt?: Date; - @ValidateDate({ description: 'Memory date' }) - memoryAt!: Date; - @ValidateDate({ optional: true, description: 'Date when memory was seen' }) - seenAt?: Date; - @ValidateDate({ optional: true, description: 'Date when memory should be shown' }) - showAt?: Date; - @ValidateDate({ optional: true, description: 'Date when memory should be hidden' }) - hideAt?: Date; - @ApiProperty({ description: 'Owner user ID' }) - ownerId!: string; - @ValidateEnum({ enum: MemoryType, name: 'MemoryType', description: 'Memory type' }) - type!: MemoryType; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - data!: MemoryData; - @ApiProperty({ description: 'Is memory saved' }) - isSaved!: boolean; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - assets!: AssetResponseDto[]; -} +const MemoryResponseSchema = z + .object({ + id: z.string().describe('Memory ID'), + createdAt: isoDatetimeToDate.describe('Creation date'), + updatedAt: isoDatetimeToDate.describe('Last update date'), + deletedAt: isoDatetimeToDate.optional().describe('Deletion date'), + memoryAt: isoDatetimeToDate.describe('Memory date'), + seenAt: isoDatetimeToDate.optional().describe('Date when memory was seen'), + showAt: isoDatetimeToDate.optional().describe('Date when memory should be shown'), + hideAt: isoDatetimeToDate.optional().describe('Date when memory should be hidden'), + ownerId: z.string().describe('Owner user ID'), + type: MemoryTypeSchema, + data: OnThisDaySchema, + isSaved: z.boolean().describe('Is memory saved'), + assets: z.array(AssetResponseSchema), + }) + .meta({ id: 'MemoryResponseDto' }); + +export class MemorySearchDto extends createZodDto(MemorySearchSchema) {} +export class MemoryUpdateDto extends createZodDto(MemoryUpdateSchema) {} +export class MemoryCreateDto extends createZodDto(MemoryCreateSchema) {} +export class MemoryStatisticsResponseDto extends createZodDto(MemoryStatisticsResponseSchema) {} +export class MemoryResponseDto extends createZodDto(MemoryResponseSchema) {} export const mapMemory = (entity: Memory, auth: AuthDto): MemoryResponseDto => { return { diff --git a/server/src/dtos/model-config.dto.ts b/server/src/dtos/model-config.dto.ts index a75808f95a..2ba6f0c365 100644 --- a/server/src/dtos/model-config.dto.ts +++ b/server/src/dtos/model-config.dto.ts @@ -1,83 +1,57 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsNotEmpty, IsNumber, IsString, Max, Min } from 'class-validator'; -import { ValidateBoolean } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import z from 'zod'; -export class TaskConfig { - @ValidateBoolean({ description: 'Whether the task is enabled' }) - enabled!: boolean; -} - -export class ModelConfig extends TaskConfig { - @ApiProperty({ description: 'Name of the model to use' }) - @IsString() - @IsNotEmpty() - modelName!: string; -} - -export class CLIPConfig extends ModelConfig {} - -export class DuplicateDetectionConfig extends TaskConfig { - @IsNumber() - @Min(0.001) - @Max(0.1) - @Type(() => Number) - @ApiProperty({ - type: 'number', - format: 'double', - description: 'Maximum distance threshold for duplicate detection', +const TaskConfigSchema = z + .object({ + enabled: z.boolean().describe('Whether the task is enabled'), }) - maxDistance!: number; -} + .meta({ id: 'TaskConfig' }); -export class FacialRecognitionConfig extends ModelConfig { - @IsNumber() - @Min(0.1) - @Max(1) - @Type(() => Number) - @ApiProperty({ type: 'number', format: 'double', description: 'Minimum confidence score for face detection' }) - minScore!: number; +const ModelConfigSchema = TaskConfigSchema.extend({ + modelName: z.string().describe('Name of the model to use'), +}); - @IsNumber() - @Min(0.1) - @Max(2) - @Type(() => Number) - @ApiProperty({ - type: 'number', - format: 'double', - description: 'Maximum distance threshold for face recognition', - }) - maxDistance!: number; +export const CLIPConfigSchema = ModelConfigSchema.meta({ id: 'CLIPConfig' }); - @IsNumber() - @Min(1) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'Minimum number of faces required for recognition' }) - minFaces!: number; -} +export const DuplicateDetectionConfigSchema = TaskConfigSchema.extend({ + maxDistance: z + .number() + .meta({ format: 'double' }) + .min(0.001) + .max(0.1) + .describe('Maximum distance threshold for duplicate detection'), +}).meta({ id: 'DuplicateDetectionConfig' }); -export class OcrConfig extends ModelConfig { - @IsNumber() - @Min(1) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'Maximum resolution for OCR processing' }) - maxResolution!: number; +export const FacialRecognitionConfigSchema = ModelConfigSchema.extend({ + minScore: z + .number() + .meta({ format: 'double' }) + .min(0.1) + .max(1) + .describe('Minimum confidence score for face detection'), + maxDistance: z + .number() + .meta({ format: 'double' }) + .min(0.1) + .max(2) + .describe('Maximum distance threshold for face recognition'), + minFaces: z.int().min(1).describe('Minimum number of faces required for recognition'), +}).meta({ id: 'FacialRecognitionConfig' }); - @IsNumber() - @Min(0.1) - @Max(1) - @Type(() => Number) - @ApiProperty({ type: 'number', format: 'double', description: 'Minimum confidence score for text detection' }) - minDetectionScore!: number; +export const OcrConfigSchema = ModelConfigSchema.extend({ + maxResolution: z.int().min(1).describe('Maximum resolution for OCR processing'), + minDetectionScore: z + .number() + .meta({ format: 'double' }) + .min(0.1) + .max(1) + .describe('Minimum confidence score for text detection'), + minRecognitionScore: z + .number() + .meta({ format: 'double' }) + .min(0.1) + .max(1) + .describe('Minimum confidence score for text recognition'), +}).meta({ id: 'OcrConfig' }); - @IsNumber() - @Min(0.1) - @Max(1) - @Type(() => Number) - @ApiProperty({ - type: 'number', - format: 'double', - description: 'Minimum confidence score for text recognition', - }) - minRecognitionScore!: number; -} +export class CLIPConfig extends createZodDto(CLIPConfigSchema) {} diff --git a/server/src/dtos/notification.dto.ts b/server/src/dtos/notification.dto.ts index 87a15f29e3..f474cfc0a1 100644 --- a/server/src/dtos/notification.dto.ts +++ b/server/src/dtos/notification.dto.ts @@ -1,118 +1,91 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { ArrayMinSize, IsString } from 'class-validator'; -import { NotificationLevel, NotificationType } from 'src/enum'; -import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateString, ValidateUUID } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import { NotificationLevel, NotificationLevelSchema, NotificationType, NotificationTypeSchema } from 'src/enum'; +import { isoDatetimeToDate, stringToBool } from 'src/validation'; +import z from 'zod'; -export class TestEmailResponseDto { - @ApiProperty({ description: 'Email message ID' }) - messageId!: string; -} -export class TemplateResponseDto { - @ApiProperty({ description: 'Template name' }) - name!: string; - @ApiProperty({ description: 'Template HTML content' }) - html!: string; -} - -export class TemplateDto { - @ApiProperty({ description: 'Template name' }) - @IsString() - template!: string; -} - -export class NotificationDto { - @ApiProperty({ description: 'Notification ID' }) - id!: string; - @ValidateDate({ description: 'Creation date' }) - createdAt!: Date; - @ValidateEnum({ enum: NotificationLevel, name: 'NotificationLevel', description: 'Notification level' }) - level!: NotificationLevel; - @ValidateEnum({ enum: NotificationType, name: 'NotificationType', description: 'Notification type' }) - type!: NotificationType; - @ApiProperty({ description: 'Notification title' }) - title!: string; - @ApiPropertyOptional({ description: 'Notification description' }) - description?: string; - @ApiPropertyOptional({ description: 'Additional notification data' }) - data?: any; - @ApiPropertyOptional({ description: 'Date when notification was read', format: 'date-time' }) - readAt?: Date; -} - -export class NotificationSearchDto { - @ValidateUUID({ optional: true, description: 'Filter by notification ID' }) - id?: string; - - @ValidateEnum({ - enum: NotificationLevel, - name: 'NotificationLevel', - optional: true, - description: 'Filter by notification level', +const TestEmailResponseSchema = z + .object({ + messageId: z.string().describe('Email message ID'), }) - level?: NotificationLevel; + .meta({ id: 'TestEmailResponseDto' }); - @ValidateEnum({ - enum: NotificationType, - name: 'NotificationType', - optional: true, - description: 'Filter by notification type', +const TemplateResponseSchema = z + .object({ + name: z.string().describe('Template name'), + html: z.string().describe('Template HTML content'), }) - type?: NotificationType; + .meta({ id: 'TemplateResponseDto' }); - @ValidateBoolean({ optional: true, description: 'Filter by unread status' }) - unread?: boolean; -} - -export class NotificationCreateDto { - @ValidateEnum({ - enum: NotificationLevel, - name: 'NotificationLevel', - optional: true, - description: 'Notification level', +const TemplateSchema = z + .object({ + template: z.string().describe('Template name'), }) - level?: NotificationLevel; + .meta({ id: 'TemplateDto' }); - @ValidateEnum({ enum: NotificationType, name: 'NotificationType', optional: true, description: 'Notification type' }) - type?: NotificationType; +const NotificationSchema = z + .object({ + id: z.string().describe('Notification ID'), + createdAt: isoDatetimeToDate.describe('Creation date'), + level: NotificationLevelSchema, + type: NotificationTypeSchema, + title: z.string().describe('Notification title'), + description: z.string().optional().describe('Notification description'), + data: z.record(z.string(), z.unknown()).optional().describe('Additional notification data'), + readAt: isoDatetimeToDate.optional().describe('Date when notification was read'), + }) + .meta({ id: 'NotificationDto' }); - @ValidateString({ description: 'Notification title' }) - title!: string; +const NotificationSearchSchema = z + .object({ + id: z.uuidv4().optional().describe('Filter by notification ID'), + level: NotificationLevelSchema.optional(), + type: NotificationTypeSchema.optional(), + unread: stringToBool.optional().describe('Filter by unread status'), + }) + .meta({ id: 'NotificationSearchDto' }); - @ValidateString({ optional: true, nullable: true, description: 'Notification description' }) - description?: string | null; +const NotificationCreateSchema = z + .object({ + level: NotificationLevelSchema.optional(), + type: NotificationTypeSchema.optional(), + title: z.string().describe('Notification title'), + description: z.string().nullish().describe('Notification description'), + data: z.record(z.string(), z.unknown()).optional().describe('Additional notification data'), + readAt: isoDatetimeToDate.nullish().describe('Date when notification was read'), + userId: z.uuidv4().describe('User ID to send notification to'), + }) + .meta({ id: 'NotificationCreateDto' }); - @ApiPropertyOptional({ description: 'Additional notification data' }) - @Optional({ nullable: true }) - data?: any; +const NotificationUpdateSchema = z + .object({ + readAt: isoDatetimeToDate.nullish().describe('Date when notification was read'), + }) + .meta({ id: 'NotificationUpdateDto' }); - @ValidateDate({ optional: true, nullable: true, description: 'Date when notification was read' }) - readAt?: Date | null; +const NotificationUpdateAllSchema = z + .object({ + ids: z.array(z.uuidv4()).min(1).describe('Notification IDs to update'), + readAt: isoDatetimeToDate.nullish().describe('Date when notifications were read'), + }) + .meta({ id: 'NotificationUpdateAllDto' }); - @ValidateUUID({ description: 'User ID to send notification to' }) - userId!: string; -} +const NotificationDeleteAllSchema = z + .object({ + ids: z.array(z.uuidv4()).min(1).describe('Notification IDs to delete'), + }) + .meta({ id: 'NotificationDeleteAllDto' }); -export class NotificationUpdateDto { - @ValidateDate({ optional: true, nullable: true, description: 'Date when notification was read' }) - readAt?: Date | null; -} +export class TestEmailResponseDto extends createZodDto(TestEmailResponseSchema) {} +export class TemplateResponseDto extends createZodDto(TemplateResponseSchema) {} +export class TemplateDto extends createZodDto(TemplateSchema) {} +export class NotificationDto extends createZodDto(NotificationSchema) {} +export class NotificationSearchDto extends createZodDto(NotificationSearchSchema) {} +export class NotificationCreateDto extends createZodDto(NotificationCreateSchema) {} +export class NotificationUpdateDto extends createZodDto(NotificationUpdateSchema) {} +export class NotificationUpdateAllDto extends createZodDto(NotificationUpdateAllSchema) {} +export class NotificationDeleteAllDto extends createZodDto(NotificationDeleteAllSchema) {} -export class NotificationUpdateAllDto { - @ValidateUUID({ each: true, description: 'Notification IDs to update' }) - @ArrayMinSize(1) - ids!: string[]; - - @ValidateDate({ optional: true, nullable: true, description: 'Date when notifications were read' }) - readAt?: Date | null; -} - -export class NotificationDeleteAllDto { - @ValidateUUID({ each: true, description: 'Notification IDs to delete' }) - @ArrayMinSize(1) - ids!: string[]; -} - -export type MapNotification = { +type MapNotification = { id: string; createdAt: Date; updateId?: string; @@ -123,6 +96,7 @@ export type MapNotification = { description: string | null; readAt: Date | null; }; + export const mapNotification = (notification: MapNotification): NotificationDto => { return { id: notification.id, diff --git a/server/src/dtos/ocr.dto.ts b/server/src/dtos/ocr.dto.ts index 1e838d0ec0..62e32ed4af 100644 --- a/server/src/dtos/ocr.dto.ts +++ b/server/src/dtos/ocr.dto.ts @@ -1,42 +1,22 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { createZodDto } from 'nestjs-zod'; +import z from 'zod'; -export class AssetOcrResponseDto { - @ApiProperty({ type: 'string', format: 'uuid' }) - id!: string; +const AssetOcrResponseSchema = z + .object({ + assetId: z.uuidv4(), + boxScore: z.number().meta({ format: 'double' }).describe('Confidence score for text detection box'), + id: z.uuidv4(), + text: z.string().describe('Recognized text'), + textScore: z.number().meta({ format: 'double' }).describe('Confidence score for text recognition'), + x1: z.number().meta({ format: 'double' }).describe('Normalized x coordinate of box corner 1 (0-1)'), + x2: z.number().meta({ format: 'double' }).describe('Normalized x coordinate of box corner 2 (0-1)'), + x3: z.number().meta({ format: 'double' }).describe('Normalized x coordinate of box corner 3 (0-1)'), + x4: z.number().meta({ format: 'double' }).describe('Normalized x coordinate of box corner 4 (0-1)'), + y1: z.number().meta({ format: 'double' }).describe('Normalized y coordinate of box corner 1 (0-1)'), + y2: z.number().meta({ format: 'double' }).describe('Normalized y coordinate of box corner 2 (0-1)'), + y3: z.number().meta({ format: 'double' }).describe('Normalized y coordinate of box corner 3 (0-1)'), + y4: z.number().meta({ format: 'double' }).describe('Normalized y coordinate of box corner 4 (0-1)'), + }) + .meta({ id: 'AssetOcrResponseDto' }); - @ApiProperty({ type: 'string', format: 'uuid' }) - assetId!: string; - - @ApiProperty({ type: 'number', format: 'double', description: 'Normalized x coordinate of box corner 1 (0-1)' }) - x1!: number; - - @ApiProperty({ type: 'number', format: 'double', description: 'Normalized y coordinate of box corner 1 (0-1)' }) - y1!: number; - - @ApiProperty({ type: 'number', format: 'double', description: 'Normalized x coordinate of box corner 2 (0-1)' }) - x2!: number; - - @ApiProperty({ type: 'number', format: 'double', description: 'Normalized y coordinate of box corner 2 (0-1)' }) - y2!: number; - - @ApiProperty({ type: 'number', format: 'double', description: 'Normalized x coordinate of box corner 3 (0-1)' }) - x3!: number; - - @ApiProperty({ type: 'number', format: 'double', description: 'Normalized y coordinate of box corner 3 (0-1)' }) - y3!: number; - - @ApiProperty({ type: 'number', format: 'double', description: 'Normalized x coordinate of box corner 4 (0-1)' }) - x4!: number; - - @ApiProperty({ type: 'number', format: 'double', description: 'Normalized y coordinate of box corner 4 (0-1)' }) - y4!: number; - - @ApiProperty({ type: 'number', format: 'double', description: 'Confidence score for text detection box' }) - boxScore!: number; - - @ApiProperty({ type: 'number', format: 'double', description: 'Confidence score for text recognition' }) - textScore!: number; - - @ApiProperty({ type: 'string', description: 'Recognized text' }) - text!: string; -} +export class AssetOcrResponseDto extends createZodDto(AssetOcrResponseSchema) {} diff --git a/server/src/dtos/onboarding.dto.ts b/server/src/dtos/onboarding.dto.ts index d2781c6b90..ae26f5e88a 100644 --- a/server/src/dtos/onboarding.dto.ts +++ b/server/src/dtos/onboarding.dto.ts @@ -1,8 +1,10 @@ -import { ValidateBoolean } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import z from 'zod'; -export class OnboardingDto { - @ValidateBoolean({ description: 'Is user onboarded' }) - isOnboarded!: boolean; -} +const OnboardingSchema = z.object({ + isOnboarded: z.boolean().describe('Is user onboarded'), +}); + +export class OnboardingDto extends createZodDto(OnboardingSchema) {} export class OnboardingResponseDto extends OnboardingDto {} diff --git a/server/src/dtos/partner.dto.ts b/server/src/dtos/partner.dto.ts index 5b949326a4..049cf2b25e 100644 --- a/server/src/dtos/partner.dto.ts +++ b/server/src/dtos/partner.dto.ts @@ -1,26 +1,35 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsNotEmpty } from 'class-validator'; -import { UserResponseDto } from 'src/dtos/user.dto'; +import { createZodDto } from 'nestjs-zod'; +import { UserResponseSchema } from 'src/dtos/user.dto'; import { PartnerDirection } from 'src/repositories/partner.repository'; -import { ValidateEnum, ValidateUUID } from 'src/validation'; +import z from 'zod'; -export class PartnerCreateDto { - @ValidateUUID({ description: 'User ID to share with' }) - sharedWithId!: string; -} +const PartnerDirectionSchema = z.enum(PartnerDirection).describe('Partner direction').meta({ id: 'PartnerDirection' }); -export class PartnerUpdateDto { - @ApiProperty({ description: 'Show partner assets in timeline' }) - @IsNotEmpty() - inTimeline!: boolean; -} +const PartnerCreateSchema = z + .object({ + sharedWithId: z.uuidv4().describe('User ID to share with'), + }) + .meta({ id: 'PartnerCreateDto' }); -export class PartnerSearchDto { - @ValidateEnum({ enum: PartnerDirection, name: 'PartnerDirection', description: 'Partner direction' }) - direction!: PartnerDirection; -} +const PartnerUpdateSchema = z + .object({ + inTimeline: z.boolean().describe('Show partner assets in timeline'), + }) + .meta({ id: 'PartnerUpdateDto' }); -export class PartnerResponseDto extends UserResponseDto { - @ApiPropertyOptional({ description: 'Show in timeline' }) - inTimeline?: boolean; -} +const PartnerSearchSchema = z + .object({ + direction: PartnerDirectionSchema, + }) + .meta({ id: 'PartnerSearchDto' }); + +const PartnerResponseSchema = UserResponseSchema.extend({ + inTimeline: z.boolean().optional().describe('Show in timeline'), +}) + .describe('Partner response') + .meta({ id: 'PartnerResponseDto' }); + +export class PartnerCreateDto extends createZodDto(PartnerCreateSchema) {} +export class PartnerUpdateDto extends createZodDto(PartnerUpdateSchema) {} +export class PartnerSearchDto extends createZodDto(PartnerSearchSchema) {} +export class PartnerResponseDto extends createZodDto(PartnerResponseSchema) {} diff --git a/server/src/dtos/person.dto.ts b/server/src/dtos/person.dto.ts index 477166d3d5..1f8f080905 100644 --- a/server/src/dtos/person.dto.ts +++ b/server/src/dtos/person.dto.ts @@ -1,230 +1,184 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsArray, IsInt, IsNotEmpty, IsNumber, IsString, Max, Min, ValidateNested } from 'class-validator'; import { Selectable } from 'kysely'; -import { DateTime } from 'luxon'; +import { createZodDto } from 'nestjs-zod'; import { AssetFace, Person } from 'src/database'; -import { HistoryBuilder, Property } from 'src/decorators'; +import { HistoryBuilder } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetEditActionItem } from 'src/dtos/editing.dto'; -import { SourceType } from 'src/enum'; +import { SourceTypeSchema } from 'src/enum'; import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; import { ImageDimensions, MaybeDehydrated } from 'src/types'; import { asBirthDateString, asDateString } from 'src/utils/date'; import { transformFaceBoundingBox } from 'src/utils/transform'; -import { - IsDateStringFormat, - MaxDateString, - Optional, - ValidateBoolean, - ValidateEnum, - ValidateHexColor, - ValidateUUID, -} from 'src/validation'; +import { emptyStringToNull, hexColor, stringToBool } from 'src/validation'; +import z from 'zod'; -export class PersonCreateDto { - @ApiPropertyOptional({ description: 'Person name' }) - @Optional() - @IsString() - name?: string; - - // Note: the mobile app cannot currently set the birth date to null. - @ApiProperty({ format: 'date', description: 'Person date of birth', required: false }) - @MaxDateString(() => DateTime.now(), { message: 'Birth date cannot be in the future' }) - @IsDateStringFormat('yyyy-MM-dd') - @Optional({ nullable: true, emptyToNull: true }) - birthDate?: string | null; - - @ValidateBoolean({ optional: true, description: 'Person visibility (hidden)' }) - isHidden?: boolean; - - @ValidateBoolean({ optional: true, description: 'Mark as favorite' }) - isFavorite?: boolean; - - @ApiPropertyOptional({ description: 'Person color (hex)' }) - @Optional({ emptyToNull: true, nullable: true }) - @ValidateHexColor() - color?: string | null; -} - -export class PersonUpdateDto extends PersonCreateDto { - @ValidateUUID({ optional: true, description: 'Asset ID used for feature face thumbnail' }) - featureFaceAssetId?: string; -} - -export class PeopleUpdateDto { - @ApiProperty({ description: 'People to update' }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => PeopleUpdateItem) - people!: PeopleUpdateItem[]; -} - -export class PeopleUpdateItem extends PersonUpdateDto { - @ApiProperty({ description: 'Person ID' }) - @IsString() - @IsNotEmpty() - id!: string; -} - -export class MergePersonDto { - @ValidateUUID({ each: true, description: 'Person IDs to merge' }) - ids!: string[]; -} - -export class PersonSearchDto { - @ValidateBoolean({ optional: true, description: 'Include hidden people' }) - withHidden?: boolean; - @ValidateUUID({ optional: true, description: 'Closest person ID for similarity search' }) - closestPersonId?: string; - @ValidateUUID({ optional: true, description: 'Closest asset ID for similarity search' }) - closestAssetId?: string; - - @ApiPropertyOptional({ description: 'Page number for pagination', default: 1 }) - @IsInt() - @Min(1) - @Type(() => Number) - page: number = 1; - - @ApiPropertyOptional({ description: 'Number of items per page', default: 500 }) - @IsInt() - @Min(1) - @Max(1000) - @Type(() => Number) - size: number = 500; -} - -export class PersonResponseDto { - @ApiProperty({ description: 'Person ID' }) - id!: string; - @ApiProperty({ description: 'Person name' }) - name!: string; - @ApiProperty({ format: 'date', description: 'Person date of birth' }) - birthDate!: string | null; - @ApiProperty({ description: 'Thumbnail path' }) - thumbnailPath!: string; - @ApiProperty({ description: 'Is hidden' }) - isHidden!: boolean; - @Property({ - description: 'Last update date', - format: 'date-time', - history: new HistoryBuilder().added('v1.107.0').stable('v2'), +const PersonCreateSchema = z + .object({ + name: z.string().optional().describe('Person name'), + // Note: the mobile app cannot currently set the birth date to null. + birthDate: emptyStringToNull(z.string().meta({ format: 'date' }).nullable()) + .optional() + .refine((val) => (val ? new Date(val) <= new Date() : true), { error: 'Birth date cannot be in the future' }) + .describe('Person date of birth'), + isHidden: z.boolean().optional().describe('Person visibility (hidden)'), + isFavorite: z.boolean().optional().describe('Mark as favorite'), + color: emptyStringToNull(hexColor.nullable()).optional().describe('Person color (hex)'), }) - updatedAt?: string; - @Property({ description: 'Is favorite', history: new HistoryBuilder().added('v1.126.0').stable('v2') }) - isFavorite?: boolean; - @Property({ description: 'Person color (hex)', history: new HistoryBuilder().added('v1.126.0').stable('v2') }) - color?: string; -} + .meta({ id: 'PersonCreateDto' }); -export class PersonWithFacesResponseDto extends PersonResponseDto { - @ApiProperty({ description: 'Face detections' }) - faces!: AssetFaceWithoutPersonResponseDto[]; -} +const PersonUpdateSchema = PersonCreateSchema.extend({ + featureFaceAssetId: z.uuidv4().optional().describe('Asset ID used for feature face thumbnail'), +}).meta({ id: 'PersonUpdateDto' }); -export class AssetFaceWithoutPersonResponseDto { - @ValidateUUID({ description: 'Face ID' }) - id!: string; - @ApiProperty({ type: 'integer', description: 'Image height in pixels' }) - imageHeight!: number; - @ApiProperty({ type: 'integer', description: 'Image width in pixels' }) - imageWidth!: number; - @ApiProperty({ type: 'integer', description: 'Bounding box X1 coordinate' }) - boundingBoxX1!: number; - @ApiProperty({ type: 'integer', description: 'Bounding box X2 coordinate' }) - boundingBoxX2!: number; - @ApiProperty({ type: 'integer', description: 'Bounding box Y1 coordinate' }) - boundingBoxY1!: number; - @ApiProperty({ type: 'integer', description: 'Bounding box Y2 coordinate' }) - boundingBoxY2!: number; - @ValidateEnum({ enum: SourceType, name: 'SourceType', optional: true, description: 'Face detection source type' }) - sourceType?: SourceType; -} +const PeopleUpdateItemSchema = PersonUpdateSchema.extend({ + id: z.string().describe('Person ID'), +}).meta({ id: 'PeopleUpdateItem' }); -export class AssetFaceResponseDto extends AssetFaceWithoutPersonResponseDto { - @ApiProperty({ description: 'Person associated with face' }) - person!: PersonResponseDto | null; -} - -export class AssetFaceUpdateDto { - @ApiProperty({ description: 'Face update items' }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => AssetFaceUpdateItem) - data!: AssetFaceUpdateItem[]; -} - -export class FaceDto { - @ValidateUUID({ description: 'Face ID' }) - id!: string; -} - -export class AssetFaceUpdateItem { - @ValidateUUID({ description: 'Person ID' }) - personId!: string; - - @ValidateUUID({ description: 'Asset ID' }) - assetId!: string; -} - -export class AssetFaceCreateDto extends AssetFaceUpdateItem { - @ApiProperty({ type: 'integer', description: 'Image width in pixels' }) - @IsNotEmpty() - @IsNumber() - imageWidth!: number; - - @ApiProperty({ type: 'integer', description: 'Image height in pixels' }) - @IsNotEmpty() - @IsNumber() - imageHeight!: number; - - @ApiProperty({ type: 'integer', description: 'Face bounding box X coordinate' }) - @IsNotEmpty() - @IsNumber() - x!: number; - - @ApiProperty({ type: 'integer', description: 'Face bounding box Y coordinate' }) - @IsNotEmpty() - @IsNumber() - y!: number; - - @ApiProperty({ type: 'integer', description: 'Face bounding box width' }) - @IsNotEmpty() - @IsNumber() - width!: number; - - @ApiProperty({ type: 'integer', description: 'Face bounding box height' }) - @IsNotEmpty() - @IsNumber() - height!: number; -} - -export class AssetFaceDeleteDto { - @ApiProperty({ description: 'Force delete even if person has other faces' }) - @IsNotEmpty() - force!: boolean; -} - -export class PersonStatisticsResponseDto { - @ApiProperty({ type: 'integer', description: 'Number of assets' }) - assets!: number; -} - -export class PeopleResponseDto { - @ApiProperty({ type: 'integer', description: 'Total number of people' }) - total!: number; - @ApiProperty({ type: 'integer', description: 'Number of hidden people' }) - hidden!: number; - @ApiProperty({ description: 'List of people' }) - people!: PersonResponseDto[]; - - // TODO: make required after a few versions - @Property({ - description: 'Whether there are more pages', - history: new HistoryBuilder().added('v1.110.0').stable('v2'), +const PeopleUpdateSchema = z + .object({ + people: z.array(PeopleUpdateItemSchema).describe('People to update'), }) - hasNextPage?: boolean; -} + .meta({ id: 'PeopleUpdateDto' }); + +const MergePersonSchema = z + .object({ + ids: z.array(z.uuidv4()).describe('Person IDs to merge'), + }) + .meta({ id: 'MergePersonDto' }); + +const PersonSearchSchema = z + .object({ + withHidden: stringToBool.optional().describe('Include hidden people'), + closestPersonId: z.uuidv4().optional().describe('Closest person ID for similarity search'), + closestAssetId: z.uuidv4().optional().describe('Closest asset ID for similarity search'), + page: z.coerce.number().min(1).default(1).describe('Page number for pagination'), + size: z.coerce.number().min(1).max(1000).default(500).describe('Number of items per page'), + }) + .meta({ id: 'PersonSearchDto' }); + +const PersonResponseSchema = z + .object({ + id: z.string().describe('Person ID'), + name: z.string().describe('Person name'), + // TODO: use `isoDateToDate` when using `ZodSerializerDto` on the controllers. + birthDate: z.string().meta({ format: 'date' }).describe('Person date of birth').nullable(), + thumbnailPath: z.string().describe('Thumbnail path'), + isHidden: z.boolean().describe('Is hidden'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + updatedAt: z + .string() + .meta({ format: 'date-time' }) + .optional() + .describe('Last update date') + .meta(new HistoryBuilder().added('v1.107.0').stable('v2').getExtensions()), + isFavorite: z + .boolean() + .optional() + .describe('Is favorite') + .meta(new HistoryBuilder().added('v1.126.0').stable('v2').getExtensions()), + color: z + .string() + .optional() + .describe('Person color (hex)') + .meta(new HistoryBuilder().added('v1.126.0').stable('v2').getExtensions()), + }) + .meta({ id: 'PersonResponseDto' }); + +export class PersonCreateDto extends createZodDto(PersonCreateSchema) {} +export class PersonUpdateDto extends createZodDto(PersonUpdateSchema) {} +export class PeopleUpdateDto extends createZodDto(PeopleUpdateSchema) {} +export class MergePersonDto extends createZodDto(MergePersonSchema) {} +export class PersonSearchDto extends createZodDto(PersonSearchSchema) {} +export class PersonResponseDto extends createZodDto(PersonResponseSchema) {} + +export const AssetFaceWithoutPersonResponseSchema = z + .object({ + id: z.uuidv4().describe('Face ID'), + imageHeight: z.int().min(0).describe('Image height in pixels'), + imageWidth: z.int().min(0).describe('Image width in pixels'), + boundingBoxX1: z.int().describe('Bounding box X1 coordinate'), + boundingBoxX2: z.int().describe('Bounding box X2 coordinate'), + boundingBoxY1: z.int().describe('Bounding box Y1 coordinate'), + boundingBoxY2: z.int().describe('Bounding box Y2 coordinate'), + sourceType: SourceTypeSchema.optional(), + }) + .describe('Asset face without person') + .meta({ id: 'AssetFaceWithoutPersonResponseDto' }); + +class AssetFaceWithoutPersonResponseDto extends createZodDto(AssetFaceWithoutPersonResponseSchema) {} + +export const PersonWithFacesResponseSchema = PersonResponseSchema.extend({ + faces: z.array(AssetFaceWithoutPersonResponseSchema), +}).meta({ id: 'PersonWithFacesResponseDto' }); + +export class PersonWithFacesResponseDto extends createZodDto(PersonWithFacesResponseSchema) {} + +const AssetFaceResponseSchema = AssetFaceWithoutPersonResponseSchema.extend({ + person: PersonResponseSchema.nullable(), +}).meta({ id: 'AssetFaceResponseDto' }); + +export class AssetFaceResponseDto extends createZodDto(AssetFaceResponseSchema) {} + +const AssetFaceUpdateItemSchema = z + .object({ + personId: z.uuidv4().describe('Person ID'), + assetId: z.uuidv4().describe('Asset ID'), + }) + .meta({ id: 'AssetFaceUpdateItem' }); + +const AssetFaceUpdateSchema = z + .object({ + data: z.array(AssetFaceUpdateItemSchema).describe('Face update items'), + }) + .meta({ id: 'AssetFaceUpdateDto' }); + +const FaceSchema = z + .object({ + id: z.uuidv4().describe('Face ID'), + }) + .meta({ id: 'FaceDto' }); + +const AssetFaceCreateSchema = AssetFaceUpdateItemSchema.extend({ + imageWidth: z.int().describe('Image width in pixels'), + imageHeight: z.int().describe('Image height in pixels'), + x: z.int().describe('Face bounding box X coordinate'), + y: z.int().describe('Face bounding box Y coordinate'), + width: z.int().describe('Face bounding box width'), + height: z.int().describe('Face bounding box height'), +}).meta({ id: 'AssetFaceCreateDto' }); + +const AssetFaceDeleteSchema = z + .object({ + force: z.boolean().describe('Force delete even if person has other faces'), + }) + .meta({ id: 'AssetFaceDeleteDto' }); + +const PersonStatisticsResponseSchema = z + .object({ + assets: z.int().describe('Number of assets'), + }) + .meta({ id: 'PersonStatisticsResponseDto' }); + +export class AssetFaceUpdateDto extends createZodDto(AssetFaceUpdateSchema) {} +export class FaceDto extends createZodDto(FaceSchema) {} +export class AssetFaceCreateDto extends createZodDto(AssetFaceCreateSchema) {} +export class AssetFaceDeleteDto extends createZodDto(AssetFaceDeleteSchema) {} +export class PersonStatisticsResponseDto extends createZodDto(PersonStatisticsResponseSchema) {} + +const PeopleResponseSchema = z + .object({ + total: z.int().min(0).describe('Total number of people'), + hidden: z.int().min(0).describe('Number of hidden people'), + people: z.array(PersonResponseSchema), + // TODO: make required after a few versions + hasNextPage: z + .boolean() + .optional() + .describe('Whether there are more pages') + .meta(new HistoryBuilder().added('v1.110.0').stable('v2').getExtensions()), + }) + .describe('People response'); +export class PeopleResponseDto extends createZodDto(PeopleResponseSchema) {} export function mapPerson(person: MaybeDehydrated): PersonResponseDto { return { diff --git a/server/src/dtos/plugin-manifest.dto.ts b/server/src/dtos/plugin-manifest.dto.ts index d5d1c52997..30aa8c0a68 100644 --- a/server/src/dtos/plugin-manifest.dto.ts +++ b/server/src/dtos/plugin-manifest.dto.ts @@ -1,128 +1,56 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { - ArrayMinSize, - IsArray, - IsEnum, - IsNotEmpty, - IsObject, - IsOptional, - IsSemVer, - IsString, - Matches, - ValidateNested, -} from 'class-validator'; -import { PluginContext } from 'src/enum'; -import { JSONSchema } from 'src/types/plugin-schema.types'; -import { ValidateEnum } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import { PluginContextSchema } from 'src/enum'; +import { JSONSchemaSchema } from 'src/types/plugin-schema.types'; +import z from 'zod'; -class PluginManifestWasmDto { - @ApiProperty({ description: 'WASM file path' }) - @IsString() - @IsNotEmpty() - path!: string; -} +const pluginNameRegex = /^[a-z0-9-]+[a-z0-9]$/; +const semverRegex = + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/; -class PluginManifestFilterDto { - @ApiProperty({ description: 'Filter method name' }) - @IsString() - @IsNotEmpty() - methodName!: string; - - @ApiProperty({ description: 'Filter title' }) - @IsString() - @IsNotEmpty() - title!: string; - - @ApiProperty({ description: 'Filter description' }) - @IsString() - @IsNotEmpty() - description!: string; - - @ApiProperty({ description: 'Supported contexts', enum: PluginContext, isArray: true }) - @IsArray() - @ArrayMinSize(1) - @IsEnum(PluginContext, { each: true }) - supportedContexts!: PluginContext[]; - - @ApiPropertyOptional({ description: 'Filter schema' }) - @IsObject() - @IsOptional() - schema?: JSONSchema; -} - -class PluginManifestActionDto { - @ApiProperty({ description: 'Action method name' }) - @IsString() - @IsNotEmpty() - methodName!: string; - - @ApiProperty({ description: 'Action title' }) - @IsString() - @IsNotEmpty() - title!: string; - - @ApiProperty({ description: 'Action description' }) - @IsString() - @IsNotEmpty() - description!: string; - - @ArrayMinSize(1) - @ValidateEnum({ enum: PluginContext, name: 'PluginContext', each: true, description: 'Supported contexts' }) - supportedContexts!: PluginContext[]; - - @ApiPropertyOptional({ description: 'Action schema' }) - @IsObject() - @IsOptional() - schema?: JSONSchema; -} - -export class PluginManifestDto { - @ApiProperty({ description: 'Plugin name (lowercase, numbers, hyphens only)' }) - @IsString() - @IsNotEmpty() - @Matches(/^[a-z0-9-]+[a-z0-9]$/, { - message: 'Plugin name must contain only lowercase letters, numbers, and hyphens, and cannot end with a hyphen', +const PluginManifestWasmSchema = z + .object({ + path: z.string().describe('WASM file path'), }) - name!: string; + .meta({ id: 'PluginManifestWasmDto' }); - @ApiProperty({ description: 'Plugin version (semver)' }) - @IsString() - @IsNotEmpty() - @IsSemVer() - version!: string; +const PluginManifestFilterSchema = z + .object({ + methodName: z.string().describe('Filter method name'), + title: z.string().describe('Filter title'), + description: z.string().describe('Filter description'), + supportedContexts: z.array(PluginContextSchema).min(1).describe('Supported contexts'), + schema: JSONSchemaSchema.optional(), + }) + .meta({ id: 'PluginManifestFilterDto' }); - @ApiProperty({ description: 'Plugin title' }) - @IsString() - @IsNotEmpty() - title!: string; +const PluginManifestActionSchema = z + .object({ + methodName: z.string().describe('Action method name'), + title: z.string().describe('Action title'), + description: z.string().describe('Action description'), + supportedContexts: z.array(PluginContextSchema).min(1).describe('Supported contexts'), + schema: JSONSchemaSchema.optional(), + }) + .meta({ id: 'PluginManifestActionDto' }); - @ApiProperty({ description: 'Plugin description' }) - @IsString() - @IsNotEmpty() - description!: string; +export const PluginManifestSchema = z + .object({ + name: z + .string() + .min(1) + .regex( + pluginNameRegex, + 'Plugin name must contain only lowercase letters, numbers, and hyphens, and cannot end with a hyphen', + ) + .describe('Plugin name (lowercase, numbers, hyphens only)'), + version: z.string().regex(semverRegex).describe('Plugin version (semver)'), + title: z.string().describe('Plugin title'), + description: z.string().describe('Plugin description'), + author: z.string().describe('Plugin author'), + wasm: PluginManifestWasmSchema, + filters: z.array(PluginManifestFilterSchema).optional().describe('Plugin filters'), + actions: z.array(PluginManifestActionSchema).optional().describe('Plugin actions'), + }) + .meta({ id: 'PluginManifestDto' }); - @ApiProperty({ description: 'Plugin author' }) - @IsString() - @IsNotEmpty() - author!: string; - - @ApiProperty({ description: 'WASM configuration' }) - @ValidateNested() - @Type(() => PluginManifestWasmDto) - wasm!: PluginManifestWasmDto; - - @ApiPropertyOptional({ description: 'Plugin filters' }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => PluginManifestFilterDto) - @IsOptional() - filters?: PluginManifestFilterDto[]; - - @ApiPropertyOptional({ description: 'Plugin actions' }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => PluginManifestActionDto) - @IsOptional() - actions?: PluginManifestActionDto[]; -} +export class PluginManifestDto extends createZodDto(PluginManifestSchema) {} diff --git a/server/src/dtos/plugin.dto.ts b/server/src/dtos/plugin.dto.ts index de1f1b28d4..2f928841cb 100644 --- a/server/src/dtos/plugin.dto.ts +++ b/server/src/dtos/plugin.dto.ts @@ -1,84 +1,59 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { PluginAction, PluginFilter } from 'src/database'; -import { PluginContext as PluginContextType, PluginTriggerType } from 'src/enum'; -import type { JSONSchema } from 'src/types/plugin-schema.types'; -import { ValidateEnum } from 'src/validation'; +import { PluginContextSchema, PluginTriggerTypeSchema } from 'src/enum'; +import { JSONSchemaSchema } from 'src/types/plugin-schema.types'; +import z from 'zod'; -export class PluginTriggerResponseDto { - @ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType', description: 'Trigger type' }) - type!: PluginTriggerType; - @ValidateEnum({ enum: PluginContextType, name: 'PluginContextType', description: 'Context type' }) - contextType!: PluginContextType; -} +const PluginTriggerResponseSchema = z + .object({ + type: PluginTriggerTypeSchema, + contextType: PluginContextSchema, + }) + .meta({ id: 'PluginTriggerResponseDto' }); -export class PluginResponseDto { - @ApiProperty({ description: 'Plugin ID' }) - id!: string; - @ApiProperty({ description: 'Plugin name' }) - name!: string; - @ApiProperty({ description: 'Plugin title' }) - title!: string; - @ApiProperty({ description: 'Plugin description' }) - description!: string; - @ApiProperty({ description: 'Plugin author' }) - author!: string; - @ApiProperty({ description: 'Plugin version' }) - version!: string; - @ApiProperty({ description: 'Creation date' }) - createdAt!: string; - @ApiProperty({ description: 'Last update date' }) - updatedAt!: string; - @ApiProperty({ description: 'Plugin filters' }) - filters!: PluginFilterResponseDto[]; - @ApiProperty({ description: 'Plugin actions' }) - actions!: PluginActionResponseDto[]; -} +const PluginFilterResponseSchema = z + .object({ + id: z.string().describe('Filter ID'), + pluginId: z.string().describe('Plugin ID'), + methodName: z.string().describe('Method name'), + title: z.string().describe('Filter title'), + description: z.string().describe('Filter description'), + supportedContexts: z.array(PluginContextSchema).describe('Supported contexts'), + schema: JSONSchemaSchema.nullable().describe('Filter schema'), + }) + .meta({ id: 'PluginFilterResponseDto' }); -export class PluginFilterResponseDto { - @ApiProperty({ description: 'Filter ID' }) - id!: string; - @ApiProperty({ description: 'Plugin ID' }) - pluginId!: string; - @ApiProperty({ description: 'Method name' }) - methodName!: string; - @ApiProperty({ description: 'Filter title' }) - title!: string; - @ApiProperty({ description: 'Filter description' }) - description!: string; +const PluginActionResponseSchema = z + .object({ + id: z.string().describe('Action ID'), + pluginId: z.string().describe('Plugin ID'), + methodName: z.string().describe('Method name'), + title: z.string().describe('Action title'), + description: z.string().describe('Action description'), + supportedContexts: z.array(PluginContextSchema).describe('Supported contexts'), + schema: JSONSchemaSchema.nullable().describe('Action schema'), + }) + .meta({ id: 'PluginActionResponseDto' }); - @ValidateEnum({ enum: PluginContextType, name: 'PluginContextType', each: true, description: 'Supported contexts' }) - supportedContexts!: PluginContextType[]; - @ApiProperty({ description: 'Filter schema' }) - schema!: JSONSchema | null; -} +const PluginResponseSchema = z + .object({ + id: z.string().describe('Plugin ID'), + name: z.string().describe('Plugin name'), + title: z.string().describe('Plugin title'), + description: z.string().describe('Plugin description'), + author: z.string().describe('Plugin author'), + version: z.string().describe('Plugin version'), + createdAt: z.string().describe('Creation date'), + updatedAt: z.string().describe('Last update date'), + filters: z.array(PluginFilterResponseSchema).describe('Plugin filters'), + actions: z.array(PluginActionResponseSchema).describe('Plugin actions'), + }) + .meta({ id: 'PluginResponseDto' }); -export class PluginActionResponseDto { - @ApiProperty({ description: 'Action ID' }) - id!: string; - @ApiProperty({ description: 'Plugin ID' }) - pluginId!: string; - @ApiProperty({ description: 'Method name' }) - methodName!: string; - @ApiProperty({ description: 'Action title' }) - title!: string; - @ApiProperty({ description: 'Action description' }) - description!: string; +export class PluginTriggerResponseDto extends createZodDto(PluginTriggerResponseSchema) {} +export class PluginResponseDto extends createZodDto(PluginResponseSchema) {} - @ValidateEnum({ enum: PluginContextType, name: 'PluginContextType', each: true, description: 'Supported contexts' }) - supportedContexts!: PluginContextType[]; - @ApiProperty({ description: 'Action schema' }) - schema!: JSONSchema | null; -} - -export class PluginInstallDto { - @ApiProperty({ description: 'Path to plugin manifest file' }) - @IsString() - @IsNotEmpty() - manifestPath!: string; -} - -export type MapPlugin = { +type MapPlugin = { id: string; name: string; title: string; diff --git a/server/src/dtos/queue-legacy.dto.ts b/server/src/dtos/queue-legacy.dto.ts index 993160a03b..dbbcec2da5 100644 --- a/server/src/dtos/queue-legacy.dto.ts +++ b/server/src/dtos/queue-legacy.dto.ts @@ -1,79 +1,47 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { QueueResponseDto, QueueStatisticsDto } from 'src/dtos/queue.dto'; +import { createZodDto } from 'nestjs-zod'; +import { QueueResponseDto, QueueStatisticsSchema } from 'src/dtos/queue.dto'; import { QueueName } from 'src/enum'; +import z from 'zod'; -export class QueueStatusLegacyDto { - @ApiProperty({ description: 'Whether the queue is currently active (has running jobs)' }) - isActive!: boolean; - @ApiProperty({ description: 'Whether the queue is paused' }) - isPaused!: boolean; -} +const QueueStatusLegacySchema = z + .object({ + isActive: z.boolean().describe('Whether the queue is currently active (has running jobs)'), + isPaused: z.boolean().describe('Whether the queue is paused'), + }) + .meta({ id: 'QueueStatusLegacyDto' }); -export class QueueResponseLegacyDto { - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - queueStatus!: QueueStatusLegacyDto; +const QueueResponseLegacySchema = z + .object({ + queueStatus: QueueStatusLegacySchema, + jobCounts: QueueStatisticsSchema, + }) + .meta({ id: 'QueueResponseLegacyDto' }); - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - jobCounts!: QueueStatisticsDto; -} +const QueuesResponseLegacySchema = z + .object({ + [QueueName.ThumbnailGeneration]: QueueResponseLegacySchema, + [QueueName.MetadataExtraction]: QueueResponseLegacySchema, + [QueueName.VideoConversion]: QueueResponseLegacySchema, + [QueueName.SmartSearch]: QueueResponseLegacySchema, + [QueueName.StorageTemplateMigration]: QueueResponseLegacySchema, + [QueueName.Migration]: QueueResponseLegacySchema, + [QueueName.BackgroundTask]: QueueResponseLegacySchema, + [QueueName.Search]: QueueResponseLegacySchema, + [QueueName.DuplicateDetection]: QueueResponseLegacySchema, + [QueueName.FaceDetection]: QueueResponseLegacySchema, + [QueueName.FacialRecognition]: QueueResponseLegacySchema, + [QueueName.Sidecar]: QueueResponseLegacySchema, + [QueueName.Library]: QueueResponseLegacySchema, + [QueueName.Notification]: QueueResponseLegacySchema, + [QueueName.BackupDatabase]: QueueResponseLegacySchema, + [QueueName.Ocr]: QueueResponseLegacySchema, + [QueueName.Workflow]: QueueResponseLegacySchema, + [QueueName.Editor]: QueueResponseLegacySchema, + }) + .meta({ id: 'QueuesResponseLegacyDto' }); -export class QueuesResponseLegacyDto implements Record { - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.ThumbnailGeneration]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.MetadataExtraction]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.VideoConversion]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.SmartSearch]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.StorageTemplateMigration]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.Migration]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.BackgroundTask]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.Search]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.DuplicateDetection]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.FaceDetection]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.FacialRecognition]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.Sidecar]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.Library]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.Notification]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.BackupDatabase]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.Ocr]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.Workflow]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.Editor]!: QueueResponseLegacyDto; -} +export class QueueResponseLegacyDto extends createZodDto(QueueResponseLegacySchema) {} +export class QueuesResponseLegacyDto extends createZodDto(QueuesResponseLegacySchema) {} export const mapQueueLegacy = (response: QueueResponseDto): QueueResponseLegacyDto => { return { diff --git a/server/src/dtos/queue.dto.ts b/server/src/dtos/queue.dto.ts index 7893581444..2147f60bde 100644 --- a/server/src/dtos/queue.dto.ts +++ b/server/src/dtos/queue.dto.ts @@ -1,82 +1,76 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { createZodDto } from 'nestjs-zod'; import { HistoryBuilder } from 'src/decorators'; -import { JobName, QueueCommand, QueueJobStatus, QueueName } from 'src/enum'; -import { ValidateBoolean, ValidateEnum } from 'src/validation'; +import { JobNameSchema, QueueCommandSchema, QueueJobStatusSchema, QueueNameSchema } from 'src/enum'; +import z from 'zod'; -export class QueueNameParamDto { - @ValidateEnum({ enum: QueueName, name: 'QueueName', description: 'Queue name' }) - name!: QueueName; -} - -export class QueueCommandDto { - @ValidateEnum({ enum: QueueCommand, name: 'QueueCommand', description: 'Queue command to execute' }) - command!: QueueCommand; - - @ValidateBoolean({ optional: true, description: 'Force the command execution (if applicable)' }) - force?: boolean; // TODO: this uses undefined as a third state, which should be refactored to be more explicit -} - -export class QueueUpdateDto { - @ValidateBoolean({ optional: true, description: 'Whether to pause the queue' }) - isPaused?: boolean; -} - -export class QueueDeleteDto { - @ValidateBoolean({ - optional: true, - description: 'If true, will also remove failed jobs from the queue.', - history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'), +const QueueNameParamSchema = z + .object({ + name: QueueNameSchema, }) - failed?: boolean; -} + .meta({ id: 'QueueNameParamDto' }); -export class QueueJobSearchDto { - @ValidateEnum({ - enum: QueueJobStatus, - name: 'QueueJobStatus', - optional: true, - each: true, - description: 'Filter jobs by status', +const QueueCommandSchemaDto = z + .object({ + command: QueueCommandSchema, + force: z.boolean().optional().describe('Force the command execution (if applicable)'), }) - status?: QueueJobStatus[]; -} -export class QueueJobResponseDto { - @ApiPropertyOptional({ description: 'Job ID' }) - id?: string; + .meta({ id: 'QueueCommandDto' }); - @ValidateEnum({ enum: JobName, name: 'JobName', description: 'Job name' }) - name!: JobName; +const QueueUpdateSchema = z + .object({ + isPaused: z.boolean().optional().describe('Whether to pause the queue'), + }) + .meta({ id: 'QueueUpdateDto' }); - @ApiProperty({ description: 'Job data payload', type: Object }) - data!: object; +const QueueDeleteSchema = z + .object({ + failed: z + .boolean() + .optional() + .describe('If true, will also remove failed jobs from the queue.') + .meta(new HistoryBuilder().added('v2.4.0').alpha('v2.4.0').getExtensions()), + }) + .meta({ id: 'QueueDeleteDto' }); - @ApiProperty({ type: 'integer', description: 'Job creation timestamp' }) - timestamp!: number; -} +const QueueJobSearchSchema = z + .object({ + status: z.array(QueueJobStatusSchema).optional().describe('Filter jobs by status'), + }) + .meta({ id: 'QueueJobSearchDto' }); -export class QueueStatisticsDto { - @ApiProperty({ type: 'integer', description: 'Number of active jobs' }) - active!: number; - @ApiProperty({ type: 'integer', description: 'Number of completed jobs' }) - completed!: number; - @ApiProperty({ type: 'integer', description: 'Number of failed jobs' }) - failed!: number; - @ApiProperty({ type: 'integer', description: 'Number of delayed jobs' }) - delayed!: number; - @ApiProperty({ type: 'integer', description: 'Number of waiting jobs' }) - waiting!: number; - @ApiProperty({ type: 'integer', description: 'Number of paused jobs' }) - paused!: number; -} +const QueueJobResponseSchema = z + .object({ + id: z.string().optional().describe('Job ID'), + name: JobNameSchema, + data: z.record(z.string(), z.unknown()).describe('Job data payload'), + timestamp: z.int().describe('Job creation timestamp'), + }) + .meta({ id: 'QueueJobResponseDto' }); -export class QueueResponseDto { - @ValidateEnum({ enum: QueueName, name: 'QueueName', description: 'Queue name' }) - name!: QueueName; +export const QueueStatisticsSchema = z + .object({ + active: z.int().describe('Number of active jobs'), + completed: z.int().describe('Number of completed jobs'), + failed: z.int().describe('Number of failed jobs'), + delayed: z.int().describe('Number of delayed jobs'), + waiting: z.int().describe('Number of waiting jobs'), + paused: z.int().describe('Number of paused jobs'), + }) + .meta({ id: 'QueueStatisticsDto' }); - @ValidateBoolean({ description: 'Whether the queue is paused' }) - isPaused!: boolean; +const QueueResponseSchema = z + .object({ + name: QueueNameSchema, + isPaused: z.boolean().describe('Whether the queue is paused'), + statistics: QueueStatisticsSchema, + }) + .meta({ id: 'QueueResponseDto' }); - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - statistics!: QueueStatisticsDto; -} +export class QueueNameParamDto extends createZodDto(QueueNameParamSchema) {} +export class QueueCommandDto extends createZodDto(QueueCommandSchemaDto) {} +export class QueueUpdateDto extends createZodDto(QueueUpdateSchema) {} +export class QueueDeleteDto extends createZodDto(QueueDeleteSchema) {} +export class QueueJobSearchDto extends createZodDto(QueueJobSearchSchema) {} +export class QueueJobResponseDto extends createZodDto(QueueJobResponseSchema) {} +export class QueueStatisticsDto extends createZodDto(QueueStatisticsSchema) {} +export class QueueResponseDto extends createZodDto(QueueResponseSchema) {} diff --git a/server/src/dtos/search.dto.ts b/server/src/dtos/search.dto.ts index 196e72c37e..c0362cdb5d 100644 --- a/server/src/dtos/search.dto.ts +++ b/server/src/dtos/search.dto.ts @@ -1,282 +1,155 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { Place } from 'src/database'; -import { HistoryBuilder, Property } from 'src/decorators'; -import { AlbumResponseDto } from 'src/dtos/album.dto'; -import { AssetResponseDto } from 'src/dtos/asset-response.dto'; -import { AssetOrder, AssetType, AssetVisibility } from 'src/enum'; -import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateString, ValidateUUID } from 'src/validation'; +import { HistoryBuilder } from 'src/decorators'; +import { AlbumResponseSchema } from 'src/dtos/album.dto'; +import { AssetResponseSchema } from 'src/dtos/asset-response.dto'; +import { AssetOrder, AssetOrderSchema, AssetTypeSchema, AssetVisibilitySchema } from 'src/enum'; +import { emptyStringToNull, isoDatetimeToDate, stringToBool } from 'src/validation'; +import z from 'zod'; -class BaseSearchDto { - @ValidateUUID({ optional: true, nullable: true, description: 'Library ID to filter by' }) - libraryId?: string | null; +const BaseSearchSchema = z.object({ + libraryId: z.uuidv4().nullish().describe('Library ID to filter by'), + type: AssetTypeSchema.optional(), + isEncoded: z.boolean().optional().describe('Filter by encoded status'), + isFavorite: z.boolean().optional().describe('Filter by favorite status'), + isMotion: z.boolean().optional().describe('Filter by motion photo status'), + isOffline: z.boolean().optional().describe('Filter by offline status'), + visibility: AssetVisibilitySchema.optional(), + createdBefore: isoDatetimeToDate.optional().describe('Filter by creation date (before)'), + createdAfter: isoDatetimeToDate.optional().describe('Filter by creation date (after)'), + updatedBefore: isoDatetimeToDate.optional().describe('Filter by update date (before)'), + updatedAfter: isoDatetimeToDate.optional().describe('Filter by update date (after)'), + trashedBefore: isoDatetimeToDate.optional().describe('Filter by trash date (before)'), + trashedAfter: isoDatetimeToDate.optional().describe('Filter by trash date (after)'), + takenBefore: isoDatetimeToDate.optional().describe('Filter by taken date (before)'), + takenAfter: isoDatetimeToDate.optional().describe('Filter by taken date (after)'), + city: emptyStringToNull(z.string().nullable()).optional().describe('Filter by city name'), + state: emptyStringToNull(z.string().nullable()).optional().describe('Filter by state/province name'), + country: emptyStringToNull(z.string().nullable()).optional().describe('Filter by country name'), + make: emptyStringToNull(z.string().nullable()).optional().describe('Filter by camera make'), + model: emptyStringToNull(z.string().nullable()).optional().describe('Filter by camera model'), + lensModel: emptyStringToNull(z.string().nullable()).optional().describe('Filter by lens model'), + isNotInAlbum: z.boolean().optional().describe('Filter assets not in any album'), + personIds: z.array(z.uuidv4()).optional().describe('Filter by person IDs'), + tagIds: z.array(z.uuidv4()).nullish().describe('Filter by tag IDs'), + albumIds: z.array(z.uuidv4()).optional().describe('Filter by album IDs'), + rating: z + .number() + .min(-1) + .max(5) + .nullish() + .describe('Filter by rating [1-5], or null for unrated') + .meta({ + ...new HistoryBuilder() + .added('v1') + .stable('v2') + .updated('v2.6.0', 'Using -1 as a rating is deprecated and will be removed in the next major version.') + .getExtensions(), + }), + ocr: z.string().optional().describe('Filter by OCR text content'), +}); - @ApiPropertyOptional({ description: 'Device ID to filter by' }) - @IsString() - @IsNotEmpty() - @Optional() - deviceId?: string; +const BaseSearchWithResultsSchema = BaseSearchSchema.extend({ + withDeleted: z.boolean().optional().describe('Include deleted assets'), + withExif: z.boolean().optional().describe('Include EXIF data in response'), + size: z.number().min(1).max(1000).optional().describe('Number of results to return'), +}); - @ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum', optional: true, description: 'Asset type filter' }) - type?: AssetType; +const RandomSearchSchema = BaseSearchWithResultsSchema.extend({ + withStacked: z.boolean().optional().describe('Include stacked assets'), + withPeople: z.boolean().optional().describe('Include people data in response'), +}).meta({ id: 'RandomSearchDto' }); - @ValidateBoolean({ optional: true, description: 'Filter by encoded status' }) - isEncoded?: boolean; +const LargeAssetSearchSchema = BaseSearchWithResultsSchema.extend({ + minFileSize: z.coerce.number().int().min(0).optional().describe('Minimum file size in bytes'), + size: z.coerce.number().min(1).max(1000).optional().describe('Number of results to return'), +}).meta({ id: 'LargeAssetSearchDto' }); - @ValidateBoolean({ optional: true, description: 'Filter by favorite status' }) - isFavorite?: boolean; +const MetadataSearchSchema = RandomSearchSchema.extend({ + id: z.uuidv4().optional().describe('Filter by asset ID'), + description: z.string().trim().optional().describe('Filter by description text'), + checksum: z.string().optional().describe('Filter by file checksum'), + originalFileName: z.string().trim().optional().describe('Filter by original file name'), + originalPath: z.string().optional().describe('Filter by original file path'), + previewPath: z.string().optional().describe('Filter by preview file path'), + thumbnailPath: z.string().optional().describe('Filter by thumbnail file path'), + encodedVideoPath: z.string().optional().describe('Filter by encoded video file path'), + order: AssetOrderSchema.default(AssetOrder.Desc).optional().describe('Sort order'), + page: z.number().min(1).optional().describe('Page number'), +}).meta({ id: 'MetadataSearchDto' }); - @ValidateBoolean({ optional: true, description: 'Filter by motion photo status' }) - isMotion?: boolean; +const StatisticsSearchSchema = BaseSearchSchema.extend({ + description: z.string().trim().optional().describe('Filter by description text'), +}).meta({ id: 'StatisticsSearchDto' }); - @ValidateBoolean({ optional: true, description: 'Filter by offline status' }) - isOffline?: boolean; +const SmartSearchSchema = BaseSearchWithResultsSchema.extend({ + query: z.string().trim().optional().describe('Natural language search query'), + queryAssetId: z.uuidv4().optional().describe('Asset ID to use as search reference'), + language: z.string().optional().describe('Search language code'), + page: z.number().min(1).optional().describe('Page number'), +}).meta({ id: 'SmartSearchDto' }); - @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true, description: 'Filter by visibility' }) - visibility?: AssetVisibility; - - @ValidateDate({ optional: true, description: 'Filter by creation date (before)' }) - createdBefore?: Date; - - @ValidateDate({ optional: true, description: 'Filter by creation date (after)' }) - createdAfter?: Date; - - @ValidateDate({ optional: true, description: 'Filter by update date (before)' }) - updatedBefore?: Date; - - @ValidateDate({ optional: true, description: 'Filter by update date (after)' }) - updatedAfter?: Date; - - @ValidateDate({ optional: true, description: 'Filter by trash date (before)' }) - trashedBefore?: Date; - - @ValidateDate({ optional: true, description: 'Filter by trash date (after)' }) - trashedAfter?: Date; - - @ValidateDate({ optional: true, description: 'Filter by taken date (before)' }) - takenBefore?: Date; - - @ValidateDate({ optional: true, description: 'Filter by taken date (after)' }) - takenAfter?: Date; - - @ApiPropertyOptional({ description: 'Filter by city name' }) - @IsString() - @Optional({ nullable: true, emptyToNull: true }) - city?: string | null; - - @ApiPropertyOptional({ description: 'Filter by state/province name' }) - @IsString() - @Optional({ nullable: true, emptyToNull: true }) - state?: string | null; - - @ApiPropertyOptional({ description: 'Filter by country name' }) - @IsString() - @IsNotEmpty() - @Optional({ nullable: true, emptyToNull: true }) - country?: string | null; - - @ApiPropertyOptional({ description: 'Filter by camera make' }) - @IsString() - @Optional({ nullable: true, emptyToNull: true }) - make?: string; - - @ApiPropertyOptional({ description: 'Filter by camera model' }) - @IsString() - @Optional({ nullable: true, emptyToNull: true }) - model?: string | null; - - @ApiPropertyOptional({ description: 'Filter by lens model' }) - @IsString() - @Optional({ nullable: true, emptyToNull: true }) - lensModel?: string | null; - - @ValidateBoolean({ optional: true, description: 'Filter assets not in any album' }) - isNotInAlbum?: boolean; - - @ValidateUUID({ each: true, optional: true, description: 'Filter by person IDs' }) - personIds?: string[]; - - @ValidateUUID({ each: true, optional: true, nullable: true, description: 'Filter by tag IDs' }) - tagIds?: string[] | null; - - @ValidateUUID({ each: true, optional: true, description: 'Filter by album IDs' }) - albumIds?: string[]; - - @Property({ - type: 'number', - description: 'Filter by rating [1-5], or null for unrated', - minimum: -1, - maximum: 5, - history: new HistoryBuilder() - .added('v1') - .stable('v2') - .updated('v2.6.0', 'Using -1 as a rating is deprecated and will be removed in the next major version.'), +const SearchPlacesSchema = z + .object({ + name: z.string().describe('Place name to search for'), }) - @Optional({ nullable: true }) - @IsInt() - @Max(5) - @Min(-1) - rating?: number | null; + .meta({ id: 'SearchPlacesDto' }); - @ApiPropertyOptional({ description: 'Filter by OCR text content' }) - @IsString() - @IsNotEmpty() - @Optional() - ocr?: string; -} - -class BaseSearchWithResultsDto extends BaseSearchDto { - @ValidateBoolean({ optional: true, description: 'Include deleted assets' }) - withDeleted?: boolean; - - @ValidateBoolean({ optional: true, description: 'Include EXIF data in response' }) - withExif?: boolean; - - @ApiPropertyOptional({ type: 'number', description: 'Number of results to return', minimum: 1, maximum: 1000 }) - @IsInt() - @Min(1) - @Max(1000) - @Type(() => Number) - @Optional() - size?: number; -} - -export class RandomSearchDto extends BaseSearchWithResultsDto { - @ValidateBoolean({ optional: true, description: 'Include stacked assets' }) - withStacked?: boolean; - - @ValidateBoolean({ optional: true, description: 'Include people data in response' }) - withPeople?: boolean; -} - -export class LargeAssetSearchDto extends BaseSearchWithResultsDto { - @ApiPropertyOptional({ type: 'integer', description: 'Minimum file size in bytes', minimum: 0 }) - @Optional() - @IsInt() - @Min(0) - @Type(() => Number) - minFileSize?: number; -} - -export class MetadataSearchDto extends RandomSearchDto { - @ValidateUUID({ optional: true, description: 'Filter by asset ID' }) - id?: string; - - @ApiPropertyOptional({ description: 'Filter by device asset ID' }) - @IsString() - @IsNotEmpty() - @Optional() - deviceAssetId?: string; - - @ValidateString({ optional: true, trim: true, description: 'Filter by description text' }) - description?: string; - - @ApiPropertyOptional({ description: 'Filter by file checksum' }) - @IsString() - @IsNotEmpty() - @Optional() - checksum?: string; - - @ValidateString({ optional: true, trim: true, description: 'Filter by original file name' }) - originalFileName?: string; - - @ApiPropertyOptional({ description: 'Filter by original file path' }) - @IsString() - @IsNotEmpty() - @Optional() - originalPath?: string; - - @ApiPropertyOptional({ description: 'Filter by preview file path' }) - @IsString() - @IsNotEmpty() - @Optional() - previewPath?: string; - - @ApiPropertyOptional({ description: 'Filter by thumbnail file path' }) - @IsString() - @IsNotEmpty() - @Optional() - thumbnailPath?: string; - - @ApiPropertyOptional({ description: 'Filter by encoded video file path' }) - @IsString() - @IsNotEmpty() - @Optional() - encodedVideoPath?: string; - - @ValidateEnum({ - enum: AssetOrder, - name: 'AssetOrder', - optional: true, - default: AssetOrder.Desc, - description: 'Sort order', +const SearchPeopleSchema = z + .object({ + name: z.string().describe('Person name to search for'), + withHidden: stringToBool.optional().describe('Include hidden people'), }) - order?: AssetOrder; + .meta({ id: 'SearchPeopleDto' }); - @ApiPropertyOptional({ type: 'number', description: 'Page number', minimum: 1 }) - @IsInt() - @Min(1) - @Type(() => Number) - @Optional() - page?: number; +const PlacesResponseSchema = z + .object({ + name: z.string().describe('Place name'), + latitude: z.number().describe('Latitude coordinate'), + longitude: z.number().describe('Longitude coordinate'), + admin1name: z.string().optional().describe('Administrative level 1 name (state/province)'), + admin2name: z.string().optional().describe('Administrative level 2 name (county/district)'), + }) + .meta({ id: 'PlacesResponseDto' }); + +export enum SearchSuggestionType { + COUNTRY = 'country', + STATE = 'state', + CITY = 'city', + CAMERA_MAKE = 'camera-make', + CAMERA_MODEL = 'camera-model', + CAMERA_LENS_MODEL = 'camera-lens-model', } -export class StatisticsSearchDto extends BaseSearchDto { - @ValidateString({ optional: true, trim: true, description: 'Filter by description text' }) - description?: string; -} +const SearchSuggestionTypeSchema = z + .enum(SearchSuggestionType) + .describe('Suggestion type') + .meta({ id: 'SearchSuggestionType' }); -export class SmartSearchDto extends BaseSearchWithResultsDto { - @ValidateString({ optional: true, trim: true, description: 'Natural language search query' }) - query?: string; +const SearchSuggestionRequestSchema = z + .object({ + type: SearchSuggestionTypeSchema, + country: z.string().optional().describe('Filter by country'), + state: z.string().optional().describe('Filter by state/province'), + make: z.string().optional().describe('Filter by camera make'), + model: z.string().optional().describe('Filter by camera model'), + lensModel: z.string().optional().describe('Filter by lens model'), + includeNull: stringToBool + .optional() + .describe('Include null values in suggestions') + .meta(new HistoryBuilder().added('v1.111.0').stable('v2').getExtensions()), + }) + .meta({ id: 'SearchSuggestionRequestDto' }); - @ValidateUUID({ optional: true, description: 'Asset ID to use as search reference' }) - queryAssetId?: string; - - @ApiPropertyOptional({ description: 'Search language code' }) - @IsString() - @IsNotEmpty() - @Optional() - language?: string; - - @ApiPropertyOptional({ type: 'number', description: 'Page number', minimum: 1 }) - @IsInt() - @Min(1) - @Type(() => Number) - @Optional() - page?: number; -} - -export class SearchPlacesDto { - @ApiProperty({ description: 'Place name to search for' }) - @IsString() - @IsNotEmpty() - name!: string; -} - -export class SearchPeopleDto { - @ApiProperty({ description: 'Person name to search for' }) - @IsString() - @IsNotEmpty() - name!: string; - - @ValidateBoolean({ optional: true, description: 'Include hidden people' }) - withHidden?: boolean; -} - -export class PlacesResponseDto { - @ApiProperty({ description: 'Place name' }) - name!: string; - @ApiProperty({ type: 'number', description: 'Latitude coordinate' }) - latitude!: number; - @ApiProperty({ type: 'number', description: 'Longitude coordinate' }) - longitude!: number; - @ApiPropertyOptional({ description: 'Administrative level 1 name (state/province)' }) - admin1name?: string; - @ApiPropertyOptional({ description: 'Administrative level 2 name (county/district)' }) - admin2name?: string; -} +export class RandomSearchDto extends createZodDto(RandomSearchSchema) {} +export class LargeAssetSearchDto extends createZodDto(LargeAssetSearchSchema) {} +export class MetadataSearchDto extends createZodDto(MetadataSearchSchema) {} +export class StatisticsSearchDto extends createZodDto(StatisticsSearchSchema) {} +export class SmartSearchDto extends createZodDto(SmartSearchSchema) {} +export class SearchPlacesDto extends createZodDto(SearchPlacesSchema) {} +export class SearchPeopleDto extends createZodDto(SearchPeopleSchema) {} +export class PlacesResponseDto extends createZodDto(PlacesResponseSchema) {} +export class SearchSuggestionRequestDto extends createZodDto(SearchSuggestionRequestSchema) {} export function mapPlaces(place: Place): PlacesResponseDto { return { @@ -288,136 +161,68 @@ export function mapPlaces(place: Place): PlacesResponseDto { }; } -export enum SearchSuggestionType { - COUNTRY = 'country', - STATE = 'state', - CITY = 'city', - CAMERA_MAKE = 'camera-make', - CAMERA_MODEL = 'camera-model', - CAMERA_LENS_MODEL = 'camera-lens-model', -} - -export class SearchSuggestionRequestDto { - @ValidateEnum({ enum: SearchSuggestionType, name: 'SearchSuggestionType', description: 'Suggestion type' }) - type!: SearchSuggestionType; - - @ApiPropertyOptional({ description: 'Filter by country' }) - @IsString() - @Optional() - country?: string; - - @ApiPropertyOptional({ description: 'Filter by state/province' }) - @IsString() - @Optional() - state?: string; - - @ApiPropertyOptional({ description: 'Filter by camera make' }) - @IsString() - @Optional() - make?: string; - - @ApiPropertyOptional({ description: 'Filter by camera model' }) - @IsString() - @Optional() - model?: string; - - @ApiPropertyOptional({ description: 'Filter by lens model' }) - @IsString() - @Optional() - lensModel?: string; - - @ValidateBoolean({ - optional: true, - description: 'Include null values in suggestions', - history: new HistoryBuilder().added('v1.111.0').stable('v2'), +const SearchFacetCountResponseSchema = z + .object({ + count: z.int().min(0).describe('Number of assets with this facet value'), + value: z.string().describe('Facet value'), }) - includeNull?: boolean; -} + .meta({ id: 'SearchFacetCountResponseDto' }); -class SearchFacetCountResponseDto { - @ApiProperty({ type: 'integer', description: 'Number of assets with this facet value' }) - count!: number; - @ApiProperty({ description: 'Facet value' }) - value!: string; -} +const SearchFacetResponseSchema = z + .object({ + fieldName: z.string().describe('Facet field name'), + counts: z.array(SearchFacetCountResponseSchema), + }) + .meta({ id: 'SearchFacetResponseDto' }); -class SearchFacetResponseDto { - @ApiProperty({ description: 'Facet field name' }) - fieldName!: string; - @ApiProperty({ description: 'Facet counts' }) - counts!: SearchFacetCountResponseDto[]; -} +const SearchAlbumResponseSchema = z + .object({ + total: z.int().min(0).describe('Total number of matching albums'), + count: z.int().min(0).describe('Number of albums in this page'), + items: z.array(AlbumResponseSchema), + facets: z.array(SearchFacetResponseSchema), + }) + .meta({ id: 'SearchAlbumResponseDto' }); -class SearchAlbumResponseDto { - @ApiProperty({ type: 'integer', description: 'Total number of matching albums' }) - total!: number; - @ApiProperty({ type: 'integer', description: 'Number of albums in this page' }) - count!: number; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - items!: AlbumResponseDto[]; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - facets!: SearchFacetResponseDto[]; -} +const SearchAssetResponseSchema = z + .object({ + total: z.int().min(0).describe('Total number of matching assets'), + count: z.int().min(0).describe('Number of assets in this page'), + items: z.array(AssetResponseSchema), + facets: z.array(SearchFacetResponseSchema), + nextPage: z.string().nullable().describe('Next page token'), + }) + .meta({ id: 'SearchAssetResponseDto' }); -class SearchAssetResponseDto { - @ApiProperty({ type: 'integer', description: 'Total number of matching assets' }) - total!: number; - @ApiProperty({ type: 'integer', description: 'Number of assets in this page' }) - count!: number; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - items!: AssetResponseDto[]; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - facets!: SearchFacetResponseDto[]; - @ApiProperty({ description: 'Next page token' }) - nextPage!: string | null; -} +const SearchResponseSchema = z + .object({ + albums: SearchAlbumResponseSchema, + assets: SearchAssetResponseSchema, + }) + .meta({ id: 'SearchResponseDto' }); -export class SearchResponseDto { - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - albums!: SearchAlbumResponseDto; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - assets!: SearchAssetResponseDto; -} +export class SearchResponseDto extends createZodDto(SearchResponseSchema) {} -export class SearchStatisticsResponseDto { - @ApiProperty({ type: 'integer', description: 'Total number of matching assets' }) - total!: number; -} +const SearchStatisticsResponseSchema = z + .object({ + total: z.int().describe('Total number of matching assets'), + }) + .meta({ id: 'SearchStatisticsResponseDto' }); -class SearchExploreItem { - @ApiProperty({ description: 'Explore value' }) - value!: string; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - data!: AssetResponseDto; -} +export class SearchStatisticsResponseDto extends createZodDto(SearchStatisticsResponseSchema) {} -export class SearchExploreResponseDto { - @ApiProperty({ description: 'Explore field name' }) - fieldName!: string; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - items!: SearchExploreItem[]; -} +const SearchExploreItemSchema = z + .object({ + value: z.string().describe('Explore value'), + data: AssetResponseSchema, + }) + .meta({ id: 'SearchExploreItem' }); -export class MemoryLaneDto { - @ApiProperty({ type: 'integer', description: 'Day of month' }) - @IsInt() - @Type(() => Number) - @Max(31) - @Min(1) - day!: number; +const SearchExploreResponseSchema = z + .object({ + fieldName: z.string().describe('Explore field name'), + items: z.array(SearchExploreItemSchema), + }) + .meta({ id: 'SearchExploreResponseDto' }); - @ApiProperty({ type: 'integer', description: 'Month' }) - @IsInt() - @Type(() => Number) - @Max(12) - @Min(1) - month!: number; -} +export class SearchExploreResponseDto extends createZodDto(SearchExploreResponseSchema) {} diff --git a/server/src/dtos/server.dto.ts b/server/src/dtos/server.dto.ts index 626c94e40a..bd42032771 100644 --- a/server/src/dtos/server.dto.ts +++ b/server/src/dtos/server.dto.ts @@ -1,242 +1,169 @@ -import { ApiProperty, ApiPropertyOptional, ApiResponseProperty } from '@nestjs/swagger'; -import { SemVer } from 'semver'; -import { SystemConfigThemeDto } from 'src/dtos/system-config.dto'; +import { createZodDto } from 'nestjs-zod'; +import type { SemVer } from 'semver'; +import { isoDatetimeToDate } from 'src/validation'; +import z from 'zod'; -export class ServerPingResponse { - @ApiResponseProperty({ type: String, example: 'pong' }) - res!: string; -} +const ServerPingResponseSchema = z + .object({ + res: z.string().meta({ example: 'pong' }), + }) + .meta({ id: 'ServerPingResponse' }); -export class ServerAboutResponseDto { - @ApiProperty({ description: 'Server version' }) - version!: string; - @ApiProperty({ description: 'URL to version information' }) - versionUrl!: string; +const ServerAboutResponseSchema = z + .object({ + version: z.string().describe('Server version'), + versionUrl: z.string().describe('URL to version information'), + repository: z.string().optional().describe('Repository name'), + repositoryUrl: z.string().optional().describe('Repository URL'), + sourceRef: z.string().optional().describe('Source reference (branch/tag)'), + sourceCommit: z.string().optional().describe('Source commit hash'), + sourceUrl: z.string().optional().describe('Source URL'), + build: z.string().optional().describe('Build identifier'), + buildUrl: z.string().optional().describe('Build URL'), + buildImage: z.string().optional().describe('Build image name'), + buildImageUrl: z.string().optional().describe('Build image URL'), + nodejs: z.string().optional().describe('Node.js version'), + ffmpeg: z.string().optional().describe('FFmpeg version'), + imagemagick: z.string().optional().describe('ImageMagick version'), + libvips: z.string().optional().describe('libvips version'), + exiftool: z.string().optional().describe('ExifTool version'), + licensed: z.boolean().describe('Whether the server is licensed'), + thirdPartySourceUrl: z.string().optional().describe('Third-party source URL'), + thirdPartyBugFeatureUrl: z.string().optional().describe('Third-party bug/feature URL'), + thirdPartyDocumentationUrl: z.string().optional().describe('Third-party documentation URL'), + thirdPartySupportUrl: z.string().optional().describe('Third-party support URL'), + }) + .meta({ id: 'ServerAboutResponseDto' }); - @ApiPropertyOptional({ description: 'Repository name' }) - repository?: string; - @ApiPropertyOptional({ description: 'Repository URL' }) - repositoryUrl?: string; +const ServerApkLinksSchema = z + .object({ + arm64v8a: z.string().describe('APK download link for ARM64 v8a architecture'), + armeabiv7a: z.string().describe('APK download link for ARM EABI v7a architecture'), + universal: z.string().describe('APK download link for universal architecture'), + x86_64: z.string().describe('APK download link for x86_64 architecture'), + }) + .meta({ id: 'ServerApkLinksDto' }); - @ApiPropertyOptional({ description: 'Source reference (branch/tag)' }) - sourceRef?: string; - @ApiPropertyOptional({ description: 'Source commit hash' }) - sourceCommit?: string; - @ApiPropertyOptional({ description: 'Source URL' }) - sourceUrl?: string; +const ServerStorageResponseSchema = z + .object({ + diskSize: z.string().describe('Total disk size (human-readable format)'), + diskUse: z.string().describe('Used disk space (human-readable format)'), + diskAvailable: z.string().describe('Available disk space (human-readable format)'), + diskSizeRaw: z.int().describe('Total disk size in bytes'), + diskUseRaw: z.int().describe('Used disk space in bytes'), + diskAvailableRaw: z.int().describe('Available disk space in bytes'), + diskUsagePercentage: z.number().meta({ format: 'double' }).describe('Disk usage percentage (0-100)'), + }) + .meta({ id: 'ServerStorageResponseDto' }); - @ApiPropertyOptional({ description: 'Build identifier' }) - build?: string; - @ApiPropertyOptional({ description: 'Build URL' }) - buildUrl?: string; - @ApiPropertyOptional({ description: 'Build image name' }) - buildImage?: string; - @ApiPropertyOptional({ description: 'Build image URL' }) - buildImageUrl?: string; +const ServerVersionResponseSchema = z + .object({ + major: z.int().describe('Major version number'), + minor: z.int().describe('Minor version number'), + patch: z.int().describe('Patch version number'), + }) + .meta({ id: 'ServerVersionResponseDto' }); - @ApiPropertyOptional({ description: 'Node.js version' }) - nodejs?: string; - @ApiPropertyOptional({ description: 'FFmpeg version' }) - ffmpeg?: string; - @ApiPropertyOptional({ description: 'ImageMagick version' }) - imagemagick?: string; - @ApiPropertyOptional({ description: 'libvips version' }) - libvips?: string; - @ApiPropertyOptional({ description: 'ExifTool version' }) - exiftool?: string; +const ServerVersionHistoryResponseSchema = z + .object({ + id: z.string().describe('Version history entry ID'), + createdAt: isoDatetimeToDate.describe('When this version was first seen'), + version: z.string().describe('Version string'), + }) + .meta({ id: 'ServerVersionHistoryResponseDto' }); - @ApiProperty({ description: 'Whether the server is licensed' }) - licensed!: boolean; +const UsageByUserSchema = z + .object({ + userId: z.string().describe('User ID'), + userName: z.string().describe('User name'), + photos: z.int().describe('Number of photos'), + videos: z.int().describe('Number of videos'), + usage: z.int().describe('Total storage usage in bytes'), + usagePhotos: z.int().describe('Storage usage for photos in bytes'), + usageVideos: z.int().describe('Storage usage for videos in bytes'), + quotaSizeInBytes: z.int().nullable().describe('User quota size in bytes (null if unlimited)'), + }) + .meta({ id: 'UsageByUserDto' }); - @ApiPropertyOptional({ description: 'Third-party source URL' }) - thirdPartySourceUrl?: string; - @ApiPropertyOptional({ description: 'Third-party bug/feature URL' }) - thirdPartyBugFeatureUrl?: string; - @ApiPropertyOptional({ description: 'Third-party documentation URL' }) - thirdPartyDocumentationUrl?: string; - @ApiPropertyOptional({ description: 'Third-party support URL' }) - thirdPartySupportUrl?: string; -} +const ServerStatsResponseSchema = z + .object({ + photos: z.int().describe('Total number of photos'), + videos: z.int().describe('Total number of videos'), + usage: z.int().describe('Total storage usage in bytes'), + usagePhotos: z.int().describe('Storage usage for photos in bytes'), + usageVideos: z.int().describe('Storage usage for videos in bytes'), + usageByUser: z.array(UsageByUserSchema).describe('Array of usage for each user'), + }) + .meta({ id: 'ServerStatsResponseDto' }); -export class ServerApkLinksDto { - @ApiProperty({ description: 'APK download link for ARM64 v8a architecture' }) - arm64v8a!: string; - @ApiProperty({ description: 'APK download link for ARM EABI v7a architecture' }) - armeabiv7a!: string; - @ApiProperty({ description: 'APK download link for universal architecture' }) - universal!: string; - @ApiProperty({ description: 'APK download link for x86_64 architecture' }) - x86_64!: string; -} +const ServerMediaTypesResponseSchema = z + .object({ + video: z.array(z.string()).describe('Supported video MIME types'), + image: z.array(z.string()).describe('Supported image MIME types'), + sidecar: z.array(z.string()).describe('Supported sidecar MIME types'), + }) + .meta({ id: 'ServerMediaTypesResponseDto' }); -export class ServerStorageResponseDto { - @ApiProperty({ description: 'Total disk size (human-readable format)' }) - diskSize!: string; - @ApiProperty({ description: 'Used disk space (human-readable format)' }) - diskUse!: string; - @ApiProperty({ description: 'Available disk space (human-readable format)' }) - diskAvailable!: string; +const ServerThemeSchema = z + .object({ + customCss: z.string().describe('Custom CSS for theming'), + }) + .meta({ id: 'ServerThemeDto' }); - @ApiProperty({ type: 'integer', format: 'int64', description: 'Total disk size in bytes' }) - diskSizeRaw!: number; +const ServerConfigSchema = z + .object({ + oauthButtonText: z.string().describe('OAuth button text'), + loginPageMessage: z.string().describe('Login page message'), + trashDays: z.int().describe('Number of days before trashed assets are permanently deleted'), + userDeleteDelay: z.int().describe('Delay in days before deleted users are permanently removed'), + isInitialized: z.boolean().describe('Whether the server has been initialized'), + isOnboarded: z.boolean().describe('Whether the admin has completed onboarding'), + externalDomain: z.string().describe('External domain URL'), + publicUsers: z.boolean().describe('Whether public user registration is enabled'), + mapDarkStyleUrl: z.string().describe('Map dark style URL'), + mapLightStyleUrl: z.string().describe('Map light style URL'), + maintenanceMode: z.boolean().describe('Whether maintenance mode is active'), + }) + .meta({ id: 'ServerConfigDto' }); - @ApiProperty({ type: 'integer', format: 'int64', description: 'Used disk space in bytes' }) - diskUseRaw!: number; +const ServerFeaturesSchema = z + .object({ + smartSearch: z.boolean().describe('Whether smart search is enabled'), + duplicateDetection: z.boolean().describe('Whether duplicate detection is enabled'), + configFile: z.boolean().describe('Whether config file is available'), + facialRecognition: z.boolean().describe('Whether facial recognition is enabled'), + map: z.boolean().describe('Whether map feature is enabled'), + trash: z.boolean().describe('Whether trash feature is enabled'), + reverseGeocoding: z.boolean().describe('Whether reverse geocoding is enabled'), + importFaces: z.boolean().describe('Whether face import is enabled'), + oauth: z.boolean().describe('Whether OAuth is enabled'), + oauthAutoLaunch: z.boolean().describe('Whether OAuth auto-launch is enabled'), + passwordLogin: z.boolean().describe('Whether password login is enabled'), + sidecar: z.boolean().describe('Whether sidecar files are supported'), + search: z.boolean().describe('Whether search is enabled'), + email: z.boolean().describe('Whether email notifications are enabled'), + ocr: z.boolean().describe('Whether OCR is enabled'), + }) + .meta({ id: 'ServerFeaturesDto' }); - @ApiProperty({ type: 'integer', format: 'int64', description: 'Available disk space in bytes' }) - diskAvailableRaw!: number; +export class ServerPingResponse extends createZodDto(ServerPingResponseSchema) {} +export class ServerAboutResponseDto extends createZodDto(ServerAboutResponseSchema) {} +export class ServerApkLinksDto extends createZodDto(ServerApkLinksSchema) {} +export class ServerStorageResponseDto extends createZodDto(ServerStorageResponseSchema) {} - @ApiProperty({ type: 'number', format: 'double', description: 'Disk usage percentage (0-100)' }) - diskUsagePercentage!: number; -} - -export class ServerVersionResponseDto { - @ApiProperty({ type: 'integer', description: 'Major version number' }) - major!: number; - @ApiProperty({ type: 'integer', description: 'Minor version number' }) - minor!: number; - @ApiProperty({ type: 'integer', description: 'Patch version number' }) - patch!: number; - - static fromSemVer(value: SemVer) { +export class ServerVersionResponseDto extends createZodDto(ServerVersionResponseSchema) { + static fromSemVer(value: SemVer): z.infer { return { major: value.major, minor: value.minor, patch: value.patch }; } } -export class ServerVersionHistoryResponseDto { - @ApiProperty({ description: 'Version history entry ID' }) - id!: string; - @ApiProperty({ description: 'When this version was first seen', format: 'date-time' }) - createdAt!: Date; - @ApiProperty({ description: 'Version string' }) - version!: string; -} - -export class UsageByUserDto { - @ApiProperty({ type: 'string', description: 'User ID' }) - userId!: string; - @ApiProperty({ type: 'string', description: 'User name' }) - userName!: string; - @ApiProperty({ type: 'integer', description: 'Number of photos' }) - photos!: number; - @ApiProperty({ type: 'integer', description: 'Number of videos' }) - videos!: number; - @ApiProperty({ type: 'integer', format: 'int64', description: 'Total storage usage in bytes' }) - usage!: number; - @ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage for photos in bytes' }) - usagePhotos!: number; - @ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage for videos in bytes' }) - usageVideos!: number; - @ApiProperty({ - type: 'integer', - format: 'int64', - nullable: true, - description: 'User quota size in bytes (null if unlimited)', - }) - quotaSizeInBytes!: number | null; -} - -export class ServerStatsResponseDto { - @ApiProperty({ type: 'integer', description: 'Total number of photos' }) - photos = 0; - - @ApiProperty({ type: 'integer', description: 'Total number of videos' }) - videos = 0; - - @ApiProperty({ type: 'integer', format: 'int64', description: 'Total storage usage in bytes' }) - usage = 0; - - @ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage for photos in bytes' }) - usagePhotos = 0; - - @ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage for videos in bytes' }) - usageVideos = 0; - - @ApiProperty({ - isArray: true, - type: UsageByUserDto, - title: 'Array of usage for each user', - example: [ - { - photos: 1, - videos: 1, - diskUsageRaw: 2, - usagePhotos: 1, - usageVideos: 1, - }, - ], - }) - usageByUser: UsageByUserDto[] = []; -} - -export class ServerMediaTypesResponseDto { - @ApiProperty({ description: 'Supported video MIME types' }) - video!: string[]; - @ApiProperty({ description: 'Supported image MIME types' }) - image!: string[]; - @ApiProperty({ description: 'Supported sidecar MIME types' }) - sidecar!: string[]; -} - -export class ServerThemeDto extends SystemConfigThemeDto {} - -export class ServerConfigDto { - @ApiProperty({ description: 'OAuth button text' }) - oauthButtonText!: string; - @ApiProperty({ description: 'Login page message' }) - loginPageMessage!: string; - @ApiProperty({ type: 'integer', description: 'Number of days before trashed assets are permanently deleted' }) - trashDays!: number; - @ApiProperty({ type: 'integer', description: 'Delay in days before deleted users are permanently removed' }) - userDeleteDelay!: number; - @ApiProperty({ description: 'Whether the server has been initialized' }) - isInitialized!: boolean; - @ApiProperty({ description: 'Whether the admin has completed onboarding' }) - isOnboarded!: boolean; - @ApiProperty({ description: 'External domain URL' }) - externalDomain!: string; - @ApiProperty({ description: 'Whether public user registration is enabled' }) - publicUsers!: boolean; - @ApiProperty({ description: 'Map dark style URL' }) - mapDarkStyleUrl!: string; - @ApiProperty({ description: 'Map light style URL' }) - mapLightStyleUrl!: string; - @ApiProperty({ description: 'Whether maintenance mode is active' }) - maintenanceMode!: boolean; -} - -export class ServerFeaturesDto { - @ApiProperty({ description: 'Whether smart search is enabled' }) - smartSearch!: boolean; - @ApiProperty({ description: 'Whether duplicate detection is enabled' }) - duplicateDetection!: boolean; - @ApiProperty({ description: 'Whether config file is available' }) - configFile!: boolean; - @ApiProperty({ description: 'Whether facial recognition is enabled' }) - facialRecognition!: boolean; - @ApiProperty({ description: 'Whether map feature is enabled' }) - map!: boolean; - @ApiProperty({ description: 'Whether trash feature is enabled' }) - trash!: boolean; - @ApiProperty({ description: 'Whether reverse geocoding is enabled' }) - reverseGeocoding!: boolean; - @ApiProperty({ description: 'Whether face import is enabled' }) - importFaces!: boolean; - @ApiProperty({ description: 'Whether OAuth is enabled' }) - oauth!: boolean; - @ApiProperty({ description: 'Whether OAuth auto-launch is enabled' }) - oauthAutoLaunch!: boolean; - @ApiProperty({ description: 'Whether password login is enabled' }) - passwordLogin!: boolean; - @ApiProperty({ description: 'Whether sidecar files are supported' }) - sidecar!: boolean; - @ApiProperty({ description: 'Whether search is enabled' }) - search!: boolean; - @ApiProperty({ description: 'Whether email notifications are enabled' }) - email!: boolean; - @ApiProperty({ description: 'Whether OCR is enabled' }) - ocr!: boolean; -} +export class ServerVersionHistoryResponseDto extends createZodDto(ServerVersionHistoryResponseSchema) {} +export class UsageByUserDto extends createZodDto(UsageByUserSchema) {} +export class ServerStatsResponseDto extends createZodDto(ServerStatsResponseSchema) {} +export class ServerMediaTypesResponseDto extends createZodDto(ServerMediaTypesResponseSchema) {} +export class ServerThemeDto extends createZodDto(ServerThemeSchema) {} +export class ServerConfigDto extends createZodDto(ServerConfigSchema) {} +export class ServerFeaturesDto extends createZodDto(ServerFeaturesSchema) {} export interface ReleaseNotification { isAvailable: boolean; diff --git a/server/src/dtos/session.dto.ts b/server/src/dtos/session.dto.ts index f918f0b3bb..179a1dfb76 100644 --- a/server/src/dtos/session.dto.ts +++ b/server/src/dtos/session.dto.ts @@ -1,57 +1,43 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Equals, IsInt, IsPositive, IsString } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { Session } from 'src/database'; -import { Optional, ValidateBoolean } from 'src/validation'; +import z from 'zod'; -export class SessionCreateDto { - @ApiPropertyOptional({ type: 'number', description: 'Session duration in seconds' }) - @IsInt() - @IsPositive() - @Optional() - duration?: number; +const SessionCreateSchema = z + .object({ + duration: z.number().min(1).optional().describe('Session duration in seconds'), + deviceType: z.string().optional().describe('Device type'), + deviceOS: z.string().optional().describe('Device OS'), + }) + .meta({ id: 'SessionCreateDto' }); - @ApiPropertyOptional({ description: 'Device type' }) - @IsString() - @Optional() - deviceType?: string; +const SessionUpdateSchema = z + .object({ + isPendingSyncReset: z.boolean().optional().describe('Reset pending sync state'), + }) + .meta({ id: 'SessionUpdateDto' }); - @ApiPropertyOptional({ description: 'Device OS' }) - @IsString() - @Optional() - deviceOS?: string; -} +const SessionResponseSchema = z + .object({ + id: z.string().describe('Session ID'), + createdAt: z.string().describe('Creation date'), + updatedAt: z.string().describe('Last update date'), + expiresAt: z.string().optional().describe('Expiration date'), + current: z.boolean().describe('Is current session'), + deviceType: z.string().describe('Device type'), + deviceOS: z.string().describe('Device OS'), + appVersion: z.string().nullable().describe('App version'), + isPendingSyncReset: z.boolean().describe('Is pending sync reset'), + }) + .meta({ id: 'SessionResponseDto' }); -export class SessionUpdateDto { - @ValidateBoolean({ optional: true, description: 'Reset pending sync state' }) - @Equals(true) - isPendingSyncReset?: true; -} +const SessionCreateResponseSchema = SessionResponseSchema.extend({ + token: z.string().describe('Session token'), +}).meta({ id: 'SessionCreateResponseDto' }); -export class SessionResponseDto { - @ApiProperty({ description: 'Session ID' }) - id!: string; - @ApiProperty({ description: 'Creation date' }) - createdAt!: string; - @ApiProperty({ description: 'Last update date' }) - updatedAt!: string; - @ApiPropertyOptional({ description: 'Expiration date' }) - expiresAt?: string; - @ApiProperty({ description: 'Is current session' }) - current!: boolean; - @ApiProperty({ description: 'Device type' }) - deviceType!: string; - @ApiProperty({ description: 'Device OS' }) - deviceOS!: string; - @ApiProperty({ description: 'App version' }) - appVersion!: string | null; - @ApiProperty({ description: 'Is pending sync reset' }) - isPendingSyncReset!: boolean; -} - -export class SessionCreateResponseDto extends SessionResponseDto { - @ApiProperty({ description: 'Session token' }) - token!: string; -} +export class SessionCreateDto extends createZodDto(SessionCreateSchema) {} +export class SessionUpdateDto extends createZodDto(SessionUpdateSchema) {} +export class SessionResponseDto extends createZodDto(SessionResponseSchema) {} +export class SessionCreateResponseDto extends createZodDto(SessionCreateResponseSchema) {} export const mapSession = (entity: Session, currentId?: string): SessionResponseDto => ({ id: entity.id, diff --git a/server/src/dtos/shared-link.dto.ts b/server/src/dtos/shared-link.dto.ts index b2ecc70a3a..2e466c5014 100644 --- a/server/src/dtos/shared-link.dto.ts +++ b/server/src/dtos/shared-link.dto.ts @@ -1,155 +1,87 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { SharedLink } from 'src/database'; -import { HistoryBuilder, Property } from 'src/decorators'; -import { AlbumResponseDto, mapAlbumWithoutAssets } from 'src/dtos/album.dto'; -import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; -import { SharedLinkType } from 'src/enum'; -import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateString, ValidateUUID } from 'src/validation'; +import { HistoryBuilder } from 'src/decorators'; +import { AlbumResponseSchema, mapAlbum } from 'src/dtos/album.dto'; +import { AssetResponseSchema, mapAsset } from 'src/dtos/asset-response.dto'; +import { SharedLinkTypeSchema } from 'src/enum'; +import { emptyStringToNull, isoDatetimeToDate } from 'src/validation'; +import z from 'zod'; -export class SharedLinkSearchDto { - @ValidateUUID({ optional: true, description: 'Filter by album ID' }) - albumId?: string; - - @ValidateUUID({ - optional: true, - description: 'Filter by shared link ID', - history: new HistoryBuilder().added('v2.5.0'), +const SharedLinkSearchSchema = z + .object({ + albumId: z.uuidv4().optional().describe('Filter by album ID'), + id: z + .uuidv4() + .optional() + .describe('Filter by shared link ID') + .meta(new HistoryBuilder().added('v2.5.0').getExtensions()), }) - id?: string; -} + .meta({ id: 'SharedLinkSearchDto' }); -export class SharedLinkCreateDto { - @ValidateEnum({ enum: SharedLinkType, name: 'SharedLinkType', description: 'Shared link type' }) - type!: SharedLinkType; - - @ValidateUUID({ each: true, optional: true, description: 'Asset IDs (for individual assets)' }) - assetIds?: string[]; - - @ValidateUUID({ optional: true, description: 'Album ID (for album sharing)' }) - albumId?: string; - - @ApiPropertyOptional({ description: 'Link description' }) - @Optional({ nullable: true, emptyToNull: true }) - @IsString() - description?: string | null; - - @ApiPropertyOptional({ description: 'Link password' }) - @Optional({ nullable: true, emptyToNull: true }) - @IsString() - password?: string | null; - - @ApiPropertyOptional({ description: 'Custom URL slug' }) - @Optional({ nullable: true, emptyToNull: true }) - @IsString() - slug?: string | null; - - @ValidateDate({ optional: true, nullable: true, description: 'Expiration date' }) - expiresAt?: Date | null = null; - - @ValidateBoolean({ optional: true, description: 'Allow uploads' }) - allowUpload?: boolean; - - @ValidateBoolean({ optional: true, description: 'Allow downloads', default: true }) - allowDownload?: boolean = true; - - @ValidateBoolean({ optional: true, description: 'Show metadata', default: true }) - showMetadata?: boolean = true; -} - -export class SharedLinkEditDto { - @ApiPropertyOptional({ description: 'Link description' }) - @Optional({ nullable: true, emptyToNull: true }) - @IsString() - description?: string | null; - - @ApiPropertyOptional({ description: 'Link password' }) - @Optional({ nullable: true, emptyToNull: true }) - @IsString() - password?: string | null; - - @ApiPropertyOptional({ description: 'Custom URL slug' }) - @Optional({ nullable: true, emptyToNull: true }) - @IsString() - slug?: string | null; - - @ApiPropertyOptional({ description: 'Expiration date' }) - @Optional({ nullable: true }) - expiresAt?: Date | null; - - @ValidateBoolean({ optional: true, description: 'Allow uploads' }) - allowUpload?: boolean; - - @ValidateBoolean({ optional: true, description: 'Allow downloads' }) - allowDownload?: boolean; - - @ValidateBoolean({ optional: true, description: 'Show metadata' }) - showMetadata?: boolean; - - @ValidateBoolean({ - optional: true, - description: - 'Whether to change the expiry time. Few clients cannot send null to set the expiryTime to never. Setting this flag and not sending expiryAt is considered as null instead. Clients that can send null values can ignore this.', +const SharedLinkCreateSchema = z + .object({ + type: SharedLinkTypeSchema, + assetIds: z.array(z.uuidv4()).optional().describe('Asset IDs (for individual assets)'), + albumId: z.uuidv4().optional().describe('Album ID (for album sharing)'), + description: emptyStringToNull(z.string().nullable()).optional().describe('Link description'), + password: emptyStringToNull(z.string().nullable()).optional().describe('Link password'), + slug: emptyStringToNull(z.string().nullable()).optional().describe('Custom URL slug'), + expiresAt: isoDatetimeToDate.nullable().describe('Expiration date').default(null).optional(), + allowUpload: z.boolean().optional().describe('Allow uploads'), + allowDownload: z.boolean().default(true).optional().describe('Allow downloads'), + showMetadata: z.boolean().default(true).optional().describe('Show metadata'), }) - changeExpiryTime?: boolean; -} + .meta({ id: 'SharedLinkCreateDto' }); -export class SharedLinkLoginDto { - @ValidateString({ description: 'Shared link password', example: 'password' }) - password!: string; -} - -export class SharedLinkPasswordDto { - @ApiPropertyOptional({ example: 'password', description: 'Link password' }) - @IsString() - @Optional() - password?: string; - - @ApiPropertyOptional({ description: 'Access token' }) - @IsString() - @Optional() - token?: string; -} -export class SharedLinkResponseDto { - @ApiProperty({ description: 'Shared link ID' }) - id!: string; - @ApiProperty({ description: 'Link description' }) - description!: string | null; - @ApiProperty({ description: 'Has password' }) - password!: string | null; - @Property({ - description: 'Access token', - history: new HistoryBuilder().added('v1').stable('v2').deprecated('v2.6.0'), +const SharedLinkEditSchema = z + .object({ + description: emptyStringToNull(z.string().nullable()).optional().describe('Link description'), + password: emptyStringToNull(z.string().nullable()).optional().describe('Link password'), + slug: emptyStringToNull(z.string().nullable()).optional().describe('Custom URL slug'), + expiresAt: isoDatetimeToDate.nullish().describe('Expiration date'), + allowUpload: z.boolean().optional().describe('Allow uploads'), + allowDownload: z.boolean().optional().describe('Allow downloads'), + showMetadata: z.boolean().optional().describe('Show metadata'), + changeExpiryTime: z + .boolean() + .optional() + .describe( + 'Whether to change the expiry time. Few clients cannot send null to set the expiryTime to never. Setting this flag and not sending expiryAt is considered as null instead. Clients that can send null values can ignore this.', + ), }) - token?: string | null; - @ApiProperty({ description: 'Owner user ID' }) - userId!: string; - @ApiProperty({ description: 'Encryption key (base64url)' }) - key!: string; + .meta({ id: 'SharedLinkEditDto' }); - @ValidateEnum({ enum: SharedLinkType, name: 'SharedLinkType', description: 'Shared link type' }) - type!: SharedLinkType; - @ApiProperty({ description: 'Creation date' }) - createdAt!: Date; - @ApiProperty({ description: 'Expiration date' }) - expiresAt!: Date | null; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - assets!: AssetResponseDto[]; - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - album?: AlbumResponseDto; - @ApiProperty({ description: 'Allow uploads' }) - allowUpload!: boolean; +const SharedLinkLoginSchema = z + .object({ + password: z.string().describe('Shared link password').meta({ example: 'password' }), + }) + .meta({ id: 'SharedLinkLoginDto' }); - @ApiProperty({ description: 'Allow downloads' }) - allowDownload!: boolean; - @ApiProperty({ description: 'Show metadata' }) - showMetadata!: boolean; +const SharedLinkResponseSchema = z + .object({ + id: z.string().describe('Shared link ID'), + description: z.string().nullable().describe('Link description'), + password: z.string().nullable().describe('Has password'), + userId: z.string().describe('Owner user ID'), + key: z.string().describe('Encryption key (base64url)'), + type: SharedLinkTypeSchema, + createdAt: isoDatetimeToDate.describe('Creation date'), + expiresAt: isoDatetimeToDate.nullable().describe('Expiration date'), + assets: z.array(AssetResponseSchema), + album: AlbumResponseSchema.optional(), + allowUpload: z.boolean().describe('Allow uploads'), + allowDownload: z.boolean().describe('Allow downloads'), + showMetadata: z.boolean().describe('Show metadata'), + slug: z.string().nullable().describe('Custom URL slug'), + }) + .describe('Shared link response') + .meta({ id: 'SharedLinkResponseDto' }); - @ApiProperty({ description: 'Custom URL slug' }) - slug!: string | null; -} +export class SharedLinkSearchDto extends createZodDto(SharedLinkSearchSchema) {} +export class SharedLinkCreateDto extends createZodDto(SharedLinkCreateSchema) {} +export class SharedLinkEditDto extends createZodDto(SharedLinkEditSchema) {} +export class SharedLinkLoginDto extends createZodDto(SharedLinkLoginSchema) {} +export class SharedLinkResponseDto extends createZodDto(SharedLinkResponseSchema) {} export function mapSharedLink(sharedLink: SharedLink, options: { stripAssetMetadata: boolean }): SharedLinkResponseDto { const assets = sharedLink.assets || []; @@ -164,7 +96,7 @@ export function mapSharedLink(sharedLink: SharedLink, options: { stripAssetMetad createdAt: sharedLink.createdAt, expiresAt: sharedLink.expiresAt, assets: assets.map((asset) => mapAsset(asset, { stripMetadata: options.stripAssetMetadata })), - album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined, + album: sharedLink.album ? mapAlbum(sharedLink.album) : undefined, allowUpload: sharedLink.allowUpload, allowDownload: sharedLink.allowDownload, showMetadata: sharedLink.showExif, diff --git a/server/src/dtos/stack.dto.ts b/server/src/dtos/stack.dto.ts index a76b35e08e..48354cec6b 100644 --- a/server/src/dtos/stack.dto.ts +++ b/server/src/dtos/stack.dto.ts @@ -1,34 +1,40 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { ArrayMinSize } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { Stack } from 'src/database'; -import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; +import { AssetResponseSchema, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { ValidateUUID } from 'src/validation'; +import z from 'zod'; -export class StackCreateDto { - @ValidateUUID({ each: true, description: 'Asset IDs (first becomes primary, min 2)' }) - @ArrayMinSize(2) - assetIds!: string[]; -} +const StackSearchSchema = z + .object({ + primaryAssetId: z.uuidv4().optional().describe('Filter by primary asset ID'), + }) + .meta({ id: 'StackSearchDto' }); -export class StackSearchDto { - @ValidateUUID({ optional: true, description: 'Filter by primary asset ID' }) - primaryAssetId?: string; -} +const StackCreateSchema = z + .object({ + assetIds: z.array(z.uuidv4()).min(2).describe('Asset IDs (first becomes primary, min 2)'), + }) + .meta({ id: 'StackCreateDto' }); -export class StackUpdateDto { - @ValidateUUID({ optional: true, description: 'Primary asset ID' }) - primaryAssetId?: string; -} +const StackUpdateSchema = z + .object({ + primaryAssetId: z.uuidv4().optional().describe('Primary asset ID'), + }) + .meta({ id: 'StackUpdateDto' }); -export class StackResponseDto { - @ApiProperty({ description: 'Stack ID' }) - id!: string; - @ApiProperty({ description: 'Primary asset ID' }) - primaryAssetId!: string; - @ApiProperty({ description: 'Stack assets' }) - assets!: AssetResponseDto[]; -} +const StackResponseSchema = z + .object({ + id: z.string().describe('Stack ID'), + primaryAssetId: z.string().describe('Primary asset ID'), + assets: z.array(AssetResponseSchema), + }) + .describe('Stack response') + .meta({ id: 'StackResponseDto' }); + +export class StackSearchDto extends createZodDto(StackSearchSchema) {} +export class StackCreateDto extends createZodDto(StackCreateSchema) {} +export class StackUpdateDto extends createZodDto(StackUpdateSchema) {} +export class StackResponseDto extends createZodDto(StackResponseSchema) {} export const mapStack = (stack: Stack, { auth }: { auth?: AuthDto }) => { const primary = stack.assets.filter((asset) => asset.id === stack.primaryAssetId); diff --git a/server/src/dtos/sync.dto.ts b/server/src/dtos/sync.dto.ts index 9a1332d303..f6383d0a5d 100644 --- a/server/src/dtos/sync.dto.ts +++ b/server/src/dtos/sync.dto.ts @@ -1,492 +1,392 @@ /* eslint-disable @typescript-eslint/no-unsafe-function-type */ -import { ApiProperty } from '@nestjs/swagger'; -import { ArrayMaxSize, IsInt, IsPositive, IsString } from 'class-validator'; -import { AssetResponseDto } from 'src/dtos/asset-response.dto'; -import { AssetEditAction } from 'src/dtos/editing.dto'; +import { createZodDto } from 'nestjs-zod'; +import { AssetEditActionSchema } from 'src/dtos/editing.dto'; import { - AlbumUserRole, - AssetOrder, - AssetType, - AssetVisibility, - MemoryType, + AlbumUserRoleSchema, + AssetOrderSchema, + AssetTypeSchema, + AssetVisibilitySchema, + MemoryTypeSchema, SyncEntityType, - SyncRequestType, - UserAvatarColor, - UserMetadataKey, + SyncEntityTypeSchema, + SyncRequestTypeSchema, + UserAvatarColorSchema, + UserMetadataKeySchema, } from 'src/enum'; -import { UserMetadata } from 'src/types'; -import { ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; - -export class AssetFullSyncDto { - @ValidateUUID({ optional: true, description: 'Last asset ID (pagination)' }) - lastId?: string; - - @ValidateDate({ description: 'Sync assets updated until this date' }) - updatedUntil!: Date; - - @ApiProperty({ type: 'integer', description: 'Maximum number of assets to return' }) - @IsInt() - @IsPositive() - limit!: number; - - @ValidateUUID({ optional: true, description: 'Filter by user ID' }) - userId?: string; -} - -export class AssetDeltaSyncDto { - @ValidateDate({ description: 'Sync assets updated after this date' }) - updatedAfter!: Date; - - @ValidateUUID({ each: true, description: 'User IDs to sync' }) - userIds!: string[]; -} - -export class AssetDeltaSyncResponseDto { - @ApiProperty({ description: 'Whether full sync is needed' }) - needsFullSync!: boolean; - @ApiProperty({ description: 'Upserted assets' }) - upserted!: AssetResponseDto[]; - @ApiProperty({ description: 'Deleted asset IDs' }) - deleted!: string[]; -} +import { isoDatetimeToDate } from 'src/validation'; +import z from 'zod'; export const extraSyncModels: Function[] = []; -export const ExtraModel = (): ClassDecorator => { +const ExtraModel = (): ClassDecorator => { // eslint-disable-next-line unicorn/consistent-function-scoping return (object: Function) => { extraSyncModels.push(object); }; }; -@ExtraModel() -export class SyncUserV1 { - @ApiProperty({ description: 'User ID' }) - id!: string; - @ApiProperty({ description: 'User name' }) - name!: string; - @ApiProperty({ description: 'User email' }) - email!: string; - @ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', description: 'User avatar color' }) - avatarColor!: UserAvatarColor | null; - @ApiProperty({ description: 'User deleted at' }) - deletedAt!: Date | null; - @ApiProperty({ description: 'User has profile image' }) - hasProfileImage!: boolean; - @ApiProperty({ description: 'User profile changed at' }) - profileChangedAt!: Date; -} +const SyncUserV1Schema = z + .object({ + id: z.string().describe('User ID'), + name: z.string().describe('User name'), + email: z.string().describe('User email'), + avatarColor: UserAvatarColorSchema.nullish(), + deletedAt: isoDatetimeToDate.nullable().describe('User deleted at'), + hasProfileImage: z.boolean().describe('User has profile image'), + profileChangedAt: isoDatetimeToDate.describe('User profile changed at'), + }) + .meta({ id: 'SyncUserV1' }); + +const SyncAuthUserV1Schema = SyncUserV1Schema.merge( + z.object({ + isAdmin: z.boolean().describe('User is admin'), + pinCode: z.string().nullable().describe('User pin code'), + oauthId: z.string().describe('User OAuth ID'), + storageLabel: z.string().nullable().describe('User storage label'), + quotaSizeInBytes: z.int().nullable().describe('Quota size in bytes'), + quotaUsageInBytes: z.int().describe('Quota usage in bytes'), + }), +).meta({ id: 'SyncAuthUserV1' }); + +const SyncUserDeleteV1Schema = z.object({ userId: z.string().describe('User ID') }).meta({ id: 'SyncUserDeleteV1' }); + +const SyncPartnerV1Schema = z + .object({ + sharedById: z.string().describe('Shared by ID'), + sharedWithId: z.string().describe('Shared with ID'), + inTimeline: z.boolean().describe('In timeline'), + }) + .meta({ id: 'SyncPartnerV1' }); + +const SyncPartnerDeleteV1Schema = z + .object({ + sharedById: z.string().describe('Shared by ID'), + sharedWithId: z.string().describe('Shared with ID'), + }) + .meta({ id: 'SyncPartnerDeleteV1' }); + +const SyncAssetV1Schema = z + .object({ + id: z.string().describe('Asset ID'), + ownerId: z.string().describe('Owner ID'), + originalFileName: z.string().describe('Original file name'), + thumbhash: z.string().nullable().describe('Thumbhash'), + checksum: z.string().describe('Checksum'), + fileCreatedAt: isoDatetimeToDate.nullable().describe('File created at'), + fileModifiedAt: isoDatetimeToDate.nullable().describe('File modified at'), + localDateTime: isoDatetimeToDate.nullable().describe('Local date time'), + duration: z.string().nullable().describe('Duration'), + type: AssetTypeSchema, + deletedAt: isoDatetimeToDate.nullable().describe('Deleted at'), + isFavorite: z.boolean().describe('Is favorite'), + visibility: AssetVisibilitySchema, + livePhotoVideoId: z.string().nullable().describe('Live photo video ID'), + stackId: z.string().nullable().describe('Stack ID'), + libraryId: z.string().nullable().describe('Library ID'), + width: z.int().nullable().describe('Asset width'), + height: z.int().nullable().describe('Asset height'), + isEdited: z.boolean().describe('Is edited'), + }) + .meta({ id: 'SyncAssetV1' }); @ExtraModel() -export class SyncAuthUserV1 extends SyncUserV1 { - @ApiProperty({ description: 'User is admin' }) - isAdmin!: boolean; - @ApiProperty({ description: 'User pin code' }) - pinCode!: string | null; - @ApiProperty({ description: 'User OAuth ID' }) - oauthId!: string; - @ApiProperty({ description: 'User storage label' }) - storageLabel!: string | null; - @ApiProperty({ type: 'integer' }) - quotaSizeInBytes!: number | null; - @ApiProperty({ type: 'integer' }) - quotaUsageInBytes!: number; -} +class SyncUserV1 extends createZodDto(SyncUserV1Schema) {} +@ExtraModel() +class SyncAuthUserV1 extends createZodDto(SyncAuthUserV1Schema) {} +@ExtraModel() +class SyncUserDeleteV1 extends createZodDto(SyncUserDeleteV1Schema) {} +@ExtraModel() +class SyncPartnerV1 extends createZodDto(SyncPartnerV1Schema) {} +@ExtraModel() +class SyncPartnerDeleteV1 extends createZodDto(SyncPartnerDeleteV1Schema) {} +@ExtraModel() +export class SyncAssetV1 extends createZodDto(SyncAssetV1Schema) {} + +const SyncAssetDeleteV1Schema = z + .object({ assetId: z.string().describe('Asset ID') }) + .meta({ id: 'SyncAssetDeleteV1' }); + +const SyncAssetExifV1Schema = z + .object({ + assetId: z.string().describe('Asset ID'), + description: z.string().nullable().describe('Description'), + exifImageWidth: z.int().nullable().describe('Exif image width'), + exifImageHeight: z.int().nullable().describe('Exif image height'), + fileSizeInByte: z.int().nullable().describe('File size in byte'), + orientation: z.string().nullable().describe('Orientation'), + dateTimeOriginal: isoDatetimeToDate.nullable().describe('Date time original'), + modifyDate: isoDatetimeToDate.nullable().describe('Modify date'), + timeZone: z.string().nullable().describe('Time zone'), + latitude: z.number().meta({ format: 'double' }).nullable().describe('Latitude'), + longitude: z.number().meta({ format: 'double' }).nullable().describe('Longitude'), + projectionType: z.string().nullable().describe('Projection type'), + city: z.string().nullable().describe('City'), + state: z.string().nullable().describe('State'), + country: z.string().nullable().describe('Country'), + make: z.string().nullable().describe('Make'), + model: z.string().nullable().describe('Model'), + lensModel: z.string().nullable().describe('Lens model'), + fNumber: z.number().meta({ format: 'double' }).nullable().describe('F number'), + focalLength: z.number().meta({ format: 'double' }).nullable().describe('Focal length'), + iso: z.int().nullable().describe('ISO'), + exposureTime: z.string().nullable().describe('Exposure time'), + profileDescription: z.string().nullable().describe('Profile description'), + rating: z.int().nullable().describe('Rating'), + fps: z.number().meta({ format: 'double' }).nullable().describe('FPS'), + }) + .meta({ id: 'SyncAssetExifV1' }); + +const SyncAssetMetadataV1Schema = z + .object({ + assetId: z.string().describe('Asset ID'), + key: z.string().describe('Key'), + value: z.record(z.string(), z.unknown()).describe('Value'), + }) + .meta({ id: 'SyncAssetMetadataV1' }); + +const SyncAssetMetadataDeleteV1Schema = z + .object({ + assetId: z.string().describe('Asset ID'), + key: z.string().describe('Key'), + }) + .meta({ id: 'SyncAssetMetadataDeleteV1' }); + +const SyncAssetEditV1Schema = z + .object({ + id: z.string().describe('Edit ID'), + assetId: z.string().describe('Asset ID'), + action: AssetEditActionSchema, + parameters: z.record(z.string(), z.unknown()).describe('Edit parameters'), + sequence: z.int().describe('Edit sequence'), + }) + .meta({ id: 'SyncAssetEditV1' }); + +const SyncAssetEditDeleteV1Schema = z + .object({ editId: z.string().describe('Edit ID') }) + .meta({ id: 'SyncAssetEditDeleteV1' }); @ExtraModel() -export class SyncUserDeleteV1 { - @ApiProperty({ description: 'User ID' }) - userId!: string; -} +class SyncAssetDeleteV1 extends createZodDto(SyncAssetDeleteV1Schema) {} +@ExtraModel() +export class SyncAssetExifV1 extends createZodDto(SyncAssetExifV1Schema) {} +@ExtraModel() +class SyncAssetMetadataV1 extends createZodDto(SyncAssetMetadataV1Schema) {} +@ExtraModel() +class SyncAssetMetadataDeleteV1 extends createZodDto(SyncAssetMetadataDeleteV1Schema) {} +@ExtraModel() +export class SyncAssetEditV1 extends createZodDto(SyncAssetEditV1Schema) {} +@ExtraModel() +class SyncAssetEditDeleteV1 extends createZodDto(SyncAssetEditDeleteV1Schema) {} + +const SyncAlbumDeleteV1Schema = z + .object({ albumId: z.string().describe('Album ID') }) + .meta({ id: 'SyncAlbumDeleteV1' }); + +const SyncAlbumUserDeleteV1Schema = z + .object({ + albumId: z.string().describe('Album ID'), + userId: z.string().describe('User ID'), + }) + .meta({ id: 'SyncAlbumUserDeleteV1' }); + +const SyncAlbumUserV1Schema = z + .object({ + albumId: z.string().describe('Album ID'), + userId: z.string().describe('User ID'), + role: AlbumUserRoleSchema, + }) + .meta({ id: 'SyncAlbumUserV1' }); + +const SyncAlbumV1Schema = z + .object({ + id: z.string().describe('Album ID'), + ownerId: z.string().describe('Owner ID'), + name: z.string().describe('Album name'), + description: z.string().describe('Album description'), + createdAt: isoDatetimeToDate.describe('Created at'), + updatedAt: isoDatetimeToDate.describe('Updated at'), + thumbnailAssetId: z.string().nullable().describe('Thumbnail asset ID'), + isActivityEnabled: z.boolean().describe('Is activity enabled'), + order: AssetOrderSchema, + }) + .meta({ id: 'SyncAlbumV1' }); + +const SyncAlbumToAssetV1Schema = z + .object({ + albumId: z.string().describe('Album ID'), + assetId: z.string().describe('Asset ID'), + }) + .meta({ id: 'SyncAlbumToAssetV1' }); + +const SyncAlbumToAssetDeleteV1Schema = z + .object({ + albumId: z.string().describe('Album ID'), + assetId: z.string().describe('Asset ID'), + }) + .meta({ id: 'SyncAlbumToAssetDeleteV1' }); @ExtraModel() -export class SyncPartnerV1 { - @ApiProperty({ description: 'Shared by ID' }) - sharedById!: string; - @ApiProperty({ description: 'Shared with ID' }) - sharedWithId!: string; - @ApiProperty({ description: 'In timeline' }) - inTimeline!: boolean; -} +class SyncAlbumDeleteV1 extends createZodDto(SyncAlbumDeleteV1Schema) {} +@ExtraModel() +class SyncAlbumUserDeleteV1 extends createZodDto(SyncAlbumUserDeleteV1Schema) {} +@ExtraModel() +class SyncAlbumUserV1 extends createZodDto(SyncAlbumUserV1Schema) {} +@ExtraModel() +class SyncAlbumV1 extends createZodDto(SyncAlbumV1Schema) {} +@ExtraModel() +class SyncAlbumToAssetV1 extends createZodDto(SyncAlbumToAssetV1Schema) {} +@ExtraModel() +class SyncAlbumToAssetDeleteV1 extends createZodDto(SyncAlbumToAssetDeleteV1Schema) {} + +const SyncMemoryV1Schema = z + .object({ + id: z.string().describe('Memory ID'), + createdAt: isoDatetimeToDate.describe('Created at'), + updatedAt: isoDatetimeToDate.describe('Updated at'), + deletedAt: isoDatetimeToDate.nullable().describe('Deleted at'), + ownerId: z.string().describe('Owner ID'), + type: MemoryTypeSchema, + data: z.record(z.string(), z.unknown()).describe('Data'), + isSaved: z.boolean().describe('Is saved'), + memoryAt: isoDatetimeToDate.describe('Memory at'), + seenAt: isoDatetimeToDate.nullable().describe('Seen at'), + showAt: isoDatetimeToDate.nullable().describe('Show at'), + hideAt: isoDatetimeToDate.nullable().describe('Hide at'), + }) + .meta({ id: 'SyncMemoryV1' }); + +const SyncMemoryDeleteV1Schema = z + .object({ memoryId: z.string().describe('Memory ID') }) + .meta({ id: 'SyncMemoryDeleteV1' }); + +const SyncMemoryAssetV1Schema = z + .object({ + memoryId: z.string().describe('Memory ID'), + assetId: z.string().describe('Asset ID'), + }) + .meta({ id: 'SyncMemoryAssetV1' }); + +const SyncMemoryAssetDeleteV1Schema = z + .object({ + memoryId: z.string().describe('Memory ID'), + assetId: z.string().describe('Asset ID'), + }) + .meta({ id: 'SyncMemoryAssetDeleteV1' }); + +const SyncStackV1Schema = z + .object({ + id: z.string().describe('Stack ID'), + createdAt: isoDatetimeToDate.describe('Created at'), + updatedAt: isoDatetimeToDate.describe('Updated at'), + primaryAssetId: z.string().describe('Primary asset ID'), + ownerId: z.string().describe('Owner ID'), + }) + .meta({ id: 'SyncStackV1' }); + +const SyncStackDeleteV1Schema = z + .object({ stackId: z.string().describe('Stack ID') }) + .meta({ id: 'SyncStackDeleteV1' }); + +const SyncPersonV1Schema = z + .object({ + id: z.string().describe('Person ID'), + createdAt: isoDatetimeToDate.describe('Created at'), + updatedAt: isoDatetimeToDate.describe('Updated at'), + ownerId: z.string().describe('Owner ID'), + name: z.string().describe('Person name'), + birthDate: isoDatetimeToDate.nullable().describe('Birth date'), + isHidden: z.boolean().describe('Is hidden'), + isFavorite: z.boolean().describe('Is favorite'), + color: z.string().nullable().describe('Color'), + faceAssetId: z.string().nullable().describe('Face asset ID'), + }) + .meta({ id: 'SyncPersonV1' }); + +const SyncPersonDeleteV1Schema = z + .object({ personId: z.string().describe('Person ID') }) + .meta({ id: 'SyncPersonDeleteV1' }); + +const SyncAssetFaceV1Schema = z + .object({ + id: z.string().describe('Asset face ID'), + assetId: z.string().describe('Asset ID'), + personId: z.string().nullable().describe('Person ID'), + imageWidth: z.int().describe('Image width'), + imageHeight: z.int().describe('Image height'), + boundingBoxX1: z.int().describe('Bounding box X1'), + boundingBoxY1: z.int().describe('Bounding box Y1'), + boundingBoxX2: z.int().describe('Bounding box X2'), + boundingBoxY2: z.int().describe('Bounding box Y2'), + sourceType: z.string().describe('Source type'), + }) + .meta({ id: 'SyncAssetFaceV1' }); + +const SyncAssetFaceV2Schema = SyncAssetFaceV1Schema.extend({ + deletedAt: isoDatetimeToDate.nullable().describe('Face deleted at'), + isVisible: z.boolean().describe('Is the face visible in the asset'), +}).meta({ id: 'SyncAssetFaceV2' }); + +const SyncAssetFaceDeleteV1Schema = z + .object({ assetFaceId: z.string().describe('Asset face ID') }) + .meta({ id: 'SyncAssetFaceDeleteV1' }); + +const SyncUserMetadataV1Schema = z + .object({ + userId: z.string().describe('User ID'), + key: UserMetadataKeySchema, + value: z.record(z.string(), z.unknown()).describe('User metadata value'), + }) + .meta({ id: 'SyncUserMetadataV1' }); + +const SyncUserMetadataDeleteV1Schema = z + .object({ + userId: z.string().describe('User ID'), + key: UserMetadataKeySchema, + }) + .meta({ id: 'SyncUserMetadataDeleteV1' }); + +const SyncAckV1Schema = z.object({}).meta({ id: 'SyncAckV1' }); +const SyncResetV1Schema = z.object({}).meta({ id: 'SyncResetV1' }); +const SyncCompleteV1Schema = z.object({}).meta({ id: 'SyncCompleteV1' }); @ExtraModel() -export class SyncPartnerDeleteV1 { - @ApiProperty({ description: 'Shared by ID' }) - sharedById!: string; - @ApiProperty({ description: 'Shared with ID' }) - sharedWithId!: string; -} - +class SyncMemoryV1 extends createZodDto(SyncMemoryV1Schema) {} @ExtraModel() -export class SyncAssetV1 { - @ApiProperty({ description: 'Asset ID' }) - id!: string; - @ApiProperty({ description: 'Owner ID' }) - ownerId!: string; - @ApiProperty({ description: 'Original file name' }) - originalFileName!: string; - @ApiProperty({ description: 'Thumbhash' }) - thumbhash!: string | null; - @ApiProperty({ description: 'Checksum' }) - checksum!: string; - @ApiProperty({ description: 'File created at' }) - fileCreatedAt!: Date | null; - @ApiProperty({ description: 'File modified at' }) - fileModifiedAt!: Date | null; - @ApiProperty({ description: 'Local date time' }) - localDateTime!: Date | null; - @ApiProperty({ description: 'Duration' }) - duration!: string | null; - @ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum', description: 'Asset type' }) - type!: AssetType; - @ApiProperty({ description: 'Deleted at' }) - deletedAt!: Date | null; - @ApiProperty({ description: 'Is favorite' }) - isFavorite!: boolean; - @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', description: 'Asset visibility' }) - visibility!: AssetVisibility; - @ApiProperty({ description: 'Live photo video ID' }) - livePhotoVideoId!: string | null; - @ApiProperty({ description: 'Stack ID' }) - stackId!: string | null; - @ApiProperty({ description: 'Library ID' }) - libraryId!: string | null; - @ApiProperty({ type: 'integer', description: 'Asset width' }) - width!: number | null; - @ApiProperty({ type: 'integer', description: 'Asset height' }) - height!: number | null; - @ApiProperty({ description: 'Is edited' }) - isEdited!: boolean; -} - +class SyncMemoryDeleteV1 extends createZodDto(SyncMemoryDeleteV1Schema) {} @ExtraModel() -export class SyncAssetDeleteV1 { - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; -} - +class SyncMemoryAssetV1 extends createZodDto(SyncMemoryAssetV1Schema) {} @ExtraModel() -export class SyncAssetExifV1 { - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; - @ApiProperty({ description: 'Description' }) - description!: string | null; - @ApiProperty({ type: 'integer', description: 'Exif image width' }) - exifImageWidth!: number | null; - @ApiProperty({ type: 'integer', description: 'Exif image height' }) - exifImageHeight!: number | null; - @ApiProperty({ type: 'integer', description: 'File size in byte' }) - fileSizeInByte!: number | null; - @ApiProperty({ description: 'Orientation' }) - orientation!: string | null; - @ApiProperty({ description: 'Date time original' }) - dateTimeOriginal!: Date | null; - @ApiProperty({ description: 'Modify date' }) - modifyDate!: Date | null; - @ApiProperty({ description: 'Time zone' }) - timeZone!: string | null; - @ApiProperty({ type: 'number', format: 'double', description: 'Latitude' }) - latitude!: number | null; - @ApiProperty({ type: 'number', format: 'double', description: 'Longitude' }) - longitude!: number | null; - @ApiProperty({ description: 'Projection type' }) - projectionType!: string | null; - @ApiProperty({ description: 'City' }) - city!: string | null; - @ApiProperty({ description: 'State' }) - state!: string | null; - @ApiProperty({ description: 'Country' }) - country!: string | null; - @ApiProperty({ description: 'Make' }) - make!: string | null; - @ApiProperty({ description: 'Model' }) - model!: string | null; - @ApiProperty({ description: 'Lens model' }) - lensModel!: string | null; - @ApiProperty({ type: 'number', format: 'double', description: 'F number' }) - fNumber!: number | null; - @ApiProperty({ type: 'number', format: 'double', description: 'Focal length' }) - focalLength!: number | null; - @ApiProperty({ type: 'integer', description: 'ISO' }) - iso!: number | null; - @ApiProperty({ description: 'Exposure time' }) - exposureTime!: string | null; - @ApiProperty({ description: 'Profile description' }) - profileDescription!: string | null; - @ApiProperty({ type: 'integer', description: 'Rating' }) - rating!: number | null; - @ApiProperty({ type: 'number', format: 'double', description: 'FPS' }) - fps!: number | null; -} - +class SyncMemoryAssetDeleteV1 extends createZodDto(SyncMemoryAssetDeleteV1Schema) {} @ExtraModel() -export class SyncAssetEditV1 { - id!: string; - assetId!: string; - - @ValidateEnum({ enum: AssetEditAction, name: 'AssetEditAction' }) - action!: AssetEditAction; - parameters!: object; - - @ApiProperty({ type: 'integer' }) - sequence!: number; -} - +class SyncStackV1 extends createZodDto(SyncStackV1Schema) {} @ExtraModel() -export class SyncAssetEditDeleteV1 { - editId!: string; -} - +class SyncStackDeleteV1 extends createZodDto(SyncStackDeleteV1Schema) {} @ExtraModel() -export class SyncAssetMetadataV1 { - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; - @ApiProperty({ description: 'Key' }) - key!: string; - @ApiProperty({ description: 'Value' }) - value!: object; -} - +class SyncPersonV1 extends createZodDto(SyncPersonV1Schema) {} @ExtraModel() -export class SyncAssetMetadataDeleteV1 { - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; - @ApiProperty({ description: 'Key' }) - key!: string; -} - +class SyncPersonDeleteV1 extends createZodDto(SyncPersonDeleteV1Schema) {} @ExtraModel() -export class SyncAlbumDeleteV1 { - @ApiProperty({ description: 'Album ID' }) - albumId!: string; -} - +class SyncAssetFaceV1 extends createZodDto(SyncAssetFaceV1Schema) {} @ExtraModel() -export class SyncAlbumUserDeleteV1 { - @ApiProperty({ description: 'Album ID' }) - albumId!: string; - @ApiProperty({ description: 'User ID' }) - userId!: string; -} - -@ExtraModel() -export class SyncAlbumUserV1 { - @ApiProperty({ description: 'Album ID' }) - albumId!: string; - @ApiProperty({ description: 'User ID' }) - userId!: string; - @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', description: 'Album user role' }) - role!: AlbumUserRole; -} - -@ExtraModel() -export class SyncAlbumV1 { - @ApiProperty({ description: 'Album ID' }) - id!: string; - @ApiProperty({ description: 'Owner ID' }) - ownerId!: string; - @ApiProperty({ description: 'Album name' }) - name!: string; - @ApiProperty({ description: 'Album description' }) - description!: string; - @ApiProperty({ description: 'Created at' }) - createdAt!: Date; - @ApiProperty({ description: 'Updated at' }) - updatedAt!: Date; - @ApiProperty({ description: 'Thumbnail asset ID' }) - thumbnailAssetId!: string | null; - @ApiProperty({ description: 'Is activity enabled' }) - isActivityEnabled!: boolean; - @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder' }) - order!: AssetOrder; -} - -@ExtraModel() -export class SyncAlbumToAssetV1 { - @ApiProperty({ description: 'Album ID' }) - albumId!: string; - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; -} - -@ExtraModel() -export class SyncAlbumToAssetDeleteV1 { - @ApiProperty({ description: 'Album ID' }) - albumId!: string; - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; -} - -@ExtraModel() -export class SyncMemoryV1 { - @ApiProperty({ description: 'Memory ID' }) - id!: string; - @ApiProperty({ description: 'Created at' }) - createdAt!: Date; - @ApiProperty({ description: 'Updated at' }) - updatedAt!: Date; - @ApiProperty({ description: 'Deleted at' }) - deletedAt!: Date | null; - @ApiProperty({ description: 'Owner ID' }) - ownerId!: string; - @ValidateEnum({ enum: MemoryType, name: 'MemoryType', description: 'Memory type' }) - type!: MemoryType; - @ApiProperty({ description: 'Data' }) - data!: object; - @ApiProperty({ description: 'Is saved' }) - isSaved!: boolean; - @ApiProperty({ description: 'Memory at' }) - memoryAt!: Date; - @ApiProperty({ description: 'Seen at' }) - seenAt!: Date | null; - @ApiProperty({ description: 'Show at' }) - showAt!: Date | null; - @ApiProperty({ description: 'Hide at' }) - hideAt!: Date | null; -} - -@ExtraModel() -export class SyncMemoryDeleteV1 { - @ApiProperty({ description: 'Memory ID' }) - memoryId!: string; -} - -@ExtraModel() -export class SyncMemoryAssetV1 { - @ApiProperty({ description: 'Memory ID' }) - memoryId!: string; - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; -} - -@ExtraModel() -export class SyncMemoryAssetDeleteV1 { - @ApiProperty({ description: 'Memory ID' }) - memoryId!: string; - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; -} - -@ExtraModel() -export class SyncStackV1 { - @ApiProperty({ description: 'Stack ID' }) - id!: string; - @ApiProperty({ description: 'Created at' }) - createdAt!: Date; - @ApiProperty({ description: 'Updated at' }) - updatedAt!: Date; - @ApiProperty({ description: 'Primary asset ID' }) - primaryAssetId!: string; - @ApiProperty({ description: 'Owner ID' }) - ownerId!: string; -} - -@ExtraModel() -export class SyncStackDeleteV1 { - @ApiProperty({ description: 'Stack ID' }) - stackId!: string; -} - -@ExtraModel() -export class SyncPersonV1 { - @ApiProperty({ description: 'Person ID' }) - id!: string; - @ApiProperty({ description: 'Created at' }) - createdAt!: Date; - @ApiProperty({ description: 'Updated at' }) - updatedAt!: Date; - @ApiProperty({ description: 'Owner ID' }) - ownerId!: string; - @ApiProperty({ description: 'Person name' }) - name!: string; - @ApiProperty({ description: 'Birth date' }) - birthDate!: Date | null; - @ApiProperty({ description: 'Is hidden' }) - isHidden!: boolean; - @ApiProperty({ description: 'Is favorite' }) - isFavorite!: boolean; - @ApiProperty({ description: 'Color' }) - color!: string | null; - @ApiProperty({ description: 'Face asset ID' }) - faceAssetId!: string | null; -} - -@ExtraModel() -export class SyncPersonDeleteV1 { - @ApiProperty({ description: 'Person ID' }) - personId!: string; -} - -@ExtraModel() -export class SyncAssetFaceV1 { - @ApiProperty({ description: 'Asset face ID' }) - id!: string; - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; - @ApiProperty({ description: 'Person ID' }) - personId!: string | null; - @ApiProperty({ type: 'integer' }) - imageWidth!: number; - @ApiProperty({ type: 'integer' }) - imageHeight!: number; - @ApiProperty({ type: 'integer' }) - boundingBoxX1!: number; - @ApiProperty({ type: 'integer' }) - boundingBoxY1!: number; - @ApiProperty({ type: 'integer' }) - boundingBoxX2!: number; - @ApiProperty({ type: 'integer' }) - boundingBoxY2!: number; - @ApiProperty({ description: 'Source type' }) - sourceType!: string; -} - -@ExtraModel() -export class SyncAssetFaceV2 extends SyncAssetFaceV1 { - @ApiProperty({ description: 'Face deleted at' }) - deletedAt!: Date | null; - @ApiProperty({ description: 'Is the face visible in the asset' }) - isVisible!: boolean; -} +class SyncAssetFaceV2 extends createZodDto(SyncAssetFaceV2Schema) {} export function syncAssetFaceV2ToV1(faceV2: SyncAssetFaceV2): SyncAssetFaceV1 { const { deletedAt: _, isVisible: __, ...faceV1 } = faceV2; return faceV1; } - @ExtraModel() -export class SyncAssetFaceDeleteV1 { - @ApiProperty({ description: 'Asset face ID' }) - assetFaceId!: string; -} - +class SyncAssetFaceDeleteV1 extends createZodDto(SyncAssetFaceDeleteV1Schema) {} @ExtraModel() -export class SyncUserMetadataV1 { - @ApiProperty({ description: 'User ID' }) - userId!: string; - @ValidateEnum({ enum: UserMetadataKey, name: 'UserMetadataKey', description: 'User metadata key' }) - key!: UserMetadataKey; - @ApiProperty({ description: 'User metadata value' }) - value!: UserMetadata[UserMetadataKey]; -} - +class SyncUserMetadataV1 extends createZodDto(SyncUserMetadataV1Schema) {} @ExtraModel() -export class SyncUserMetadataDeleteV1 { - @ApiProperty({ description: 'User ID' }) - userId!: string; - @ValidateEnum({ enum: UserMetadataKey, name: 'UserMetadataKey', description: 'User metadata key' }) - key!: UserMetadataKey; -} - +class SyncUserMetadataDeleteV1 extends createZodDto(SyncUserMetadataDeleteV1Schema) {} @ExtraModel() -export class SyncAckV1 {} - +class SyncAckV1 extends createZodDto(SyncAckV1Schema) {} @ExtraModel() -export class SyncResetV1 {} - +class SyncResetV1 extends createZodDto(SyncResetV1Schema) {} @ExtraModel() -export class SyncCompleteV1 {} +class SyncCompleteV1 extends createZodDto(SyncCompleteV1Schema) {} export type SyncItem = { [SyncEntityType.AuthUserV1]: SyncAuthUserV1; @@ -541,35 +441,33 @@ export type SyncItem = { [SyncEntityType.SyncResetV1]: SyncResetV1; }; -export class SyncStreamDto { - @ValidateEnum({ enum: SyncRequestType, name: 'SyncRequestType', each: true, description: 'Sync request types' }) - types!: SyncRequestType[]; - - @ValidateBoolean({ optional: true, description: 'Reset sync state' }) - reset?: boolean; -} - -export class SyncAckDto { - @ValidateEnum({ enum: SyncEntityType, name: 'SyncEntityType', description: 'Sync entity type' }) - type!: SyncEntityType; - @ApiProperty({ description: 'Acknowledgment ID' }) - ack!: string; -} - -export class SyncAckSetDto { - @ApiProperty({ description: 'Acknowledgment IDs (max 1000)' }) - @ArrayMaxSize(1000) - @IsString({ each: true }) - acks!: string[]; -} - -export class SyncAckDeleteDto { - @ValidateEnum({ - enum: SyncEntityType, - name: 'SyncEntityType', - optional: true, - each: true, - description: 'Sync entity types to delete acks for', +const SyncStreamSchema = z + .object({ + types: z.array(SyncRequestTypeSchema).describe('Sync request types'), + reset: z.boolean().optional().describe('Reset sync state'), }) - types?: SyncEntityType[]; -} + .meta({ id: 'SyncStreamDto' }); + +const SyncAckSchema = z + .object({ + type: SyncEntityTypeSchema, + ack: z.string().describe('Acknowledgment ID'), + }) + .meta({ id: 'SyncAckDto' }); + +const SyncAckSetSchema = z + .object({ + acks: z.array(z.string()).max(1000).describe('Acknowledgment IDs (max 1000)'), + }) + .meta({ id: 'SyncAckSetDto' }); + +const SyncAckDeleteSchema = z + .object({ + types: z.array(SyncEntityTypeSchema).optional().describe('Sync entity types to delete acks for'), + }) + .meta({ id: 'SyncAckDeleteDto' }); + +export class SyncStreamDto extends createZodDto(SyncStreamSchema) {} +export class SyncAckDto extends createZodDto(SyncAckSchema) {} +export class SyncAckSetDto extends createZodDto(SyncAckSetSchema) {} +export class SyncAckDeleteDto extends createZodDto(SyncAckDeleteSchema) {} diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index a214dbc467..b5222fd883 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -1,863 +1,374 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Transform, Type } from 'class-transformer'; -import { - ArrayMinSize, - IsInt, - IsNotEmpty, - IsNumber, - IsObject, - IsPositive, - IsString, - IsUrl, - Max, - Min, - ValidateIf, - ValidateNested, -} from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { SystemConfig } from 'src/config'; -import { CLIPConfig, DuplicateDetectionConfig, FacialRecognitionConfig, OcrConfig } from 'src/dtos/model-config.dto'; +import { + CLIPConfigSchema, + DuplicateDetectionConfigSchema, + FacialRecognitionConfigSchema, + OcrConfigSchema, +} from 'src/dtos/model-config.dto'; import { AudioCodec, - CQMode, - Colorspace, - ImageFormat, - LogLevel, - OAuthTokenEndpointAuthMethod, - QueueName, - ToneMapping, - TranscodeHardwareAcceleration, - TranscodePolicy, - VideoCodec, - VideoContainer, + AudioCodecSchema, + ColorspaceSchema, + CQModeSchema, + ImageFormatSchema, + LogLevelSchema, + OAuthTokenEndpointAuthMethodSchema, + ToneMappingSchema, + TranscodeHardwareAccelerationSchema, + TranscodePolicySchema, + VideoCodecSchema, + VideoContainerSchema, } from 'src/enum'; -import { ConcurrentQueueName } from 'src/types'; -import { IsCronExpression, IsDateStringFormat, Optional, ValidateBoolean, ValidateEnum } from 'src/validation'; +import { isValidTime } from 'src/validation'; +import z from 'zod'; -const isLibraryScanEnabled = (config: SystemConfigLibraryScanDto) => config.enabled; -const isOAuthEnabled = (config: SystemConfigOAuthDto) => config.enabled; -const isOAuthOverrideEnabled = (config: SystemConfigOAuthDto) => config.mobileOverrideEnabled; -const isEmailNotificationEnabled = (config: SystemConfigSmtpDto) => config.enabled; -const isDatabaseBackupEnabled = (config: DatabaseBackupConfig) => config.enabled; +/** Coerces 'true'/'false' strings to boolean, but also allows booleans. */ +const configBool = z + .preprocess((val) => { + if (val === 'true') { + return true; + } + if (val === 'false') { + return false; + } + return val; + }, z.boolean()) + .meta({ type: 'boolean' }); -export class DatabaseBackupConfig { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; +const JobSettingsSchema = z + .object({ + concurrency: z.int().min(1).describe('Concurrency'), + }) + .meta({ id: 'JobSettingsDto' }); - @ValidateIf(isDatabaseBackupEnabled) - @IsNotEmpty() - @IsCronExpression() - @IsString() - @ApiProperty({ description: 'Cron expression' }) - cronExpression!: string; +const cronExpressionSchema = z + .string() + .regex(/(((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,7}/, 'Invalid cron expression') + .describe('Cron expression'); - @IsInt() - @IsPositive() - @IsNotEmpty() - @ApiProperty({ description: 'Keep last amount' }) - keepLastAmount!: number; -} +const DatabaseBackupSchema = z + .object({ + enabled: configBool.describe('Enabled'), + cronExpression: cronExpressionSchema, + keepLastAmount: z.number().min(1).describe('Keep last amount'), + }) + .meta({ id: 'DatabaseBackupConfig' }); -export class SystemConfigBackupsDto { - @Type(() => DatabaseBackupConfig) - @ValidateNested() - @IsObject() - database!: DatabaseBackupConfig; -} +const SystemConfigBackupsSchema = z.object({ database: DatabaseBackupSchema }).meta({ id: 'SystemConfigBackupsDto' }); -export class SystemConfigFFmpegDto { - @IsInt() - @Min(0) - @Max(51) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'CRF' }) - crf!: number; +const SystemConfigFFmpegSchema = z + .object({ + crf: z.coerce.number().int().min(0).max(51).describe('CRF'), + threads: z.coerce.number().int().min(0).describe('Threads'), + preset: z.string().describe('Preset'), + targetVideoCodec: VideoCodecSchema, + acceptedVideoCodecs: z.array(VideoCodecSchema).describe('Accepted video codecs'), + targetAudioCodec: AudioCodecSchema, + acceptedAudioCodecs: z + .array(AudioCodecSchema) + .transform((value): AudioCodec[] => value.map((v) => (v === AudioCodec.Libopus ? AudioCodec.Opus : v))) + .describe('Accepted audio codecs'), + acceptedContainers: z.array(VideoContainerSchema).describe('Accepted containers'), + targetResolution: z.string().describe('Target resolution'), + maxBitrate: z.string().describe('Max bitrate'), + bframes: z.coerce.number().int().min(-1).max(16).describe('B-frames'), + refs: z.coerce.number().int().min(0).max(6).describe('References'), + gopSize: z.coerce.number().int().min(0).describe('GOP size'), + temporalAQ: configBool.describe('Temporal AQ'), + cqMode: CQModeSchema, + twoPass: configBool.describe('Two pass'), + preferredHwDevice: z.string().describe('Preferred hardware device'), + transcode: TranscodePolicySchema, + accel: TranscodeHardwareAccelerationSchema, + accelDecode: configBool.describe('Accelerated decode'), + tonemap: ToneMappingSchema, + }) + .meta({ id: 'SystemConfigFFmpegDto' }); - @IsInt() - @Min(0) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'Threads' }) - threads!: number; +const SystemConfigJobSchema = z + .object({ + thumbnailGeneration: JobSettingsSchema, + metadataExtraction: JobSettingsSchema, + videoConversion: JobSettingsSchema, + faceDetection: JobSettingsSchema, + smartSearch: JobSettingsSchema, + backgroundTask: JobSettingsSchema, + migration: JobSettingsSchema, + search: JobSettingsSchema, + sidecar: JobSettingsSchema, + library: JobSettingsSchema, + notifications: JobSettingsSchema, + ocr: JobSettingsSchema, + workflow: JobSettingsSchema, + editor: JobSettingsSchema, + }) + .meta({ id: 'SystemConfigJobDto' }); - @IsString() - @ApiProperty({ description: 'Preset' }) - preset!: string; +const SystemConfigLibraryScanSchema = z + .object({ + enabled: configBool.describe('Enabled'), + cronExpression: cronExpressionSchema, + }) + .meta({ id: 'SystemConfigLibraryScanDto' }); - @ValidateEnum({ enum: VideoCodec, name: 'VideoCodec', description: 'Target video codec' }) - targetVideoCodec!: VideoCodec; +const SystemConfigLibraryWatchSchema = z + .object({ enabled: configBool.describe('Enabled') }) + .meta({ id: 'SystemConfigLibraryWatchDto' }); - @ValidateEnum({ enum: VideoCodec, name: 'VideoCodec', each: true, description: 'Accepted video codecs' }) - acceptedVideoCodecs!: VideoCodec[]; +const SystemConfigLibrarySchema = z + .object({ scan: SystemConfigLibraryScanSchema, watch: SystemConfigLibraryWatchSchema }) + .meta({ id: 'SystemConfigLibraryDto' }); - @ValidateEnum({ enum: AudioCodec, name: 'AudioCodec', description: 'Target audio codec' }) - targetAudioCodec!: AudioCodec; +const SystemConfigLoggingSchema = z + .object({ + enabled: configBool.describe('Enabled'), + level: LogLevelSchema, + }) + .meta({ id: 'SystemConfigLoggingDto' }); - @ValidateEnum({ enum: AudioCodec, name: 'AudioCodec', each: true, description: 'Accepted audio codecs' }) - @Transform(({ value }) => { - if (Array.isArray(value)) { - const libopusIndex = value.indexOf('libopus'); - if (libopusIndex !== -1) { - value[libopusIndex] = 'opus'; - } +const MachineLearningAvailabilityChecksSchema = z + .object({ + enabled: configBool.describe('Enabled'), + timeout: z.number(), + interval: z.number(), + }) + .meta({ id: 'MachineLearningAvailabilityChecksDto' }); + +const SystemConfigMachineLearningSchema = z + .object({ + enabled: configBool.describe('Enabled'), + urls: z.array(z.string()).min(1).describe('ML service URLs'), + availabilityChecks: MachineLearningAvailabilityChecksSchema, + clip: CLIPConfigSchema, + duplicateDetection: DuplicateDetectionConfigSchema, + facialRecognition: FacialRecognitionConfigSchema, + ocr: OcrConfigSchema, + }) + .meta({ id: 'SystemConfigMachineLearningDto' }); + +const SystemConfigMapSchema = z + .object({ + enabled: configBool.describe('Enabled'), + lightStyle: z.url().describe('Light map style URL'), + darkStyle: z.url().describe('Dark map style URL'), + }) + .meta({ id: 'SystemConfigMapDto' }); + +const SystemConfigNewVersionCheckSchema = z + .object({ enabled: configBool.describe('Enabled') }) + .meta({ id: 'SystemConfigNewVersionCheckDto' }); + +const SystemConfigNightlyTasksSchema = z + .object({ + startTime: isValidTime.describe('Start time'), + databaseCleanup: configBool.describe('Database cleanup'), + missingThumbnails: configBool.describe('Missing thumbnails'), + clusterNewFaces: configBool.describe('Cluster new faces'), + generateMemories: configBool.describe('Generate memories'), + syncQuotaUsage: configBool.describe('Sync quota usage'), + }) + .meta({ id: 'SystemConfigNightlyTasksDto' }); + +const SystemConfigOAuthSchema = z + .object({ + autoLaunch: configBool.describe('Auto launch'), + autoRegister: configBool.describe('Auto register'), + buttonText: z.string().describe('Button text'), + clientId: z.string().describe('Client ID'), + clientSecret: z.string().describe('Client secret'), + tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethodSchema, + timeout: z.int().min(1).describe('Timeout'), + defaultStorageQuota: z.number().min(0).nullable().describe('Default storage quota'), + enabled: configBool.describe('Enabled'), + issuerUrl: z.string().describe('Issuer URL'), + scope: z.string().describe('Scope'), + signingAlgorithm: z.string().describe('Signing algorithm'), + profileSigningAlgorithm: z.string().describe('Profile signing algorithm'), + storageLabelClaim: z.string().describe('Storage label claim'), + storageQuotaClaim: z.string().describe('Storage quota claim'), + roleClaim: z.string().describe('Role claim'), + mobileOverrideEnabled: configBool.describe('Mobile override enabled'), + mobileRedirectUri: z.string().describe('Mobile redirect URI (set to empty string to disable)'), + }) + .transform((value, ctx) => { + if (!value.mobileOverrideEnabled || value.mobileRedirectUri === '') { + return value; + } + + if (!z.url().safeParse(value.mobileRedirectUri).success) { + ctx.issues.push({ + code: 'custom', + message: 'Mobile redirect URI must be an empty string or a valid URL', + input: value.mobileRedirectUri, + }); + return z.NEVER; } return value; }) - acceptedAudioCodecs!: AudioCodec[]; + .meta({ + id: 'SystemConfigOAuthDto', + }); - @ValidateEnum({ enum: VideoContainer, name: 'VideoContainer', each: true, description: 'Accepted containers' }) - acceptedContainers!: VideoContainer[]; +const SystemConfigPasswordLoginSchema = z + .object({ enabled: configBool.describe('Enabled') }) + .meta({ id: 'SystemConfigPasswordLoginDto' }); - @IsString() - @ApiProperty({ description: 'Target resolution' }) - targetResolution!: string; +const SystemConfigReverseGeocodingSchema = z + .object({ enabled: configBool.describe('Enabled') }) + .meta({ id: 'SystemConfigReverseGeocodingDto' }); - @IsString() - @ApiProperty({ description: 'Max bitrate' }) - maxBitrate!: string; +const SystemConfigFacesSchema = z + .object({ import: configBool.describe('Import') }) + .meta({ id: 'SystemConfigFacesDto' }); +const SystemConfigMetadataSchema = z.object({ faces: SystemConfigFacesSchema }).meta({ id: 'SystemConfigMetadataDto' }); - @IsInt() - @Min(-1) - @Max(16) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'B-frames' }) - bframes!: number; - - @IsInt() - @Min(0) - @Max(6) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'References' }) - refs!: number; - - @IsInt() - @Min(0) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'GOP size' }) - gopSize!: number; - - @ValidateBoolean({ description: 'Temporal AQ' }) - temporalAQ!: boolean; - - @ValidateEnum({ enum: CQMode, name: 'CQMode', description: 'CQ mode' }) - cqMode!: CQMode; - - @ValidateBoolean({ description: 'Two pass' }) - twoPass!: boolean; - - @ApiProperty({ description: 'Preferred hardware device' }) - @IsString() - preferredHwDevice!: string; - - @ValidateEnum({ enum: TranscodePolicy, name: 'TranscodePolicy', description: 'Transcode policy' }) - transcode!: TranscodePolicy; - - @ValidateEnum({ - enum: TranscodeHardwareAcceleration, - name: 'TranscodeHWAccel', - description: 'Transcode hardware acceleration', +const SystemConfigServerSchema = z + .object({ + externalDomain: z + .string() + .refine((url) => url.length === 0 || z.url().safeParse(url).success, { + error: 'External domain must be an empty string or a valid URL', + }) + .describe('External domain'), + loginPageMessage: z.string().describe('Login page message'), + publicUsers: configBool.describe('Public users'), }) - accel!: TranscodeHardwareAcceleration; + .meta({ id: 'SystemConfigServerDto' }); - @ValidateBoolean({ description: 'Accelerated decode' }) - accelDecode!: boolean; - - @ValidateEnum({ enum: ToneMapping, name: 'ToneMapping', description: 'Tone mapping' }) - tonemap!: ToneMapping; -} - -class JobSettingsDto { - @IsInt() - @IsPositive() - @ApiProperty({ type: 'integer', description: 'Concurrency' }) - concurrency!: number; -} - -class SystemConfigJobDto implements Record { - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.ThumbnailGeneration]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.MetadataExtraction]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.VideoConversion]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.SmartSearch]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.Migration]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.BackgroundTask]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.Search]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.FaceDetection]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.Ocr]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.Sidecar]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.Library]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.Notification]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.Workflow]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.Editor]!: JobSettingsDto; -} - -class SystemConfigLibraryScanDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; - - @ValidateIf(isLibraryScanEnabled) - @IsNotEmpty() - @IsCronExpression() - @IsString() - cronExpression!: string; -} - -class SystemConfigLibraryWatchDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; -} - -class SystemConfigLibraryDto { - @Type(() => SystemConfigLibraryScanDto) - @ValidateNested() - @IsObject() - scan!: SystemConfigLibraryScanDto; - - @Type(() => SystemConfigLibraryWatchDto) - @ValidateNested() - @IsObject() - watch!: SystemConfigLibraryWatchDto; -} - -class SystemConfigLoggingDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; - - @ValidateEnum({ enum: LogLevel, name: 'LogLevel' }) - level!: LogLevel; -} - -class MachineLearningAvailabilityChecksDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; - - @IsInt() - timeout!: number; - - @IsInt() - interval!: number; -} - -class SystemConfigMachineLearningDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; - - @IsUrl({ require_tld: false, allow_underscores: true }, { each: true }) - @ArrayMinSize(1) - @ValidateIf((dto) => dto.enabled) - @ApiProperty({ type: 'array', items: { type: 'string', format: 'uri' }, minItems: 1 }) - urls!: string[]; - - @Type(() => MachineLearningAvailabilityChecksDto) - @ValidateNested() - @IsObject() - availabilityChecks!: MachineLearningAvailabilityChecksDto; - - @Type(() => CLIPConfig) - @ValidateNested() - @IsObject() - clip!: CLIPConfig; - - @Type(() => DuplicateDetectionConfig) - @ValidateNested() - @IsObject() - duplicateDetection!: DuplicateDetectionConfig; - - @Type(() => FacialRecognitionConfig) - @ValidateNested() - @IsObject() - facialRecognition!: FacialRecognitionConfig; - - @Type(() => OcrConfig) - @ValidateNested() - @IsObject() - ocr!: OcrConfig; -} - -enum MapTheme { - LIGHT = 'light', - DARK = 'dark', -} - -export class MapThemeDto { - @ValidateEnum({ enum: MapTheme, name: 'MapTheme' }) - theme!: MapTheme; -} - -class SystemConfigMapDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; - - @IsNotEmpty() - @IsUrl() - lightStyle!: string; - - @IsNotEmpty() - @IsUrl() - darkStyle!: string; -} - -class SystemConfigNewVersionCheckDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; -} - -class SystemConfigNightlyTasksDto { - @IsDateStringFormat('HH:mm', { message: 'startTime must be in HH:mm format' }) - startTime!: string; - - @ValidateBoolean({ description: 'Database cleanup' }) - databaseCleanup!: boolean; - - @ValidateBoolean({ description: 'Missing thumbnails' }) - missingThumbnails!: boolean; - - @ValidateBoolean({ description: 'Cluster new faces' }) - clusterNewFaces!: boolean; - - @ValidateBoolean({ description: 'Generate memories' }) - generateMemories!: boolean; - - @ValidateBoolean({ description: 'Sync quota usage' }) - syncQuotaUsage!: boolean; -} - -class SystemConfigOAuthDto { - @ValidateBoolean({ description: 'Auto launch' }) - autoLaunch!: boolean; - - @ValidateBoolean({ description: 'Auto register' }) - autoRegister!: boolean; - - @IsString() - @ApiProperty({ description: 'Button text' }) - buttonText!: string; - - @ValidateIf(isOAuthEnabled) - @IsNotEmpty() - @IsString() - @ApiProperty({ description: 'Client ID' }) - clientId!: string; - - @ValidateIf(isOAuthEnabled) - @IsString() - @ApiProperty({ description: 'Client secret' }) - clientSecret!: string; - - @ValidateEnum({ - enum: OAuthTokenEndpointAuthMethod, - name: 'OAuthTokenEndpointAuthMethod', - description: 'Token endpoint auth method', +const SystemConfigSmtpTransportSchema = z + .object({ + ignoreCert: configBool.describe('Whether to ignore SSL certificate errors'), + host: z.string().describe('SMTP server hostname'), + port: z.number().min(0).max(65_535).describe('SMTP server port'), + secure: configBool.describe('Whether to use secure connection (TLS/SSL)'), + username: z.string().describe('SMTP username'), + password: z.string().describe('SMTP password'), }) - tokenEndpointAuthMethod!: OAuthTokenEndpointAuthMethod; - - @IsInt() - @IsPositive() - @Optional() - @ApiProperty({ type: 'integer', description: 'Timeout' }) - timeout!: number; - - @IsNumber() - @Min(0) - @Optional({ nullable: true }) - @ApiProperty({ type: 'integer', format: 'int64', description: 'Default storage quota' }) - defaultStorageQuota!: number | null; - - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; - - @ValidateIf(isOAuthEnabled) - @IsNotEmpty() - @IsString() - @ApiProperty({ description: 'Issuer URL' }) - issuerUrl!: string; - - @ValidateBoolean({ description: 'Mobile override enabled' }) - mobileOverrideEnabled!: boolean; - - @ValidateIf(isOAuthOverrideEnabled) - @IsUrl() - @ApiProperty({ description: 'Mobile redirect URI' }) - mobileRedirectUri!: string; - - @IsString() - @ApiProperty({ description: 'Scope' }) - scope!: string; - - @IsString() - @IsNotEmpty() - signingAlgorithm!: string; - - @IsString() - @IsNotEmpty() - @ApiProperty({ description: 'Profile signing algorithm' }) - profileSigningAlgorithm!: string; - - @IsString() - @ApiProperty({ description: 'Storage label claim' }) - storageLabelClaim!: string; - - @IsString() - @ApiProperty({ description: 'Storage quota claim' }) - storageQuotaClaim!: string; - - @IsString() - @ApiProperty({ description: 'Role claim' }) - roleClaim!: string; -} - -class SystemConfigPasswordLoginDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; -} - -class SystemConfigReverseGeocodingDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; -} - -class SystemConfigFacesDto { - @ValidateBoolean({ description: 'Import' }) - import!: boolean; -} - -class SystemConfigMetadataDto { - @Type(() => SystemConfigFacesDto) - @ValidateNested() - @IsObject() - faces!: SystemConfigFacesDto; -} - -class SystemConfigServerDto { - @ValidateIf((_, value: string) => value !== '') - @IsUrl({ require_tld: false, require_protocol: true, protocols: ['http', 'https'] }) - @ApiProperty({ description: 'External domain' }) - externalDomain!: string; - - @IsString() - @ApiProperty({ description: 'Login page message' }) - loginPageMessage!: string; - - @ValidateBoolean({ description: 'Public users' }) - publicUsers!: boolean; -} - -class SystemConfigSmtpTransportDto { - @ValidateBoolean({ description: 'Whether to ignore SSL certificate errors' }) - ignoreCert!: boolean; - - @ApiProperty({ description: 'SMTP server hostname' }) - @IsNotEmpty() - @IsString() - host!: string; - - @ApiProperty({ description: 'SMTP server port', type: Number, minimum: 0, maximum: 65_535 }) - @IsNumber() - @Min(0) - @Max(65_535) - port!: number; - - @ValidateBoolean({ description: 'Whether to use secure connection (TLS/SSL)' }) - secure!: boolean; - - @ApiProperty({ description: 'SMTP username' }) - @IsString() - username!: string; - - @ApiProperty({ description: 'SMTP password' }) - @IsString() - password!: string; -} - -export class SystemConfigSmtpDto { - @ValidateBoolean({ description: 'Whether SMTP email notifications are enabled' }) - enabled!: boolean; - - @ApiProperty({ description: 'Email address to send from' }) - @ValidateIf(isEmailNotificationEnabled) - @IsNotEmpty() - @IsString() - @IsNotEmpty() - from!: string; - - @ApiProperty({ description: 'Email address for replies' }) - @IsString() - replyTo!: string; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @ValidateIf(isEmailNotificationEnabled) - @Type(() => SystemConfigSmtpTransportDto) - @ValidateNested() - @IsObject() - transport!: SystemConfigSmtpTransportDto; -} - -class SystemConfigNotificationsDto { - @Type(() => SystemConfigSmtpDto) - @ValidateNested() - @IsObject() - smtp!: SystemConfigSmtpDto; -} - -class SystemConfigTemplateEmailsDto { - @IsString() - albumInviteTemplate!: string; - - @IsString() - welcomeTemplate!: string; - - @IsString() - albumUpdateTemplate!: string; -} - -class SystemConfigTemplatesDto { - @Type(() => SystemConfigTemplateEmailsDto) - @ValidateNested() - @IsObject() - email!: SystemConfigTemplateEmailsDto; -} - -class SystemConfigStorageTemplateDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; - - @ValidateBoolean({ description: 'Hash verification enabled' }) - hashVerificationEnabled!: boolean; - - @IsNotEmpty() - @IsString() - @ApiProperty({ description: 'Template' }) - template!: string; -} - -export class SystemConfigTemplateStorageOptionDto { - @ApiProperty({ description: 'Available year format options for storage template' }) - yearOptions!: string[]; - @ApiProperty({ description: 'Available month format options for storage template' }) - monthOptions!: string[]; - @ApiProperty({ description: 'Available week format options for storage template' }) - weekOptions!: string[]; - @ApiProperty({ description: 'Available day format options for storage template' }) - dayOptions!: string[]; - @ApiProperty({ description: 'Available hour format options for storage template' }) - hourOptions!: string[]; - @ApiProperty({ description: 'Available minute format options for storage template' }) - minuteOptions!: string[]; - @ApiProperty({ description: 'Available second format options for storage template' }) - secondOptions!: string[]; - @ApiProperty({ description: 'Available preset template options' }) - presetOptions!: string[]; -} - -export class SystemConfigThemeDto { - @ApiProperty({ description: 'Custom CSS for theming' }) - @IsString() - customCss!: string; -} - -class SystemConfigGeneratedImageDto { - @ValidateEnum({ enum: ImageFormat, name: 'ImageFormat', description: 'Image format' }) - format!: ImageFormat; - - @IsInt() - @Min(1) - @Max(100) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'Quality' }) - quality!: number; - - @IsInt() - @Min(1) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'Size' }) - size!: number; - - @ValidateBoolean({ optional: true, default: false }) - progressive?: boolean; -} - -class SystemConfigGeneratedFullsizeImageDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; - - @ValidateEnum({ enum: ImageFormat, name: 'ImageFormat', description: 'Image format' }) - format!: ImageFormat; - - @IsInt() - @Min(1) - @Max(100) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'Quality' }) - quality!: number; - - @ValidateBoolean({ optional: true, default: false, description: 'Progressive' }) - progressive?: boolean; -} - -export class SystemConfigImageDto { - @Type(() => SystemConfigGeneratedImageDto) - @ValidateNested() - @IsObject() - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - thumbnail!: SystemConfigGeneratedImageDto; - - @Type(() => SystemConfigGeneratedImageDto) - @ValidateNested() - @IsObject() - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - preview!: SystemConfigGeneratedImageDto; - - @Type(() => SystemConfigGeneratedFullsizeImageDto) - @ValidateNested() - @IsObject() - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - fullsize!: SystemConfigGeneratedFullsizeImageDto; - - @ValidateEnum({ enum: Colorspace, name: 'Colorspace', description: 'Colorspace' }) - colorspace!: Colorspace; - - @ValidateBoolean({ description: 'Extract embedded' }) - extractEmbedded!: boolean; -} - -class SystemConfigTrashDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; - - @IsInt() - @Min(0) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'Days' }) - days!: number; -} - -class SystemConfigUserDto { - @IsInt() - @Min(1) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'Delete delay' }) - deleteDelay!: number; -} - -export class SystemConfigDto implements SystemConfig { - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigBackupsDto) - @ValidateNested() - @IsObject() - backup!: SystemConfigBackupsDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigFFmpegDto) - @ValidateNested() - @IsObject() - ffmpeg!: SystemConfigFFmpegDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigLoggingDto) - @ValidateNested() - @IsObject() - logging!: SystemConfigLoggingDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigMachineLearningDto) - @ValidateNested() - @IsObject() - machineLearning!: SystemConfigMachineLearningDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigMapDto) - @ValidateNested() - @IsObject() - map!: SystemConfigMapDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigNewVersionCheckDto) - @ValidateNested() - @IsObject() - newVersionCheck!: SystemConfigNewVersionCheckDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigNightlyTasksDto) - @ValidateNested() - @IsObject() - nightlyTasks!: SystemConfigNightlyTasksDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigOAuthDto) - @ValidateNested() - @IsObject() - oauth!: SystemConfigOAuthDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigPasswordLoginDto) - @ValidateNested() - @IsObject() - passwordLogin!: SystemConfigPasswordLoginDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigReverseGeocodingDto) - @ValidateNested() - @IsObject() - reverseGeocoding!: SystemConfigReverseGeocodingDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigMetadataDto) - @ValidateNested() - @IsObject() - metadata!: SystemConfigMetadataDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigStorageTemplateDto) - @ValidateNested() - @IsObject() - storageTemplate!: SystemConfigStorageTemplateDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigJobDto) - @ValidateNested() - @IsObject() - job!: SystemConfigJobDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigImageDto) - @ValidateNested() - @IsObject() - image!: SystemConfigImageDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigTrashDto) - @ValidateNested() - @IsObject() - trash!: SystemConfigTrashDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigThemeDto) - @ValidateNested() - @IsObject() - theme!: SystemConfigThemeDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigLibraryDto) - @ValidateNested() - @IsObject() - library!: SystemConfigLibraryDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigNotificationsDto) - @ValidateNested() - @IsObject() - notifications!: SystemConfigNotificationsDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigTemplatesDto) - @ValidateNested() - @IsObject() - templates!: SystemConfigTemplatesDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigServerDto) - @ValidateNested() - @IsObject() - server!: SystemConfigServerDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigUserDto) - @ValidateNested() - @IsObject() - user!: SystemConfigUserDto; -} + .meta({ id: 'SystemConfigSmtpTransportDto' }); + +const SystemConfigSmtpSchema = z + .object({ + enabled: configBool.describe('Whether SMTP email notifications are enabled'), + from: z.string().describe('Email address to send from'), + replyTo: z.string().describe('Email address for replies'), + transport: SystemConfigSmtpTransportSchema, + }) + .meta({ id: 'SystemConfigSmtpDto' }); + +const SystemConfigNotificationsSchema = z + .object({ smtp: SystemConfigSmtpSchema }) + .meta({ id: 'SystemConfigNotificationsDto' }); + +const SystemConfigTemplateEmailsSchema = z + .object({ + albumInviteTemplate: z.string().describe('Album invite template'), + welcomeTemplate: z.string().describe('Welcome template'), + albumUpdateTemplate: z.string().describe('Album update template'), + }) + .meta({ id: 'SystemConfigTemplateEmailsDto' }); +const SystemConfigTemplatesSchema = z + .object({ email: SystemConfigTemplateEmailsSchema }) + .meta({ id: 'SystemConfigTemplatesDto' }); + +const SystemConfigStorageTemplateSchema = z + .object({ + enabled: configBool.describe('Enabled'), + hashVerificationEnabled: configBool.describe('Hash verification enabled'), + template: z.string().describe('Template'), + }) + .meta({ id: 'SystemConfigStorageTemplateDto' }); + +const SystemConfigTemplateStorageOptionSchema = z + .object({ + yearOptions: z.array(z.string()).describe('Available year format options for storage template'), + monthOptions: z.array(z.string()).describe('Available month format options for storage template'), + weekOptions: z.array(z.string()).describe('Available week format options for storage template'), + dayOptions: z.array(z.string()).describe('Available day format options for storage template'), + hourOptions: z.array(z.string()).describe('Available hour format options for storage template'), + minuteOptions: z.array(z.string()).describe('Available minute format options for storage template'), + secondOptions: z.array(z.string()).describe('Available second format options for storage template'), + presetOptions: z.array(z.string()).describe('Available preset template options'), + }) + .meta({ id: 'SystemConfigTemplateStorageOptionDto' }); + +const SystemConfigThemeSchema = z + .object({ customCss: z.string().describe('Custom CSS for theming') }) + .meta({ id: 'SystemConfigThemeDto' }); + +const SystemConfigGeneratedImageSchema = z + .object({ + format: ImageFormatSchema, + quality: z.int().min(1).max(100).describe('Quality'), + size: z.int().min(1).describe('Size'), + progressive: configBool.default(false).optional().describe('Progressive'), + }) + .meta({ id: 'SystemConfigGeneratedImageDto' }); + +const SystemConfigGeneratedFullsizeImageSchema = z + .object({ + enabled: configBool.describe('Enabled'), + format: ImageFormatSchema, + quality: z.int().min(1).max(100).describe('Quality'), + progressive: configBool.default(false).optional().describe('Progressive'), + }) + .meta({ id: 'SystemConfigGeneratedFullsizeImageDto' }); + +const SystemConfigImageSchema = z + .object({ + thumbnail: SystemConfigGeneratedImageSchema, + preview: SystemConfigGeneratedImageSchema, + fullsize: SystemConfigGeneratedFullsizeImageSchema, + colorspace: ColorspaceSchema, + extractEmbedded: configBool.describe('Extract embedded'), + }) + .meta({ id: 'SystemConfigImageDto' }); + +const SystemConfigTrashSchema = z + .object({ + enabled: configBool.describe('Enabled'), + days: z.int().min(0).describe('Days'), + }) + .meta({ id: 'SystemConfigTrashDto' }); + +const SystemConfigUserSchema = z + .object({ + deleteDelay: z.int().min(1).describe('Delete delay'), + }) + .meta({ id: 'SystemConfigUserDto' }); + +export const SystemConfigSchema = z + .object({ + backup: SystemConfigBackupsSchema, + ffmpeg: SystemConfigFFmpegSchema, + logging: SystemConfigLoggingSchema, + machineLearning: SystemConfigMachineLearningSchema, + map: SystemConfigMapSchema, + newVersionCheck: SystemConfigNewVersionCheckSchema, + nightlyTasks: SystemConfigNightlyTasksSchema, + oauth: SystemConfigOAuthSchema, + passwordLogin: SystemConfigPasswordLoginSchema, + reverseGeocoding: SystemConfigReverseGeocodingSchema, + metadata: SystemConfigMetadataSchema, + storageTemplate: SystemConfigStorageTemplateSchema, + job: SystemConfigJobSchema, + image: SystemConfigImageSchema, + trash: SystemConfigTrashSchema, + theme: SystemConfigThemeSchema, + library: SystemConfigLibrarySchema, + notifications: SystemConfigNotificationsSchema, + templates: SystemConfigTemplatesSchema, + server: SystemConfigServerSchema, + user: SystemConfigUserSchema, + }) + .describe('System configuration') + .meta({ id: 'SystemConfigDto' }); + +export class SystemConfigFFmpegDto extends createZodDto(SystemConfigFFmpegSchema) {} +export class SystemConfigSmtpDto extends createZodDto(SystemConfigSmtpSchema) {} +export class SystemConfigTemplateStorageOptionDto extends createZodDto(SystemConfigTemplateStorageOptionSchema) {} +export class SystemConfigDto extends createZodDto(SystemConfigSchema) {} export function mapConfig(config: SystemConfig): SystemConfigDto { return config; diff --git a/server/src/dtos/system-metadata.dto.ts b/server/src/dtos/system-metadata.dto.ts index 0a4d55c970..676a06f774 100644 --- a/server/src/dtos/system-metadata.dto.ts +++ b/server/src/dtos/system-metadata.dto.ts @@ -1,26 +1,33 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { ValidateBoolean } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import z from 'zod'; -export class AdminOnboardingUpdateDto { - @ValidateBoolean({ description: 'Is admin onboarded' }) - isOnboarded!: boolean; -} +const AdminOnboardingUpdateSchema = z + .object({ + isOnboarded: z.boolean().describe('Is admin onboarded'), + }) + .meta({ id: 'AdminOnboardingUpdateDto' }); -export class AdminOnboardingResponseDto { - @ValidateBoolean({ description: 'Is admin onboarded' }) - isOnboarded!: boolean; -} +const AdminOnboardingResponseSchema = z + .object({ + isOnboarded: z.boolean().describe('Is admin onboarded'), + }) + .meta({ id: 'AdminOnboardingResponseDto' }); -export class ReverseGeocodingStateResponseDto { - @ApiProperty({ description: 'Last update timestamp' }) - lastUpdate!: string | null; - @ApiProperty({ description: 'Last import file name' }) - lastImportFileName!: string | null; -} +const ReverseGeocodingStateResponseSchema = z + .object({ + lastUpdate: z.string().nullable().describe('Last update timestamp'), + lastImportFileName: z.string().nullable().describe('Last import file name'), + }) + .meta({ id: 'ReverseGeocodingStateResponseDto' }); -export class VersionCheckStateResponseDto { - @ApiProperty({ description: 'Last check timestamp' }) - checkedAt!: string | null; - @ApiProperty({ description: 'Release version' }) - releaseVersion!: string | null; -} +const VersionCheckStateResponseSchema = z + .object({ + checkedAt: z.string().nullable().describe('Last check timestamp'), + releaseVersion: z.string().nullable().describe('Release version'), + }) + .meta({ id: 'VersionCheckStateResponseDto' }); + +export class AdminOnboardingUpdateDto extends createZodDto(AdminOnboardingUpdateSchema) {} +export class AdminOnboardingResponseDto extends createZodDto(AdminOnboardingResponseSchema) {} +export class ReverseGeocodingStateResponseDto extends createZodDto(ReverseGeocodingStateResponseSchema) {} +export class VersionCheckStateResponseDto extends createZodDto(VersionCheckStateResponseSchema) {} diff --git a/server/src/dtos/tag.dto.ts b/server/src/dtos/tag.dto.ts index ea85ea71f3..67dbca9914 100644 --- a/server/src/dtos/tag.dto.ts +++ b/server/src/dtos/tag.dto.ts @@ -1,68 +1,63 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsHexColor, IsNotEmpty, IsString } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { Tag } from 'src/database'; import { MaybeDehydrated } from 'src/types'; import { asDateString } from 'src/utils/date'; -import { Optional, ValidateHexColor, ValidateUUID } from 'src/validation'; +import { emptyStringToNull, hexColor } from 'src/validation'; +import z from 'zod'; -export class TagCreateDto { - @ApiProperty({ description: 'Tag name' }) - @IsString() - @IsNotEmpty() - name!: string; +const TagCreateSchema = z + .object({ + name: z.string().describe('Tag name'), + parentId: z.uuidv4().nullish().describe('Parent tag ID'), + color: emptyStringToNull(hexColor.nullable()).optional().describe('Tag color (hex)'), + }) + .meta({ id: 'TagCreateDto' }); - @ValidateUUID({ nullable: true, optional: true, description: 'Parent tag ID' }) - parentId?: string | null; +const TagUpdateSchema = z + .object({ + color: emptyStringToNull(hexColor.nullable()).optional().describe('Tag color (hex)'), + }) + .meta({ id: 'TagUpdateDto' }); - @ApiPropertyOptional({ description: 'Tag color (hex)' }) - @IsHexColor() - @Optional({ nullable: true, emptyToNull: true }) - color?: string; -} +const TagUpsertSchema = z + .object({ + tags: z.array(z.string()).describe('Tag names to upsert'), + }) + .meta({ id: 'TagUpsertDto' }); -export class TagUpdateDto { - @ApiPropertyOptional({ description: 'Tag color (hex)' }) - @Optional({ nullable: true, emptyToNull: true }) - @ValidateHexColor() - color?: string | null; -} +const TagBulkAssetsSchema = z + .object({ + tagIds: z.array(z.uuidv4()).describe('Tag IDs'), + assetIds: z.array(z.uuidv4()).describe('Asset IDs'), + }) + .meta({ id: 'TagBulkAssetsDto' }); -export class TagUpsertDto { - @ApiProperty({ description: 'Tag names to upsert' }) - @IsString({ each: true }) - @IsNotEmpty({ each: true }) - tags!: string[]; -} +const TagBulkAssetsResponseSchema = z + .object({ + count: z.int().describe('Number of assets tagged'), + }) + .meta({ id: 'TagBulkAssetsResponseDto' }); -export class TagBulkAssetsDto { - @ValidateUUID({ each: true, description: 'Tag IDs' }) - tagIds!: string[]; +export const TagResponseSchema = z + .object({ + id: z.string().describe('Tag ID'), + parentId: z.string().optional().describe('Parent tag ID'), + name: z.string().describe('Tag name'), + value: z.string().describe('Tag value (full path)'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + createdAt: z.string().meta({ format: 'date-time' }).describe('Creation date'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + updatedAt: z.string().meta({ format: 'date-time' }).describe('Last update date'), + color: z.string().optional().describe('Tag color (hex)'), + }) + .meta({ id: 'TagResponseDto' }); - @ValidateUUID({ each: true, description: 'Asset IDs' }) - assetIds!: string[]; -} - -export class TagBulkAssetsResponseDto { - @ApiProperty({ type: 'integer', description: 'Number of assets tagged' }) - count!: number; -} - -export class TagResponseDto { - @ApiProperty({ description: 'Tag ID' }) - id!: string; - @ApiPropertyOptional({ description: 'Parent tag ID' }) - parentId?: string; - @ApiProperty({ description: 'Tag name' }) - name!: string; - @ApiProperty({ description: 'Tag value (full path)' }) - value!: string; - @ApiProperty({ description: 'Creation date', format: 'date-time' }) - createdAt!: string; - @ApiProperty({ description: 'Last update date', format: 'date-time' }) - updatedAt!: string; - @ApiPropertyOptional({ description: 'Tag color (hex)' }) - color?: string; -} +export class TagCreateDto extends createZodDto(TagCreateSchema) {} +export class TagUpdateDto extends createZodDto(TagUpdateSchema) {} +export class TagUpsertDto extends createZodDto(TagUpsertSchema) {} +export class TagBulkAssetsDto extends createZodDto(TagBulkAssetsSchema) {} +export class TagBulkAssetsResponseDto extends createZodDto(TagBulkAssetsResponseSchema) {} +export class TagResponseDto extends createZodDto(TagResponseSchema) {} export function mapTag(entity: MaybeDehydrated): TagResponseDto { return { diff --git a/server/src/dtos/time-bucket.dto.ts b/server/src/dtos/time-bucket.dto.ts index 9ea9dc49ae..af820e6868 100644 --- a/server/src/dtos/time-bucket.dto.ts +++ b/server/src/dtos/time-bucket.dto.ts @@ -1,230 +1,128 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; -import type { BBoxDto } from 'src/dtos/bbox.dto'; -import { AssetOrder, AssetVisibility } from 'src/enum'; -import { ValidateBBox } from 'src/utils/bbox'; -import { ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import { BBoxSchema } from 'src/dtos/bbox.dto'; +import { AssetOrderSchema, AssetVisibilitySchema } from 'src/enum'; +import { stringToBool } from 'src/validation'; +import z from 'zod'; -export class TimeBucketDto { - @ValidateUUID({ optional: true, description: 'Filter assets by specific user ID' }) - userId?: string; +const TimeBucketQueryBaseSchema = z + .object({ + userId: z.uuidv4().optional().describe('Filter assets by specific user ID'), + albumId: z.uuidv4().optional().describe('Filter assets belonging to a specific album'), + personId: z.uuidv4().optional().describe('Filter assets containing a specific person (face recognition)'), + tagId: z.uuidv4().optional().describe('Filter assets with a specific tag'), + isFavorite: stringToBool + .optional() + .describe('Filter by favorite status (true for favorites only, false for non-favorites only)'), + isTrashed: stringToBool + .optional() + .describe('Filter by trash status (true for trashed assets only, false for non-trashed only)'), + withStacked: stringToBool + .optional() + .describe('Include stacked assets in the response. When true, only primary assets from stacks are returned.'), + withPartners: stringToBool.optional().describe('Include assets shared by partners'), + order: AssetOrderSchema.optional().describe( + 'Sort order for assets within time buckets (ASC for oldest first, DESC for newest first)', + ), + visibility: AssetVisibilitySchema.optional().describe( + 'Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED)', + ), + withCoordinates: stringToBool.optional().describe('Include location data in the response'), + key: z.string().optional(), + slug: z.string().optional(), + bbox: z + .string() + .transform((value, ctx) => { + const parts = value.split(','); + if (parts.length !== 4) { + ctx.issues.push({ + code: 'custom', + message: 'bbox must have 4 comma-separated numbers: west,south,east,north', + input: value, + }); + return z.NEVER; + } - @ValidateUUID({ optional: true, description: 'Filter assets belonging to a specific album' }) - albumId?: string; + const [west, south, east, north] = parts.map(Number); + if ([west, south, east, north].some((part) => Number.isNaN(part))) { + ctx.issues.push({ + code: 'custom', + message: 'bbox parts must be valid numbers', + input: value, + }); + return z.NEVER; + } - @ValidateUUID({ optional: true, description: 'Filter assets containing a specific person (face recognition)' }) - personId?: string; - - @ValidateUUID({ optional: true, description: 'Filter assets with a specific tag' }) - tagId?: string; - - @ValidateBoolean({ - optional: true, - description: 'Filter by favorite status (true for favorites only, false for non-favorites only)', + return { west, south, east, north }; + }) + .pipe(BBoxSchema) + .optional() + .describe('Bounding box coordinates as west,south,east,north (WGS84)') + .meta({ example: '11.075683,49.416711,11.117589,49.454875' }), }) - isFavorite?: boolean; + .meta({ id: 'TimeBucketDto' }); - @ValidateBoolean({ - optional: true, - description: 'Filter by trash status (true for trashed assets only, false for non-trashed only)', +const TimeBucketSchema = TimeBucketQueryBaseSchema; +const TimeBucketAssetSchema = TimeBucketQueryBaseSchema.extend({ + timeBucket: z.string().describe('Time bucket identifier in YYYY-MM-DD format').meta({ example: '2024-01-01' }), +}).meta({ id: 'TimeBucketAssetDto' }); + +const stackTupleSchema = z.array(z.string()).length(2).nullable(); + +const TimeBucketAssetResponseSchema = z + .object({ + id: z.array(z.string()).describe('Array of asset IDs in the time bucket'), + ownerId: z.array(z.string()).describe('Array of owner IDs for each asset'), + ratio: z.array(z.number()).describe('Array of aspect ratios (width/height) for each asset'), + isFavorite: z.array(z.boolean()).describe('Array indicating whether each asset is favorited'), + visibility: z + .array(AssetVisibilitySchema) + .describe('Array of visibility statuses for each asset (e.g., ARCHIVE, TIMELINE, HIDDEN, LOCKED)'), + isTrashed: z.array(z.boolean()).describe('Array indicating whether each asset is in the trash'), + isImage: z.array(z.boolean()).describe('Array indicating whether each asset is an image (false for videos)'), + thumbhash: z + .array(z.string().nullable()) + .describe('Array of BlurHash strings for generating asset previews (base64 encoded)'), + fileCreatedAt: z.array(z.string()).describe('Array of file creation timestamps in UTC'), + localOffsetHours: z + .array(z.number()) + .describe( + "Array of UTC offset hours at the time each photo was taken. Positive values are east of UTC, negative values are west of UTC. Values may be fractional (e.g., 5.5 for +05:30, -9.75 for -09:45). Applying this offset to 'fileCreatedAt' will give you the time the photo was taken from the photographer's perspective.", + ), + duration: z.array(z.string().nullable()).describe('Array of video durations in HH:MM:SS format (null for images)'), + stack: z + .array(stackTupleSchema) + .optional() + .describe('Array of stack information as [stackId, assetCount] tuples (null for non-stacked assets)'), + projectionType: z + .array(z.string().nullable()) + .describe('Array of projection types for 360° content (e.g., "EQUIRECTANGULAR", "CUBEFACE", "CYLINDRICAL")'), + livePhotoVideoId: z + .array(z.string().nullable()) + .describe('Array of live photo video asset IDs (null for non-live photos)'), + city: z.array(z.string().nullable()).describe('Array of city names extracted from EXIF GPS data'), + country: z.array(z.string().nullable()).describe('Array of country names extracted from EXIF GPS data'), + latitude: z + .array(z.number().nullable()) + .optional() + .describe('Array of latitude coordinates extracted from EXIF GPS data'), + longitude: z + .array(z.number().nullable()) + .optional() + .describe('Array of longitude coordinates extracted from EXIF GPS data'), }) - isTrashed?: boolean; + .meta({ id: 'TimeBucketAssetResponseDto' }); - @ValidateBoolean({ - optional: true, - description: 'Include stacked assets in the response. When true, only primary assets from stacks are returned.', +const TimeBucketsResponseSchema = z + .object({ + timeBucket: z + .string() + .describe('Time bucket identifier in YYYY-MM-DD format representing the start of the time period') + .meta({ example: '2024-01-01' }), + count: z.int().describe('Number of assets in this time bucket').meta({ example: 42 }), }) - withStacked?: boolean; + .meta({ id: 'TimeBucketsResponseDto' }); - @ValidateBoolean({ optional: true, description: 'Include assets shared by partners' }) - withPartners?: boolean; - - @ValidateEnum({ - enum: AssetOrder, - name: 'AssetOrder', - description: 'Sort order for assets within time buckets (ASC for oldest first, DESC for newest first)', - optional: true, - }) - order?: AssetOrder; - - @ValidateEnum({ - enum: AssetVisibility, - name: 'AssetVisibility', - optional: true, - description: 'Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED)', - }) - visibility?: AssetVisibility; - - @ValidateBoolean({ - optional: true, - description: 'Include location data in the response', - }) - withCoordinates?: boolean; - - @ValidateBBox({ optional: true }) - bbox?: BBoxDto; -} - -export class TimeBucketAssetDto extends TimeBucketDto { - @ApiProperty({ - type: 'string', - description: 'Time bucket identifier in YYYY-MM-DD format (e.g., "2024-01-01" for January 2024)', - example: '2024-01-01', - }) - @IsString() - timeBucket!: string; -} - -export class TimeBucketAssetResponseDto { - @ApiProperty({ - type: 'array', - items: { type: 'string' }, - description: 'Array of asset IDs in the time bucket', - }) - id!: string[]; - - @ApiProperty({ - type: 'array', - items: { type: 'string' }, - description: 'Array of owner IDs for each asset', - }) - ownerId!: string[]; - - @ApiProperty({ - type: 'array', - items: { type: 'number' }, - description: 'Array of aspect ratios (width/height) for each asset', - }) - ratio!: number[]; - - @ApiProperty({ - type: 'array', - items: { type: 'boolean' }, - description: 'Array indicating whether each asset is favorited', - }) - isFavorite!: boolean[]; - - @ValidateEnum({ - enum: AssetVisibility, - name: 'AssetVisibility', - each: true, - description: 'Array of visibility statuses for each asset (e.g., ARCHIVE, TIMELINE, HIDDEN, LOCKED)', - }) - visibility!: AssetVisibility[]; - - @ApiProperty({ - type: 'array', - items: { type: 'boolean' }, - description: 'Array indicating whether each asset is in the trash', - }) - isTrashed!: boolean[]; - - @ApiProperty({ - type: 'array', - items: { type: 'boolean' }, - description: 'Array indicating whether each asset is an image (false for videos)', - }) - isImage!: boolean[]; - - @ApiProperty({ - type: 'array', - items: { type: 'string', nullable: true }, - description: 'Array of BlurHash strings for generating asset previews (base64 encoded)', - }) - thumbhash!: (string | null)[]; - - @ApiProperty({ - type: 'array', - items: { type: 'string' }, - description: 'Array of file creation timestamps in UTC', - }) - fileCreatedAt!: string[]; - - @ApiProperty({ - type: 'array', - items: { type: 'number' }, - description: - "Array of UTC offset hours at the time each photo was taken. Positive values are east of UTC, negative values are west of UTC. Values may be fractional (e.g., 5.5 for +05:30, -9.75 for -09:45). Applying this offset to 'fileCreatedAt' will give you the time the photo was taken from the photographer's perspective.", - }) - localOffsetHours!: number[]; - - @ApiProperty({ - type: 'array', - items: { type: 'string', nullable: true }, - description: 'Array of video durations in HH:MM:SS format (null for images)', - }) - duration!: (string | null)[]; - - @ApiProperty({ - type: 'array', - items: { - type: 'array', - items: { type: 'string' }, - minItems: 2, - maxItems: 2, - nullable: true, - }, - description: 'Array of stack information as [stackId, assetCount] tuples (null for non-stacked assets)', - }) - stack?: ([string, string] | null)[]; - - @ApiProperty({ - type: 'array', - items: { type: 'string', nullable: true }, - description: 'Array of projection types for 360° content (e.g., "EQUIRECTANGULAR", "CUBEFACE", "CYLINDRICAL")', - }) - projectionType!: (string | null)[]; - - @ApiProperty({ - type: 'array', - items: { type: 'string', nullable: true }, - description: 'Array of live photo video asset IDs (null for non-live photos)', - }) - livePhotoVideoId!: (string | null)[]; - - @ApiProperty({ - type: 'array', - items: { type: 'string', nullable: true }, - description: 'Array of city names extracted from EXIF GPS data', - }) - city!: (string | null)[]; - - @ApiProperty({ - type: 'array', - items: { type: 'string', nullable: true }, - description: 'Array of country names extracted from EXIF GPS data', - }) - country!: (string | null)[]; - - @ApiProperty({ - type: 'array', - required: false, - items: { type: 'number', nullable: true }, - description: 'Array of latitude coordinates extracted from EXIF GPS data', - }) - latitude!: number[]; - - @ApiProperty({ - type: 'array', - required: false, - items: { type: 'number', nullable: true }, - description: 'Array of longitude coordinates extracted from EXIF GPS data', - }) - longitude!: number[]; -} - -export class TimeBucketsResponseDto { - @ApiProperty({ - type: 'string', - description: 'Time bucket identifier in YYYY-MM-DD format representing the start of the time period', - example: '2024-01-01', - }) - timeBucket!: string; - - @ApiProperty({ - type: 'integer', - description: 'Number of assets in this time bucket', - example: 42, - }) - count!: number; -} +export class TimeBucketDto extends createZodDto(TimeBucketSchema) {} +export class TimeBucketAssetDto extends createZodDto(TimeBucketAssetSchema) {} +export class TimeBucketAssetResponseDto extends createZodDto(TimeBucketAssetResponseSchema) {} +export class TimeBucketsResponseDto extends createZodDto(TimeBucketsResponseSchema) {} diff --git a/server/src/dtos/trash.dto.ts b/server/src/dtos/trash.dto.ts index f1d1f109f6..9a725bc6c8 100644 --- a/server/src/dtos/trash.dto.ts +++ b/server/src/dtos/trash.dto.ts @@ -1,6 +1,10 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { createZodDto } from 'nestjs-zod'; +import z from 'zod'; -export class TrashResponseDto { - @ApiProperty({ type: 'integer', description: 'Number of items in trash' }) - count!: number; -} +const TrashResponseSchema = z + .object({ + count: z.int().describe('Number of items in trash'), + }) + .meta({ id: 'TrashResponseDto' }); + +export class TrashResponseDto extends createZodDto(TrashResponseSchema) {} diff --git a/server/src/dtos/user-preferences.dto.ts b/server/src/dtos/user-preferences.dto.ts index cce1994007..7a7c1d2558 100644 --- a/server/src/dtos/user-preferences.dto.ts +++ b/server/src/dtos/user-preferences.dto.ts @@ -1,302 +1,212 @@ -import { ApiProperty, ApiPropertyOptional, ApiSchema } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsDateString, IsInt, IsPositive, ValidateNested } from 'class-validator'; -import { AssetOrder, UserAvatarColor } from 'src/enum'; +import { createZodDto } from 'nestjs-zod'; +import { AssetOrderSchema, UserAvatarColorSchema } from 'src/enum'; import { UserPreferences } from 'src/types'; -import { Optional, ValidateBoolean, ValidateEnum } from 'src/validation'; +import z from 'zod'; -class AvatarUpdate { - @ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, description: 'Avatar color' }) - color?: UserAvatarColor; -} +const AlbumsUpdateSchema = z + .object({ + defaultAssetOrder: AssetOrderSchema.optional(), + }) + .optional() + .describe('Album preferences') + .meta({ id: 'AlbumsUpdate' }); -class MemoriesUpdate { - @ValidateBoolean({ optional: true, description: 'Whether memories are enabled' }) - enabled?: boolean; +const AvatarUpdateSchema = z + .object({ + color: UserAvatarColorSchema.optional(), + }) + .optional() + .meta({ id: 'AvatarUpdate' }); - @Optional() - @IsInt() - @IsPositive() - @ApiProperty({ type: 'integer', description: 'Memory duration in seconds' }) - duration?: number; -} +const MemoriesUpdateSchema = z + .object({ + enabled: z.boolean().optional().describe('Whether memories are enabled'), + duration: z.int().min(1).optional().describe('Memory duration in seconds'), + }) + .optional() + .meta({ id: 'MemoriesUpdate' }); -class RatingsUpdate { - @ValidateBoolean({ optional: true, description: 'Whether ratings are enabled' }) - enabled?: boolean; -} +const RatingsUpdateSchema = z + .object({ + enabled: z.boolean().optional().describe('Whether ratings are enabled'), + }) + .optional() + .meta({ id: 'RatingsUpdate' }); -@ApiSchema({ description: 'Album preferences' }) -class AlbumsUpdate { - @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true, description: 'Default asset order for albums' }) - defaultAssetOrder?: AssetOrder; -} +const FoldersUpdateSchema = z + .object({ + enabled: z.boolean().optional().describe('Whether folders are enabled'), + sidebarWeb: z.boolean().optional().describe('Whether folders appear in web sidebar'), + }) + .optional() + .meta({ id: 'FoldersUpdate' }); -class FoldersUpdate { - @ValidateBoolean({ optional: true, description: 'Whether folders are enabled' }) - enabled?: boolean; +const PeopleUpdateSchema = z + .object({ + enabled: z.boolean().optional().describe('Whether people are enabled'), + sidebarWeb: z.boolean().optional().describe('Whether people appear in web sidebar'), + }) + .optional() + .meta({ id: 'PeopleUpdate' }); - @ValidateBoolean({ optional: true, description: 'Whether folders appear in web sidebar' }) - sidebarWeb?: boolean; -} +const SharedLinksUpdateSchema = z + .object({ + enabled: z.boolean().optional().describe('Whether shared links are enabled'), + sidebarWeb: z.boolean().optional().describe('Whether shared links appear in web sidebar'), + }) + .optional() + .meta({ id: 'SharedLinksUpdate' }); -class PeopleUpdate { - @ValidateBoolean({ optional: true, description: 'Whether people are enabled' }) - enabled?: boolean; +const TagsUpdateSchema = z + .object({ + enabled: z.boolean().optional().describe('Whether tags are enabled'), + sidebarWeb: z.boolean().optional().describe('Whether tags appear in web sidebar'), + }) + .optional() + .meta({ id: 'TagsUpdate' }); - @ValidateBoolean({ optional: true, description: 'Whether people appear in web sidebar' }) - sidebarWeb?: boolean; -} +const EmailNotificationsUpdateSchema = z + .object({ + enabled: z.boolean().optional().describe('Whether email notifications are enabled'), + albumInvite: z.boolean().optional().describe('Whether to receive email notifications for album invites'), + albumUpdate: z.boolean().optional().describe('Whether to receive email notifications for album updates'), + }) + .optional() + .meta({ id: 'EmailNotificationsUpdate' }); -class SharedLinksUpdate { - @ValidateBoolean({ optional: true, description: 'Whether shared links are enabled' }) - enabled?: boolean; +const DownloadUpdateSchema = z + .object({ + archiveSize: z.int().min(1).optional().describe('Maximum archive size in bytes'), + includeEmbeddedVideos: z.boolean().optional().describe('Whether to include embedded videos in downloads'), + }) + .optional() + .meta({ id: 'DownloadUpdate' }); - @ValidateBoolean({ optional: true, description: 'Whether shared links appear in web sidebar' }) - sidebarWeb?: boolean; -} +const PurchaseUpdateSchema = z + .object({ + showSupportBadge: z.boolean().optional().describe('Whether to show support badge'), + hideBuyButtonUntil: z.string().optional().describe('Date until which to hide buy button'), + }) + .optional() + .meta({ id: 'PurchaseUpdate' }); -class TagsUpdate { - @ValidateBoolean({ optional: true, description: 'Whether tags are enabled' }) - enabled?: boolean; +const CastUpdateSchema = z + .object({ + gCastEnabled: z.boolean().optional().describe('Whether Google Cast is enabled'), + }) + .optional() + .meta({ id: 'CastUpdate' }); - @ValidateBoolean({ optional: true, description: 'Whether tags appear in web sidebar' }) - sidebarWeb?: boolean; -} +const UserPreferencesUpdateSchema = z + .object({ + albums: AlbumsUpdateSchema, + avatar: AvatarUpdateSchema, + cast: CastUpdateSchema, + download: DownloadUpdateSchema, + emailNotifications: EmailNotificationsUpdateSchema, + folders: FoldersUpdateSchema, + memories: MemoriesUpdateSchema, + people: PeopleUpdateSchema, + purchase: PurchaseUpdateSchema, + ratings: RatingsUpdateSchema, + sharedLinks: SharedLinksUpdateSchema, + tags: TagsUpdateSchema, + }) + .meta({ id: 'UserPreferencesUpdateDto' }); -class EmailNotificationsUpdate { - @ValidateBoolean({ optional: true, description: 'Whether email notifications are enabled' }) - enabled?: boolean; +const AlbumsResponseSchema = z + .object({ + defaultAssetOrder: AssetOrderSchema, + }) + .meta({ id: 'AlbumsResponse' }); - @ValidateBoolean({ optional: true, description: 'Whether to receive email notifications for album invites' }) - albumInvite?: boolean; +const FoldersResponseSchema = z + .object({ + enabled: z.boolean().describe('Whether folders are enabled'), + sidebarWeb: z.boolean().describe('Whether folders appear in web sidebar'), + }) + .meta({ id: 'FoldersResponse' }); - @ValidateBoolean({ optional: true, description: 'Whether to receive email notifications for album updates' }) - albumUpdate?: boolean; -} +const MemoriesResponseSchema = z + .object({ + enabled: z.boolean().describe('Whether memories are enabled'), + duration: z.int().describe('Memory duration in seconds'), + }) + .meta({ id: 'MemoriesResponse' }); -class DownloadUpdate implements Partial { - @Optional() - @IsInt() - @IsPositive() - @ApiPropertyOptional({ type: 'integer', description: 'Maximum archive size in bytes' }) - archiveSize?: number; +const PeopleResponseSchema = z + .object({ + enabled: z.boolean().describe('Whether people are enabled'), + sidebarWeb: z.boolean().describe('Whether people appear in web sidebar'), + }) + .meta({ id: 'PeopleResponse' }); - @ValidateBoolean({ optional: true, description: 'Whether to include embedded videos in downloads' }) - includeEmbeddedVideos?: boolean; -} +const RatingsResponseSchema = z + .object({ + enabled: z.boolean().describe('Whether ratings are enabled'), + }) + .meta({ id: 'RatingsResponse' }); -class PurchaseUpdate { - @ValidateBoolean({ optional: true, description: 'Whether to show support badge' }) - showSupportBadge?: boolean; +const SharedLinksResponseSchema = z + .object({ + enabled: z.boolean().describe('Whether shared links are enabled'), + sidebarWeb: z.boolean().describe('Whether shared links appear in web sidebar'), + }) + .meta({ id: 'SharedLinksResponse' }); - @ApiPropertyOptional({ description: 'Date until which to hide buy button' }) - @IsDateString() - @Optional() - hideBuyButtonUntil?: string; -} +const TagsResponseSchema = z + .object({ + enabled: z.boolean().describe('Whether tags are enabled'), + sidebarWeb: z.boolean().describe('Whether tags appear in web sidebar'), + }) + .meta({ id: 'TagsResponse' }); -class CastUpdate { - @ValidateBoolean({ optional: true, description: 'Whether Google Cast is enabled' }) - gCastEnabled?: boolean; -} +const EmailNotificationsResponseSchema = z + .object({ + enabled: z.boolean().describe('Whether email notifications are enabled'), + albumInvite: z.boolean().describe('Whether to receive email notifications for album invites'), + albumUpdate: z.boolean().describe('Whether to receive email notifications for album updates'), + }) + .meta({ id: 'EmailNotificationsResponse' }); -export class UserPreferencesUpdateDto { - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => AlbumsUpdate) - albums?: AlbumsUpdate; +const DownloadResponseSchema = z + .object({ + archiveSize: z.int().describe('Maximum archive size in bytes'), + includeEmbeddedVideos: z.boolean().describe('Whether to include embedded videos in downloads'), + }) + .meta({ id: 'DownloadResponse' }); - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => FoldersUpdate) - folders?: FoldersUpdate; +const PurchaseResponseSchema = z + .object({ + showSupportBadge: z.boolean().describe('Whether to show support badge'), + hideBuyButtonUntil: z.string().describe('Date until which to hide buy button'), + }) + .meta({ id: 'PurchaseResponse' }); - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => MemoriesUpdate) - memories?: MemoriesUpdate; +const CastResponseSchema = z + .object({ + gCastEnabled: z.boolean().describe('Whether Google Cast is enabled'), + }) + .meta({ id: 'CastResponse' }); - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => PeopleUpdate) - people?: PeopleUpdate; +const UserPreferencesResponseSchema = z + .object({ + albums: AlbumsResponseSchema, + folders: FoldersResponseSchema, + memories: MemoriesResponseSchema, + people: PeopleResponseSchema, + ratings: RatingsResponseSchema, + sharedLinks: SharedLinksResponseSchema, + tags: TagsResponseSchema, + emailNotifications: EmailNotificationsResponseSchema, + download: DownloadResponseSchema, + purchase: PurchaseResponseSchema, + cast: CastResponseSchema, + }) + .meta({ id: 'UserPreferencesResponseDto' }); - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => RatingsUpdate) - ratings?: RatingsUpdate; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined, required: false }) - @Optional() - @ValidateNested() - @Type(() => SharedLinksUpdate) - sharedLinks?: SharedLinksUpdate; - - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => TagsUpdate) - tags?: TagsUpdate; - - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => AvatarUpdate) - avatar?: AvatarUpdate; - - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => EmailNotificationsUpdate) - emailNotifications?: EmailNotificationsUpdate; - - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => DownloadUpdate) - download?: DownloadUpdate; - - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => PurchaseUpdate) - purchase?: PurchaseUpdate; - - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => CastUpdate) - cast?: CastUpdate; -} - -class AlbumsResponse { - @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', description: 'Default asset order for albums' }) - defaultAssetOrder: AssetOrder = AssetOrder.Desc; -} - -class RatingsResponse { - @ApiProperty({ description: 'Whether ratings are enabled' }) - enabled: boolean = false; -} - -class MemoriesResponse { - @ApiProperty({ description: 'Whether memories are enabled' }) - enabled: boolean = true; - - @ApiProperty({ type: 'integer', description: 'Memory duration in seconds' }) - duration: number = 5; -} - -class FoldersResponse { - @ApiProperty({ description: 'Whether folders are enabled' }) - enabled: boolean = false; - @ApiProperty({ description: 'Whether folders appear in web sidebar' }) - sidebarWeb: boolean = false; -} - -class PeopleResponse { - @ApiProperty({ description: 'Whether people are enabled' }) - enabled: boolean = true; - @ApiProperty({ description: 'Whether people appear in web sidebar' }) - sidebarWeb: boolean = false; -} - -class TagsResponse { - @ApiProperty({ description: 'Whether tags are enabled' }) - enabled: boolean = true; - @ApiProperty({ description: 'Whether tags appear in web sidebar' }) - sidebarWeb: boolean = true; -} - -class SharedLinksResponse { - @ApiProperty({ description: 'Whether shared links are enabled' }) - enabled: boolean = true; - @ApiProperty({ description: 'Whether shared links appear in web sidebar' }) - sidebarWeb: boolean = false; -} - -class EmailNotificationsResponse { - @ApiProperty({ description: 'Whether email notifications are enabled' }) - enabled!: boolean; - @ApiProperty({ description: 'Whether to receive email notifications for album invites' }) - albumInvite!: boolean; - @ApiProperty({ description: 'Whether to receive email notifications for album updates' }) - albumUpdate!: boolean; -} - -class DownloadResponse { - @ApiProperty({ type: 'integer', description: 'Maximum archive size in bytes' }) - archiveSize!: number; - - @ApiProperty({ description: 'Whether to include embedded videos in downloads' }) - includeEmbeddedVideos: boolean = false; -} - -class PurchaseResponse { - @ApiProperty({ description: 'Whether to show support badge' }) - showSupportBadge!: boolean; - @ApiProperty({ description: 'Date until which to hide buy button' }) - hideBuyButtonUntil!: string; -} - -class CastResponse { - @ApiProperty({ description: 'Whether Google Cast is enabled' }) - gCastEnabled: boolean = false; -} - -export class UserPreferencesResponseDto implements UserPreferences { - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - albums!: AlbumsResponse; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - folders!: FoldersResponse; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - memories!: MemoriesResponse; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - people!: PeopleResponse; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - ratings!: RatingsResponse; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - sharedLinks!: SharedLinksResponse; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - tags!: TagsResponse; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - emailNotifications!: EmailNotificationsResponse; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - download!: DownloadResponse; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - purchase!: PurchaseResponse; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - cast!: CastResponse; -} +export class UserPreferencesUpdateDto extends createZodDto(UserPreferencesUpdateSchema) {} +export class UserPreferencesResponseDto extends createZodDto(UserPreferencesResponseSchema) {} export const mapPreferences = (preferences: UserPreferences): UserPreferencesResponseDto => { return preferences; diff --git a/server/src/dtos/user-profile.dto.ts b/server/src/dtos/user-profile.dto.ts index 6559dd052c..c3c91d3d95 100644 --- a/server/src/dtos/user-profile.dto.ts +++ b/server/src/dtos/user-profile.dto.ts @@ -1,16 +1,20 @@ import { ApiProperty } from '@nestjs/swagger'; +import { createZodDto } from 'nestjs-zod'; import { UploadFieldName } from 'src/dtos/asset-media.dto'; +import { isoDatetimeToDate } from 'src/validation'; +import z from 'zod'; export class CreateProfileImageDto { @ApiProperty({ type: 'string', format: 'binary', description: 'Profile image file' }) [UploadFieldName.PROFILE_DATA]!: Express.Multer.File; } -export class CreateProfileImageResponseDto { - @ApiProperty({ description: 'User ID' }) - userId!: string; - @ApiProperty({ description: 'Profile image change date', format: 'date-time' }) - profileChangedAt!: Date; - @ApiProperty({ description: 'Profile image file path' }) - profileImagePath!: string; -} +const CreateProfileImageResponseSchema = z + .object({ + userId: z.string().describe('User ID'), + profileChangedAt: isoDatetimeToDate.describe('Profile image change date'), + profileImagePath: z.string().describe('Profile image file path'), + }) + .meta({ id: 'CreateProfileImageResponseDto' }); + +export class CreateProfileImageResponseDto extends createZodDto(CreateProfileImageResponseSchema) {} diff --git a/server/src/dtos/user.dto.spec.ts b/server/src/dtos/user.dto.spec.ts index e6be3b17d1..6acc9554f9 100644 --- a/server/src/dtos/user.dto.spec.ts +++ b/server/src/dtos/user.dto.spec.ts @@ -1,69 +1,59 @@ -import { plainToInstance } from 'class-transformer'; -import { validate } from 'class-validator'; -import { UserAdminCreateDto, UserUpdateMeDto } from 'src/dtos/user.dto'; +import { UserAdminCreateSchema, UserUpdateMeSchema } from 'src/dtos/user.dto'; describe('update user DTO', () => { - it('should allow emails without a tld', async () => { + it('should allow emails without a tld', () => { const someEmail = 'test@test'; - - const dto = plainToInstance(UserUpdateMeDto, { + const result = UserUpdateMeSchema.safeParse({ email: someEmail, id: '3fe388e4-2078-44d7-b36c-39d9dee3a657', }); - const errors = await validate(dto); - expect(errors).toHaveLength(0); - expect(dto.email).toEqual(someEmail); + expect(result.success).toBe(true); + expect(result.data?.email).toEqual(someEmail); }); }); describe('create user DTO', () => { - it('validates the email', async () => { - const params: Partial = { - email: undefined, + it('validates the email', () => { + expect(UserAdminCreateSchema.safeParse({ password: 'password', name: 'name' }).success).toBe(false); + + expect( + UserAdminCreateSchema.safeParse({ email: 'invalid email', password: 'password', name: 'name' }).success, + ).toBe(false); + + const result = UserAdminCreateSchema.safeParse({ + email: 'valid@email.com', password: 'password', name: 'name', - }; - let dto: UserAdminCreateDto = plainToInstance(UserAdminCreateDto, params); - let errors = await validate(dto); - expect(errors).toHaveLength(1); - - params.email = 'invalid email'; - dto = plainToInstance(UserAdminCreateDto, params); - errors = await validate(dto); - expect(errors).toHaveLength(1); - - params.email = 'valid@email.com'; - dto = plainToInstance(UserAdminCreateDto, params); - errors = await validate(dto); - expect(errors).toHaveLength(0); + }); + expect(result.success).toBe(true); }); - it('validates invalid email type', async () => { - let dto = plainToInstance(UserAdminCreateDto, { - email: [], - password: 'some password', - name: 'some name', - }); - expect(await validate(dto)).toHaveLength(1); + it('validates invalid email type', () => { + expect( + UserAdminCreateSchema.safeParse({ + email: [], + password: 'some password', + name: 'some name', + }).success, + ).toBe(false); - dto = plainToInstance(UserAdminCreateDto, { - email: {}, - password: 'some password', - name: 'some name', - }); - expect(await validate(dto)).toHaveLength(1); + expect( + UserAdminCreateSchema.safeParse({ + email: {}, + password: 'some password', + name: 'some name', + }).success, + ).toBe(false); }); - it('should allow emails without a tld', async () => { + it('should allow emails without a tld', () => { const someEmail = 'test@test'; - - const dto = plainToInstance(UserAdminCreateDto, { + const result = UserAdminCreateSchema.safeParse({ email: someEmail, password: 'some password', name: 'some name', }); - const errors = await validate(dto); - expect(errors).toHaveLength(0); - expect(dto.email).toEqual(someEmail); + expect(result.success).toBe(true); + expect(result.data?.email).toEqual(someEmail); }); }); diff --git a/server/src/dtos/user.dto.ts b/server/src/dtos/user.dto.ts index ebd0018bba..75256b9e1a 100644 --- a/server/src/dtos/user.dto.ts +++ b/server/src/dtos/user.dto.ts @@ -1,65 +1,50 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; -import { IsEmail, IsInt, IsNotEmpty, IsString, Min } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { User, UserAdmin } from 'src/database'; -import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum'; +import { pinCodeRegex } from 'src/dtos/auth.dto'; +import { UserAvatarColor, UserAvatarColorSchema, UserMetadataKey, UserStatusSchema } from 'src/enum'; import { MaybeDehydrated, UserMetadataItem } from 'src/types'; import { asDateString } from 'src/utils/date'; -import { Optional, PinCode, ValidateBoolean, ValidateEnum, ValidateUUID, toEmail, toSanitized } from 'src/validation'; +import { emptyStringToNull, isoDatetimeToDate, sanitizeFilename, stringToBool, toEmail } from 'src/validation'; +import z from 'zod'; -export class UserUpdateMeDto { - @ApiPropertyOptional({ description: 'User email' }) - @Optional() - @IsEmail({ require_tld: false }) - @Transform(toEmail) - email?: string; - - // TODO: migrate to the other change password endpoint - @ApiPropertyOptional({ description: 'User password (deprecated, use change password endpoint)' }) - @Optional() - @IsNotEmpty() - @IsString() - password?: string; - - @ApiPropertyOptional({ description: 'User name' }) - @Optional() - @IsString() - @IsNotEmpty() - name?: string; - - @ValidateEnum({ - enum: UserAvatarColor, - name: 'UserAvatarColor', - optional: true, - nullable: true, - description: 'Avatar color', +export const UserUpdateMeSchema = z + .object({ + email: toEmail.optional().describe('User email'), + password: z + .string() + .optional() + .describe('User password (deprecated, use change password endpoint)') + .meta({ deprecated: true }), + name: z.string().optional().describe('User name'), + avatarColor: UserAvatarColorSchema.nullish(), }) - avatarColor?: UserAvatarColor | null; -} + .meta({ id: 'UserUpdateMeDto' }); -export class UserResponseDto { - @ApiProperty({ description: 'User ID' }) - id!: string; - @ApiProperty({ description: 'User name' }) - name!: string; - @ApiProperty({ description: 'User email' }) - email!: string; - @ApiProperty({ description: 'Profile image path' }) - profileImagePath!: string; - @ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', description: 'Avatar color' }) - avatarColor!: UserAvatarColor; - @ApiProperty({ description: 'Profile change date', format: 'date-time' }) - profileChangedAt!: string; -} +export class UserUpdateMeDto extends createZodDto(UserUpdateMeSchema) {} -export class UserLicense { - @ApiProperty({ description: 'License key' }) - licenseKey!: string; - @ApiProperty({ description: 'Activation key' }) - activationKey!: string; - @ApiProperty({ description: 'Activation date' }) - activatedAt!: Date; -} +export const UserResponseSchema = z + .object({ + id: z.uuidv4().describe('User ID'), + name: z.string().describe('User name'), + email: toEmail.describe('User email'), + profileImagePath: z.string().describe('Profile image path'), + avatarColor: UserAvatarColorSchema, + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + profileChangedAt: z.string().meta({ format: 'date-time' }).describe('Profile change date'), + }) + .meta({ id: 'UserResponseDto' }); + +export class UserResponseDto extends createZodDto(UserResponseSchema) {} + +const licenseKeyRegex = /^IM(SV|CL)(-[\dA-Za-z]{4}){8}$/; + +export const UserLicenseSchema = z + .object({ + licenseKey: z.string().regex(licenseKeyRegex).describe(`License key (format: ${licenseKeyRegex.toString()})`), + activationKey: z.string().describe('Activation key'), + activatedAt: isoDatetimeToDate.describe('Activation date'), + }) + .meta({ id: 'UserLicense' }); const emailToAvatarColor = (email: string): UserAvatarColor => { const values = Object.values(UserAvatarColor); @@ -80,144 +65,77 @@ export const mapUser = (entity: MaybeDehydrated): UserResponse }; }; -export class UserAdminSearchDto { - @ValidateBoolean({ optional: true, description: 'Include deleted users' }) - withDeleted?: boolean; - - @ValidateUUID({ optional: true, description: 'User ID filter' }) - id?: string; -} - -export class UserAdminCreateDto { - @ApiProperty({ description: 'User email' }) - @IsEmail({ require_tld: false }) - @Transform(toEmail) - email!: string; - - @ApiProperty({ description: 'User password' }) - @IsString() - password!: string; - - @ApiProperty({ description: 'User name' }) - @IsNotEmpty() - @IsString() - name!: string; - - @ValidateEnum({ - enum: UserAvatarColor, - name: 'UserAvatarColor', - optional: true, - nullable: true, - description: 'Avatar color', +const UserAdminSearchSchema = z + .object({ + withDeleted: stringToBool.optional().describe('Include deleted users'), + id: z.uuidv4().optional().describe('User ID filter'), }) - avatarColor?: UserAvatarColor | null; + .meta({ id: 'UserAdminSearchDto' }); - @ApiPropertyOptional({ description: 'PIN code' }) - @PinCode({ optional: true, nullable: true, emptyToNull: true }) - pinCode?: string | null; +export class UserAdminSearchDto extends createZodDto(UserAdminSearchSchema) {} - @ApiPropertyOptional({ description: 'Storage label' }) - @Optional({ nullable: true }) - @IsString() - @Transform(toSanitized) - storageLabel?: string | null; - - @ApiPropertyOptional({ type: 'integer', format: 'int64', description: 'Storage quota in bytes' }) - @Optional({ nullable: true }) - @IsInt() - @Min(0) - quotaSizeInBytes?: number | null; - - @ValidateBoolean({ optional: true, description: 'Require password change on next login' }) - shouldChangePassword?: boolean; - - @ValidateBoolean({ optional: true, description: 'Send notification email' }) - notify?: boolean; - - @ValidateBoolean({ optional: true, description: 'Grant admin privileges' }) - isAdmin?: boolean; -} - -export class UserAdminUpdateDto { - @ApiPropertyOptional({ description: 'User email' }) - @Optional() - @IsEmail({ require_tld: false }) - @Transform(toEmail) - email?: string; - - @ApiPropertyOptional({ description: 'User password' }) - @Optional() - @IsNotEmpty() - @IsString() - password?: string; - - @ApiPropertyOptional({ description: 'PIN code' }) - @PinCode({ optional: true, nullable: true, emptyToNull: true }) - pinCode?: string | null; - - @ApiPropertyOptional({ description: 'User name' }) - @Optional() - @IsString() - @IsNotEmpty() - name?: string; - - @ValidateEnum({ - enum: UserAvatarColor, - name: 'UserAvatarColor', - optional: true, - nullable: true, - description: 'Avatar color', +export const UserAdminCreateSchema = z + .object({ + email: toEmail.describe('User email'), + password: z.string().describe('User password'), + name: z.string().describe('User name'), + avatarColor: UserAvatarColorSchema.nullish(), + pinCode: emptyStringToNull(z.string().regex(pinCodeRegex).nullable()) + .optional() + .describe('PIN code') + .meta({ example: '123456' }), + storageLabel: z.string().pipe(sanitizeFilename).nullish().describe('Storage label'), + quotaSizeInBytes: z.int().min(0).nullish().describe('Storage quota in bytes'), + shouldChangePassword: z.boolean().optional().describe('Require password change on next login'), + notify: z.boolean().optional().describe('Send notification email'), + isAdmin: z.boolean().optional().describe('Grant admin privileges'), }) - avatarColor?: UserAvatarColor | null; + .meta({ id: 'UserAdminCreateDto' }); - @ApiPropertyOptional({ description: 'Storage label' }) - @Optional({ nullable: true }) - @IsString() - @Transform(toSanitized) - storageLabel?: string | null; +export class UserAdminCreateDto extends createZodDto(UserAdminCreateSchema) {} - @ValidateBoolean({ optional: true, description: 'Require password change on next login' }) - shouldChangePassword?: boolean; +const UserAdminUpdateSchema = z + .object({ + email: toEmail.optional().describe('User email'), + password: z.string().optional().describe('User password'), + pinCode: emptyStringToNull(z.string().regex(pinCodeRegex).nullable()) + .optional() + .describe('PIN code') + .meta({ example: '123456' }), + name: z.string().optional().describe('User name'), + avatarColor: UserAvatarColorSchema.nullish(), + storageLabel: z.string().pipe(sanitizeFilename).nullish().describe('Storage label'), + shouldChangePassword: z.boolean().optional().describe('Require password change on next login'), + quotaSizeInBytes: z.int().min(0).nullish().describe('Storage quota in bytes'), + isAdmin: z.boolean().optional().describe('Grant admin privileges'), + }) + .meta({ id: 'UserAdminUpdateDto' }); - @ApiPropertyOptional({ type: 'integer', format: 'int64', description: 'Storage quota in bytes' }) - @Optional({ nullable: true }) - @IsInt() - @Min(0) - quotaSizeInBytes?: number | null; +export class UserAdminUpdateDto extends createZodDto(UserAdminUpdateSchema) {} - @ValidateBoolean({ optional: true, description: 'Grant admin privileges' }) - isAdmin?: boolean; -} +const UserAdminDeleteSchema = z + .object({ + force: z.boolean().optional().describe('Force delete even if user has assets'), + }) + .meta({ id: 'UserAdminDeleteDto' }); -export class UserAdminDeleteDto { - @ValidateBoolean({ optional: true, description: 'Force delete even if user has assets' }) - force?: boolean; -} +export class UserAdminDeleteDto extends createZodDto(UserAdminDeleteSchema) {} -export class UserAdminResponseDto extends UserResponseDto { - @ApiProperty({ description: 'Storage label' }) - storageLabel!: string | null; - @ApiProperty({ description: 'Require password change on next login' }) - shouldChangePassword!: boolean; - @ApiProperty({ description: 'Is admin user' }) - isAdmin!: boolean; - @ApiProperty({ description: 'Creation date' }) - createdAt!: Date; - @ApiProperty({ description: 'Deletion date' }) - deletedAt!: Date | null; - @ApiProperty({ description: 'Last update date' }) - updatedAt!: Date; - @ApiProperty({ description: 'OAuth ID' }) - oauthId!: string; - @ApiProperty({ type: 'integer', format: 'int64', description: 'Storage quota in bytes' }) - quotaSizeInBytes!: number | null; - @ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage in bytes' }) - quotaUsageInBytes!: number | null; - @ValidateEnum({ enum: UserStatus, name: 'UserStatus', description: 'User status' }) - status!: string; - @ApiProperty({ description: 'User license' }) - license!: UserLicense | null; -} +const UserAdminResponseSchema = UserResponseSchema.extend({ + storageLabel: z.string().nullable().describe('Storage label'), + shouldChangePassword: z.boolean().describe('Require password change on next login'), + isAdmin: z.boolean().describe('Is admin user'), + createdAt: isoDatetimeToDate.describe('Creation date'), + deletedAt: isoDatetimeToDate.nullable().describe('Deletion date'), + updatedAt: isoDatetimeToDate.describe('Last update date'), + oauthId: z.string().describe('OAuth ID'), + quotaSizeInBytes: z.int().min(0).nullable().describe('Storage quota in bytes'), + quotaUsageInBytes: z.int().min(0).nullable().describe('Storage usage in bytes'), + status: UserStatusSchema, + license: UserLicenseSchema.nullable(), +}).meta({ id: 'UserAdminResponseDto' }); + +export class UserAdminResponseDto extends createZodDto(UserAdminResponseSchema) {} export function mapUserAdmin(entity: UserAdmin): UserAdminResponseDto { const metadata = entity.metadata || []; @@ -237,6 +155,6 @@ export function mapUserAdmin(entity: UserAdmin): UserAdminResponseDto { quotaSizeInBytes: entity.quotaSizeInBytes, quotaUsageInBytes: entity.quotaUsageInBytes, status: entity.status, - license: license ? { ...license, activatedAt: new Date(license?.activatedAt) } : null, + license: license ? { ...license, activatedAt: new Date(license.activatedAt) } : null, }; } diff --git a/server/src/dtos/workflow.dto.ts b/server/src/dtos/workflow.dto.ts index c4e5ac9c4c..0307c7f483 100644 --- a/server/src/dtos/workflow.dto.ts +++ b/server/src/dtos/workflow.dto.ts @@ -1,143 +1,84 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsNotEmpty, IsObject, IsString, IsUUID, ValidateNested } from 'class-validator'; -import { WorkflowAction, WorkflowFilter } from 'src/database'; -import { PluginTriggerType } from 'src/enum'; -import type { ActionConfig, FilterConfig } from 'src/types/plugin-schema.types'; -import { Optional, ValidateBoolean, ValidateEnum } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import type { WorkflowAction, WorkflowFilter } from 'src/database'; +import { PluginTriggerTypeSchema } from 'src/enum'; +import { ActionConfigSchema, FilterConfigSchema } from 'src/types/plugin-schema.types'; +import z from 'zod'; -export class WorkflowFilterItemDto { - @ApiProperty({ description: 'Plugin filter ID' }) - @IsUUID() - pluginFilterId!: string; - - @ApiPropertyOptional({ description: 'Filter configuration' }) - @IsObject() - @Optional() - filterConfig?: FilterConfig; -} - -export class WorkflowActionItemDto { - @ApiProperty({ description: 'Plugin action ID' }) - @IsUUID() - pluginActionId!: string; - - @ApiPropertyOptional({ description: 'Action configuration' }) - @IsObject() - @Optional() - actionConfig?: ActionConfig; -} - -export class WorkflowCreateDto { - @ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType', description: 'Workflow trigger type' }) - triggerType!: PluginTriggerType; - - @ApiProperty({ description: 'Workflow name' }) - @IsString() - @IsNotEmpty() - name!: string; - - @ApiPropertyOptional({ description: 'Workflow description' }) - @IsString() - @Optional() - description?: string; - - @ValidateBoolean({ optional: true, description: 'Workflow enabled' }) - enabled?: boolean; - - @ApiProperty({ description: 'Workflow filters' }) - @ValidateNested({ each: true }) - @Type(() => WorkflowFilterItemDto) - filters!: WorkflowFilterItemDto[]; - - @ApiProperty({ description: 'Workflow actions' }) - @ValidateNested({ each: true }) - @Type(() => WorkflowActionItemDto) - actions!: WorkflowActionItemDto[]; -} - -export class WorkflowUpdateDto { - @ValidateEnum({ - enum: PluginTriggerType, - name: 'PluginTriggerType', - optional: true, - description: 'Workflow trigger type', +const WorkflowFilterItemSchema = z + .object({ + pluginFilterId: z.uuidv4().describe('Plugin filter ID'), + filterConfig: FilterConfigSchema.optional(), }) - triggerType?: PluginTriggerType; + .meta({ id: 'WorkflowFilterItemDto' }); - @ApiPropertyOptional({ description: 'Workflow name' }) - @IsString() - @IsNotEmpty() - @Optional() - name?: string; +const WorkflowActionItemSchema = z + .object({ + pluginActionId: z.uuidv4().describe('Plugin action ID'), + actionConfig: ActionConfigSchema.optional(), + }) + .meta({ id: 'WorkflowActionItemDto' }); - @ApiPropertyOptional({ description: 'Workflow description' }) - @IsString() - @Optional() - description?: string; +const WorkflowCreateSchema = z + .object({ + triggerType: PluginTriggerTypeSchema, + name: z.string().describe('Workflow name'), + description: z.string().optional().describe('Workflow description'), + enabled: z.boolean().optional().describe('Workflow enabled'), + filters: z.array(WorkflowFilterItemSchema).describe('Workflow filters'), + actions: z.array(WorkflowActionItemSchema).describe('Workflow actions'), + }) + .meta({ id: 'WorkflowCreateDto' }); - @ValidateBoolean({ optional: true, description: 'Workflow enabled' }) - enabled?: boolean; +const WorkflowUpdateSchema = z + .object({ + triggerType: PluginTriggerTypeSchema.optional(), + name: z.string().optional().describe('Workflow name'), + description: z.string().optional().describe('Workflow description'), + enabled: z.boolean().optional().describe('Workflow enabled'), + filters: z.array(WorkflowFilterItemSchema).optional().describe('Workflow filters'), + actions: z.array(WorkflowActionItemSchema).optional().describe('Workflow actions'), + }) + .meta({ id: 'WorkflowUpdateDto' }); - @ApiPropertyOptional({ description: 'Workflow filters' }) - @ValidateNested({ each: true }) - @Type(() => WorkflowFilterItemDto) - @Optional() - filters?: WorkflowFilterItemDto[]; +const WorkflowFilterResponseSchema = z + .object({ + id: z.string().describe('Filter ID'), + workflowId: z.string().describe('Workflow ID'), + pluginFilterId: z.string().describe('Plugin filter ID'), + filterConfig: FilterConfigSchema.nullable(), + order: z.number().describe('Filter order'), + }) + .meta({ id: 'WorkflowFilterResponseDto' }); - @ApiPropertyOptional({ description: 'Workflow actions' }) - @ValidateNested({ each: true }) - @Type(() => WorkflowActionItemDto) - @Optional() - actions?: WorkflowActionItemDto[]; -} +const WorkflowActionResponseSchema = z + .object({ + id: z.string().describe('Action ID'), + workflowId: z.string().describe('Workflow ID'), + pluginActionId: z.string().describe('Plugin action ID'), + actionConfig: ActionConfigSchema.nullable(), + order: z.number().describe('Action order'), + }) + .meta({ id: 'WorkflowActionResponseDto' }); -export class WorkflowResponseDto { - @ApiProperty({ description: 'Workflow ID' }) - id!: string; - @ApiProperty({ description: 'Owner user ID' }) - ownerId!: string; - @ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType', description: 'Workflow trigger type' }) - triggerType!: PluginTriggerType; - @ApiProperty({ description: 'Workflow name' }) - name!: string | null; - @ApiProperty({ description: 'Workflow description' }) - description!: string; - @ApiProperty({ description: 'Creation date' }) - createdAt!: string; - @ApiProperty({ description: 'Workflow enabled' }) - enabled!: boolean; - @ApiProperty({ description: 'Workflow filters' }) - filters!: WorkflowFilterResponseDto[]; - @ApiProperty({ description: 'Workflow actions' }) - actions!: WorkflowActionResponseDto[]; -} +const WorkflowResponseSchema = z + .object({ + id: z.string().describe('Workflow ID'), + ownerId: z.string().describe('Owner user ID'), + triggerType: PluginTriggerTypeSchema, + name: z.string().nullable().describe('Workflow name'), + description: z.string().describe('Workflow description'), + createdAt: z.string().describe('Creation date'), + enabled: z.boolean().describe('Workflow enabled'), + filters: z.array(WorkflowFilterResponseSchema).describe('Workflow filters'), + actions: z.array(WorkflowActionResponseSchema).describe('Workflow actions'), + }) + .meta({ id: 'WorkflowResponseDto' }); -export class WorkflowFilterResponseDto { - @ApiProperty({ description: 'Filter ID' }) - id!: string; - @ApiProperty({ description: 'Workflow ID' }) - workflowId!: string; - @ApiProperty({ description: 'Plugin filter ID' }) - pluginFilterId!: string; - @ApiProperty({ description: 'Filter configuration' }) - filterConfig!: FilterConfig | null; - @ApiProperty({ description: 'Filter order', type: 'number' }) - order!: number; -} - -export class WorkflowActionResponseDto { - @ApiProperty({ description: 'Action ID' }) - id!: string; - @ApiProperty({ description: 'Workflow ID' }) - workflowId!: string; - @ApiProperty({ description: 'Plugin action ID' }) - pluginActionId!: string; - @ApiProperty({ description: 'Action configuration' }) - actionConfig!: ActionConfig | null; - @ApiProperty({ description: 'Action order', type: 'number' }) - order!: number; -} +export class WorkflowCreateDto extends createZodDto(WorkflowCreateSchema) {} +export class WorkflowUpdateDto extends createZodDto(WorkflowUpdateSchema) {} +export class WorkflowResponseDto extends createZodDto(WorkflowResponseSchema) {} +class WorkflowFilterResponseDto extends createZodDto(WorkflowFilterResponseSchema) {} +class WorkflowActionResponseDto extends createZodDto(WorkflowActionResponseSchema) {} export function mapWorkflowFilter(filter: WorkflowFilter): WorkflowFilterResponseDto { return { diff --git a/server/src/enum.ts b/server/src/enum.ts index de85d24db3..08d710e08c 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -1,8 +1,12 @@ +import z from 'zod'; + export enum AuthType { Password = 'password', OAuth = 'oauth', } +export const AuthTypeSchema = z.enum(AuthType).describe('Auth type').meta({ id: 'AuthType' }); + export enum ImmichCookie { AccessToken = 'immich_access_token', MaintenanceToken = 'immich_maintenance_token', @@ -13,6 +17,8 @@ export enum ImmichCookie { OAuthCodeVerifier = 'immich_oauth_code_verifier', } +export const ImmichCookieSchema = z.enum(ImmichCookie).describe('Immich cookie').meta({ id: 'ImmichCookie' }); + export enum ImmichHeader { ApiKey = 'x-api-key', UserToken = 'x-immich-user-token', @@ -23,6 +29,8 @@ export enum ImmichHeader { Cid = 'x-immich-cid', } +export const ImmichHeaderSchema = z.enum(ImmichHeader).describe('Immich header').meta({ id: 'ImmichHeader' }); + export enum ImmichQuery { SharedLinkKey = 'key', SharedLinkSlug = 'slug', @@ -30,6 +38,8 @@ export enum ImmichQuery { SessionKey = 'sessionKey', } +export const ImmichQuerySchema = z.enum(ImmichQuery).describe('Immich query').meta({ id: 'ImmichQuery' }); + export enum AssetType { Image = 'IMAGE', Video = 'VIDEO', @@ -37,11 +47,20 @@ export enum AssetType { Other = 'OTHER', } +export const AssetTypeSchema = z.enum(AssetType).describe('Asset type').meta({ id: 'AssetTypeEnum' }); + export enum ChecksumAlgorithm { - sha1File = 'sha1', // sha1 checksum of the whole file contents - sha1Path = 'sha1-path', // sha1 checksum of "path:" plus the file path, currently used in external libraries, deprecated + /** sha1 checksum of the whole file contents */ + sha1File = 'sha1', + /** sha1 checksum of "path:" plus the file path, currently used in external libraries, deprecated */ + sha1Path = 'sha1-path', } +export const ChecksumAlgorithmSchema = z + .enum(ChecksumAlgorithm) + .describe('Checksum algorithm') + .meta({ id: 'ChecksumAlgorithmEnum' }); + export enum AssetFileType { /** * An full/large-size image extracted/converted from RAW photos @@ -53,32 +72,29 @@ export enum AssetFileType { EncodedVideo = 'encoded_video', } +export const AssetFileTypeSchema = z.enum(AssetFileType).describe('Asset file type').meta({ id: 'AssetFileType' }); + export enum AlbumUserRole { Editor = 'editor', Viewer = 'viewer', } +export const AlbumUserRoleSchema = z.enum(AlbumUserRole).describe('Album user role').meta({ id: 'AlbumUserRole' }); + export enum AssetOrder { Asc = 'asc', Desc = 'desc', } -export enum DatabaseAction { - Create = 'CREATE', - Update = 'UPDATE', - Delete = 'DELETE', -} - -export enum EntityType { - Asset = 'ASSET', - Album = 'ALBUM', -} +export const AssetOrderSchema = z.enum(AssetOrder).describe('Asset sort order').meta({ id: 'AssetOrder' }); export enum MemoryType { /** pictures taken on this day X years ago */ OnThisDay = 'on_this_day', } +export const MemoryTypeSchema = z.enum(MemoryType).describe('Memory type').meta({ id: 'MemoryType' }); + export enum AssetOrderWithRandom { // Include existing values Asc = AssetOrder.Asc, @@ -87,6 +103,11 @@ export enum AssetOrderWithRandom { Random = 'random', } +export const AssetOrderWithRandomSchema = z + .enum(AssetOrderWithRandom) + .describe('Sort order') + .meta({ id: 'MemorySearchOrder' }); + export enum Permission { All = 'all', @@ -110,7 +131,6 @@ export enum Permission { AssetView = 'asset.view', AssetDownload = 'asset.download', AssetUpload = 'asset.upload', - AssetReplace = 'asset.replace', AssetCopy = 'asset.copy', AssetDerive = 'asset.derive', @@ -293,6 +313,8 @@ export enum Permission { AdminAuthUnlinkAll = 'adminAuth.unlinkAll', } +export const PermissionSchema = z.enum(Permission).describe('Permission').meta({ id: 'Permission' }); + export enum SharedLinkType { Album = 'ALBUM', @@ -303,6 +325,8 @@ export enum SharedLinkType { Individual = 'INDIVIDUAL', } +export const SharedLinkTypeSchema = z.enum(SharedLinkType).describe('Shared link type').meta({ id: 'SharedLinkType' }); + export enum StorageFolder { EncodedVideo = 'encoded-video', Library = 'library', @@ -312,6 +336,8 @@ export enum StorageFolder { Backups = 'backups', } +export const StorageFolderSchema = z.enum(StorageFolder).describe('Storage folder').meta({ id: 'StorageFolder' }); + export enum SystemMetadataKey { MediaLocation = 'MediaLocation', ReverseGeocodingState = 'reverse-geocoding-state', @@ -325,16 +351,31 @@ export enum SystemMetadataKey { License = 'license', } +export const SystemMetadataKeySchema = z + .enum(SystemMetadataKey) + .describe('System metadata key') + .meta({ id: 'SystemMetadataKey' }); + export enum UserMetadataKey { Preferences = 'preferences', License = 'license', Onboarding = 'onboarding', } +export const UserMetadataKeySchema = z + .enum(UserMetadataKey) + .describe('User metadata key') + .meta({ id: 'UserMetadataKey' }); + export enum AssetMetadataKey { MobileApp = 'mobile-app', } +export const AssetMetadataKeySchema = z + .enum(AssetMetadataKey) + .describe('Asset metadata key') + .meta({ id: 'AssetMetadataKey' }); + export enum UserAvatarColor { Primary = 'primary', Pink = 'pink', @@ -348,24 +389,35 @@ export enum UserAvatarColor { Amber = 'amber', } +export const UserAvatarColorSchema = z + .enum(UserAvatarColor) + .describe('User avatar color') + .meta({ id: 'UserAvatarColor' }); + export enum UserStatus { Active = 'active', Removing = 'removing', Deleted = 'deleted', } +export const UserStatusSchema = z.enum(UserStatus).describe('User status').meta({ id: 'UserStatus' }); + export enum AssetStatus { Active = 'active', Trashed = 'trashed', Deleted = 'deleted', } +export const AssetStatusSchema = z.enum(AssetStatus).describe('Asset status').meta({ id: 'AssetStatus' }); + export enum SourceType { MachineLearning = 'machine-learning', Exif = 'exif', Manual = 'manual', } +export const SourceTypeSchema = z.enum(SourceType).describe('Face detection source type').meta({ id: 'SourceType' }); + export enum ManualJobName { PersonCleanup = 'person-cleanup', TagCleanup = 'tag-cleanup', @@ -375,19 +427,27 @@ export enum ManualJobName { BackupDatabase = 'backup-database', } +export const ManualJobNameSchema = z.enum(ManualJobName).describe('Manual job name').meta({ id: 'ManualJobName' }); + export enum AssetPathType { Original = 'original', EncodedVideo = 'encoded_video', } +export const AssetPathTypeSchema = z.enum(AssetPathType).describe('Asset path type').meta({ id: 'AssetPathType' }); + export enum PersonPathType { Face = 'face', } +export const PersonPathTypeSchema = z.enum(PersonPathType).describe('Person path type').meta({ id: 'PersonPathType' }); + export enum UserPathType { Profile = 'profile', } +export const UserPathTypeSchema = z.enum(UserPathType).describe('User path type').meta({ id: 'UserPathType' }); + export type PathType = AssetFileType | AssetPathType | PersonPathType | UserPathType; export enum TranscodePolicy { @@ -398,6 +458,11 @@ export enum TranscodePolicy { Disabled = 'disabled', } +export const TranscodePolicySchema = z + .enum(TranscodePolicy) + .describe('Transcode policy') + .meta({ id: 'TranscodePolicy' }); + export enum TranscodeTarget { None = 'NONE', Audio = 'AUDIO', @@ -405,6 +470,11 @@ export enum TranscodeTarget { All = 'ALL', } +export const TranscodeTargetSchema = z + .enum(TranscodeTarget) + .describe('Transcode target') + .meta({ id: 'TranscodeTarget' }); + export enum VideoCodec { H264 = 'h264', Hevc = 'hevc', @@ -412,6 +482,8 @@ export enum VideoCodec { Av1 = 'av1', } +export const VideoCodecSchema = z.enum(VideoCodec).describe('Target video codec').meta({ id: 'VideoCodec' }); + export enum AudioCodec { Mp3 = 'mp3', Aac = 'aac', @@ -421,6 +493,8 @@ export enum AudioCodec { PcmS16le = 'pcm_s16le', } +export const AudioCodecSchema = z.enum(AudioCodec).describe('Target audio codec').meta({ id: 'AudioCodec' }); + export enum VideoContainer { Mov = 'mov', Mp4 = 'mp4', @@ -428,6 +502,11 @@ export enum VideoContainer { Webm = 'webm', } +export const VideoContainerSchema = z + .enum(VideoContainer) + .describe('Accepted video containers') + .meta({ id: 'VideoContainer' }); + export enum TranscodeHardwareAcceleration { Nvenc = 'nvenc', Qsv = 'qsv', @@ -436,6 +515,11 @@ export enum TranscodeHardwareAcceleration { Disabled = 'disabled', } +export const TranscodeHardwareAccelerationSchema = z + .enum(TranscodeHardwareAcceleration) + .describe('Transcode hardware acceleration') + .meta({ id: 'TranscodeHWAccel' }); + export enum ToneMapping { Hable = 'hable', Mobius = 'mobius', @@ -443,27 +527,40 @@ export enum ToneMapping { Disabled = 'disabled', } +export const ToneMappingSchema = z.enum(ToneMapping).describe('Tone mapping').meta({ id: 'ToneMapping' }); + export enum CQMode { Auto = 'auto', Cqp = 'cqp', Icq = 'icq', } +export const CQModeSchema = z.enum(CQMode).describe('CQ mode').meta({ id: 'CQMode' }); + export enum Colorspace { Srgb = 'srgb', P3 = 'p3', } +export const ColorspaceSchema = z.enum(Colorspace).describe('Colorspace').meta({ id: 'Colorspace' }); + export enum ImageFormat { Jpeg = 'jpeg', Webp = 'webp', } +export const ImageFormatSchema = z.enum(ImageFormat).describe('Image format').meta({ id: 'ImageFormat' }); + export enum RawExtractedFormat { Jpeg = 'jpeg', Jxl = 'jxl', } +export const RawExtractedFormatSchema = z + .enum(RawExtractedFormat) + .describe('Raw extracted format') + .meta({ id: 'RawExtractedFormat' }); + export enum LogLevel { Verbose = 'verbose', Debug = 'debug', @@ -473,11 +570,15 @@ export enum LogLevel { Fatal = 'fatal', } +export const LogLevelSchema = z.enum(LogLevel).describe('Log level').meta({ id: 'LogLevel' }); + export enum LogFormat { Console = 'console', Json = 'json', } +export const LogFormatSchema = z.enum(LogFormat).describe('Log format').meta({ id: 'LogFormat' }); + export enum ApiCustomExtension { Permission = 'x-immich-permission', AdminOnly = 'x-immich-admin-only', @@ -485,6 +586,11 @@ export enum ApiCustomExtension { State = 'x-immich-state', } +export const ApiCustomExtensionSchema = z + .enum(ApiCustomExtension) + .describe('API custom extension') + .meta({ id: 'ApiCustomExtension' }); + export enum MetadataKey { AuthRoute = 'auth_route', AdminRoute = 'admin_route', @@ -495,29 +601,42 @@ export enum MetadataKey { TelemetryEnabled = 'telemetry_enabled', } +export const MetadataKeySchema = z.enum(MetadataKey).describe('Metadata key').meta({ id: 'MetadataKey' }); + export enum RouteKey { Asset = 'assets', User = 'users', } +export const RouteKeySchema = z.enum(RouteKey).describe('Route key').meta({ id: 'RouteKey' }); + export enum CacheControl { PrivateWithCache = 'private_with_cache', PrivateWithoutCache = 'private_without_cache', None = 'none', } +export const CacheControlSchema = z.enum(CacheControl).describe('Cache control').meta({ id: 'CacheControl' }); + export enum ImmichEnvironment { Development = 'development', Testing = 'testing', Production = 'production', } +export const ImmichEnvironmentSchema = z + .enum(ImmichEnvironment) + .describe('Immich environment') + .meta({ id: 'ImmichEnvironment' }); + export enum ImmichWorker { Api = 'api', Maintenance = 'maintenance', Microservices = 'microservices', } +export const ImmichWorkerSchema = z.enum(ImmichWorker).describe('Immich worker').meta({ id: 'ImmichWorker' }); + export enum ImmichTelemetry { Host = 'host', Api = 'api', @@ -526,6 +645,11 @@ export enum ImmichTelemetry { Job = 'job', } +export const ImmichTelemetrySchema = z + .enum(ImmichTelemetry) + .describe('Immich telemetry') + .meta({ id: 'ImmichTelemetry' }); + export enum ExifOrientation { Horizontal = 1, MirrorHorizontal = 2, @@ -537,6 +661,11 @@ export enum ExifOrientation { Rotate270CW = 8, } +export const ExifOrientationSchema = z + .enum(ExifOrientation) + .describe('EXIF orientation') + .meta({ id: 'ExifOrientation' }); + export enum DatabaseExtension { Cube = 'cube', EarthDistance = 'earthdistance', @@ -545,6 +674,11 @@ export enum DatabaseExtension { VectorChord = 'vchord', } +export const DatabaseExtensionSchema = z + .enum(DatabaseExtension) + .describe('Database extension') + .meta({ id: 'DatabaseExtension' }); + export enum BootstrapEventPriority { // Database service should be initialized before anything else, most other services need database access DatabaseService = -200, @@ -556,6 +690,11 @@ export enum BootstrapEventPriority { SystemConfig = 100, } +export const BootstrapEventPrioritySchema = z + .enum(BootstrapEventPriority) + .describe('Bootstrap event priority') + .meta({ id: 'BootstrapEventPriority' }); + export enum QueueName { ThumbnailGeneration = 'thumbnailGeneration', MetadataExtraction = 'metadataExtraction', @@ -577,6 +716,8 @@ export enum QueueName { Editor = 'editor', } +export const QueueNameSchema = z.enum(QueueName).describe('Queue name').meta({ id: 'QueueName' }); + export enum QueueJobStatus { Active = 'active', Failed = 'failed', @@ -586,6 +727,8 @@ export enum QueueJobStatus { Paused = 'paused', } +export const QueueJobStatusSchema = z.enum(QueueJobStatus).describe('Queue job status').meta({ id: 'QueueJobStatus' }); + export enum JobName { AssetDelete = 'AssetDelete', AssetDeleteCheck = 'AssetDeleteCheck', @@ -603,7 +746,6 @@ export enum JobName { AssetGenerateThumbnailsQueueAll = 'AssetGenerateThumbnailsQueueAll', AssetGenerateThumbnails = 'AssetGenerateThumbnails', - AuditLogCleanup = 'AuditLogCleanup', AuditTableCleanup = 'AuditTableCleanup', DatabaseBackup = 'DatabaseBackup', @@ -666,6 +808,8 @@ export enum JobName { WorkflowRun = 'WorkflowRun', } +export const JobNameSchema = z.enum(JobName).describe('Job name').meta({ id: 'JobName' }); + export enum QueueCommand { Start = 'start', /** @deprecated Use `updateQueue` instead */ @@ -678,21 +822,32 @@ export enum QueueCommand { ClearFailed = 'clear-failed', } +export const QueueCommandSchema = z + .enum(QueueCommand) + .describe('Queue command to execute') + .meta({ id: 'QueueCommand' }); + export enum JobStatus { Success = 'success', Failed = 'failed', Skipped = 'skipped', } +export const JobStatusSchema = z.enum(JobStatus).describe('Job status').meta({ id: 'JobStatus' }); + export enum QueueCleanType { Failed = 'failed', } +export const QueueCleanTypeSchema = z.enum(QueueCleanType).describe('Queue clean type').meta({ id: 'QueueCleanType' }); + export enum VectorIndex { Clip = 'clip_index', Face = 'face_index', } +export const VectorIndexSchema = z.enum(VectorIndex).describe('Vector index').meta({ id: 'VectorIndex' }); + export enum DatabaseLock { GeodataImport = 100, Migrations = 200, @@ -710,6 +865,8 @@ export enum DatabaseLock { VersionCheck = 800, } +export const DatabaseLockSchema = z.enum(DatabaseLock).describe('Database lock').meta({ id: 'DatabaseLock' }); + export enum MaintenanceAction { Start = 'start', End = 'end', @@ -717,10 +874,17 @@ export enum MaintenanceAction { RestoreDatabase = 'restore_database', } +export const MaintenanceActionSchema = z + .enum(MaintenanceAction) + .describe('Maintenance action') + .meta({ id: 'MaintenanceAction' }); + export enum ExitCode { AppRestart = 7, } +export const ExitCodeSchema = z.enum(ExitCode).describe('Exit code').meta({ id: 'ExitCode' }); + export enum SyncRequestType { AlbumsV1 = 'AlbumsV1', AlbumUsersV1 = 'AlbumUsersV1', @@ -746,6 +910,11 @@ export enum SyncRequestType { UserMetadataV1 = 'UserMetadataV1', } +export const SyncRequestTypeSchema = z + .enum(SyncRequestType) + .describe('Sync request type') + .meta({ id: 'SyncRequestType' }); + export enum SyncEntityType { AuthUserV1 = 'AuthUserV1', @@ -814,6 +983,8 @@ export enum SyncEntityType { SyncCompleteV1 = 'SyncCompleteV1', } +export const SyncEntityTypeSchema = z.enum(SyncEntityType).describe('Sync entity type').meta({ id: 'SyncEntityType' }); + export enum NotificationLevel { Success = 'success', Error = 'error', @@ -821,6 +992,11 @@ export enum NotificationLevel { Info = 'info', } +export const NotificationLevelSchema = z + .enum(NotificationLevel) + .describe('Notification level') + .meta({ id: 'NotificationLevel' }); + export enum NotificationType { JobFailed = 'JobFailed', BackupFailed = 'BackupFailed', @@ -830,11 +1006,21 @@ export enum NotificationType { Custom = 'Custom', } +export const NotificationTypeSchema = z + .enum(NotificationType) + .describe('Notification type') + .meta({ id: 'NotificationType' }); + export enum OAuthTokenEndpointAuthMethod { ClientSecretPost = 'client_secret_post', ClientSecretBasic = 'client_secret_basic', } +export const OAuthTokenEndpointAuthMethodSchema = z + .enum(OAuthTokenEndpointAuthMethod) + .describe('OAuth token endpoint auth method') + .meta({ id: 'OAuthTokenEndpointAuthMethod' }); + export enum AssetVisibility { Archive = 'archive', Timeline = 'timeline', @@ -846,12 +1032,19 @@ export enum AssetVisibility { Locked = 'locked', } +export const AssetVisibilitySchema = z + .enum(AssetVisibility) + .describe('Asset visibility') + .meta({ id: 'AssetVisibility' }); + export enum CronJob { LibraryScan = 'LibraryScan', NightlyJobs = 'NightlyJobs', VersionCheck = 'VersionCheck', } +export const CronJobSchema = z.enum(CronJob).describe('Cron job').meta({ id: 'CronJob' }); + export enum ApiTag { Activities = 'Activities', Albums = 'Albums', @@ -892,13 +1085,22 @@ export enum ApiTag { Workflows = 'Workflows', } +export const ApiTagSchema = z.enum(ApiTag).describe('API tag').meta({ id: 'ApiTag' }); + export enum PluginContext { Asset = 'asset', Album = 'album', Person = 'person', } +export const PluginContextSchema = z.enum(PluginContext).describe('Plugin context').meta({ id: 'PluginContextType' }); + export enum PluginTriggerType { AssetCreate = 'AssetCreate', PersonRecognized = 'PersonRecognized', } + +export const PluginTriggerTypeSchema = z + .enum(PluginTriggerType) + .describe('Plugin trigger type') + .meta({ id: 'PluginTriggerType' }); diff --git a/server/src/middleware/global-exception.filter.ts b/server/src/middleware/global-exception.filter.ts index a8afa91cbc..f91bb2b122 100644 --- a/server/src/middleware/global-exception.filter.ts +++ b/server/src/middleware/global-exception.filter.ts @@ -1,8 +1,10 @@ import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common'; import { Response } from 'express'; import { ClsService } from 'nestjs-cls'; +import { ZodSerializationException, ZodValidationException } from 'nestjs-zod'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { logGlobalError } from 'src/utils/logger'; +import { ZodError } from 'zod'; @Catch() export class GlobalExceptionFilter implements ExceptionFilter { @@ -41,6 +43,19 @@ export class GlobalExceptionFilter implements ExceptionFilter { body = { message: body }; } + // handle both request and response validation errors + if (error instanceof ZodValidationException || error instanceof ZodSerializationException) { + const zodError = error.getZodError(); + if (zodError instanceof ZodError && zodError.issues.length > 0) { + body = { + message: zodError.issues.map((issue) => + issue.path.length > 0 ? `[${issue.path.join('.')}] ${issue.message}` : issue.message, + ), + error: 'Bad Request', + }; + } + } + return { status, body }; } diff --git a/server/src/queries/asset.job.repository.sql b/server/src/queries/asset.job.repository.sql index 333d4ace81..bd903f3952 100644 --- a/server/src/queries/asset.job.repository.sql +++ b/server/src/queries/asset.job.repository.sql @@ -250,8 +250,6 @@ select "asset"."id", "asset"."checksum", "asset"."checksumAlgorithm", - "asset"."deviceAssetId", - "asset"."deviceId", "asset"."fileCreatedAt", "asset"."fileModifiedAt", "asset"."isExternal", diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index a68fde2b93..ebc2de90e1 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -242,17 +242,6 @@ where limit $3 --- AssetRepository.getAllByDeviceId -select - "deviceAssetId" -from - "asset" -where - "ownerId" = $1::uuid - and "deviceId" = $2 - and "visibility" != $3 - and "deletedAt" is null - -- AssetRepository.getLivePhotoCount select count(*) as "count" @@ -312,9 +301,8 @@ limit -- AssetRepository.updateAll update "asset" set - "deviceId" = $1 where - "id" = any ($2::uuid[]) + "id" = any ($1::uuid[]) -- AssetRepository.getByChecksum select @@ -497,63 +485,6 @@ where limit $5 --- AssetRepository.getAllForUserFullSync -select - "asset".*, - to_json("asset_exif") as "exifInfo", - to_json("stacked_assets") as "stack" -from - "asset" - left join "asset_exif" on "asset"."id" = "asset_exif"."assetId" - left join "stack" on "stack"."id" = "asset"."stackId" - left join lateral ( - select - "stack".*, - count("stacked") as "assetCount" - from - "asset" as "stacked" - where - "stacked"."stackId" = "stack"."id" - group by - "stack"."id" - ) as "stacked_assets" on "stack"."id" is not null -where - "asset"."ownerId" = $1::uuid - and "asset"."visibility" != $2 - and "asset"."updatedAt" <= $3 - and "asset"."id" > $4 -order by - "asset"."id" -limit - $5 - --- AssetRepository.getChangedDeltaSync -select - "asset".*, - to_json("asset_exif") as "exifInfo", - to_json("stacked_assets") as "stack" -from - "asset" - left join "asset_exif" on "asset"."id" = "asset_exif"."assetId" - left join "stack" on "stack"."id" = "asset"."stackId" - left join lateral ( - select - "stack".*, - count("stacked") as "assetCount" - from - "asset" as "stacked" - where - "stacked"."stackId" = "stack"."id" - group by - "stack"."id" - ) as "stacked_assets" on "stack"."id" is not null -where - "asset"."ownerId" = any ($1::uuid[]) - and "asset"."visibility" != $2 - and "asset"."updatedAt" > $3 -limit - $4 - -- AssetRepository.detectOfflineExternalAssets update "asset" set diff --git a/server/src/queries/audit.repository.sql b/server/src/queries/audit.repository.sql deleted file mode 100644 index b1a10abf48..0000000000 --- a/server/src/queries/audit.repository.sql +++ /dev/null @@ -1,16 +0,0 @@ --- NOTE: This file is auto generated by ./sql-generator - --- AuditRepository.getAfter -select distinct - on ("audit"."entityId", "audit"."entityType") "audit"."entityId" -from - "audit" -where - "audit"."createdAt" > $1 - and "audit"."action" = $2 - and "audit"."entityType" = $3 - and "audit"."ownerId" in ($4) -order by - "audit"."entityId" desc, - "audit"."entityType" desc, - "audit"."createdAt" desc diff --git a/server/src/queries/map.repository.sql b/server/src/queries/map.repository.sql index d7e98b1cd2..8ae844b36c 100644 --- a/server/src/queries/map.repository.sql +++ b/server/src/queries/map.repository.sql @@ -1,5 +1,25 @@ -- NOTE: This file is auto generated by ./sql-generator +-- MapRepository.getAlbumMapMarkers +select + "id", + "asset_exif"."latitude" as "lat", + "asset_exif"."longitude" as "lon", + "asset_exif"."city", + "asset_exif"."state", + "asset_exif"."country" +from + "asset" + inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId" + and "asset_exif"."latitude" is not null + and "asset_exif"."longitude" is not null + inner join "album_asset" on "asset"."id" = "album_asset"."assetId" +where + "asset"."deletedAt" is null + and "album_asset"."albumId" = $1 +order by + "fileCreatedAt" desc + -- MapRepository.getMapMarkers select "id", @@ -14,8 +34,8 @@ from and "asset_exif"."latitude" is not null and "asset_exif"."longitude" is not null where - "asset"."visibility" = $1 - and "deletedAt" is null + "asset"."deletedAt" is null + and "asset"."visibility" = $1 and ( "ownerId" in ($2) or exists ( diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 5876b934e5..784cf68b5b 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -106,19 +106,6 @@ interface AssetExploreFieldOptions { minAssetsPerField: number; } -interface AssetFullSyncOptions { - ownerId: string; - lastId?: string; - updatedUntil: Date; - limit: number; -} - -interface AssetDeltaSyncOptions { - userIds: string[]; - updatedAfter: Date; - limit: number; -} - interface AssetGetByChecksumOptions { ownerId: string; checksum: Buffer; @@ -318,7 +305,7 @@ export class AssetRepository { .execute(); } - upsertMetadata(id: string, items: Array<{ key: string; value: object }>) { + upsertMetadata(id: string, items: Array<{ key: string; value: Record }>) { if (items.length === 0) { return []; } @@ -461,18 +448,6 @@ export class AssetRepository { await this.db.deleteFrom('asset').where('ownerId', '=', ownerId).execute(); } - async getByDeviceIds(ownerId: string, deviceId: string, deviceAssetIds: string[]): Promise { - const assets = await this.db - .selectFrom('asset') - .select(['deviceAssetId']) - .where('deviceAssetId', 'in', deviceAssetIds) - .where('deviceId', '=', deviceId) - .where('ownerId', '=', asUuid(ownerId)) - .execute(); - - return assets.map((asset) => asset.deviceAssetId); - } - @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string) { return this.db @@ -484,27 +459,6 @@ export class AssetRepository { .executeTakeFirst(); } - /** - * Get assets by device's Id on the database - * @param ownerId - * @param deviceId - * - * @returns Promise - Array of assetIds belong to the device - */ - @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) - async getAllByDeviceId(ownerId: string, deviceId: string): Promise { - const items = await this.db - .selectFrom('asset') - .select(['deviceAssetId']) - .where('ownerId', '=', asUuid(ownerId)) - .where('deviceId', '=', deviceId) - .where('visibility', '!=', AssetVisibility.Hidden) - .where('deletedAt', 'is', null) - .execute(); - - return items.map((asset) => asset.deviceAssetId); - } - @GenerateSql({ params: [DummyValue.UUID] }) async getLivePhotoCount(motionId: string): Promise { const [{ count }] = await this.db @@ -581,7 +535,7 @@ export class AssetRepository { .executeTakeFirst(); } - @GenerateSql({ params: [[DummyValue.UUID], { deviceId: DummyValue.STRING }] }) + @GenerateSql({ params: [[DummyValue.UUID], {}] }) @Chunked() async updateAll(ids: string[], options: Updateable): Promise { if (ids.length === 0) { @@ -681,19 +635,6 @@ export class AssetRepository { .executeTakeFirstOrThrow(); } - getRandom(userIds: string[], take: number) { - return this.db - .selectFrom('asset') - .selectAll('asset') - .$call(withExif) - .$call(withDefaultVisibility) - .where('ownerId', '=', anyUuid(userIds)) - .where('deletedAt', 'is', null) - .orderBy((eb) => eb.fn('random')) - .limit(take) - .execute(); - } - @GenerateSql({ params: [{}] }) async getTimeBuckets(options: TimeBucketOptions): Promise { return this.db @@ -918,70 +859,6 @@ export class AssetRepository { return { fieldName: 'exifInfo.city', items }; } - @GenerateSql({ - params: [ - { - ownerId: DummyValue.UUID, - lastId: DummyValue.UUID, - updatedUntil: DummyValue.DATE, - limit: 10, - }, - ], - }) - getAllForUserFullSync(options: AssetFullSyncOptions) { - const { ownerId, lastId, updatedUntil, limit } = options; - return this.db - .selectFrom('asset') - .selectAll('asset') - .$call(withExif) - .leftJoin('stack', 'stack.id', 'asset.stackId') - .leftJoinLateral( - (eb) => - eb - .selectFrom('asset as stacked') - .selectAll('stack') - .select((eb) => eb.fn.count(eb.table('stacked')).as('assetCount')) - .whereRef('stacked.stackId', '=', 'stack.id') - .groupBy('stack.id') - .as('stacked_assets'), - (join) => join.on('stack.id', 'is not', null), - ) - .select((eb) => eb.fn.toJson(eb.table('stacked_assets')).$castTo().as('stack')) - .where('asset.ownerId', '=', asUuid(ownerId)) - .where('asset.visibility', '!=', AssetVisibility.Hidden) - .where('asset.updatedAt', '<=', updatedUntil) - .$if(!!lastId, (qb) => qb.where('asset.id', '>', lastId!)) - .orderBy('asset.id') - .limit(limit) - .execute(); - } - - @GenerateSql({ params: [{ userIds: [DummyValue.UUID], updatedAfter: DummyValue.DATE, limit: 100 }] }) - async getChangedDeltaSync(options: AssetDeltaSyncOptions) { - return this.db - .selectFrom('asset') - .selectAll('asset') - .$call(withExif) - .leftJoin('stack', 'stack.id', 'asset.stackId') - .leftJoinLateral( - (eb) => - eb - .selectFrom('asset as stacked') - .selectAll('stack') - .select((eb) => eb.fn.count(eb.table('stacked')).as('assetCount')) - .whereRef('stacked.stackId', '=', 'stack.id') - .groupBy('stack.id') - .as('stacked_assets'), - (join) => join.on('stack.id', 'is not', null), - ) - .select((eb) => eb.fn.toJson(eb.table('stacked_assets').$castTo()).as('stack')) - .where('asset.ownerId', '=', anyUuid(options.userIds)) - .where('asset.visibility', '!=', AssetVisibility.Hidden) - .where('asset.updatedAt', '>', options.updatedAfter) - .limit(options.limit) - .execute(); - } - async upsertFile( file: Pick< Insertable, diff --git a/server/src/repositories/audit.repository.ts b/server/src/repositories/audit.repository.ts deleted file mode 100644 index 2d56eddc9a..0000000000 --- a/server/src/repositories/audit.repository.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { Kysely } from 'kysely'; -import { InjectKysely } from 'nestjs-kysely'; -import { DummyValue, GenerateSql } from 'src/decorators'; -import { DatabaseAction, EntityType } from 'src/enum'; -import { DB } from 'src/schema'; - -export interface AuditSearch { - action?: DatabaseAction; - entityType?: EntityType; - userIds: string[]; -} - -@Injectable() -export class AuditRepository { - constructor(@InjectKysely() private db: Kysely) {} - - @GenerateSql({ - params: [ - DummyValue.DATE, - { action: DatabaseAction.Create, entityType: EntityType.Asset, userIds: [DummyValue.UUID] }, - ], - }) - async getAfter(since: Date, options: AuditSearch): Promise { - const records = await this.db - .selectFrom('audit') - .where('audit.createdAt', '>', since) - .$if(!!options.action, (qb) => qb.where('audit.action', '=', options.action!)) - .$if(!!options.entityType, (qb) => qb.where('audit.entityType', '=', options.entityType!)) - .where('audit.ownerId', 'in', options.userIds) - .distinctOn(['audit.entityId', 'audit.entityType']) - .orderBy('audit.entityId', 'desc') - .orderBy('audit.entityType', 'desc') - .orderBy('audit.createdAt', 'desc') - .select('audit.entityId') - .execute(); - - return records.map(({ entityId }) => entityId); - } - - async removeBefore(before: Date): Promise { - await this.db.deleteFrom('audit').where('createdAt', '<', before).execute(); - } -} diff --git a/server/src/repositories/config.repository.spec.ts b/server/src/repositories/config.repository.spec.ts index a3dc8ba5cb..3c579a1a94 100644 --- a/server/src/repositories/config.repository.spec.ts +++ b/server/src/repositories/config.repository.spec.ts @@ -85,7 +85,7 @@ describe('getEnv', () => { describe('IMMICH_MEDIA_LOCATION', () => { it('should throw an error for relative paths', () => { process.env.IMMICH_MEDIA_LOCATION = './relative/path'; - expect(() => getEnv()).toThrowError('IMMICH_MEDIA_LOCATION must be an absolute path'); + expect(() => getEnv()).toThrowError('[IMMICH_MEDIA_LOCATION] Must be an absolute path'); }); }); @@ -98,7 +98,7 @@ describe('getEnv', () => { it('should throw an error for invalid value', () => { process.env.IMMICH_ALLOW_EXTERNAL_PLUGINS = 'invalid'; - expect(() => getEnv()).toThrowError('IMMICH_ALLOW_EXTERNAL_PLUGINS must be a boolean value'); + expect(() => getEnv()).toThrowError('[IMMICH_ALLOW_EXTERNAL_PLUGINS] Invalid option: expected one of'); }); }); @@ -111,7 +111,7 @@ describe('getEnv', () => { it('should throw an error for invalid value', () => { process.env.IMMICH_ALLOW_SETUP = 'invalid'; - expect(() => getEnv()).toThrowError('IMMICH_ALLOW_SETUP must be a boolean value'); + expect(() => getEnv()).toThrowError('[IMMICH_ALLOW_SETUP] Invalid option: expected one of'); }); }); @@ -134,7 +134,7 @@ describe('getEnv', () => { it('should validate DB_SSL_MODE', () => { process.env.DB_SSL_MODE = 'invalid'; - expect(() => getEnv()).toThrowError('DB_SSL_MODE must be one of the following values:'); + expect(() => getEnv()).toThrow(/\[DB_SSL_MODE\] Invalid option: expected one of/); }); it('should accept a valid DB_SSL_MODE', () => { @@ -278,7 +278,7 @@ describe('getEnv', () => { it('should reject invalid trusted proxies', () => { process.env.IMMICH_TRUSTED_PROXIES = '10.1'; - expect(() => getEnv()).toThrow('IMMICH_TRUSTED_PROXIES must be an ip address, or ip address range'); + expect(() => getEnv()).toThrow('[IMMICH_TRUSTED_PROXIES] Must be an ip address or ip address range'); }); }); diff --git a/server/src/repositories/config.repository.ts b/server/src/repositories/config.repository.ts index fa4823362e..97ec3f1cdc 100644 --- a/server/src/repositories/config.repository.ts +++ b/server/src/repositories/config.repository.ts @@ -2,8 +2,6 @@ import { DatabaseConnectionParams } from '@immich/sql-tools'; import { RegisterQueueOptions } from '@nestjs/bullmq'; import { Inject, Injectable, Optional } from '@nestjs/common'; import { QueueOptions } from 'bullmq'; -import { plainToInstance } from 'class-transformer'; -import { validateSync } from 'class-validator'; import { Request, Response } from 'express'; import { HelmetOptions } from 'helmet'; import { RedisOptions } from 'ioredis'; @@ -13,7 +11,7 @@ import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { citiesFile, excludePaths, IWorker } from 'src/constants'; import { Telemetry } from 'src/decorators'; -import { EnvDto } from 'src/dtos/env.dto'; +import { EnvSchema } from 'src/dtos/env.dto'; import { DatabaseExtension, ImmichEnvironment, @@ -173,15 +171,16 @@ const resolveHelmetFile = (helmetFile: 'true' | 'false' | string | undefined) => }; const getEnv = (): EnvData => { - const dto = plainToInstance(EnvDto, process.env); - const errors = validateSync(dto); - if (errors.length > 0) { - const messages = [`Invalid environment variables: `]; - for (const error of errors) { - messages.push(` - ${error.property}=${error.value} (${Object.values(error.constraints || {}).join(', ')})`); + const parseResult = EnvSchema.safeParse(process.env); + if (!parseResult.success) { + const messages = ['Invalid environment variables: ']; + for (const issue of parseResult.error.issues) { + const path = issue.path.join('.'); + messages.push(` - [${path}] ${issue.message}`); } throw new Error(messages.join('\n')); } + const dto = parseResult.data; const includedWorkers = asSet(dto.IMMICH_WORKERS_INCLUDE, [ImmichWorker.Api, ImmichWorker.Microservices]); const excludedWorkers = asSet(dto.IMMICH_WORKERS_EXCLUDE, []); diff --git a/server/src/repositories/event.repository.ts b/server/src/repositories/event.repository.ts index fbc281ccb3..c505dd3fb3 100644 --- a/server/src/repositories/event.repository.ts +++ b/server/src/repositories/event.repository.ts @@ -1,6 +1,5 @@ import { Injectable } from '@nestjs/common'; import { ModuleRef, Reflector } from '@nestjs/core'; -import { ClassConstructor } from 'class-transformer'; import _ from 'lodash'; import { Socket } from 'socket.io'; import { SystemConfig } from 'src/config'; @@ -152,7 +151,7 @@ export class EventRepository { this.logger.setContext(EventRepository.name); } - setup({ services }: { services: ClassConstructor[] }) { + setup({ services }: { services: (new (...args: any[]) => unknown)[] }) { const reflector = this.moduleRef.get(Reflector, { strict: false }); const items: Item[] = []; const worker = this.configRepository.getWorker(); diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 361a2e7179..fcff171a5e 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -7,7 +7,6 @@ import { AppRepository } from 'src/repositories/app.repository'; import { AssetEditRepository } from 'src/repositories/asset-edit.repository'; import { AssetJobRepository } from 'src/repositories/asset-job.repository'; import { AssetRepository } from 'src/repositories/asset.repository'; -import { AuditRepository } from 'src/repositories/audit.repository'; import { ConfigRepository } from 'src/repositories/config.repository'; import { CronRepository } from 'src/repositories/cron.repository'; import { CryptoRepository } from 'src/repositories/crypto.repository'; @@ -56,7 +55,6 @@ export const repositories = [ ActivityRepository, AlbumRepository, AlbumUserRepository, - AuditRepository, ApiKeyRepository, AppRepository, AssetRepository, diff --git a/server/src/repositories/job.repository.ts b/server/src/repositories/job.repository.ts index 142d5e3252..a94e5aa9f6 100644 --- a/server/src/repositories/job.repository.ts +++ b/server/src/repositories/job.repository.ts @@ -2,7 +2,6 @@ import { getQueueToken } from '@nestjs/bullmq'; import { Injectable } from '@nestjs/common'; import { ModuleRef, Reflector } from '@nestjs/core'; import { JobsOptions, Queue, Worker } from 'bullmq'; -import { ClassConstructor } from 'class-transformer'; import { setTimeout } from 'node:timers/promises'; import { JobConfig } from 'src/decorators'; import { QueueJobResponseDto, QueueJobSearchDto } from 'src/dtos/queue.dto'; @@ -34,7 +33,7 @@ export class JobRepository { this.logger.setContext(JobRepository.name); } - setup(services: ClassConstructor[]) { + setup(services: (new (...args: any[]) => unknown)[]) { const reflector = this.moduleRef.get(Reflector, { strict: false }); // discovery diff --git a/server/src/repositories/map.repository.ts b/server/src/repositories/map.repository.ts index 304cf89c32..7c384b44b2 100644 --- a/server/src/repositories/map.repository.ts +++ b/server/src/repositories/map.repository.ts @@ -76,29 +76,21 @@ export class MapRepository { this.logger.log('Geodata import completed'); } + @GenerateSql({ params: [DummyValue.UUID] }) + getAlbumMapMarkers(albumId: string) { + return this.mapMarkersQuery() + .innerJoin('album_asset', 'asset.id', 'album_asset.assetId') + .where('album_asset.albumId', '=', albumId) + .execute(); + } + @GenerateSql({ params: [[DummyValue.UUID], [DummyValue.UUID]] }) getMapMarkers( ownerIds: string[], albumIds: string[], { isArchived, isFavorite, fileCreatedAfter, fileCreatedBefore }: MapMarkerSearchOptions = {}, ) { - return this.db - .selectFrom('asset') - .innerJoin('asset_exif', (builder) => - builder - .onRef('asset.id', '=', 'asset_exif.assetId') - .on('asset_exif.latitude', 'is not', null) - .on('asset_exif.longitude', 'is not', null), - ) - .select([ - 'id', - 'asset_exif.latitude as lat', - 'asset_exif.longitude as lon', - 'asset_exif.city', - 'asset_exif.state', - 'asset_exif.country', - ]) - .$narrowType<{ lat: NotNull; lon: NotNull }>() + return this.mapMarkersQuery() .$if(isArchived === true, (qb) => qb.where((eb) => eb.or([ @@ -113,7 +105,6 @@ export class MapRepository { .$if(isFavorite !== undefined, (q) => q.where('isFavorite', '=', isFavorite!)) .$if(fileCreatedAfter !== undefined, (q) => q.where('fileCreatedAt', '>=', fileCreatedAfter!)) .$if(fileCreatedBefore !== undefined, (q) => q.where('fileCreatedAt', '<=', fileCreatedBefore!)) - .where('deletedAt', 'is', null) .where((eb) => { const expression: Expression[] = []; @@ -134,10 +125,31 @@ export class MapRepository { return eb.or(expression); }) - .orderBy('fileCreatedAt', 'desc') .execute(); } + private mapMarkersQuery() { + return this.db + .selectFrom('asset') + .innerJoin('asset_exif', (builder) => + builder + .onRef('asset.id', '=', 'asset_exif.assetId') + .on('asset_exif.latitude', 'is not', null) + .on('asset_exif.longitude', 'is not', null), + ) + .where('asset.deletedAt', 'is', null) + .orderBy('fileCreatedAt', 'desc') + .select([ + 'id', + 'asset_exif.latitude as lat', + 'asset_exif.longitude as lon', + 'asset_exif.city', + 'asset_exif.state', + 'asset_exif.country', + ]) + .$narrowType<{ lat: NotNull; lon: NotNull }>(); + } + async reverseGeocode(point: GeoPoint): Promise { this.logger.debug(`Request: ${point.latitude},${point.longitude}`); diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index 8f8a5be0bd..171102a660 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -14,12 +14,10 @@ import { isValidInteger } from 'src/validation'; export interface SearchAssetIdOptions { checksum?: Buffer; - deviceAssetId?: string; id?: string; } export interface SearchUserIdOptions { - deviceId?: string; libraryId?: string | null; userIds?: string[]; } diff --git a/server/src/repositories/telemetry.repository.ts b/server/src/repositories/telemetry.repository.ts index 5fbbb76cf7..d87c0acf5a 100644 --- a/server/src/repositories/telemetry.repository.ts +++ b/server/src/repositories/telemetry.repository.ts @@ -11,7 +11,6 @@ import { resourceFromAttributes } from '@opentelemetry/resources'; import { AggregationType } from '@opentelemetry/sdk-metrics'; import { NodeSDK, contextBase } from '@opentelemetry/sdk-node'; import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions'; -import { ClassConstructor } from 'class-transformer'; import { snakeCase, startCase } from 'lodash'; import { MetricService } from 'nestjs-otel'; import { copyMetadataFromFunctionToFunction } from 'nestjs-otel/lib/opentelemetry.utils'; @@ -118,7 +117,7 @@ export class TelemetryRepository { this.repo = new MetricGroupRepository(metricService).configure({ enabled: metrics.has(ImmichTelemetry.Repo) }); } - setup({ repositories }: { repositories: ClassConstructor[] }) { + setup({ repositories }: { repositories: (new (...args: any[]) => unknown)[] }) { const { telemetry } = this.configRepository.getEnv(); const { metrics } = telemetry; if (!metrics.has(ImmichTelemetry.Repo)) { @@ -136,7 +135,7 @@ export class TelemetryRepository { } } - private wrap(Repository: ClassConstructor) { + private wrap(Repository: new (...args: any[]) => unknown) { const className = Repository.name; const descriptors = Object.getOwnPropertyDescriptors(Repository.prototype); const unit = 'ms'; diff --git a/server/src/schema/index.ts b/server/src/schema/index.ts index 2426c2aab7..e3db3d01c7 100644 --- a/server/src/schema/index.ts +++ b/server/src/schema/index.ts @@ -40,7 +40,6 @@ import { AssetMetadataAuditTable } from 'src/schema/tables/asset-metadata-audit. import { AssetMetadataTable } from 'src/schema/tables/asset-metadata.table'; import { AssetOcrTable } from 'src/schema/tables/asset-ocr.table'; import { AssetTable } from 'src/schema/tables/asset.table'; -import { AuditTable } from 'src/schema/tables/audit.table'; import { FaceSearchTable } from 'src/schema/tables/face-search.table'; import { GeodataPlacesTable } from 'src/schema/tables/geodata-places.table'; import { LibraryTable } from 'src/schema/tables/library.table'; @@ -98,7 +97,6 @@ export class ImmichDatabase { AssetOcrTable, AssetTable, AssetFileTable, - AuditTable, AssetExifTable, FaceSearchTable, GeodataPlacesTable, @@ -197,8 +195,6 @@ export interface DB { asset_ocr: AssetOcrTable; ocr_search: OcrSearchTable; - audit: AuditTable; - face_search: FaceSearchTable; geodata_places: GeodataPlacesTable; diff --git a/server/src/schema/migrations/1776217577402-DropAuditTable.ts b/server/src/schema/migrations/1776217577402-DropAuditTable.ts new file mode 100644 index 0000000000..f7e968a3ed --- /dev/null +++ b/server/src/schema/migrations/1776217577402-DropAuditTable.ts @@ -0,0 +1,18 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`DROP TABLE "audit";`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`CREATE TABLE "audit" ( + "id" serial NOT NULL, + "entityType" character varying NOT NULL, + "entityId" uuid NOT NULL, + "action" character varying NOT NULL, + "ownerId" uuid NOT NULL, + "createdAt" timestamp with time zone NOT NULL DEFAULT now(), + CONSTRAINT "audit_pkey" PRIMARY KEY ("id") +);`.execute(db); + await sql`CREATE INDEX "audit_ownerId_createdAt_idx" ON "audit" ("ownerId", "createdAt");`.execute(db); +} diff --git a/server/src/schema/migrations/1776263790468-DropDeviceIdAndDeviceAssetId.ts b/server/src/schema/migrations/1776263790468-DropDeviceIdAndDeviceAssetId.ts new file mode 100644 index 0000000000..97ed6eceda --- /dev/null +++ b/server/src/schema/migrations/1776263790468-DropDeviceIdAndDeviceAssetId.ts @@ -0,0 +1,11 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "asset" DROP COLUMN "deviceAssetId";`.execute(db); + await sql`ALTER TABLE "asset" DROP COLUMN "deviceId";`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`ALTER TABLE "asset" ADD "deviceAssetId" character varying NOT NULL;`.execute(db); + await sql`ALTER TABLE "asset" ADD "deviceId" character varying NOT NULL;`.execute(db); +} diff --git a/server/src/schema/tables/asset-metadata.table.ts b/server/src/schema/tables/asset-metadata.table.ts index 53e3121a41..dc5b984160 100644 --- a/server/src/schema/tables/asset-metadata.table.ts +++ b/server/src/schema/tables/asset-metadata.table.ts @@ -35,7 +35,7 @@ export class AssetMetadataTable { key!: AssetMetadataKey | string; @Column({ type: 'jsonb' }) - value!: object; + value!: Record; @UpdateIdColumn({ index: true }) updateId!: Generated; diff --git a/server/src/schema/tables/asset.table.ts b/server/src/schema/tables/asset.table.ts index 7418dab102..e19a52af9c 100644 --- a/server/src/schema/tables/asset.table.ts +++ b/server/src/schema/tables/asset.table.ts @@ -65,15 +65,9 @@ export class AssetTable { @PrimaryGeneratedColumn() id!: Generated; - @Column() - deviceAssetId!: string; - @ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false }) ownerId!: string; - @Column() - deviceId!: string; - @Column() type!: AssetType; diff --git a/server/src/schema/tables/audit.table.ts b/server/src/schema/tables/audit.table.ts deleted file mode 100644 index 78c9a57c09..0000000000 --- a/server/src/schema/tables/audit.table.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Column, CreateDateColumn, Generated, Index, PrimaryColumn, Table, Timestamp } from '@immich/sql-tools'; -import { DatabaseAction, EntityType } from 'src/enum'; - -@Table('audit') -@Index({ columns: ['ownerId', 'createdAt'] }) -export class AuditTable { - @PrimaryColumn({ type: 'serial', synchronize: false }) - id!: Generated; - - @Column() - entityType!: EntityType; - - @Column({ type: 'uuid' }) - entityId!: string; - - @Column() - action!: DatabaseAction; - - @Column({ type: 'uuid' }) - ownerId!: string; - - @CreateDateColumn() - createdAt!: Generated; -} diff --git a/server/src/schema/tables/memory.table.ts b/server/src/schema/tables/memory.table.ts index 8b9867b4cc..c132d42fee 100644 --- a/server/src/schema/tables/memory.table.ts +++ b/server/src/schema/tables/memory.table.ts @@ -43,7 +43,7 @@ export class MemoryTable { type!: MemoryType; @Column({ type: 'jsonb' }) - data!: object; + data!: Record; /** unless set to true, will be automatically deleted in the future */ @Column({ type: 'boolean', default: false }) diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index 47646d0c6d..4f5d4edd00 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -563,9 +563,9 @@ describe(AlbumService.name, () => { }, ]); - await sut.get(AuthFactory.create(album.owner), album.id, {}); + await sut.get(AuthFactory.create(album.owner), album.id); - expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: true }); + expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: false }); expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(album.owner.id, new Set([album.id])); }); @@ -584,9 +584,9 @@ describe(AlbumService.name, () => { ]); const auth = AuthFactory.from().sharedLink().build(); - await sut.get(auth, album.id, {}); + await sut.get(auth, album.id); - expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: true }); + expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: false }); expect(mocks.access.album.checkSharedLinkAccess).toHaveBeenCalledWith(auth.sharedLink!.id, new Set([album.id])); }); @@ -605,9 +605,9 @@ describe(AlbumService.name, () => { }, ]); - await sut.get(AuthFactory.create(user), album.id, {}); + await sut.get(AuthFactory.create(user), album.id); - expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: true }); + expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: false }); expect(mocks.access.album.checkSharedAlbumAccess).toHaveBeenCalledWith( user.id, new Set([album.id]), @@ -617,7 +617,7 @@ describe(AlbumService.name, () => { it('should throw an error for no access', async () => { const auth = AuthFactory.create(); - await expect(sut.get(auth, 'album-123', {})).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.get(auth, 'album-123')).rejects.toBeInstanceOf(BadRequestException); expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set(['album-123'])); expect(mocks.access.album.checkSharedAlbumAccess).toHaveBeenCalledWith( @@ -717,31 +717,6 @@ describe(AlbumService.name, () => { expect(mocks.album.update).not.toHaveBeenCalled(); }); - it('should allow a shared link user to add assets', async () => { - const album = AlbumFactory.create(); - const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()]; - const auth = AuthFactory.from(album.owner).sharedLink({ allowUpload: true, userId: album.ownerId }).build(); - mocks.access.album.checkSharedLinkAccess.mockResolvedValue(new Set([album.id])); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id])); - mocks.album.getById.mockResolvedValue(getForAlbum(album)); - mocks.album.getAssetIds.mockResolvedValueOnce(new Set()); - - await expect(sut.addAssets(auth, album.id, { ids: [asset1.id, asset2.id, asset3.id] })).resolves.toEqual([ - { success: true, id: asset1.id }, - { success: true, id: asset2.id }, - { success: true, id: asset3.id }, - ]); - - expect(mocks.album.update).toHaveBeenCalledWith(album.id, { - id: album.id, - updatedAt: expect.any(Date), - albumThumbnailAssetId: asset1.id, - }); - expect(mocks.album.addAssetIds).toHaveBeenCalledWith(album.id, [asset1.id, asset2.id, asset3.id]); - - expect(mocks.access.album.checkSharedLinkAccess).toHaveBeenCalledWith(auth.sharedLink?.id, new Set([album.id])); - }); - it('should allow adding assets shared via partner sharing', async () => { const album = AlbumFactory.create(); const asset = AssetFactory.create(); @@ -964,40 +939,6 @@ describe(AlbumService.name, () => { expect(mocks.album.update).not.toHaveBeenCalled(); }); - it('should not allow a shared link user to add assets to multiple albums', async () => { - const album1 = AlbumFactory.create(); - const album2 = AlbumFactory.create(); - const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()]; - mocks.access.album.checkSharedLinkAccess.mockResolvedValueOnce(new Set([album1.id])); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id])); - mocks.album.getById.mockResolvedValueOnce(getForAlbum(album1)).mockResolvedValueOnce(getForAlbum(album2)); - mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set()); - - const auth = AuthFactory.from(album1.owner).sharedLink({ allowUpload: true }).build(); - await expect( - sut.addAssetsToAlbums(auth, { - albumIds: [album1.id, album2.id], - assetIds: [asset1.id, asset2.id, asset3.id], - }), - ).resolves.toEqual({ success: true, error: undefined }); - - expect(mocks.album.update).toHaveBeenCalledTimes(1); - expect(mocks.album.update).toHaveBeenNthCalledWith(1, album1.id, { - id: album1.id, - updatedAt: expect.any(Date), - albumThumbnailAssetId: asset1.id, - }); - expect(mocks.album.addAssetIdsToAlbums).toHaveBeenCalledWith([ - { albumId: album1.id, assetId: asset1.id }, - { albumId: album1.id, assetId: asset2.id }, - { albumId: album1.id, assetId: asset3.id }, - ]); - expect(mocks.access.album.checkSharedLinkAccess).toHaveBeenCalledWith( - auth.sharedLink?.id, - new Set([album1.id, album2.id]), - ); - }); - it('should allow adding assets shared via partner sharing', async () => { const user = UserFactory.create(); const album1 = AlbumFactory.create(); diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index 547ec63bf8..8142bfeff5 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -1,7 +1,6 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { AddUsersDto, - AlbumInfoDto, AlbumResponseDto, AlbumsAddAssetsDto, AlbumsAddAssetsResponseDto, @@ -10,13 +9,12 @@ import { GetAlbumsDto, mapAlbum, MapAlbumDto, - mapAlbumWithAssets, - mapAlbumWithoutAssets, UpdateAlbumDto, UpdateAlbumUserDto, } from 'src/dtos/album.dto'; import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; +import { MapMarkerResponseDto } from 'src/dtos/map.dto'; import { Permission } from 'src/enum'; import { AlbumAssetCount, AlbumInfoOptions } from 'src/repositories/album.repository'; import { BaseService } from 'src/services/base.service'; @@ -63,7 +61,7 @@ export class AlbumService extends BaseService { } return albums.map((album) => ({ - ...mapAlbumWithoutAssets(album), + ...mapAlbum(album), sharedLinks: undefined, startDate: asDateString(albumMetadata[album.id]?.startDate ?? undefined), endDate: asDateString(albumMetadata[album.id]?.endDate ?? undefined), @@ -73,11 +71,10 @@ export class AlbumService extends BaseService { })); } - async get(auth: AuthDto, id: string, dto: AlbumInfoDto): Promise { + async get(auth: AuthDto, id: string): Promise { await this.requireAccess({ auth, permission: Permission.AlbumRead, ids: [id] }); await this.albumRepository.updateThumbnails(); - const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets; - const album = await this.findOrFail(id, { withAssets }); + const album = await this.findOrFail(id, { withAssets: false }); const [albumMetadataForIds] = await this.albumRepository.getMetadataForIds([album.id]); const hasSharedUsers = album.albumUsers && album.albumUsers.length > 0; @@ -85,7 +82,7 @@ export class AlbumService extends BaseService { const isShared = hasSharedUsers || hasSharedLink; return { - ...mapAlbum(album, withAssets, auth), + ...mapAlbum(album), startDate: asDateString(albumMetadataForIds?.startDate ?? undefined), endDate: asDateString(albumMetadataForIds?.endDate ?? undefined), assetCount: albumMetadataForIds?.assetCount ?? 0, @@ -94,6 +91,16 @@ export class AlbumService extends BaseService { }; } + async getMapMarkers(auth: AuthDto, id: string): Promise { + await this.requireAccess({ auth, permission: Permission.AlbumRead, ids: [id] }); + + if (auth.sharedLink && !auth.sharedLink.showExif) { + return []; + } + + return this.mapRepository.getAlbumMapMarkers(id); + } + async create(auth: AuthDto, dto: CreateAlbumDto): Promise { const albumUsers = dto.albumUsers || []; @@ -133,7 +140,7 @@ export class AlbumService extends BaseService { await this.eventRepository.emit('AlbumInvite', { id: album.id, userId }); } - return mapAlbumWithAssets(album); + return mapAlbum(album); } async update(auth: AuthDto, id: string, dto: UpdateAlbumDto): Promise { @@ -156,7 +163,7 @@ export class AlbumService extends BaseService { order: dto.order, }); - return mapAlbumWithoutAssets({ ...updatedAlbum, assets: album.assets }); + return mapAlbum({ ...updatedAlbum, assets: album.assets }); } async delete(auth: AuthDto, id: string): Promise { @@ -165,12 +172,6 @@ export class AlbumService extends BaseService { } async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { - if (auth.sharedLink) { - this.logger.deprecate( - 'Assets uploaded to a shared link are automatically added and calling this endpoint is no longer necessary. It will be removed in the next major release.', - ); - } - const album = await this.findOrFail(id, { withAssets: false }); await this.requireAccess({ auth, permission: Permission.AlbumAssetCreate, ids: [id] }); @@ -201,12 +202,6 @@ export class AlbumService extends BaseService { } async addAssetsToAlbums(auth: AuthDto, dto: AlbumsAddAssetsDto): Promise { - if (auth.sharedLink) { - this.logger.deprecate( - 'Assets uploaded to a shared link are automatically added and calling this endpoint is no longer necessary. It will be removed in the next major release.', - ); - } - const results: AlbumsAddAssetsResponseDto = { success: false, error: BulkIdErrorReason.DUPLICATE, @@ -306,7 +301,7 @@ export class AlbumService extends BaseService { await this.eventRepository.emit('AlbumInvite', { id, userId }); } - return this.findOrFail(id, { withAssets: true }).then(mapAlbumWithoutAssets); + return this.findOrFail(id, { withAssets: true }).then(mapAlbum); } async removeUser(auth: AuthDto, id: string, userId: string | 'me'): Promise { diff --git a/server/src/services/api-key.service.ts b/server/src/services/api-key.service.ts index 534de69107..acd3b17e94 100644 --- a/server/src/services/api-key.service.ts +++ b/server/src/services/api-key.service.ts @@ -1,6 +1,6 @@ import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common'; import { ApiKey } from 'src/database'; -import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto, APIKeyUpdateDto } from 'src/dtos/api-key.dto'; +import { ApiKeyCreateDto, ApiKeyCreateResponseDto, ApiKeyResponseDto, ApiKeyUpdateDto } from 'src/dtos/api-key.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { Permission } from 'src/enum'; import { BaseService } from 'src/services/base.service'; @@ -8,7 +8,7 @@ import { isGranted } from 'src/utils/access'; @Injectable() export class ApiKeyService extends BaseService { - async create(auth: AuthDto, dto: APIKeyCreateDto): Promise { + async create(auth: AuthDto, dto: ApiKeyCreateDto): Promise { const token = this.cryptoRepository.randomBytesAsText(32); const hashed = this.cryptoRepository.hashSha256(token); @@ -26,7 +26,7 @@ export class ApiKeyService extends BaseService { return { secret: token, apiKey: this.map(entity) }; } - async update(auth: AuthDto, id: string, dto: APIKeyUpdateDto): Promise { + async update(auth: AuthDto, id: string, dto: ApiKeyUpdateDto): Promise { const exists = await this.apiKeyRepository.getById(auth.user.id, id); if (!exists) { throw new BadRequestException('API Key not found'); @@ -54,7 +54,7 @@ export class ApiKeyService extends BaseService { await this.apiKeyRepository.delete(auth.user.id, id); } - async getMine(auth: AuthDto): Promise { + async getMine(auth: AuthDto): Promise { if (!auth.apiKey) { throw new ForbiddenException('Not authenticated with an API Key'); } @@ -67,7 +67,7 @@ export class ApiKeyService extends BaseService { return this.map(key); } - async getById(auth: AuthDto, id: string): Promise { + async getById(auth: AuthDto, id: string): Promise { const key = await this.apiKeyRepository.getById(auth.user.id, id); if (!key) { throw new BadRequestException('API Key not found'); @@ -75,12 +75,12 @@ export class ApiKeyService extends BaseService { return this.map(key); } - async getAll(auth: AuthDto): Promise { + async getAll(auth: AuthDto): Promise { const keys = await this.apiKeyRepository.getByUserId(auth.user.id); return keys.map((key) => this.map(key)); } - private map(entity: ApiKey): APIKeyResponseDto { + private map(entity: ApiKey): ApiKeyResponseDto { return { id: entity.id, name: entity.name, diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts index 3dba0c6d34..ce8ecac72c 100644 --- a/server/src/services/asset-media.service.spec.ts +++ b/server/src/services/asset-media.service.spec.ts @@ -145,8 +145,6 @@ const uploadTests = [ ]; const createDto = Object.freeze({ - deviceAssetId: 'deviceAssetId', - deviceId: 'deviceId', fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), isFavorite: false, @@ -156,8 +154,6 @@ const createDto = Object.freeze({ const assetEntity = Object.freeze({ id: 'id_1', ownerId: 'user_id_1', - deviceAssetId: 'device_asset_id_1', - deviceId: 'device_id_1', type: AssetType.Video, originalPath: 'fake_path/asset_1.jpeg', fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), @@ -765,17 +761,6 @@ describe(AssetMediaService.name, () => { }); }); - describe('checkExistingAssets', () => { - it('should get existing asset ids', async () => { - mocks.asset.getByDeviceIds.mockResolvedValue(['42']); - await expect( - sut.checkExistingAssets(authStub.admin, { deviceId: '420', deviceAssetIds: ['69'] }), - ).resolves.toEqual({ existingIds: ['42'] }); - - expect(mocks.asset.getByDeviceIds).toHaveBeenCalledWith(userStub.admin.id, '420', ['69']); - }); - }); - describe('bulkUploadCheck', () => { it('should accept hex and base64 checksums', async () => { const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex'); diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts index ddb6f412c5..74aaa8fcbd 100644 --- a/server/src/services/asset-media.service.ts +++ b/server/src/services/asset-media.service.ts @@ -2,29 +2,25 @@ import { BadRequestException, Injectable, InternalServerErrorException, NotFound import { extname } from 'node:path'; import sanitize from 'sanitize-filename'; import { StorageCore } from 'src/cores/storage.core'; -import { Asset, AuthSharedLink } from 'src/database'; +import { AuthSharedLink } from 'src/database'; import { AssetBulkUploadCheckResponseDto, AssetMediaResponseDto, AssetMediaStatus, AssetRejectReason, AssetUploadAction, - CheckExistingAssetsResponseDto, } from 'src/dtos/asset-media-response.dto'; import { AssetBulkUploadCheckDto, AssetMediaCreateDto, AssetMediaOptionsDto, - AssetMediaReplaceDto, AssetMediaSize, - CheckExistingAssetsDto, UploadFieldName, } from 'src/dtos/asset-media.dto'; import { AssetDownloadOriginalDto } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetFileType, - AssetStatus, AssetVisibility, CacheControl, ChecksumAlgorithm, @@ -164,40 +160,6 @@ export class AssetMediaService extends BaseService { } } - async replaceAsset( - auth: AuthDto, - id: string, - dto: AssetMediaReplaceDto, - file: UploadFile, - sidecarFile?: UploadFile, - ): Promise { - try { - await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: [id] }); - const asset = await this.assetRepository.getById(id); - - if (!asset) { - throw new Error('Asset not found'); - } - - this.requireQuota(auth, file.size); - - await this.replaceFileData(asset.id, dto, file, sidecarFile?.originalPath); - - // Next, create a backup copy of the existing record. The db record has already been updated above, - // but the local variable holds the original file data paths. - const copiedPhoto = await this.createCopy(asset); - // and immediate trash it - await this.assetRepository.updateAll([copiedPhoto.id], { deletedAt: new Date(), status: AssetStatus.Trashed }); - await this.eventRepository.emit('AssetTrash', { assetId: copiedPhoto.id, userId: auth.user.id }); - - await this.userRepository.updateUsage(auth.user.id, file.size); - - return { status: AssetMediaStatus.REPLACED, id: copiedPhoto.id }; - } catch (error: any) { - return this.handleUploadError(error, auth, file, sidecarFile); - } - } - async downloadOriginal(auth: AuthDto, id: string, dto: AssetDownloadOriginalDto): Promise { await this.requireAccess({ auth, permission: Permission.AssetDownload, ids: [id] }); @@ -287,18 +249,6 @@ export class AssetMediaService extends BaseService { }); } - async checkExistingAssets( - auth: AuthDto, - checkExistingAssetsDto: CheckExistingAssetsDto, - ): Promise { - const existingIds = await this.assetRepository.getByDeviceIds( - auth.user.id, - checkExistingAssetsDto.deviceId, - checkExistingAssetsDto.deviceAssetIds, - ); - return { existingIds }; - } - async bulkUploadCheck(auth: AuthDto, dto: AssetBulkUploadCheckDto): Promise { const checksums: Buffer[] = dto.assets.map((asset) => fromChecksum(asset.checksum)); const results = await this.assetRepository.getByChecksums(auth.user.id, checksums); @@ -367,83 +317,6 @@ export class AssetMediaService extends BaseService { throw error; } - /** - * Updates the specified assetId to the specified photo data file properties: checksum, path, - * timestamps, deviceIds, and sidecar. Derived properties like: faces, smart search info, etc - * are UNTOUCHED. The photo data files modification times on the filesysytem are updated to - * the specified timestamps. The exif db record is upserted, and then A METADATA_EXTRACTION - * job is queued to update these derived properties. - */ - private async replaceFileData( - assetId: string, - dto: AssetMediaReplaceDto, - file: UploadFile, - sidecarPath?: string, - ): Promise { - await this.assetRepository.update({ - id: assetId, - - checksum: file.checksum, - originalPath: file.originalPath, - type: mimeTypes.assetType(file.originalPath), - originalFileName: file.originalName, - - deviceAssetId: dto.deviceAssetId, - deviceId: dto.deviceId, - fileCreatedAt: dto.fileCreatedAt, - fileModifiedAt: dto.fileModifiedAt, - localDateTime: dto.fileCreatedAt, - duration: dto.duration || null, - - livePhotoVideoId: null, - }); - - await (sidecarPath - ? this.assetRepository.upsertFile({ assetId, type: AssetFileType.Sidecar, path: sidecarPath }) - : this.assetRepository.deleteFile({ assetId, type: AssetFileType.Sidecar })); - - await this.storageRepository.utimes(file.originalPath, new Date(), new Date(dto.fileModifiedAt)); - await this.assetRepository.upsertExif( - { assetId, fileSizeInByte: file.size }, - { lockedPropertiesBehavior: 'override' }, - ); - await this.jobRepository.queue({ - name: JobName.AssetExtractMetadata, - data: { id: assetId, source: 'upload' }, - }); - } - - /** - * Create a 'shallow' copy of the specified asset record creating a new asset record in the database. - * Uses only vital properties excluding things like: stacks, faces, smart search info, etc, - * and then queues a METADATA_EXTRACTION job. - */ - private async createCopy(asset: Omit) { - const created = await this.assetRepository.create({ - ownerId: asset.ownerId, - originalPath: asset.originalPath, - originalFileName: asset.originalFileName, - libraryId: asset.libraryId, - deviceAssetId: asset.deviceAssetId, - deviceId: asset.deviceId, - type: asset.type, - checksum: asset.checksum, - checksumAlgorithm: asset.checksumAlgorithm, - fileCreatedAt: asset.fileCreatedAt, - localDateTime: asset.localDateTime, - fileModifiedAt: asset.fileModifiedAt, - livePhotoVideoId: asset.livePhotoVideoId, - }); - - const { size } = await this.storageRepository.stat(created.originalPath); - await this.assetRepository.upsertExif( - { assetId: created.id, fileSizeInByte: size }, - { lockedPropertiesBehavior: 'override' }, - ); - await this.jobRepository.queue({ name: JobName.AssetExtractMetadata, data: { id: created.id, source: 'copy' } }); - return created; - } - private async create(ownerId: string, dto: AssetMediaCreateDto, file: UploadFile, sidecarFile?: UploadFile) { const asset = await this.assetRepository.create({ ownerId, @@ -453,9 +326,6 @@ export class AssetMediaService extends BaseService { checksumAlgorithm: ChecksumAlgorithm.sha1File, originalPath: file.originalPath, - deviceAssetId: dto.deviceAssetId, - deviceId: dto.deviceId, - fileCreatedAt: dto.fileCreatedAt, fileModifiedAt: dto.fileModifiedAt, localDateTime: dto.fileCreatedAt, diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index 718ec00f1d..13462a3246 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -7,9 +7,8 @@ import { AssetStats } from 'src/repositories/asset.repository'; import { AssetService } from 'src/services/asset.service'; import { AssetFactory } from 'test/factories/asset.factory'; import { AuthFactory } from 'test/factories/auth.factory'; -import { PartnerFactory } from 'test/factories/partner.factory'; import { authStub } from 'test/fixtures/auth.stub'; -import { getForAsset, getForAssetDeletion, getForPartner } from 'test/mappers'; +import { getForAsset, getForAssetDeletion } from 'test/mappers'; import { factory, newUuid } from 'test/small.factory'; import { makeStream, newTestService, ServiceMocks } from 'test/utils'; @@ -70,41 +69,6 @@ describe(AssetService.name, () => { }); }); - describe('getRandom', () => { - it('should get own random assets', async () => { - mocks.partner.getAll.mockResolvedValue([]); - mocks.asset.getRandom.mockResolvedValue([getForAsset(AssetFactory.create())]); - - await sut.getRandom(authStub.admin, 1); - - expect(mocks.asset.getRandom).toHaveBeenCalledWith([authStub.admin.user.id], 1); - }); - - it('should not include partner assets if not in timeline', async () => { - const partner = PartnerFactory.create({ inTimeline: false }); - const auth = AuthFactory.create({ id: partner.sharedWithId }); - - mocks.asset.getRandom.mockResolvedValue([getForAsset(AssetFactory.create())]); - mocks.partner.getAll.mockResolvedValue([getForPartner(partner)]); - - await sut.getRandom(auth, 1); - - expect(mocks.asset.getRandom).toHaveBeenCalledWith([auth.user.id], 1); - }); - - it('should include partner assets if in timeline', async () => { - const partner = PartnerFactory.create({ inTimeline: true }); - const auth = AuthFactory.create({ id: partner.sharedWithId }); - - mocks.asset.getRandom.mockResolvedValue([getForAsset(AssetFactory.create())]); - mocks.partner.getAll.mockResolvedValue([getForPartner(partner)]); - - await sut.getRandom(auth, 1); - - expect(mocks.asset.getRandom).toHaveBeenCalledWith([auth.user.id, partner.sharedById], 1); - }); - }); - describe('get', () => { it('should allow owner access', async () => { const asset = AssetFactory.create(); @@ -721,20 +685,6 @@ describe(AssetService.name, () => { }); }); - describe('getUserAssetsByDeviceId', () => { - it('get assets by device id', async () => { - const assets = [AssetFactory.create(), AssetFactory.create()]; - - mocks.asset.getAllByDeviceId.mockResolvedValue(assets.map((asset) => asset.deviceAssetId)); - - const deviceId = 'device-id'; - const result = await sut.getUserAssetsByDeviceId(authStub.user1, deviceId); - - expect(result.length).toEqual(2); - expect(result).toEqual(assets.map((asset) => asset.deviceAssetId)); - }); - }); - describe('upsertMetadata', () => { it('should throw a bad request exception if duplicate keys are sent', async () => { const asset = AssetFactory.create(); diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 1e5d23a98d..613029fe3c 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -39,7 +39,6 @@ import { requireElevatedPermission } from 'src/utils/access'; import { getAssetFiles, getDimensions, - getMyPartnerIds, isPanorama, onAfterUnlink, onBeforeLink, @@ -60,20 +59,6 @@ export class AssetService extends BaseService { return mapStats(stats); } - async getRandom(auth: AuthDto, count: number): Promise { - const partnerIds = await getMyPartnerIds({ - userId: auth.user.id, - repository: this.partnerRepository, - timelineEnabled: true, - }); - const assets = await this.assetRepository.getRandom([auth.user.id, ...partnerIds], count); - return assets.map((a) => mapAsset(a, { auth })); - } - - async getUserAssetsByDeviceId(auth: AuthDto, deviceId: string) { - return this.assetRepository.getAllByDeviceId(auth.user.id, deviceId); - } - async get(auth: AuthDto, id: string): Promise { await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [id] }); diff --git a/server/src/services/audit.service.spec.ts b/server/src/services/audit.service.spec.ts deleted file mode 100644 index 7363ea74e1..0000000000 --- a/server/src/services/audit.service.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { JobStatus } from 'src/enum'; -import { AuditService } from 'src/services/audit.service'; -import { newTestService, ServiceMocks } from 'test/utils'; - -describe(AuditService.name, () => { - let sut: AuditService; - let mocks: ServiceMocks; - - beforeEach(() => { - ({ sut, mocks } = newTestService(AuditService)); - }); - - it('should work', () => { - expect(sut).toBeDefined(); - }); - - describe('handleCleanup', () => { - it('should delete old audit entries', async () => { - mocks.audit.removeBefore.mockResolvedValue(); - - await expect(sut.handleCleanup()).resolves.toBe(JobStatus.Success); - - expect(mocks.audit.removeBefore).toHaveBeenCalledWith(expect.any(Date)); - }); - }); -}); diff --git a/server/src/services/audit.service.ts b/server/src/services/audit.service.ts deleted file mode 100644 index 498d99b82c..0000000000 --- a/server/src/services/audit.service.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { DateTime } from 'luxon'; -import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; -import { OnJob } from 'src/decorators'; -import { JobName, JobStatus, QueueName } from 'src/enum'; -import { BaseService } from 'src/services/base.service'; - -@Injectable() -export class AuditService extends BaseService { - @OnJob({ name: JobName.AuditLogCleanup, queue: QueueName.BackgroundTask }) - async handleCleanup(): Promise { - await this.auditRepository.removeBefore(DateTime.now().minus(AUDIT_LOG_MAX_DURATION).toJSDate()); - return JobStatus.Success; - } -} diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index 2c4b31c83a..94b8acd25e 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -13,6 +13,7 @@ import { SessionFactory } from 'test/factories/session.factory'; import { UserFactory } from 'test/factories/user.factory'; import { sharedLinkStub } from 'test/fixtures/shared-link.stub'; import { systemConfigStub } from 'test/fixtures/system-config.stub'; +import { userStub } from 'test/fixtures/user.stub'; import { newUuid } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; @@ -209,11 +210,13 @@ describe(AuthService.name, () => { it('should sign up the admin', async () => { mocks.user.getAdmin.mockResolvedValue(void 0); mocks.user.create.mockResolvedValue({ + ...userStub.admin, ...dto, id: 'admin', + name: 'immich admin', createdAt: new Date('2021-01-01'), metadata: [] as UserMetadataItem[], - } as unknown as UserAdmin); + } as UserAdmin); await expect(sut.adminSignUp(dto)).resolves.toMatchObject({ avatarColor: expect.any(String), diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 5932855a21..498c165888 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -1,5 +1,4 @@ import { BadRequestException, ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common'; -import { isString } from 'class-validator'; import { parse } from 'cookie'; import { DateTime } from 'luxon'; import { IncomingHttpHeaders } from 'node:http'; @@ -312,7 +311,7 @@ export class AuthService extends BaseService { const storageLabel = this.getClaim(profile, { key: storageLabelClaim, default: '', - isValid: isString, + isValid: (value: unknown): value is string => typeof value === 'string', }); const storageQuota = this.getClaim(profile, { key: storageQuotaClaim, @@ -322,7 +321,7 @@ export class AuthService extends BaseService { const role = this.getClaim<'admin' | 'user'>(profile, { key: roleClaim, default: 'user', - isValid: (value: unknown) => isString(value) && ['admin', 'user'].includes(value), + isValid: (value: unknown) => typeof value === 'string' && ['admin', 'user'].includes(value), }); user = await this.createUser({ diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts index b3a50a07ae..4b02d6e944 100644 --- a/server/src/services/base.service.ts +++ b/server/src/services/base.service.ts @@ -14,7 +14,6 @@ import { AppRepository } from 'src/repositories/app.repository'; import { AssetEditRepository } from 'src/repositories/asset-edit.repository'; import { AssetJobRepository } from 'src/repositories/asset-job.repository'; import { AssetRepository } from 'src/repositories/asset.repository'; -import { AuditRepository } from 'src/repositories/audit.repository'; import { ConfigRepository } from 'src/repositories/config.repository'; import { CronRepository } from 'src/repositories/cron.repository'; import { CryptoRepository } from 'src/repositories/crypto.repository'; @@ -72,7 +71,6 @@ export const BASE_SERVICE_DEPENDENCIES = [ AssetRepository, AssetEditRepository, AssetJobRepository, - AuditRepository, ConfigRepository, CronRepository, CryptoRepository, @@ -131,7 +129,6 @@ export class BaseService { protected assetRepository: AssetRepository, protected assetEditRepository: AssetEditRepository, protected assetJobRepository: AssetJobRepository, - protected auditRepository: AuditRepository, protected configRepository: ConfigRepository, protected cronRepository: CronRepository, protected cryptoRepository: CryptoRepository, diff --git a/server/src/services/index.ts b/server/src/services/index.ts index ba54474b71..f4e82b13a4 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -4,7 +4,6 @@ import { ApiKeyService } from 'src/services/api-key.service'; import { ApiService } from 'src/services/api.service'; import { AssetMediaService } from 'src/services/asset-media.service'; import { AssetService } from 'src/services/asset.service'; -import { AuditService } from 'src/services/audit.service'; import { AuthAdminService } from 'src/services/auth-admin.service'; import { AuthService } from 'src/services/auth.service'; import { CliService } from 'src/services/cli.service'; @@ -54,7 +53,6 @@ export const services = [ ApiService, AssetMediaService, AssetService, - AuditService, AuthService, AuthAdminService, CliService, diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index 9a2ec00815..5b43ed9f61 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -570,7 +570,6 @@ describe(LibraryService.name, () => { ownerId: library.ownerId, libraryId: library.id, originalPath: '/data/user1/photo.jpg', - deviceId: 'Library Import', type: AssetType.Image, originalFileName: 'photo.jpg', isExternal: true, diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index ce3c9ee662..085650be6f 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -2,7 +2,7 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { Insertable } from 'kysely'; import { R_OK } from 'node:constants'; import { Stats } from 'node:fs'; -import path, { basename, isAbsolute, parse } from 'node:path'; +import path, { isAbsolute, parse } from 'node:path'; import picomatch from 'picomatch'; import { JOBS_LIBRARY_PAGINATION_SIZE } from 'src/constants'; import { StorageCore } from 'src/cores/storage.core'; @@ -283,6 +283,7 @@ export class LibraryService extends BaseService { private async validateImportPath(importPath: string): Promise { const validation = new ValidateLibraryImportPathResponseDto(); validation.importPath = importPath; + validation.isValid = false; if (StorageCore.isImmichPath(importPath)) { validation.message = 'Cannot use media upload folder for external libraries'; @@ -410,9 +411,6 @@ export class LibraryService extends BaseService { fileCreatedAt: stat.mtime, fileModifiedAt: stat.mtime, localDateTime: stat.mtime, - // TODO: device asset id is deprecated, remove it - deviceAssetId: `${basename(assetPath)}`.replaceAll(/\s+/g, ''), - deviceId: 'Library Import', type: mimeTypes.isVideo(assetPath) ? AssetType.Video : AssetType.Image, originalFileName: parse(assetPath).base, isExternal: true, diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index f9d17079e3..245bb441a6 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -654,8 +654,6 @@ describe(MetadataService.name, () => { expect(mocks.asset.create).toHaveBeenCalledWith({ checksum: expect.any(Buffer), checksumAlgorithm: ChecksumAlgorithm.sha1File, - deviceAssetId: 'NONE', - deviceId: 'NONE', fileCreatedAt: asset.fileCreatedAt, fileModifiedAt: asset.fileModifiedAt, id: motionAsset.id, @@ -708,8 +706,6 @@ describe(MetadataService.name, () => { expect(mocks.asset.create).toHaveBeenCalledWith({ checksum: expect.any(Buffer), checksumAlgorithm: ChecksumAlgorithm.sha1File, - deviceAssetId: 'NONE', - deviceId: 'NONE', fileCreatedAt: asset.fileCreatedAt, fileModifiedAt: asset.fileModifiedAt, id: motionAsset.id, @@ -762,8 +758,6 @@ describe(MetadataService.name, () => { expect(mocks.asset.create).toHaveBeenCalledWith({ checksum: expect.any(Buffer), checksumAlgorithm: ChecksumAlgorithm.sha1File, - deviceAssetId: 'NONE', - deviceId: 'NONE', fileCreatedAt: asset.fileCreatedAt, fileModifiedAt: asset.fileModifiedAt, id: motionAsset.id, diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index c2cf66ad57..c548d94c74 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -681,8 +681,6 @@ export class MetadataService extends BaseService { originalPath: StorageCore.getAndroidMotionPath(asset, motionAssetId), originalFileName: `${parse(asset.originalFileName).name}.mp4`, visibility: AssetVisibility.Hidden, - deviceAssetId: 'NONE', - deviceId: 'NONE', }); isNewMotionAsset = true; diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index c7bea2b440..1eaa4f9a2c 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -1,4 +1,3 @@ -import { plainToInstance } from 'class-transformer'; import { defaults, SystemConfig } from 'src/config'; import { SystemConfigDto } from 'src/dtos/system-config.dto'; import { AssetFileType, JobName, JobStatus, UserMetadataKey } from 'src/enum'; @@ -102,7 +101,7 @@ describe(NotificationService.name, () => { it('skips smtp validation with DTO when there are no changes', async () => { const oldConfig = { ...configs.smtpEnabled }; - const newConfig = plainToInstance(SystemConfigDto, configs.smtpEnabled); + const newConfig = configs.smtpEnabled as SystemConfigDto; await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow(); expect(mocks.email.verifySmtp).not.toHaveBeenCalled(); diff --git a/server/src/services/plugin.service.ts b/server/src/services/plugin.service.ts index d78b8940d3..7209a613fe 100644 --- a/server/src/services/plugin.service.ts +++ b/server/src/services/plugin.service.ts @@ -1,11 +1,9 @@ import { Plugin as ExtismPlugin, newPlugin } from '@extism/extism'; import { BadRequestException, Injectable } from '@nestjs/common'; -import { plainToInstance } from 'class-transformer'; -import { validateOrReject } from 'class-validator'; import { join } from 'node:path'; import { Asset, WorkflowAction, WorkflowFilter } from 'src/database'; import { OnEvent, OnJob } from 'src/decorators'; -import { PluginManifestDto } from 'src/dtos/plugin-manifest.dto'; +import { PluginManifestDto, PluginManifestSchema } from 'src/dtos/plugin-manifest.dto'; import { mapPlugin, PluginResponseDto, PluginTriggerResponseDto } from 'src/dtos/plugin.dto'; import { JobName, JobStatus, PluginTriggerType, QueueName } from 'src/enum'; import { pluginTriggers } from 'src/plugins'; @@ -138,14 +136,7 @@ export class PluginService extends BaseService { private async readAndValidateManifest(manifestPath: string): Promise { const content = await this.storageRepository.readTextFile(manifestPath); const manifestData = JSON.parse(content); - const manifest = plainToInstance(PluginManifestDto, manifestData); - - await validateOrReject(manifest, { - whitelist: true, - forbidNonWhitelisted: true, - }); - - return manifest; + return PluginManifestSchema.parse(manifestData); } /////////////////////////////////////////// diff --git a/server/src/services/queue.service.spec.ts b/server/src/services/queue.service.spec.ts index 2c76fee877..d4c425e8bd 100644 --- a/server/src/services/queue.service.spec.ts +++ b/server/src/services/queue.service.spec.ts @@ -42,7 +42,6 @@ describe(QueueService.name, () => { { name: JobName.MemoryCleanup }, { name: JobName.SessionCleanup }, { name: JobName.AuditTableCleanup }, - { name: JobName.AuditLogCleanup }, { name: JobName.MemoryGenerate }, { name: JobName.UserSyncUsage }, { name: JobName.AssetGenerateThumbnailsQueueAll, data: { force: false } }, diff --git a/server/src/services/queue.service.ts b/server/src/services/queue.service.ts index cdfa2ad2ed..ba6f4c5f3b 100644 --- a/server/src/services/queue.service.ts +++ b/server/src/services/queue.service.ts @@ -1,5 +1,4 @@ import { BadRequestException, Injectable } from '@nestjs/common'; -import { ClassConstructor } from 'class-transformer'; import { SystemConfig } from 'src/config'; import { OnEvent } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; @@ -39,7 +38,7 @@ const asNightlyTasksCron = (config: SystemConfig) => { @Injectable() export class QueueService extends BaseService { - private services: ClassConstructor[] = []; + private services: (new (...args: any[]) => unknown)[] = []; private nightlyJobsLock = false; @OnEvent({ name: 'ConfigInit' }) @@ -96,7 +95,7 @@ export class QueueService extends BaseService { } } - setServices(services: ClassConstructor[]) { + setServices(services: (new (...args: any[]) => unknown)[]) { this.services = services; } @@ -271,7 +270,6 @@ export class QueueService extends BaseService { { name: JobName.MemoryCleanup }, { name: JobName.SessionCleanup }, { name: JobName.AuditTableCleanup }, - { name: JobName.AuditLogCleanup }, ); } diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index 30bc1f1f0d..77636acfd2 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -138,6 +138,12 @@ export class ServerService extends BaseService { async getStatistics(): Promise { const userStats: UserStatsQueryResponse[] = await this.userRepository.getUserStats(); const serverStats = new ServerStatsResponseDto(); + serverStats.photos ??= 0; + serverStats.videos ??= 0; + serverStats.usage ??= 0; + serverStats.usagePhotos ??= 0; + serverStats.usageVideos ??= 0; + serverStats.usageByUser ??= []; for (const user of userStats) { const usage = new UsageByUserDto(); diff --git a/server/src/services/shared-link.service.ts b/server/src/services/shared-link.service.ts index 26b15031ee..31c50b7c2c 100644 --- a/server/src/services/shared-link.service.ts +++ b/server/src/services/shared-link.service.ts @@ -150,14 +150,7 @@ export class SharedLinkService extends BaseService { } async addAssets(auth: AuthDto, id: string, dto: AssetIdsDto): Promise { - if (auth.sharedLink) { - this.logger.deprecate( - 'Assets uploaded using shared link authentication are now automatically added to the shared link during upload and in the next major release this endpoint will no longer accept shared link authentication', - ); - } - const sharedLink = await this.findOrFail(auth.user.id, id); - if (sharedLink.type !== SharedLinkType.Individual) { throw new BadRequestException('Invalid shared link type'); } diff --git a/server/src/services/storage-template.service.spec.ts b/server/src/services/storage-template.service.spec.ts index 9d7262246c..8f11a1dfa2 100644 --- a/server/src/services/storage-template.service.spec.ts +++ b/server/src/services/storage-template.service.spec.ts @@ -321,6 +321,59 @@ describe(StorageTemplateService.name, () => { }); }); + it('should render storage datetime tokens in server timezone to preserve chronological filename ordering across time zones', async () => { + const user = UserFactory.create(); + const assetBerlin = AssetFactory.from({ + fileCreatedAt: new Date('2025-12-02T14:00:00.000Z'), + originalFileName: 'A.jpg', + }) + .owner(user) + .exif({ timeZone: 'Europe/Berlin' }) + .build(); + const assetLondon = AssetFactory.from({ + fileCreatedAt: new Date('2025-12-02T14:55:00.000Z'), + originalFileName: 'B.jpg', + }) + .owner(user) + .exif({ timeZone: 'Europe/London' }) + .build(); + const config = structuredClone(defaults); + config.storageTemplate.template = '{{y}}{{MM}}{{dd}}_{{HH}}{{mm}}{{ss}}/{{filename}}'; + sut.onConfigInit({ newConfig: config }); + + mocks.user.get.mockResolvedValue(user); + mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(assetBerlin)); + mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(assetLondon)); + + await expect(sut.handleMigrationSingle({ id: assetBerlin.id })).resolves.toBe(JobStatus.Success); + await expect(sut.handleMigrationSingle({ id: assetLondon.id })).resolves.toBe(JobStatus.Success); + + const formatStorageDateTime = (date: Date) => { + const year = date.getFullYear().toString(); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const day = date.getDate().toString().padStart(2, '0'); + const hour = date.getHours().toString().padStart(2, '0'); + const minute = date.getMinutes().toString().padStart(2, '0'); + const second = date.getSeconds().toString().padStart(2, '0'); + return `${year}${month}${day}_${hour}${minute}${second}`; + }; + + expect(mocks.move.create).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + entityId: assetBerlin.id, + newPath: `/data/library/${user.id}/${formatStorageDateTime(assetBerlin.fileCreatedAt)}/A.jpg`, + }), + ); + expect(mocks.move.create).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + entityId: assetLondon.id, + newPath: `/data/library/${user.id}/${formatStorageDateTime(assetLondon.fileCreatedAt)}/B.jpg`, + }), + ); + }); + it('should migrate previously failed move from original path when it still exists', async () => { const user = UserFactory.create(); const asset = AssetFactory.from({ diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts index 3d1bc8f835..acdcc868b2 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -413,20 +413,16 @@ export class StorageTemplateService extends BaseService { lensModel: lensModel ?? '', }; - const systemTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; - const zone = asset.timeZone || systemTimeZone; - const dt = DateTime.fromJSDate(asset.fileCreatedAt, { zone }); + const dt = DateTime.fromJSDate(asset.fileCreatedAt); for (const token of Object.values(storageTokens).flat()) { substitutions[token] = dt.toFormat(token); if (albumName) { - // Use system time zone for album dates to ensure all assets get the exact same date. + // Album date tokens are rendered in the server time zone to match storage template datetime behavior. substitutions['album-startDate-' + token] = albumStartDate - ? DateTime.fromJSDate(albumStartDate, { zone: systemTimeZone }).toFormat(token) - : ''; - substitutions['album-endDate-' + token] = albumEndDate - ? DateTime.fromJSDate(albumEndDate, { zone: systemTimeZone }).toFormat(token) + ? DateTime.fromJSDate(albumStartDate).toFormat(token) : ''; + substitutions['album-endDate-' + token] = albumEndDate ? DateTime.fromJSDate(albumEndDate).toFormat(token) : ''; } } diff --git a/server/src/services/sync.service.spec.ts b/server/src/services/sync.service.spec.ts deleted file mode 100644 index 234e3ac223..0000000000 --- a/server/src/services/sync.service.spec.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { mapAsset } from 'src/dtos/asset-response.dto'; -import { SyncService } from 'src/services/sync.service'; -import { AssetFactory } from 'test/factories/asset.factory'; -import { PartnerFactory } from 'test/factories/partner.factory'; -import { authStub } from 'test/fixtures/auth.stub'; -import { getForAsset, getForPartner } from 'test/mappers'; -import { factory } from 'test/small.factory'; -import { newTestService, ServiceMocks } from 'test/utils'; - -const untilDate = new Date(2024); -const mapAssetOpts = { auth: authStub.user1, stripMetadata: false, withStack: true }; - -describe(SyncService.name, () => { - let sut: SyncService; - let mocks: ServiceMocks; - - beforeEach(() => { - ({ sut, mocks } = newTestService(SyncService)); - }); - - it('should exist', () => { - expect(sut).toBeDefined(); - }); - - describe('getAllAssetsForUserFullSync', () => { - it('should return a list of all assets owned by the user', async () => { - const [asset1, asset2] = [ - AssetFactory.from({ libraryId: 'library-id', isExternal: true }).owner(authStub.user1.user).build(), - AssetFactory.from().owner(authStub.user1.user).build(), - ]; - mocks.asset.getAllForUserFullSync.mockResolvedValue([getForAsset(asset1), getForAsset(asset2)]); - await expect(sut.getFullSync(authStub.user1, { limit: 2, updatedUntil: untilDate })).resolves.toEqual([ - mapAsset(getForAsset(asset1), mapAssetOpts), - mapAsset(getForAsset(asset2), mapAssetOpts), - ]); - expect(mocks.asset.getAllForUserFullSync).toHaveBeenCalledWith({ - ownerId: authStub.user1.user.id, - updatedUntil: untilDate, - limit: 2, - }); - }); - }); - - describe('getChangesForDeltaSync', () => { - it('should return a response requiring a full sync when partners are out of sync', async () => { - const partner = PartnerFactory.create(); - const auth = factory.auth({ user: { id: partner.sharedWithId } }); - - mocks.partner.getAll.mockResolvedValue([getForPartner(partner)]); - - await expect( - sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [auth.user.id] }), - ).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] }); - - expect(mocks.asset.getChangedDeltaSync).toHaveBeenCalledTimes(0); - expect(mocks.audit.getAfter).toHaveBeenCalledTimes(0); - }); - - it('should return a response requiring a full sync when last sync was too long ago', async () => { - mocks.partner.getAll.mockResolvedValue([]); - await expect( - sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(2000), userIds: [authStub.user1.user.id] }), - ).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] }); - expect(mocks.asset.getChangedDeltaSync).toHaveBeenCalledTimes(0); - expect(mocks.audit.getAfter).toHaveBeenCalledTimes(0); - }); - - it('should return a response requiring a full sync when there are too many changes', async () => { - const asset = AssetFactory.create(); - mocks.partner.getAll.mockResolvedValue([]); - mocks.asset.getChangedDeltaSync.mockResolvedValue( - Array.from>({ length: 10_000 }).fill(getForAsset(asset)), - ); - await expect( - sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }), - ).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] }); - expect(mocks.asset.getChangedDeltaSync).toHaveBeenCalledTimes(1); - expect(mocks.audit.getAfter).toHaveBeenCalledTimes(0); - }); - - it('should return a response with changes and deletions', async () => { - const asset = AssetFactory.create({ ownerId: authStub.user1.user.id }); - const deletedAsset = AssetFactory.create({ libraryId: 'library-id', isExternal: true }); - mocks.partner.getAll.mockResolvedValue([]); - mocks.asset.getChangedDeltaSync.mockResolvedValue([getForAsset(asset)]); - mocks.audit.getAfter.mockResolvedValue([deletedAsset.id]); - await expect( - sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }), - ).resolves.toEqual({ - needsFullSync: false, - upserted: [mapAsset(getForAsset(asset), mapAssetOpts)], - deleted: [deletedAsset.id], - }); - expect(mocks.asset.getChangedDeltaSync).toHaveBeenCalledTimes(1); - expect(mocks.audit.getAfter).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/server/src/services/sync.service.ts b/server/src/services/sync.service.ts index 9bdeca14d7..50bf9368c8 100644 --- a/server/src/services/sync.service.ts +++ b/server/src/services/sync.service.ts @@ -2,14 +2,9 @@ import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/com import { Insertable } from 'kysely'; import { DateTime, Duration } from 'luxon'; import { Writable } from 'node:stream'; -import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; import { OnJob } from 'src/decorators'; -import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { - AssetDeltaSyncDto, - AssetDeltaSyncResponseDto, - AssetFullSyncDto, SyncAckDeleteDto, SyncAckSetDto, syncAssetFaceV2ToV1, @@ -17,23 +12,12 @@ import { SyncItem, SyncStreamDto, } from 'src/dtos/sync.dto'; -import { - AssetVisibility, - DatabaseAction, - EntityType, - JobName, - Permission, - QueueName, - SyncEntityType, - SyncRequestType, -} from 'src/enum'; +import { JobName, QueueName, SyncEntityType, SyncRequestType } from 'src/enum'; import { SyncQueryOptions } from 'src/repositories/sync.repository'; import { SessionSyncCheckpointTable } from 'src/schema/tables/sync-checkpoint.table'; import { BaseService } from 'src/services/base.service'; import { SyncAck } from 'src/types'; -import { getMyPartnerIds } from 'src/utils/asset.util'; import { hexOrBufferToBase64 } from 'src/utils/bytes'; -import { setIsEqual } from 'src/utils/set'; import { fromAck, serialize, SerializeOptions, toAck } from 'src/utils/sync'; type CheckpointMap = Partial>; @@ -66,7 +50,6 @@ const sendEntityBackfillCompleteAck = (response: Writable, ackType: SyncEntityTy send(response, { type: SyncEntityType.SyncAckV1, data: {}, ackType, ids: [id, COMPLETE_ID] }); }; -const FULL_SYNC = { needsFullSync: true, deleted: [], upserted: [] }; export const SYNC_TYPES_ORDER = [ SyncRequestType.AuthUsersV1, SyncRequestType.UsersV1, @@ -887,68 +870,4 @@ export class SyncService extends BaseService { }, ]); } - - async getFullSync(auth: AuthDto, dto: AssetFullSyncDto): Promise { - // mobile implementation is faster if this is a single id - const userId = dto.userId || auth.user.id; - await this.requireAccess({ auth, permission: Permission.TimelineRead, ids: [userId] }); - const assets = await this.assetRepository.getAllForUserFullSync({ - ownerId: userId, - updatedUntil: dto.updatedUntil, - lastId: dto.lastId, - limit: dto.limit, - }); - return assets.map((a) => mapAsset(a, { auth, stripMetadata: false, withStack: true })); - } - - async getDeltaSync(auth: AuthDto, dto: AssetDeltaSyncDto): Promise { - // app has not synced in the last 100 days - const duration = DateTime.now().diff(DateTime.fromJSDate(dto.updatedAfter)); - if (duration > AUDIT_LOG_MAX_DURATION) { - return FULL_SYNC; - } - - // app does not have the correct partners synced - const partnerIds = await getMyPartnerIds({ userId: auth.user.id, repository: this.partnerRepository }); - const userIds = [auth.user.id, ...partnerIds]; - if (!setIsEqual(new Set(userIds), new Set(dto.userIds))) { - return FULL_SYNC; - } - - await this.requireAccess({ auth, permission: Permission.TimelineRead, ids: dto.userIds }); - - const limit = 10_000; - const upserted = await this.assetRepository.getChangedDeltaSync({ limit, updatedAfter: dto.updatedAfter, userIds }); - - // too many changes, need to do a full sync - if (upserted.length === limit) { - return FULL_SYNC; - } - - const deleted = await this.auditRepository.getAfter(dto.updatedAfter, { - userIds, - entityType: EntityType.Asset, - action: DatabaseAction.Delete, - }); - - const result = { - needsFullSync: false, - upserted: upserted - // do not return archived assets for partner users - .filter( - (a) => - a.ownerId === auth.user.id || (a.ownerId !== auth.user.id && a.visibility === AssetVisibility.Timeline), - ) - .map((a) => - mapAsset(a, { - auth, - stripMetadata: false, - // ignore stacks for non partner users - withStack: a.ownerId === auth.user.id, - }), - ), - deleted, - }; - return result; - } } diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index b346906fc8..bb68f70d13 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -311,9 +311,7 @@ describe(SystemConfigService.name, () => { mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' })); mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify({ library: { scan: { cronExpression: 'foo' } } })); - await expect(sut.getSystemConfig()).rejects.toThrow( - 'library.scan.cronExpression has failed the following constraints: cronValidator', - ); + await expect(sut.getSystemConfig()).rejects.toThrow('[library.scan.cronExpression] Invalid cron expression'); }); it('should log errors with the config file', async () => { @@ -402,10 +400,26 @@ describe(SystemConfigService.name, () => { }); const tests = [ - { should: 'validate numbers', config: { ffmpeg: { crf: 'not-a-number' } } }, - { should: 'validate booleans', config: { oauth: { enabled: 'invalid' } } }, - { should: 'validate enums', config: { ffmpeg: { transcode: 'unknown' } } }, - { should: 'validate required oauth fields', config: { oauth: { enabled: true } } }, + { + should: 'validate numbers', + config: { ffmpeg: { crf: 'not-a-number' } }, + throws: '[ffmpeg.crf] Invalid input: expected number, received NaN', + }, + { + should: 'validate booleans', + config: { oauth: { enabled: 'invalid' } }, + throws: '[oauth.enabled] Invalid input: expected boolean, received string', + }, + { + should: 'validate enums', + config: { ffmpeg: { transcode: 'unknown' } }, + throws: '[ffmpeg.transcode] Invalid option: expected one of', + }, + { + should: 'validate required oauth fields', + config: { oauth: { enabled: true } }, + check: (c: SystemConfig) => expect(c.oauth.enabled).toBe(true), + }, { should: 'warn for top level unknown options', warn: true, config: { unknownOption: true } }, { should: 'warn for nested unknown options', warn: true, config: { ffmpeg: { unknownOption: true } } }, ]; @@ -415,11 +429,14 @@ describe(SystemConfigService.name, () => { mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' })); mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify(test.config)); - if (test.warn) { + if (test.throws) { + await expect(sut.getSystemConfig()).rejects.toThrow(test.throws); + } else if (test.warn) { await sut.getSystemConfig(); expect(mocks.logger.warn).toHaveBeenCalled(); } else { - await expect(sut.getSystemConfig()).rejects.toBeInstanceOf(Error); + const config = await sut.getSystemConfig(); + test.check!(config); } }); } diff --git a/server/src/services/system-config.service.ts b/server/src/services/system-config.service.ts index ea95b4df24..2762b9e754 100644 --- a/server/src/services/system-config.service.ts +++ b/server/src/services/system-config.service.ts @@ -1,5 +1,4 @@ import { BadRequestException, Injectable } from '@nestjs/common'; -import { instanceToPlain } from 'class-transformer'; import _ from 'lodash'; import { defaults } from 'src/config'; import { OnEvent } from 'src/decorators'; @@ -16,15 +15,6 @@ export class SystemConfigService extends BaseService { async onBootstrap() { const config = await this.getConfig({ withCache: false }); await this.eventRepository.emit('ConfigInit', { newConfig: config }); - - if ( - process.env.IMMICH_MACHINE_LEARNING_PING_TIMEOUT || - process.env.IMMICH_MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME - ) { - this.logger.deprecate( - 'IMMICH_MACHINE_LEARNING_PING_TIMEOUT and MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME have been moved to system config(`machineLearning.availabilityChecks`) and will be removed in a future release.', - ); - } } @OnEvent({ name: 'AppShutdown' }) @@ -61,7 +51,7 @@ export class SystemConfigService extends BaseService { @OnEvent({ name: 'ConfigValidate' }) onConfigValidate({ newConfig, oldConfig }: ArgOf<'ConfigValidate'>) { const { logLevel } = this.configRepository.getEnv(); - if (!_.isEqual(instanceToPlain(newConfig.logging), oldConfig.logging) && logLevel) { + if (!_.isEqual(toPlainObject(newConfig.logging), oldConfig.logging) && logLevel) { throw new Error('Logging cannot be changed while the environment variable IMMICH_LOG_LEVEL is set.'); } } diff --git a/server/src/types.ts b/server/src/types.ts index 33174e187e..f9bde47aae 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -351,7 +351,6 @@ export type JobItem = | { name: JobName.FileDelete; data: IDeleteFilesJob } // Cleanup - | { name: JobName.AuditLogCleanup; data?: IBaseJob } | { name: JobName.SessionCleanup; data?: IBaseJob } // Tags diff --git a/server/src/types/plugin-schema.types.ts b/server/src/types/plugin-schema.types.ts index 793bb3c1ff..da1f6da935 100644 --- a/server/src/types/plugin-schema.types.ts +++ b/server/src/types/plugin-schema.types.ts @@ -3,33 +3,54 @@ * Based on JSON Schema Draft 7 */ -export type JSONSchemaType = 'string' | 'number' | 'integer' | 'boolean' | 'object' | 'array' | 'null'; +import z from 'zod'; -export interface JSONSchemaProperty { - type?: JSONSchemaType | JSONSchemaType[]; - description?: string; - default?: any; - enum?: any[]; - items?: JSONSchemaProperty; - properties?: Record; - required?: string[]; - additionalProperties?: boolean | JSONSchemaProperty; -} +const JSONSchemaTypeSchema = z + .enum(['string', 'number', 'integer', 'boolean', 'object', 'array', 'null']) + .meta({ id: 'PluginJsonSchemaType' }); -export interface JSONSchema { - type: 'object'; - properties?: Record; - required?: string[]; - additionalProperties?: boolean; - description?: string; -} +const JSONSchemaPropertySchema = z + .object({ + type: JSONSchemaTypeSchema.optional(), + description: z.string().optional(), + default: z.any().optional(), + enum: z.array(z.string()).optional(), -export type ConfigValue = string | number | boolean | null | ConfigValue[] | { [key: string]: ConfigValue }; + get items() { + return JSONSchemaPropertySchema.optional(); + }, -export interface FilterConfig { - [key: string]: ConfigValue; -} + get properties() { + return z.record(z.string(), JSONSchemaPropertySchema).optional(); + }, -export interface ActionConfig { - [key: string]: ConfigValue; -} + required: z.array(z.string()).optional(), + + get additionalProperties() { + return z.union([z.boolean(), JSONSchemaPropertySchema]).optional(); + }, + }) + .meta({ id: 'PluginJsonSchemaProperty' }); + +export type JSONSchemaProperty = z.infer; + +export const JSONSchemaSchema = z + .object({ + type: JSONSchemaTypeSchema.optional(), + properties: z.record(z.string(), JSONSchemaPropertySchema).optional(), + required: z.array(z.string()).optional(), + additionalProperties: z.boolean().optional(), + description: z.string().optional(), + }) + .meta({ id: 'PluginJsonSchema' }); +export type JSONSchema = z.infer; + +type ConfigValue = string | number | boolean | null | ConfigValue[] | { [key: string]: ConfigValue }; + +const ConfigValueSchema: z.ZodType = z.any().meta({ id: 'PluginConfigValue' }); + +export const FilterConfigSchema = z.record(z.string(), ConfigValueSchema).meta({ id: 'WorkflowFilterConfig' }); +export type FilterConfig = z.infer; + +export const ActionConfigSchema = z.record(z.string(), ConfigValueSchema).meta({ id: 'WorkflowActionConfig' }); +export type ActionConfig = z.infer; diff --git a/server/src/utils/access.ts b/server/src/utils/access.ts index 21e8bdd66e..b8e7733772 100644 --- a/server/src/utils/access.ts +++ b/server/src/utils/access.ts @@ -79,11 +79,6 @@ const checkSharedLinkAccess = async ( return sharedLink.allowUpload ? ids : new Set(); } - case Permission.AssetShare: { - // TODO: fix this to not use sharedLink.userId for access control - return await access.asset.checkOwnerAccess(sharedLink.userId, ids, false); - } - case Permission.AlbumRead: { return await access.album.checkSharedLinkAccess(sharedLinkId, ids); } diff --git a/server/src/utils/bbox.ts b/server/src/utils/bbox.ts deleted file mode 100644 index ad02e8355e..0000000000 --- a/server/src/utils/bbox.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { applyDecorators } from '@nestjs/common'; -import { ApiPropertyOptions } from '@nestjs/swagger'; -import { Transform, Type } from 'class-transformer'; -import { IsNotEmpty, ValidateNested } from 'class-validator'; -import { Property } from 'src/decorators'; -import { BBoxDto } from 'src/dtos/bbox.dto'; -import { Optional } from 'src/validation'; - -type BBoxOptions = { optional?: boolean }; -export const ValidateBBox = (options: BBoxOptions & ApiPropertyOptions = {}) => { - const { optional, ...apiPropertyOptions } = options; - - return applyDecorators( - Transform(({ value }) => { - if (typeof value !== 'string') { - return value; - } - - const [west, south, east, north] = value.split(',', 4).map(Number); - return Object.assign(new BBoxDto(), { west, south, east, north }); - }), - Type(() => BBoxDto), - ValidateNested(), - Property({ - type: 'string', - description: 'Bounding box coordinates as west,south,east,north (WGS84)', - example: '11.075683,49.416711,11.117589,49.454875', - ...apiPropertyOptions, - }), - optional ? Optional({}) : IsNotEmpty(), - ); -}; diff --git a/server/src/utils/config.ts b/server/src/utils/config.ts index a669af31cf..df7d05978c 100644 --- a/server/src/utils/config.ts +++ b/server/src/utils/config.ts @@ -1,10 +1,8 @@ import AsyncLock from 'async-lock'; -import { instanceToPlain, plainToInstance } from 'class-transformer'; -import { validate } from 'class-validator'; import { load as loadYaml } from 'js-yaml'; import * as _ from 'lodash'; import { SystemConfig, defaults } from 'src/config'; -import { SystemConfigDto } from 'src/dtos/system-config.dto'; +import { SystemConfigSchema } from 'src/dtos/system-config.dto'; import { DatabaseLock, SystemMetadataKey } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; @@ -101,19 +99,22 @@ const buildConfig = async (repos: RepoDeps) => { logger.warn(`Unknown keys found: ${JSON.stringify(unknownKeys, null, 2)}`); } - // validate full config - const instance = plainToInstance(SystemConfigDto, rawConfig); - const errors = await validate(instance); - if (errors.length > 0) { + // validate with Zod schema + const result = SystemConfigSchema.safeParse(rawConfig); + if (!result.success) { + const messages = ['Invalid system config: ']; + for (const issue of result.error.issues) { + const path = issue.path.join('.'); + messages.push(` - [${path}] ${issue.message}`); + } if (configFile) { - throw new Error(`Invalid value(s) in file: ${errors}`); + throw new Error(messages.join('\n')); } else { - logger.error('Validation error', errors); + logger.error('Validation error', messages); } } - // return config with class-transform changes - const config = instanceToPlain(instance) as SystemConfig; + const config = (result.success ? result.data : rawConfig) as SystemConfig; if (config.server.externalDomain.length > 0) { const domain = new URL(config.server.externalDomain); diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index d25d99b491..44a4b37ad7 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -351,8 +351,6 @@ export function searchAssetBuilder(kysely: Kysely, options: AssetSearchBuild .where('asset_exif.rating', options.rating === null ? 'is' : '=', options.rating!), ) .$if(!!options.checksum, (qb) => qb.where('asset.checksum', '=', options.checksum!)) - .$if(!!options.deviceAssetId, (qb) => qb.where('asset.deviceAssetId', '=', options.deviceAssetId!)) - .$if(!!options.deviceId, (qb) => qb.where('asset.deviceId', '=', options.deviceId!)) .$if(!!options.id, (qb) => qb.where('asset.id', '=', asUuid(options.id!))) .$if(!!options.libraryId, (qb) => qb.where('asset.libraryId', '=', asUuid(options.libraryId!))) .$if(!!options.userIds, (qb) => qb.where('asset.ownerId', '=', anyUuid(options.userIds!))) diff --git a/server/src/utils/date.ts b/server/src/utils/date.ts index 092a0e6619..d4de1eba86 100644 --- a/server/src/utils/date.ts +++ b/server/src/utils/date.ts @@ -1,9 +1,21 @@ import { DateTime } from 'luxon'; +/** + * Convert a date to a ISO 8601 datetime string. + * @param x - The date to convert. + * @returns The ISO 8601 datetime string. + * @deprecated Remove this and all references when using `ZodSerializerDto` on the controllers. Then the codec in `isoDatetimeToDate` in validation.ts will handle the conversion instead. + */ export const asDateString = (x: T) => { return x instanceof Date ? x.toISOString() : (x as Exclude); }; +/** + * Convert a date to a date string. + * @param x - The date to convert. + * @returns The date string. + * @deprecated Remove this and all references when using `ZodSerializerDto` on the controllers. Then the codec in `isoDateToDate` in validation.ts will handle the conversion instead. + */ export const asBirthDateString = (x: Date | string | null): string | null => { return x instanceof Date ? x.toISOString().split('T')[0] : x; }; diff --git a/server/src/utils/duplicate.spec.ts b/server/src/utils/duplicate.spec.ts index 4c5d5ddfc4..d63f0d3e32 100644 --- a/server/src/utils/duplicate.spec.ts +++ b/server/src/utils/duplicate.spec.ts @@ -1,12 +1,16 @@ import { AssetResponseDto } from 'src/dtos/asset-response.dto'; +import { ExifResponseSchema } from 'src/dtos/exif.dto'; import { AssetType, AssetVisibility } from 'src/enum'; import { getExifCount, suggestDuplicate, suggestDuplicateKeepAssetIds } from 'src/utils/duplicate'; import { describe, expect, it } from 'vitest'; +import type { z } from 'zod'; + +type ExifInfoInput = Partial>; const createAsset = ( id: string, fileSizeInByte: number | null = null, - exifFields: Record = {}, + exifFields: ExifInfoInput = {}, ): AssetResponseDto => ({ id, type: AssetType.Image, @@ -17,8 +21,6 @@ const createAsset = ( width: 1920, height: 1080, createdAt: new Date().toISOString(), - deviceAssetId: 'device-asset-1', - deviceId: 'device-1', ownerId: 'owner-1', originalPath: '/path/to/asset', originalFileName: 'asset.jpg', @@ -33,7 +35,9 @@ const createAsset = ( visibility: AssetVisibility.Timeline, checksum: 'checksum', exifInfo: - fileSizeInByte !== null || Object.keys(exifFields).length > 0 ? { fileSizeInByte, ...exifFields } : undefined, + fileSizeInByte !== null || Object.keys(exifFields).length > 0 + ? ExifResponseSchema.parse({ fileSizeInByte, ...exifFields }) + : undefined, }); describe('duplicate utils', () => { @@ -46,7 +50,7 @@ describe('duplicate utils', () => { it('should return 0 for empty exifInfo', () => { const asset = createAsset('asset-1'); - asset.exifInfo = {}; + asset.exifInfo = ExifResponseSchema.parse({}); expect(getExifCount(asset)).toBe(0); }); @@ -54,7 +58,7 @@ describe('duplicate utils', () => { const asset = createAsset('asset-1', 1000, { make: 'Canon', model: 'EOS 5D', - dateTimeOriginal: new Date(), + dateTimeOriginal: new Date().toISOString(), timeZone: 'UTC', latitude: 40.7128, longitude: -74.006, @@ -107,7 +111,7 @@ describe('duplicate utils', () => { const moreExif = createAsset('more-exif', 1000, { make: 'Canon', model: 'EOS 5D', - dateTimeOriginal: new Date(), + dateTimeOriginal: new Date().toISOString(), city: 'New York', }); @@ -125,7 +129,7 @@ describe('duplicate utils', () => { it('should handle assets with exifInfo but no fileSizeInByte', () => { const noFileSize = createAsset('no-file-size'); - noFileSize.exifInfo = { make: 'Canon', model: 'EOS 5D' }; + noFileSize.exifInfo = ExifResponseSchema.parse({ make: 'Canon', model: 'EOS 5D' }); const withFileSize = createAsset('with-file-size', 1000); expect(suggestDuplicate([noFileSize, withFileSize])?.id).toBe('with-file-size'); @@ -148,7 +152,7 @@ describe('duplicate utils', () => { const smallWithMoreExif = createAsset('small-more-exif', 1000, { make: 'Canon', model: 'EOS 5D', - dateTimeOriginal: new Date(), + dateTimeOriginal: new Date().toISOString(), city: 'New York', state: 'NY', country: 'USA', diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts index 7d2e99a215..450563cf7e 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -12,6 +12,7 @@ import { SchemaObject, } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; import _ from 'lodash'; +import { cleanupOpenApiDoc } from 'nestjs-zod'; import { writeFileSync } from 'node:fs'; import path from 'node:path'; import picomatch from 'picomatch'; @@ -158,11 +159,38 @@ const isSchema = (schema: string | ReferenceObject | SchemaObject): schema is Sc }; const patchOpenAPI = (document: OpenAPIObject) => { + const removeOpenApi30IncompatibleKeys = (target: unknown) => { + if (!target || typeof target !== 'object') { + return; + } + + if (Array.isArray(target)) { + for (const item of target) { + removeOpenApi30IncompatibleKeys(item); + } + return; + } + + const object = target as Record; + delete object.propertyNames; + delete object.contentEncoding; + + for (const value of Object.values(object)) { + removeOpenApi30IncompatibleKeys(value); + } + }; + document.paths = sortKeys(document.paths); + // Allowed in OpenAPI v3.1 (JSON Schema 2020-12), but not in OpenAPI v3.0 (current spec). + removeOpenApi30IncompatibleKeys(document); if (document.components?.schemas) { const schemas = document.components.schemas as Record; + for (const schema of Object.values(schemas)) { + delete (schema as Record).id; + } + document.components.schemas = sortKeys(schemas); for (const [schemaName, schema] of Object.entries(schemas)) { @@ -265,6 +293,7 @@ export const useSwagger = (app: INestApplication, { write }: { write: boolean }) }; const specification = SwaggerModule.createDocument(app, config, options); + const openApiDoc = cleanupOpenApiDoc(specification); const customOptions: SwaggerCustomOptions = { swaggerOptions: { @@ -275,12 +304,12 @@ export const useSwagger = (app: INestApplication, { write }: { write: boolean }) customSiteTitle: 'Immich API Documentation', }; - SwaggerModule.setup('doc', app, specification, customOptions); + SwaggerModule.setup('doc', app, openApiDoc, customOptions); if (write) { // Generate API Documentation only in development mode const outputPath = path.resolve(process.cwd(), '../open-api/immich-openapi-specs.json'); - writeFileSync(outputPath, JSON.stringify(patchOpenAPI(specification), null, 2), { encoding: 'utf8' }); + writeFileSync(outputPath, JSON.stringify(patchOpenAPI(openApiDoc), null, 2), { encoding: 'utf8' }); } }; diff --git a/server/src/validation.spec.ts b/server/src/validation.spec.ts index 631ba60a60..434ac89cee 100644 --- a/server/src/validation.spec.ts +++ b/server/src/validation.spec.ts @@ -1,92 +1,45 @@ -import { plainToInstance } from 'class-transformer'; -import { validate } from 'class-validator'; -import { DateTime } from 'luxon'; -import { IsDateStringFormat, IsNotSiblingOf, MaxDateString, Optional } from 'src/validation'; -import { describe } from 'vitest'; +import { IsNotSiblingOf } from 'src/validation'; +import { describe, expect, it } from 'vitest'; +import z from 'zod'; describe('Validation', () => { - describe('MaxDateString', () => { - const maxDate = DateTime.fromISO('2000-01-01', { zone: 'utc' }); - - class MyDto { - @MaxDateString(maxDate) - date!: string; - } - - it('passes when date is before maxDate', async () => { - const dto = plainToInstance(MyDto, { date: '1999-12-31' }); - await expect(validate(dto)).resolves.toHaveLength(0); - }); - - it('passes when date is equal to maxDate', async () => { - const dto = plainToInstance(MyDto, { date: '2000-01-01' }); - await expect(validate(dto)).resolves.toHaveLength(0); - }); - - it('fails when date is after maxDate', async () => { - const dto = plainToInstance(MyDto, { date: '2010-01-01' }); - await expect(validate(dto)).resolves.toHaveLength(1); - }); - }); - - describe('IsDateStringFormat', () => { - class MyDto { - @IsDateStringFormat('yyyy-MM-dd') - date!: string; - } - - it('passes when date is valid', async () => { - const dto = plainToInstance(MyDto, { date: '1999-12-31' }); - await expect(validate(dto)).resolves.toHaveLength(0); - }); - - it('fails when date has invalid format', async () => { - const dto = plainToInstance(MyDto, { date: '2000-01-01T00:00:00Z' }); - await expect(validate(dto)).resolves.toHaveLength(1); - }); - - it('fails when empty string', async () => { - const dto = plainToInstance(MyDto, { date: '' }); - await expect(validate(dto)).resolves.toHaveLength(1); - }); - - it('fails when undefined', async () => { - const dto = plainToInstance(MyDto, {}); - await expect(validate(dto)).resolves.toHaveLength(1); - }); - }); - describe('IsNotSiblingOf', () => { - class MyDto { - @IsNotSiblingOf(['attribute2']) - @Optional() - attribute1?: string; - - @IsNotSiblingOf(['attribute1', 'attribute3']) - @Optional() - attribute2?: string; - - @IsNotSiblingOf(['attribute2']) - @Optional() - attribute3?: string; - - @Optional() - unrelatedAttribute?: string; - } - - it('passes when only one attribute is present', async () => { - const dto = plainToInstance(MyDto, { attribute1: 'value1', unrelatedAttribute: 'value2' }); - await expect(validate(dto)).resolves.toHaveLength(0); + const MySchemaBase = z.object({ + attribute1: z.string().optional(), + attribute2: z.string().optional(), + attribute3: z.string().optional(), + unrelatedAttribute: z.string().optional(), }); - it('fails when colliding attributes are present', async () => { - const dto = plainToInstance(MyDto, { attribute1: 'value1', attribute2: 'value2' }); - await expect(validate(dto)).resolves.toHaveLength(2); + const MySchema = MySchemaBase.pipe(IsNotSiblingOf(MySchemaBase, 'attribute1', ['attribute2'])) + .pipe(IsNotSiblingOf(MySchemaBase, 'attribute2', ['attribute1', 'attribute3'])) + .pipe(IsNotSiblingOf(MySchemaBase, 'attribute3', ['attribute2'])); + + it('passes when only one attribute is present', () => { + const result = MySchema.safeParse({ + attribute1: 'value1', + unrelatedAttribute: 'value2', + }); + expect(result.success).toBe(true); }); - it('passes when no colliding attributes are present', async () => { - const dto = plainToInstance(MyDto, { attribute1: 'value1', attribute3: 'value2' }); - await expect(validate(dto)).resolves.toHaveLength(0); + it('fails when colliding attributes are present', () => { + const result = MySchema.safeParse({ + attribute1: 'value1', + attribute2: 'value2', + }); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toBe('attribute1 cannot exist alongside attribute2'); + } + }); + + it('passes when no colliding attributes are present', () => { + const result = MySchema.safeParse({ + attribute1: 'value1', + attribute3: 'value2', + }); + expect(result.success).toBe(true); }); }); }); diff --git a/server/src/validation.ts b/server/src/validation.ts index b959de94b1..54e3b1820e 100644 --- a/server/src/validation.ts +++ b/server/src/validation.ts @@ -1,40 +1,62 @@ -import { - ArgumentMetadata, - BadRequestException, - FileValidator, - Injectable, - ParseUUIDPipe, - applyDecorators, -} from '@nestjs/common'; -import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; -import { - IsArray, - IsBoolean, - IsDate, - IsEnum, - IsHexColor, - IsNotEmpty, - IsOptional, - IsString, - IsUUID, - Matches, - Validate, - ValidateBy, - ValidateIf, - ValidationArguments, - ValidationOptions, - ValidatorConstraint, - ValidatorConstraintInterface, - buildMessage, - isDateString, - isDefined, -} from 'class-validator'; -import { CronJob } from 'cron'; -import { DateTime } from 'luxon'; +import { ArgumentMetadata, FileValidator, Injectable, ParseUUIDPipe } from '@nestjs/common'; +import { createZodDto } from 'nestjs-zod'; import sanitize from 'sanitize-filename'; -import { Property, PropertyOptions } from 'src/decorators'; import { isIP, isIPRange } from 'validator'; +import z from 'zod'; + +export type IsIPRangeOptions = { requireCIDR?: boolean }; + +function isIPOrRange(value: string, options?: IsIPRangeOptions): boolean { + const { requireCIDR = true } = options ?? {}; + if (isIPRange(value)) { + return true; + } + if (!requireCIDR && isIP(value)) { + return true; + } + return false; +} + +/** + * Zod schema that validates an array of strings as IP addresses or IP/CIDR ranges. + * When requireCIDR is true (default), plain IPs are rejected; only CIDR ranges are allowed. + * + * @example + * z.string().optional().transform(...).pipe(IsIPRange()) + * @example + * z.string().optional().transform(...).pipe(IsIPRange({ requireCIDR: false })) + */ +export function IsIPRange(options?: IsIPRangeOptions) { + return z + .array(z.string()) + .refine((arr) => arr.every((item) => isIPOrRange(item, options)), 'Must be an ip address or ip address range'); +} + +/** + * Zod schema that validates sibling-exclusion for object schemas. + * Validation passes when the target property is missing, or when none of the sibling properties are present. + * Use with .pipe() like IsIPRange. + * + * @example + * const Schema = z.object({ a: z.string().optional(), b: z.string().optional() }); + * Schema.pipe(IsNotSiblingOf(Schema, 'a', ['b'])); + */ +export function IsNotSiblingOf< + TSchema extends z.ZodObject, + TKey extends z.infer> & keyof z.infer, +>(_schema: TSchema, property: TKey, siblings: TKey[]) { + type T = z.infer; + const message = `${String(property)} cannot exist alongside ${siblings.join(' or ')}`; + return z.custom().refine( + (data) => { + if (data[property] === undefined) { + return true; + } + return siblings.every((sibling) => data[sibling] === undefined); + }, + { message }, + ); +} @Injectable() export class ParseMeUUIDPipe extends ParseUUIDPipe { @@ -66,386 +88,163 @@ export class FileNotEmptyValidator extends FileValidator { } } -type UUIDOptions = { optional?: boolean; each?: boolean; nullable?: boolean }; -export const ValidateUUID = (options?: UUIDOptions & PropertyOptions) => { - const { optional, each, nullable, ...apiPropertyOptions } = { - optional: false, - each: false, - nullable: false, - ...options, - }; - return applyDecorators( - IsUUID('4', { each }), - Property({ format: 'uuid', ...apiPropertyOptions }), - optional ? Optional({ nullable }) : IsNotEmpty(), - each ? IsArray() : IsString(), - ); -}; +const UUIDParamSchema = z.object({ + id: z.uuidv4(), +}); -export function IsAxisAlignedRotation() { - return ValidateBy( - { - name: 'isAxisAlignedRotation', - validator: { - validate(value: any) { - return [0, 90, 180, 270].includes(value); - }, - defaultMessage: buildMessage( - (eachPrefix) => eachPrefix + '$property must be one of the following values: 0, 90, 180, 270', - {}, - ), - }, - }, - {}, - ); -} +export class UUIDParamDto extends createZodDto(UUIDParamSchema) {} -@ValidatorConstraint({ name: 'uniqueEditActions' }) -class UniqueEditActionsValidator implements ValidatorConstraintInterface { - validate(edits: { action: string; parameters?: unknown }[]): boolean { - if (!Array.isArray(edits)) { - return true; - } +const UUIDAssetIDParamSchema = z.object({ + id: z.uuidv4(), + assetId: z.uuidv4(), +}); - const actionSet = new Set(); - for (const edit of edits) { - const key = edit.action === 'mirror' ? `${edit.action}-${JSON.stringify(edit.parameters)}` : edit.action; - if (actionSet.has(key)) { - return false; - } - actionSet.add(key); - } - return true; - } +export class UUIDAssetIDParamDto extends createZodDto(UUIDAssetIDParamSchema) {} - defaultMessage(): string { - return 'Duplicate edit actions are not allowed'; - } -} +const FilenameParamSchema = z.object({ + filename: z.string().regex(/^[a-zA-Z0-9_\-.]+$/, { + error: 'Filename contains invalid characters', + }), +}); -export const IsUniqueEditActions = () => Validate(UniqueEditActionsValidator); - -export class UUIDParamDto { - @IsNotEmpty() - @IsUUID('4') - @ApiProperty({ format: 'uuid' }) - id!: string; -} - -export class UUIDAssetIDParamDto { - @ValidateUUID() - id!: string; - - @ValidateUUID() - assetId!: string; -} - -export class FilenameParamDto { - @IsNotEmpty() - @IsString() - @ApiProperty({ format: 'string' }) - @Matches(/^[a-zA-Z0-9_\-.]+$/, { - message: 'Filename contains invalid characters', - }) - filename!: string; -} - -type PinCodeOptions = { optional?: boolean } & OptionalOptions; -export const PinCode = (options?: PinCodeOptions & ApiPropertyOptions) => { - const { optional, nullable, emptyToNull, ...apiPropertyOptions } = { - optional: false, - nullable: false, - emptyToNull: false, - ...options, - }; - const decorators = [ - IsString(), - IsNotEmpty(), - Matches(/^\d{6}$/, { message: ({ property }) => `${property} must be a 6-digit numeric string` }), - ApiProperty({ example: '123456', ...apiPropertyOptions }), - ]; - - if (optional) { - decorators.push(Optional({ nullable, emptyToNull })); - } - - return applyDecorators(...decorators); -}; - -export interface OptionalOptions { - nullable?: boolean; - /** convert empty strings to null */ - emptyToNull?: boolean; -} - -/** - * Checks if value is missing and if so, ignores all validators. - * - * @param validationOptions {@link OptionalOptions} - * - * @see IsOptional exported from `class-validator. - */ -// https://stackoverflow.com/a/71353929 -export function Optional({ nullable, emptyToNull, ...validationOptions }: OptionalOptions = {}) { - const decorators: PropertyDecorator[] = []; - - if (nullable === true) { - decorators.push(IsOptional(validationOptions)); - } else { - decorators.push(ValidateIf((object: any, v: any) => v !== undefined, validationOptions)); - } - - if (emptyToNull) { - decorators.push(Transform(({ value }) => (value === '' ? null : value))); - } - - return applyDecorators(...decorators); -} - -export function IsNotSiblingOf(siblings: string[], validationOptions?: ValidationOptions) { - return ValidateBy( - { - name: 'isNotSiblingOf', - constraints: siblings, - validator: { - validate(value: any, args: ValidationArguments) { - if (!isDefined(value)) { - return true; - } - return args.constraints.filter((prop) => isDefined((args.object as any)[prop])).length === 0; - }, - defaultMessage: (args: ValidationArguments) => { - return `${args.property} cannot exist alongside any of the following properties: ${args.constraints.join(', ')}`; - }, - }, - }, - validationOptions, - ); -} - -export const ValidateHexColor = () => { - const decorators = [ - IsHexColor(), - Transform(({ value }) => (typeof value === 'string' && value[0] !== '#' ? `#${value}` : value)), - ]; - - return applyDecorators(...decorators); -}; - -type DateOptions = OptionalOptions & { optional?: boolean; format?: 'date' | 'date-time' }; -export const ValidateDate = (options?: DateOptions & PropertyOptions) => { - const { - optional, - nullable = false, - emptyToNull = false, - format = 'date-time', - ...apiPropertyOptions - } = options || {}; - - return applyDecorators( - Property({ format, ...apiPropertyOptions }), - IsDate(), - optional ? Optional({ nullable, emptyToNull }) : IsNotEmpty(), - Transform(({ key, value }) => { - if (value === null || value === undefined) { - return value; - } - - if (!isDateString(value)) { - throw new BadRequestException(`${key} must be a date string`); - } - - return new Date(value as string); - }), - ); -}; - -type StringOptions = OptionalOptions & { optional?: boolean; trim?: boolean }; -export const ValidateString = (options?: StringOptions & ApiPropertyOptions) => { - const { optional, nullable, emptyToNull, trim, ...apiPropertyOptions } = options || {}; - const decorators = [ - ApiProperty(apiPropertyOptions), - IsString(), - optional ? Optional({ nullable, emptyToNull }) : IsNotEmpty(), - ]; - - if (trim) { - decorators.push(Transform(({ value }: { value: string }) => value?.trim())); - } - - return applyDecorators(...decorators); -}; - -type BooleanOptions = OptionalOptions & { optional?: boolean }; -export const ValidateBoolean = (options?: BooleanOptions & PropertyOptions) => { - const { optional, nullable, emptyToNull, ...apiPropertyOptions } = options || {}; - const decorators = [ - Property(apiPropertyOptions), - IsBoolean(), - Transform(({ value }) => { - if (value == 'true') { - return true; - } else if (value == 'false') { - return false; - } - return value; - }), - optional ? Optional({ nullable, emptyToNull }) : IsNotEmpty(), - ]; - - return applyDecorators(...decorators); -}; - -type EnumOptions = { - enum: T; - name: string; - each?: boolean; - optional?: boolean; - nullable?: boolean; - default?: T[keyof T]; - description?: string; -}; -export const ValidateEnum = ({ - name, - enum: value, - each, - optional, - nullable, - default: defaultValue, - description, -}: EnumOptions) => { - return applyDecorators( - optional ? Optional({ nullable }) : IsNotEmpty(), - IsEnum(value, { each }), - ApiProperty({ enumName: name, enum: value, isArray: each, default: defaultValue, description }), - ); -}; - -@ValidatorConstraint({ name: 'cronValidator' }) -class CronValidator implements ValidatorConstraintInterface { - validate(expression: string): boolean { - try { - new CronJob(expression, () => {}); - return true; - } catch { - return false; - } - } -} - -export const IsCronExpression = () => Validate(CronValidator, { message: 'Invalid cron expression' }); - -type IValue = { value: unknown }; - -export const toEmail = ({ value }: IValue) => (typeof value === 'string' ? value.toLowerCase() : value); - -export const toSanitized = ({ value }: IValue) => { - const input = typeof value === 'string' ? value : ''; - return sanitize(input.replaceAll('.', '')); -}; +export class FilenameParamDto extends createZodDto(FilenameParamSchema) {} export const isValidInteger = (value: number, options: { min?: number; max?: number }): value is number => { const { min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER } = options; return Number.isInteger(value) && value >= min && value <= max; }; -export function isDateStringFormat(value: unknown, format: string) { - if (typeof value !== 'string') { - return false; - } - return DateTime.fromFormat(value, format, { zone: 'utc' }).isValid; -} +/** + * Unified email validation + * Converts email strings to lowercase and validates against HTML5 email regex + * @docs https://zod.dev/api?id=email + */ +export const toEmail = z + .email({ + pattern: z.regexes.html5Email, + error: (iss) => `Invalid input: expected email, received ${typeof iss.input}`, + }) + .transform((val) => val.toLowerCase()); -export function IsDateStringFormat(format: string, validationOptions?: ValidationOptions) { - return ValidateBy( +/** + * Parse ISO 8601 datetime strings to Date objects + * @docs https://zod.dev/api?id=codec + */ +export const isoDatetimeToDate = z + .codec( + z.iso.datetime({ + error: (iss) => `Invalid input: expected ISO 8601 datetime string, received ${typeof iss.input}`, + }), + z.date(), { - name: 'isDateStringFormat', - constraints: [format], - validator: { - validate(value: unknown) { - return isDateStringFormat(value, format); - }, - defaultMessage: () => `$property must be a string in the format ${format}`, - }, + decode: (isoString) => new Date(isoString), + encode: (date) => date.toISOString(), }, - validationOptions, - ); -} + ) + .meta({ example: '2024-01-01T00:00:00.000Z' }); -function maxDate(date: DateTime, maxDate: DateTime | (() => DateTime)) { - return date <= (maxDate instanceof DateTime ? maxDate : maxDate()); -} - -export function MaxDateString( - date: DateTime | (() => DateTime), - validationOptions?: ValidationOptions, -): PropertyDecorator { - return ValidateBy( +/** + * Parse ISO date strings to Date objects + * @docs https://zod.dev/api?id=codec + */ +export const isoDateToDate = z + .codec( + z.iso.date({ + error: (iss) => `Invalid input: expected ISO date string (YYYY-MM-DD), received ${typeof iss.input}`, + }), + z.date(), { - name: 'maxDateString', - constraints: [date], - validator: { - validate: (value, args) => { - const date = DateTime.fromISO(value, { zone: 'utc' }); - return maxDate(date, args?.constraints[0]); - }, - defaultMessage: buildMessage( - (eachPrefix) => 'maximal allowed date for ' + eachPrefix + '$property is $constraint1', - validationOptions, - ), - }, + decode: (isoString) => new Date(isoString), + encode: (date) => date.toISOString().slice(0, 10), }, - validationOptions, - ); -} + ) + .meta({ example: '2024-01-01' }); -type IsIPRangeOptions = { requireCIDR?: boolean }; -export function IsIPRange(options: IsIPRangeOptions, validationOptions?: ValidationOptions): PropertyDecorator { - const { requireCIDR } = { requireCIDR: true, ...options }; +export const isValidTime = z + .string() + .regex(/^([01]\d|2[0-3]):[0-5]\d$/, 'Invalid input: expected string in HH:mm format, received string'); - return ValidateBy( - { - name: 'isIPRange', - validator: { - validate: (value): boolean => { - if (isIPRange(value)) { - return true; - } +/** + * Latitude in range [-90, 90]. Reuse for body or query params. + * + * @example + * // Regular (body): optional coordinates + * latitudeSchema.optional().describe('Latitude coordinate') + * + * @example + * // Pipe (query): coerce string to number then validate range + * z.coerce.number().pipe(latitudeSchema).describe('Latitude (-90 to 90)') + */ +export const latitudeSchema = z.number().min(-90).max(90); - if (!requireCIDR && isIP(value)) { - return true; - } +/** + * Longitude in range [-180, 180]. Reuse for body or query params. + * + * @example + * // Regular (body): optional coordinates + * longitudeSchema.optional().describe('Longitude coordinate') + * + * @example + * // Pipe (query): coerce string to number then validate range + * z.coerce.number().pipe(longitudeSchema).describe('Longitude (-180 to 180)') + */ +export const longitudeSchema = z.number().min(-180).max(180); - return false; - }, - defaultMessage: buildMessage( - (eachPrefix) => eachPrefix + '$property must be an ip address, or ip address range', - validationOptions, - ), - }, - }, - validationOptions, - ); -} +/** + * Parse string to boolean + * This should be used for boolean query parameters and path parameters, but not for boolean request body parameters, as the first are always string. + * We don't use z.coerce.boolean() as any truthy value is considered true + * z.stringbool() is a more robust way to parse strings to booleans as it lets you specify the truthy and falsy values and the case sensitivity. + * @docs https://zod.dev/api?id=coercion + * @docs https://zod.dev/api?id=stringbool + */ +export const stringToBool = z + .stringbool({ truthy: ['true'], falsy: ['false'], case: 'sensitive' }) + .meta({ type: 'boolean' }); -@ValidatorConstraint({ name: 'isGreaterThanOrEqualTo' }) -export class IsGreaterThanOrEqualToConstraint implements ValidatorConstraintInterface { - validate(value: unknown, args: ValidationArguments) { - const relatedPropertyName = args.constraints?.[0] as string; - const relatedValue = (args.object as Record)[relatedPropertyName]; - if (!Number.isFinite(value) || !Number.isFinite(relatedValue)) { - return true; +/** + * Parse JSON strings from multipart/form-data + */ +export const JsonParsed = z.transform((val, ctx) => { + if (typeof val === 'string') { + try { + return JSON.parse(val); + } catch { + ctx.issues.push({ + code: 'custom', + message: `Invalid input: expected JSON string, received ${typeof val}`, + input: val, + }); + return z.NEVER; } - - return Number(value) >= Number(relatedValue); } + return val; +}); - defaultMessage(args: ValidationArguments) { - const relatedPropertyName = args.constraints?.[0] as string; - return `${args.property} must be greater than or equal to ${relatedPropertyName}`; - } -} +/** + * Hex color validation and normalization. + * Accepts formats: #RGB, #RGBA, #RRGGBB, #RRGGBBAA (with or without # prefix). + * Normalizes output to always include the # prefix. + * + * @example + * hexColor.optional() + */ +const hexColorRegex = /^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/; +export const hexColor = z + .string() + .regex(hexColorRegex) + .transform((val) => (val.startsWith('#') ? val : `#${val}`)); -export const IsGreaterThanOrEqualTo = (property: string, validationOptions?: ValidationOptions) => { - return Validate(IsGreaterThanOrEqualToConstraint, [property], validationOptions); -}; +/** + * Transform empty strings to null. Inner schema passed to this function must accept null. + * @docs https://zod.dev/api?id=preprocess + * @example emptyStringToNull(z.string().nullable()).optional() // [encouraged] final schema is optional + * @example emptyStringToNull(z.string().nullable()) // [encouraged] same as the one above, but final schema is not optional + * @example emptyStringToNull(z.string().nullish()) // [discouraged] same as the one above, might be confusing + * @example emptyStringToNull(z.string().optional()) // fails: string schema rejects null + * @example emptyStringToNull(z.string().nullable()).nullish() // [discouraged] passes, null is duplicated. use the first example instead + */ +export const emptyStringToNull = (schema: T) => + z.preprocess((val) => (val === '' ? null : val), schema); + +export const sanitizeFilename = z.string().transform((val) => sanitize(val.replaceAll('.', ''))); diff --git a/server/test/factories/asset.factory.ts b/server/test/factories/asset.factory.ts index 9caec31d6f..0a992719e3 100644 --- a/server/test/factories/asset.factory.ts +++ b/server/test/factories/asset.factory.ts @@ -54,8 +54,6 @@ export class AssetFactory { status: AssetStatus.Active, checksum: newSha1(), checksumAlgorithm: ChecksumAlgorithm.sha1File, - deviceAssetId: '', - deviceId: '', duplicateId: null, duration: null, fileCreatedAt: new Date(now++), diff --git a/server/test/mappers.ts b/server/test/mappers.ts index ed2c9431f3..edd85a386d 100644 --- a/server/test/mappers.ts +++ b/server/test/mappers.ts @@ -127,8 +127,6 @@ export const getForMetadataExtraction = (asset: ReturnType> = {}) => { const id = asset.id || newUuid(); const now = newDate(); const defaults: Insertable = { - deviceAssetId: '', - deviceId: '', originalFileName: '', checksum: randomBytes(32), checksumAlgorithm: ChecksumAlgorithm.sha1File, diff --git a/server/test/medium/specs/services/asset-media.service.spec.ts b/server/test/medium/specs/services/asset-media.service.spec.ts index f10844ca4a..1cfe8e9f84 100644 --- a/server/test/medium/specs/services/asset-media.service.spec.ts +++ b/server/test/medium/specs/services/asset-media.service.spec.ts @@ -52,8 +52,6 @@ describe(AssetService.name, () => { sut.uploadAsset( auth, { - deviceId: 'some-id', - deviceAssetId: 'some-id', fileModifiedAt: new Date(), fileCreatedAt: new Date(), assetData: Buffer.from('some data'), @@ -66,7 +64,7 @@ describe(AssetService.name, () => { }); expect(ctx.getMock(EventRepository).emit).toHaveBeenCalledWith('AssetCreate', { - asset: expect.objectContaining({ deviceAssetId: 'some-id' }), + asset: expect.objectContaining({}), }); }); @@ -87,8 +85,6 @@ describe(AssetService.name, () => { sut.uploadAsset( auth, { - deviceId: 'some-id', - deviceAssetId: 'some-id', fileModifiedAt: new Date(), fileCreatedAt: new Date(), assetData: Buffer.from('some data'), @@ -125,8 +121,6 @@ describe(AssetService.name, () => { const auth = factory.auth({ user: { id: user.id }, sharedLink }); const file = mediumFactory.uploadFile(); const uploadDto = { - deviceId: 'some-id', - deviceAssetId: 'some-id', fileModifiedAt: new Date(), fileCreatedAt: new Date(), assetData: Buffer.from('some data'), @@ -166,8 +160,6 @@ describe(AssetService.name, () => { const auth = factory.auth({ user: { id: user.id }, sharedLink }); const uploadDto = { - deviceId: 'some-id', - deviceAssetId: 'some-id', fileModifiedAt: new Date(), fileCreatedAt: new Date(), assetData: Buffer.from('some data'), @@ -206,8 +198,6 @@ describe(AssetService.name, () => { const auth = factory.auth({ user: { id: user.id }, sharedLink }); const uploadDto = { - deviceId: 'some-id', - deviceAssetId: 'some-id', fileModifiedAt: new Date(), fileCreatedAt: new Date(), assetData: Buffer.from('some data'), @@ -248,8 +238,6 @@ describe(AssetService.name, () => { const auth = factory.auth({ user: { id: user.id }, sharedLink }); const uploadDto = { - deviceId: 'some-id', - deviceAssetId: 'some-id', fileModifiedAt: new Date(), fileCreatedAt: new Date(), assetData: Buffer.from('some data'), diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index 68667fa109..e3a1dbdf05 100644 --- a/server/test/repositories/asset.repository.mock.ts +++ b/server/test/repositories/asset.repository.mock.ts @@ -15,13 +15,10 @@ export const newAssetRepositoryMock = (): Mocked Promise; }; -export const controllerSetup = async (controller: ClassConstructor, providers: Provider[]) => { +export const controllerSetup = async (controller: new (...args: any[]) => unknown, providers: Provider[]) => { const noopInterceptor = { intercept: (ctx: never, next: CallHandler) => next.handle() }; const upload = multer({ storage: multer.memoryStorage() }); const memoryFileInterceptor = { @@ -113,9 +114,12 @@ export const controllerSetup = async (controller: ClassConstructor, pro const moduleRef = await Test.createTestingModule({ controllers: [controller], providers: [ - { provide: APP_PIPE, useValue: new ValidationPipe({ transform: true, whitelist: true }) }, + { provide: APP_FILTER, useClass: GlobalExceptionFilter }, + { provide: APP_PIPE, useClass: ZodValidationPipe }, + { provide: APP_INTERCEPTOR, useClass: ZodSerializerInterceptor }, { provide: APP_GUARD, useClass: AuthGuard }, { provide: LoggingRepository, useValue: LoggingRepository.create() }, + { provide: ClsService, useValue: { getId: vi.fn() } }, { provide: AuthService, useValue: { authenticate: vi.fn() } }, ...providers, ], @@ -158,14 +162,14 @@ const mockFn = (label: string, { strict }: { strict: boolean }) => { }); }; -export const mockBaseService = (service: ClassConstructor) => { +export const mockBaseService = (service: new (...args: any[]) => T) => { return automock(service, { args: [{ setContext: () => {} }], strict: false }); }; export const automock = ( - Dependency: ClassConstructor, + Dependency: new (...args: any[]) => T, options?: { - args?: ConstructorParameters>; + args?: ConstructorParameters T>; strict?: boolean; }, ): AutoMocked => { @@ -214,7 +218,6 @@ export type ServiceOverrides = { albumUser: AlbumUserRepository; apiKey: ApiKeyRepository; app: AppRepository; - audit: AuditRepository; asset: AssetRepository; assetEdit: AssetEditRepository; assetJob: AssetJobRepository; @@ -294,7 +297,6 @@ export const getMocks = () => { cron: automock(CronRepository, { args: [, loggerMock] }), crypto: newCryptoRepositoryMock(), activity: automock(ActivityRepository), - audit: automock(AuditRepository), album: automock(AlbumRepository, { strict: false }), albumUser: automock(AlbumUserRepository), asset: newAssetRepositoryMock(), @@ -368,7 +370,6 @@ export const newTestService = ( overrides.asset || (mocks.asset as As), overrides.assetEdit || (mocks.assetEdit as As), overrides.assetJob || (mocks.assetJob as As), - overrides.audit || (mocks.audit as As), overrides.config || (mocks.config as As as ConfigRepository), overrides.cron || (mocks.cron as As), overrides.crypto || (mocks.crypto as As), diff --git a/web/package.json b/web/package.json index 1cfb7e8216..1d82e19c59 100644 --- a/web/package.json +++ b/web/package.json @@ -27,7 +27,7 @@ "@formatjs/icu-messageformat-parser": "^3.0.0", "@immich/justified-layout-wasm": "^0.4.3", "@immich/sdk": "workspace:*", - "@immich/ui": "^0.69.0", + "@immich/ui": "^0.76.0", "@mapbox/mapbox-gl-rtl-text": "0.3.0", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.14.0", diff --git a/web/src/app.html b/web/src/app.html index 776764850f..a998769704 100644 --- a/web/src/app.html +++ b/web/src/app.html @@ -1,5 +1,5 @@ - + @@ -15,7 +15,22 @@ + + + %sveltekit.head% + - - -