mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
merge: remote-tracking branch 'origin/main' into feat/integrity-checks-izzy
This commit is contained in:
+1
-1
@@ -20,7 +20,7 @@
|
|||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/micromatch": "^4.0.9",
|
"@types/micromatch": "^4.0.9",
|
||||||
"@types/mock-fs": "^4.13.1",
|
"@types/mock-fs": "^4.13.1",
|
||||||
"@types/node": "^24.10.4",
|
"@types/node": "^24.10.8",
|
||||||
"@vitest/coverage-v8": "^3.0.0",
|
"@vitest/coverage-v8": "^3.0.0",
|
||||||
"byte-size": "^9.0.0",
|
"byte-size": "^9.0.0",
|
||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
|
|||||||
@@ -17,12 +17,17 @@ Hardware and software requirements for Immich:
|
|||||||
- Immich runs well in a virtualized environment when running in a full virtual machine.
|
- Immich runs well in a virtualized environment when running in a full virtual machine.
|
||||||
The use of Docker in LXC containers is [not recommended](https://pve.proxmox.com/wiki/Linux_Container), but may be possible for advanced users.
|
The use of Docker in LXC containers is [not recommended](https://pve.proxmox.com/wiki/Linux_Container), but may be possible for advanced users.
|
||||||
If you have issues, we recommend that you switch to a supported VM deployment.
|
If you have issues, we recommend that you switch to a supported VM deployment.
|
||||||
- **RAM**: Minimum 4GB, recommended 6GB.
|
- **RAM**: Minimum 6GB, recommended 8GB.
|
||||||
- **CPU**: Minimum 2 cores, recommended 4 cores.
|
- **CPU**: Minimum 2 cores, recommended 4 cores.
|
||||||
- **Storage**: Recommended Unix-compatible filesystem (EXT4, ZFS, APFS, etc.) with support for user/group ownership and permissions.
|
- **Storage**: Recommended Unix-compatible filesystem (EXT4, ZFS, APFS, etc.) with support for user/group ownership and permissions.
|
||||||
- The generation of thumbnails and transcoded video can increase the size of the photo library by 10-20% on average.
|
- The generation of thumbnails and transcoded video can increase the size of the photo library by 10-20% on average.
|
||||||
|
|
||||||
:::tip
|
:::note RAM requirements
|
||||||
|
For a smooth experience, especially during asset upload, Immich requires at least 6GB of RAM.
|
||||||
|
For systems with only 4GB of RAM, Immich can be run with machine learning features disabled.
|
||||||
|
:::
|
||||||
|
|
||||||
|
:::tip Postgres setup
|
||||||
Good performance and a stable connection to the Postgres database is critical to a smooth Immich experience.
|
Good performance and a stable connection to the Postgres database is critical to a smooth Immich experience.
|
||||||
The Postgres database files are typically between 1-3 GB in size.
|
The Postgres database files are typically between 1-3 GB in size.
|
||||||
For this reason, the Postgres database (`DB_DATA_LOCATION`) should ideally use local SSD storage, and never a network share of any kind.
|
For this reason, the Postgres database (`DB_DATA_LOCATION`) should ideally use local SSD storage, and never a network share of any kind.
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@
|
|||||||
"@playwright/test": "^1.44.1",
|
"@playwright/test": "^1.44.1",
|
||||||
"@socket.io/component-emitter": "^3.1.2",
|
"@socket.io/component-emitter": "^3.1.2",
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/node": "^24.10.4",
|
"@types/node": "^24.10.8",
|
||||||
"@types/pg": "^8.15.1",
|
"@types/pg": "^8.15.1",
|
||||||
"@types/pngjs": "^6.0.4",
|
"@types/pngjs": "^6.0.4",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ dotenv.config({ path: resolve(import.meta.dirname, '.env') });
|
|||||||
export const playwrightHost = process.env.PLAYWRIGHT_HOST ?? '127.0.0.1';
|
export const playwrightHost = process.env.PLAYWRIGHT_HOST ?? '127.0.0.1';
|
||||||
export const playwrightDbHost = process.env.PLAYWRIGHT_DB_HOST ?? '127.0.0.1';
|
export const playwrightDbHost = process.env.PLAYWRIGHT_DB_HOST ?? '127.0.0.1';
|
||||||
export const playwriteBaseUrl = process.env.PLAYWRIGHT_BASE_URL ?? `http://${playwrightHost}:2285`;
|
export const playwriteBaseUrl = process.env.PLAYWRIGHT_BASE_URL ?? `http://${playwrightHost}:2285`;
|
||||||
export const playwriteSlowMo = parseInt(process.env.PLAYWRIGHT_SLOW_MO ?? '0');
|
export const playwriteSlowMo = Number.parseInt(process.env.PLAYWRIGHT_SLOW_MO ?? '0');
|
||||||
export const playwrightDisableWebserver = process.env.PLAYWRIGHT_DISABLE_WEBSERVER;
|
export const playwrightDisableWebserver = process.env.PLAYWRIGHT_DISABLE_WEBSERVER;
|
||||||
|
|
||||||
process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS = '1';
|
process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS = '1';
|
||||||
@@ -39,13 +39,13 @@ const config: PlaywrightTestConfig = {
|
|||||||
testMatch: /.*\.e2e-spec\.ts/,
|
testMatch: /.*\.e2e-spec\.ts/,
|
||||||
workers: 1,
|
workers: 1,
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
name: 'parallel tests',
|
// name: 'parallel tests',
|
||||||
use: { ...devices['Desktop Chrome'] },
|
// use: { ...devices['Desktop Chrome'] },
|
||||||
testMatch: /.*\.parallel-e2e-spec\.ts/,
|
// testMatch: /.*\.parallel-e2e-spec\.ts/,
|
||||||
fullyParallel: true,
|
// fullyParallel: true,
|
||||||
workers: process.env.CI ? 3 : Math.max(1, Math.round(cpus().length * 0.75) - 1),
|
// workers: process.env.CI ? 3 : Math.max(1, Math.round(cpus().length * 0.75) - 1),
|
||||||
},
|
// },
|
||||||
|
|
||||||
// {
|
// {
|
||||||
// name: 'firefox',
|
// name: 'firefox',
|
||||||
|
|||||||
@@ -0,0 +1,350 @@
|
|||||||
|
import { LoginResponseDto, ManualJobName } from '@immich/sdk';
|
||||||
|
import { errorDto } from 'src/responses';
|
||||||
|
import { app, utils } from 'src/utils';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
describe('/admin/database-backups', () => {
|
||||||
|
let cookie: string | undefined;
|
||||||
|
let admin: LoginResponseDto;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await utils.resetDatabase();
|
||||||
|
admin = await utils.adminSetup();
|
||||||
|
await utils.resetBackups(admin.accessToken);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /', async () => {
|
||||||
|
it('should succeed and be empty', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/admin/database-backups')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual({
|
||||||
|
backups: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain a created backup', async () => {
|
||||||
|
await utils.createJob(admin.accessToken, {
|
||||||
|
name: ManualJobName.BackupDatabase,
|
||||||
|
});
|
||||||
|
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'backupDatabase');
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/admin/database-backups')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
return body;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interval: 500,
|
||||||
|
timeout: 10_000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
backups: [
|
||||||
|
expect.objectContaining({
|
||||||
|
filename: expect.stringMatching(/immich-db-backup-\d{8}T\d{6}-v.*-pg.*\.sql\.gz$/),
|
||||||
|
filesize: expect.any(Number),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DELETE /', async () => {
|
||||||
|
it('should delete backup', async () => {
|
||||||
|
const filename = await utils.createBackup(admin.accessToken);
|
||||||
|
|
||||||
|
const { status } = await request(app)
|
||||||
|
.delete(`/admin/database-backups`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({ backups: [filename] });
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
|
||||||
|
const { status: listStatus, body } = await request(app)
|
||||||
|
.get('/admin/database-backups')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
|
expect(listStatus).toBe(200);
|
||||||
|
expect(body).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
backups: [],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// => action: restore database flow
|
||||||
|
|
||||||
|
describe.sequential('POST /start-restore', () => {
|
||||||
|
afterAll(async () => {
|
||||||
|
await request(app).post('/admin/maintenance').set('cookie', cookie!).send({ action: 'end' });
|
||||||
|
await utils.poll(
|
||||||
|
() => request(app).get('/server/config'),
|
||||||
|
({ status, body }) => status === 200 && !body.maintenanceMode,
|
||||||
|
);
|
||||||
|
|
||||||
|
admin = await utils.adminSetup();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.sequential('should not work when the server is configured', async () => {
|
||||||
|
const { status, body } = await request(app).post('/admin/database-backups/start-restore').send();
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest('The server already has an admin'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it.sequential('should enter maintenance mode in "database restore mode"', async () => {
|
||||||
|
await utils.resetDatabase(); // reset database before running this test
|
||||||
|
|
||||||
|
const { status, headers } = await request(app).post('/admin/database-backups/start-restore').send();
|
||||||
|
|
||||||
|
expect(status).toBe(201);
|
||||||
|
|
||||||
|
cookie = headers['set-cookie'][0].split(';')[0];
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const { status, body } = await request(app).get('/server/config');
|
||||||
|
expect(status).toBe(200);
|
||||||
|
return body.maintenanceMode;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interval: 500,
|
||||||
|
timeout: 10_000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toBeTruthy();
|
||||||
|
|
||||||
|
const { status: status2, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' });
|
||||||
|
expect(status2).toBe(200);
|
||||||
|
expect(body).toEqual({
|
||||||
|
active: true,
|
||||||
|
action: 'select_database_restore',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// => action: restore database
|
||||||
|
|
||||||
|
describe.sequential('POST /backups/restore', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await utils.disconnectDatabase();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await utils.connectDatabase();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.sequential('should restore a backup', { timeout: 60_000 }, async () => {
|
||||||
|
let filename = await utils.createBackup(admin.accessToken);
|
||||||
|
|
||||||
|
// work-around until test is running on released version
|
||||||
|
await utils.move(
|
||||||
|
`/data/backups/${filename}`,
|
||||||
|
'/data/backups/immich-db-backup-20260114T184016-v2.5.0-pg14.19.sql.gz',
|
||||||
|
);
|
||||||
|
filename = 'immich-db-backup-20260114T184016-v2.5.0-pg14.19.sql.gz';
|
||||||
|
|
||||||
|
const { status } = await request(app)
|
||||||
|
.post('/admin/maintenance')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({
|
||||||
|
action: 'restore_database',
|
||||||
|
restoreBackupFilename: filename,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(status).toBe(201);
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const { status, body } = await request(app).get('/server/config');
|
||||||
|
expect(status).toBe(200);
|
||||||
|
return body.maintenanceMode;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interval: 500,
|
||||||
|
timeout: 10_000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toBeTruthy();
|
||||||
|
|
||||||
|
const { status: status2, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' });
|
||||||
|
expect(status2).toBe(200);
|
||||||
|
expect(body).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
active: true,
|
||||||
|
action: 'restore_database',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const { status, body } = await request(app).get('/server/config');
|
||||||
|
expect(status).toBe(200);
|
||||||
|
return body.maintenanceMode;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interval: 500,
|
||||||
|
timeout: 60_000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.sequential('fail to restore a corrupted backup', { timeout: 60_000 }, async () => {
|
||||||
|
await utils.prepareTestBackup('corrupted');
|
||||||
|
|
||||||
|
const { status, headers } = await request(app)
|
||||||
|
.post('/admin/maintenance')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({
|
||||||
|
action: 'restore_database',
|
||||||
|
restoreBackupFilename: 'development-corrupted.sql.gz',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(status).toBe(201);
|
||||||
|
cookie = headers['set-cookie'][0].split(';')[0];
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const { status, body } = await request(app).get('/server/config');
|
||||||
|
expect(status).toBe(200);
|
||||||
|
return body.maintenanceMode;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interval: 500,
|
||||||
|
timeout: 10_000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toBeTruthy();
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const { status, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' });
|
||||||
|
expect(status).toBe(200);
|
||||||
|
return body;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interval: 500,
|
||||||
|
timeout: 10_000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
active: true,
|
||||||
|
action: 'restore_database',
|
||||||
|
error: 'Something went wrong, see logs!',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { status: status2, body: body2 } = await request(app)
|
||||||
|
.get('/admin/maintenance/status')
|
||||||
|
.set('cookie', cookie!)
|
||||||
|
.send({ token: 'token' });
|
||||||
|
expect(status2).toBe(200);
|
||||||
|
expect(body2).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
active: true,
|
||||||
|
action: 'restore_database',
|
||||||
|
error: expect.stringContaining('IM CORRUPTED'),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await request(app).post('/admin/maintenance').set('cookie', cookie!).send({
|
||||||
|
action: 'end',
|
||||||
|
});
|
||||||
|
|
||||||
|
await utils.poll(
|
||||||
|
() => request(app).get('/server/config'),
|
||||||
|
({ status, body }) => status === 200 && !body.maintenanceMode,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.sequential('rollback to restore point if backup is missing admin', { timeout: 60_000 }, async () => {
|
||||||
|
await utils.prepareTestBackup('empty');
|
||||||
|
|
||||||
|
const { status, headers } = await request(app)
|
||||||
|
.post('/admin/maintenance')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({
|
||||||
|
action: 'restore_database',
|
||||||
|
restoreBackupFilename: 'development-empty.sql.gz',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(status).toBe(201);
|
||||||
|
cookie = headers['set-cookie'][0].split(';')[0];
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const { status, body } = await request(app).get('/server/config');
|
||||||
|
expect(status).toBe(200);
|
||||||
|
return body.maintenanceMode;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interval: 500,
|
||||||
|
timeout: 10_000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toBeTruthy();
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const { status, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' });
|
||||||
|
expect(status).toBe(200);
|
||||||
|
return body;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interval: 500,
|
||||||
|
timeout: 30_000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
active: true,
|
||||||
|
action: 'restore_database',
|
||||||
|
error: 'Something went wrong, see logs!',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { status: status2, body: body2 } = await request(app)
|
||||||
|
.get('/admin/maintenance/status')
|
||||||
|
.set('cookie', cookie!)
|
||||||
|
.send({ token: 'token' });
|
||||||
|
expect(status2).toBe(200);
|
||||||
|
expect(body2).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
active: true,
|
||||||
|
action: 'restore_database',
|
||||||
|
error: expect.stringContaining('Server health check failed, no admin exists.'),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await request(app).post('/admin/maintenance').set('cookie', cookie!).send({
|
||||||
|
action: 'end',
|
||||||
|
});
|
||||||
|
|
||||||
|
await utils.poll(
|
||||||
|
() => request(app).get('/server/config'),
|
||||||
|
({ status, body }) => status === 200 && !body.maintenanceMode,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -14,6 +14,7 @@ describe('/admin/maintenance', () => {
|
|||||||
await utils.resetDatabase();
|
await utils.resetDatabase();
|
||||||
admin = await utils.adminSetup();
|
admin = await utils.adminSetup();
|
||||||
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
|
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
|
||||||
|
await utils.resetBackups(admin.accessToken);
|
||||||
});
|
});
|
||||||
|
|
||||||
// => outside of maintenance mode
|
// => outside of maintenance mode
|
||||||
@@ -26,6 +27,17 @@ describe('/admin/maintenance', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('GET /status', async () => {
|
||||||
|
it('to always indicate we are not in maintenance mode', async () => {
|
||||||
|
const { status, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' });
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual({
|
||||||
|
active: false,
|
||||||
|
action: 'end',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('POST /login', async () => {
|
describe('POST /login', async () => {
|
||||||
it('should not work out of maintenance mode', async () => {
|
it('should not work out of maintenance mode', async () => {
|
||||||
const { status, body } = await request(app).post('/admin/maintenance/login').send({ token: 'token' });
|
const { status, body } = await request(app).post('/admin/maintenance/login').send({ token: 'token' });
|
||||||
@@ -39,6 +51,7 @@ describe('/admin/maintenance', () => {
|
|||||||
describe.sequential('POST /', () => {
|
describe.sequential('POST /', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).post('/admin/maintenance').send({
|
const { status, body } = await request(app).post('/admin/maintenance').send({
|
||||||
|
active: false,
|
||||||
action: 'end',
|
action: 'end',
|
||||||
});
|
});
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
@@ -69,6 +82,7 @@ describe('/admin/maintenance', () => {
|
|||||||
.send({
|
.send({
|
||||||
action: 'start',
|
action: 'start',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(status).toBe(201);
|
expect(status).toBe(201);
|
||||||
|
|
||||||
cookie = headers['set-cookie'][0].split(';')[0];
|
cookie = headers['set-cookie'][0].split(';')[0];
|
||||||
@@ -79,7 +93,8 @@ describe('/admin/maintenance', () => {
|
|||||||
await expect
|
await expect
|
||||||
.poll(
|
.poll(
|
||||||
async () => {
|
async () => {
|
||||||
const { body } = await request(app).get('/server/config');
|
const { status, body } = await request(app).get('/server/config');
|
||||||
|
expect(status).toBe(200);
|
||||||
return body.maintenanceMode;
|
return body.maintenanceMode;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -102,6 +117,17 @@ describe('/admin/maintenance', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('GET /status', async () => {
|
||||||
|
it('to indicate we are in maintenance mode', async () => {
|
||||||
|
const { status, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' });
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual({
|
||||||
|
active: true,
|
||||||
|
action: 'start',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('POST /login', async () => {
|
describe('POST /login', async () => {
|
||||||
it('should fail without cookie or token in body', async () => {
|
it('should fail without cookie or token in body', async () => {
|
||||||
const { status, body } = await request(app).post('/admin/maintenance/login').send({});
|
const { status, body } = await request(app).post('/admin/maintenance/login').send({});
|
||||||
@@ -158,7 +184,8 @@ describe('/admin/maintenance', () => {
|
|||||||
await expect
|
await expect
|
||||||
.poll(
|
.poll(
|
||||||
async () => {
|
async () => {
|
||||||
const { body } = await request(app).get('/server/config');
|
const { status, body } = await request(app).get('/server/config');
|
||||||
|
expect(status).toBe(200);
|
||||||
return body.maintenanceMode;
|
return body.maintenanceMode;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -348,6 +348,7 @@ export function toAssetResponseDto(asset: MockTimelineAsset, owner?: UserRespons
|
|||||||
checksum: asset.checksum,
|
checksum: asset.checksum,
|
||||||
width: exifInfo.exifImageWidth ?? 1,
|
width: exifInfo.exifImageWidth ?? 1,
|
||||||
height: exifInfo.exifImageHeight ?? 1,
|
height: exifInfo.exifImageHeight ?? 1,
|
||||||
|
isEdited: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+74
-6
@@ -8,6 +8,7 @@ import {
|
|||||||
CreateLibraryDto,
|
CreateLibraryDto,
|
||||||
JobCreateDto,
|
JobCreateDto,
|
||||||
MaintenanceAction,
|
MaintenanceAction,
|
||||||
|
ManualJobName,
|
||||||
MetadataSearchDto,
|
MetadataSearchDto,
|
||||||
Permission,
|
Permission,
|
||||||
PersonCreateDto,
|
PersonCreateDto,
|
||||||
@@ -30,10 +31,12 @@ import {
|
|||||||
createStack,
|
createStack,
|
||||||
createUserAdmin,
|
createUserAdmin,
|
||||||
deleteAssets,
|
deleteAssets,
|
||||||
|
deleteDatabaseBackup,
|
||||||
getAssetInfo,
|
getAssetInfo,
|
||||||
getConfig,
|
getConfig,
|
||||||
getConfigDefaults,
|
getConfigDefaults,
|
||||||
getQueuesLegacy,
|
getQueuesLegacy,
|
||||||
|
listDatabaseBackups,
|
||||||
login,
|
login,
|
||||||
runQueueCommandLegacy,
|
runQueueCommandLegacy,
|
||||||
scanLibrary,
|
scanLibrary,
|
||||||
@@ -62,6 +65,7 @@ import { Readable } from 'node:stream';
|
|||||||
import { pipeline } from 'node:stream/promises';
|
import { pipeline } from 'node:stream/promises';
|
||||||
import { setTimeout as setAsyncTimeout } from 'node:timers/promises';
|
import { setTimeout as setAsyncTimeout } from 'node:timers/promises';
|
||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
|
import { createGzip } from 'node:zlib';
|
||||||
import pg from 'pg';
|
import pg from 'pg';
|
||||||
import { io, type Socket } from 'socket.io-client';
|
import { io, type Socket } from 'socket.io-client';
|
||||||
import { loginDto, signupDto } from 'src/fixtures';
|
import { loginDto, signupDto } from 'src/fixtures';
|
||||||
@@ -89,8 +93,9 @@ export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer $
|
|||||||
export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
|
export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
|
||||||
export const immichCli = (args: string[]) =>
|
export const immichCli = (args: string[]) =>
|
||||||
executeCommand('pnpm', ['exec', 'immich', '-d', `/${tempDir}/immich/`, ...args], { cwd: '../cli' }).promise;
|
executeCommand('pnpm', ['exec', 'immich', '-d', `/${tempDir}/immich/`, ...args], { cwd: '../cli' }).promise;
|
||||||
export const immichAdmin = (args: string[]) =>
|
export const dockerExec = (args: string[]) =>
|
||||||
executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]);
|
executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', args.join(' ')]);
|
||||||
|
export const immichAdmin = (args: string[]) => dockerExec([`immich-admin ${args.join(' ')}`]);
|
||||||
export const specialCharStrings = ["'", '"', ',', '{', '}', '*'];
|
export const specialCharStrings = ["'", '"', ',', '{', '}', '*'];
|
||||||
export const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
export const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||||
|
|
||||||
@@ -154,12 +159,26 @@ const onEvent = ({ event, id }: { event: EventType; id: string }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const utils = {
|
export const utils = {
|
||||||
|
connectDatabase: async () => {
|
||||||
|
if (!client) {
|
||||||
|
client = new pg.Client(dbUrl);
|
||||||
|
client.on('end', () => (client = null));
|
||||||
|
client.on('error', () => (client = null));
|
||||||
|
await client.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
return client;
|
||||||
|
},
|
||||||
|
|
||||||
|
disconnectDatabase: async () => {
|
||||||
|
if (client) {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
resetDatabase: async (tables?: string[]) => {
|
resetDatabase: async (tables?: string[]) => {
|
||||||
try {
|
try {
|
||||||
if (!client) {
|
client = await utils.connectDatabase();
|
||||||
client = new pg.Client(dbUrl);
|
|
||||||
await client.connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
tables = tables || [
|
tables = tables || [
|
||||||
// TODO e2e test for deleting a stack, since it is quite complex
|
// TODO e2e test for deleting a stack, since it is quite complex
|
||||||
@@ -616,6 +635,36 @@ export const utils = {
|
|||||||
return executeCommand('docker', ['exec', 'immich-e2e-server', 'mkdir', '-p', path]).promise;
|
return executeCommand('docker', ['exec', 'immich-e2e-server', 'mkdir', '-p', path]).promise;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
createBackup: async (accessToken: string) => {
|
||||||
|
await utils.createJob(accessToken, {
|
||||||
|
name: ManualJobName.BackupDatabase,
|
||||||
|
});
|
||||||
|
|
||||||
|
return utils.poll(
|
||||||
|
() => request(app).get('/admin/database-backups').set('Authorization', `Bearer ${accessToken}`),
|
||||||
|
({ status, body }) => status === 200 && body.backups.length === 1,
|
||||||
|
({ body }) => body.backups[0].filename,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
resetBackups: async (accessToken: string) => {
|
||||||
|
const { backups } = await listDatabaseBackups({ headers: asBearerAuth(accessToken) });
|
||||||
|
await deleteDatabaseBackup({ databaseBackupDeleteDto: { backups } }, { headers: asBearerAuth(accessToken) });
|
||||||
|
},
|
||||||
|
|
||||||
|
prepareTestBackup: async (generate: 'empty' | 'corrupted') => {
|
||||||
|
const dir = await mkdtemp(join(tmpdir(), 'test-'));
|
||||||
|
const fn = join(dir, 'file');
|
||||||
|
|
||||||
|
const sql = Readable.from(generate === 'corrupted' ? 'IM CORRUPTED;' : 'SELECT 1;');
|
||||||
|
const gzip = createGzip();
|
||||||
|
const writeStream = createWriteStream(fn);
|
||||||
|
await pipeline(sql, gzip, writeStream);
|
||||||
|
|
||||||
|
await executeCommand('docker', ['cp', fn, `immich-e2e-server:/data/backups/development-${generate}.sql.gz`])
|
||||||
|
.promise;
|
||||||
|
},
|
||||||
|
|
||||||
resetAdminConfig: async (accessToken: string) => {
|
resetAdminConfig: async (accessToken: string) => {
|
||||||
const defaultConfig = await getConfigDefaults({ headers: asBearerAuth(accessToken) });
|
const defaultConfig = await getConfigDefaults({ headers: asBearerAuth(accessToken) });
|
||||||
await updateConfig({ systemConfigDto: defaultConfig }, { headers: asBearerAuth(accessToken) });
|
await updateConfig({ systemConfigDto: defaultConfig }, { headers: asBearerAuth(accessToken) });
|
||||||
@@ -658,6 +707,25 @@ export const utils = {
|
|||||||
await utils.waitForQueueFinish(accessToken, 'sidecar');
|
await utils.waitForQueueFinish(accessToken, 'sidecar');
|
||||||
await utils.waitForQueueFinish(accessToken, 'metadataExtraction');
|
await utils.waitForQueueFinish(accessToken, 'metadataExtraction');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async poll<T>(cb: () => Promise<T>, validate: (value: T) => boolean, map?: (value: T) => any) {
|
||||||
|
let timeout = 0;
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
const data = await cb();
|
||||||
|
if (validate(data)) {
|
||||||
|
return map ? map(data) : data;
|
||||||
|
}
|
||||||
|
timeout++;
|
||||||
|
if (timeout >= 10) {
|
||||||
|
throw 'Could not clean up test.';
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 5e2));
|
||||||
|
} catch {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
utils.initSdk();
|
utils.initSdk();
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import { setupBaseMockApiRoutes } from 'src/mock-network/base-network';
|
import { setupBaseMockApiRoutes } from 'src/mock-network/base-network';
|
||||||
import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/mock-network/timeline-network';
|
import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/mock-network/timeline-network';
|
||||||
import { utils } from 'src/utils';
|
import { utils } from 'src/utils';
|
||||||
import { assetViewerUtils, cancelAllPollers } from 'src/web/specs/timeline/utils';
|
import { assetViewerUtils } from 'src/web/specs/timeline/utils';
|
||||||
|
|
||||||
test.describe.configure({ mode: 'parallel' });
|
test.describe.configure({ mode: 'parallel' });
|
||||||
test.describe('asset-viewer', () => {
|
test.describe('asset-viewer', () => {
|
||||||
@@ -49,7 +49,6 @@ test.describe('asset-viewer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.afterEach(() => {
|
test.afterEach(() => {
|
||||||
cancelAllPollers();
|
|
||||||
testContext.slowBucket = false;
|
testContext.slowBucket = false;
|
||||||
changes.albumAdditions = [];
|
changes.albumAdditions = [];
|
||||||
changes.assetDeletions = [];
|
changes.assetDeletions = [];
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { LoginResponseDto } from '@immich/sdk';
|
||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
import { utils } from 'src/utils';
|
||||||
|
|
||||||
|
test.describe.configure({ mode: 'serial' });
|
||||||
|
|
||||||
|
test.describe('Database Backups', () => {
|
||||||
|
let admin: LoginResponseDto;
|
||||||
|
|
||||||
|
test.beforeAll(async () => {
|
||||||
|
utils.initSdk();
|
||||||
|
await utils.resetDatabase();
|
||||||
|
admin = await utils.adminSetup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('restore a backup from settings', async ({ context, page }) => {
|
||||||
|
test.setTimeout(60_000);
|
||||||
|
|
||||||
|
await utils.resetBackups(admin.accessToken);
|
||||||
|
const filename = await utils.createBackup(admin.accessToken);
|
||||||
|
await utils.setAuthCookies(context, admin.accessToken);
|
||||||
|
|
||||||
|
// work-around until test is running on released version
|
||||||
|
await utils.move(
|
||||||
|
`/data/backups/${filename}`,
|
||||||
|
'/data/backups/immich-db-backup-20260114T184016-v2.5.0-pg14.19.sql.gz',
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.goto('/admin/maintenance?isOpen=backups');
|
||||||
|
await page.getByRole('button', { name: 'Restore', exact: true }).click();
|
||||||
|
await page.getByRole('dialog').getByRole('button', { name: 'Restore' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL('/maintenance?**');
|
||||||
|
await page.waitForURL('/admin/maintenance**', { timeout: 60_000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handle backup restore failure', async ({ context, page }) => {
|
||||||
|
test.setTimeout(60_000);
|
||||||
|
|
||||||
|
await utils.resetBackups(admin.accessToken);
|
||||||
|
await utils.prepareTestBackup('corrupted');
|
||||||
|
await utils.setAuthCookies(context, admin.accessToken);
|
||||||
|
|
||||||
|
await page.goto('/admin/maintenance?isOpen=backups');
|
||||||
|
await page.getByRole('button', { name: 'Restore', exact: true }).click();
|
||||||
|
await page.getByRole('dialog').getByRole('button', { name: 'Restore' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL('/maintenance?**');
|
||||||
|
await expect(page.getByText('IM CORRUPTED')).toBeVisible({ timeout: 60_000 });
|
||||||
|
await page.getByRole('button', { name: 'End maintenance mode' }).click();
|
||||||
|
await page.waitForURL('/admin/maintenance**');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rollback to restore point if backup is missing admin', async ({ context, page }) => {
|
||||||
|
test.setTimeout(60_000);
|
||||||
|
|
||||||
|
await utils.resetBackups(admin.accessToken);
|
||||||
|
await utils.prepareTestBackup('empty');
|
||||||
|
await utils.setAuthCookies(context, admin.accessToken);
|
||||||
|
|
||||||
|
await page.goto('/admin/maintenance?isOpen=backups');
|
||||||
|
await page.getByRole('button', { name: 'Restore', exact: true }).click();
|
||||||
|
await page.getByRole('dialog').getByRole('button', { name: 'Restore' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL('/maintenance?**');
|
||||||
|
await expect(page.getByText('Server health check failed, no admin exists.')).toBeVisible({ timeout: 60_000 });
|
||||||
|
await page.getByRole('button', { name: 'End maintenance mode' }).click();
|
||||||
|
await page.waitForURL('/admin/maintenance**');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('restore a backup from onboarding', async ({ context, page }) => {
|
||||||
|
test.setTimeout(60_000);
|
||||||
|
|
||||||
|
await utils.resetBackups(admin.accessToken);
|
||||||
|
const filename = await utils.createBackup(admin.accessToken);
|
||||||
|
await utils.setAuthCookies(context, admin.accessToken);
|
||||||
|
|
||||||
|
// work-around until test is running on released version
|
||||||
|
await utils.move(
|
||||||
|
`/data/backups/${filename}`,
|
||||||
|
'/data/backups/immich-db-backup-20260114T184016-v2.5.0-pg14.19.sql.gz',
|
||||||
|
);
|
||||||
|
|
||||||
|
await utils.resetDatabase();
|
||||||
|
|
||||||
|
await page.goto('/');
|
||||||
|
await page.getByRole('button', { name: 'Restore from backup' }).click();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await page.waitForURL('/maintenance**');
|
||||||
|
} catch {
|
||||||
|
// when chained with the rest of the tests
|
||||||
|
// this navigation may fail..? not sure why...
|
||||||
|
await page.goto('/maintenance');
|
||||||
|
await page.waitForURL('/maintenance**');
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Next' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Restore', exact: true }).click();
|
||||||
|
await page.getByRole('dialog').getByRole('button', { name: 'Restore' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL('/maintenance?**');
|
||||||
|
await page.waitForURL('/photos', { timeout: 60_000 });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -16,12 +16,12 @@ test.describe('Maintenance', () => {
|
|||||||
test('enter and exit maintenance mode', async ({ context, page }) => {
|
test('enter and exit maintenance mode', async ({ context, page }) => {
|
||||||
await utils.setAuthCookies(context, admin.accessToken);
|
await utils.setAuthCookies(context, admin.accessToken);
|
||||||
|
|
||||||
await page.goto('/admin/system-settings?isOpen=maintenance');
|
await page.goto('/admin/maintenance');
|
||||||
await page.getByRole('button', { name: 'Start maintenance mode' }).click();
|
await page.getByRole('button', { name: 'Switch to maintenance mode' }).click();
|
||||||
|
|
||||||
await expect(page.getByText('Temporarily Unavailable')).toBeVisible({ timeout: 10_000 });
|
await expect(page.getByText('Temporarily Unavailable')).toBeVisible({ timeout: 10_000 });
|
||||||
await page.getByRole('button', { name: 'End maintenance mode' }).click();
|
await page.getByRole('button', { name: 'End maintenance mode' }).click();
|
||||||
await page.waitForURL('**/admin/system-settings*', { timeout: 10_000 });
|
await page.waitForURL('**/admin/maintenance*', { timeout: 10_000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('maintenance shows no options to users until they authenticate', async ({ page }) => {
|
test('maintenance shows no options to users until they authenticate', async ({ page }) => {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import { pageRoutePromise, setupTimelineMockApiRoutes, TimelineTestContext } fro
|
|||||||
import { utils } from 'src/utils';
|
import { utils } from 'src/utils';
|
||||||
import {
|
import {
|
||||||
assetViewerUtils,
|
assetViewerUtils,
|
||||||
cancelAllPollers,
|
|
||||||
padYearMonth,
|
padYearMonth,
|
||||||
pageUtils,
|
pageUtils,
|
||||||
poll,
|
poll,
|
||||||
@@ -64,7 +63,6 @@ test.describe('Timeline', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.afterEach(() => {
|
test.afterEach(() => {
|
||||||
cancelAllPollers();
|
|
||||||
testContext.slowBucket = false;
|
testContext.slowBucket = false;
|
||||||
changes.albumAdditions = [];
|
changes.albumAdditions = [];
|
||||||
changes.assetDeletions = [];
|
changes.assetDeletions = [];
|
||||||
|
|||||||
@@ -23,13 +23,6 @@ export async function throttlePage(context: BrowserContext, page: Page) {
|
|||||||
await session.send('Emulation.setCPUThrottlingRate', { rate: 10 });
|
await session.send('Emulation.setCPUThrottlingRate', { rate: 10 });
|
||||||
}
|
}
|
||||||
|
|
||||||
let activePollsAbortController = new AbortController();
|
|
||||||
|
|
||||||
export const cancelAllPollers = () => {
|
|
||||||
activePollsAbortController.abort();
|
|
||||||
activePollsAbortController = new AbortController();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const poll = async <T>(
|
export const poll = async <T>(
|
||||||
page: Page,
|
page: Page,
|
||||||
query: () => Promise<T>,
|
query: () => Promise<T>,
|
||||||
@@ -37,21 +30,14 @@ export const poll = async <T>(
|
|||||||
) => {
|
) => {
|
||||||
let result;
|
let result;
|
||||||
const timeout = Date.now() + 10_000;
|
const timeout = Date.now() + 10_000;
|
||||||
const signal = activePollsAbortController.signal;
|
|
||||||
|
|
||||||
const terminate = callback || ((result: Awaited<T> | undefined) => !!result);
|
const terminate = callback || ((result: Awaited<T> | undefined) => !!result);
|
||||||
while (!terminate(result) && Date.now() < timeout) {
|
while (!terminate(result) && Date.now() < timeout) {
|
||||||
if (signal.aborted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
result = await query();
|
result = await query();
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
if (signal.aborted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (page.isClosed()) {
|
if (page.isClosed()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-3
@@ -189,6 +189,9 @@
|
|||||||
"machine_learning_smart_search_enabled": "Enable smart search",
|
"machine_learning_smart_search_enabled": "Enable smart search",
|
||||||
"machine_learning_smart_search_enabled_description": "If disabled, images will not be encoded for smart search.",
|
"machine_learning_smart_search_enabled_description": "If disabled, images will not be encoded for smart search.",
|
||||||
"machine_learning_url_description": "The URL of the machine learning server. If more than one URL is provided, each server will be attempted one-at-a-time until one responds successfully, in order from first to last. Servers that don't respond will be temporarily ignored until they come back online.",
|
"machine_learning_url_description": "The URL of the machine learning server. If more than one URL is provided, each server will be attempted one-at-a-time until one responds successfully, in order from first to last. Servers that don't respond will be temporarily ignored until they come back online.",
|
||||||
|
"maintenance_delete_backup": "Delete Backup",
|
||||||
|
"maintenance_delete_backup_description": "This file will be irrevocably deleted.",
|
||||||
|
"maintenance_delete_error": "Failed to delete backup.",
|
||||||
"maintenance_integrity_check_all": "Check All",
|
"maintenance_integrity_check_all": "Check All",
|
||||||
"maintenance_integrity_checksum_mismatch": "Checksum Mismatch",
|
"maintenance_integrity_checksum_mismatch": "Checksum Mismatch",
|
||||||
"maintenance_integrity_checksum_mismatch_job": "Check for checksum mismatches",
|
"maintenance_integrity_checksum_mismatch_job": "Check for checksum mismatches",
|
||||||
@@ -200,10 +203,18 @@
|
|||||||
"maintenance_integrity_untracked_file": "Untracked Files",
|
"maintenance_integrity_untracked_file": "Untracked Files",
|
||||||
"maintenance_integrity_untracked_file_job": "Check for untracked files",
|
"maintenance_integrity_untracked_file_job": "Check for untracked files",
|
||||||
"maintenance_integrity_untracked_file_refresh_job": "Refresh untracked file reports",
|
"maintenance_integrity_untracked_file_refresh_job": "Refresh untracked file reports",
|
||||||
|
"maintenance_restore_backup": "Restore Backup",
|
||||||
|
"maintenance_restore_backup_description": "Immich will be wiped and restored from the chosen backup. A backup will be created before continuing.",
|
||||||
|
"maintenance_restore_backup_different_version": "This backup was created with a different version of Immich!",
|
||||||
|
"maintenance_restore_backup_unknown_version": "Couldn't determine backup version.",
|
||||||
|
"maintenance_restore_database_backup": "Restore database backup",
|
||||||
|
"maintenance_restore_database_backup_description": "Rollback to an earlier database state using a backup file",
|
||||||
"maintenance_settings": "Maintenance",
|
"maintenance_settings": "Maintenance",
|
||||||
"maintenance_settings_description": "Put Immich into maintenance mode.",
|
"maintenance_settings_description": "Put Immich into maintenance mode.",
|
||||||
"maintenance_start": "Start maintenance mode",
|
"maintenance_start": "Switch to maintenance mode",
|
||||||
"maintenance_start_error": "Failed to start maintenance mode.",
|
"maintenance_start_error": "Failed to start maintenance mode.",
|
||||||
|
"maintenance_upload_backup": "Upload database backup file",
|
||||||
|
"maintenance_upload_backup_error": "Could not upload backup, is it an .sql/.sql.gz file?",
|
||||||
"manage_concurrency": "Manage Concurrency",
|
"manage_concurrency": "Manage Concurrency",
|
||||||
"manage_concurrency_description": "Navigate to the jobs page to manage job concurrency",
|
"manage_concurrency_description": "Navigate to the jobs page to manage job concurrency",
|
||||||
"manage_log_settings": "Manage log settings",
|
"manage_log_settings": "Manage log settings",
|
||||||
@@ -615,7 +626,7 @@
|
|||||||
"backup_album_selection_page_select_albums": "Select albums",
|
"backup_album_selection_page_select_albums": "Select albums",
|
||||||
"backup_album_selection_page_selection_info": "Selection Info",
|
"backup_album_selection_page_selection_info": "Selection Info",
|
||||||
"backup_album_selection_page_total_assets": "Total unique assets",
|
"backup_album_selection_page_total_assets": "Total unique assets",
|
||||||
"backup_albums_sync": "Backup albums synchronization",
|
"backup_albums_sync": "Backup Albums Synchronization",
|
||||||
"backup_all": "All",
|
"backup_all": "All",
|
||||||
"backup_background_service_backup_failed_message": "Failed to backup assets. Retrying…",
|
"backup_background_service_backup_failed_message": "Failed to backup assets. Retrying…",
|
||||||
"backup_background_service_complete_notification": "Asset backup complete",
|
"backup_background_service_complete_notification": "Asset backup complete",
|
||||||
@@ -940,6 +951,7 @@
|
|||||||
"download_include_embedded_motion_videos": "Embedded videos",
|
"download_include_embedded_motion_videos": "Embedded videos",
|
||||||
"download_include_embedded_motion_videos_description": "Include videos embedded in motion photos as a separate file",
|
"download_include_embedded_motion_videos_description": "Include videos embedded in motion photos as a separate file",
|
||||||
"download_notfound": "Download not found",
|
"download_notfound": "Download not found",
|
||||||
|
"download_original": "Download original",
|
||||||
"download_paused": "Download paused",
|
"download_paused": "Download paused",
|
||||||
"download_settings": "Download",
|
"download_settings": "Download",
|
||||||
"download_settings_description": "Manage settings related to asset download",
|
"download_settings_description": "Manage settings related to asset download",
|
||||||
@@ -1418,10 +1430,28 @@
|
|||||||
"loop_videos_description": "Enable to automatically loop a video in the detail viewer.",
|
"loop_videos_description": "Enable to automatically loop a video in the detail viewer.",
|
||||||
"main_branch_warning": "You're using a development version; we strongly recommend using a release version!",
|
"main_branch_warning": "You're using a development version; we strongly recommend using a release version!",
|
||||||
"main_menu": "Main menu",
|
"main_menu": "Main menu",
|
||||||
|
"maintenance_action_restore": "Restoring Database",
|
||||||
"maintenance_description": "Immich has been put into <link>maintenance mode</link>.",
|
"maintenance_description": "Immich has been put into <link>maintenance mode</link>.",
|
||||||
"maintenance_end": "End maintenance mode",
|
"maintenance_end": "End maintenance mode",
|
||||||
"maintenance_end_error": "Failed to end maintenance mode.",
|
"maintenance_end_error": "Failed to end maintenance mode.",
|
||||||
"maintenance_logged_in_as": "Currently logged in as {user}",
|
"maintenance_logged_in_as": "Currently logged in as {user}",
|
||||||
|
"maintenance_restore_from_backup": "Restore From Backup",
|
||||||
|
"maintenance_restore_library": "Restore Your Library",
|
||||||
|
"maintenance_restore_library_confirm": "If this looks correct, continue to restoring a backup!",
|
||||||
|
"maintenance_restore_library_description": "Restoring Database",
|
||||||
|
"maintenance_restore_library_folder_has_files": "{folder} has {count} folder(s)",
|
||||||
|
"maintenance_restore_library_folder_no_files": "{folder} is missing files!",
|
||||||
|
"maintenance_restore_library_folder_pass": "readable and writable",
|
||||||
|
"maintenance_restore_library_folder_read_fail": "not readable",
|
||||||
|
"maintenance_restore_library_folder_write_fail": "not writable",
|
||||||
|
"maintenance_restore_library_hint_missing_files": "You may be missing important files",
|
||||||
|
"maintenance_restore_library_hint_regenerate_later": "You can regenerate these later in settings",
|
||||||
|
"maintenance_restore_library_hint_storage_template_missing_files": "Using storage template? You may be missing files",
|
||||||
|
"maintenance_restore_library_loading": "Loading integrity checks and heuristics…",
|
||||||
|
"maintenance_task_backup": "Creating a backup of the existing database…",
|
||||||
|
"maintenance_task_migrations": "Running database migrations…",
|
||||||
|
"maintenance_task_restore": "Restoring the chosen backup…",
|
||||||
|
"maintenance_task_rollback": "Restore failed, rolling back to restore point…",
|
||||||
"maintenance_title": "Temporarily Unavailable",
|
"maintenance_title": "Temporarily Unavailable",
|
||||||
"make": "Make",
|
"make": "Make",
|
||||||
"manage_geolocation": "Manage location",
|
"manage_geolocation": "Manage location",
|
||||||
@@ -2229,6 +2259,7 @@
|
|||||||
"unhide_person": "Unhide person",
|
"unhide_person": "Unhide person",
|
||||||
"unknown": "Unknown",
|
"unknown": "Unknown",
|
||||||
"unknown_country": "Unknown Country",
|
"unknown_country": "Unknown Country",
|
||||||
|
"unknown_date": "Unknown date",
|
||||||
"unknown_year": "Unknown Year",
|
"unknown_year": "Unknown Year",
|
||||||
"unlimited": "Unlimited",
|
"unlimited": "Unlimited",
|
||||||
"unlink_motion_video": "Unlink motion video",
|
"unlink_motion_video": "Unlink motion video",
|
||||||
@@ -2271,7 +2302,7 @@
|
|||||||
"url": "URL",
|
"url": "URL",
|
||||||
"usage": "Usage",
|
"usage": "Usage",
|
||||||
"use_biometric": "Use biometric",
|
"use_biometric": "Use biometric",
|
||||||
"use_current_connection": "use current connection",
|
"use_current_connection": "Use current connection",
|
||||||
"use_custom_date_range": "Use custom date range instead",
|
"use_custom_date_range": "Use custom date range instead",
|
||||||
"user": "User",
|
"user": "User",
|
||||||
"user_has_been_deleted": "This user has been deleted.",
|
"user_has_been_deleted": "This user has been deleted.",
|
||||||
|
|||||||
@@ -92,14 +92,14 @@ FROM python:3.13-slim-trixie@sha256:0222b795db95bf7412cede36ab46a266cfb31f632e64
|
|||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \
|
apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \
|
||||||
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.24.8/intel-igc-core-2_2.24.8+20344_amd64.deb && \
|
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.27.10/intel-igc-core-2_2.27.10+20617_amd64.deb && \
|
||||||
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.24.8/intel-igc-opencl-2_2.24.8+20344_amd64.deb && \
|
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.27.10/intel-igc-opencl-2_2.27.10+20617_amd64.deb && \
|
||||||
wget -nv https://github.com/intel/compute-runtime/releases/download/25.48.36300.8/intel-opencl-icd_25.48.36300.8-0_amd64.deb && \
|
wget -nv https://github.com/intel/compute-runtime/releases/download/26.01.36711.4/intel-opencl-icd_26.01.36711.4-0_amd64.deb && \
|
||||||
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-core_1.0.17537.24_amd64.deb && \
|
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-core_1.0.17537.24_amd64.deb && \
|
||||||
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-opencl_1.0.17537.24_amd64.deb && \
|
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-opencl_1.0.17537.24_amd64.deb && \
|
||||||
wget -nv https://github.com/intel/compute-runtime/releases/download/24.35.30872.36/intel-opencl-icd-legacy1_24.35.30872.36_amd64.deb && \
|
wget -nv https://github.com/intel/compute-runtime/releases/download/24.35.30872.36/intel-opencl-icd-legacy1_24.35.30872.36_amd64.deb && \
|
||||||
# TODO: Figure out how to get renovate to manage this differently versioned libigdgmm file
|
# TODO: Figure out how to get renovate to manage this differently versioned libigdgmm file
|
||||||
wget -nv https://github.com/intel/compute-runtime/releases/download/25.48.36300.8/libigdgmm12_22.8.2_amd64.deb && \
|
wget -nv https://github.com/intel/compute-runtime/releases/download/26.01.36711.4/libigdgmm12_22.9.0_amd64.deb && \
|
||||||
dpkg -i *.deb && \
|
dpkg -i *.deb && \
|
||||||
rm *.deb && \
|
rm *.deb && \
|
||||||
apt-get remove wget -yqq && \
|
apt-get remove wget -yqq && \
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ experimental_monorepo_root = true
|
|||||||
[tools]
|
[tools]
|
||||||
node = "24.13.0"
|
node = "24.13.0"
|
||||||
flutter = "3.35.7"
|
flutter = "3.35.7"
|
||||||
pnpm = "10.27.0"
|
pnpm = "10.28.0"
|
||||||
terragrunt = "0.93.10"
|
terragrunt = "0.93.10"
|
||||||
opentofu = "1.10.7"
|
opentofu = "1.10.7"
|
||||||
java = "25.0.1"
|
java = "25.0.1"
|
||||||
|
|||||||
@@ -117,6 +117,9 @@
|
|||||||
<data
|
<data
|
||||||
android:host="my.immich.app"
|
android:host="my.immich.app"
|
||||||
android:pathPrefix="/memories/" />
|
android:pathPrefix="/memories/" />
|
||||||
|
<data
|
||||||
|
android:host="my.immich.app"
|
||||||
|
android:path="/memory" />
|
||||||
<data
|
<data
|
||||||
android:host="my.immich.app"
|
android:host="my.immich.app"
|
||||||
android:pathPrefix="/photos/" />
|
android:pathPrefix="/photos/" />
|
||||||
|
|||||||
+1
File diff suppressed because one or more lines are too long
@@ -22,6 +22,7 @@ sealed class BaseAsset {
|
|||||||
final int? durationInSeconds;
|
final int? durationInSeconds;
|
||||||
final bool isFavorite;
|
final bool isFavorite;
|
||||||
final String? livePhotoVideoId;
|
final String? livePhotoVideoId;
|
||||||
|
final bool isEdited;
|
||||||
|
|
||||||
const BaseAsset({
|
const BaseAsset({
|
||||||
required this.name,
|
required this.name,
|
||||||
@@ -34,6 +35,7 @@ sealed class BaseAsset {
|
|||||||
this.durationInSeconds,
|
this.durationInSeconds,
|
||||||
this.isFavorite = false,
|
this.isFavorite = false,
|
||||||
this.livePhotoVideoId,
|
this.livePhotoVideoId,
|
||||||
|
required this.isEdited,
|
||||||
});
|
});
|
||||||
|
|
||||||
bool get isImage => type == AssetType.image;
|
bool get isImage => type == AssetType.image;
|
||||||
@@ -71,6 +73,7 @@ sealed class BaseAsset {
|
|||||||
height: ${height ?? "<NA>"},
|
height: ${height ?? "<NA>"},
|
||||||
durationInSeconds: ${durationInSeconds ?? "<NA>"},
|
durationInSeconds: ${durationInSeconds ?? "<NA>"},
|
||||||
isFavorite: $isFavorite,
|
isFavorite: $isFavorite,
|
||||||
|
isEdited: $isEdited,
|
||||||
}''';
|
}''';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +88,8 @@ sealed class BaseAsset {
|
|||||||
width == other.width &&
|
width == other.width &&
|
||||||
height == other.height &&
|
height == other.height &&
|
||||||
durationInSeconds == other.durationInSeconds &&
|
durationInSeconds == other.durationInSeconds &&
|
||||||
isFavorite == other.isFavorite;
|
isFavorite == other.isFavorite &&
|
||||||
|
isEdited == other.isEdited;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -99,6 +103,7 @@ sealed class BaseAsset {
|
|||||||
width.hashCode ^
|
width.hashCode ^
|
||||||
height.hashCode ^
|
height.hashCode ^
|
||||||
durationInSeconds.hashCode ^
|
durationInSeconds.hashCode ^
|
||||||
isFavorite.hashCode;
|
isFavorite.hashCode ^
|
||||||
|
isEdited.hashCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class LocalAsset extends BaseAsset {
|
|||||||
this.adjustmentTime,
|
this.adjustmentTime,
|
||||||
this.latitude,
|
this.latitude,
|
||||||
this.longitude,
|
this.longitude,
|
||||||
|
required super.isEdited,
|
||||||
}) : remoteAssetId = remoteId;
|
}) : remoteAssetId = remoteId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -107,6 +108,7 @@ class LocalAsset extends BaseAsset {
|
|||||||
DateTime? adjustmentTime,
|
DateTime? adjustmentTime,
|
||||||
double? latitude,
|
double? latitude,
|
||||||
double? longitude,
|
double? longitude,
|
||||||
|
bool? isEdited,
|
||||||
}) {
|
}) {
|
||||||
return LocalAsset(
|
return LocalAsset(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -125,6 +127,7 @@ class LocalAsset extends BaseAsset {
|
|||||||
adjustmentTime: adjustmentTime ?? this.adjustmentTime,
|
adjustmentTime: adjustmentTime ?? this.adjustmentTime,
|
||||||
latitude: latitude ?? this.latitude,
|
latitude: latitude ?? this.latitude,
|
||||||
longitude: longitude ?? this.longitude,
|
longitude: longitude ?? this.longitude,
|
||||||
|
isEdited: isEdited ?? this.isEdited,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class RemoteAsset extends BaseAsset {
|
|||||||
this.visibility = AssetVisibility.timeline,
|
this.visibility = AssetVisibility.timeline,
|
||||||
super.livePhotoVideoId,
|
super.livePhotoVideoId,
|
||||||
this.stackId,
|
this.stackId,
|
||||||
|
required super.isEdited,
|
||||||
}) : localAssetId = localId;
|
}) : localAssetId = localId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -104,6 +105,7 @@ class RemoteAsset extends BaseAsset {
|
|||||||
AssetVisibility? visibility,
|
AssetVisibility? visibility,
|
||||||
String? livePhotoVideoId,
|
String? livePhotoVideoId,
|
||||||
String? stackId,
|
String? stackId,
|
||||||
|
bool? isEdited,
|
||||||
}) {
|
}) {
|
||||||
return RemoteAsset(
|
return RemoteAsset(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -122,6 +124,7 @@ class RemoteAsset extends BaseAsset {
|
|||||||
visibility: visibility ?? this.visibility,
|
visibility: visibility ?? this.visibility,
|
||||||
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
||||||
stackId: stackId ?? this.stackId,
|
stackId: stackId ?? this.stackId,
|
||||||
|
isEdited: isEdited ?? this.isEdited,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -436,5 +436,6 @@ extension PlatformToLocalAsset on PlatformAsset {
|
|||||||
adjustmentTime: tryFromSecondsSinceEpoch(adjustmentTime, isUtc: true),
|
adjustmentTime: tryFromSecondsSinceEpoch(adjustmentTime, isUtc: true),
|
||||||
latitude: latitude,
|
latitude: latitude,
|
||||||
longitude: longitude,
|
longitude: longitude,
|
||||||
|
isEdited: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ extension on AssetResponseDto {
|
|||||||
thumbHash: thumbhash,
|
thumbHash: thumbhash,
|
||||||
localId: null,
|
localId: null,
|
||||||
type: type.toAssetType(),
|
type: type.toAssetType(),
|
||||||
|
isEdited: isEdited,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -247,6 +247,42 @@ class SyncStreamService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> handleWsAssetEditReadyV1Batch(List<dynamic> batchData) async {
|
||||||
|
if (batchData.isEmpty) return;
|
||||||
|
|
||||||
|
_logger.info('Processing batch of ${batchData.length} AssetEditReadyV1 events');
|
||||||
|
|
||||||
|
final List<SyncAssetV1> assets = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (final data in batchData) {
|
||||||
|
if (data is! Map<String, dynamic>) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final payload = data;
|
||||||
|
final assetData = payload['asset'];
|
||||||
|
|
||||||
|
if (assetData == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final asset = SyncAssetV1.fromJson(assetData);
|
||||||
|
|
||||||
|
if (asset != null) {
|
||||||
|
assets.add(asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assets.isNotEmpty) {
|
||||||
|
await _syncStreamRepository.updateAssetsV1(assets, debugLabel: 'websocket-edit');
|
||||||
|
_logger.info('Successfully processed ${assets.length} edited assets');
|
||||||
|
}
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
_logger.severe("Error processing AssetEditReadyV1 websocket batch events", error, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _handleRemoteTrashed(Iterable<String> checksums) async {
|
Future<void> _handleRemoteTrashed(Iterable<String> checksums) async {
|
||||||
if (checksums.isEmpty) {
|
if (checksums.isEmpty) {
|
||||||
return Future.value();
|
return Future.value();
|
||||||
|
|||||||
@@ -196,6 +196,16 @@ class BackgroundSyncManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> syncWebsocketEditBatch(List<dynamic> batchData) {
|
||||||
|
if (_syncWebsocketTask != null) {
|
||||||
|
return _syncWebsocketTask!.future;
|
||||||
|
}
|
||||||
|
_syncWebsocketTask = _handleWsAssetEditReadyV1Batch(batchData);
|
||||||
|
return _syncWebsocketTask!.whenComplete(() {
|
||||||
|
_syncWebsocketTask = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> syncLinkedAlbum() {
|
Future<void> syncLinkedAlbum() {
|
||||||
if (_linkedAlbumSyncTask != null) {
|
if (_linkedAlbumSyncTask != null) {
|
||||||
return _linkedAlbumSyncTask!.future;
|
return _linkedAlbumSyncTask!.future;
|
||||||
@@ -231,3 +241,8 @@ Cancelable<void> _handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) => ru
|
|||||||
computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetUploadReadyV1Batch(batchData),
|
computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetUploadReadyV1Batch(batchData),
|
||||||
debugLabel: 'websocket-batch',
|
debugLabel: 'websocket-batch',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Cancelable<void> _handleWsAssetEditReadyV1Batch(List<dynamic> batchData) => runInIsolateGentle(
|
||||||
|
computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetEditReadyV1Batch(batchData),
|
||||||
|
debugLabel: 'websocket-edit',
|
||||||
|
);
|
||||||
|
|||||||
@@ -47,5 +47,6 @@ extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData {
|
|||||||
latitude: latitude,
|
latitude: latitude,
|
||||||
longitude: longitude,
|
longitude: longitude,
|
||||||
cloudId: iCloudId,
|
cloudId: iCloudId,
|
||||||
|
isEdited: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ SELECT
|
|||||||
NULL as i_cloud_id,
|
NULL as i_cloud_id,
|
||||||
NULL as latitude,
|
NULL as latitude,
|
||||||
NULL as longitude,
|
NULL as longitude,
|
||||||
NULL as adjustmentTime
|
NULL as adjustmentTime,
|
||||||
|
rae.is_edited
|
||||||
FROM
|
FROM
|
||||||
remote_asset_entity rae
|
remote_asset_entity rae
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
@@ -61,7 +62,8 @@ SELECT
|
|||||||
lae.i_cloud_id,
|
lae.i_cloud_id,
|
||||||
lae.latitude,
|
lae.latitude,
|
||||||
lae.longitude,
|
lae.longitude,
|
||||||
lae.adjustment_time
|
lae.adjustment_time,
|
||||||
|
0 as is_edited
|
||||||
FROM
|
FROM
|
||||||
local_asset_entity lae
|
local_asset_entity lae
|
||||||
WHERE NOT EXISTS (
|
WHERE NOT EXISTS (
|
||||||
|
|||||||
+4
-1
@@ -29,7 +29,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
|||||||
);
|
);
|
||||||
$arrayStartIndex += generatedlimit.amountOfVariables;
|
$arrayStartIndex += generatedlimit.amountOfVariables;
|
||||||
return customSelect(
|
return customSelect(
|
||||||
'SELECT rae.id AS remote_id, (SELECT lae.id FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum LIMIT 1) AS local_id, rae.name, rae.type, rae.created_at AS created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id, NULL AS i_cloud_id, NULL AS latitude, NULL AS longitude, NULL AS adjustmentTime FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at AS created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation, NULL AS stack_id, lae.i_cloud_id, lae.latitude, lae.longitude, lae.adjustment_time FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2) ORDER BY created_at DESC ${generatedlimit.sql}',
|
'SELECT rae.id AS remote_id, (SELECT lae.id FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum LIMIT 1) AS local_id, rae.name, rae.type, rae.created_at AS created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id, NULL AS i_cloud_id, NULL AS latitude, NULL AS longitude, NULL AS adjustmentTime, rae.is_edited FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at AS created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation, NULL AS stack_id, lae.i_cloud_id, lae.latitude, lae.longitude, lae.adjustment_time, 0 AS is_edited FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2) ORDER BY created_at DESC ${generatedlimit.sql}',
|
||||||
variables: [
|
variables: [
|
||||||
for (var $ in userIds) i0.Variable<String>($),
|
for (var $ in userIds) i0.Variable<String>($),
|
||||||
...generatedlimit.introducedVariables,
|
...generatedlimit.introducedVariables,
|
||||||
@@ -66,6 +66,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
|||||||
latitude: row.readNullable<double>('latitude'),
|
latitude: row.readNullable<double>('latitude'),
|
||||||
longitude: row.readNullable<double>('longitude'),
|
longitude: row.readNullable<double>('longitude'),
|
||||||
adjustmentTime: row.readNullable<DateTime>('adjustmentTime'),
|
adjustmentTime: row.readNullable<DateTime>('adjustmentTime'),
|
||||||
|
isEdited: row.read<bool>('is_edited'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -137,6 +138,7 @@ class MergedAssetResult {
|
|||||||
final double? latitude;
|
final double? latitude;
|
||||||
final double? longitude;
|
final double? longitude;
|
||||||
final DateTime? adjustmentTime;
|
final DateTime? adjustmentTime;
|
||||||
|
final bool isEdited;
|
||||||
MergedAssetResult({
|
MergedAssetResult({
|
||||||
this.remoteId,
|
this.remoteId,
|
||||||
this.localId,
|
this.localId,
|
||||||
@@ -158,6 +160,7 @@ class MergedAssetResult {
|
|||||||
this.latitude,
|
this.latitude,
|
||||||
this.longitude,
|
this.longitude,
|
||||||
this.adjustmentTime,
|
this.adjustmentTime,
|
||||||
|
required this.isEdited,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin
|
|||||||
|
|
||||||
TextColumn get libraryId => text().nullable()();
|
TextColumn get libraryId => text().nullable()();
|
||||||
|
|
||||||
|
BoolColumn get isEdited => boolean().withDefault(const Constant(false))();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<Column> get primaryKey => {id};
|
Set<Column> get primaryKey => {id};
|
||||||
}
|
}
|
||||||
@@ -66,5 +68,6 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
|
|||||||
livePhotoVideoId: livePhotoVideoId,
|
livePhotoVideoId: livePhotoVideoId,
|
||||||
localId: localId,
|
localId: localId,
|
||||||
stackId: stackId,
|
stackId: stackId,
|
||||||
|
isEdited: isEdited,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ typedef $$RemoteAssetEntityTableCreateCompanionBuilder =
|
|||||||
required i2.AssetVisibility visibility,
|
required i2.AssetVisibility visibility,
|
||||||
i0.Value<String?> stackId,
|
i0.Value<String?> stackId,
|
||||||
i0.Value<String?> libraryId,
|
i0.Value<String?> libraryId,
|
||||||
|
i0.Value<bool> isEdited,
|
||||||
});
|
});
|
||||||
typedef $$RemoteAssetEntityTableUpdateCompanionBuilder =
|
typedef $$RemoteAssetEntityTableUpdateCompanionBuilder =
|
||||||
i1.RemoteAssetEntityCompanion Function({
|
i1.RemoteAssetEntityCompanion Function({
|
||||||
@@ -52,6 +53,7 @@ typedef $$RemoteAssetEntityTableUpdateCompanionBuilder =
|
|||||||
i0.Value<i2.AssetVisibility> visibility,
|
i0.Value<i2.AssetVisibility> visibility,
|
||||||
i0.Value<String?> stackId,
|
i0.Value<String?> stackId,
|
||||||
i0.Value<String?> libraryId,
|
i0.Value<String?> libraryId,
|
||||||
|
i0.Value<bool> isEdited,
|
||||||
});
|
});
|
||||||
|
|
||||||
final class $$RemoteAssetEntityTableReferences
|
final class $$RemoteAssetEntityTableReferences
|
||||||
@@ -196,6 +198,11 @@ class $$RemoteAssetEntityTableFilterComposer
|
|||||||
builder: (column) => i0.ColumnFilters(column),
|
builder: (column) => i0.ColumnFilters(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
i0.ColumnFilters<bool> get isEdited => $composableBuilder(
|
||||||
|
column: $table.isEdited,
|
||||||
|
builder: (column) => i0.ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
i5.$$UserEntityTableFilterComposer get ownerId {
|
i5.$$UserEntityTableFilterComposer get ownerId {
|
||||||
final i5.$$UserEntityTableFilterComposer composer = $composerBuilder(
|
final i5.$$UserEntityTableFilterComposer composer = $composerBuilder(
|
||||||
composer: this,
|
composer: this,
|
||||||
@@ -318,6 +325,11 @@ class $$RemoteAssetEntityTableOrderingComposer
|
|||||||
builder: (column) => i0.ColumnOrderings(column),
|
builder: (column) => i0.ColumnOrderings(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
i0.ColumnOrderings<bool> get isEdited => $composableBuilder(
|
||||||
|
column: $table.isEdited,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
i5.$$UserEntityTableOrderingComposer get ownerId {
|
i5.$$UserEntityTableOrderingComposer get ownerId {
|
||||||
final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder(
|
final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder(
|
||||||
composer: this,
|
composer: this,
|
||||||
@@ -417,6 +429,9 @@ class $$RemoteAssetEntityTableAnnotationComposer
|
|||||||
i0.GeneratedColumn<String> get libraryId =>
|
i0.GeneratedColumn<String> get libraryId =>
|
||||||
$composableBuilder(column: $table.libraryId, builder: (column) => column);
|
$composableBuilder(column: $table.libraryId, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<bool> get isEdited =>
|
||||||
|
$composableBuilder(column: $table.isEdited, builder: (column) => column);
|
||||||
|
|
||||||
i5.$$UserEntityTableAnnotationComposer get ownerId {
|
i5.$$UserEntityTableAnnotationComposer get ownerId {
|
||||||
final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
|
final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
|
||||||
composer: this,
|
composer: this,
|
||||||
@@ -497,6 +512,7 @@ class $$RemoteAssetEntityTableTableManager
|
|||||||
const i0.Value.absent(),
|
const i0.Value.absent(),
|
||||||
i0.Value<String?> stackId = const i0.Value.absent(),
|
i0.Value<String?> stackId = const i0.Value.absent(),
|
||||||
i0.Value<String?> libraryId = const i0.Value.absent(),
|
i0.Value<String?> libraryId = const i0.Value.absent(),
|
||||||
|
i0.Value<bool> isEdited = const i0.Value.absent(),
|
||||||
}) => i1.RemoteAssetEntityCompanion(
|
}) => i1.RemoteAssetEntityCompanion(
|
||||||
name: name,
|
name: name,
|
||||||
type: type,
|
type: type,
|
||||||
@@ -516,6 +532,7 @@ class $$RemoteAssetEntityTableTableManager
|
|||||||
visibility: visibility,
|
visibility: visibility,
|
||||||
stackId: stackId,
|
stackId: stackId,
|
||||||
libraryId: libraryId,
|
libraryId: libraryId,
|
||||||
|
isEdited: isEdited,
|
||||||
),
|
),
|
||||||
createCompanionCallback:
|
createCompanionCallback:
|
||||||
({
|
({
|
||||||
@@ -537,6 +554,7 @@ class $$RemoteAssetEntityTableTableManager
|
|||||||
required i2.AssetVisibility visibility,
|
required i2.AssetVisibility visibility,
|
||||||
i0.Value<String?> stackId = const i0.Value.absent(),
|
i0.Value<String?> stackId = const i0.Value.absent(),
|
||||||
i0.Value<String?> libraryId = const i0.Value.absent(),
|
i0.Value<String?> libraryId = const i0.Value.absent(),
|
||||||
|
i0.Value<bool> isEdited = const i0.Value.absent(),
|
||||||
}) => i1.RemoteAssetEntityCompanion.insert(
|
}) => i1.RemoteAssetEntityCompanion.insert(
|
||||||
name: name,
|
name: name,
|
||||||
type: type,
|
type: type,
|
||||||
@@ -556,6 +574,7 @@ class $$RemoteAssetEntityTableTableManager
|
|||||||
visibility: visibility,
|
visibility: visibility,
|
||||||
stackId: stackId,
|
stackId: stackId,
|
||||||
libraryId: libraryId,
|
libraryId: libraryId,
|
||||||
|
isEdited: isEdited,
|
||||||
),
|
),
|
||||||
withReferenceMapper: (p0) => p0
|
withReferenceMapper: (p0) => p0
|
||||||
.map(
|
.map(
|
||||||
@@ -844,6 +863,21 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
|
|||||||
type: i0.DriftSqlType.string,
|
type: i0.DriftSqlType.string,
|
||||||
requiredDuringInsert: false,
|
requiredDuringInsert: false,
|
||||||
);
|
);
|
||||||
|
static const i0.VerificationMeta _isEditedMeta = const i0.VerificationMeta(
|
||||||
|
'isEdited',
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<bool> isEdited = i0.GeneratedColumn<bool>(
|
||||||
|
'is_edited',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i0.DriftSqlType.bool,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_edited" IN (0, 1))',
|
||||||
|
),
|
||||||
|
defaultValue: const i4.Constant(false),
|
||||||
|
);
|
||||||
@override
|
@override
|
||||||
List<i0.GeneratedColumn> get $columns => [
|
List<i0.GeneratedColumn> get $columns => [
|
||||||
name,
|
name,
|
||||||
@@ -864,6 +898,7 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
|
|||||||
visibility,
|
visibility,
|
||||||
stackId,
|
stackId,
|
||||||
libraryId,
|
libraryId,
|
||||||
|
isEdited,
|
||||||
];
|
];
|
||||||
@override
|
@override
|
||||||
String get aliasedName => _alias ?? actualTableName;
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
@@ -987,6 +1022,12 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
|
|||||||
libraryId.isAcceptableOrUnknown(data['library_id']!, _libraryIdMeta),
|
libraryId.isAcceptableOrUnknown(data['library_id']!, _libraryIdMeta),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (data.containsKey('is_edited')) {
|
||||||
|
context.handle(
|
||||||
|
_isEditedMeta,
|
||||||
|
isEdited.isAcceptableOrUnknown(data['is_edited']!, _isEditedMeta),
|
||||||
|
);
|
||||||
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1075,6 +1116,10 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
|
|||||||
i0.DriftSqlType.string,
|
i0.DriftSqlType.string,
|
||||||
data['${effectivePrefix}library_id'],
|
data['${effectivePrefix}library_id'],
|
||||||
),
|
),
|
||||||
|
isEdited: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.bool,
|
||||||
|
data['${effectivePrefix}is_edited'],
|
||||||
|
)!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1115,6 +1160,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
final i2.AssetVisibility visibility;
|
final i2.AssetVisibility visibility;
|
||||||
final String? stackId;
|
final String? stackId;
|
||||||
final String? libraryId;
|
final String? libraryId;
|
||||||
|
final bool isEdited;
|
||||||
const RemoteAssetEntityData({
|
const RemoteAssetEntityData({
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.type,
|
required this.type,
|
||||||
@@ -1134,6 +1180,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
required this.visibility,
|
required this.visibility,
|
||||||
this.stackId,
|
this.stackId,
|
||||||
this.libraryId,
|
this.libraryId,
|
||||||
|
required this.isEdited,
|
||||||
});
|
});
|
||||||
@override
|
@override
|
||||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||||
@@ -1182,6 +1229,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
if (!nullToAbsent || libraryId != null) {
|
if (!nullToAbsent || libraryId != null) {
|
||||||
map['library_id'] = i0.Variable<String>(libraryId);
|
map['library_id'] = i0.Variable<String>(libraryId);
|
||||||
}
|
}
|
||||||
|
map['is_edited'] = i0.Variable<bool>(isEdited);
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1213,6 +1261,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
),
|
),
|
||||||
stackId: serializer.fromJson<String?>(json['stackId']),
|
stackId: serializer.fromJson<String?>(json['stackId']),
|
||||||
libraryId: serializer.fromJson<String?>(json['libraryId']),
|
libraryId: serializer.fromJson<String?>(json['libraryId']),
|
||||||
|
isEdited: serializer.fromJson<bool>(json['isEdited']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@override
|
@override
|
||||||
@@ -1241,6 +1290,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
),
|
),
|
||||||
'stackId': serializer.toJson<String?>(stackId),
|
'stackId': serializer.toJson<String?>(stackId),
|
||||||
'libraryId': serializer.toJson<String?>(libraryId),
|
'libraryId': serializer.toJson<String?>(libraryId),
|
||||||
|
'isEdited': serializer.toJson<bool>(isEdited),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1263,6 +1313,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
i2.AssetVisibility? visibility,
|
i2.AssetVisibility? visibility,
|
||||||
i0.Value<String?> stackId = const i0.Value.absent(),
|
i0.Value<String?> stackId = const i0.Value.absent(),
|
||||||
i0.Value<String?> libraryId = const i0.Value.absent(),
|
i0.Value<String?> libraryId = const i0.Value.absent(),
|
||||||
|
bool? isEdited,
|
||||||
}) => i1.RemoteAssetEntityData(
|
}) => i1.RemoteAssetEntityData(
|
||||||
name: name ?? this.name,
|
name: name ?? this.name,
|
||||||
type: type ?? this.type,
|
type: type ?? this.type,
|
||||||
@@ -1288,6 +1339,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
visibility: visibility ?? this.visibility,
|
visibility: visibility ?? this.visibility,
|
||||||
stackId: stackId.present ? stackId.value : this.stackId,
|
stackId: stackId.present ? stackId.value : this.stackId,
|
||||||
libraryId: libraryId.present ? libraryId.value : this.libraryId,
|
libraryId: libraryId.present ? libraryId.value : this.libraryId,
|
||||||
|
isEdited: isEdited ?? this.isEdited,
|
||||||
);
|
);
|
||||||
RemoteAssetEntityData copyWithCompanion(i1.RemoteAssetEntityCompanion data) {
|
RemoteAssetEntityData copyWithCompanion(i1.RemoteAssetEntityCompanion data) {
|
||||||
return RemoteAssetEntityData(
|
return RemoteAssetEntityData(
|
||||||
@@ -1319,6 +1371,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
: this.visibility,
|
: this.visibility,
|
||||||
stackId: data.stackId.present ? data.stackId.value : this.stackId,
|
stackId: data.stackId.present ? data.stackId.value : this.stackId,
|
||||||
libraryId: data.libraryId.present ? data.libraryId.value : this.libraryId,
|
libraryId: data.libraryId.present ? data.libraryId.value : this.libraryId,
|
||||||
|
isEdited: data.isEdited.present ? data.isEdited.value : this.isEdited,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1342,7 +1395,8 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
..write('livePhotoVideoId: $livePhotoVideoId, ')
|
..write('livePhotoVideoId: $livePhotoVideoId, ')
|
||||||
..write('visibility: $visibility, ')
|
..write('visibility: $visibility, ')
|
||||||
..write('stackId: $stackId, ')
|
..write('stackId: $stackId, ')
|
||||||
..write('libraryId: $libraryId')
|
..write('libraryId: $libraryId, ')
|
||||||
|
..write('isEdited: $isEdited')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
@@ -1367,6 +1421,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
visibility,
|
visibility,
|
||||||
stackId,
|
stackId,
|
||||||
libraryId,
|
libraryId,
|
||||||
|
isEdited,
|
||||||
);
|
);
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
@@ -1389,7 +1444,8 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
other.livePhotoVideoId == this.livePhotoVideoId &&
|
other.livePhotoVideoId == this.livePhotoVideoId &&
|
||||||
other.visibility == this.visibility &&
|
other.visibility == this.visibility &&
|
||||||
other.stackId == this.stackId &&
|
other.stackId == this.stackId &&
|
||||||
other.libraryId == this.libraryId);
|
other.libraryId == this.libraryId &&
|
||||||
|
other.isEdited == this.isEdited);
|
||||||
}
|
}
|
||||||
|
|
||||||
class RemoteAssetEntityCompanion
|
class RemoteAssetEntityCompanion
|
||||||
@@ -1412,6 +1468,7 @@ class RemoteAssetEntityCompanion
|
|||||||
final i0.Value<i2.AssetVisibility> visibility;
|
final i0.Value<i2.AssetVisibility> visibility;
|
||||||
final i0.Value<String?> stackId;
|
final i0.Value<String?> stackId;
|
||||||
final i0.Value<String?> libraryId;
|
final i0.Value<String?> libraryId;
|
||||||
|
final i0.Value<bool> isEdited;
|
||||||
const RemoteAssetEntityCompanion({
|
const RemoteAssetEntityCompanion({
|
||||||
this.name = const i0.Value.absent(),
|
this.name = const i0.Value.absent(),
|
||||||
this.type = const i0.Value.absent(),
|
this.type = const i0.Value.absent(),
|
||||||
@@ -1431,6 +1488,7 @@ class RemoteAssetEntityCompanion
|
|||||||
this.visibility = const i0.Value.absent(),
|
this.visibility = const i0.Value.absent(),
|
||||||
this.stackId = const i0.Value.absent(),
|
this.stackId = const i0.Value.absent(),
|
||||||
this.libraryId = const i0.Value.absent(),
|
this.libraryId = const i0.Value.absent(),
|
||||||
|
this.isEdited = const i0.Value.absent(),
|
||||||
});
|
});
|
||||||
RemoteAssetEntityCompanion.insert({
|
RemoteAssetEntityCompanion.insert({
|
||||||
required String name,
|
required String name,
|
||||||
@@ -1451,6 +1509,7 @@ class RemoteAssetEntityCompanion
|
|||||||
required i2.AssetVisibility visibility,
|
required i2.AssetVisibility visibility,
|
||||||
this.stackId = const i0.Value.absent(),
|
this.stackId = const i0.Value.absent(),
|
||||||
this.libraryId = const i0.Value.absent(),
|
this.libraryId = const i0.Value.absent(),
|
||||||
|
this.isEdited = const i0.Value.absent(),
|
||||||
}) : name = i0.Value(name),
|
}) : name = i0.Value(name),
|
||||||
type = i0.Value(type),
|
type = i0.Value(type),
|
||||||
id = i0.Value(id),
|
id = i0.Value(id),
|
||||||
@@ -1476,6 +1535,7 @@ class RemoteAssetEntityCompanion
|
|||||||
i0.Expression<int>? visibility,
|
i0.Expression<int>? visibility,
|
||||||
i0.Expression<String>? stackId,
|
i0.Expression<String>? stackId,
|
||||||
i0.Expression<String>? libraryId,
|
i0.Expression<String>? libraryId,
|
||||||
|
i0.Expression<bool>? isEdited,
|
||||||
}) {
|
}) {
|
||||||
return i0.RawValuesInsertable({
|
return i0.RawValuesInsertable({
|
||||||
if (name != null) 'name': name,
|
if (name != null) 'name': name,
|
||||||
@@ -1496,6 +1556,7 @@ class RemoteAssetEntityCompanion
|
|||||||
if (visibility != null) 'visibility': visibility,
|
if (visibility != null) 'visibility': visibility,
|
||||||
if (stackId != null) 'stack_id': stackId,
|
if (stackId != null) 'stack_id': stackId,
|
||||||
if (libraryId != null) 'library_id': libraryId,
|
if (libraryId != null) 'library_id': libraryId,
|
||||||
|
if (isEdited != null) 'is_edited': isEdited,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1518,6 +1579,7 @@ class RemoteAssetEntityCompanion
|
|||||||
i0.Value<i2.AssetVisibility>? visibility,
|
i0.Value<i2.AssetVisibility>? visibility,
|
||||||
i0.Value<String?>? stackId,
|
i0.Value<String?>? stackId,
|
||||||
i0.Value<String?>? libraryId,
|
i0.Value<String?>? libraryId,
|
||||||
|
i0.Value<bool>? isEdited,
|
||||||
}) {
|
}) {
|
||||||
return i1.RemoteAssetEntityCompanion(
|
return i1.RemoteAssetEntityCompanion(
|
||||||
name: name ?? this.name,
|
name: name ?? this.name,
|
||||||
@@ -1538,6 +1600,7 @@ class RemoteAssetEntityCompanion
|
|||||||
visibility: visibility ?? this.visibility,
|
visibility: visibility ?? this.visibility,
|
||||||
stackId: stackId ?? this.stackId,
|
stackId: stackId ?? this.stackId,
|
||||||
libraryId: libraryId ?? this.libraryId,
|
libraryId: libraryId ?? this.libraryId,
|
||||||
|
isEdited: isEdited ?? this.isEdited,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1602,6 +1665,9 @@ class RemoteAssetEntityCompanion
|
|||||||
if (libraryId.present) {
|
if (libraryId.present) {
|
||||||
map['library_id'] = i0.Variable<String>(libraryId.value);
|
map['library_id'] = i0.Variable<String>(libraryId.value);
|
||||||
}
|
}
|
||||||
|
if (isEdited.present) {
|
||||||
|
map['is_edited'] = i0.Variable<bool>(isEdited.value);
|
||||||
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1625,7 +1691,8 @@ class RemoteAssetEntityCompanion
|
|||||||
..write('livePhotoVideoId: $livePhotoVideoId, ')
|
..write('livePhotoVideoId: $livePhotoVideoId, ')
|
||||||
..write('visibility: $visibility, ')
|
..write('visibility: $visibility, ')
|
||||||
..write('stackId: $stackId, ')
|
..write('stackId: $stackId, ')
|
||||||
..write('libraryId: $libraryId')
|
..write('libraryId: $libraryId, ')
|
||||||
|
..write('isEdited: $isEdited')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,5 +45,6 @@ extension TrashedLocalAssetEntityDataDomainExtension on TrashedLocalAssetEntityD
|
|||||||
height: height,
|
height: height,
|
||||||
width: width,
|
width: width,
|
||||||
orientation: orientation,
|
orientation: orientation,
|
||||||
|
isEdited: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ class Drift extends $Drift implements IDatabaseRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 16;
|
int get schemaVersion => 17;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
@@ -201,6 +201,9 @@ class Drift extends $Drift implements IDatabaseRepository {
|
|||||||
await m.createIndex(v16.idxLocalAssetCloudId);
|
await m.createIndex(v16.idxLocalAssetCloudId);
|
||||||
await m.createTable(v16.remoteAssetCloudIdEntity);
|
await m.createTable(v16.remoteAssetCloudIdEntity);
|
||||||
},
|
},
|
||||||
|
from16To17: (m, v17) async {
|
||||||
|
await m.addColumn(v17.remoteAssetEntity, v17.remoteAssetEntity.isEdited);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -6911,6 +6911,503 @@ i1.GeneratedColumn<DateTime> _column_100(String aliasedName) =>
|
|||||||
true,
|
true,
|
||||||
type: i1.DriftSqlType.dateTime,
|
type: i1.DriftSqlType.dateTime,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final class Schema17 extends i0.VersionedSchema {
|
||||||
|
Schema17({required super.database}) : super(version: 17);
|
||||||
|
@override
|
||||||
|
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||||
|
userEntity,
|
||||||
|
remoteAssetEntity,
|
||||||
|
stackEntity,
|
||||||
|
localAssetEntity,
|
||||||
|
remoteAlbumEntity,
|
||||||
|
localAlbumEntity,
|
||||||
|
localAlbumAssetEntity,
|
||||||
|
idxLocalAssetChecksum,
|
||||||
|
idxLocalAssetCloudId,
|
||||||
|
idxRemoteAssetOwnerChecksum,
|
||||||
|
uQRemoteAssetsOwnerChecksum,
|
||||||
|
uQRemoteAssetsOwnerLibraryChecksum,
|
||||||
|
idxRemoteAssetChecksum,
|
||||||
|
authUserEntity,
|
||||||
|
userMetadataEntity,
|
||||||
|
partnerEntity,
|
||||||
|
remoteExifEntity,
|
||||||
|
remoteAlbumAssetEntity,
|
||||||
|
remoteAlbumUserEntity,
|
||||||
|
remoteAssetCloudIdEntity,
|
||||||
|
memoryEntity,
|
||||||
|
memoryAssetEntity,
|
||||||
|
personEntity,
|
||||||
|
assetFaceEntity,
|
||||||
|
storeEntity,
|
||||||
|
trashedLocalAssetEntity,
|
||||||
|
idxLatLng,
|
||||||
|
idxTrashedLocalAssetChecksum,
|
||||||
|
idxTrashedLocalAssetAlbum,
|
||||||
|
];
|
||||||
|
late final Shape20 userEntity = Shape20(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_3,
|
||||||
|
_column_84,
|
||||||
|
_column_85,
|
||||||
|
_column_91,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape28 remoteAssetEntity = Shape28(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_1,
|
||||||
|
_column_8,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_10,
|
||||||
|
_column_11,
|
||||||
|
_column_12,
|
||||||
|
_column_0,
|
||||||
|
_column_13,
|
||||||
|
_column_14,
|
||||||
|
_column_15,
|
||||||
|
_column_16,
|
||||||
|
_column_17,
|
||||||
|
_column_18,
|
||||||
|
_column_19,
|
||||||
|
_column_20,
|
||||||
|
_column_21,
|
||||||
|
_column_86,
|
||||||
|
_column_101,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape3 stackEntity = Shape3(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'stack_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [_column_0, _column_9, _column_5, _column_15, _column_75],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape26 localAssetEntity = Shape26(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_1,
|
||||||
|
_column_8,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_10,
|
||||||
|
_column_11,
|
||||||
|
_column_12,
|
||||||
|
_column_0,
|
||||||
|
_column_22,
|
||||||
|
_column_14,
|
||||||
|
_column_23,
|
||||||
|
_column_98,
|
||||||
|
_column_96,
|
||||||
|
_column_46,
|
||||||
|
_column_47,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape9 remoteAlbumEntity = Shape9(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_album_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_56,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_15,
|
||||||
|
_column_57,
|
||||||
|
_column_58,
|
||||||
|
_column_59,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape19 localAlbumEntity = Shape19(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_album_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_5,
|
||||||
|
_column_31,
|
||||||
|
_column_32,
|
||||||
|
_column_90,
|
||||||
|
_column_33,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape22 localAlbumAssetEntity = Shape22(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_album_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||||
|
columns: [_column_34, _column_35, _column_33],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
final i1.Index idxLocalAssetChecksum = i1.Index(
|
||||||
|
'idx_local_asset_checksum',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
|
||||||
|
);
|
||||||
|
final i1.Index idxLocalAssetCloudId = i1.Index(
|
||||||
|
'idx_local_asset_cloud_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetOwnerChecksum = i1.Index(
|
||||||
|
'idx_remote_asset_owner_checksum',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
|
||||||
|
);
|
||||||
|
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
|
||||||
|
'UQ_remote_assets_owner_checksum',
|
||||||
|
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
|
||||||
|
);
|
||||||
|
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
|
||||||
|
'UQ_remote_assets_owner_library_checksum',
|
||||||
|
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetChecksum = i1.Index(
|
||||||
|
'idx_remote_asset_checksum',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
|
||||||
|
);
|
||||||
|
late final Shape21 authUserEntity = Shape21(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'auth_user_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_3,
|
||||||
|
_column_2,
|
||||||
|
_column_84,
|
||||||
|
_column_85,
|
||||||
|
_column_92,
|
||||||
|
_column_93,
|
||||||
|
_column_7,
|
||||||
|
_column_94,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape4 userMetadataEntity = Shape4(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_metadata_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
|
||||||
|
columns: [_column_25, _column_26, _column_27],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape5 partnerEntity = Shape5(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'partner_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
|
||||||
|
columns: [_column_28, _column_29, _column_30],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape8 remoteExifEntity = Shape8(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_exif_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_36,
|
||||||
|
_column_37,
|
||||||
|
_column_38,
|
||||||
|
_column_39,
|
||||||
|
_column_40,
|
||||||
|
_column_41,
|
||||||
|
_column_11,
|
||||||
|
_column_10,
|
||||||
|
_column_42,
|
||||||
|
_column_43,
|
||||||
|
_column_44,
|
||||||
|
_column_45,
|
||||||
|
_column_46,
|
||||||
|
_column_47,
|
||||||
|
_column_48,
|
||||||
|
_column_49,
|
||||||
|
_column_50,
|
||||||
|
_column_51,
|
||||||
|
_column_52,
|
||||||
|
_column_53,
|
||||||
|
_column_54,
|
||||||
|
_column_55,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape7 remoteAlbumAssetEntity = Shape7(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_album_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||||
|
columns: [_column_36, _column_60],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape10 remoteAlbumUserEntity = Shape10(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_album_user_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
|
||||||
|
columns: [_column_60, _column_25, _column_61],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape27 remoteAssetCloudIdEntity = Shape27(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_asset_cloud_id_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_36,
|
||||||
|
_column_99,
|
||||||
|
_column_100,
|
||||||
|
_column_96,
|
||||||
|
_column_46,
|
||||||
|
_column_47,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape11 memoryEntity = Shape11(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'memory_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_18,
|
||||||
|
_column_15,
|
||||||
|
_column_8,
|
||||||
|
_column_62,
|
||||||
|
_column_63,
|
||||||
|
_column_64,
|
||||||
|
_column_65,
|
||||||
|
_column_66,
|
||||||
|
_column_67,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape12 memoryAssetEntity = Shape12(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'memory_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
|
||||||
|
columns: [_column_36, _column_68],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape14 personEntity = Shape14(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'person_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_15,
|
||||||
|
_column_1,
|
||||||
|
_column_69,
|
||||||
|
_column_71,
|
||||||
|
_column_72,
|
||||||
|
_column_73,
|
||||||
|
_column_74,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape15 assetFaceEntity = Shape15(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'asset_face_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_36,
|
||||||
|
_column_76,
|
||||||
|
_column_77,
|
||||||
|
_column_78,
|
||||||
|
_column_79,
|
||||||
|
_column_80,
|
||||||
|
_column_81,
|
||||||
|
_column_82,
|
||||||
|
_column_83,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape18 storeEntity = Shape18(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'store_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [_column_87, _column_88, _column_89],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape25 trashedLocalAssetEntity = Shape25(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'trashed_local_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id, album_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_1,
|
||||||
|
_column_8,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_10,
|
||||||
|
_column_11,
|
||||||
|
_column_12,
|
||||||
|
_column_0,
|
||||||
|
_column_95,
|
||||||
|
_column_22,
|
||||||
|
_column_14,
|
||||||
|
_column_23,
|
||||||
|
_column_97,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
final i1.Index idxLatLng = i1.Index(
|
||||||
|
'idx_lat_lng',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
|
||||||
|
);
|
||||||
|
final i1.Index idxTrashedLocalAssetChecksum = i1.Index(
|
||||||
|
'idx_trashed_local_asset_checksum',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)',
|
||||||
|
);
|
||||||
|
final i1.Index idxTrashedLocalAssetAlbum = i1.Index(
|
||||||
|
'idx_trashed_local_asset_album',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Shape28 extends i0.VersionedTable {
|
||||||
|
Shape28({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get name =>
|
||||||
|
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get type =>
|
||||||
|
columnsByName['type']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<int> get width =>
|
||||||
|
columnsByName['width']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get height =>
|
||||||
|
columnsByName['height']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get durationInSeconds =>
|
||||||
|
columnsByName['duration_in_seconds']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get checksum =>
|
||||||
|
columnsByName['checksum']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<bool> get isFavorite =>
|
||||||
|
columnsByName['is_favorite']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<String> get ownerId =>
|
||||||
|
columnsByName['owner_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get localDateTime =>
|
||||||
|
columnsByName['local_date_time']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<String> get thumbHash =>
|
||||||
|
columnsByName['thumb_hash']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get deletedAt =>
|
||||||
|
columnsByName['deleted_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<String> get livePhotoVideoId =>
|
||||||
|
columnsByName['live_photo_video_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get visibility =>
|
||||||
|
columnsByName['visibility']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get stackId =>
|
||||||
|
columnsByName['stack_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get libraryId =>
|
||||||
|
columnsByName['library_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<bool> get isEdited =>
|
||||||
|
columnsByName['is_edited']! as i1.GeneratedColumn<bool>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<bool> _column_101(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>(
|
||||||
|
'is_edited',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_edited" IN (0, 1))',
|
||||||
|
),
|
||||||
|
defaultValue: const CustomExpression('0'),
|
||||||
|
);
|
||||||
i0.MigrationStepWithVersion migrationSteps({
|
i0.MigrationStepWithVersion migrationSteps({
|
||||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||||
@@ -6927,6 +7424,7 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14,
|
required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14,
|
||||||
required Future<void> Function(i1.Migrator m, Schema15 schema) from14To15,
|
required Future<void> Function(i1.Migrator m, Schema15 schema) from14To15,
|
||||||
required Future<void> Function(i1.Migrator m, Schema16 schema) from15To16,
|
required Future<void> Function(i1.Migrator m, Schema16 schema) from15To16,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema17 schema) from16To17,
|
||||||
}) {
|
}) {
|
||||||
return (currentVersion, database) async {
|
return (currentVersion, database) async {
|
||||||
switch (currentVersion) {
|
switch (currentVersion) {
|
||||||
@@ -7005,6 +7503,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
final migrator = i1.Migrator(database, schema);
|
final migrator = i1.Migrator(database, schema);
|
||||||
await from15To16(migrator, schema);
|
await from15To16(migrator, schema);
|
||||||
return 16;
|
return 16;
|
||||||
|
case 16:
|
||||||
|
final schema = Schema17(database: database);
|
||||||
|
final migrator = i1.Migrator(database, schema);
|
||||||
|
await from16To17(migrator, schema);
|
||||||
|
return 17;
|
||||||
default:
|
default:
|
||||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||||
}
|
}
|
||||||
@@ -7027,6 +7530,7 @@ i1.OnUpgrade stepByStep({
|
|||||||
required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14,
|
required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14,
|
||||||
required Future<void> Function(i1.Migrator m, Schema15 schema) from14To15,
|
required Future<void> Function(i1.Migrator m, Schema15 schema) from14To15,
|
||||||
required Future<void> Function(i1.Migrator m, Schema16 schema) from15To16,
|
required Future<void> Function(i1.Migrator m, Schema16 schema) from15To16,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema17 schema) from16To17,
|
||||||
}) => i0.VersionedSchema.stepByStepHelper(
|
}) => i0.VersionedSchema.stepByStepHelper(
|
||||||
step: migrationSteps(
|
step: migrationSteps(
|
||||||
from1To2: from1To2,
|
from1To2: from1To2,
|
||||||
@@ -7044,5 +7548,6 @@ i1.OnUpgrade stepByStep({
|
|||||||
from13To14: from13To14,
|
from13To14: from13To14,
|
||||||
from14To15: from14To15,
|
from14To15: from14To15,
|
||||||
from15To16: from15To16,
|
from15To16: from15To16,
|
||||||
|
from16To17: from16To17,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
libraryId: Value(asset.libraryId),
|
libraryId: Value(asset.libraryId),
|
||||||
width: Value(asset.width),
|
width: Value(asset.width),
|
||||||
height: Value(asset.height),
|
height: Value(asset.height),
|
||||||
|
isEdited: Value(asset.isEdited),
|
||||||
);
|
);
|
||||||
|
|
||||||
batch.insert(
|
batch.insert(
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
durationInSeconds: row.durationInSeconds,
|
durationInSeconds: row.durationInSeconds,
|
||||||
livePhotoVideoId: row.livePhotoVideoId,
|
livePhotoVideoId: row.livePhotoVideoId,
|
||||||
stackId: row.stackId,
|
stackId: row.stackId,
|
||||||
|
isEdited: row.isEdited,
|
||||||
)
|
)
|
||||||
: LocalAsset(
|
: LocalAsset(
|
||||||
id: row.localId!,
|
id: row.localId!,
|
||||||
@@ -88,6 +89,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
latitude: row.latitude,
|
latitude: row.latitude,
|
||||||
longitude: row.longitude,
|
longitude: row.longitude,
|
||||||
adjustmentTime: row.adjustmentTime,
|
adjustmentTime: row.adjustmentTime,
|
||||||
|
isEdited: row.isEdited,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.get();
|
.get();
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class _MobileLayout extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
return ListView(padding: const EdgeInsets.only(top: 10.0, bottom: 16), children: [...settings]);
|
return ListView(padding: const EdgeInsets.only(top: 10.0, bottom: 60), children: [...settings]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ class SettingsSubPage extends StatelessWidget {
|
|||||||
context.locale;
|
context.locale;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(centerTitle: false, title: Text(section.title).tr()),
|
appBar: AppBar(centerTitle: false, title: Text(section.title).tr()),
|
||||||
body: section.widget,
|
body: Padding(padding: const EdgeInsets.only(bottom: 60.0), child: section.widget),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:immich_mobile/providers/background_sync.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/backup/backup.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/drift_backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/gallery_permission.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/providers/websocket.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
@@ -49,6 +50,7 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
|||||||
final accessToken = Store.tryGet(StoreKey.accessToken);
|
final accessToken = Store.tryGet(StoreKey.accessToken);
|
||||||
|
|
||||||
if (accessToken != null && serverUrl != null && endpoint != null) {
|
if (accessToken != null && serverUrl != null && endpoint != null) {
|
||||||
|
final infoProvider = ref.read(serverInfoProvider.notifier);
|
||||||
final wsProvider = ref.read(websocketProvider.notifier);
|
final wsProvider = ref.read(websocketProvider.notifier);
|
||||||
final backgroundManager = ref.read(backgroundSyncProvider);
|
final backgroundManager = ref.read(backgroundSyncProvider);
|
||||||
final backupProvider = ref.read(driftBackupProvider.notifier);
|
final backupProvider = ref.read(driftBackupProvider.notifier);
|
||||||
@@ -58,6 +60,7 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
|||||||
(_) async {
|
(_) async {
|
||||||
try {
|
try {
|
||||||
wsProvider.connect();
|
wsProvider.connect();
|
||||||
|
unawaited(infoProvider.getServerInfo());
|
||||||
|
|
||||||
if (Store.isBetaTimelineEnabled) {
|
if (Store.isBetaTimelineEnabled) {
|
||||||
bool syncSuccess = false;
|
bool syncSuccess = false;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class SyncStatusPage extends StatelessWidget {
|
|||||||
splashRadius: 24,
|
splashRadius: 24,
|
||||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||||
),
|
),
|
||||||
|
centerTitle: false,
|
||||||
),
|
),
|
||||||
body: const SyncStatusAndActions(),
|
body: const SyncStatusAndActions(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ class _AssetPropertiesSectionState extends ConsumerState<_AssetPropertiesSection
|
|||||||
),
|
),
|
||||||
_PropertyItem(label: 'Is Favorite', value: asset.isFavorite.toString()),
|
_PropertyItem(label: 'Is Favorite', value: asset.isFavorite.toString()),
|
||||||
_PropertyItem(label: 'Live Photo Video ID', value: asset.livePhotoVideoId),
|
_PropertyItem(label: 'Live Photo Video ID', value: asset.livePhotoVideoId),
|
||||||
|
_PropertyItem(label: 'Is Edited', value: asset.isEdited.toString()),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ class _PlaceTile extends StatelessWidget {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 80,
|
width: 80,
|
||||||
height: 80,
|
height: 80,
|
||||||
child: Thumbnail.remote(remoteId: place.$2, fit: BoxFit.cover),
|
child: Thumbnail.remote(remoteId: place.$2, fit: BoxFit.cover, thumbhash: ""),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,14 +14,15 @@ import 'package:immich_mobile/models/albums/album_search.model.dart';
|
|||||||
import 'package:immich_mobile/presentation/widgets/album/album_tile.dart';
|
import 'package:immich_mobile/presentation/widgets/album/album_tile.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/album/new_album_name_modal.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/album/new_album_name_modal.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/album.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/infrastructure/asset_viewer/current_asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.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:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/utils/album_filter.utils.dart';
|
import 'package:immich_mobile/utils/album_filter.utils.dart';
|
||||||
import 'package:immich_mobile/widgets/common/confirm_dialog.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/immich_toast.dart';
|
||||||
@@ -310,18 +311,17 @@ class _SortButtonState extends ConsumerState<_SortButton> {
|
|||||||
: const Icon(Icons.abc, color: Colors.transparent),
|
: const Icon(Icons.abc, color: Colors.transparent),
|
||||||
onPressed: () => onMenuTapped(sortMode),
|
onPressed: () => onMenuTapped(sortMode),
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
padding: WidgetStateProperty.all(const EdgeInsets.fromLTRB(16, 16, 32, 16)),
|
padding: WidgetStateProperty.all(const EdgeInsets.fromLTRB(12, 12, 24, 12)),
|
||||||
backgroundColor: WidgetStateProperty.all(
|
backgroundColor: WidgetStateProperty.all(
|
||||||
albumSortOption == sortMode ? context.colorScheme.primary : Colors.transparent,
|
albumSortOption == sortMode ? context.colorScheme.primary : Colors.transparent,
|
||||||
),
|
),
|
||||||
shape: WidgetStateProperty.all(
|
shape: WidgetStateProperty.all(
|
||||||
const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(24))),
|
const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
sortMode.label.t(context: context),
|
sortMode.label.t(context: context),
|
||||||
style: context.textTheme.titleSmall?.copyWith(
|
style: context.textTheme.labelLarge?.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: albumSortOption == sortMode
|
color: albumSortOption == sortMode
|
||||||
? context.colorScheme.onPrimary
|
? context.colorScheme.onPrimary
|
||||||
: context.colorScheme.onSurface.withAlpha(185),
|
: context.colorScheme.onSurface.withAlpha(185),
|
||||||
@@ -344,15 +344,12 @@ class _SortButtonState extends ConsumerState<_SortButton> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 5),
|
padding: const EdgeInsets.only(right: 5),
|
||||||
child: albumSortIsReverse
|
child: albumSortIsReverse
|
||||||
? const Icon(Icons.keyboard_arrow_down)
|
? Icon(Icons.keyboard_arrow_down, color: context.colorScheme.onSurface)
|
||||||
: const Icon(Icons.keyboard_arrow_up_rounded),
|
: Icon(Icons.keyboard_arrow_up_rounded, color: context.colorScheme.onSurface),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
albumSortOption.label.t(context: context),
|
albumSortOption.label.t(context: context),
|
||||||
style: context.textTheme.bodyLarge?.copyWith(
|
style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurface.withAlpha(225)),
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: context.colorScheme.onSurface.withAlpha(225),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
isSorting
|
isSorting
|
||||||
? SizedBox(
|
? SizedBox(
|
||||||
@@ -542,7 +539,11 @@ class _QuickSortAndViewMode extends StatelessWidget {
|
|||||||
initialIsReverse: currentIsReverse,
|
initialIsReverse: currentIsReverse,
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined, size: 24),
|
icon: Icon(
|
||||||
|
isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined,
|
||||||
|
size: 24,
|
||||||
|
color: context.colorScheme.onSurface,
|
||||||
|
),
|
||||||
onPressed: onToggleViewMode,
|
onPressed: onToggleViewMode,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -662,6 +663,8 @@ class _GridAlbumCard extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final albumThumbnailAsset = ref.read(assetServiceProvider).getRemoteAsset(album.thumbnailAssetId ?? "");
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => onAlbumSelected(album),
|
onTap: () => onAlbumSelected(album),
|
||||||
child: Card(
|
child: Card(
|
||||||
@@ -680,12 +683,22 @@ class _GridAlbumCard extends ConsumerWidget {
|
|||||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(15)),
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(15)),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: album.thumbnailAssetId != null
|
child: FutureBuilder(
|
||||||
? Thumbnail.remote(remoteId: album.thumbnailAssetId!)
|
future: albumThumbnailAsset,
|
||||||
: Container(
|
builder: (context, snapshot) {
|
||||||
color: context.colorScheme.surfaceContainerHighest,
|
if (snapshot.hasData && snapshot.data != null) {
|
||||||
child: const Icon(Icons.photo_album_rounded, size: 40, color: Colors.grey),
|
return Thumbnail.remote(
|
||||||
),
|
remoteId: album.thumbnailAssetId!,
|
||||||
|
thumbhash: snapshot.data!.thumbHash ?? "",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
color: context.colorScheme.surfaceContainerHighest,
|
||||||
|
child: const Icon(Icons.photo_album_rounded, size: 40, color: Colors.grey),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||||
|
|
||||||
class AlbumTile extends StatelessWidget {
|
class AlbumTile extends ConsumerWidget {
|
||||||
const AlbumTile({super.key, required this.album, required this.isOwner, this.onAlbumSelected});
|
const AlbumTile({super.key, required this.album, required this.isOwner, this.onAlbumSelected});
|
||||||
|
|
||||||
final RemoteAlbum album;
|
final RemoteAlbum album;
|
||||||
@@ -14,7 +16,9 @@ class AlbumTile extends StatelessWidget {
|
|||||||
final Function(RemoteAlbum)? onAlbumSelected;
|
final Function(RemoteAlbum)? onAlbumSelected;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final albumThumbnailAsset = ref.read(assetServiceProvider).getRemoteAsset(album.thumbnailAssetId ?? "");
|
||||||
|
|
||||||
return LargeLeadingTile(
|
return LargeLeadingTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
album.name,
|
album.name,
|
||||||
@@ -29,23 +33,35 @@ class AlbumTile extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
onTap: () => onAlbumSelected?.call(album),
|
onTap: () => onAlbumSelected?.call(album),
|
||||||
leadingPadding: const EdgeInsets.only(right: 16),
|
leadingPadding: const EdgeInsets.only(right: 16),
|
||||||
leading: album.thumbnailAssetId != null
|
leading: FutureBuilder(
|
||||||
? ClipRRect(
|
future: albumThumbnailAsset,
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
builder: (context, snapshot) {
|
||||||
child: SizedBox(width: 80, height: 80, child: Thumbnail.remote(remoteId: album.thumbnailAssetId!)),
|
return snapshot.hasData && snapshot.data != null
|
||||||
)
|
? ClipRRect(
|
||||||
: SizedBox(
|
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
||||||
width: 80,
|
child: SizedBox(
|
||||||
height: 80,
|
width: 80,
|
||||||
child: Container(
|
height: 80,
|
||||||
decoration: BoxDecoration(
|
child: Thumbnail.remote(
|
||||||
color: context.colorScheme.surfaceContainer,
|
remoteId: album.thumbnailAssetId!,
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
thumbhash: snapshot.data!.thumbHash ?? "",
|
||||||
border: Border.all(color: context.colorScheme.outline.withAlpha(50), width: 1),
|
),
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.photo_album_rounded, size: 24, color: Colors.grey),
|
)
|
||||||
),
|
: SizedBox(
|
||||||
),
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.colorScheme.surfaceContainer,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||||
|
border: Border.all(color: context.colorScheme.outline.withAlpha(50), width: 1),
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.photo_album_rounded, size: 24, color: Colors.grey),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
|||||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/duration_extensions.dart';
|
import 'package:immich_mobile/extensions/duration_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/album/album_tile.dart';
|
import 'package:immich_mobile/presentation/widgets/album/album_tile.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||||
@@ -164,11 +165,8 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
if (albums.isNotEmpty)
|
if (albums.isNotEmpty)
|
||||||
SheetTile(
|
SheetTile(
|
||||||
title: 'appears_in'.t(context: context).toUpperCase(),
|
title: 'appears_in'.t(context: context),
|
||||||
titleStyle: context.textTheme.labelMedium?.copyWith(
|
titleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 24),
|
padding: const EdgeInsets.only(left: 24),
|
||||||
@@ -224,9 +222,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
color: context.textTheme.labelLarge?.color,
|
color: context.textTheme.labelLarge?.color,
|
||||||
),
|
),
|
||||||
subtitle: _getFileInfo(asset, exifInfo),
|
subtitle: _getFileInfo(asset, exifInfo),
|
||||||
subtitleStyle: context.textTheme.labelMedium?.copyWith(
|
subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -241,9 +237,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
color: context.textTheme.labelLarge?.color,
|
color: context.textTheme.labelLarge?.color,
|
||||||
),
|
),
|
||||||
subtitle: _getFileInfo(asset, exifInfo),
|
subtitle: _getFileInfo(asset, exifInfo),
|
||||||
subtitleStyle: context.textTheme.labelMedium?.copyWith(
|
subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,11 +256,8 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
const SheetLocationDetails(),
|
const SheetLocationDetails(),
|
||||||
// Details header
|
// Details header
|
||||||
SheetTile(
|
SheetTile(
|
||||||
title: 'details'.t(context: context).toUpperCase(),
|
title: 'details'.t(context: context),
|
||||||
titleStyle: context.textTheme.labelMedium?.copyWith(
|
titleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
// File info
|
// File info
|
||||||
buildFileInfoTile(),
|
buildFileInfoTile(),
|
||||||
@@ -278,9 +269,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
titleStyle: context.textTheme.labelLarge,
|
titleStyle: context.textTheme.labelLarge,
|
||||||
leading: Icon(Icons.camera_alt_outlined, size: 24, color: context.textTheme.labelLarge?.color),
|
leading: Icon(Icons.camera_alt_outlined, size: 24, color: context.textTheme.labelLarge?.color),
|
||||||
subtitle: _getCameraInfoSubtitle(exifInfo),
|
subtitle: _getCameraInfoSubtitle(exifInfo),
|
||||||
subtitleStyle: context.textTheme.labelMedium?.copyWith(
|
subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
// Lens info
|
// Lens info
|
||||||
@@ -291,15 +280,13 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
titleStyle: context.textTheme.labelLarge,
|
titleStyle: context.textTheme.labelLarge,
|
||||||
leading: Icon(Icons.camera_outlined, size: 24, color: context.textTheme.labelLarge?.color),
|
leading: Icon(Icons.camera_outlined, size: 24, color: context.textTheme.labelLarge?.color),
|
||||||
subtitle: _getLensInfoSubtitle(exifInfo),
|
subtitle: _getLensInfoSubtitle(exifInfo),
|
||||||
subtitleStyle: context.textTheme.labelMedium?.copyWith(
|
subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
// Appears in (Albums)
|
// Appears in (Albums)
|
||||||
Padding(padding: const EdgeInsets.only(top: 16.0), child: _buildAppearsInList(ref, context)),
|
Padding(padding: const EdgeInsets.only(top: 16.0), child: _buildAppearsInList(ref, context)),
|
||||||
// padding at the bottom to avoid cut-off
|
// padding at the bottom to avoid cut-off
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 60),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-8
@@ -4,6 +4,7 @@ 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/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.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/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
@@ -77,11 +78,8 @@ class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
SheetTile(
|
SheetTile(
|
||||||
title: 'location'.t(context: context).toUpperCase(),
|
title: 'location'.t(context: context),
|
||||||
titleStyle: context.textTheme.labelMedium?.copyWith(
|
titleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
trailing: hasCoordinates ? const Icon(Icons.edit_location_alt, size: 20) : null,
|
trailing: hasCoordinates ? const Icon(Icons.edit_location_alt, size: 20) : null,
|
||||||
onTap: editLocation,
|
onTap: editLocation,
|
||||||
),
|
),
|
||||||
@@ -105,9 +103,7 @@ class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
|
|||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
coordinates,
|
coordinates,
|
||||||
style: context.textTheme.labelMedium?.copyWith(
|
style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
+3
-5
@@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/person.model.dart';
|
import 'package:immich_mobile/domain/models/person.model.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.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/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/people/person_edit_name_modal.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/people/person_edit_name_modal.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
@@ -53,11 +54,8 @@ class _SheetPeopleDetailsState extends ConsumerState<SheetPeopleDetails> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 16, top: 16, bottom: 16),
|
padding: const EdgeInsets.only(left: 16, top: 16, bottom: 16),
|
||||||
child: Text(
|
child: Text(
|
||||||
"people".t(context: context).toUpperCase(),
|
"people".t(context: context),
|
||||||
style: context.textTheme.labelMedium?.copyWith(
|
style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
|
|||||||
@@ -112,14 +112,17 @@ ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080
|
|||||||
provider = LocalFullImageProvider(id: id, size: size, assetType: asset.type);
|
provider = LocalFullImageProvider(id: id, size: size, assetType: asset.type);
|
||||||
} else {
|
} else {
|
||||||
final String assetId;
|
final String assetId;
|
||||||
|
final String thumbhash;
|
||||||
if (asset is LocalAsset && asset.hasRemote) {
|
if (asset is LocalAsset && asset.hasRemote) {
|
||||||
assetId = asset.remoteId!;
|
assetId = asset.remoteId!;
|
||||||
|
thumbhash = "";
|
||||||
} else if (asset is RemoteAsset) {
|
} else if (asset is RemoteAsset) {
|
||||||
assetId = asset.id;
|
assetId = asset.id;
|
||||||
|
thumbhash = asset.thumbHash ?? "";
|
||||||
} else {
|
} else {
|
||||||
throw ArgumentError("Unsupported asset type: ${asset.runtimeType}");
|
throw ArgumentError("Unsupported asset type: ${asset.runtimeType}");
|
||||||
}
|
}
|
||||||
provider = RemoteFullImageProvider(assetId: assetId);
|
provider = RemoteFullImageProvider(assetId: assetId, thumbhash: thumbhash);
|
||||||
}
|
}
|
||||||
|
|
||||||
return provider;
|
return provider;
|
||||||
@@ -132,8 +135,9 @@ ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnai
|
|||||||
}
|
}
|
||||||
|
|
||||||
final assetId = asset is RemoteAsset ? asset.id : (asset as LocalAsset).remoteId;
|
final assetId = asset is RemoteAsset ? asset.id : (asset as LocalAsset).remoteId;
|
||||||
return assetId != null ? RemoteThumbProvider(assetId: assetId) : null;
|
final thumbhash = asset is RemoteAsset ? asset.thumbHash ?? "" : "";
|
||||||
|
return assetId != null ? RemoteThumbProvider(assetId: assetId, thumbhash: thumbhash) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _shouldUseLocalAsset(BaseAsset asset) =>
|
bool _shouldUseLocalAsset(BaseAsset asset) =>
|
||||||
asset.hasLocal && (!asset.hasRemote || !AppSetting.get(Setting.preferRemoteImage));
|
asset.hasLocal && (!asset.hasRemote || !AppSetting.get(Setting.preferRemoteImage)) && !asset.isEdited;
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ class RemoteThumbProvider extends CancellableImageProvider<RemoteThumbProvider>
|
|||||||
with CancellableImageProviderMixin<RemoteThumbProvider> {
|
with CancellableImageProviderMixin<RemoteThumbProvider> {
|
||||||
static final cacheManager = RemoteThumbnailCacheManager();
|
static final cacheManager = RemoteThumbnailCacheManager();
|
||||||
final String assetId;
|
final String assetId;
|
||||||
|
final String thumbhash;
|
||||||
|
|
||||||
RemoteThumbProvider({required this.assetId});
|
RemoteThumbProvider({required this.assetId, required this.thumbhash});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<RemoteThumbProvider> obtainKey(ImageConfiguration configuration) {
|
Future<RemoteThumbProvider> obtainKey(ImageConfiguration configuration) {
|
||||||
@@ -38,7 +39,7 @@ class RemoteThumbProvider extends CancellableImageProvider<RemoteThumbProvider>
|
|||||||
|
|
||||||
Stream<ImageInfo> _codec(RemoteThumbProvider key, ImageDecoderCallback decode) {
|
Stream<ImageInfo> _codec(RemoteThumbProvider key, ImageDecoderCallback decode) {
|
||||||
final request = this.request = RemoteImageRequest(
|
final request = this.request = RemoteImageRequest(
|
||||||
uri: getThumbnailUrlForRemoteId(key.assetId),
|
uri: getThumbnailUrlForRemoteId(key.assetId, thumbhash: key.thumbhash),
|
||||||
headers: ApiService.getRequestHeaders(),
|
headers: ApiService.getRequestHeaders(),
|
||||||
cacheManager: cacheManager,
|
cacheManager: cacheManager,
|
||||||
);
|
);
|
||||||
@@ -49,22 +50,23 @@ class RemoteThumbProvider extends CancellableImageProvider<RemoteThumbProvider>
|
|||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
if (other is RemoteThumbProvider) {
|
if (other is RemoteThumbProvider) {
|
||||||
return assetId == other.assetId;
|
return assetId == other.assetId && thumbhash == other.thumbhash;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => assetId.hashCode;
|
int get hashCode => assetId.hashCode ^ thumbhash.hashCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImageProvider>
|
class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImageProvider>
|
||||||
with CancellableImageProviderMixin<RemoteFullImageProvider> {
|
with CancellableImageProviderMixin<RemoteFullImageProvider> {
|
||||||
static final cacheManager = RemoteThumbnailCacheManager();
|
static final cacheManager = RemoteThumbnailCacheManager();
|
||||||
final String assetId;
|
final String assetId;
|
||||||
|
final String thumbhash;
|
||||||
|
|
||||||
RemoteFullImageProvider({required this.assetId});
|
RemoteFullImageProvider({required this.assetId, required this.thumbhash});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<RemoteFullImageProvider> obtainKey(ImageConfiguration configuration) {
|
Future<RemoteFullImageProvider> obtainKey(ImageConfiguration configuration) {
|
||||||
@@ -75,7 +77,7 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
|
|||||||
ImageStreamCompleter loadImage(RemoteFullImageProvider key, ImageDecoderCallback decode) {
|
ImageStreamCompleter loadImage(RemoteFullImageProvider key, ImageDecoderCallback decode) {
|
||||||
return OneFramePlaceholderImageStreamCompleter(
|
return OneFramePlaceholderImageStreamCompleter(
|
||||||
_codec(key, decode),
|
_codec(key, decode),
|
||||||
initialImage: getInitialImage(RemoteThumbProvider(assetId: key.assetId)),
|
initialImage: getInitialImage(RemoteThumbProvider(assetId: key.assetId, thumbhash: key.thumbhash)),
|
||||||
informationCollector: () => <DiagnosticsNode>[
|
informationCollector: () => <DiagnosticsNode>[
|
||||||
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
||||||
DiagnosticsProperty<String>('Asset Id', key.assetId),
|
DiagnosticsProperty<String>('Asset Id', key.assetId),
|
||||||
@@ -94,7 +96,7 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
|
|||||||
|
|
||||||
final headers = ApiService.getRequestHeaders();
|
final headers = ApiService.getRequestHeaders();
|
||||||
final request = this.request = RemoteImageRequest(
|
final request = this.request = RemoteImageRequest(
|
||||||
uri: getThumbnailUrlForRemoteId(key.assetId, type: AssetMediaSize.preview),
|
uri: getThumbnailUrlForRemoteId(key.assetId, type: AssetMediaSize.preview, thumbhash: key.thumbhash),
|
||||||
headers: headers,
|
headers: headers,
|
||||||
cacheManager: cacheManager,
|
cacheManager: cacheManager,
|
||||||
);
|
);
|
||||||
@@ -115,12 +117,12 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
|
|||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
if (other is RemoteFullImageProvider) {
|
if (other is RemoteFullImageProvider) {
|
||||||
return assetId == other.assetId;
|
return assetId == other.assetId && thumbhash == other.thumbhash;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => assetId.hashCode;
|
int get hashCode => assetId.hashCode ^ thumbhash.hashCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,14 @@ class Thumbnail extends StatefulWidget {
|
|||||||
|
|
||||||
const Thumbnail({this.imageProvider, this.fit = BoxFit.cover, this.thumbhashProvider, super.key});
|
const Thumbnail({this.imageProvider, this.fit = BoxFit.cover, this.thumbhashProvider, super.key});
|
||||||
|
|
||||||
Thumbnail.remote({required String remoteId, this.fit = BoxFit.cover, Size size = kThumbnailResolution, super.key})
|
Thumbnail.remote({
|
||||||
: imageProvider = RemoteThumbProvider(assetId: remoteId),
|
required String remoteId,
|
||||||
thumbhashProvider = null;
|
required String thumbhash,
|
||||||
|
this.fit = BoxFit.cover,
|
||||||
|
Size size = kThumbnailResolution,
|
||||||
|
super.key,
|
||||||
|
}) : imageProvider = RemoteThumbProvider(assetId: remoteId, thumbhash: thumbhash),
|
||||||
|
thumbhashProvider = null;
|
||||||
|
|
||||||
Thumbnail.fromAsset({
|
Thumbnail.fromAsset({
|
||||||
required BaseAsset? asset,
|
required BaseAsset? asset,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:immich_mobile/domain/models/setting.model.dart';
|
|||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/duration_extensions.dart';
|
import 'package:immich_mobile/extensions/duration_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
||||||
import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart';
|
import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart';
|
||||||
@@ -47,6 +48,7 @@ class _ThumbnailTileState extends ConsumerState<ThumbnailTile> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final asset = widget.asset;
|
final asset = widget.asset;
|
||||||
final heroIndex = widget.heroOffset ?? TabsRouterScope.of(context)?.controller.activeIndex ?? 0;
|
final heroIndex = widget.heroOffset ?? TabsRouterScope.of(context)?.controller.activeIndex ?? 0;
|
||||||
|
final isCurrentAsset = ref.watch(assetViewerProvider.select((current) => current.currentAsset == asset));
|
||||||
|
|
||||||
final assetContainerColor = context.isDarkTheme
|
final assetContainerColor = context.isDarkTheme
|
||||||
? context.primaryColor.darken(amount: 0.4)
|
? context.primaryColor.darken(amount: 0.4)
|
||||||
@@ -59,6 +61,10 @@ class _ThumbnailTileState extends ConsumerState<ThumbnailTile> {
|
|||||||
final bool storageIndicator =
|
final bool storageIndicator =
|
||||||
ref.watch(settingsProvider.select((s) => s.get(Setting.showStorageIndicator))) && widget.showStorageIndicator;
|
ref.watch(settingsProvider.select((s) => s.get(Setting.showStorageIndicator))) && widget.showStorageIndicator;
|
||||||
|
|
||||||
|
if (!isCurrentAsset) {
|
||||||
|
_hideIndicators = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
_showSelectionContainer = true;
|
_showSelectionContainer = true;
|
||||||
}
|
}
|
||||||
@@ -96,7 +102,11 @@ class _ThumbnailTileState extends ConsumerState<ThumbnailTile> {
|
|||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Hero(
|
child: Hero(
|
||||||
tag: '${asset?.heroTag ?? ''}_$heroIndex',
|
// This key resets the hero animation when the asset is changed in the asset viewer.
|
||||||
|
// It doesn't seem like the best solution, and only works to reset the hero, not prime the hero of the new active asset for animation,
|
||||||
|
// but other solutions have failed thus far.
|
||||||
|
key: ValueKey(isCurrentAsset),
|
||||||
|
tag: '${asset?.heroTag}_$heroIndex',
|
||||||
child: Thumbnail.fromAsset(asset: asset, size: widget.size),
|
child: Thumbnail.fromAsset(asset: asset, size: widget.size),
|
||||||
// Placeholderbuilder used to hide indicators on first hero animation, since flightShuttleBuilder isn't called until both source and destination hero exist in widget tree.
|
// Placeholderbuilder used to hide indicators on first hero animation, since flightShuttleBuilder isn't called until both source and destination hero exist in widget tree.
|
||||||
placeholderBuilder: (context, heroSize, child) {
|
placeholderBuilder: (context, heroSize, child) {
|
||||||
|
|||||||
@@ -60,7 +60,11 @@ class DriftMemoryCard extends ConsumerWidget {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 205,
|
width: 205,
|
||||||
height: 200,
|
height: 200,
|
||||||
child: Thumbnail.remote(remoteId: memory.assets[0].id, fit: BoxFit.cover),
|
child: Thumbnail.remote(
|
||||||
|
remoteId: memory.assets[0].id,
|
||||||
|
thumbhash: memory.assets[0].thumbHash ?? "",
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
|||||||
_resumeBackup();
|
_resumeBackup();
|
||||||
}),
|
}),
|
||||||
_resumeBackup(),
|
_resumeBackup(),
|
||||||
backgroundManager.syncCloudIds(),
|
_safeRun(backgroundManager.syncCloudIds(), "syncCloudIds"),
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
await _safeRun(backgroundManager.hashAssets(), "hashAssets");
|
await _safeRun(backgroundManager.hashAssets(), "hashAssets");
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ class CastNotifier extends StateNotifier<CastManagerState> {
|
|||||||
: AssetType.other,
|
: AssetType.other,
|
||||||
createdAt: asset.fileCreatedAt,
|
createdAt: asset.fileCreatedAt,
|
||||||
updatedAt: asset.updatedAt,
|
updatedAt: asset.updatedAt,
|
||||||
|
isEdited: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
_gCastService.loadMedia(remoteAsset, reload);
|
_gCastService.loadMedia(remoteAsset, reload);
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
|||||||
socket.on('on_asset_hidden', _handleOnAssetHidden);
|
socket.on('on_asset_hidden', _handleOnAssetHidden);
|
||||||
} else {
|
} else {
|
||||||
socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReady);
|
socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReady);
|
||||||
|
socket.on('AssetEditReadyV1', _handleSyncAssetEditReady);
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.on('on_config_update', _handleOnConfigUpdate);
|
socket.on('on_config_update', _handleOnConfigUpdate);
|
||||||
@@ -192,10 +193,12 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
|||||||
|
|
||||||
void stopListeningToBetaEvents() {
|
void stopListeningToBetaEvents() {
|
||||||
state.socket?.off('AssetUploadReadyV1');
|
state.socket?.off('AssetUploadReadyV1');
|
||||||
|
state.socket?.off('AssetEditReadyV1');
|
||||||
}
|
}
|
||||||
|
|
||||||
void startListeningToBetaEvents() {
|
void startListeningToBetaEvents() {
|
||||||
state.socket?.on('AssetUploadReadyV1', _handleSyncAssetUploadReady);
|
state.socket?.on('AssetUploadReadyV1', _handleSyncAssetUploadReady);
|
||||||
|
state.socket?.on('AssetEditReadyV1', _handleSyncAssetEditReady);
|
||||||
}
|
}
|
||||||
|
|
||||||
void listenUploadEvent() {
|
void listenUploadEvent() {
|
||||||
@@ -315,6 +318,10 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
|||||||
_batchDebouncer.run(_processBatchedAssetUploadReady);
|
_batchDebouncer.run(_processBatchedAssetUploadReady);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleSyncAssetEditReady(dynamic data) {
|
||||||
|
unawaited(_ref.read(backgroundSyncProvider).syncWebsocketEditBatch([data]));
|
||||||
|
}
|
||||||
|
|
||||||
void _processBatchedAssetUploadReady() {
|
void _processBatchedAssetUploadReady() {
|
||||||
if (_batchedAssetUploadReady.isEmpty) {
|
if (_batchedAssetUploadReady.isEmpty) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class FileMediaRepository {
|
|||||||
type: AssetType.image,
|
type: AssetType.image,
|
||||||
createdAt: entity.createDateTime,
|
createdAt: entity.createDateTime,
|
||||||
updatedAt: entity.modifiedDateTime,
|
updatedAt: entity.modifiedDateTime,
|
||||||
|
isEdited: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -307,10 +307,10 @@ class BackgroundUploadService {
|
|||||||
priority: priority,
|
priority: priority,
|
||||||
isFavorite: asset.isFavorite,
|
isFavorite: asset.isFavorite,
|
||||||
requiresWiFi: requiresWiFi,
|
requiresWiFi: requiresWiFi,
|
||||||
cloudId: asset.cloudId,
|
cloudId: entity.isLivePhoto ? null : asset.cloudId,
|
||||||
adjustmentTime: asset.adjustmentTime?.toIso8601String(),
|
adjustmentTime: entity.isLivePhoto ? null : asset.adjustmentTime?.toIso8601String(),
|
||||||
latitude: asset.latitude?.toString(),
|
latitude: entity.isLivePhoto ? null : asset.latitude?.toString(),
|
||||||
longitude: asset.longitude?.toString(),
|
longitude: entity.isLivePhoto ? null : asset.longitude?.toString(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import 'package:immich_mobile/platform/connectivity_api.g.dart';
|
|||||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
|
||||||
|
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/upload.repository.dart';
|
import 'package:immich_mobile/repositories/upload.repository.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
@@ -40,6 +41,7 @@ final foregroundUploadServiceProvider = Provider((ref) {
|
|||||||
ref.watch(backupRepositoryProvider),
|
ref.watch(backupRepositoryProvider),
|
||||||
ref.watch(connectivityApiProvider),
|
ref.watch(connectivityApiProvider),
|
||||||
ref.watch(appSettingsServiceProvider),
|
ref.watch(appSettingsServiceProvider),
|
||||||
|
ref.watch(assetMediaRepositoryProvider),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -55,6 +57,7 @@ class ForegroundUploadService {
|
|||||||
this._backupRepository,
|
this._backupRepository,
|
||||||
this._connectivityApi,
|
this._connectivityApi,
|
||||||
this._appSettingsService,
|
this._appSettingsService,
|
||||||
|
this._assetMediaRepository,
|
||||||
);
|
);
|
||||||
|
|
||||||
final UploadRepository _uploadRepository;
|
final UploadRepository _uploadRepository;
|
||||||
@@ -62,6 +65,7 @@ class ForegroundUploadService {
|
|||||||
final DriftBackupRepository _backupRepository;
|
final DriftBackupRepository _backupRepository;
|
||||||
final ConnectivityApi _connectivityApi;
|
final ConnectivityApi _connectivityApi;
|
||||||
final AppSettingsService _appSettingsService;
|
final AppSettingsService _appSettingsService;
|
||||||
|
final AssetMediaRepository _assetMediaRepository;
|
||||||
final Logger _logger = Logger('ForegroundUploadService');
|
final Logger _logger = Logger('ForegroundUploadService');
|
||||||
|
|
||||||
bool shouldAbortUpload = false;
|
bool shouldAbortUpload = false;
|
||||||
@@ -311,7 +315,8 @@ class ForegroundUploadService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final originalFileName = entity.isLivePhoto ? p.setExtension(asset.name, p.extension(file.path)) : asset.name;
|
final fileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name;
|
||||||
|
final originalFileName = entity.isLivePhoto ? p.setExtension(fileName, p.extension(file.path)) : fileName;
|
||||||
final deviceId = Store.get(StoreKey.deviceId);
|
final deviceId = Store.get(StoreKey.deviceId);
|
||||||
|
|
||||||
final headers = ApiService.getRequestHeaders();
|
final headers = ApiService.getRequestHeaders();
|
||||||
@@ -322,19 +327,6 @@ class ForegroundUploadService {
|
|||||||
'fileModifiedAt': asset.updatedAt.toUtc().toIso8601String(),
|
'fileModifiedAt': asset.updatedAt.toUtc().toIso8601String(),
|
||||||
'isFavorite': asset.isFavorite.toString(),
|
'isFavorite': asset.isFavorite.toString(),
|
||||||
'duration': asset.duration.toString(),
|
'duration': asset.duration.toString(),
|
||||||
if (CurrentPlatform.isIOS && asset.cloudId != null)
|
|
||||||
'metadata': jsonEncode([
|
|
||||||
RemoteAssetMetadataItem(
|
|
||||||
key: RemoteAssetMetadataKey.mobileApp,
|
|
||||||
value: RemoteAssetMobileAppMetadata(
|
|
||||||
cloudId: asset.cloudId,
|
|
||||||
createdAt: asset.createdAt.toIso8601String(),
|
|
||||||
adjustmentTime: asset.adjustmentTime?.toIso8601String(),
|
|
||||||
latitude: asset.latitude?.toString(),
|
|
||||||
longitude: asset.longitude?.toString(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Upload live photo video first if available
|
// Upload live photo video first if available
|
||||||
@@ -363,6 +355,22 @@ class ForegroundUploadService {
|
|||||||
fields['livePhotoVideoId'] = livePhotoVideoId;
|
fields['livePhotoVideoId'] = livePhotoVideoId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add cloudId metadata only to the still image, not the motion video, becasue when the sync id happens, the motion video can get associated with the wrong still image.
|
||||||
|
if (CurrentPlatform.isIOS && asset.cloudId != null) {
|
||||||
|
fields['metadata'] = jsonEncode([
|
||||||
|
RemoteAssetMetadataItem(
|
||||||
|
key: RemoteAssetMetadataKey.mobileApp,
|
||||||
|
value: RemoteAssetMobileAppMetadata(
|
||||||
|
cloudId: asset.cloudId,
|
||||||
|
createdAt: asset.createdAt.toIso8601String(),
|
||||||
|
adjustmentTime: asset.adjustmentTime?.toIso8601String(),
|
||||||
|
latitude: asset.latitude?.toString(),
|
||||||
|
longitude: asset.longitude?.toString(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
final result = await _uploadRepository.uploadFile(
|
final result = await _uploadRepository.uploadFile(
|
||||||
file: file,
|
file: file,
|
||||||
originalFileName: originalFileName,
|
originalFileName: originalFileName,
|
||||||
|
|||||||
@@ -61,7 +61,12 @@ ThemeData getThemeData({required ColorScheme colorScheme, required Locale locale
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
chipTheme: const ChipThemeData(side: BorderSide.none),
|
chipTheme: const ChipThemeData(side: BorderSide.none),
|
||||||
sliderTheme: const SliderThemeData(thumbShape: RoundSliderThumbShape(enabledThumbRadius: 7), trackHeight: 2.0),
|
sliderTheme: const SliderThemeData(
|
||||||
|
thumbShape: RoundSliderThumbShape(enabledThumbRadius: 7),
|
||||||
|
trackHeight: 2.0,
|
||||||
|
// ignore: deprecated_member_use
|
||||||
|
year2023: false,
|
||||||
|
),
|
||||||
bottomNavigationBarTheme: const BottomNavigationBarThemeData(type: BottomNavigationBarType.fixed),
|
bottomNavigationBarTheme: const BottomNavigationBarThemeData(type: BottomNavigationBarType.fixed),
|
||||||
popupMenuTheme: const PopupMenuThemeData(
|
popupMenuTheme: const PopupMenuThemeData(
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
|
||||||
|
|||||||
@@ -50,8 +50,10 @@ String getThumbnailUrlForRemoteId(
|
|||||||
final String id, {
|
final String id, {
|
||||||
AssetMediaSize type = AssetMediaSize.thumbnail,
|
AssetMediaSize type = AssetMediaSize.thumbnail,
|
||||||
bool edited = true,
|
bool edited = true,
|
||||||
|
String? thumbhash,
|
||||||
}) {
|
}) {
|
||||||
return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=${type.value}&edited=$edited';
|
final url = '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=${type.value}&edited=$edited';
|
||||||
|
return thumbhash != null ? '$url&c=${Uri.encodeComponent(thumbhash)}' : url;
|
||||||
}
|
}
|
||||||
|
|
||||||
String getPlaybackUrlForRemoteId(final String id) {
|
String getPlaybackUrlForRemoteId(final String id) {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ dynamic upgradeDto(dynamic value, String targetType) {
|
|||||||
if (value is Map) {
|
if (value is Map) {
|
||||||
addDefault(value, 'visibility', 'timeline');
|
addDefault(value, 'visibility', 'timeline');
|
||||||
addDefault(value, 'createdAt', DateTime.now().toIso8601String());
|
addDefault(value, 'createdAt', DateTime.now().toIso8601String());
|
||||||
|
addDefault(value, 'isEdited', false);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'UserAdminResponseDto':
|
case 'UserAdminResponseDto':
|
||||||
@@ -46,6 +47,10 @@ dynamic upgradeDto(dynamic value, String targetType) {
|
|||||||
addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String());
|
addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String());
|
||||||
addDefault(value, 'hasProfileImage', false);
|
addDefault(value, 'hasProfileImage', false);
|
||||||
}
|
}
|
||||||
|
case 'SyncAssetV1':
|
||||||
|
if (value is Map) {
|
||||||
|
addDefault(value, 'isEdited', false);
|
||||||
|
}
|
||||||
case 'ServerFeaturesDto':
|
case 'ServerFeaturesDto':
|
||||||
if (value is Map) {
|
if (value is Map) {
|
||||||
addDefault(value, 'ocr', false);
|
addDefault(value, 'ocr', false);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class PersonNameEditForm extends HookConsumerWidget {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'name'.tr(),
|
hintText: 'name'.tr(),
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
errorText: isError.value ? 'Error occured' : null,
|
errorText: isError.value ? 'Error occurred' : null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.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/utils/hooks/app_settings_update_hook.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.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';
|
import 'package:immich_mobile/widgets/settings/settings_radio_list_tile.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/settings_sub_title.dart';
|
|
||||||
|
|
||||||
class GroupSettings extends HookConsumerWidget {
|
class GroupSettings extends HookConsumerWidget {
|
||||||
const GroupSettings({super.key});
|
const GroupSettings({super.key});
|
||||||
@@ -33,12 +33,24 @@ class GroupSettings extends HookConsumerWidget {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
SettingsSubTitle(title: "asset_list_group_by_sub_title".tr()),
|
SettingGroupTitle(
|
||||||
|
title: "asset_list_group_by_sub_title".t(context: context),
|
||||||
|
icon: Icons.group_work_outlined,
|
||||||
|
),
|
||||||
SettingsRadioListTile(
|
SettingsRadioListTile(
|
||||||
groups: [
|
groups: [
|
||||||
SettingsRadioGroup(title: 'asset_list_layout_settings_group_by_month_day'.tr(), value: GroupAssetsBy.day),
|
SettingsRadioGroup(
|
||||||
SettingsRadioGroup(title: 'month'.tr(), value: GroupAssetsBy.month),
|
title: 'asset_list_layout_settings_group_by_month_day'.t(context: context),
|
||||||
SettingsRadioGroup(title: 'asset_list_layout_settings_group_automatically'.tr(), value: GroupAssetsBy.auto),
|
value: GroupAssetsBy.day,
|
||||||
|
),
|
||||||
|
SettingsRadioGroup(
|
||||||
|
title: 'month'.t(context: context),
|
||||||
|
value: GroupAssetsBy.month,
|
||||||
|
),
|
||||||
|
SettingsRadioGroup(
|
||||||
|
title: 'asset_list_layout_settings_group_automatically'.t(context: context),
|
||||||
|
value: GroupAssetsBy.auto,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
groupBy: groupBy,
|
groupBy: groupBy,
|
||||||
onRadioChanged: changeGroupValue,
|
onRadioChanged: changeGroupValue,
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.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/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_slider_list_tile.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/settings_sub_title.dart';
|
|
||||||
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
||||||
|
|
||||||
class LayoutSettings extends HookConsumerWidget {
|
class LayoutSettings extends HookConsumerWidget {
|
||||||
@@ -19,10 +20,13 @@ class LayoutSettings extends HookConsumerWidget {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
SettingsSubTitle(title: "asset_list_layout_sub_title".tr()),
|
SettingGroupTitle(
|
||||||
|
title: "asset_list_layout_sub_title".t(context: context),
|
||||||
|
icon: Icons.view_module_outlined,
|
||||||
|
),
|
||||||
SettingsSwitchListTile(
|
SettingsSwitchListTile(
|
||||||
valueNotifier: useDynamicLayout,
|
valueNotifier: useDynamicLayout,
|
||||||
title: "asset_list_layout_settings_dynamic_layout_title".tr(),
|
title: "asset_list_layout_settings_dynamic_layout_title".t(context: context),
|
||||||
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
||||||
),
|
),
|
||||||
SettingsSliderListTile(
|
SettingsSliderListTile(
|
||||||
|
|||||||
+10
-11
@@ -1,10 +1,9 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/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/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/settings_sub_title.dart';
|
import 'package:immich_mobile/widgets/settings/setting_group_title.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
||||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||||
|
|
||||||
@@ -19,21 +18,21 @@ class ImageViewerQualitySetting extends HookConsumerWidget {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
SettingsSubTitle(title: "setting_image_viewer_title".tr()),
|
SettingGroupTitle(
|
||||||
ListTile(
|
title: "photos".t(context: context),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
icon: Icons.image_outlined,
|
||||||
title: Text('setting_image_viewer_help', style: context.textTheme.bodyMedium).tr(),
|
subtitle: "setting_image_viewer_help".t(context: context),
|
||||||
),
|
),
|
||||||
SettingsSwitchListTile(
|
SettingsSwitchListTile(
|
||||||
valueNotifier: isPreview,
|
valueNotifier: isPreview,
|
||||||
title: "setting_image_viewer_preview_title".tr(),
|
title: "setting_image_viewer_preview_title".t(context: context),
|
||||||
subtitle: "setting_image_viewer_preview_subtitle".tr(),
|
subtitle: "setting_image_viewer_preview_subtitle".t(context: context),
|
||||||
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
||||||
),
|
),
|
||||||
SettingsSwitchListTile(
|
SettingsSwitchListTile(
|
||||||
valueNotifier: isOriginal,
|
valueNotifier: isOriginal,
|
||||||
title: "setting_image_viewer_original_title".tr(),
|
title: "setting_image_viewer_original_title".t(context: context),
|
||||||
subtitle: "setting_image_viewer_original_subtitle".tr(),
|
subtitle: "setting_image_viewer_original_subtitle".t(context: context),
|
||||||
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/settings_sub_title.dart';
|
import 'package:immich_mobile/widgets/settings/setting_group_title.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
||||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||||
|
|
||||||
@@ -19,23 +19,26 @@ class VideoViewerSettings extends HookConsumerWidget {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
SettingsSubTitle(title: "videos".tr()),
|
SettingGroupTitle(
|
||||||
|
title: "videos".t(context: context),
|
||||||
|
icon: Icons.video_camera_back_outlined,
|
||||||
|
),
|
||||||
SettingsSwitchListTile(
|
SettingsSwitchListTile(
|
||||||
valueNotifier: useAutoPlayVideo,
|
valueNotifier: useAutoPlayVideo,
|
||||||
title: "setting_video_viewer_auto_play_title".tr(),
|
title: "setting_video_viewer_auto_play_title".t(context: context),
|
||||||
subtitle: "setting_video_viewer_auto_play_subtitle".tr(),
|
subtitle: "setting_video_viewer_auto_play_subtitle".t(context: context),
|
||||||
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
||||||
),
|
),
|
||||||
SettingsSwitchListTile(
|
SettingsSwitchListTile(
|
||||||
valueNotifier: useLoopVideo,
|
valueNotifier: useLoopVideo,
|
||||||
title: "setting_video_viewer_looping_title".tr(),
|
title: "setting_video_viewer_looping_title".t(context: context),
|
||||||
subtitle: "loop_videos_description".tr(),
|
subtitle: "loop_videos_description".t(context: context),
|
||||||
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
||||||
),
|
),
|
||||||
SettingsSwitchListTile(
|
SettingsSwitchListTile(
|
||||||
valueNotifier: useOriginalVideo,
|
valueNotifier: useOriginalVideo,
|
||||||
title: "setting_video_viewer_original_video_title".tr(),
|
title: "setting_video_viewer_original_video_title".t(context: context),
|
||||||
subtitle: "setting_video_viewer_original_video_subtitle".tr(),
|
subtitle: "setting_video_viewer_original_video_subtitle".t(context: context),
|
||||||
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
|
import 'package:immich_mobile/widgets/settings/setting_group_title.dart';
|
||||||
|
import 'package:immich_mobile/widgets/settings/setting_list_tile.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
|
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
|
||||||
|
|
||||||
class DriftBackupSettings extends ConsumerWidget {
|
class DriftBackupSettings extends ConsumerWidget {
|
||||||
@@ -25,36 +27,25 @@ class DriftBackupSettings extends ConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return SettingsSubPageScaffold(
|
return SettingsSubPageScaffold(
|
||||||
settings: [
|
settings: [
|
||||||
Padding(
|
SettingGroupTitle(
|
||||||
padding: const EdgeInsets.only(left: 16.0),
|
title: "network_requirements".t(context: context),
|
||||||
child: Text(
|
icon: Icons.cell_tower,
|
||||||
"network_requirements".t(context: context).toUpperCase(),
|
|
||||||
style: context.textTheme.labelSmall?.copyWith(color: context.colorScheme.onSurface.withValues(alpha: 0.7)),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const _UseWifiForUploadVideosButton(),
|
const _UseWifiForUploadVideosButton(),
|
||||||
const _UseWifiForUploadPhotosButton(),
|
const _UseWifiForUploadPhotosButton(),
|
||||||
if (CurrentPlatform.isAndroid) ...[
|
if (CurrentPlatform.isAndroid) ...[
|
||||||
const Divider(),
|
const Divider(),
|
||||||
Padding(
|
SettingGroupTitle(
|
||||||
padding: const EdgeInsets.only(left: 16.0),
|
title: "background_options".t(context: context),
|
||||||
child: Text(
|
icon: Icons.charging_station_rounded,
|
||||||
"background_options".t(context: context).toUpperCase(),
|
|
||||||
style: context.textTheme.labelSmall?.copyWith(
|
|
||||||
color: context.colorScheme.onSurface.withValues(alpha: 0.7),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const _BackupOnlyWhenChargingButton(),
|
const _BackupOnlyWhenChargingButton(),
|
||||||
const _BackupDelaySlider(),
|
const _BackupDelaySlider(),
|
||||||
],
|
],
|
||||||
const Divider(),
|
const Divider(),
|
||||||
Padding(
|
SettingGroupTitle(
|
||||||
padding: const EdgeInsets.only(left: 16.0),
|
title: "backup_albums_sync".t(context: context),
|
||||||
child: Text(
|
icon: Icons.sync,
|
||||||
"backup_albums_sync".t(context: context).toUpperCase(),
|
|
||||||
style: context.textTheme.labelSmall?.copyWith(color: context.colorScheme.onSurface.withValues(alpha: 0.7)),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const _AlbumSyncActionButton(),
|
const _AlbumSyncActionButton(),
|
||||||
],
|
],
|
||||||
@@ -105,81 +96,67 @@ class _AlbumSyncActionButtonState extends ConsumerState<_AlbumSyncActionButton>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListView(
|
return Padding(
|
||||||
shrinkWrap: true,
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
children: [
|
child: ListView(
|
||||||
StreamBuilder(
|
shrinkWrap: true,
|
||||||
stream: Store.watch(StoreKey.syncAlbums),
|
children: [
|
||||||
initialData: Store.tryGet(StoreKey.syncAlbums) ?? false,
|
StreamBuilder(
|
||||||
builder: (context, snapshot) {
|
stream: Store.watch(StoreKey.syncAlbums),
|
||||||
final albumSyncEnable = snapshot.data ?? false;
|
initialData: Store.tryGet(StoreKey.syncAlbums) ?? false,
|
||||||
return Column(
|
builder: (context, snapshot) {
|
||||||
children: [
|
final albumSyncEnable = snapshot.data ?? false;
|
||||||
ListTile(
|
return Column(
|
||||||
title: Text(
|
children: [
|
||||||
"sync_albums".t(context: context),
|
SettingListTile(
|
||||||
style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor),
|
title: "sync_albums".t(context: context),
|
||||||
),
|
subtitle: "sync_upload_album_setting_subtitle".t(context: context),
|
||||||
subtitle: Text(
|
trailing: Switch(
|
||||||
"sync_upload_album_setting_subtitle".t(context: context),
|
value: albumSyncEnable,
|
||||||
style: context.textTheme.labelLarge,
|
onChanged: (bool newValue) async {
|
||||||
),
|
await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.syncAlbums, newValue);
|
||||||
trailing: Switch(
|
|
||||||
value: albumSyncEnable,
|
|
||||||
onChanged: (bool newValue) async {
|
|
||||||
await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.syncAlbums, newValue);
|
|
||||||
|
|
||||||
if (newValue == true) {
|
if (newValue == true) {
|
||||||
await _manageLinkedAlbums();
|
await _manageLinkedAlbums();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
AnimatedSize(
|
||||||
AnimatedSize(
|
duration: const Duration(milliseconds: 300),
|
||||||
duration: const Duration(milliseconds: 300),
|
curve: Curves.easeInOut,
|
||||||
curve: Curves.easeInOut,
|
child: AnimatedOpacity(
|
||||||
child: AnimatedOpacity(
|
duration: const Duration(milliseconds: 200),
|
||||||
duration: const Duration(milliseconds: 200),
|
opacity: albumSyncEnable ? 1.0 : 0.0,
|
||||||
opacity: albumSyncEnable ? 1.0 : 0.0,
|
child: albumSyncEnable
|
||||||
child: albumSyncEnable
|
? SettingListTile(
|
||||||
? ListTile(
|
onTap: _manualSyncAlbums,
|
||||||
onTap: _manualSyncAlbums,
|
contentPadding: const EdgeInsets.only(left: 32, right: 16),
|
||||||
contentPadding: const EdgeInsets.only(left: 32, right: 16),
|
title: "organize_into_albums".t(context: context),
|
||||||
title: Text(
|
subtitle: "organize_into_albums_description".t(context: context),
|
||||||
"organize_into_albums".t(context: context),
|
trailing: isAlbumSyncInProgress
|
||||||
style: context.textTheme.titleSmall?.copyWith(
|
? const SizedBox(
|
||||||
color: context.colorScheme.onSurface,
|
width: 32,
|
||||||
fontWeight: FontWeight.normal,
|
height: 32,
|
||||||
),
|
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||||
),
|
)
|
||||||
subtitle: Text(
|
: IconButton(
|
||||||
"organize_into_albums_description".t(context: context),
|
onPressed: _manualSyncAlbums,
|
||||||
style: context.textTheme.bodyMedium?.copyWith(
|
icon: const Icon(Icons.sync_rounded),
|
||||||
color: context.colorScheme.onSurface.withValues(alpha: 0.7),
|
color: context.colorScheme.onSurface.withValues(alpha: 0.7),
|
||||||
),
|
iconSize: 20,
|
||||||
),
|
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
|
||||||
trailing: isAlbumSyncInProgress
|
),
|
||||||
? const SizedBox(
|
)
|
||||||
width: 32,
|
: const SizedBox.shrink(),
|
||||||
height: 32,
|
),
|
||||||
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
|
|
||||||
)
|
|
||||||
: IconButton(
|
|
||||||
onPressed: _manualSyncAlbums,
|
|
||||||
icon: const Icon(Icons.sync_rounded),
|
|
||||||
color: context.colorScheme.onSurface.withValues(alpha: 0.7),
|
|
||||||
iconSize: 20,
|
|
||||||
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink(),
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,24 +199,24 @@ class _SettingsSwitchTileState extends ConsumerState<_SettingsSwitchTile> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListTile(
|
return Padding(
|
||||||
title: Text(
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
widget.titleKey.t(context: context),
|
child: SettingListTile(
|
||||||
style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor),
|
title: widget.titleKey.t(context: context),
|
||||||
),
|
subtitle: widget.subtitleKey.t(context: context),
|
||||||
subtitle: Text(widget.subtitleKey.t(context: context), style: context.textTheme.labelLarge),
|
trailing: StreamBuilder(
|
||||||
trailing: StreamBuilder(
|
stream: valueStream,
|
||||||
stream: valueStream,
|
initialData: Store.tryGet(widget.appSettingsEnum.storeKey) ?? widget.appSettingsEnum.defaultValue,
|
||||||
initialData: Store.tryGet(widget.appSettingsEnum.storeKey) ?? widget.appSettingsEnum.defaultValue,
|
builder: (context, snapshot) {
|
||||||
builder: (context, snapshot) {
|
final value = snapshot.data ?? false;
|
||||||
final value = snapshot.data ?? false;
|
return Switch(
|
||||||
return Switch(
|
value: value,
|
||||||
value: value,
|
onChanged: (bool newValue) async {
|
||||||
onChanged: (bool newValue) async {
|
await ref.read(appSettingsServiceProvider).setSetting(widget.appSettingsEnum, newValue);
|
||||||
await ref.read(appSettingsServiceProvider).setSetting(widget.appSettingsEnum, newValue);
|
},
|
||||||
},
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -354,7 +331,7 @@ class _BackupDelaySliderState extends ConsumerState<_BackupDelaySlider> {
|
|||||||
'backup_controller_page_background_delay'.tr(
|
'backup_controller_page_background_delay'.tr(
|
||||||
namedArgs: {'duration': formatBackupDelaySliderValue(currentValue)},
|
namedArgs: {'duration': formatBackupDelaySliderValue(currentValue)},
|
||||||
),
|
),
|
||||||
style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor),
|
style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Slider(
|
Slider(
|
||||||
|
|||||||
@@ -34,33 +34,36 @@ class EntityCountTile extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
// Icon and Label
|
// Icon and Label
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, color: context.primaryColor),
|
Icon(icon, color: context.primaryColor, size: 14),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 4),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
label,
|
label,
|
||||||
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 16),
|
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.w500),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
// Number
|
// Number
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
RichText(
|
Padding(
|
||||||
text: TextSpan(
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
style: const TextStyle(fontSize: 18, fontFamily: 'GoogleSansCode', fontWeight: FontWeight.w600),
|
child: RichText(
|
||||||
children: [
|
text: TextSpan(
|
||||||
TextSpan(
|
style: const TextStyle(fontSize: 18, fontFamily: 'GoogleSansCode'),
|
||||||
text: zeroPadding(count, maxDigits),
|
children: [
|
||||||
style: TextStyle(color: context.colorScheme.onSurfaceSecondary.withAlpha(75)),
|
TextSpan(
|
||||||
),
|
text: zeroPadding(count, maxDigits),
|
||||||
TextSpan(
|
style: TextStyle(color: context.colorScheme.onSurfaceSecondary.withAlpha(75)),
|
||||||
text: count.toString(),
|
),
|
||||||
style: TextStyle(color: context.primaryColor),
|
TextSpan(
|
||||||
),
|
text: count.toString(),
|
||||||
],
|
style: TextStyle(color: context.colorScheme.onSurface),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import 'package:immich_mobile/providers/infrastructure/trash_sync.provider.dart'
|
|||||||
import 'package:immich_mobile/providers/sync_status.provider.dart';
|
import 'package:immich_mobile/providers/sync_status.provider.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/beta_sync_settings/entity_count_tile.dart';
|
import 'package:immich_mobile/widgets/settings/beta_sync_settings/entity_count_tile.dart';
|
||||||
|
import 'package:immich_mobile/widgets/settings/setting_group_title.dart';
|
||||||
|
import 'package:immich_mobile/widgets/settings/setting_list_tile.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
@@ -112,48 +114,39 @@ class SyncStatusAndActions extends HookConsumerWidget {
|
|||||||
padding: const EdgeInsets.only(top: 16, bottom: 96),
|
padding: const EdgeInsets.only(top: 16, bottom: 96),
|
||||||
children: [
|
children: [
|
||||||
const _SyncStatsCounts(),
|
const _SyncStatsCounts(),
|
||||||
const Divider(height: 1, indent: 16, endIndent: 16),
|
const Divider(height: 10),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 16),
|
||||||
_SectionHeaderText(text: "jobs".t(context: context)),
|
SettingGroupTitle(title: "jobs".t(context: context)),
|
||||||
ListTile(
|
SettingListTile(
|
||||||
title: Text(
|
title: "sync_local".t(context: context),
|
||||||
"sync_local".t(context: context),
|
subtitle: "tap_to_run_job".t(context: context),
|
||||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
|
||||||
),
|
|
||||||
subtitle: Text("tap_to_run_job".t(context: context)),
|
|
||||||
leading: const Icon(Icons.sync),
|
leading: const Icon(Icons.sync),
|
||||||
trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).localSyncStatus),
|
trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).localSyncStatus),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ref.read(backgroundSyncProvider).syncLocal(full: true);
|
ref.read(backgroundSyncProvider).syncLocal(full: true);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
SettingListTile(
|
||||||
title: Text(
|
title: "sync_remote".t(context: context),
|
||||||
"sync_remote".t(context: context),
|
subtitle: "tap_to_run_job".t(context: context),
|
||||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
|
||||||
),
|
|
||||||
subtitle: Text("tap_to_run_job".t(context: context)),
|
|
||||||
leading: const Icon(Icons.cloud_sync),
|
leading: const Icon(Icons.cloud_sync),
|
||||||
trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).remoteSyncStatus),
|
trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).remoteSyncStatus),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ref.read(backgroundSyncProvider).syncRemote();
|
ref.read(backgroundSyncProvider).syncRemote();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
SettingListTile(
|
||||||
title: Text(
|
title: "hash_asset".t(context: context),
|
||||||
"hash_asset".t(context: context),
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
|
||||||
),
|
|
||||||
leading: const Icon(Icons.tag),
|
leading: const Icon(Icons.tag),
|
||||||
subtitle: Text("tap_to_run_job".t(context: context)),
|
subtitle: "tap_to_run_job".t(context: context),
|
||||||
trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).hashJobStatus),
|
trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).hashJobStatus),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ref.read(backgroundSyncProvider).hashAssets();
|
ref.read(backgroundSyncProvider).hashAssets();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Divider(height: 1, indent: 16, endIndent: 16),
|
const Divider(height: 1),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 16),
|
||||||
_SectionHeaderText(text: "actions".t(context: context)),
|
SettingGroupTitle(title: "actions".t(context: context)),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
"clear_file_cache".t(context: context),
|
"clear_file_cache".t(context: context),
|
||||||
@@ -202,26 +195,6 @@ class _SyncStatusIcon extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SectionHeaderText extends StatelessWidget {
|
|
||||||
final String text;
|
|
||||||
|
|
||||||
const _SectionHeaderText({required this.text});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 16.0),
|
|
||||||
child: Text(
|
|
||||||
text.toUpperCase(),
|
|
||||||
style: context.textTheme.labelLarge?.copyWith(
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: context.colorScheme.onSurface.withAlpha(200),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SyncStatsCounts extends ConsumerWidget {
|
class _SyncStatsCounts extends ConsumerWidget {
|
||||||
const _SyncStatsCounts();
|
const _SyncStatsCounts();
|
||||||
|
|
||||||
@@ -279,9 +252,9 @@ class _SyncStatsCounts extends ConsumerWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_SectionHeaderText(text: "assets".t(context: context)),
|
SettingGroupTitle(title: "assets".t(context: context)),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||||
// 1. Wrap in IntrinsicHeight
|
// 1. Wrap in IntrinsicHeight
|
||||||
child: IntrinsicHeight(
|
child: IntrinsicHeight(
|
||||||
child: Flex(
|
child: Flex(
|
||||||
@@ -309,9 +282,9 @@ class _SyncStatsCounts extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_SectionHeaderText(text: "albums".t(context: context)),
|
SettingGroupTitle(title: "albums".t(context: context)),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||||
child: IntrinsicHeight(
|
child: IntrinsicHeight(
|
||||||
child: Flex(
|
child: Flex(
|
||||||
direction: Axis.horizontal,
|
direction: Axis.horizontal,
|
||||||
@@ -337,9 +310,9 @@ class _SyncStatsCounts extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_SectionHeaderText(text: "other".t(context: context)),
|
SettingGroupTitle(title: "other".t(context: context)),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||||
child: IntrinsicHeight(
|
child: IntrinsicHeight(
|
||||||
child: Flex(
|
child: Flex(
|
||||||
direction: Axis.horizontal,
|
direction: Axis.horizontal,
|
||||||
@@ -368,7 +341,7 @@ class _SyncStatsCounts extends ConsumerWidget {
|
|||||||
// To be removed once the experimental feature is stable
|
// To be removed once the experimental feature is stable
|
||||||
if (CurrentPlatform.isAndroid &&
|
if (CurrentPlatform.isAndroid &&
|
||||||
appSettingsService.getSetting<bool>(AppSettingsEnum.manageLocalMediaAndroid)) ...[
|
appSettingsService.getSetting<bool>(AppSettingsEnum.manageLocalMediaAndroid)) ...[
|
||||||
_SectionHeaderText(text: "trash".t(context: context)),
|
SettingGroupTitle(title: "trash".t(context: context)),
|
||||||
Consumer(
|
Consumer(
|
||||||
builder: (context, ref, _) {
|
builder: (context, ref, _) {
|
||||||
final counts = ref.watch(trashedAssetsCountProvider);
|
final counts = ref.watch(trashedAssetsCountProvider);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import 'package:immich_mobile/providers/app_settings.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
|
import 'package:immich_mobile/widgets/settings/setting_list_tile.dart';
|
||||||
|
|
||||||
class BetaTimelineListTile extends ConsumerWidget {
|
class BetaTimelineListTile extends ConsumerWidget {
|
||||||
const BetaTimelineListTile({super.key});
|
const BetaTimelineListTile({super.key});
|
||||||
@@ -56,8 +57,8 @@ class BetaTimelineListTile extends ConsumerWidget {
|
|||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(left: 4.0),
|
padding: const EdgeInsets.only(left: 4.0),
|
||||||
child: ListTile(
|
child: SettingListTile(
|
||||||
title: Text("new_timeline".t(context: context)),
|
title: "new_timeline".t(context: context),
|
||||||
trailing: Switch.adaptive(
|
trailing: Switch.adaptive(
|
||||||
value: betaTimelineValue,
|
value: betaTimelineValue,
|
||||||
onChanged: onSwitchChanged,
|
onChanged: onSwitchChanged,
|
||||||
|
|||||||
@@ -142,7 +142,9 @@ class _FreeUpSpaceSettingsState extends ConsumerState<FreeUpSpaceSettings> {
|
|||||||
final state = ref.watch(cleanupProvider);
|
final state = ref.watch(cleanupProvider);
|
||||||
final hasDate = state.selectedDate != null;
|
final hasDate = state.selectedDate != null;
|
||||||
final hasAssets = _hasScanned && state.assetsToDelete.isNotEmpty;
|
final hasAssets = _hasScanned && state.assetsToDelete.isNotEmpty;
|
||||||
|
final subtitleStyle = context.textTheme.bodyMedium!.copyWith(
|
||||||
|
color: context.textTheme.bodyMedium!.color!.withAlpha(215),
|
||||||
|
);
|
||||||
StepStyle styleForState(StepState stepState, {bool isDestructive = false}) {
|
StepStyle styleForState(StepState stepState, {bool isDestructive = false}) {
|
||||||
switch (stepState) {
|
switch (stepState) {
|
||||||
case StepState.complete:
|
case StepState.complete:
|
||||||
@@ -214,10 +216,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState<FreeUpSpaceSettings> {
|
|||||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||||
border: Border.all(color: context.primaryColor.withValues(alpha: 0.25)),
|
border: Border.all(color: context.primaryColor.withValues(alpha: 0.25)),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text('free_up_space_description'.t(context: context), style: context.textTheme.bodyMedium),
|
||||||
'free_up_space_description'.t(context: context),
|
|
||||||
style: context.textTheme.labelLarge?.copyWith(fontSize: 15),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -256,7 +255,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState<FreeUpSpaceSettings> {
|
|||||||
content: Column(
|
content: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text('cutoff_date_description'.t(context: context), style: context.textTheme.labelLarge),
|
Text('cutoff_date_description'.t(context: context), style: subtitleStyle),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
GridView.count(
|
GridView.count(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
@@ -352,7 +351,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState<FreeUpSpaceSettings> {
|
|||||||
content: Column(
|
content: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text('cleanup_filter_description'.t(context: context), style: context.textTheme.labelLarge),
|
Text('cleanup_filter_description'.t(context: context), style: subtitleStyle),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
SegmentedButton<AssetFilterType>(
|
SegmentedButton<AssetFilterType>(
|
||||||
segments: [
|
segments: [
|
||||||
@@ -381,10 +380,15 @@ class _FreeUpSpaceSettingsState extends ConsumerState<FreeUpSpaceSettings> {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
title: Text('keep_favorites'.t(context: context), style: context.textTheme.titleSmall),
|
title: Text(
|
||||||
|
'keep_favorites'.t(context: context),
|
||||||
|
style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5),
|
||||||
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
'keep_favorites_description'.t(context: context),
|
'keep_favorites_description'.t(context: context),
|
||||||
style: context.textTheme.labelLarge,
|
style: context.textTheme.bodyMedium!.copyWith(
|
||||||
|
color: context.textTheme.bodyMedium!.color!.withAlpha(215),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
value: state.keepFavorites,
|
value: state.keepFavorites,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@@ -435,10 +439,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState<FreeUpSpaceSettings> {
|
|||||||
: null,
|
: null,
|
||||||
content: Column(
|
content: Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text('cleanup_step3_description'.t(context: context), style: subtitleStyle),
|
||||||
'cleanup_step3_description'.t(context: context),
|
|
||||||
style: context.textTheme.labelLarge?.copyWith(fontSize: 15),
|
|
||||||
),
|
|
||||||
if (CurrentPlatform.isIOS) ...[
|
if (CurrentPlatform.isIOS) ...[
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Container(
|
Container(
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ class EndpointInputState extends ConsumerState<EndpointInput> {
|
|||||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
validator: validateUrl,
|
validator: validateUrl,
|
||||||
keyboardType: TextInputType.url,
|
keyboardType: TextInputType.url,
|
||||||
style: const TextStyle(fontFamily: 'GoogleSansCode', fontWeight: FontWeight.w600, fontSize: 14),
|
style: const TextStyle(fontFamily: 'GoogleSansCode', fontSize: 14),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'http(s)://immich.domain.com',
|
hintText: 'http(s)://immich.domain.com',
|
||||||
contentPadding: const EdgeInsets.all(16),
|
contentPadding: const EdgeInsets.all(16),
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
|
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/networking_settings/endpoint_input.dart';
|
import 'package:immich_mobile/widgets/settings/networking_settings/endpoint_input.dart';
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ class ExternalNetworkPreference extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 24),
|
padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 24),
|
||||||
child: Text("external_network_sheet_info".tr(), style: context.textTheme.bodyMedium),
|
child: Text("external_network_sheet_info".t(context: context), style: context.textTheme.bodyMedium),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Divider(color: context.colorScheme.surfaceContainerHighest),
|
Divider(color: context.colorScheme.surfaceContainerHighest),
|
||||||
@@ -135,7 +135,7 @@ class ExternalNetworkPreference extends HookConsumerWidget {
|
|||||||
height: 48,
|
height: 48,
|
||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
label: Text('add_endpoint'.tr().toUpperCase()),
|
label: Text('add_endpoint'.t(context: context)),
|
||||||
onPressed: enabled
|
onPressed: enabled
|
||||||
? () {
|
? () {
|
||||||
entries.value = [
|
entries.value = [
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
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/auth.provider.dart';
|
||||||
import 'package:immich_mobile/providers/network.provider.dart';
|
import 'package:immich_mobile/providers/network.provider.dart';
|
||||||
|
|
||||||
@@ -167,13 +168,12 @@ class LocalNetworkPreference extends HookConsumerWidget {
|
|||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
contentPadding: const EdgeInsets.only(left: 24, right: 8),
|
contentPadding: const EdgeInsets.only(left: 24, right: 8),
|
||||||
leading: const Icon(Icons.lan_rounded),
|
leading: const Icon(Icons.lan_rounded),
|
||||||
title: Text("server_endpoint".tr()),
|
title: Text("server_endpoint".t(context: context)),
|
||||||
subtitle: localEndpointText.value.isEmpty
|
subtitle: localEndpointText.value.isEmpty
|
||||||
? const Text("http://local-ip:2283")
|
? const Text("http://local-ip:2283")
|
||||||
: Text(
|
: Text(
|
||||||
localEndpointText.value,
|
localEndpointText.value,
|
||||||
style: context.textTheme.labelLarge?.copyWith(
|
style: context.textTheme.labelLarge?.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: enabled ? context.primaryColor : context.colorScheme.onSurface.withAlpha(100),
|
color: enabled ? context.primaryColor : context.colorScheme.onSurface.withAlpha(100),
|
||||||
fontFamily: 'GoogleSansCode',
|
fontFamily: 'GoogleSansCode',
|
||||||
),
|
),
|
||||||
@@ -190,7 +190,7 @@ class LocalNetworkPreference extends HookConsumerWidget {
|
|||||||
height: 48,
|
height: 48,
|
||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
icon: const Icon(Icons.wifi_find_rounded),
|
icon: const Icon(Icons.wifi_find_rounded),
|
||||||
label: Text('use_current_connection'.tr().toUpperCase()),
|
label: Text('use_current_connection'.t(context: context)),
|
||||||
onPressed: enabled ? autofillCurrentNetwork : null,
|
onPressed: enabled ? autofillCurrentNetwork : null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
|
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
|
||||||
import 'package:immich_mobile/providers/network.provider.dart';
|
import 'package:immich_mobile/providers/network.provider.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
@@ -10,6 +11,7 @@ import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
|||||||
import 'package:immich_mobile/utils/url_helper.dart';
|
import 'package:immich_mobile/utils/url_helper.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/networking_settings/external_network_preference.dart';
|
import 'package:immich_mobile/widgets/settings/networking_settings/external_network_preference.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/networking_settings/local_network_preference.dart';
|
import 'package:immich_mobile/widgets/settings/networking_settings/local_network_preference.dart';
|
||||||
|
import 'package:immich_mobile/widgets/settings/setting_group_title.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
||||||
|
|
||||||
class NetworkingSettings extends HookConsumerWidget {
|
class NetworkingSettings extends HookConsumerWidget {
|
||||||
@@ -87,12 +89,10 @@ class NetworkingSettings extends HookConsumerWidget {
|
|||||||
return ListView(
|
return ListView(
|
||||||
padding: const EdgeInsets.only(bottom: 96),
|
padding: const EdgeInsets.only(bottom: 96),
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Padding(
|
const SizedBox(height: 8),
|
||||||
padding: const EdgeInsets.only(top: 8, left: 16, bottom: 8),
|
SettingGroupTitle(
|
||||||
child: NetworkPreferenceTitle(
|
title: "current_server_address".t(context: context),
|
||||||
title: "current_server_address".tr().toUpperCase(),
|
icon: (currentEndpoint?.startsWith('https') ?? false) ? Icons.https_outlined : Icons.http_outlined,
|
||||||
icon: (currentEndpoint?.startsWith('https') ?? false) ? Icons.https_outlined : Icons.http_outlined,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
@@ -108,12 +108,7 @@ class NetworkingSettings extends HookConsumerWidget {
|
|||||||
: const Icon(Icons.circle_outlined),
|
: const Icon(Icons.circle_outlined),
|
||||||
title: Text(
|
title: Text(
|
||||||
currentEndpoint ?? "--",
|
currentEndpoint ?? "--",
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 14, fontFamily: 'GoogleSansCode', color: context.primaryColor),
|
||||||
fontSize: 16,
|
|
||||||
fontFamily: 'GoogleSansCode',
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: context.primaryColor,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -128,14 +123,16 @@ class NetworkingSettings extends HookConsumerWidget {
|
|||||||
title: "automatic_endpoint_switching_title".tr(),
|
title: "automatic_endpoint_switching_title".tr(),
|
||||||
subtitle: "automatic_endpoint_switching_subtitle".tr(),
|
subtitle: "automatic_endpoint_switching_subtitle".tr(),
|
||||||
),
|
),
|
||||||
Padding(
|
const SizedBox(height: 8),
|
||||||
padding: const EdgeInsets.only(top: 8, left: 16, bottom: 16),
|
SettingGroupTitle(
|
||||||
child: NetworkPreferenceTitle(title: "local_network".tr().toUpperCase(), icon: Icons.home_outlined),
|
title: "local_network".t(context: context),
|
||||||
|
icon: Icons.home_outlined,
|
||||||
),
|
),
|
||||||
LocalNetworkPreference(enabled: featureEnabled.value),
|
LocalNetworkPreference(enabled: featureEnabled.value),
|
||||||
Padding(
|
const SizedBox(height: 16),
|
||||||
padding: const EdgeInsets.only(top: 32, left: 16, bottom: 16),
|
SettingGroupTitle(
|
||||||
child: NetworkPreferenceTitle(title: "external_network".tr().toUpperCase(), icon: Icons.dns_outlined),
|
title: "external_network".t(context: context),
|
||||||
|
icon: Icons.dns_outlined,
|
||||||
),
|
),
|
||||||
ExternalNetworkPreference(enabled: featureEnabled.value),
|
ExternalNetworkPreference(enabled: featureEnabled.value),
|
||||||
],
|
],
|
||||||
@@ -143,30 +140,6 @@ class NetworkingSettings extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NetworkPreferenceTitle extends StatelessWidget {
|
|
||||||
const NetworkPreferenceTitle({super.key, required this.icon, required this.title});
|
|
||||||
|
|
||||||
final IconData icon;
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Icon(icon, color: context.colorScheme.onSurface.withAlpha(150)),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style: context.textTheme.displaySmall?.copyWith(
|
|
||||||
color: context.colorScheme.onSurface.withAlpha(200),
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class NetworkStatusIcon extends StatelessWidget {
|
class NetworkStatusIcon extends StatelessWidget {
|
||||||
const NetworkStatusIcon({super.key, required this.status, this.enabled = true}) : super();
|
const NetworkStatusIcon({super.key, required this.status, this.enabled = true}) : super();
|
||||||
|
|
||||||
@@ -175,10 +148,10 @@ class NetworkStatusIcon extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AnimatedSwitcher(duration: const Duration(milliseconds: 200), child: _buildIcon(context));
|
return AnimatedSwitcher(duration: const Duration(milliseconds: 200), child: buildIcon(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildIcon(BuildContext context) => switch (status) {
|
Widget buildIcon(BuildContext context) => switch (status) {
|
||||||
AuxCheckStatus.loading => Padding(
|
AuxCheckStatus.loading => Padding(
|
||||||
padding: const EdgeInsets.only(left: 4.0),
|
padding: const EdgeInsets.only(left: 4.0),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/settings_sub_title.dart';
|
import 'package:immich_mobile/widgets/settings/setting_group_title.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
||||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||||
|
|
||||||
@@ -22,10 +22,13 @@ class HapticSetting extends HookConsumerWidget {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
SettingsSubTitle(title: "haptic_feedback_title".tr()),
|
SettingGroupTitle(
|
||||||
|
title: "haptic_feedback_title".t(context: context),
|
||||||
|
icon: Icons.vibration_outlined,
|
||||||
|
),
|
||||||
SettingsSwitchListTile(
|
SettingsSwitchListTile(
|
||||||
valueNotifier: isHapticFeedbackEnabled,
|
valueNotifier: isHapticFeedbackEnabled,
|
||||||
title: 'haptic_feedback_switch'.tr(),
|
title: 'enabled'.t(context: context),
|
||||||
onChanged: onHapticFeedbackChange,
|
onChanged: onHapticFeedbackChange,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/theme.provider.dart';
|
import 'package:immich_mobile/providers/theme.provider.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/preference_settings/primary_color_setting.dart';
|
import 'package:immich_mobile/widgets/settings/preference_settings/primary_color_setting.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/settings_sub_title.dart';
|
import 'package:immich_mobile/widgets/settings/setting_group_title.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
||||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||||
|
|
||||||
@@ -74,23 +74,26 @@ class ThemeSetting extends HookConsumerWidget {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
SettingsSubTitle(title: "theme".tr()),
|
SettingGroupTitle(
|
||||||
|
title: "theme".t(context: context),
|
||||||
|
icon: Icons.color_lens_outlined,
|
||||||
|
),
|
||||||
SettingsSwitchListTile(
|
SettingsSwitchListTile(
|
||||||
valueNotifier: isSystemTheme,
|
valueNotifier: isSystemTheme,
|
||||||
title: 'theme_setting_system_theme_switch'.tr(),
|
title: 'theme_setting_system_theme_switch'.t(context: context),
|
||||||
onChanged: onSystemThemeChange,
|
onChanged: onSystemThemeChange,
|
||||||
),
|
),
|
||||||
if (currentTheme.value != ThemeMode.system)
|
if (currentTheme.value != ThemeMode.system)
|
||||||
SettingsSwitchListTile(
|
SettingsSwitchListTile(
|
||||||
valueNotifier: isDarkTheme,
|
valueNotifier: isDarkTheme,
|
||||||
title: 'map_settings_dark_mode'.tr(),
|
title: 'map_settings_dark_mode'.t(context: context),
|
||||||
onChanged: onThemeChange,
|
onChanged: onThemeChange,
|
||||||
),
|
),
|
||||||
const PrimaryColorSetting(),
|
const PrimaryColorSetting(),
|
||||||
SettingsSwitchListTile(
|
SettingsSwitchListTile(
|
||||||
valueNotifier: applyThemeToBackgroundProvider,
|
valueNotifier: applyThemeToBackgroundProvider,
|
||||||
title: "theme_setting_colorful_interface_title".tr(),
|
title: "theme_setting_colorful_interface_title".t(context: context),
|
||||||
subtitle: 'theme_setting_colorful_interface_subtitle'.tr(),
|
subtitle: 'theme_setting_colorful_interface_subtitle'.t(context: context),
|
||||||
onChanged: onSurfaceColorSettingChange,
|
onChanged: onSurfaceColorSettingChange,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
|
|
||||||
|
class SettingGroupTitle extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final String? subtitle;
|
||||||
|
final IconData? icon;
|
||||||
|
final EdgeInsetsGeometry? contentPadding;
|
||||||
|
|
||||||
|
const SettingGroupTitle({super.key, required this.title, this.icon, this.subtitle, this.contentPadding});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: contentPadding ?? const EdgeInsets.only(left: 20.0, right: 20.0, bottom: 8.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
if (icon != null) ...[
|
||||||
|
Icon(icon, color: context.colorScheme.onSurfaceSecondary, size: 20),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
],
|
||||||
|
Text(title, style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (subtitle != null) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
subtitle!,
|
||||||
|
style: context.textTheme.bodyMedium!.copyWith(color: context.colorScheme.onSurface.withAlpha(200)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
|
||||||
|
class SettingListTile extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final String? subtitle;
|
||||||
|
final Widget? leading;
|
||||||
|
final Widget? trailing;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
final EdgeInsetsGeometry? contentPadding;
|
||||||
|
|
||||||
|
const SettingListTile({
|
||||||
|
required this.title,
|
||||||
|
this.subtitle,
|
||||||
|
this.leading,
|
||||||
|
this.trailing,
|
||||||
|
this.onTap,
|
||||||
|
this.contentPadding,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListTile(
|
||||||
|
title: Text(title, style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5)),
|
||||||
|
subtitle: subtitle != null
|
||||||
|
? Text(
|
||||||
|
subtitle!,
|
||||||
|
style: context.textTheme.bodyMedium!.copyWith(color: context.textTheme.bodyMedium!.color!.withAlpha(215)),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
leading: leading,
|
||||||
|
trailing: trailing,
|
||||||
|
onTap: onTap,
|
||||||
|
contentPadding: contentPadding,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,11 +36,8 @@ class SettingsCard extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Icon(icon, color: context.primaryColor),
|
child: Icon(icon, color: context.primaryColor),
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(title, style: context.textTheme.titleMedium!.copyWith(color: context.primaryColor)),
|
||||||
title,
|
subtitle: Text(subtitle, style: context.textTheme.bodyMedium),
|
||||||
style: context.textTheme.titleMedium!.copyWith(fontWeight: FontWeight.w600, color: context.primaryColor),
|
|
||||||
),
|
|
||||||
subtitle: Text(subtitle, style: context.textTheme.labelLarge),
|
|
||||||
onTap: () => context.pushRoute(settingRoute),
|
onTap: () => context.pushRoute(settingRoute),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -9,13 +9,11 @@ class SettingsSubPageScaffold extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListView.separated(
|
return ListView.separated(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
itemCount: settings.length,
|
itemCount: settings.length,
|
||||||
itemBuilder: (ctx, index) => settings[index],
|
itemBuilder: (ctx, index) => settings[index],
|
||||||
separatorBuilder: (context, index) => showDivider
|
separatorBuilder: (context, index) => showDivider
|
||||||
? const Column(
|
? const Column(children: [SizedBox(height: 5), Divider(height: 10), SizedBox(height: 15)])
|
||||||
children: [SizedBox(height: 5), Divider(height: 10, indent: 15, endIndent: 15), SizedBox(height: 15)],
|
|
||||||
)
|
|
||||||
: const SizedBox(height: 10),
|
: const SizedBox(height: 10),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+269
@@ -0,0 +1,269 @@
|
|||||||
|
//
|
||||||
|
// 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 DatabaseBackupsAdminApi {
|
||||||
|
DatabaseBackupsAdminApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
|
||||||
|
|
||||||
|
final ApiClient apiClient;
|
||||||
|
|
||||||
|
/// Delete database backup
|
||||||
|
///
|
||||||
|
/// Delete a backup by its filename
|
||||||
|
///
|
||||||
|
/// Note: This method returns the HTTP [Response].
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [DatabaseBackupDeleteDto] databaseBackupDeleteDto (required):
|
||||||
|
Future<Response> deleteDatabaseBackupWithHttpInfo(DatabaseBackupDeleteDto databaseBackupDeleteDto,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/admin/database-backups';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody = databaseBackupDeleteDto;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>['application/json'];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'DELETE',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete database backup
|
||||||
|
///
|
||||||
|
/// Delete a backup by its filename
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [DatabaseBackupDeleteDto] databaseBackupDeleteDto (required):
|
||||||
|
Future<void> deleteDatabaseBackup(DatabaseBackupDeleteDto databaseBackupDeleteDto,) async {
|
||||||
|
final response = await deleteDatabaseBackupWithHttpInfo(databaseBackupDeleteDto,);
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Download database backup
|
||||||
|
///
|
||||||
|
/// Downloads the database backup file
|
||||||
|
///
|
||||||
|
/// Note: This method returns the HTTP [Response].
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] filename (required):
|
||||||
|
Future<Response> downloadDatabaseBackupWithHttpInfo(String filename,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/admin/database-backups/{filename}'
|
||||||
|
.replaceAll('{filename}', filename);
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Download database backup
|
||||||
|
///
|
||||||
|
/// Downloads the database backup file
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] filename (required):
|
||||||
|
Future<MultipartFile?> downloadDatabaseBackup(String filename,) async {
|
||||||
|
final response = await downloadDatabaseBackupWithHttpInfo(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), 'MultipartFile',) as MultipartFile;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List database backups
|
||||||
|
///
|
||||||
|
/// Get the list of the successful and failed backups
|
||||||
|
///
|
||||||
|
/// Note: This method returns the HTTP [Response].
|
||||||
|
Future<Response> listDatabaseBackupsWithHttpInfo() async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/admin/database-backups';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List database backups
|
||||||
|
///
|
||||||
|
/// Get the list of the successful and failed backups
|
||||||
|
Future<DatabaseBackupListResponseDto?> listDatabaseBackups() async {
|
||||||
|
final response = await listDatabaseBackupsWithHttpInfo();
|
||||||
|
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), 'DatabaseBackupListResponseDto',) as DatabaseBackupListResponseDto;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start database backup restore flow
|
||||||
|
///
|
||||||
|
/// Put Immich into maintenance mode to restore a backup (Immich must not be configured)
|
||||||
|
///
|
||||||
|
/// Note: This method returns the HTTP [Response].
|
||||||
|
Future<Response> startDatabaseRestoreFlowWithHttpInfo() async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/admin/database-backups/start-restore';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'POST',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start database backup restore flow
|
||||||
|
///
|
||||||
|
/// Put Immich into maintenance mode to restore a backup (Immich must not be configured)
|
||||||
|
Future<void> startDatabaseRestoreFlow() async {
|
||||||
|
final response = await startDatabaseRestoreFlowWithHttpInfo();
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload database backup
|
||||||
|
///
|
||||||
|
/// Uploads .sql/.sql.gz file to restore backup from
|
||||||
|
///
|
||||||
|
/// Note: This method returns the HTTP [Response].
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [MultipartFile] file:
|
||||||
|
Future<Response> uploadDatabaseBackupWithHttpInfo({ MultipartFile? file, }) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/admin/database-backups/upload';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>['multipart/form-data'];
|
||||||
|
|
||||||
|
bool hasFields = false;
|
||||||
|
final mp = MultipartRequest('POST', Uri.parse(apiPath));
|
||||||
|
if (file != null) {
|
||||||
|
hasFields = true;
|
||||||
|
mp.fields[r'file'] = file.field;
|
||||||
|
mp.files.add(file);
|
||||||
|
}
|
||||||
|
if (hasFields) {
|
||||||
|
postBody = mp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'POST',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload database backup
|
||||||
|
///
|
||||||
|
/// Uploads .sql/.sql.gz file to restore backup from
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [MultipartFile] file:
|
||||||
|
Future<void> uploadDatabaseBackup({ MultipartFile? file, }) async {
|
||||||
|
final response = await uploadDatabaseBackupWithHttpInfo( file: file, );
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-1
@@ -26,6 +26,7 @@ class AssetResponseDto {
|
|||||||
required this.height,
|
required this.height,
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.isArchived,
|
required this.isArchived,
|
||||||
|
required this.isEdited,
|
||||||
required this.isFavorite,
|
required this.isFavorite,
|
||||||
required this.isOffline,
|
required this.isOffline,
|
||||||
required this.isTrashed,
|
required this.isTrashed,
|
||||||
@@ -85,6 +86,8 @@ class AssetResponseDto {
|
|||||||
|
|
||||||
bool isArchived;
|
bool isArchived;
|
||||||
|
|
||||||
|
bool isEdited;
|
||||||
|
|
||||||
bool isFavorite;
|
bool isFavorite;
|
||||||
|
|
||||||
bool isOffline;
|
bool isOffline;
|
||||||
@@ -162,6 +165,7 @@ class AssetResponseDto {
|
|||||||
other.height == height &&
|
other.height == height &&
|
||||||
other.id == id &&
|
other.id == id &&
|
||||||
other.isArchived == isArchived &&
|
other.isArchived == isArchived &&
|
||||||
|
other.isEdited == isEdited &&
|
||||||
other.isFavorite == isFavorite &&
|
other.isFavorite == isFavorite &&
|
||||||
other.isOffline == isOffline &&
|
other.isOffline == isOffline &&
|
||||||
other.isTrashed == isTrashed &&
|
other.isTrashed == isTrashed &&
|
||||||
@@ -200,6 +204,7 @@ class AssetResponseDto {
|
|||||||
(height == null ? 0 : height!.hashCode) +
|
(height == null ? 0 : height!.hashCode) +
|
||||||
(id.hashCode) +
|
(id.hashCode) +
|
||||||
(isArchived.hashCode) +
|
(isArchived.hashCode) +
|
||||||
|
(isEdited.hashCode) +
|
||||||
(isFavorite.hashCode) +
|
(isFavorite.hashCode) +
|
||||||
(isOffline.hashCode) +
|
(isOffline.hashCode) +
|
||||||
(isTrashed.hashCode) +
|
(isTrashed.hashCode) +
|
||||||
@@ -223,7 +228,7 @@ class AssetResponseDto {
|
|||||||
(width == null ? 0 : width!.hashCode);
|
(width == null ? 0 : width!.hashCode);
|
||||||
|
|
||||||
@override
|
@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, 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, 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]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@@ -252,6 +257,7 @@ class AssetResponseDto {
|
|||||||
}
|
}
|
||||||
json[r'id'] = this.id;
|
json[r'id'] = this.id;
|
||||||
json[r'isArchived'] = this.isArchived;
|
json[r'isArchived'] = this.isArchived;
|
||||||
|
json[r'isEdited'] = this.isEdited;
|
||||||
json[r'isFavorite'] = this.isFavorite;
|
json[r'isFavorite'] = this.isFavorite;
|
||||||
json[r'isOffline'] = this.isOffline;
|
json[r'isOffline'] = this.isOffline;
|
||||||
json[r'isTrashed'] = this.isTrashed;
|
json[r'isTrashed'] = this.isTrashed;
|
||||||
@@ -332,6 +338,7 @@ class AssetResponseDto {
|
|||||||
: num.parse('${json[r'height']}'),
|
: num.parse('${json[r'height']}'),
|
||||||
id: mapValueOfType<String>(json, r'id')!,
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
isArchived: mapValueOfType<bool>(json, r'isArchived')!,
|
isArchived: mapValueOfType<bool>(json, r'isArchived')!,
|
||||||
|
isEdited: mapValueOfType<bool>(json, r'isEdited')!,
|
||||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
|
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
|
||||||
isOffline: mapValueOfType<bool>(json, r'isOffline')!,
|
isOffline: mapValueOfType<bool>(json, r'isOffline')!,
|
||||||
isTrashed: mapValueOfType<bool>(json, r'isTrashed')!,
|
isTrashed: mapValueOfType<bool>(json, r'isTrashed')!,
|
||||||
@@ -413,6 +420,7 @@ class AssetResponseDto {
|
|||||||
'height',
|
'height',
|
||||||
'id',
|
'id',
|
||||||
'isArchived',
|
'isArchived',
|
||||||
|
'isEdited',
|
||||||
'isFavorite',
|
'isFavorite',
|
||||||
'isOffline',
|
'isOffline',
|
||||||
'isTrashed',
|
'isTrashed',
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class DatabaseBackupDeleteDto {
|
||||||
|
/// Returns a new [DatabaseBackupDeleteDto] instance.
|
||||||
|
DatabaseBackupDeleteDto({
|
||||||
|
this.backups = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
List<String> backups;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is DatabaseBackupDeleteDto &&
|
||||||
|
_deepEquality.equals(other.backups, backups);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(backups.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'DatabaseBackupDeleteDto[backups=$backups]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'backups'] = this.backups;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [DatabaseBackupDeleteDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static DatabaseBackupDeleteDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "DatabaseBackupDeleteDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return DatabaseBackupDeleteDto(
|
||||||
|
backups: json[r'backups'] is Iterable
|
||||||
|
? (json[r'backups'] as Iterable).cast<String>().toList(growable: false)
|
||||||
|
: const [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<DatabaseBackupDeleteDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <DatabaseBackupDeleteDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = DatabaseBackupDeleteDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, DatabaseBackupDeleteDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, DatabaseBackupDeleteDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = DatabaseBackupDeleteDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of DatabaseBackupDeleteDto-objects as value to a dart map
|
||||||
|
static Map<String, List<DatabaseBackupDeleteDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<DatabaseBackupDeleteDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = DatabaseBackupDeleteDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'backups',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class DatabaseBackupDto {
|
||||||
|
/// Returns a new [DatabaseBackupDto] instance.
|
||||||
|
DatabaseBackupDto({
|
||||||
|
required this.filename,
|
||||||
|
required this.filesize,
|
||||||
|
});
|
||||||
|
|
||||||
|
String filename;
|
||||||
|
|
||||||
|
num filesize;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is DatabaseBackupDto &&
|
||||||
|
other.filename == filename &&
|
||||||
|
other.filesize == filesize;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(filename.hashCode) +
|
||||||
|
(filesize.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'DatabaseBackupDto[filename=$filename, filesize=$filesize]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'filename'] = this.filename;
|
||||||
|
json[r'filesize'] = this.filesize;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [DatabaseBackupDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static DatabaseBackupDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "DatabaseBackupDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return DatabaseBackupDto(
|
||||||
|
filename: mapValueOfType<String>(json, r'filename')!,
|
||||||
|
filesize: num.parse('${json[r'filesize']}'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<DatabaseBackupDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <DatabaseBackupDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = DatabaseBackupDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, DatabaseBackupDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, DatabaseBackupDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = DatabaseBackupDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of DatabaseBackupDto-objects as value to a dart map
|
||||||
|
static Map<String, List<DatabaseBackupDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<DatabaseBackupDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = DatabaseBackupDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'filename',
|
||||||
|
'filesize',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
//
|
||||||
|
// 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 DatabaseBackupListResponseDto {
|
||||||
|
/// Returns a new [DatabaseBackupListResponseDto] instance.
|
||||||
|
DatabaseBackupListResponseDto({
|
||||||
|
this.backups = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
List<DatabaseBackupDto> backups;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is DatabaseBackupListResponseDto &&
|
||||||
|
_deepEquality.equals(other.backups, backups);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(backups.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'DatabaseBackupListResponseDto[backups=$backups]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'backups'] = this.backups;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [DatabaseBackupListResponseDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static DatabaseBackupListResponseDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "DatabaseBackupListResponseDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return DatabaseBackupListResponseDto(
|
||||||
|
backups: DatabaseBackupDto.listFromJson(json[r'backups']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<DatabaseBackupListResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <DatabaseBackupListResponseDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = DatabaseBackupListResponseDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, DatabaseBackupListResponseDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, DatabaseBackupListResponseDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = DatabaseBackupListResponseDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of DatabaseBackupListResponseDto-objects as value to a dart map
|
||||||
|
static Map<String, List<DatabaseBackupListResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<DatabaseBackupListResponseDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = DatabaseBackupListResponseDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'backups',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@@ -25,11 +25,15 @@ class MaintenanceAction {
|
|||||||
|
|
||||||
static const start = MaintenanceAction._(r'start');
|
static const start = MaintenanceAction._(r'start');
|
||||||
static const end = MaintenanceAction._(r'end');
|
static const end = MaintenanceAction._(r'end');
|
||||||
|
static const selectDatabaseRestore = MaintenanceAction._(r'select_database_restore');
|
||||||
|
static const restoreDatabase = MaintenanceAction._(r'restore_database');
|
||||||
|
|
||||||
/// List of all possible values in this [enum][MaintenanceAction].
|
/// List of all possible values in this [enum][MaintenanceAction].
|
||||||
static const values = <MaintenanceAction>[
|
static const values = <MaintenanceAction>[
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
|
selectDatabaseRestore,
|
||||||
|
restoreDatabase,
|
||||||
];
|
];
|
||||||
|
|
||||||
static MaintenanceAction? fromJson(dynamic value) => MaintenanceActionTypeTransformer().decode(value);
|
static MaintenanceAction? fromJson(dynamic value) => MaintenanceActionTypeTransformer().decode(value);
|
||||||
@@ -70,6 +74,8 @@ class MaintenanceActionTypeTransformer {
|
|||||||
switch (data) {
|
switch (data) {
|
||||||
case r'start': return MaintenanceAction.start;
|
case r'start': return MaintenanceAction.start;
|
||||||
case r'end': return MaintenanceAction.end;
|
case r'end': return MaintenanceAction.end;
|
||||||
|
case r'select_database_restore': return MaintenanceAction.selectDatabaseRestore;
|
||||||
|
case r'restore_database': return MaintenanceAction.restoreDatabase;
|
||||||
default:
|
default:
|
||||||
if (!allowNull) {
|
if (!allowNull) {
|
||||||
throw ArgumentError('Unknown enum value to decode: $data');
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
//
|
||||||
|
// 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 MaintenanceDetectInstallResponseDto {
|
||||||
|
/// Returns a new [MaintenanceDetectInstallResponseDto] instance.
|
||||||
|
MaintenanceDetectInstallResponseDto({
|
||||||
|
this.storage = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
List<MaintenanceDetectInstallStorageFolderDto> storage;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is MaintenanceDetectInstallResponseDto &&
|
||||||
|
_deepEquality.equals(other.storage, storage);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(storage.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'MaintenanceDetectInstallResponseDto[storage=$storage]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'storage'] = this.storage;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [MaintenanceDetectInstallResponseDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static MaintenanceDetectInstallResponseDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "MaintenanceDetectInstallResponseDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return MaintenanceDetectInstallResponseDto(
|
||||||
|
storage: MaintenanceDetectInstallStorageFolderDto.listFromJson(json[r'storage']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<MaintenanceDetectInstallResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <MaintenanceDetectInstallResponseDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = MaintenanceDetectInstallResponseDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, MaintenanceDetectInstallResponseDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, MaintenanceDetectInstallResponseDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = MaintenanceDetectInstallResponseDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of MaintenanceDetectInstallResponseDto-objects as value to a dart map
|
||||||
|
static Map<String, List<MaintenanceDetectInstallResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<MaintenanceDetectInstallResponseDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = MaintenanceDetectInstallResponseDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'storage',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
+123
@@ -0,0 +1,123 @@
|
|||||||
|
//
|
||||||
|
// 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 MaintenanceDetectInstallStorageFolderDto {
|
||||||
|
/// Returns a new [MaintenanceDetectInstallStorageFolderDto] instance.
|
||||||
|
MaintenanceDetectInstallStorageFolderDto({
|
||||||
|
required this.files,
|
||||||
|
required this.folder,
|
||||||
|
required this.readable,
|
||||||
|
required this.writable,
|
||||||
|
});
|
||||||
|
|
||||||
|
num files;
|
||||||
|
|
||||||
|
StorageFolder folder;
|
||||||
|
|
||||||
|
bool readable;
|
||||||
|
|
||||||
|
bool writable;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is MaintenanceDetectInstallStorageFolderDto &&
|
||||||
|
other.files == files &&
|
||||||
|
other.folder == folder &&
|
||||||
|
other.readable == readable &&
|
||||||
|
other.writable == writable;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(files.hashCode) +
|
||||||
|
(folder.hashCode) +
|
||||||
|
(readable.hashCode) +
|
||||||
|
(writable.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'MaintenanceDetectInstallStorageFolderDto[files=$files, folder=$folder, readable=$readable, writable=$writable]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'files'] = this.files;
|
||||||
|
json[r'folder'] = this.folder;
|
||||||
|
json[r'readable'] = this.readable;
|
||||||
|
json[r'writable'] = this.writable;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [MaintenanceDetectInstallStorageFolderDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static MaintenanceDetectInstallStorageFolderDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "MaintenanceDetectInstallStorageFolderDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return MaintenanceDetectInstallStorageFolderDto(
|
||||||
|
files: num.parse('${json[r'files']}'),
|
||||||
|
folder: StorageFolder.fromJson(json[r'folder'])!,
|
||||||
|
readable: mapValueOfType<bool>(json, r'readable')!,
|
||||||
|
writable: mapValueOfType<bool>(json, r'writable')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<MaintenanceDetectInstallStorageFolderDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <MaintenanceDetectInstallStorageFolderDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = MaintenanceDetectInstallStorageFolderDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, MaintenanceDetectInstallStorageFolderDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, MaintenanceDetectInstallStorageFolderDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = MaintenanceDetectInstallStorageFolderDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of MaintenanceDetectInstallStorageFolderDto-objects as value to a dart map
|
||||||
|
static Map<String, List<MaintenanceDetectInstallStorageFolderDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<MaintenanceDetectInstallStorageFolderDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = MaintenanceDetectInstallStorageFolderDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'files',
|
||||||
|
'folder',
|
||||||
|
'readable',
|
||||||
|
'writable',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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 MaintenanceStatusResponseDto {
|
||||||
|
/// Returns a new [MaintenanceStatusResponseDto] instance.
|
||||||
|
MaintenanceStatusResponseDto({
|
||||||
|
required this.action,
|
||||||
|
required this.active,
|
||||||
|
this.error,
|
||||||
|
this.progress,
|
||||||
|
this.task,
|
||||||
|
});
|
||||||
|
|
||||||
|
MaintenanceAction action;
|
||||||
|
|
||||||
|
bool active;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// 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? 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.
|
||||||
|
///
|
||||||
|
num? progress;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// 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? task;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is MaintenanceStatusResponseDto &&
|
||||||
|
other.action == action &&
|
||||||
|
other.active == active &&
|
||||||
|
other.error == error &&
|
||||||
|
other.progress == progress &&
|
||||||
|
other.task == task;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(action.hashCode) +
|
||||||
|
(active.hashCode) +
|
||||||
|
(error == null ? 0 : error!.hashCode) +
|
||||||
|
(progress == null ? 0 : progress!.hashCode) +
|
||||||
|
(task == null ? 0 : task!.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'MaintenanceStatusResponseDto[action=$action, active=$active, error=$error, progress=$progress, task=$task]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'action'] = this.action;
|
||||||
|
json[r'active'] = this.active;
|
||||||
|
if (this.error != null) {
|
||||||
|
json[r'error'] = this.error;
|
||||||
|
} else {
|
||||||
|
// json[r'error'] = null;
|
||||||
|
}
|
||||||
|
if (this.progress != null) {
|
||||||
|
json[r'progress'] = this.progress;
|
||||||
|
} else {
|
||||||
|
// json[r'progress'] = null;
|
||||||
|
}
|
||||||
|
if (this.task != null) {
|
||||||
|
json[r'task'] = this.task;
|
||||||
|
} else {
|
||||||
|
// json[r'task'] = null;
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [MaintenanceStatusResponseDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static MaintenanceStatusResponseDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "MaintenanceStatusResponseDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return MaintenanceStatusResponseDto(
|
||||||
|
action: MaintenanceAction.fromJson(json[r'action'])!,
|
||||||
|
active: mapValueOfType<bool>(json, r'active')!,
|
||||||
|
error: mapValueOfType<String>(json, r'error'),
|
||||||
|
progress: num.parse('${json[r'progress']}'),
|
||||||
|
task: mapValueOfType<String>(json, r'task'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<MaintenanceStatusResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <MaintenanceStatusResponseDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = MaintenanceStatusResponseDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, MaintenanceStatusResponseDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, MaintenanceStatusResponseDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = MaintenanceStatusResponseDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of MaintenanceStatusResponseDto-objects as value to a dart map
|
||||||
|
static Map<String, List<MaintenanceStatusResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<MaintenanceStatusResponseDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = MaintenanceStatusResponseDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'action',
|
||||||
|
'active',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Generated
+12
@@ -62,6 +62,10 @@ class Permission {
|
|||||||
static const authPeriodChangePassword = Permission._(r'auth.changePassword');
|
static const authPeriodChangePassword = Permission._(r'auth.changePassword');
|
||||||
static const authDevicePeriodDelete = Permission._(r'authDevice.delete');
|
static const authDevicePeriodDelete = Permission._(r'authDevice.delete');
|
||||||
static const archivePeriodRead = Permission._(r'archive.read');
|
static const archivePeriodRead = Permission._(r'archive.read');
|
||||||
|
static const backupPeriodList = Permission._(r'backup.list');
|
||||||
|
static const backupPeriodDownload = Permission._(r'backup.download');
|
||||||
|
static const backupPeriodUpload = Permission._(r'backup.upload');
|
||||||
|
static const backupPeriodDelete = Permission._(r'backup.delete');
|
||||||
static const duplicatePeriodRead = Permission._(r'duplicate.read');
|
static const duplicatePeriodRead = Permission._(r'duplicate.read');
|
||||||
static const duplicatePeriodDelete = Permission._(r'duplicate.delete');
|
static const duplicatePeriodDelete = Permission._(r'duplicate.delete');
|
||||||
static const facePeriodCreate = Permission._(r'face.create');
|
static const facePeriodCreate = Permission._(r'face.create');
|
||||||
@@ -214,6 +218,10 @@ class Permission {
|
|||||||
authPeriodChangePassword,
|
authPeriodChangePassword,
|
||||||
authDevicePeriodDelete,
|
authDevicePeriodDelete,
|
||||||
archivePeriodRead,
|
archivePeriodRead,
|
||||||
|
backupPeriodList,
|
||||||
|
backupPeriodDownload,
|
||||||
|
backupPeriodUpload,
|
||||||
|
backupPeriodDelete,
|
||||||
duplicatePeriodRead,
|
duplicatePeriodRead,
|
||||||
duplicatePeriodDelete,
|
duplicatePeriodDelete,
|
||||||
facePeriodCreate,
|
facePeriodCreate,
|
||||||
@@ -401,6 +409,10 @@ class PermissionTypeTransformer {
|
|||||||
case r'auth.changePassword': return Permission.authPeriodChangePassword;
|
case r'auth.changePassword': return Permission.authPeriodChangePassword;
|
||||||
case r'authDevice.delete': return Permission.authDevicePeriodDelete;
|
case r'authDevice.delete': return Permission.authDevicePeriodDelete;
|
||||||
case r'archive.read': return Permission.archivePeriodRead;
|
case r'archive.read': return Permission.archivePeriodRead;
|
||||||
|
case r'backup.list': return Permission.backupPeriodList;
|
||||||
|
case r'backup.download': return Permission.backupPeriodDownload;
|
||||||
|
case r'backup.upload': return Permission.backupPeriodUpload;
|
||||||
|
case r'backup.delete': return Permission.backupPeriodDelete;
|
||||||
case r'duplicate.read': return Permission.duplicatePeriodRead;
|
case r'duplicate.read': return Permission.duplicatePeriodRead;
|
||||||
case r'duplicate.delete': return Permission.duplicatePeriodDelete;
|
case r'duplicate.delete': return Permission.duplicatePeriodDelete;
|
||||||
case r'face.create': return Permission.facePeriodCreate;
|
case r'face.create': return Permission.facePeriodCreate;
|
||||||
|
|||||||
+20
-3
@@ -14,25 +14,41 @@ class SetMaintenanceModeDto {
|
|||||||
/// Returns a new [SetMaintenanceModeDto] instance.
|
/// Returns a new [SetMaintenanceModeDto] instance.
|
||||||
SetMaintenanceModeDto({
|
SetMaintenanceModeDto({
|
||||||
required this.action,
|
required this.action,
|
||||||
|
this.restoreBackupFilename,
|
||||||
});
|
});
|
||||||
|
|
||||||
MaintenanceAction action;
|
MaintenanceAction action;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// 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? restoreBackupFilename;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is SetMaintenanceModeDto &&
|
bool operator ==(Object other) => identical(this, other) || other is SetMaintenanceModeDto &&
|
||||||
other.action == action;
|
other.action == action &&
|
||||||
|
other.restoreBackupFilename == restoreBackupFilename;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(action.hashCode);
|
(action.hashCode) +
|
||||||
|
(restoreBackupFilename == null ? 0 : restoreBackupFilename!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SetMaintenanceModeDto[action=$action]';
|
String toString() => 'SetMaintenanceModeDto[action=$action, restoreBackupFilename=$restoreBackupFilename]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
json[r'action'] = this.action;
|
json[r'action'] = this.action;
|
||||||
|
if (this.restoreBackupFilename != null) {
|
||||||
|
json[r'restoreBackupFilename'] = this.restoreBackupFilename;
|
||||||
|
} else {
|
||||||
|
// json[r'restoreBackupFilename'] = null;
|
||||||
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +62,7 @@ class SetMaintenanceModeDto {
|
|||||||
|
|
||||||
return SetMaintenanceModeDto(
|
return SetMaintenanceModeDto(
|
||||||
action: MaintenanceAction.fromJson(json[r'action'])!,
|
action: MaintenanceAction.fromJson(json[r'action'])!,
|
||||||
|
restoreBackupFilename: mapValueOfType<String>(json, r'restoreBackupFilename'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
//
|
||||||
|
// 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 StorageFolder {
|
||||||
|
/// Instantiate a new enum with the provided [value].
|
||||||
|
const StorageFolder._(this.value);
|
||||||
|
|
||||||
|
/// The underlying value of this enum member.
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => value;
|
||||||
|
|
||||||
|
String toJson() => value;
|
||||||
|
|
||||||
|
static const encodedVideo = StorageFolder._(r'encoded-video');
|
||||||
|
static const library_ = StorageFolder._(r'library');
|
||||||
|
static const upload = StorageFolder._(r'upload');
|
||||||
|
static const profile = StorageFolder._(r'profile');
|
||||||
|
static const thumbs = StorageFolder._(r'thumbs');
|
||||||
|
static const backups = StorageFolder._(r'backups');
|
||||||
|
|
||||||
|
/// List of all possible values in this [enum][StorageFolder].
|
||||||
|
static const values = <StorageFolder>[
|
||||||
|
encodedVideo,
|
||||||
|
library_,
|
||||||
|
upload,
|
||||||
|
profile,
|
||||||
|
thumbs,
|
||||||
|
backups,
|
||||||
|
];
|
||||||
|
|
||||||
|
static StorageFolder? fromJson(dynamic value) => StorageFolderTypeTransformer().decode(value);
|
||||||
|
|
||||||
|
static List<StorageFolder> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <StorageFolder>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = StorageFolder.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transformation class that can [encode] an instance of [StorageFolder] to String,
|
||||||
|
/// and [decode] dynamic data back to [StorageFolder].
|
||||||
|
class StorageFolderTypeTransformer {
|
||||||
|
factory StorageFolderTypeTransformer() => _instance ??= const StorageFolderTypeTransformer._();
|
||||||
|
|
||||||
|
const StorageFolderTypeTransformer._();
|
||||||
|
|
||||||
|
String encode(StorageFolder data) => data.value;
|
||||||
|
|
||||||
|
/// Decodes a [dynamic value][data] to a StorageFolder.
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
StorageFolder? decode(dynamic data, {bool allowNull = true}) {
|
||||||
|
if (data != null) {
|
||||||
|
switch (data) {
|
||||||
|
case r'encoded-video': return StorageFolder.encodedVideo;
|
||||||
|
case r'library': return StorageFolder.library_;
|
||||||
|
case r'upload': return StorageFolder.upload;
|
||||||
|
case r'profile': return StorageFolder.profile;
|
||||||
|
case r'thumbs': return StorageFolder.thumbs;
|
||||||
|
case r'backups': return StorageFolder.backups;
|
||||||
|
default:
|
||||||
|
if (!allowNull) {
|
||||||
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Singleton [StorageFolderTypeTransformer] instance.
|
||||||
|
static StorageFolderTypeTransformer? _instance;
|
||||||
|
}
|
||||||
|
|
||||||
+9
-1
@@ -20,6 +20,7 @@ class SyncAssetV1 {
|
|||||||
required this.fileModifiedAt,
|
required this.fileModifiedAt,
|
||||||
required this.height,
|
required this.height,
|
||||||
required this.id,
|
required this.id,
|
||||||
|
required this.isEdited,
|
||||||
required this.isFavorite,
|
required this.isFavorite,
|
||||||
required this.libraryId,
|
required this.libraryId,
|
||||||
required this.livePhotoVideoId,
|
required this.livePhotoVideoId,
|
||||||
@@ -47,6 +48,8 @@ class SyncAssetV1 {
|
|||||||
|
|
||||||
String id;
|
String id;
|
||||||
|
|
||||||
|
bool isEdited;
|
||||||
|
|
||||||
bool isFavorite;
|
bool isFavorite;
|
||||||
|
|
||||||
String? libraryId;
|
String? libraryId;
|
||||||
@@ -78,6 +81,7 @@ class SyncAssetV1 {
|
|||||||
other.fileModifiedAt == fileModifiedAt &&
|
other.fileModifiedAt == fileModifiedAt &&
|
||||||
other.height == height &&
|
other.height == height &&
|
||||||
other.id == id &&
|
other.id == id &&
|
||||||
|
other.isEdited == isEdited &&
|
||||||
other.isFavorite == isFavorite &&
|
other.isFavorite == isFavorite &&
|
||||||
other.libraryId == libraryId &&
|
other.libraryId == libraryId &&
|
||||||
other.livePhotoVideoId == livePhotoVideoId &&
|
other.livePhotoVideoId == livePhotoVideoId &&
|
||||||
@@ -100,6 +104,7 @@ class SyncAssetV1 {
|
|||||||
(fileModifiedAt == null ? 0 : fileModifiedAt!.hashCode) +
|
(fileModifiedAt == null ? 0 : fileModifiedAt!.hashCode) +
|
||||||
(height == null ? 0 : height!.hashCode) +
|
(height == null ? 0 : height!.hashCode) +
|
||||||
(id.hashCode) +
|
(id.hashCode) +
|
||||||
|
(isEdited.hashCode) +
|
||||||
(isFavorite.hashCode) +
|
(isFavorite.hashCode) +
|
||||||
(libraryId == null ? 0 : libraryId!.hashCode) +
|
(libraryId == null ? 0 : libraryId!.hashCode) +
|
||||||
(livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
|
(livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
|
||||||
@@ -113,7 +118,7 @@ class SyncAssetV1 {
|
|||||||
(width == null ? 0 : width!.hashCode);
|
(width == null ? 0 : width!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, duration=$duration, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, height=$height, id=$id, isFavorite=$isFavorite, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, ownerId=$ownerId, stackId=$stackId, thumbhash=$thumbhash, type=$type, visibility=$visibility, width=$width]';
|
String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, duration=$duration, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, height=$height, id=$id, isEdited=$isEdited, isFavorite=$isFavorite, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, ownerId=$ownerId, stackId=$stackId, thumbhash=$thumbhash, type=$type, visibility=$visibility, width=$width]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@@ -144,6 +149,7 @@ class SyncAssetV1 {
|
|||||||
// json[r'height'] = null;
|
// json[r'height'] = null;
|
||||||
}
|
}
|
||||||
json[r'id'] = this.id;
|
json[r'id'] = this.id;
|
||||||
|
json[r'isEdited'] = this.isEdited;
|
||||||
json[r'isFavorite'] = this.isFavorite;
|
json[r'isFavorite'] = this.isFavorite;
|
||||||
if (this.libraryId != null) {
|
if (this.libraryId != null) {
|
||||||
json[r'libraryId'] = this.libraryId;
|
json[r'libraryId'] = this.libraryId;
|
||||||
@@ -198,6 +204,7 @@ class SyncAssetV1 {
|
|||||||
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r''),
|
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r''),
|
||||||
height: mapValueOfType<int>(json, r'height'),
|
height: mapValueOfType<int>(json, r'height'),
|
||||||
id: mapValueOfType<String>(json, r'id')!,
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
|
isEdited: mapValueOfType<bool>(json, r'isEdited')!,
|
||||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
|
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
|
||||||
libraryId: mapValueOfType<String>(json, r'libraryId'),
|
libraryId: mapValueOfType<String>(json, r'libraryId'),
|
||||||
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
|
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
|
||||||
@@ -263,6 +270,7 @@ class SyncAssetV1 {
|
|||||||
'fileModifiedAt',
|
'fileModifiedAt',
|
||||||
'height',
|
'height',
|
||||||
'id',
|
'id',
|
||||||
|
'isEdited',
|
||||||
'isFavorite',
|
'isFavorite',
|
||||||
'libraryId',
|
'libraryId',
|
||||||
'livePhotoVideoId',
|
'livePhotoVideoId',
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ SyncAssetV1 _createAsset({
|
|||||||
livePhotoVideoId: null,
|
livePhotoVideoId: null,
|
||||||
stackId: null,
|
stackId: null,
|
||||||
thumbhash: null,
|
thumbhash: null,
|
||||||
|
isEdited: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
@@ -19,6 +19,7 @@ import 'schema_v13.dart' as v13;
|
|||||||
import 'schema_v14.dart' as v14;
|
import 'schema_v14.dart' as v14;
|
||||||
import 'schema_v15.dart' as v15;
|
import 'schema_v15.dart' as v15;
|
||||||
import 'schema_v16.dart' as v16;
|
import 'schema_v16.dart' as v16;
|
||||||
|
import 'schema_v17.dart' as v17;
|
||||||
|
|
||||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
@override
|
@override
|
||||||
@@ -56,6 +57,8 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
|||||||
return v15.DatabaseAtV15(db);
|
return v15.DatabaseAtV15(db);
|
||||||
case 16:
|
case 16:
|
||||||
return v16.DatabaseAtV16(db);
|
return v16.DatabaseAtV16(db);
|
||||||
|
case 17:
|
||||||
|
return v17.DatabaseAtV17(db);
|
||||||
default:
|
default:
|
||||||
throw MissingSchemaException(version, versions);
|
throw MissingSchemaException(version, versions);
|
||||||
}
|
}
|
||||||
@@ -78,5 +81,6 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
|||||||
14,
|
14,
|
||||||
15,
|
15,
|
||||||
16,
|
16,
|
||||||
|
17,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
+8337
File diff suppressed because it is too large
Load Diff
Vendored
+2
@@ -64,6 +64,7 @@ abstract final class LocalAssetStub {
|
|||||||
type: AssetType.image,
|
type: AssetType.image,
|
||||||
createdAt: DateTime(2025),
|
createdAt: DateTime(2025),
|
||||||
updatedAt: DateTime(2025, 2),
|
updatedAt: DateTime(2025, 2),
|
||||||
|
isEdited: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
static final image2 = LocalAsset(
|
static final image2 = LocalAsset(
|
||||||
@@ -72,5 +73,6 @@ abstract final class LocalAssetStub {
|
|||||||
type: AssetType.image,
|
type: AssetType.image,
|
||||||
createdAt: DateTime(2000),
|
createdAt: DateTime(2000),
|
||||||
updatedAt: DateTime(20021),
|
updatedAt: DateTime(20021),
|
||||||
|
isEdited: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -128,6 +128,7 @@ abstract final class SyncStreamStub {
|
|||||||
visibility: AssetVisibility.timeline,
|
visibility: AssetVisibility.timeline,
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
|
isEdited: false,
|
||||||
),
|
),
|
||||||
ack: ack,
|
ack: ack,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ void main() {
|
|||||||
latitude: 37.7749,
|
latitude: 37.7749,
|
||||||
longitude: -122.4194,
|
longitude: -122.4194,
|
||||||
adjustmentTime: DateTime(2026, 1, 2),
|
adjustmentTime: DateTime(2026, 1, 2),
|
||||||
|
isEdited: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
final mockEntity = MockAssetEntity();
|
final mockEntity = MockAssetEntity();
|
||||||
@@ -242,6 +243,7 @@ void main() {
|
|||||||
cloudId: 'cloud-id-123',
|
cloudId: 'cloud-id-123',
|
||||||
latitude: 37.7749,
|
latitude: 37.7749,
|
||||||
longitude: -122.4194,
|
longitude: -122.4194,
|
||||||
|
isEdited: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
final mockEntity = MockAssetEntity();
|
final mockEntity = MockAssetEntity();
|
||||||
@@ -279,6 +281,7 @@ void main() {
|
|||||||
createdAt: DateTime(2025, 1, 1),
|
createdAt: DateTime(2025, 1, 1),
|
||||||
updatedAt: DateTime(2025, 1, 2),
|
updatedAt: DateTime(2025, 1, 2),
|
||||||
cloudId: null, // No cloudId
|
cloudId: null, // No cloudId
|
||||||
|
isEdited: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
final mockEntity = MockAssetEntity();
|
final mockEntity = MockAssetEntity();
|
||||||
@@ -320,6 +323,7 @@ void main() {
|
|||||||
cloudId: 'cloud-id-livephoto',
|
cloudId: 'cloud-id-livephoto',
|
||||||
latitude: 37.7749,
|
latitude: 37.7749,
|
||||||
longitude: -122.4194,
|
longitude: -122.4194,
|
||||||
|
isEdited: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
final mockEntity = MockAssetEntity();
|
final mockEntity = MockAssetEntity();
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ abstract final class TestUtils {
|
|||||||
isFavorite: false,
|
isFavorite: false,
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
|
isEdited: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,6 +155,7 @@ abstract final class TestUtils {
|
|||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
orientation: orientation,
|
orientation: orientation,
|
||||||
|
isEdited: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class MediumFactory {
|
|||||||
type: type ?? AssetType.image,
|
type: type ?? AssetType.image,
|
||||||
createdAt: createdAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)),
|
createdAt: createdAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)),
|
||||||
updatedAt: updatedAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)),
|
updatedAt: updatedAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)),
|
||||||
|
isEdited: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user