Compare commits

..

1 Commits

Author SHA1 Message Date
renovate[bot]
c71a23f4f5 fix(deps): update mobile 2026-01-19 18:23:10 +00:00
189 changed files with 2359 additions and 17079 deletions

View File

@@ -20,7 +20,7 @@
"@types/lodash-es": "^4.17.12",
"@types/micromatch": "^4.0.9",
"@types/mock-fs": "^4.13.1",
"@types/node": "^24.10.8",
"@types/node": "^24.10.4",
"@vitest/coverage-v8": "^3.0.0",
"byte-size": "^9.0.0",
"cli-progress": "^3.12.0",

View File

@@ -27,7 +27,7 @@
"@playwright/test": "^1.44.1",
"@socket.io/component-emitter": "^3.1.2",
"@types/luxon": "^3.4.2",
"@types/node": "^24.10.8",
"@types/node": "^24.10.4",
"@types/pg": "^8.15.1",
"@types/pngjs": "^6.0.4",
"@types/supertest": "^6.0.2",

View File

@@ -1,350 +0,0 @@
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,
);
});
});
});

View File

@@ -14,7 +14,6 @@ describe('/admin/maintenance', () => {
await utils.resetDatabase();
admin = await utils.adminSetup();
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
await utils.resetBackups(admin.accessToken);
});
// => outside of maintenance mode
@@ -27,17 +26,6 @@ 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 () => {
it('should not work out of maintenance mode', async () => {
const { status, body } = await request(app).post('/admin/maintenance/login').send({ token: 'token' });
@@ -51,7 +39,6 @@ describe('/admin/maintenance', () => {
describe.sequential('POST /', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/admin/maintenance').send({
active: false,
action: 'end',
});
expect(status).toBe(401);
@@ -82,7 +69,6 @@ describe('/admin/maintenance', () => {
.send({
action: 'start',
});
expect(status).toBe(201);
cookie = headers['set-cookie'][0].split(';')[0];
@@ -93,13 +79,12 @@ describe('/admin/maintenance', () => {
await expect
.poll(
async () => {
const { status, body } = await request(app).get('/server/config');
expect(status).toBe(200);
const { body } = await request(app).get('/server/config');
return body.maintenanceMode;
},
{
interval: 500,
timeout: 10_000,
interval: 5e2,
timeout: 1e4,
},
)
.toBeTruthy();
@@ -117,17 +102,6 @@ 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 () => {
it('should fail without cookie or token in body', async () => {
const { status, body } = await request(app).post('/admin/maintenance/login').send({});
@@ -184,13 +158,12 @@ describe('/admin/maintenance', () => {
await expect
.poll(
async () => {
const { status, body } = await request(app).get('/server/config');
expect(status).toBe(200);
const { body } = await request(app).get('/server/config');
return body.maintenanceMode;
},
{
interval: 500,
timeout: 10_000,
interval: 5e2,
timeout: 1e4,
},
)
.toBeFalsy();

View File

@@ -348,7 +348,6 @@ export function toAssetResponseDto(asset: MockTimelineAsset, owner?: UserRespons
checksum: asset.checksum,
width: exifInfo.exifImageWidth ?? 1,
height: exifInfo.exifImageHeight ?? 1,
isEdited: false,
};
}

View File

@@ -6,9 +6,7 @@ import {
CheckExistingAssetsDto,
CreateAlbumDto,
CreateLibraryDto,
JobCreateDto,
MaintenanceAction,
ManualJobName,
MetadataSearchDto,
Permission,
PersonCreateDto,
@@ -23,7 +21,6 @@ import {
checkExistingAssets,
createAlbum,
createApiKey,
createJob,
createLibrary,
createPartner,
createPerson,
@@ -31,12 +28,10 @@ import {
createStack,
createUserAdmin,
deleteAssets,
deleteDatabaseBackup,
getAssetInfo,
getConfig,
getConfigDefaults,
getQueuesLegacy,
listDatabaseBackups,
login,
runQueueCommandLegacy,
scanLibrary,
@@ -57,15 +52,11 @@ import {
import { BrowserContext } from '@playwright/test';
import { exec, spawn } from 'node:child_process';
import { createHash } from 'node:crypto';
import { createWriteStream, existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
import { mkdtemp } from 'node:fs/promises';
import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
import { dirname, resolve } from 'node:path';
import { setTimeout as setAsyncTimeout } from 'node:timers/promises';
import { promisify } from 'node:util';
import { createGzip } from 'node:zlib';
import pg from 'pg';
import { io, type Socket } from 'socket.io-client';
import { loginDto, signupDto } from 'src/fixtures';
@@ -93,9 +84,8 @@ export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer $
export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
export const immichCli = (args: string[]) =>
executeCommand('pnpm', ['exec', 'immich', '-d', `/${tempDir}/immich/`, ...args], { cwd: '../cli' }).promise;
export const dockerExec = (args: string[]) =>
executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', args.join(' ')]);
export const immichAdmin = (args: string[]) => dockerExec([`immich-admin ${args.join(' ')}`]);
export const immichAdmin = (args: string[]) =>
executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]);
export const specialCharStrings = ["'", '"', ',', '{', '}', '*'];
export const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
@@ -159,26 +149,12 @@ const onEvent = ({ event, id }: { event: EventType; id: string }) => {
};
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[]) => {
try {
client = await utils.connectDatabase();
if (!client) {
client = new pg.Client(dbUrl);
await client.connect();
}
tables = tables || [
// TODO e2e test for deleting a stack, since it is quite complex
@@ -505,9 +481,6 @@ export const utils = {
tagAssets: (accessToken: string, tagId: string, assetIds: string[]) =>
tagAssets({ id: tagId, bulkIdsDto: { ids: assetIds } }, { headers: asBearerAuth(accessToken) }),
createJob: async (accessToken: string, jobCreateDto: JobCreateDto) =>
createJob({ jobCreateDto }, { headers: asBearerAuth(accessToken) }),
queueCommand: async (accessToken: string, name: QueueName, queueCommandDto: QueueCommandDto) =>
runQueueCommandLegacy({ name, queueCommandDto }, { headers: asBearerAuth(accessToken) }),
@@ -586,45 +559,6 @@ export const utils = {
mkdirSync(`${testAssetDir}/temp`, { recursive: true });
},
async move(source: string, dest: string) {
return executeCommand('docker', ['exec', 'immich-e2e-server', 'mv', source, dest]).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) });
const backupFiles = backups.map((b) => b.filename);
await deleteDatabaseBackup(
{ databaseBackupDeleteDto: { backups: backupFiles } },
{ 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) => {
const defaultConfig = await getConfigDefaults({ headers: asBearerAuth(accessToken) });
await updateConfig({ systemConfigDto: defaultConfig }, { headers: asBearerAuth(accessToken) });
@@ -667,25 +601,6 @@ export const utils = {
await utils.waitForQueueFinish(accessToken, 'sidecar');
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();

View File

@@ -1,105 +0,0 @@
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 });
});
});

View File

@@ -16,12 +16,12 @@ test.describe('Maintenance', () => {
test('enter and exit maintenance mode', async ({ context, page }) => {
await utils.setAuthCookies(context, admin.accessToken);
await page.goto('/admin/maintenance');
await page.getByRole('button', { name: 'Switch to maintenance mode' }).click();
await page.goto('/admin/system-settings?isOpen=maintenance');
await page.getByRole('button', { name: 'Start maintenance mode' }).click();
await expect(page.getByText('Temporarily Unavailable')).toBeVisible({ timeout: 10_000 });
await page.getByRole('button', { name: 'End maintenance mode' }).click();
await page.waitForURL('**/admin/maintenance*', { timeout: 10_000 });
await page.waitForURL('**/admin/system-settings*', { timeout: 10_000 });
});
test('maintenance shows no options to users until they authenticate', async ({ page }) => {

View File

@@ -188,21 +188,10 @@
"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_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_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_description": "Put Immich into maintenance mode.",
"maintenance_start": "Switch to maintenance mode",
"maintenance_start": "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_description": "Navigate to the jobs page to manage job concurrency",
"manage_log_settings": "Manage log settings",
@@ -614,7 +603,7 @@
"backup_album_selection_page_select_albums": "Select albums",
"backup_album_selection_page_selection_info": "Selection Info",
"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_background_service_backup_failed_message": "Failed to backup assets. Retrying…",
"backup_background_service_complete_notification": "Asset backup complete",
@@ -1415,28 +1404,10 @@
"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_menu": "Main menu",
"maintenance_action_restore": "Restoring Database",
"maintenance_description": "Immich has been put into <link>maintenance mode</link>.",
"maintenance_end": "End maintenance mode",
"maintenance_end_error": "Failed to end maintenance mode.",
"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",
"make": "Make",
"manage_geolocation": "Manage location",
@@ -2244,7 +2215,6 @@
"unhide_person": "Unhide person",
"unknown": "Unknown",
"unknown_country": "Unknown Country",
"unknown_date": "Unknown date",
"unknown_year": "Unknown Year",
"unlimited": "Unlimited",
"unlink_motion_video": "Unlink motion video",
@@ -2287,7 +2257,7 @@
"url": "URL",
"usage": "Usage",
"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",
"user": "User",
"user_has_been_deleted": "This user has been deleted.",

View File

@@ -3,7 +3,7 @@ experimental_monorepo_root = true
[tools]
node = "24.13.0"
flutter = "3.35.7"
pnpm = "10.28.0"
pnpm = "10.27.0"
terragrunt = "0.93.10"
opentofu = "1.10.7"
java = "25.0.1"

View File

@@ -1,3 +1,3 @@
{
"flutter": "3.35.7"
"flutter": "3.38.7"
}

View File

@@ -117,9 +117,6 @@
<data
android:host="my.immich.app"
android:pathPrefix="/memories/" />
<data
android:host="my.immich.app"
android:path="/memory" />
<data
android:host="my.immich.app"
android:pathPrefix="/photos/" />

File diff suppressed because one or more lines are too long

View File

@@ -6,8 +6,8 @@ environment:
dependencies:
analyzer: ^7.0.0
analyzer_plugin: ^0.13.0
custom_lint_builder: ^0.7.5
analyzer_plugin: 0.14.0
custom_lint_builder: 0.8.1
glob: ^2.1.2
dev_dependencies:

View File

@@ -22,7 +22,6 @@ sealed class BaseAsset {
final int? durationInSeconds;
final bool isFavorite;
final String? livePhotoVideoId;
final bool isEdited;
const BaseAsset({
required this.name,
@@ -35,7 +34,6 @@ sealed class BaseAsset {
this.durationInSeconds,
this.isFavorite = false,
this.livePhotoVideoId,
required this.isEdited,
});
bool get isImage => type == AssetType.image;
@@ -73,7 +71,6 @@ sealed class BaseAsset {
height: ${height ?? "<NA>"},
durationInSeconds: ${durationInSeconds ?? "<NA>"},
isFavorite: $isFavorite,
isEdited: $isEdited,
}''';
}
@@ -88,8 +85,7 @@ sealed class BaseAsset {
width == other.width &&
height == other.height &&
durationInSeconds == other.durationInSeconds &&
isFavorite == other.isFavorite &&
isEdited == other.isEdited;
isFavorite == other.isFavorite;
}
return false;
}
@@ -103,7 +99,6 @@ sealed class BaseAsset {
width.hashCode ^
height.hashCode ^
durationInSeconds.hashCode ^
isFavorite.hashCode ^
isEdited.hashCode;
isFavorite.hashCode;
}
}

View File

@@ -28,7 +28,6 @@ class LocalAsset extends BaseAsset {
this.adjustmentTime,
this.latitude,
this.longitude,
required super.isEdited,
}) : remoteAssetId = remoteId;
@override
@@ -108,7 +107,6 @@ class LocalAsset extends BaseAsset {
DateTime? adjustmentTime,
double? latitude,
double? longitude,
bool? isEdited,
}) {
return LocalAsset(
id: id ?? this.id,
@@ -127,7 +125,6 @@ class LocalAsset extends BaseAsset {
adjustmentTime: adjustmentTime ?? this.adjustmentTime,
latitude: latitude ?? this.latitude,
longitude: longitude ?? this.longitude,
isEdited: isEdited ?? this.isEdited,
);
}
}

View File

@@ -28,7 +28,6 @@ class RemoteAsset extends BaseAsset {
this.visibility = AssetVisibility.timeline,
super.livePhotoVideoId,
this.stackId,
required super.isEdited,
}) : localAssetId = localId;
@override
@@ -105,7 +104,6 @@ class RemoteAsset extends BaseAsset {
AssetVisibility? visibility,
String? livePhotoVideoId,
String? stackId,
bool? isEdited,
}) {
return RemoteAsset(
id: id ?? this.id,
@@ -124,7 +122,6 @@ class RemoteAsset extends BaseAsset {
visibility: visibility ?? this.visibility,
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
stackId: stackId ?? this.stackId,
isEdited: isEdited ?? this.isEdited,
);
}
}

View File

@@ -436,6 +436,5 @@ extension PlatformToLocalAsset on PlatformAsset {
adjustmentTime: tryFromSecondsSinceEpoch(adjustmentTime, isUtc: true),
latitude: latitude,
longitude: longitude,
isEdited: false,
);
}

View File

@@ -77,7 +77,6 @@ extension on AssetResponseDto {
thumbHash: thumbhash,
localId: null,
type: type.toAssetType(),
isEdited: isEdited,
);
}
}

View File

@@ -247,42 +247,6 @@ 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 {
if (checksums.isEmpty) {
return Future.value();

View File

@@ -196,16 +196,6 @@ 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() {
if (_linkedAlbumSyncTask != null) {
return _linkedAlbumSyncTask!.future;
@@ -241,8 +231,3 @@ Cancelable<void> _handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) => ru
computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetUploadReadyV1Batch(batchData),
debugLabel: 'websocket-batch',
);
Cancelable<void> _handleWsAssetEditReadyV1Batch(List<dynamic> batchData) => runInIsolateGentle(
computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetEditReadyV1Batch(batchData),
debugLabel: 'websocket-edit',
);

View File

@@ -47,6 +47,5 @@ extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData {
latitude: latitude,
longitude: longitude,
cloudId: iCloudId,
isEdited: false,
);
}

View File

@@ -25,8 +25,7 @@ SELECT
NULL as i_cloud_id,
NULL as latitude,
NULL as longitude,
NULL as adjustmentTime,
rae.is_edited
NULL as adjustmentTime
FROM
remote_asset_entity rae
LEFT JOIN
@@ -62,8 +61,7 @@ SELECT
lae.i_cloud_id,
lae.latitude,
lae.longitude,
lae.adjustment_time,
0 as is_edited
lae.adjustment_time
FROM
local_asset_entity lae
WHERE NOT EXISTS (

View File

@@ -29,7 +29,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
);
$arrayStartIndex += generatedlimit.amountOfVariables;
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, 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}',
'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}',
variables: [
for (var $ in userIds) i0.Variable<String>($),
...generatedlimit.introducedVariables,
@@ -66,7 +66,6 @@ class MergedAssetDrift extends i1.ModularAccessor {
latitude: row.readNullable<double>('latitude'),
longitude: row.readNullable<double>('longitude'),
adjustmentTime: row.readNullable<DateTime>('adjustmentTime'),
isEdited: row.read<bool>('is_edited'),
),
);
}
@@ -138,7 +137,6 @@ class MergedAssetResult {
final double? latitude;
final double? longitude;
final DateTime? adjustmentTime;
final bool isEdited;
MergedAssetResult({
this.remoteId,
this.localId,
@@ -160,7 +158,6 @@ class MergedAssetResult {
this.latitude,
this.longitude,
this.adjustmentTime,
required this.isEdited,
});
}

View File

@@ -44,8 +44,6 @@ class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin
TextColumn get libraryId => text().nullable()();
BoolColumn get isEdited => boolean().withDefault(const Constant(false))();
@override
Set<Column> get primaryKey => {id};
}
@@ -68,6 +66,5 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
livePhotoVideoId: livePhotoVideoId,
localId: localId,
stackId: stackId,
isEdited: isEdited,
);
}

View File

@@ -31,7 +31,6 @@ typedef $$RemoteAssetEntityTableCreateCompanionBuilder =
required i2.AssetVisibility visibility,
i0.Value<String?> stackId,
i0.Value<String?> libraryId,
i0.Value<bool> isEdited,
});
typedef $$RemoteAssetEntityTableUpdateCompanionBuilder =
i1.RemoteAssetEntityCompanion Function({
@@ -53,7 +52,6 @@ typedef $$RemoteAssetEntityTableUpdateCompanionBuilder =
i0.Value<i2.AssetVisibility> visibility,
i0.Value<String?> stackId,
i0.Value<String?> libraryId,
i0.Value<bool> isEdited,
});
final class $$RemoteAssetEntityTableReferences
@@ -198,11 +196,6 @@ class $$RemoteAssetEntityTableFilterComposer
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<bool> get isEdited => $composableBuilder(
column: $table.isEdited,
builder: (column) => i0.ColumnFilters(column),
);
i5.$$UserEntityTableFilterComposer get ownerId {
final i5.$$UserEntityTableFilterComposer composer = $composerBuilder(
composer: this,
@@ -325,11 +318,6 @@ class $$RemoteAssetEntityTableOrderingComposer
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<bool> get isEdited => $composableBuilder(
column: $table.isEdited,
builder: (column) => i0.ColumnOrderings(column),
);
i5.$$UserEntityTableOrderingComposer get ownerId {
final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder(
composer: this,
@@ -429,9 +417,6 @@ class $$RemoteAssetEntityTableAnnotationComposer
i0.GeneratedColumn<String> get libraryId =>
$composableBuilder(column: $table.libraryId, builder: (column) => column);
i0.GeneratedColumn<bool> get isEdited =>
$composableBuilder(column: $table.isEdited, builder: (column) => column);
i5.$$UserEntityTableAnnotationComposer get ownerId {
final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
composer: this,
@@ -512,7 +497,6 @@ class $$RemoteAssetEntityTableTableManager
const i0.Value.absent(),
i0.Value<String?> stackId = const i0.Value.absent(),
i0.Value<String?> libraryId = const i0.Value.absent(),
i0.Value<bool> isEdited = const i0.Value.absent(),
}) => i1.RemoteAssetEntityCompanion(
name: name,
type: type,
@@ -532,7 +516,6 @@ class $$RemoteAssetEntityTableTableManager
visibility: visibility,
stackId: stackId,
libraryId: libraryId,
isEdited: isEdited,
),
createCompanionCallback:
({
@@ -554,7 +537,6 @@ class $$RemoteAssetEntityTableTableManager
required i2.AssetVisibility visibility,
i0.Value<String?> stackId = const i0.Value.absent(),
i0.Value<String?> libraryId = const i0.Value.absent(),
i0.Value<bool> isEdited = const i0.Value.absent(),
}) => i1.RemoteAssetEntityCompanion.insert(
name: name,
type: type,
@@ -574,7 +556,6 @@ class $$RemoteAssetEntityTableTableManager
visibility: visibility,
stackId: stackId,
libraryId: libraryId,
isEdited: isEdited,
),
withReferenceMapper: (p0) => p0
.map(
@@ -863,21 +844,6 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
type: i0.DriftSqlType.string,
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
List<i0.GeneratedColumn> get $columns => [
name,
@@ -898,7 +864,6 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
visibility,
stackId,
libraryId,
isEdited,
];
@override
String get aliasedName => _alias ?? actualTableName;
@@ -1022,12 +987,6 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
libraryId.isAcceptableOrUnknown(data['library_id']!, _libraryIdMeta),
);
}
if (data.containsKey('is_edited')) {
context.handle(
_isEditedMeta,
isEdited.isAcceptableOrUnknown(data['is_edited']!, _isEditedMeta),
);
}
return context;
}
@@ -1116,10 +1075,6 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
i0.DriftSqlType.string,
data['${effectivePrefix}library_id'],
),
isEdited: attachedDatabase.typeMapping.read(
i0.DriftSqlType.bool,
data['${effectivePrefix}is_edited'],
)!,
);
}
@@ -1160,7 +1115,6 @@ class RemoteAssetEntityData extends i0.DataClass
final i2.AssetVisibility visibility;
final String? stackId;
final String? libraryId;
final bool isEdited;
const RemoteAssetEntityData({
required this.name,
required this.type,
@@ -1180,7 +1134,6 @@ class RemoteAssetEntityData extends i0.DataClass
required this.visibility,
this.stackId,
this.libraryId,
required this.isEdited,
});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
@@ -1229,7 +1182,6 @@ class RemoteAssetEntityData extends i0.DataClass
if (!nullToAbsent || libraryId != null) {
map['library_id'] = i0.Variable<String>(libraryId);
}
map['is_edited'] = i0.Variable<bool>(isEdited);
return map;
}
@@ -1261,7 +1213,6 @@ class RemoteAssetEntityData extends i0.DataClass
),
stackId: serializer.fromJson<String?>(json['stackId']),
libraryId: serializer.fromJson<String?>(json['libraryId']),
isEdited: serializer.fromJson<bool>(json['isEdited']),
);
}
@override
@@ -1290,7 +1241,6 @@ class RemoteAssetEntityData extends i0.DataClass
),
'stackId': serializer.toJson<String?>(stackId),
'libraryId': serializer.toJson<String?>(libraryId),
'isEdited': serializer.toJson<bool>(isEdited),
};
}
@@ -1313,7 +1263,6 @@ class RemoteAssetEntityData extends i0.DataClass
i2.AssetVisibility? visibility,
i0.Value<String?> stackId = const i0.Value.absent(),
i0.Value<String?> libraryId = const i0.Value.absent(),
bool? isEdited,
}) => i1.RemoteAssetEntityData(
name: name ?? this.name,
type: type ?? this.type,
@@ -1339,7 +1288,6 @@ class RemoteAssetEntityData extends i0.DataClass
visibility: visibility ?? this.visibility,
stackId: stackId.present ? stackId.value : this.stackId,
libraryId: libraryId.present ? libraryId.value : this.libraryId,
isEdited: isEdited ?? this.isEdited,
);
RemoteAssetEntityData copyWithCompanion(i1.RemoteAssetEntityCompanion data) {
return RemoteAssetEntityData(
@@ -1371,7 +1319,6 @@ class RemoteAssetEntityData extends i0.DataClass
: this.visibility,
stackId: data.stackId.present ? data.stackId.value : this.stackId,
libraryId: data.libraryId.present ? data.libraryId.value : this.libraryId,
isEdited: data.isEdited.present ? data.isEdited.value : this.isEdited,
);
}
@@ -1395,8 +1342,7 @@ class RemoteAssetEntityData extends i0.DataClass
..write('livePhotoVideoId: $livePhotoVideoId, ')
..write('visibility: $visibility, ')
..write('stackId: $stackId, ')
..write('libraryId: $libraryId, ')
..write('isEdited: $isEdited')
..write('libraryId: $libraryId')
..write(')'))
.toString();
}
@@ -1421,7 +1367,6 @@ class RemoteAssetEntityData extends i0.DataClass
visibility,
stackId,
libraryId,
isEdited,
);
@override
bool operator ==(Object other) =>
@@ -1444,8 +1389,7 @@ class RemoteAssetEntityData extends i0.DataClass
other.livePhotoVideoId == this.livePhotoVideoId &&
other.visibility == this.visibility &&
other.stackId == this.stackId &&
other.libraryId == this.libraryId &&
other.isEdited == this.isEdited);
other.libraryId == this.libraryId);
}
class RemoteAssetEntityCompanion
@@ -1468,7 +1412,6 @@ class RemoteAssetEntityCompanion
final i0.Value<i2.AssetVisibility> visibility;
final i0.Value<String?> stackId;
final i0.Value<String?> libraryId;
final i0.Value<bool> isEdited;
const RemoteAssetEntityCompanion({
this.name = const i0.Value.absent(),
this.type = const i0.Value.absent(),
@@ -1488,7 +1431,6 @@ class RemoteAssetEntityCompanion
this.visibility = const i0.Value.absent(),
this.stackId = const i0.Value.absent(),
this.libraryId = const i0.Value.absent(),
this.isEdited = const i0.Value.absent(),
});
RemoteAssetEntityCompanion.insert({
required String name,
@@ -1509,7 +1451,6 @@ class RemoteAssetEntityCompanion
required i2.AssetVisibility visibility,
this.stackId = const i0.Value.absent(),
this.libraryId = const i0.Value.absent(),
this.isEdited = const i0.Value.absent(),
}) : name = i0.Value(name),
type = i0.Value(type),
id = i0.Value(id),
@@ -1535,7 +1476,6 @@ class RemoteAssetEntityCompanion
i0.Expression<int>? visibility,
i0.Expression<String>? stackId,
i0.Expression<String>? libraryId,
i0.Expression<bool>? isEdited,
}) {
return i0.RawValuesInsertable({
if (name != null) 'name': name,
@@ -1556,7 +1496,6 @@ class RemoteAssetEntityCompanion
if (visibility != null) 'visibility': visibility,
if (stackId != null) 'stack_id': stackId,
if (libraryId != null) 'library_id': libraryId,
if (isEdited != null) 'is_edited': isEdited,
});
}
@@ -1579,7 +1518,6 @@ class RemoteAssetEntityCompanion
i0.Value<i2.AssetVisibility>? visibility,
i0.Value<String?>? stackId,
i0.Value<String?>? libraryId,
i0.Value<bool>? isEdited,
}) {
return i1.RemoteAssetEntityCompanion(
name: name ?? this.name,
@@ -1600,7 +1538,6 @@ class RemoteAssetEntityCompanion
visibility: visibility ?? this.visibility,
stackId: stackId ?? this.stackId,
libraryId: libraryId ?? this.libraryId,
isEdited: isEdited ?? this.isEdited,
);
}
@@ -1665,9 +1602,6 @@ class RemoteAssetEntityCompanion
if (libraryId.present) {
map['library_id'] = i0.Variable<String>(libraryId.value);
}
if (isEdited.present) {
map['is_edited'] = i0.Variable<bool>(isEdited.value);
}
return map;
}
@@ -1691,8 +1625,7 @@ class RemoteAssetEntityCompanion
..write('livePhotoVideoId: $livePhotoVideoId, ')
..write('visibility: $visibility, ')
..write('stackId: $stackId, ')
..write('libraryId: $libraryId, ')
..write('isEdited: $isEdited')
..write('libraryId: $libraryId')
..write(')'))
.toString();
}

View File

@@ -45,6 +45,5 @@ extension TrashedLocalAssetEntityDataDomainExtension on TrashedLocalAssetEntityD
height: height,
width: width,
orientation: orientation,
isEdited: false,
);
}

View File

@@ -97,7 +97,7 @@ class Drift extends $Drift implements IDatabaseRepository {
}
@override
int get schemaVersion => 17;
int get schemaVersion => 16;
@override
MigrationStrategy get migration => MigrationStrategy(
@@ -201,9 +201,6 @@ class Drift extends $Drift implements IDatabaseRepository {
await m.createIndex(v16.idxLocalAssetCloudId);
await m.createTable(v16.remoteAssetCloudIdEntity);
},
from16To17: (m, v17) async {
await m.addColumn(v17.remoteAssetEntity, v17.remoteAssetEntity.isEdited);
},
),
);

View File

@@ -6911,503 +6911,6 @@ i1.GeneratedColumn<DateTime> _column_100(String aliasedName) =>
true,
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({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@@ -7424,7 +6927,6 @@ i0.MigrationStepWithVersion migrationSteps({
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, Schema16 schema) from15To16,
required Future<void> Function(i1.Migrator m, Schema17 schema) from16To17,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
@@ -7503,11 +7005,6 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema);
await from15To16(migrator, schema);
return 16;
case 16:
final schema = Schema17(database: database);
final migrator = i1.Migrator(database, schema);
await from16To17(migrator, schema);
return 17;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
@@ -7530,7 +7027,6 @@ i1.OnUpgrade stepByStep({
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, Schema16 schema) from15To16,
required Future<void> Function(i1.Migrator m, Schema17 schema) from16To17,
}) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from1To2: from1To2,
@@ -7548,6 +7044,5 @@ i1.OnUpgrade stepByStep({
from13To14: from13To14,
from14To15: from14To15,
from15To16: from15To16,
from16To17: from16To17,
),
);

View File

@@ -200,7 +200,6 @@ class SyncStreamRepository extends DriftDatabaseRepository {
libraryId: Value(asset.libraryId),
width: Value(asset.width),
height: Value(asset.height),
isEdited: Value(asset.isEdited),
);
batch.insert(

View File

@@ -70,7 +70,6 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
durationInSeconds: row.durationInSeconds,
livePhotoVideoId: row.livePhotoVideoId,
stackId: row.stackId,
isEdited: row.isEdited,
)
: LocalAsset(
id: row.localId!,
@@ -89,7 +88,6 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
latitude: row.latitude,
longitude: row.longitude,
adjustmentTime: row.adjustmentTime,
isEdited: row.isEdited,
),
)
.get();

View File

@@ -92,7 +92,7 @@ class _MobileLayout extends StatelessWidget {
],
)
.toList();
return ListView(padding: const EdgeInsets.only(top: 10.0, bottom: 60), children: [...settings]);
return ListView(padding: const EdgeInsets.only(top: 10.0, bottom: 16), children: [...settings]);
}
}
@@ -142,7 +142,7 @@ class SettingsSubPage extends StatelessWidget {
context.locale;
return Scaffold(
appBar: AppBar(centerTitle: false, title: Text(section.title).tr()),
body: Padding(padding: const EdgeInsets.only(bottom: 60.0), child: section.widget),
body: section.widget,
);
}
}

View File

@@ -18,7 +18,6 @@ class SyncStatusPage extends StatelessWidget {
splashRadius: 24,
icon: const Icon(Icons.arrow_back_ios_rounded),
),
centerTitle: false,
),
body: const SyncStatusAndActions(),
);

View File

@@ -118,7 +118,6 @@ class _AssetPropertiesSectionState extends ConsumerState<_AssetPropertiesSection
),
_PropertyItem(label: 'Is Favorite', value: asset.isFavorite.toString()),
_PropertyItem(label: 'Live Photo Video ID', value: asset.livePhotoVideoId),
_PropertyItem(label: 'Is Edited', value: asset.isEdited.toString()),
]);
}

View File

@@ -167,7 +167,7 @@ class _PlaceTile extends StatelessWidget {
child: SizedBox(
width: 80,
height: 80,
child: Thumbnail.remote(remoteId: place.$2, fit: BoxFit.cover, thumbhash: ""),
child: Thumbnail.remote(remoteId: place.$2, fit: BoxFit.cover),
),
),
);

View File

@@ -14,15 +14,14 @@ 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/new_album_name_modal.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/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/user.provider.dart';
import 'package:immich_mobile/routing/router.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/utils/album_filter.utils.dart';
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
@@ -311,17 +310,18 @@ class _SortButtonState extends ConsumerState<_SortButton> {
: const Icon(Icons.abc, color: Colors.transparent),
onPressed: () => onMenuTapped(sortMode),
style: ButtonStyle(
padding: WidgetStateProperty.all(const EdgeInsets.fromLTRB(12, 12, 24, 12)),
padding: WidgetStateProperty.all(const EdgeInsets.fromLTRB(16, 16, 32, 16)),
backgroundColor: WidgetStateProperty.all(
albumSortOption == sortMode ? context.colorScheme.primary : Colors.transparent,
),
shape: WidgetStateProperty.all(
const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(24))),
),
),
child: Text(
sortMode.label.t(context: context),
style: context.textTheme.labelLarge?.copyWith(
style: context.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
color: albumSortOption == sortMode
? context.colorScheme.onPrimary
: context.colorScheme.onSurface.withAlpha(185),
@@ -349,7 +349,10 @@ class _SortButtonState extends ConsumerState<_SortButton> {
),
Text(
albumSortOption.label.t(context: context),
style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurface.withAlpha(225)),
style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
color: context.colorScheme.onSurface.withAlpha(225),
),
),
isSorting
? SizedBox(
@@ -663,8 +666,6 @@ class _GridAlbumCard extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final albumThumbnailAsset = ref.read(assetServiceProvider).getRemoteAsset(album.thumbnailAssetId ?? "");
return GestureDetector(
onTap: () => onAlbumSelected(album),
child: Card(
@@ -683,22 +684,12 @@ class _GridAlbumCard extends ConsumerWidget {
borderRadius: const BorderRadius.vertical(top: Radius.circular(15)),
child: SizedBox(
width: double.infinity,
child: FutureBuilder(
future: albumThumbnailAsset,
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data != null) {
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),
);
},
),
child: album.thumbnailAssetId != null
? Thumbnail.remote(remoteId: album.thumbnailAssetId!)
: Container(
color: context.colorScheme.surfaceContainerHighest,
child: const Icon(Icons.photo_album_rounded, size: 40, color: Colors.grey),
),
),
),
),

View File

@@ -1,14 +1,12 @@
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/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
class AlbumTile extends ConsumerWidget {
class AlbumTile extends StatelessWidget {
const AlbumTile({super.key, required this.album, required this.isOwner, this.onAlbumSelected});
final RemoteAlbum album;
@@ -16,9 +14,7 @@ class AlbumTile extends ConsumerWidget {
final Function(RemoteAlbum)? onAlbumSelected;
@override
Widget build(BuildContext context, WidgetRef ref) {
final albumThumbnailAsset = ref.read(assetServiceProvider).getRemoteAsset(album.thumbnailAssetId ?? "");
Widget build(BuildContext context) {
return LargeLeadingTile(
title: Text(
album.name,
@@ -33,35 +29,23 @@ class AlbumTile extends ConsumerWidget {
),
onTap: () => onAlbumSelected?.call(album),
leadingPadding: const EdgeInsets.only(right: 16),
leading: FutureBuilder(
future: albumThumbnailAsset,
builder: (context, snapshot) {
return snapshot.hasData && snapshot.data != null
? ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(15)),
child: SizedBox(
width: 80,
height: 80,
child: Thumbnail.remote(
remoteId: album.thumbnailAssetId!,
thumbhash: snapshot.data!.thumbHash ?? "",
),
),
)
: 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),
),
);
},
),
leading: album.thumbnailAssetId != null
? ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(15)),
child: SizedBox(width: 80, height: 80, child: Thumbnail.remote(remoteId: album.thumbnailAssetId!)),
)
: 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),
),
),
);
}
}

View File

@@ -10,7 +10,6 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/extensions/build_context_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/presentation/widgets/album/album_tile.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
@@ -165,8 +164,11 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
children: [
if (albums.isNotEmpty)
SheetTile(
title: 'appears_in'.t(context: context),
titleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary),
title: 'appears_in'.t(context: context).toUpperCase(),
titleStyle: context.textTheme.labelMedium?.copyWith(
color: context.textTheme.labelMedium?.color?.withAlpha(200),
fontWeight: FontWeight.w600,
),
),
Padding(
padding: const EdgeInsets.only(left: 24),
@@ -222,7 +224,9 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
color: context.textTheme.labelLarge?.color,
),
subtitle: _getFileInfo(asset, exifInfo),
subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
subtitleStyle: context.textTheme.labelMedium?.copyWith(
color: context.textTheme.labelMedium?.color?.withAlpha(200),
),
);
},
);
@@ -237,7 +241,9 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
color: context.textTheme.labelLarge?.color,
),
subtitle: _getFileInfo(asset, exifInfo),
subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
subtitleStyle: context.textTheme.labelMedium?.copyWith(
color: context.textTheme.labelMedium?.color?.withAlpha(200),
),
);
}
}
@@ -256,8 +262,11 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
const SheetLocationDetails(),
// Details header
SheetTile(
title: 'details'.t(context: context),
titleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary),
title: 'details'.t(context: context).toUpperCase(),
titleStyle: context.textTheme.labelMedium?.copyWith(
color: context.textTheme.labelMedium?.color?.withAlpha(200),
fontWeight: FontWeight.w600,
),
),
// File info
buildFileInfoTile(),
@@ -269,7 +278,9 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
titleStyle: context.textTheme.labelLarge,
leading: Icon(Icons.camera_alt_outlined, size: 24, color: context.textTheme.labelLarge?.color),
subtitle: _getCameraInfoSubtitle(exifInfo),
subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
subtitleStyle: context.textTheme.labelMedium?.copyWith(
color: context.textTheme.labelMedium?.color?.withAlpha(200),
),
),
],
// Lens info
@@ -280,13 +291,15 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
titleStyle: context.textTheme.labelLarge,
leading: Icon(Icons.camera_outlined, size: 24, color: context.textTheme.labelLarge?.color),
subtitle: _getLensInfoSubtitle(exifInfo),
subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
subtitleStyle: context.textTheme.labelMedium?.copyWith(
color: context.textTheme.labelMedium?.color?.withAlpha(200),
),
),
],
// Appears in (Albums)
Padding(padding: const EdgeInsets.only(top: 16.0), child: _buildAppearsInList(ref, context)),
// padding at the bottom to avoid cut-off
const SizedBox(height: 60),
const SizedBox(height: 30),
],
);
}

View File

@@ -4,7 +4,6 @@ import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_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/providers/infrastructure/action.provider.dart';
@@ -78,8 +77,11 @@ class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SheetTile(
title: 'location'.t(context: context),
titleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary),
title: 'location'.t(context: context).toUpperCase(),
titleStyle: context.textTheme.labelMedium?.copyWith(
color: context.textTheme.labelMedium?.color?.withAlpha(200),
fontWeight: FontWeight.w600,
),
trailing: hasCoordinates ? const Icon(Icons.edit_location_alt, size: 20) : null,
onTap: editLocation,
),
@@ -103,7 +105,9 @@ class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
),
Text(
coordinates,
style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceSecondary),
style: context.textTheme.labelMedium?.copyWith(
color: context.textTheme.labelMedium?.color?.withAlpha(200),
),
),
],
),

View File

@@ -4,7 +4,6 @@ import 'package:hooks_riverpod/hooks_riverpod.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/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/presentation/widgets/people/person_edit_name_modal.widget.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
@@ -54,8 +53,11 @@ class _SheetPeopleDetailsState extends ConsumerState<SheetPeopleDetails> {
Padding(
padding: const EdgeInsets.only(left: 16, top: 16, bottom: 16),
child: Text(
"people".t(context: context),
style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary),
"people".t(context: context).toUpperCase(),
style: context.textTheme.labelMedium?.copyWith(
color: context.textTheme.labelMedium?.color?.withAlpha(200),
fontWeight: FontWeight.w600,
),
),
),
SizedBox(

View File

@@ -112,17 +112,14 @@ ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080
provider = LocalFullImageProvider(id: id, size: size, assetType: asset.type);
} else {
final String assetId;
final String thumbhash;
if (asset is LocalAsset && asset.hasRemote) {
assetId = asset.remoteId!;
thumbhash = "";
} else if (asset is RemoteAsset) {
assetId = asset.id;
thumbhash = asset.thumbHash ?? "";
} else {
throw ArgumentError("Unsupported asset type: ${asset.runtimeType}");
}
provider = RemoteFullImageProvider(assetId: assetId, thumbhash: thumbhash);
provider = RemoteFullImageProvider(assetId: assetId);
}
return provider;
@@ -135,9 +132,8 @@ ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnai
}
final assetId = asset is RemoteAsset ? asset.id : (asset as LocalAsset).remoteId;
final thumbhash = asset is RemoteAsset ? asset.thumbHash ?? "" : "";
return assetId != null ? RemoteThumbProvider(assetId: assetId, thumbhash: thumbhash) : null;
return assetId != null ? RemoteThumbProvider(assetId: assetId) : null;
}
bool _shouldUseLocalAsset(BaseAsset asset) =>
asset.hasLocal && (!asset.hasRemote || !AppSetting.get(Setting.preferRemoteImage)) && !asset.isEdited;
asset.hasLocal && (!asset.hasRemote || !AppSetting.get(Setting.preferRemoteImage));

View File

@@ -16,9 +16,8 @@ class RemoteThumbProvider extends CancellableImageProvider<RemoteThumbProvider>
with CancellableImageProviderMixin<RemoteThumbProvider> {
static final cacheManager = RemoteThumbnailCacheManager();
final String assetId;
final String thumbhash;
RemoteThumbProvider({required this.assetId, required this.thumbhash});
RemoteThumbProvider({required this.assetId});
@override
Future<RemoteThumbProvider> obtainKey(ImageConfiguration configuration) {
@@ -39,7 +38,7 @@ class RemoteThumbProvider extends CancellableImageProvider<RemoteThumbProvider>
Stream<ImageInfo> _codec(RemoteThumbProvider key, ImageDecoderCallback decode) {
final request = this.request = RemoteImageRequest(
uri: getThumbnailUrlForRemoteId(key.assetId, thumbhash: key.thumbhash),
uri: getThumbnailUrlForRemoteId(key.assetId),
headers: ApiService.getRequestHeaders(),
cacheManager: cacheManager,
);
@@ -50,23 +49,22 @@ class RemoteThumbProvider extends CancellableImageProvider<RemoteThumbProvider>
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is RemoteThumbProvider) {
return assetId == other.assetId && thumbhash == other.thumbhash;
return assetId == other.assetId;
}
return false;
}
@override
int get hashCode => assetId.hashCode ^ thumbhash.hashCode;
int get hashCode => assetId.hashCode;
}
class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImageProvider>
with CancellableImageProviderMixin<RemoteFullImageProvider> {
static final cacheManager = RemoteThumbnailCacheManager();
final String assetId;
final String thumbhash;
RemoteFullImageProvider({required this.assetId, required this.thumbhash});
RemoteFullImageProvider({required this.assetId});
@override
Future<RemoteFullImageProvider> obtainKey(ImageConfiguration configuration) {
@@ -77,7 +75,7 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
ImageStreamCompleter loadImage(RemoteFullImageProvider key, ImageDecoderCallback decode) {
return OneFramePlaceholderImageStreamCompleter(
_codec(key, decode),
initialImage: getInitialImage(RemoteThumbProvider(assetId: key.assetId, thumbhash: key.thumbhash)),
initialImage: getInitialImage(RemoteThumbProvider(assetId: key.assetId)),
informationCollector: () => <DiagnosticsNode>[
DiagnosticsProperty<ImageProvider>('Image provider', this),
DiagnosticsProperty<String>('Asset Id', key.assetId),
@@ -96,7 +94,7 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
final headers = ApiService.getRequestHeaders();
final request = this.request = RemoteImageRequest(
uri: getThumbnailUrlForRemoteId(key.assetId, type: AssetMediaSize.preview, thumbhash: key.thumbhash),
uri: getThumbnailUrlForRemoteId(key.assetId, type: AssetMediaSize.preview),
headers: headers,
cacheManager: cacheManager,
);
@@ -117,12 +115,12 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is RemoteFullImageProvider) {
return assetId == other.assetId && thumbhash == other.thumbhash;
return assetId == other.assetId;
}
return false;
}
@override
int get hashCode => assetId.hashCode ^ thumbhash.hashCode;
int get hashCode => assetId.hashCode;
}

View File

@@ -21,14 +21,9 @@ class Thumbnail extends StatefulWidget {
const Thumbnail({this.imageProvider, this.fit = BoxFit.cover, this.thumbhashProvider, super.key});
Thumbnail.remote({
required String remoteId,
required String thumbhash,
this.fit = BoxFit.cover,
Size size = kThumbnailResolution,
super.key,
}) : imageProvider = RemoteThumbProvider(assetId: remoteId, thumbhash: thumbhash),
thumbhashProvider = null;
Thumbnail.remote({required String remoteId, this.fit = BoxFit.cover, Size size = kThumbnailResolution, super.key})
: imageProvider = RemoteThumbProvider(assetId: remoteId),
thumbhashProvider = null;
Thumbnail.fromAsset({
required BaseAsset? asset,

View File

@@ -6,7 +6,6 @@ import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/duration_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/timeline/constants.dart';
import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart';
@@ -48,7 +47,6 @@ class _ThumbnailTileState extends ConsumerState<ThumbnailTile> {
Widget build(BuildContext context) {
final asset = widget.asset;
final heroIndex = widget.heroOffset ?? TabsRouterScope.of(context)?.controller.activeIndex ?? 0;
final isCurrentAsset = ref.watch(assetViewerProvider.select((current) => current.currentAsset == asset));
final assetContainerColor = context.isDarkTheme
? context.primaryColor.darken(amount: 0.4)
@@ -61,10 +59,6 @@ class _ThumbnailTileState extends ConsumerState<ThumbnailTile> {
final bool storageIndicator =
ref.watch(settingsProvider.select((s) => s.get(Setting.showStorageIndicator))) && widget.showStorageIndicator;
if (!isCurrentAsset) {
_hideIndicators = false;
}
if (isSelected) {
_showSelectionContainer = true;
}
@@ -102,11 +96,7 @@ class _ThumbnailTileState extends ConsumerState<ThumbnailTile> {
children: [
Positioned.fill(
child: Hero(
// 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',
tag: '${asset?.heroTag ?? ''}_$heroIndex',
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: (context, heroSize, child) {

View File

@@ -60,11 +60,7 @@ class DriftMemoryCard extends ConsumerWidget {
child: SizedBox(
width: 205,
height: 200,
child: Thumbnail.remote(
remoteId: memory.assets[0].id,
thumbhash: memory.assets[0].thumbHash ?? "",
fit: BoxFit.cover,
),
child: Thumbnail.remote(remoteId: memory.assets[0].id, fit: BoxFit.cover),
),
),
Positioned(

View File

@@ -69,7 +69,6 @@ class CastNotifier extends StateNotifier<CastManagerState> {
: AssetType.other,
createdAt: asset.fileCreatedAt,
updatedAt: asset.updatedAt,
isEdited: false,
);
_gCastService.loadMedia(remoteAsset, reload);

View File

@@ -144,7 +144,6 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
socket.on('on_asset_hidden', _handleOnAssetHidden);
} else {
socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReady);
socket.on('AssetEditReadyV1', _handleSyncAssetEditReady);
}
socket.on('on_config_update', _handleOnConfigUpdate);
@@ -193,12 +192,10 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
void stopListeningToBetaEvents() {
state.socket?.off('AssetUploadReadyV1');
state.socket?.off('AssetEditReadyV1');
}
void startListeningToBetaEvents() {
state.socket?.on('AssetUploadReadyV1', _handleSyncAssetUploadReady);
state.socket?.on('AssetEditReadyV1', _handleSyncAssetEditReady);
}
void listenUploadEvent() {
@@ -318,10 +315,6 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
_batchDebouncer.run(_processBatchedAssetUploadReady);
}
void _handleSyncAssetEditReady(dynamic data) {
unawaited(_ref.read(backgroundSyncProvider).syncWebsocketEditBatch([data]));
}
void _processBatchedAssetUploadReady() {
if (_batchedAssetUploadReady.isEmpty) {
return;

View File

@@ -25,7 +25,6 @@ class FileMediaRepository {
type: AssetType.image,
createdAt: entity.createDateTime,
updatedAt: entity.modifiedDateTime,
isEdited: false,
);
}

View File

@@ -61,12 +61,7 @@ ThemeData getThemeData({required ColorScheme colorScheme, required Locale locale
),
),
chipTheme: const ChipThemeData(side: BorderSide.none),
sliderTheme: const SliderThemeData(
thumbShape: RoundSliderThumbShape(enabledThumbRadius: 7),
trackHeight: 2.0,
// ignore: deprecated_member_use
year2023: false,
),
sliderTheme: const SliderThemeData(thumbShape: RoundSliderThumbShape(enabledThumbRadius: 7), trackHeight: 2.0),
bottomNavigationBarTheme: const BottomNavigationBarThemeData(type: BottomNavigationBarType.fixed),
popupMenuTheme: const PopupMenuThemeData(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),

View File

@@ -50,10 +50,8 @@ String getThumbnailUrlForRemoteId(
final String id, {
AssetMediaSize type = AssetMediaSize.thumbnail,
bool edited = true,
String? thumbhash,
}) {
final url = '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=${type.value}&edited=$edited';
return thumbhash != null ? '$url&c=${Uri.encodeComponent(thumbhash)}' : url;
return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=${type.value}&edited=$edited';
}
String getPlaybackUrlForRemoteId(final String id) {

View File

@@ -49,7 +49,7 @@ dynamic upgradeDto(dynamic value, String targetType) {
}
case 'SyncAssetV1':
if (value is Map) {
addDefault(value, 'isEdited', false);
addDefault(value, 'editCount', 0);
}
case 'ServerFeaturesDto':
if (value is Map) {

View File

@@ -1,14 +1,14 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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/services/app_settings.service.dart';
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/widgets/settings/setting_group_title.dart';
import 'package:immich_mobile/widgets/settings/settings_radio_list_tile.dart';
import 'package:immich_mobile/widgets/settings/settings_sub_title.dart';
class GroupSettings extends HookConsumerWidget {
const GroupSettings({super.key});
@@ -33,24 +33,12 @@ class GroupSettings extends HookConsumerWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SettingGroupTitle(
title: "asset_list_group_by_sub_title".t(context: context),
icon: Icons.group_work_outlined,
),
SettingsSubTitle(title: "asset_list_group_by_sub_title".tr()),
SettingsRadioListTile(
groups: [
SettingsRadioGroup(
title: 'asset_list_layout_settings_group_by_month_day'.t(context: context),
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,
),
SettingsRadioGroup(title: 'asset_list_layout_settings_group_by_month_day'.tr(), value: GroupAssetsBy.day),
SettingsRadioGroup(title: 'month'.tr(), value: GroupAssetsBy.month),
SettingsRadioGroup(title: 'asset_list_layout_settings_group_automatically'.tr(), value: GroupAssetsBy.auto),
],
groupBy: groupBy,
onRadioChanged: changeGroupValue,

View File

@@ -1,12 +1,11 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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/services/app_settings.service.dart';
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
import 'package:immich_mobile/widgets/settings/setting_group_title.dart';
import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart';
import 'package:immich_mobile/widgets/settings/settings_sub_title.dart';
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
class LayoutSettings extends HookConsumerWidget {
@@ -20,13 +19,10 @@ class LayoutSettings extends HookConsumerWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SettingGroupTitle(
title: "asset_list_layout_sub_title".t(context: context),
icon: Icons.view_module_outlined,
),
SettingsSubTitle(title: "asset_list_layout_sub_title".tr()),
SettingsSwitchListTile(
valueNotifier: useDynamicLayout,
title: "asset_list_layout_settings_dynamic_layout_title".t(context: context),
title: "asset_list_layout_settings_dynamic_layout_title".tr(),
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
),
SettingsSliderListTile(

View File

@@ -1,9 +1,10 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/app_settings.provider.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/settings_sub_title.dart';
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
@@ -18,21 +19,21 @@ class ImageViewerQualitySetting extends HookConsumerWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SettingGroupTitle(
title: "photos".t(context: context),
icon: Icons.image_outlined,
subtitle: "setting_image_viewer_help".t(context: context),
SettingsSubTitle(title: "setting_image_viewer_title".tr()),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
title: Text('setting_image_viewer_help', style: context.textTheme.bodyMedium).tr(),
),
SettingsSwitchListTile(
valueNotifier: isPreview,
title: "setting_image_viewer_preview_title".t(context: context),
subtitle: "setting_image_viewer_preview_subtitle".t(context: context),
title: "setting_image_viewer_preview_title".tr(),
subtitle: "setting_image_viewer_preview_subtitle".tr(),
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
),
SettingsSwitchListTile(
valueNotifier: isOriginal,
title: "setting_image_viewer_original_title".t(context: context),
subtitle: "setting_image_viewer_original_subtitle".t(context: context),
title: "setting_image_viewer_original_title".tr(),
subtitle: "setting_image_viewer_original_subtitle".tr(),
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
),
],

View File

@@ -1,9 +1,9 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/settings/setting_group_title.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/utils/hooks/app_settings_update_hook.dart';
@@ -19,26 +19,23 @@ class VideoViewerSettings extends HookConsumerWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SettingGroupTitle(
title: "videos".t(context: context),
icon: Icons.video_camera_back_outlined,
),
SettingsSubTitle(title: "videos".tr()),
SettingsSwitchListTile(
valueNotifier: useAutoPlayVideo,
title: "setting_video_viewer_auto_play_title".t(context: context),
subtitle: "setting_video_viewer_auto_play_subtitle".t(context: context),
title: "setting_video_viewer_auto_play_title".tr(),
subtitle: "setting_video_viewer_auto_play_subtitle".tr(),
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
),
SettingsSwitchListTile(
valueNotifier: useLoopVideo,
title: "setting_video_viewer_looping_title".t(context: context),
subtitle: "loop_videos_description".t(context: context),
title: "setting_video_viewer_looping_title".tr(),
subtitle: "loop_videos_description".tr(),
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
),
SettingsSwitchListTile(
valueNotifier: useOriginalVideo,
title: "setting_video_viewer_original_video_title".t(context: context),
subtitle: "setting_video_viewer_original_video_subtitle".t(context: context),
title: "setting_video_viewer_original_video_title".tr(),
subtitle: "setting_video_viewer_original_video_subtitle".tr(),
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
),
],

View File

@@ -16,8 +16,6 @@ import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/user.provider.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';
class DriftBackupSettings extends ConsumerWidget {
@@ -27,25 +25,36 @@ class DriftBackupSettings extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
return SettingsSubPageScaffold(
settings: [
SettingGroupTitle(
title: "network_requirements".t(context: context),
icon: Icons.cell_tower,
Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Text(
"network_requirements".t(context: context).toUpperCase(),
style: context.textTheme.labelSmall?.copyWith(color: context.colorScheme.onSurface.withValues(alpha: 0.7)),
),
),
const _UseWifiForUploadVideosButton(),
const _UseWifiForUploadPhotosButton(),
if (CurrentPlatform.isAndroid) ...[
const Divider(),
SettingGroupTitle(
title: "background_options".t(context: context),
icon: Icons.charging_station_rounded,
Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Text(
"background_options".t(context: context).toUpperCase(),
style: context.textTheme.labelSmall?.copyWith(
color: context.colorScheme.onSurface.withValues(alpha: 0.7),
),
),
),
const _BackupOnlyWhenChargingButton(),
const _BackupDelaySlider(),
],
const Divider(),
SettingGroupTitle(
title: "backup_albums_sync".t(context: context),
icon: Icons.sync,
Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Text(
"backup_albums_sync".t(context: context).toUpperCase(),
style: context.textTheme.labelSmall?.copyWith(color: context.colorScheme.onSurface.withValues(alpha: 0.7)),
),
),
const _AlbumSyncActionButton(),
],
@@ -96,67 +105,81 @@ class _AlbumSyncActionButtonState extends ConsumerState<_AlbumSyncActionButton>
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 8.0),
child: ListView(
shrinkWrap: true,
children: [
StreamBuilder(
stream: Store.watch(StoreKey.syncAlbums),
initialData: Store.tryGet(StoreKey.syncAlbums) ?? false,
builder: (context, snapshot) {
final albumSyncEnable = snapshot.data ?? false;
return Column(
children: [
SettingListTile(
title: "sync_albums".t(context: context),
subtitle: "sync_upload_album_setting_subtitle".t(context: context),
trailing: Switch(
value: albumSyncEnable,
onChanged: (bool newValue) async {
await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.syncAlbums, newValue);
return ListView(
shrinkWrap: true,
children: [
StreamBuilder(
stream: Store.watch(StoreKey.syncAlbums),
initialData: Store.tryGet(StoreKey.syncAlbums) ?? false,
builder: (context, snapshot) {
final albumSyncEnable = snapshot.data ?? false;
return Column(
children: [
ListTile(
title: Text(
"sync_albums".t(context: context),
style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor),
),
subtitle: Text(
"sync_upload_album_setting_subtitle".t(context: context),
style: context.textTheme.labelLarge,
),
trailing: Switch(
value: albumSyncEnable,
onChanged: (bool newValue) async {
await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.syncAlbums, newValue);
if (newValue == true) {
await _manageLinkedAlbums();
}
},
),
if (newValue == true) {
await _manageLinkedAlbums();
}
},
),
AnimatedSize(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: albumSyncEnable ? 1.0 : 0.0,
child: albumSyncEnable
? SettingListTile(
onTap: _manualSyncAlbums,
contentPadding: const EdgeInsets.only(left: 32, right: 16),
title: "organize_into_albums".t(context: context),
subtitle: "organize_into_albums_description".t(context: context),
trailing: isAlbumSyncInProgress
? const SizedBox(
width: 32,
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(),
),
),
AnimatedSize(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: albumSyncEnable ? 1.0 : 0.0,
child: albumSyncEnable
? ListTile(
onTap: _manualSyncAlbums,
contentPadding: const EdgeInsets.only(left: 32, right: 16),
title: Text(
"organize_into_albums".t(context: context),
style: context.textTheme.titleSmall?.copyWith(
color: context.colorScheme.onSurface,
fontWeight: FontWeight.normal,
),
),
subtitle: Text(
"organize_into_albums_description".t(context: context),
style: context.textTheme.bodyMedium?.copyWith(
color: context.colorScheme.onSurface.withValues(alpha: 0.7),
),
),
trailing: isAlbumSyncInProgress
? const SizedBox(
width: 32,
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(),
),
],
);
},
),
],
),
),
],
);
},
),
],
);
}
}
@@ -199,24 +222,24 @@ class _SettingsSwitchTileState extends ConsumerState<_SettingsSwitchTile> {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 8.0),
child: SettingListTile(
title: widget.titleKey.t(context: context),
subtitle: widget.subtitleKey.t(context: context),
trailing: StreamBuilder(
stream: valueStream,
initialData: Store.tryGet(widget.appSettingsEnum.storeKey) ?? widget.appSettingsEnum.defaultValue,
builder: (context, snapshot) {
final value = snapshot.data ?? false;
return Switch(
value: value,
onChanged: (bool newValue) async {
await ref.read(appSettingsServiceProvider).setSetting(widget.appSettingsEnum, newValue);
},
);
},
),
return ListTile(
title: Text(
widget.titleKey.t(context: context),
style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor),
),
subtitle: Text(widget.subtitleKey.t(context: context), style: context.textTheme.labelLarge),
trailing: StreamBuilder(
stream: valueStream,
initialData: Store.tryGet(widget.appSettingsEnum.storeKey) ?? widget.appSettingsEnum.defaultValue,
builder: (context, snapshot) {
final value = snapshot.data ?? false;
return Switch(
value: value,
onChanged: (bool newValue) async {
await ref.read(appSettingsServiceProvider).setSetting(widget.appSettingsEnum, newValue);
},
);
},
),
);
}
@@ -331,7 +354,7 @@ class _BackupDelaySliderState extends ConsumerState<_BackupDelaySlider> {
'backup_controller_page_background_delay'.tr(
namedArgs: {'duration': formatBackupDelaySliderValue(currentValue)},
),
style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500),
style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor),
),
),
Slider(

View File

@@ -34,36 +34,33 @@ class EntityCountTile extends StatelessWidget {
children: [
// Icon and Label
Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Icon(icon, color: context.primaryColor, size: 14),
const SizedBox(width: 4),
Icon(icon, color: context.primaryColor),
const SizedBox(width: 8),
Flexible(
child: Text(
label,
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.w500),
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 16),
),
),
],
),
// Number
const Spacer(),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: RichText(
text: TextSpan(
style: const TextStyle(fontSize: 18, fontFamily: 'GoogleSansCode'),
children: [
TextSpan(
text: zeroPadding(count, maxDigits),
style: TextStyle(color: context.colorScheme.onSurfaceSecondary.withAlpha(75)),
),
TextSpan(
text: count.toString(),
style: TextStyle(color: context.colorScheme.onSurface),
),
],
),
RichText(
text: TextSpan(
style: const TextStyle(fontSize: 18, fontFamily: 'GoogleSansCode', fontWeight: FontWeight.w600),
children: [
TextSpan(
text: zeroPadding(count, maxDigits),
style: TextStyle(color: context.colorScheme.onSurfaceSecondary.withAlpha(75)),
),
TextSpan(
text: count.toString(),
style: TextStyle(color: context.primaryColor),
),
],
),
),
],

View File

@@ -16,8 +16,6 @@ import 'package:immich_mobile/providers/infrastructure/trash_sync.provider.dart'
import 'package:immich_mobile/providers/sync_status.provider.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/setting_group_title.dart';
import 'package:immich_mobile/widgets/settings/setting_list_tile.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
@@ -114,39 +112,48 @@ class SyncStatusAndActions extends HookConsumerWidget {
padding: const EdgeInsets.only(top: 16, bottom: 96),
children: [
const _SyncStatsCounts(),
const Divider(height: 10),
const SizedBox(height: 16),
SettingGroupTitle(title: "jobs".t(context: context)),
SettingListTile(
title: "sync_local".t(context: context),
subtitle: "tap_to_run_job".t(context: context),
const Divider(height: 1, indent: 16, endIndent: 16),
const SizedBox(height: 24),
_SectionHeaderText(text: "jobs".t(context: context)),
ListTile(
title: Text(
"sync_local".t(context: context),
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text("tap_to_run_job".t(context: context)),
leading: const Icon(Icons.sync),
trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).localSyncStatus),
onTap: () {
ref.read(backgroundSyncProvider).syncLocal(full: true);
},
),
SettingListTile(
title: "sync_remote".t(context: context),
subtitle: "tap_to_run_job".t(context: context),
ListTile(
title: Text(
"sync_remote".t(context: context),
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text("tap_to_run_job".t(context: context)),
leading: const Icon(Icons.cloud_sync),
trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).remoteSyncStatus),
onTap: () {
ref.read(backgroundSyncProvider).syncRemote();
},
),
SettingListTile(
title: "hash_asset".t(context: context),
ListTile(
title: Text(
"hash_asset".t(context: context),
style: const TextStyle(fontWeight: FontWeight.w500),
),
leading: const Icon(Icons.tag),
subtitle: "tap_to_run_job".t(context: context),
subtitle: Text("tap_to_run_job".t(context: context)),
trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).hashJobStatus),
onTap: () {
ref.read(backgroundSyncProvider).hashAssets();
},
),
const Divider(height: 1),
const SizedBox(height: 16),
SettingGroupTitle(title: "actions".t(context: context)),
const Divider(height: 1, indent: 16, endIndent: 16),
const SizedBox(height: 24),
_SectionHeaderText(text: "actions".t(context: context)),
ListTile(
title: Text(
"clear_file_cache".t(context: context),
@@ -195,6 +202,26 @@ 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 {
const _SyncStatsCounts();
@@ -252,9 +279,9 @@ class _SyncStatsCounts extends ConsumerWidget {
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SettingGroupTitle(title: "assets".t(context: context)),
_SectionHeaderText(text: "assets".t(context: context)),
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
// 1. Wrap in IntrinsicHeight
child: IntrinsicHeight(
child: Flex(
@@ -282,9 +309,9 @@ class _SyncStatsCounts extends ConsumerWidget {
),
),
),
SettingGroupTitle(title: "albums".t(context: context)),
_SectionHeaderText(text: "albums".t(context: context)),
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: IntrinsicHeight(
child: Flex(
direction: Axis.horizontal,
@@ -310,9 +337,9 @@ class _SyncStatsCounts extends ConsumerWidget {
),
),
),
SettingGroupTitle(title: "other".t(context: context)),
_SectionHeaderText(text: "other".t(context: context)),
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: IntrinsicHeight(
child: Flex(
direction: Axis.horizontal,
@@ -341,7 +368,7 @@ class _SyncStatsCounts extends ConsumerWidget {
// To be removed once the experimental feature is stable
if (CurrentPlatform.isAndroid &&
appSettingsService.getSetting<bool>(AppSettingsEnum.manageLocalMediaAndroid)) ...[
SettingGroupTitle(title: "trash".t(context: context)),
_SectionHeaderText(text: "trash".t(context: context)),
Consumer(
builder: (context, ref, _) {
final counts = ref.watch(trashedAssetsCountProvider);

View File

@@ -9,7 +9,6 @@ import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/settings/setting_list_tile.dart';
class BetaTimelineListTile extends ConsumerWidget {
const BetaTimelineListTile({super.key});
@@ -57,8 +56,8 @@ class BetaTimelineListTile extends ConsumerWidget {
return Padding(
padding: const EdgeInsets.only(left: 4.0),
child: SettingListTile(
title: "new_timeline".t(context: context),
child: ListTile(
title: Text("new_timeline".t(context: context)),
trailing: Switch.adaptive(
value: betaTimelineValue,
onChanged: onSwitchChanged,

View File

@@ -142,9 +142,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState<FreeUpSpaceSettings> {
final state = ref.watch(cleanupProvider);
final hasDate = state.selectedDate != null;
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}) {
switch (stepState) {
case StepState.complete:
@@ -216,7 +214,10 @@ class _FreeUpSpaceSettingsState extends ConsumerState<FreeUpSpaceSettings> {
borderRadius: const BorderRadius.all(Radius.circular(12)),
border: Border.all(color: context.primaryColor.withValues(alpha: 0.25)),
),
child: Text('free_up_space_description'.t(context: context), style: context.textTheme.bodyMedium),
child: Text(
'free_up_space_description'.t(context: context),
style: context.textTheme.labelLarge?.copyWith(fontSize: 15),
),
),
),
@@ -255,7 +256,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState<FreeUpSpaceSettings> {
content: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('cutoff_date_description'.t(context: context), style: subtitleStyle),
Text('cutoff_date_description'.t(context: context), style: context.textTheme.labelLarge),
const SizedBox(height: 16),
GridView.count(
shrinkWrap: true,
@@ -351,7 +352,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState<FreeUpSpaceSettings> {
content: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('cleanup_filter_description'.t(context: context), style: subtitleStyle),
Text('cleanup_filter_description'.t(context: context), style: context.textTheme.labelLarge),
const SizedBox(height: 16),
SegmentedButton<AssetFilterType>(
segments: [
@@ -380,15 +381,10 @@ class _FreeUpSpaceSettingsState extends ConsumerState<FreeUpSpaceSettings> {
const SizedBox(height: 16),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(
'keep_favorites'.t(context: context),
style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5),
),
title: Text('keep_favorites'.t(context: context), style: context.textTheme.titleSmall),
subtitle: Text(
'keep_favorites_description'.t(context: context),
style: context.textTheme.bodyMedium!.copyWith(
color: context.textTheme.bodyMedium!.color!.withAlpha(215),
),
style: context.textTheme.labelLarge,
),
value: state.keepFavorites,
onChanged: (value) {
@@ -439,7 +435,10 @@ class _FreeUpSpaceSettingsState extends ConsumerState<FreeUpSpaceSettings> {
: null,
content: Column(
children: [
Text('cleanup_step3_description'.t(context: context), style: subtitleStyle),
Text(
'cleanup_step3_description'.t(context: context),
style: context.textTheme.labelLarge?.copyWith(fontSize: 15),
),
if (CurrentPlatform.isIOS) ...[
const SizedBox(height: 12),
Container(

View File

@@ -117,7 +117,7 @@ class EndpointInputState extends ConsumerState<EndpointInput> {
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: validateUrl,
keyboardType: TextInputType.url,
style: const TextStyle(fontFamily: 'GoogleSansCode', fontSize: 14),
style: const TextStyle(fontFamily: 'GoogleSansCode', fontWeight: FontWeight.w600, fontSize: 14),
decoration: InputDecoration(
hintText: 'http(s)://immich.domain.com',
contentPadding: const EdgeInsets.all(16),

View File

@@ -1,12 +1,12 @@
import 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.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/widgets/settings/networking_settings/endpoint_input.dart';
@@ -103,7 +103,7 @@ class ExternalNetworkPreference extends HookConsumerWidget {
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 24),
child: Text("external_network_sheet_info".t(context: context), style: context.textTheme.bodyMedium),
child: Text("external_network_sheet_info".tr(), style: context.textTheme.bodyMedium),
),
const SizedBox(height: 4),
Divider(color: context.colorScheme.surfaceContainerHighest),
@@ -135,7 +135,7 @@ class ExternalNetworkPreference extends HookConsumerWidget {
height: 48,
child: OutlinedButton.icon(
icon: const Icon(Icons.add),
label: Text('add_endpoint'.t(context: context)),
label: Text('add_endpoint'.tr().toUpperCase()),
onPressed: enabled
? () {
entries.value = [

View File

@@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/network.provider.dart';
@@ -168,12 +167,13 @@ class LocalNetworkPreference extends HookConsumerWidget {
enabled: enabled,
contentPadding: const EdgeInsets.only(left: 24, right: 8),
leading: const Icon(Icons.lan_rounded),
title: Text("server_endpoint".t(context: context)),
title: Text("server_endpoint".tr()),
subtitle: localEndpointText.value.isEmpty
? const Text("http://local-ip:2283")
: Text(
localEndpointText.value,
style: context.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.bold,
color: enabled ? context.primaryColor : context.colorScheme.onSurface.withAlpha(100),
fontFamily: 'GoogleSansCode',
),
@@ -190,7 +190,7 @@ class LocalNetworkPreference extends HookConsumerWidget {
height: 48,
child: OutlinedButton.icon(
icon: const Icon(Icons.wifi_find_rounded),
label: Text('use_current_connection'.t(context: context)),
label: Text('use_current_connection'.tr().toUpperCase()),
onPressed: enabled ? autofillCurrentNetwork : null,
),
),

View File

@@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/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/providers/network.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
@@ -11,7 +10,6 @@ import 'package:immich_mobile/utils/hooks/app_settings_update_hook.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/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';
class NetworkingSettings extends HookConsumerWidget {
@@ -89,10 +87,12 @@ class NetworkingSettings extends HookConsumerWidget {
return ListView(
padding: const EdgeInsets.only(bottom: 96),
children: <Widget>[
const SizedBox(height: 8),
SettingGroupTitle(
title: "current_server_address".t(context: context),
icon: (currentEndpoint?.startsWith('https') ?? false) ? Icons.https_outlined : Icons.http_outlined,
Padding(
padding: const EdgeInsets.only(top: 8, left: 16, bottom: 8),
child: NetworkPreferenceTitle(
title: "current_server_address".tr().toUpperCase(),
icon: (currentEndpoint?.startsWith('https') ?? false) ? Icons.https_outlined : Icons.http_outlined,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
@@ -108,7 +108,12 @@ class NetworkingSettings extends HookConsumerWidget {
: const Icon(Icons.circle_outlined),
title: Text(
currentEndpoint ?? "--",
style: TextStyle(fontSize: 14, fontFamily: 'GoogleSansCode', color: context.primaryColor),
style: TextStyle(
fontSize: 16,
fontFamily: 'GoogleSansCode',
fontWeight: FontWeight.bold,
color: context.primaryColor,
),
),
),
),
@@ -123,16 +128,14 @@ class NetworkingSettings extends HookConsumerWidget {
title: "automatic_endpoint_switching_title".tr(),
subtitle: "automatic_endpoint_switching_subtitle".tr(),
),
const SizedBox(height: 8),
SettingGroupTitle(
title: "local_network".t(context: context),
icon: Icons.home_outlined,
Padding(
padding: const EdgeInsets.only(top: 8, left: 16, bottom: 16),
child: NetworkPreferenceTitle(title: "local_network".tr().toUpperCase(), icon: Icons.home_outlined),
),
LocalNetworkPreference(enabled: featureEnabled.value),
const SizedBox(height: 16),
SettingGroupTitle(
title: "external_network".t(context: context),
icon: Icons.dns_outlined,
Padding(
padding: const EdgeInsets.only(top: 32, left: 16, bottom: 16),
child: NetworkPreferenceTitle(title: "external_network".tr().toUpperCase(), icon: Icons.dns_outlined),
),
ExternalNetworkPreference(enabled: featureEnabled.value),
],
@@ -140,6 +143,30 @@ 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 {
const NetworkStatusIcon({super.key, required this.status, this.enabled = true}) : super();
@@ -148,10 +175,10 @@ class NetworkStatusIcon extends StatelessWidget {
@override
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(
padding: const EdgeInsets.only(left: 4.0),
child: SizedBox(

View File

@@ -1,9 +1,9 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/translate_extensions.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/settings_sub_title.dart';
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
@@ -22,13 +22,10 @@ class HapticSetting extends HookConsumerWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SettingGroupTitle(
title: "haptic_feedback_title".t(context: context),
icon: Icons.vibration_outlined,
),
SettingsSubTitle(title: "haptic_feedback_title".tr()),
SettingsSwitchListTile(
valueNotifier: isHapticFeedbackEnabled,
title: 'enabled'.t(context: context),
title: 'haptic_feedback_switch'.tr(),
onChanged: onHapticFeedbackChange,
),
],

View File

@@ -1,12 +1,12 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/theme.provider.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/setting_group_title.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/utils/hooks/app_settings_update_hook.dart';
@@ -74,26 +74,23 @@ class ThemeSetting extends HookConsumerWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SettingGroupTitle(
title: "theme".t(context: context),
icon: Icons.color_lens_outlined,
),
SettingsSubTitle(title: "theme".tr()),
SettingsSwitchListTile(
valueNotifier: isSystemTheme,
title: 'theme_setting_system_theme_switch'.t(context: context),
title: 'theme_setting_system_theme_switch'.tr(),
onChanged: onSystemThemeChange,
),
if (currentTheme.value != ThemeMode.system)
SettingsSwitchListTile(
valueNotifier: isDarkTheme,
title: 'map_settings_dark_mode'.t(context: context),
title: 'map_settings_dark_mode'.tr(),
onChanged: onThemeChange,
),
const PrimaryColorSetting(),
SettingsSwitchListTile(
valueNotifier: applyThemeToBackgroundProvider,
title: "theme_setting_colorful_interface_title".t(context: context),
subtitle: 'theme_setting_colorful_interface_subtitle'.t(context: context),
title: "theme_setting_colorful_interface_title".tr(),
subtitle: 'theme_setting_colorful_interface_subtitle'.tr(),
onChanged: onSurfaceColorSettingChange,
),
],

View File

@@ -1,39 +0,0 @@
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)),
),
],
],
),
);
}
}

View File

@@ -1,38 +0,0 @@
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,
);
}
}

View File

@@ -36,8 +36,11 @@ class SettingsCard extends StatelessWidget {
padding: const EdgeInsets.all(16.0),
child: Icon(icon, color: context.primaryColor),
),
title: Text(title, style: context.textTheme.titleMedium!.copyWith(color: context.primaryColor)),
subtitle: Text(subtitle, style: context.textTheme.bodyMedium),
title: Text(
title,
style: context.textTheme.titleMedium!.copyWith(fontWeight: FontWeight.w600, color: context.primaryColor),
),
subtitle: Text(subtitle, style: context.textTheme.labelLarge),
onTap: () => context.pushRoute(settingRoute),
),
),

View File

@@ -9,11 +9,13 @@ class SettingsSubPageScaffold extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 16),
padding: const EdgeInsets.symmetric(vertical: 20),
itemCount: settings.length,
itemBuilder: (ctx, index) => settings[index],
separatorBuilder: (context, index) => showDivider
? const Column(children: [SizedBox(height: 5), Divider(height: 10), SizedBox(height: 15)])
? const Column(
children: [SizedBox(height: 5), Divider(height: 10, indent: 15, endIndent: 15), SizedBox(height: 15)],
)
: const SizedBox(height: 10),
);
}

View File

@@ -1,5 +1,5 @@
[tools]
flutter = "3.35.7"
flutter = "3.38.7"
[tools."github:CQLabs/homebrew-dcm"]
version = "1.30.0"

View File

@@ -138,11 +138,6 @@ Class | Method | HTTP request | Description
*AuthenticationApi* | [**unlockAuthSession**](doc//AuthenticationApi.md#unlockauthsession) | **POST** /auth/session/unlock | Unlock auth session
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken | Validate access token
*AuthenticationAdminApi* | [**unlinkAllOAuthAccountsAdmin**](doc//AuthenticationAdminApi.md#unlinkalloauthaccountsadmin) | **POST** /admin/auth/unlink-all | Unlink all OAuth accounts
*DatabaseBackupsAdminApi* | [**deleteDatabaseBackup**](doc//DatabaseBackupsAdminApi.md#deletedatabasebackup) | **DELETE** /admin/database-backups | Delete database backup
*DatabaseBackupsAdminApi* | [**downloadDatabaseBackup**](doc//DatabaseBackupsAdminApi.md#downloaddatabasebackup) | **GET** /admin/database-backups/{filename} | Download database backup
*DatabaseBackupsAdminApi* | [**listDatabaseBackups**](doc//DatabaseBackupsAdminApi.md#listdatabasebackups) | **GET** /admin/database-backups | List database backups
*DatabaseBackupsAdminApi* | [**startDatabaseRestoreFlow**](doc//DatabaseBackupsAdminApi.md#startdatabaserestoreflow) | **POST** /admin/database-backups/start-restore | Start database backup restore flow
*DatabaseBackupsAdminApi* | [**uploadDatabaseBackup**](doc//DatabaseBackupsAdminApi.md#uploaddatabasebackup) | **POST** /admin/database-backups/upload | Upload database backup
*DeprecatedApi* | [**createPartnerDeprecated**](doc//DeprecatedApi.md#createpartnerdeprecated) | **POST** /partners/{id} | Create a partner
*DeprecatedApi* | [**getAllUserAssetsByDeviceId**](doc//DeprecatedApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | Retrieve assets by device ID
*DeprecatedApi* | [**getDeltaSync**](doc//DeprecatedApi.md#getdeltasync) | **POST** /sync/delta-sync | Get delta sync for user
@@ -171,8 +166,6 @@ Class | Method | HTTP request | Description
*LibrariesApi* | [**scanLibrary**](doc//LibrariesApi.md#scanlibrary) | **POST** /libraries/{id}/scan | Scan a library
*LibrariesApi* | [**updateLibrary**](doc//LibrariesApi.md#updatelibrary) | **PUT** /libraries/{id} | Update a library
*LibrariesApi* | [**validate**](doc//LibrariesApi.md#validate) | **POST** /libraries/{id}/validate | Validate library settings
*MaintenanceAdminApi* | [**detectPriorInstall**](doc//MaintenanceAdminApi.md#detectpriorinstall) | **GET** /admin/maintenance/detect-install | Detect existing install
*MaintenanceAdminApi* | [**getMaintenanceStatus**](doc//MaintenanceAdminApi.md#getmaintenancestatus) | **GET** /admin/maintenance/status | Get maintenance mode status
*MaintenanceAdminApi* | [**maintenanceLogin**](doc//MaintenanceAdminApi.md#maintenancelogin) | **POST** /admin/maintenance/login | Log into maintenance mode
*MaintenanceAdminApi* | [**setMaintenanceMode**](doc//MaintenanceAdminApi.md#setmaintenancemode) | **POST** /admin/maintenance | Set maintenance mode
*MapApi* | [**getMapMarkers**](doc//MapApi.md#getmapmarkers) | **GET** /map/markers | Retrieve map markers
@@ -412,9 +405,6 @@ Class | Method | HTTP request | Description
- [CreateProfileImageResponseDto](doc//CreateProfileImageResponseDto.md)
- [CropParameters](doc//CropParameters.md)
- [DatabaseBackupConfig](doc//DatabaseBackupConfig.md)
- [DatabaseBackupDeleteDto](doc//DatabaseBackupDeleteDto.md)
- [DatabaseBackupDto](doc//DatabaseBackupDto.md)
- [DatabaseBackupListResponseDto](doc//DatabaseBackupListResponseDto.md)
- [DownloadArchiveInfo](doc//DownloadArchiveInfo.md)
- [DownloadInfoDto](doc//DownloadInfoDto.md)
- [DownloadResponse](doc//DownloadResponse.md)
@@ -444,10 +434,7 @@ Class | Method | HTTP request | Description
- [MachineLearningAvailabilityChecksDto](doc//MachineLearningAvailabilityChecksDto.md)
- [MaintenanceAction](doc//MaintenanceAction.md)
- [MaintenanceAuthDto](doc//MaintenanceAuthDto.md)
- [MaintenanceDetectInstallResponseDto](doc//MaintenanceDetectInstallResponseDto.md)
- [MaintenanceDetectInstallStorageFolderDto](doc//MaintenanceDetectInstallStorageFolderDto.md)
- [MaintenanceLoginDto](doc//MaintenanceLoginDto.md)
- [MaintenanceStatusResponseDto](doc//MaintenanceStatusResponseDto.md)
- [ManualJobName](doc//ManualJobName.md)
- [MapMarkerResponseDto](doc//MapMarkerResponseDto.md)
- [MapReverseGeocodeResponseDto](doc//MapReverseGeocodeResponseDto.md)
@@ -563,7 +550,6 @@ Class | Method | HTTP request | Description
- [StackResponseDto](doc//StackResponseDto.md)
- [StackUpdateDto](doc//StackUpdateDto.md)
- [StatisticsSearchDto](doc//StatisticsSearchDto.md)
- [StorageFolder](doc//StorageFolder.md)
- [SyncAckDeleteDto](doc//SyncAckDeleteDto.md)
- [SyncAckDto](doc//SyncAckDto.md)
- [SyncAckSetDto](doc//SyncAckSetDto.md)

View File

@@ -36,7 +36,6 @@ part 'api/albums_api.dart';
part 'api/assets_api.dart';
part 'api/authentication_api.dart';
part 'api/authentication_admin_api.dart';
part 'api/database_backups_admin_api.dart';
part 'api/deprecated_api.dart';
part 'api/download_api.dart';
part 'api/duplicates_api.dart';
@@ -152,9 +151,6 @@ part 'model/create_library_dto.dart';
part 'model/create_profile_image_response_dto.dart';
part 'model/crop_parameters.dart';
part 'model/database_backup_config.dart';
part 'model/database_backup_delete_dto.dart';
part 'model/database_backup_dto.dart';
part 'model/database_backup_list_response_dto.dart';
part 'model/download_archive_info.dart';
part 'model/download_info_dto.dart';
part 'model/download_response.dart';
@@ -184,10 +180,7 @@ part 'model/logout_response_dto.dart';
part 'model/machine_learning_availability_checks_dto.dart';
part 'model/maintenance_action.dart';
part 'model/maintenance_auth_dto.dart';
part 'model/maintenance_detect_install_response_dto.dart';
part 'model/maintenance_detect_install_storage_folder_dto.dart';
part 'model/maintenance_login_dto.dart';
part 'model/maintenance_status_response_dto.dart';
part 'model/manual_job_name.dart';
part 'model/map_marker_response_dto.dart';
part 'model/map_reverse_geocode_response_dto.dart';
@@ -303,7 +296,6 @@ part 'model/stack_create_dto.dart';
part 'model/stack_response_dto.dart';
part 'model/stack_update_dto.dart';
part 'model/statistics_search_dto.dart';
part 'model/storage_folder.dart';
part 'model/sync_ack_delete_dto.dart';
part 'model/sync_ack_dto.dart';
part 'model/sync_ack_set_dto.dart';

View File

@@ -1,269 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class 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));
}
}
}

View File

@@ -16,102 +16,6 @@ class MaintenanceAdminApi {
final ApiClient apiClient;
/// Detect existing install
///
/// Collect integrity checks and other heuristics about local data.
///
/// Note: This method returns the HTTP [Response].
Future<Response> detectPriorInstallWithHttpInfo() async {
// ignore: prefer_const_declarations
final apiPath = r'/admin/maintenance/detect-install';
// 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,
);
}
/// Detect existing install
///
/// Collect integrity checks and other heuristics about local data.
Future<MaintenanceDetectInstallResponseDto?> detectPriorInstall() async {
final response = await detectPriorInstallWithHttpInfo();
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), 'MaintenanceDetectInstallResponseDto',) as MaintenanceDetectInstallResponseDto;
}
return null;
}
/// Get maintenance mode status
///
/// Fetch information about the currently running maintenance action.
///
/// Note: This method returns the HTTP [Response].
Future<Response> getMaintenanceStatusWithHttpInfo() async {
// ignore: prefer_const_declarations
final apiPath = r'/admin/maintenance/status';
// 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,
);
}
/// Get maintenance mode status
///
/// Fetch information about the currently running maintenance action.
Future<MaintenanceStatusResponseDto?> getMaintenanceStatus() async {
final response = await getMaintenanceStatusWithHttpInfo();
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), 'MaintenanceStatusResponseDto',) as MaintenanceStatusResponseDto;
}
return null;
}
/// Log into maintenance mode
///
/// Login with maintenance token or cookie to receive current information and perform further actions.

View File

@@ -350,12 +350,6 @@ class ApiClient {
return CropParameters.fromJson(value);
case 'DatabaseBackupConfig':
return DatabaseBackupConfig.fromJson(value);
case 'DatabaseBackupDeleteDto':
return DatabaseBackupDeleteDto.fromJson(value);
case 'DatabaseBackupDto':
return DatabaseBackupDto.fromJson(value);
case 'DatabaseBackupListResponseDto':
return DatabaseBackupListResponseDto.fromJson(value);
case 'DownloadArchiveInfo':
return DownloadArchiveInfo.fromJson(value);
case 'DownloadInfoDto':
@@ -414,14 +408,8 @@ class ApiClient {
return MaintenanceActionTypeTransformer().decode(value);
case 'MaintenanceAuthDto':
return MaintenanceAuthDto.fromJson(value);
case 'MaintenanceDetectInstallResponseDto':
return MaintenanceDetectInstallResponseDto.fromJson(value);
case 'MaintenanceDetectInstallStorageFolderDto':
return MaintenanceDetectInstallStorageFolderDto.fromJson(value);
case 'MaintenanceLoginDto':
return MaintenanceLoginDto.fromJson(value);
case 'MaintenanceStatusResponseDto':
return MaintenanceStatusResponseDto.fromJson(value);
case 'ManualJobName':
return ManualJobNameTypeTransformer().decode(value);
case 'MapMarkerResponseDto':
@@ -652,8 +640,6 @@ class ApiClient {
return StackUpdateDto.fromJson(value);
case 'StatisticsSearchDto':
return StatisticsSearchDto.fromJson(value);
case 'StorageFolder':
return StorageFolderTypeTransformer().decode(value);
case 'SyncAckDeleteDto':
return SyncAckDeleteDto.fromJson(value);
case 'SyncAckDto':

View File

@@ -160,9 +160,6 @@ String parameterToString(dynamic value) {
if (value is SourceType) {
return SourceTypeTypeTransformer().encode(value).toString();
}
if (value is StorageFolder) {
return StorageFolderTypeTransformer().encode(value).toString();
}
if (value is SyncEntityType) {
return SyncEntityTypeTypeTransformer().encode(value).toString();
}

View File

@@ -1,101 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class 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',
};
}

View File

@@ -1,107 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class 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',
};
}

View File

@@ -1,99 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class 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',
};
}

View File

@@ -25,15 +25,11 @@ class MaintenanceAction {
static const start = MaintenanceAction._(r'start');
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].
static const values = <MaintenanceAction>[
start,
end,
selectDatabaseRestore,
restoreDatabase,
];
static MaintenanceAction? fromJson(dynamic value) => MaintenanceActionTypeTransformer().decode(value);
@@ -74,8 +70,6 @@ class MaintenanceActionTypeTransformer {
switch (data) {
case r'start': return MaintenanceAction.start;
case r'end': return MaintenanceAction.end;
case r'select_database_restore': return MaintenanceAction.selectDatabaseRestore;
case r'restore_database': return MaintenanceAction.restoreDatabase;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');

View File

@@ -1,99 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class 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',
};
}

View File

@@ -1,123 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class 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',
};
}

View File

@@ -1,158 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class 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',
};
}

View File

@@ -62,10 +62,6 @@ class Permission {
static const authPeriodChangePassword = Permission._(r'auth.changePassword');
static const authDevicePeriodDelete = Permission._(r'authDevice.delete');
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 duplicatePeriodDelete = Permission._(r'duplicate.delete');
static const facePeriodCreate = Permission._(r'face.create');
@@ -218,10 +214,6 @@ class Permission {
authPeriodChangePassword,
authDevicePeriodDelete,
archivePeriodRead,
backupPeriodList,
backupPeriodDownload,
backupPeriodUpload,
backupPeriodDelete,
duplicatePeriodRead,
duplicatePeriodDelete,
facePeriodCreate,
@@ -409,10 +401,6 @@ class PermissionTypeTransformer {
case r'auth.changePassword': return Permission.authPeriodChangePassword;
case r'authDevice.delete': return Permission.authDevicePeriodDelete;
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.delete': return Permission.duplicatePeriodDelete;
case r'face.create': return Permission.facePeriodCreate;

View File

@@ -14,41 +14,25 @@ class SetMaintenanceModeDto {
/// Returns a new [SetMaintenanceModeDto] instance.
SetMaintenanceModeDto({
required this.action,
this.restoreBackupFilename,
});
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
bool operator ==(Object other) => identical(this, other) || other is SetMaintenanceModeDto &&
other.action == action &&
other.restoreBackupFilename == restoreBackupFilename;
other.action == action;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(action.hashCode) +
(restoreBackupFilename == null ? 0 : restoreBackupFilename!.hashCode);
(action.hashCode);
@override
String toString() => 'SetMaintenanceModeDto[action=$action, restoreBackupFilename=$restoreBackupFilename]';
String toString() => 'SetMaintenanceModeDto[action=$action]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'action'] = this.action;
if (this.restoreBackupFilename != null) {
json[r'restoreBackupFilename'] = this.restoreBackupFilename;
} else {
// json[r'restoreBackupFilename'] = null;
}
return json;
}
@@ -62,7 +46,6 @@ class SetMaintenanceModeDto {
return SetMaintenanceModeDto(
action: MaintenanceAction.fromJson(json[r'action'])!,
restoreBackupFilename: mapValueOfType<String>(json, r'restoreBackupFilename'),
);
}
return null;

View File

@@ -1,97 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class 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;
}

View File

@@ -16,11 +16,11 @@ class SyncAssetV1 {
required this.checksum,
required this.deletedAt,
required this.duration,
required this.editCount,
required this.fileCreatedAt,
required this.fileModifiedAt,
required this.height,
required this.id,
required this.isEdited,
required this.isFavorite,
required this.libraryId,
required this.livePhotoVideoId,
@@ -40,6 +40,8 @@ class SyncAssetV1 {
String? duration;
int editCount;
DateTime? fileCreatedAt;
DateTime? fileModifiedAt;
@@ -48,8 +50,6 @@ class SyncAssetV1 {
String id;
bool isEdited;
bool isFavorite;
String? libraryId;
@@ -77,11 +77,11 @@ class SyncAssetV1 {
other.checksum == checksum &&
other.deletedAt == deletedAt &&
other.duration == duration &&
other.editCount == editCount &&
other.fileCreatedAt == fileCreatedAt &&
other.fileModifiedAt == fileModifiedAt &&
other.height == height &&
other.id == id &&
other.isEdited == isEdited &&
other.isFavorite == isFavorite &&
other.libraryId == libraryId &&
other.livePhotoVideoId == livePhotoVideoId &&
@@ -100,11 +100,11 @@ class SyncAssetV1 {
(checksum.hashCode) +
(deletedAt == null ? 0 : deletedAt!.hashCode) +
(duration == null ? 0 : duration!.hashCode) +
(editCount.hashCode) +
(fileCreatedAt == null ? 0 : fileCreatedAt!.hashCode) +
(fileModifiedAt == null ? 0 : fileModifiedAt!.hashCode) +
(height == null ? 0 : height!.hashCode) +
(id.hashCode) +
(isEdited.hashCode) +
(isFavorite.hashCode) +
(libraryId == null ? 0 : libraryId!.hashCode) +
(livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
@@ -118,7 +118,7 @@ class SyncAssetV1 {
(width == null ? 0 : width!.hashCode);
@override
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]';
String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, duration=$duration, editCount=$editCount, 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]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -133,6 +133,7 @@ class SyncAssetV1 {
} else {
// json[r'duration'] = null;
}
json[r'editCount'] = this.editCount;
if (this.fileCreatedAt != null) {
json[r'fileCreatedAt'] = this.fileCreatedAt!.toUtc().toIso8601String();
} else {
@@ -149,7 +150,6 @@ class SyncAssetV1 {
// json[r'height'] = null;
}
json[r'id'] = this.id;
json[r'isEdited'] = this.isEdited;
json[r'isFavorite'] = this.isFavorite;
if (this.libraryId != null) {
json[r'libraryId'] = this.libraryId;
@@ -200,11 +200,11 @@ class SyncAssetV1 {
checksum: mapValueOfType<String>(json, r'checksum')!,
deletedAt: mapDateTime(json, r'deletedAt', r''),
duration: mapValueOfType<String>(json, r'duration'),
editCount: mapValueOfType<int>(json, r'editCount')!,
fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r''),
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r''),
height: mapValueOfType<int>(json, r'height'),
id: mapValueOfType<String>(json, r'id')!,
isEdited: mapValueOfType<bool>(json, r'isEdited')!,
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
libraryId: mapValueOfType<String>(json, r'libraryId'),
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
@@ -266,11 +266,11 @@ class SyncAssetV1 {
'checksum',
'deletedAt',
'duration',
'editCount',
'fileCreatedAt',
'fileModifiedAt',
'height',
'id',
'isEdited',
'isFavorite',
'libraryId',
'livePhotoVideoId',

View File

@@ -6,7 +6,7 @@ version: 2.4.1+3030
environment:
sdk: '>=3.8.0 <4.0.0'
flutter: 3.35.7
flutter: 3.38.7
dependencies:
async: ^2.13.0
@@ -39,7 +39,7 @@ dependencies:
flutter_web_auth_2: ^5.0.0-alpha.0
fluttertoast: ^8.2.12
geolocator: ^14.0.2
home_widget: ^0.8.1
home_widget: 0.9.0
hooks_riverpod: ^2.6.1
http: ^1.5.0
image_picker: ^1.2.0
@@ -54,7 +54,7 @@ dependencies:
isar_community_flutter_libs: 3.3.0-dev.3
local_auth: ^2.3.0
logging: ^1.3.0
maplibre_gl: ^0.22.0
maplibre_gl: 0.25.0
native_video_player:
git:
@@ -81,7 +81,7 @@ dependencies:
socket_io_client: ^2.0.3+1
stream_transform: ^2.1.1
thumbhash: 0.1.0+1
timezone: ^0.9.4
timezone: 0.11.0
url_launcher: ^6.3.2
uuid: ^4.5.1
wakelock_plus: ^1.3.0
@@ -90,7 +90,7 @@ dependencies:
dev_dependencies:
auto_route_generator: ^9.0.0
build_runner: ^2.4.8
custom_lint: ^0.7.5
custom_lint: 0.8.1
# Drift generator
drift_dev: ^2.26.0
fake_async: ^1.3.3

View File

@@ -44,7 +44,7 @@ SyncAssetV1 _createAsset({
livePhotoVideoId: null,
stackId: null,
thumbhash: null,
isEdited: false,
editCount: 0,
);
}

View File

@@ -19,7 +19,6 @@ import 'schema_v13.dart' as v13;
import 'schema_v14.dart' as v14;
import 'schema_v15.dart' as v15;
import 'schema_v16.dart' as v16;
import 'schema_v17.dart' as v17;
class GeneratedHelper implements SchemaInstantiationHelper {
@override
@@ -57,8 +56,6 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v15.DatabaseAtV15(db);
case 16:
return v16.DatabaseAtV16(db);
case 17:
return v17.DatabaseAtV17(db);
default:
throw MissingSchemaException(version, versions);
}
@@ -81,6 +78,5 @@ class GeneratedHelper implements SchemaInstantiationHelper {
14,
15,
16,
17,
];
}

File diff suppressed because it is too large Load Diff

View File

@@ -64,7 +64,6 @@ abstract final class LocalAssetStub {
type: AssetType.image,
createdAt: DateTime(2025),
updatedAt: DateTime(2025, 2),
isEdited: false,
);
static final image2 = LocalAsset(
@@ -73,6 +72,5 @@ abstract final class LocalAssetStub {
type: AssetType.image,
createdAt: DateTime(2000),
updatedAt: DateTime(20021),
isEdited: false,
);
}

View File

@@ -128,7 +128,7 @@ abstract final class SyncStreamStub {
visibility: AssetVisibility.timeline,
width: null,
height: null,
isEdited: false,
editCount: 0,
),
ack: ack,
);

View File

@@ -194,7 +194,6 @@ void main() {
latitude: 37.7749,
longitude: -122.4194,
adjustmentTime: DateTime(2026, 1, 2),
isEdited: false,
);
final mockEntity = MockAssetEntity();
@@ -243,7 +242,6 @@ void main() {
cloudId: 'cloud-id-123',
latitude: 37.7749,
longitude: -122.4194,
isEdited: false,
);
final mockEntity = MockAssetEntity();
@@ -281,7 +279,6 @@ void main() {
createdAt: DateTime(2025, 1, 1),
updatedAt: DateTime(2025, 1, 2),
cloudId: null, // No cloudId
isEdited: false,
);
final mockEntity = MockAssetEntity();
@@ -323,7 +320,6 @@ void main() {
cloudId: 'cloud-id-livephoto',
latitude: 37.7749,
longitude: -122.4194,
isEdited: false,
);
final mockEntity = MockAssetEntity();

View File

@@ -131,7 +131,6 @@ abstract final class TestUtils {
isFavorite: false,
width: width,
height: height,
isEdited: false,
);
}
@@ -155,7 +154,6 @@ abstract final class TestUtils {
width: width,
height: height,
orientation: orientation,
isEdited: false,
);
}
}

View File

@@ -27,7 +27,6 @@ class MediumFactory {
type: type ?? AssetType.image,
createdAt: createdAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)),
updatedAt: updatedAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)),
isEdited: false,
);
}

View File

@@ -23,7 +23,6 @@ LocalAsset createLocalAsset({
createdAt: createdAt ?? DateTime.now(),
updatedAt: updatedAt ?? DateTime.now(),
isFavorite: isFavorite,
isEdited: false,
);
}
@@ -46,7 +45,6 @@ RemoteAsset createRemoteAsset({
createdAt: createdAt ?? DateTime.now(),
updatedAt: updatedAt ?? DateTime.now(),
isFavorite: isFavorite,
isEdited: false,
);
}

View File

@@ -322,237 +322,6 @@
"x-immich-state": "Stable"
}
},
"/admin/database-backups": {
"delete": {
"description": "Delete a backup by its filename",
"operationId": "deleteDatabaseBackup",
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DatabaseBackupDeleteDto"
}
}
},
"required": true
},
"responses": {
"200": {
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"summary": "Delete database backup",
"tags": [
"Database Backups (admin)"
],
"x-immich-admin-only": true,
"x-immich-history": [
{
"version": "v2.5.0",
"state": "Added"
},
{
"version": "v2.5.0",
"state": "Alpha"
}
],
"x-immich-permission": "backup.delete",
"x-immich-state": "Alpha"
},
"get": {
"description": "Get the list of the successful and failed backups",
"operationId": "listDatabaseBackups",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DatabaseBackupListResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"summary": "List database backups",
"tags": [
"Database Backups (admin)"
],
"x-immich-admin-only": true,
"x-immich-history": [
{
"version": "v2.5.0",
"state": "Added"
},
{
"version": "v2.5.0",
"state": "Alpha"
}
],
"x-immich-permission": "maintenance",
"x-immich-state": "Alpha"
}
},
"/admin/database-backups/start-restore": {
"post": {
"description": "Put Immich into maintenance mode to restore a backup (Immich must not be configured)",
"operationId": "startDatabaseRestoreFlow",
"parameters": [],
"responses": {
"201": {
"description": ""
}
},
"summary": "Start database backup restore flow",
"tags": [
"Database Backups (admin)"
],
"x-immich-history": [
{
"version": "v2.5.0",
"state": "Added"
},
{
"version": "v2.5.0",
"state": "Alpha"
}
],
"x-immich-state": "Alpha"
}
},
"/admin/database-backups/upload": {
"post": {
"description": "Uploads .sql/.sql.gz file to restore backup from",
"operationId": "uploadDatabaseBackup",
"parameters": [],
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/DatabaseBackupUploadDto"
}
}
},
"description": "Backup Upload",
"required": true
},
"responses": {
"201": {
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"summary": "Upload database backup",
"tags": [
"Database Backups (admin)"
],
"x-immich-admin-only": true,
"x-immich-history": [
{
"version": "v2.5.0",
"state": "Added"
},
{
"version": "v2.5.0",
"state": "Alpha"
}
],
"x-immich-permission": "backup.upload",
"x-immich-state": "Alpha"
}
},
"/admin/database-backups/{filename}": {
"get": {
"description": "Downloads the database backup file",
"operationId": "downloadDatabaseBackup",
"parameters": [
{
"name": "filename",
"required": true,
"in": "path",
"schema": {
"format": "string",
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/octet-stream": {
"schema": {
"format": "binary",
"type": "string"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"summary": "Download database backup",
"tags": [
"Database Backups (admin)"
],
"x-immich-admin-only": true,
"x-immich-history": [
{
"version": "v2.5.0",
"state": "Added"
},
{
"version": "v2.5.0",
"state": "Alpha"
}
],
"x-immich-permission": "backup.download",
"x-immich-state": "Alpha"
}
},
"/admin/maintenance": {
"post": {
"description": "Put Immich into or take it out of maintenance mode",
@@ -603,53 +372,6 @@
"x-immich-state": "Alpha"
}
},
"/admin/maintenance/detect-install": {
"get": {
"description": "Collect integrity checks and other heuristics about local data.",
"operationId": "detectPriorInstall",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MaintenanceDetectInstallResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"summary": "Detect existing install",
"tags": [
"Maintenance (admin)"
],
"x-immich-admin-only": true,
"x-immich-history": [
{
"version": "v2.5.0",
"state": "Added"
},
{
"version": "v2.5.0",
"state": "Alpha"
}
],
"x-immich-permission": "maintenance",
"x-immich-state": "Alpha"
}
},
"/admin/maintenance/login": {
"post": {
"description": "Login with maintenance token or cookie to receive current information and perform further actions.",
@@ -694,40 +416,6 @@
"x-immich-state": "Alpha"
}
},
"/admin/maintenance/status": {
"get": {
"description": "Fetch information about the currently running maintenance action.",
"operationId": "getMaintenanceStatus",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MaintenanceStatusResponseDto"
}
}
},
"description": ""
}
},
"summary": "Get maintenance mode status",
"tags": [
"Maintenance (admin)"
],
"x-immich-history": [
{
"version": "v2.5.0",
"state": "Added"
},
{
"version": "v2.5.0",
"state": "Alpha"
}
],
"x-immich-state": "Alpha"
}
},
"/admin/notifications": {
"post": {
"description": "Create a new notification for a specific user.",
@@ -14973,10 +14661,6 @@
"name": "Authentication (admin)",
"description": "Administrative endpoints related to authentication."
},
{
"name": "Database Backups (admin)",
"description": "Manage backups of the Immich database."
},
{
"name": "Deprecated",
"description": "Deprecated endpoints that are planned for removal in the next major release."
@@ -17165,58 +16849,6 @@
],
"type": "object"
},
"DatabaseBackupDeleteDto": {
"properties": {
"backups": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"backups"
],
"type": "object"
},
"DatabaseBackupDto": {
"properties": {
"filename": {
"type": "string"
},
"filesize": {
"type": "number"
}
},
"required": [
"filename",
"filesize"
],
"type": "object"
},
"DatabaseBackupListResponseDto": {
"properties": {
"backups": {
"items": {
"$ref": "#/components/schemas/DatabaseBackupDto"
},
"type": "array"
}
},
"required": [
"backups"
],
"type": "object"
},
"DatabaseBackupUploadDto": {
"properties": {
"file": {
"format": "binary",
"type": "string"
}
},
"type": "object"
},
"DownloadArchiveInfo": {
"properties": {
"assetIds": {
@@ -17884,9 +17516,7 @@
"MaintenanceAction": {
"enum": [
"start",
"end",
"select_database_restore",
"restore_database"
"end"
],
"type": "string"
},
@@ -17901,47 +17531,6 @@
],
"type": "object"
},
"MaintenanceDetectInstallResponseDto": {
"properties": {
"storage": {
"items": {
"$ref": "#/components/schemas/MaintenanceDetectInstallStorageFolderDto"
},
"type": "array"
}
},
"required": [
"storage"
],
"type": "object"
},
"MaintenanceDetectInstallStorageFolderDto": {
"properties": {
"files": {
"type": "number"
},
"folder": {
"allOf": [
{
"$ref": "#/components/schemas/StorageFolder"
}
]
},
"readable": {
"type": "boolean"
},
"writable": {
"type": "boolean"
}
},
"required": [
"files",
"folder",
"readable",
"writable"
],
"type": "object"
},
"MaintenanceLoginDto": {
"properties": {
"token": {
@@ -17950,34 +17539,6 @@
},
"type": "object"
},
"MaintenanceStatusResponseDto": {
"properties": {
"action": {
"allOf": [
{
"$ref": "#/components/schemas/MaintenanceAction"
}
]
},
"active": {
"type": "boolean"
},
"error": {
"type": "string"
},
"progress": {
"type": "number"
},
"task": {
"type": "string"
}
},
"required": [
"action",
"active"
],
"type": "object"
},
"ManualJobName": {
"enum": [
"person-cleanup",
@@ -18946,10 +18507,6 @@
"auth.changePassword",
"authDevice.delete",
"archive.read",
"backup.list",
"backup.download",
"backup.upload",
"backup.delete",
"duplicate.read",
"duplicate.delete",
"face.create",
@@ -20728,9 +20285,6 @@
"$ref": "#/components/schemas/MaintenanceAction"
}
]
},
"restoreBackupFilename": {
"type": "string"
}
},
"required": [
@@ -21303,17 +20857,6 @@
},
"type": "object"
},
"StorageFolder": {
"enum": [
"encoded-video",
"library",
"upload",
"profile",
"thumbs",
"backups"
],
"type": "string"
},
"SyncAckDeleteDto": {
"properties": {
"types": {
@@ -21748,6 +21291,9 @@
"nullable": true,
"type": "string"
},
"editCount": {
"type": "integer"
},
"fileCreatedAt": {
"format": "date-time",
"nullable": true,
@@ -21765,9 +21311,6 @@
"id": {
"type": "string"
},
"isEdited": {
"type": "boolean"
},
"isFavorite": {
"type": "boolean"
},
@@ -21821,11 +21364,11 @@
"checksum",
"deletedAt",
"duration",
"editCount",
"fileCreatedAt",
"fileModifiedAt",
"height",
"id",
"isEdited",
"isFavorite",
"libraryId",
"livePhotoVideoId",

View File

@@ -19,7 +19,7 @@
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^24.10.8",
"@types/node": "^24.10.4",
"typescript": "^5.3.3"
},
"repository": {

Some files were not shown because too many files have changed in this diff Show More