From 134c0d4dfbc949932d2b33b80c587201d4397382 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 28 May 2026 17:01:47 -0400 Subject: [PATCH 01/50] feat: search by album name and id (#28672) --- mobile/openapi/lib/api/albums_api.dart | 24 ++++++++++++++++++--- open-api/immich-openapi-specs.json | 20 +++++++++++++++++ packages/sdk/src/fetch-client.ts | 8 +++++-- server/src/dtos/album.dto.ts | 2 ++ server/src/repositories/album.repository.ts | 7 +++++- server/src/services/album.service.ts | 7 ++---- 6 files changed, 57 insertions(+), 11 deletions(-) diff --git a/mobile/openapi/lib/api/albums_api.dart b/mobile/openapi/lib/api/albums_api.dart index e0fc383c1d..c22a8ced07 100644 --- a/mobile/openapi/lib/api/albums_api.dart +++ b/mobile/openapi/lib/api/albums_api.dart @@ -508,12 +508,18 @@ class AlbumsApi { /// * [String] assetId: /// Filter albums containing this asset ID (ignores other parameters) /// + /// * [String] id: + /// Album ID + /// /// * [bool] isOwned: /// Filter by ownership: true = only owned, false = only shared-with-me, undefined = no filter /// /// * [bool] isShared: /// Filter by shared status: true = only shared, false = not shared, undefined = no filter - Future getAllAlbumsWithHttpInfo({ String? assetId, bool? isOwned, bool? isShared, }) async { + /// + /// * [String] name: + /// Album name (exact match) + Future getAllAlbumsWithHttpInfo({ String? assetId, String? id, bool? isOwned, bool? isShared, String? name, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums'; @@ -527,12 +533,18 @@ class AlbumsApi { if (assetId != null) { queryParams.addAll(_queryParams('', 'assetId', assetId)); } + if (id != null) { + queryParams.addAll(_queryParams('', 'id', id)); + } if (isOwned != null) { queryParams.addAll(_queryParams('', 'isOwned', isOwned)); } if (isShared != null) { queryParams.addAll(_queryParams('', 'isShared', isShared)); } + if (name != null) { + queryParams.addAll(_queryParams('', 'name', name)); + } const contentTypes = []; @@ -557,13 +569,19 @@ class AlbumsApi { /// * [String] assetId: /// Filter albums containing this asset ID (ignores other parameters) /// + /// * [String] id: + /// Album ID + /// /// * [bool] isOwned: /// Filter by ownership: true = only owned, false = only shared-with-me, undefined = no filter /// /// * [bool] isShared: /// Filter by shared status: true = only shared, false = not shared, undefined = no filter - Future?> getAllAlbums({ String? assetId, bool? isOwned, bool? isShared, }) async { - final response = await getAllAlbumsWithHttpInfo( assetId: assetId, isOwned: isOwned, isShared: isShared, ); + /// + /// * [String] name: + /// Album name (exact match) + Future?> getAllAlbums({ String? assetId, String? id, bool? isOwned, bool? isShared, String? name, }) async { + final response = await getAllAlbumsWithHttpInfo( assetId: assetId, id: id, isOwned: isOwned, isShared: isShared, name: name, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 9fda205b9a..5857e6b1ce 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -1627,6 +1627,17 @@ "type": "string" } }, + { + "name": "id", + "required": false, + "in": "query", + "description": "Album ID", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + }, { "name": "isOwned", "required": false, @@ -1644,6 +1655,15 @@ "schema": { "type": "boolean" } + }, + { + "name": "name", + "required": false, + "in": "query", + "description": "Album name (exact match)", + "schema": { + "type": "string" + } } ], "responses": { diff --git a/packages/sdk/src/fetch-client.ts b/packages/sdk/src/fetch-client.ts index 3f328088ee..66d7a996a3 100644 --- a/packages/sdk/src/fetch-client.ts +++ b/packages/sdk/src/fetch-client.ts @@ -3597,18 +3597,22 @@ export function getUserStatisticsAdmin({ id, isFavorite, isTrashed, visibility } /** * List all albums */ -export function getAllAlbums({ assetId, isOwned, isShared }: { +export function getAllAlbums({ assetId, id, isOwned, isShared, name }: { assetId?: string; + id?: string; isOwned?: boolean; isShared?: boolean; + name?: string; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: AlbumResponseDto[]; }>(`/albums${QS.query(QS.explode({ assetId, + id, isOwned, - isShared + isShared, + name }))}`, { ...opts })); diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts index 095e399b96..100550659d 100644 --- a/server/src/dtos/album.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -65,6 +65,8 @@ const UpdateAlbumSchema = z const GetAlbumsSchema = z .object({ + id: z.uuidv4().optional().describe('Album ID'), + name: z.string().optional().describe('Album name (exact match)'), isOwned: stringToBool .optional() .describe('Filter by ownership: true = only owned, false = only shared-with-me, undefined = no filter'), diff --git a/server/src/repositories/album.repository.ts b/server/src/repositories/album.repository.ts index a712151355..724788fa74 100644 --- a/server/src/repositories/album.repository.ts +++ b/server/src/repositories/album.repository.ts @@ -209,11 +209,16 @@ export class AlbumRepository { } @GenerateSql({ params: [DummyValue.UUID, { isOwned: true, isShared: true }] }) - getAll(ownerId: string, options: { isOwned?: boolean; isShared?: boolean } = {}): Promise { + getAll( + ownerId: string, + options: { id?: string; isOwned?: boolean; isShared?: boolean; name?: string } = {}, + ): Promise { return this.buildAlbumBaseQuery(ownerId, options) .selectAll('album') .select(withAlbumUsers(ownerId)) .select(withSharedLink) + .$if(!!options.id, (qb) => qb.where('album.id', '=', options.id!)) + .$if(!!options.name, (qb) => qb.where('album.albumName', '=', options.name!)) .orderBy('album.createdAt', 'desc') .execute(); } diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index 723288e5b5..31c4ff2e38 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -37,15 +37,12 @@ export class AlbumService extends BaseService { }; } - async getAll( - { user: { id: ownerId } }: AuthDto, - { assetId, isOwned, isShared }: GetAlbumsDto, - ): Promise { + async getAll({ user: { id: ownerId } }: AuthDto, { assetId, ...rest }: GetAlbumsDto): Promise { await this.albumRepository.updateThumbnails(); const albums = assetId ? await this.albumRepository.getByAssetId(ownerId, assetId) - : await this.albumRepository.getAll(ownerId, { isOwned, isShared }); + : await this.albumRepository.getAll(ownerId, rest); if (albums.length === 0) { return []; From 8783180cf3f7380dbb5c13733d08e074bba38571 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 28 May 2026 17:23:49 -0400 Subject: [PATCH 02/50] refactor: plugin manifest (#28673) --- i18n/en.json | 1 + mobile/openapi/lib/model/job_name.dart | 6 +- .../model/plugin_template_response_dto.dart | 17 +++- open-api/immich-openapi-specs.json | 12 ++- packages/plugin-core/manifest.json | 29 +++---- packages/plugin-core/src/index.ts | 2 +- packages/plugin-sdk/src/types.ts | 18 ++--- packages/sdk/src/fetch-client.ts | 4 +- server/src/dtos/plugin-manifest.dto.ts | 1 + server/src/dtos/plugin.dto.ts | 3 + server/src/enum.ts | 2 +- .../src/repositories/workflow.repository.ts | 4 +- .../services/workflow-execution.service.ts | 77 +++++++++++++------ server/src/services/workflow.service.ts | 2 +- server/src/types.ts | 2 +- .../workflow/workflow-core-plugin.spec.ts | 18 ++--- .../lib/components/SchemaConfiguration.svelte | 2 +- web/src/lib/modals/PluginMethodPicker.svelte | 2 +- web/src/lib/modals/WorkflowEditTrigger.svelte | 37 --------- ...lte => WorkflowTemplatePickerModal.svelte} | 7 +- web/src/lib/services/workflow.service.ts | 4 +- web/src/lib/types.ts | 2 +- .../[workflowId]/WorkflowStepCard.svelte | 4 +- .../[workflowId]/WorkflowSummary.svelte | 2 +- 24 files changed, 140 insertions(+), 118 deletions(-) delete mode 100644 web/src/lib/modals/WorkflowEditTrigger.svelte rename web/src/lib/modals/{WorkflowTemplatePicker.svelte => WorkflowTemplatePickerModal.svelte} (87%) diff --git a/i18n/en.json b/i18n/en.json index 4ebd24b56b..dba0caf393 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -2233,6 +2233,7 @@ "slideshow_repeat": "Repeat slideshow", "slideshow_repeat_description": "Loop back to beginning when slideshow ends", "slideshow_settings": "Slideshow settings", + "smart_album": "Smart album", "sort_albums_by": "Sort albums by...", "sort_created": "Date created", "sort_items": "Number of items", diff --git a/mobile/openapi/lib/model/job_name.dart b/mobile/openapi/lib/model/job_name.dart index 444b080c12..511e1158e9 100644 --- a/mobile/openapi/lib/model/job_name.dart +++ b/mobile/openapi/lib/model/job_name.dart @@ -77,7 +77,7 @@ class JobName { static const versionCheck = JobName._(r'VersionCheck'); static const ocrQueueAll = JobName._(r'OcrQueueAll'); static const ocr = JobName._(r'Ocr'); - static const workflowAssetCreate = JobName._(r'WorkflowAssetCreate'); + static const workflowAssetTrigger = JobName._(r'WorkflowAssetTrigger'); /// List of all possible values in this [enum][JobName]. static const values = [ @@ -135,7 +135,7 @@ class JobName { versionCheck, ocrQueueAll, ocr, - workflowAssetCreate, + workflowAssetTrigger, ]; static JobName? fromJson(dynamic value) => JobNameTypeTransformer().decode(value); @@ -228,7 +228,7 @@ class JobNameTypeTransformer { case r'VersionCheck': return JobName.versionCheck; case r'OcrQueueAll': return JobName.ocrQueueAll; case r'Ocr': return JobName.ocr; - case r'WorkflowAssetCreate': return JobName.workflowAssetCreate; + case r'WorkflowAssetTrigger': return JobName.workflowAssetTrigger; default: if (!allowNull) { throw ArgumentError('Unknown enum value to decode: $data'); diff --git a/mobile/openapi/lib/model/plugin_template_response_dto.dart b/mobile/openapi/lib/model/plugin_template_response_dto.dart index 4625da37d3..9f54753f49 100644 --- a/mobile/openapi/lib/model/plugin_template_response_dto.dart +++ b/mobile/openapi/lib/model/plugin_template_response_dto.dart @@ -18,6 +18,7 @@ class PluginTemplateResponseDto { this.steps = const [], required this.title, required this.trigger, + this.uiHints = const [], }); /// Template description @@ -34,13 +35,17 @@ class PluginTemplateResponseDto { WorkflowTrigger trigger; + /// Ui hints, for example \"smart-album\" + List uiHints; + @override bool operator ==(Object other) => identical(this, other) || other is PluginTemplateResponseDto && other.description == description && other.key == key && _deepEquality.equals(other.steps, steps) && other.title == title && - other.trigger == trigger; + other.trigger == trigger && + _deepEquality.equals(other.uiHints, uiHints); @override int get hashCode => @@ -49,10 +54,11 @@ class PluginTemplateResponseDto { (key.hashCode) + (steps.hashCode) + (title.hashCode) + - (trigger.hashCode); + (trigger.hashCode) + + (uiHints.hashCode); @override - String toString() => 'PluginTemplateResponseDto[description=$description, key=$key, steps=$steps, title=$title, trigger=$trigger]'; + String toString() => 'PluginTemplateResponseDto[description=$description, key=$key, steps=$steps, title=$title, trigger=$trigger, uiHints=$uiHints]'; Map toJson() { final json = {}; @@ -61,6 +67,7 @@ class PluginTemplateResponseDto { json[r'steps'] = this.steps; json[r'title'] = this.title; json[r'trigger'] = this.trigger; + json[r'uiHints'] = this.uiHints; return json; } @@ -78,6 +85,9 @@ class PluginTemplateResponseDto { steps: PluginTemplateStepResponseDto.listFromJson(json[r'steps']), title: mapValueOfType(json, r'title')!, trigger: WorkflowTrigger.fromJson(json[r'trigger'])!, + uiHints: json[r'uiHints'] is Iterable + ? (json[r'uiHints'] as Iterable).cast().toList(growable: false) + : const [], ); } return null; @@ -130,6 +140,7 @@ class PluginTemplateResponseDto { 'steps', 'title', 'trigger', + 'uiHints', }; } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 5857e6b1ce..20033cbc09 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -18171,7 +18171,7 @@ "VersionCheck", "OcrQueueAll", "Ocr", - "WorkflowAssetCreate" + "WorkflowAssetTrigger" ], "type": "string" }, @@ -20219,6 +20219,13 @@ "trigger": { "$ref": "#/components/schemas/WorkflowTrigger", "description": "Workflow trigger" + }, + "uiHints": { + "description": "Ui hints, for example \"smart-album\"", + "items": { + "type": "string" + }, + "type": "array" } }, "required": [ @@ -20226,7 +20233,8 @@ "key", "steps", "title", - "trigger" + "trigger", + "uiHints" ], "type": "object" }, diff --git a/packages/plugin-core/manifest.json b/packages/plugin-core/manifest.json index 3111678862..7a9a5f26cd 100644 --- a/packages/plugin-core/manifest.json +++ b/packages/plugin-core/manifest.json @@ -20,19 +20,20 @@ "caseSensitive": false } }, - { - "method": "immich-plugin-core#assetAddToAlbums", - "config": { - "albumIds": [] - } - }, { "method": "immich-plugin-core#assetArchive", "config": { "inverse": false } + }, + { + "method": "immich-plugin-core#assetAddToAlbums", + "config": { + "albumIds": [] + } } - ] + ], + "uiHints": ["SmartAlbum"] } ], "methods": [ @@ -65,7 +66,7 @@ }, "required": ["pattern"] }, - "uiHints": ["filter"] + "uiHints": ["Filter"] }, { "name": "filterFileType", @@ -85,7 +86,7 @@ }, "required": ["fileTypes"] }, - "uiHints": ["filter"] + "uiHints": ["Filter"] }, { "name": "filterPerson", @@ -99,7 +100,7 @@ "array": true, "title": "Person IDs", "description": "List of person to match", - "uiHint": "personI" + "uiHint": "personId" }, "matchAny": { "type": "boolean", @@ -110,7 +111,7 @@ }, "required": ["personIds"] }, - "uiHints": ["filter"] + "uiHints": ["Filter"] }, { "name": "assetArchive", @@ -187,7 +188,7 @@ "title": "Album IDs", "array": true, "description": "Target album IDs", - "uiHint": "albumId" + "uiHint": "AlbumId" } }, "required": ["albumIds"] @@ -272,14 +273,14 @@ "type": "string", "title": "Album ID", "description": "Target album ID", - "uiHint": "albumId" + "uiHint": "AlbumId" }, "albumIds": { "type": "string", "title": "Album IDs", "description": "Target album IDs", "array": true, - "uiHint": "albumId" + "uiHint": "AlbumId" } } } diff --git a/packages/plugin-core/src/index.ts b/packages/plugin-core/src/index.ts index 85a4a449e7..2b498614fa 100644 --- a/packages/plugin-core/src/index.ts +++ b/packages/plugin-core/src/index.ts @@ -93,7 +93,7 @@ export const assetTrash = () => { changes: { asset: config.inverse ? { deletedAt: null, status: AssetStatus.Active } - : { deletedAt: new Date(), status: AssetStatus.Trashed }, + : { deletedAt: new Date().toISOString(), status: AssetStatus.Trashed }, }, })); }; diff --git a/packages/plugin-sdk/src/types.ts b/packages/plugin-sdk/src/types.ts index 54cca3a5aa..2613922e95 100644 --- a/packages/plugin-sdk/src/types.ts +++ b/packages/plugin-sdk/src/types.ts @@ -68,19 +68,19 @@ export type AssetV1 = { ownerId: string; type: AssetType; originalPath: string; - fileCreatedAt: Date; - fileModifiedAt: Date; + fileCreatedAt: string; + fileModifiedAt: string; isFavorite: boolean; checksum: Buffer; // sha1 checksum livePhotoVideoId: string | null; - updatedAt: Date; - createdAt: Date; + updatedAt: string; + createdAt: string; originalFileName: string; isOffline: boolean; libraryId: string | null; isExternal: boolean; - deletedAt: Date | null; - localDateTime: Date; + deletedAt: string | null; + localDateTime: string; stackId: string | null; duplicateId: string | null; status: AssetStatus; @@ -93,8 +93,8 @@ export type AssetV1 = { exifImageHeight: number | null; fileSizeInByte: number | null; orientation: string | null; - dateTimeOriginal: Date | null; - modifyDate: Date | null; + dateTimeOriginal: string | null; + modifyDate: string | null; lensModel: string | null; fNumber: number | null; focalLength: number | null; @@ -116,7 +116,7 @@ export type AssetV1 = { autoStackId: string | null; rating: number | null; tags: string[] | null; - updatedAt: Date | null; + updatedAt: string | null; } | null; }; }; diff --git a/packages/sdk/src/fetch-client.ts b/packages/sdk/src/fetch-client.ts index 66d7a996a3..2e35f25f3d 100644 --- a/packages/sdk/src/fetch-client.ts +++ b/packages/sdk/src/fetch-client.ts @@ -1535,6 +1535,8 @@ export type PluginTemplateResponseDto = { title: string; /** Workflow trigger */ trigger: WorkflowTrigger; + /** Ui hints, for example "smart-album" */ + uiHints: string[]; }; export type QueueResponseDto = { /** Whether the queue is paused */ @@ -7144,7 +7146,7 @@ export enum JobName { VersionCheck = "VersionCheck", OcrQueueAll = "OcrQueueAll", Ocr = "Ocr", - WorkflowAssetCreate = "WorkflowAssetCreate" + WorkflowAssetTrigger = "WorkflowAssetTrigger" } export enum SearchSuggestionType { Country = "country", diff --git a/server/src/dtos/plugin-manifest.dto.ts b/server/src/dtos/plugin-manifest.dto.ts index c8f043fde1..b175c6e1bb 100644 --- a/server/src/dtos/plugin-manifest.dto.ts +++ b/server/src/dtos/plugin-manifest.dto.ts @@ -38,6 +38,7 @@ const PluginManifestTemplateSchema = z description: z.string().min(1).describe('Template description'), trigger: WorkflowTriggerSchema.describe('Workflow trigger'), steps: z.array(PluginManifestTemplateStepSchema).describe('Workflow steps'), + uiHints: z.array(z.string()).optional().default([]).describe('Ui hints, for example "smart-album"'), }) .meta({ id: 'PluginManifestTemplateDto' }); diff --git a/server/src/dtos/plugin.dto.ts b/server/src/dtos/plugin.dto.ts index 074321bb44..62ff365a43 100644 --- a/server/src/dtos/plugin.dto.ts +++ b/server/src/dtos/plugin.dto.ts @@ -58,6 +58,7 @@ const PluginTemplateResponseSchema = z description: z.string().describe('Template description'), trigger: WorkflowTriggerSchema.describe('Workflow trigger'), steps: z.array(PluginTemplateStepResponseSchema).describe('Workflow steps'), + uiHints: z.array(z.string()).describe('Ui hints, for example "smart-album"'), }) .meta({ id: 'PluginTemplateResponseDto' }); @@ -91,6 +92,7 @@ export type PluginTemplate = { config?: Record | null; enabled?: boolean; }>; + uiHints: string[]; }; export const mapTemplate = (plugin: { name: string }, template: PluginTemplate): PluginTemplateResponseDto => { @@ -104,6 +106,7 @@ export const mapTemplate = (plugin: { name: string }, template: PluginTemplate): config: step.config ?? null, enabled: step.enabled, })), + uiHints: template.uiHints ?? [], }; }; diff --git a/server/src/enum.ts b/server/src/enum.ts index bc52e65f83..baf34b806e 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -866,7 +866,7 @@ export enum JobName { Ocr = 'Ocr', // Workflow - WorkflowAssetCreate = 'WorkflowAssetCreate', + WorkflowAssetTrigger = 'WorkflowAssetTrigger', } export const JobNameSchema = z.enum(JobName).describe('Job name').meta({ id: 'JobName' }); diff --git a/server/src/repositories/workflow.repository.ts b/server/src/repositories/workflow.repository.ts index 69ecb83ae9..1b888b759b 100644 --- a/server/src/repositories/workflow.repository.ts +++ b/server/src/repositories/workflow.repository.ts @@ -45,10 +45,10 @@ export class WorkflowRepository { } @GenerateSql({ params: [DummyValue.UUID] }) - search(dto: WorkflowSearchDto & { ownerId?: string }) { + search(dto: WorkflowSearchDto & { userId?: string }) { return this.queryBuilder() .$if(!!dto.id, (qb) => qb.where('id', '=', dto.id!)) - .$if(!!dto.ownerId, (qb) => qb.where('ownerId', '=', dto.ownerId!)) + .$if(!!dto.userId, (qb) => qb.where('ownerId', '=', dto.userId!)) .$if(!!dto.trigger, (qb) => qb.where('trigger', '=', dto.trigger!)) .$if(dto.enabled !== undefined, (qb) => qb.where('enabled', '=', dto.enabled!)) .orderBy('createdAt', 'desc') diff --git a/server/src/services/workflow-execution.service.ts b/server/src/services/workflow-execution.service.ts index a1ecf4526d..0f600e117b 100644 --- a/server/src/services/workflow-execution.service.ts +++ b/server/src/services/workflow-execution.service.ts @@ -1,9 +1,8 @@ import { CurrentPlugin } from '@extism/extism'; import { WorkflowChanges, WorkflowEventData, WorkflowEventPayload, WorkflowResponse } from '@immich/plugin-sdk'; import { HttpException, UnauthorizedException } from '@nestjs/common'; -import _ from 'lodash'; import { join } from 'node:path'; -import { OnEvent, OnJob } from 'src/decorators'; +import { DummyValue, OnEvent, OnJob } from 'src/decorators'; import { AlbumsAddAssetsDto } from 'src/dtos/album.dto'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; @@ -21,6 +20,7 @@ import { } from 'src/enum'; import { ArgOf } from 'src/repositories/event.repository'; import { AlbumService } from 'src/services/album.service'; +import { AssetService } from 'src/services/asset.service'; import { BaseService } from 'src/services/base.service'; import { JobOf } from 'src/types'; @@ -32,9 +32,11 @@ const dummy = () => { type ExecuteOptions = { read: (type: T) => Promise<{ authUserId: string; data: WorkflowEventData }>; - write: (changes: WorkflowChanges) => Promise; + write: (auth: AuthDto, changes: WorkflowChanges) => Promise; }; +type AssetTrigger = { userId: string; assetId: string; trigger: WorkflowTrigger }; + export class WorkflowExecutionService extends BaseService { private jwtSecret!: string; @@ -62,7 +64,6 @@ export class WorkflowExecutionService extends BaseService { const albumAddAssets = this.wrap<[id: string, dto: BulkIdsDto]>((authDto, args) => albumService.addAssets(authDto, ...args), ); - const addAssetsToAlbums = this.wrap<[dto: AlbumsAddAssetsDto]>((authDto, args) => albumService.addAssetsToAlbums(authDto, ...args), ); @@ -247,20 +248,25 @@ export class WorkflowExecutionService extends BaseService { } @OnEvent({ name: 'AssetCreate' }) - async onAssetCreate({ asset }: ArgOf<'AssetCreate'>) { - const dto = { ownerId: asset.ownerId, trigger: WorkflowTrigger.AssetCreate }; - const items = await this.workflowRepository.search(dto); + onAssetCreate({ asset: { ownerId: userId, id: assetId } }: ArgOf<'AssetCreate'>) { + return this.onAssetTrigger({ userId, assetId, trigger: WorkflowTrigger.AssetCreate }); + } + + private async onAssetTrigger({ userId, assetId, trigger }: AssetTrigger) { + const items = await this.workflowRepository.search({ userId, trigger }); await this.jobRepository.queueAll( items.map((workflow) => ({ - name: JobName.WorkflowAssetCreate, - data: { workflowId: workflow.id, assetId: asset.id }, + name: JobName.WorkflowAssetTrigger, + data: { workflowId: workflow.id, assetId, trigger }, })), ); } - @OnJob({ name: JobName.WorkflowAssetCreate, queue: QueueName.Workflow }) - handleAssetCreate({ workflowId, assetId }: JobOf) { + @OnJob({ name: JobName.WorkflowAssetTrigger, queue: QueueName.Workflow }) + handleAssetTrigger({ workflowId, assetId }: JobOf) { return this.execute(workflowId, (type) => { + const assetService = BaseService.create(AssetService, this); + switch (type) { case WorkflowType.AssetV1: { return { @@ -271,19 +277,16 @@ export class WorkflowExecutionService extends BaseService { authUserId: asset.ownerId, }; }, - write: async (changes) => { - if (changes.asset) { - await this.assetRepository.update({ - id: assetId, - ..._.omitBy( - { - isFavorite: changes.asset?.isFavorite, - visibility: changes.asset?.visibility, - }, - _.isUndefined, - ), - }); + write: async (auth, changes) => { + const asset = changes.asset; + if (!asset) { + return; } + + await assetService.update(auth, assetId, { + isFavorite: asset.isFavorite, + visibility: asset.visibility, + }); }, } satisfies ExecuteOptions; } @@ -301,7 +304,19 @@ export class WorkflowExecutionService extends BaseService { } // TODO infer from steps - const type = 'AssetV1' as T; + let type: T | undefined; + for (const targetType of Object.values(WorkflowType)) { + const missing = workflow.steps.some((step) => !step.types.includes(targetType)); + if (!missing) { + type = targetType as unknown as T; + break; + } + } + + if (!type) { + throw new Error('Unable to infer workflow event type from steps'); + } + const handler = getHandler(type); if (!handler) { this.logger.error(`Misconfigured workflow ${workflowId}: no handler for type ${type}`); @@ -337,7 +352,19 @@ export class WorkflowExecutionService extends BaseService { payload, ); if (result?.changes) { - await write(result.changes); + await write( + { + user: { + id: readResult.authUserId, + }, + session: { + id: DummyValue.UUID, + // TODO move this to auth.elevated or similar + hasElevatedPermission: true, + }, + } as AuthDto, + result.changes, + ); ({ data } = await read(type)); } diff --git a/server/src/services/workflow.service.ts b/server/src/services/workflow.service.ts index 0a62a60887..382ae2fe06 100644 --- a/server/src/services/workflow.service.ts +++ b/server/src/services/workflow.service.ts @@ -23,7 +23,7 @@ export class WorkflowService extends BaseService { } async search(auth: AuthDto, dto: WorkflowSearchDto): Promise { - const workflows = await this.workflowRepository.search({ ...dto, ownerId: auth.user.id }); + const workflows = await this.workflowRepository.search({ ...dto, userId: auth.user.id }); return workflows.map((workflow) => mapWorkflow(workflow)); } diff --git a/server/src/types.ts b/server/src/types.ts index c7dc1f5e18..480bd93118 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -404,7 +404,7 @@ export type JobItem = | { name: JobName.Ocr; data: IEntityJob } // Workflow - | { name: JobName.WorkflowAssetCreate; data: { workflowId: string; assetId: string } } + | { name: JobName.WorkflowAssetTrigger; data: { workflowId: string; assetId: string } } // Editor | { name: JobName.AssetEditThumbnailGeneration; data: IEntityJob }; diff --git a/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts b/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts index 99f6c67d5c..fffddfc32a 100644 --- a/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts +++ b/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts @@ -137,7 +137,7 @@ describe('core plugin', () => { steps: [{ method: 'immich-plugin-core#assetArchive' }], }); - await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); + await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ visibility: AssetVisibility.Archive, @@ -154,7 +154,7 @@ describe('core plugin', () => { steps: [{ method: 'immich-plugin-core#assetArchive', config: { inverse: true } }], }); - await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); + await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ visibility: AssetVisibility.Timeline, @@ -173,7 +173,7 @@ describe('core plugin', () => { steps: [{ method: 'immich-plugin-core#assetLock' }], }); - await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); + await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ visibility: AssetVisibility.Locked, @@ -190,7 +190,7 @@ describe('core plugin', () => { steps: [{ method: 'immich-plugin-core#assetLock', config: { inverse: true } }], }); - await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); + await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ visibility: AssetVisibility.Timeline, @@ -209,7 +209,7 @@ describe('core plugin', () => { steps: [{ method: 'immich-plugin-core#assetFavorite' }], }); - await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); + await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ isFavorite: true }); }); @@ -224,7 +224,7 @@ describe('core plugin', () => { steps: [{ method: 'immich-plugin-core#assetFavorite', config: { inverse: true } }], }); - await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); + await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ isFavorite: false }); }); @@ -242,7 +242,7 @@ describe('core plugin', () => { steps: [{ method: 'immich-plugin-core#assetAddToAlbums', config: { albumIds: [album.id] } }], }); - await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); + await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); await expect(ctx.get(AlbumRepository).getAssetIds(album.id, [asset.id])).resolves.toContain(asset.id); }); @@ -261,7 +261,7 @@ describe('core plugin', () => { steps: [{ method: 'immich-plugin-core#assetAddToAlbums', config: { albumIds: [album1.id, album2.id] } }], }); - await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); + await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); await expect(ctx.get(AlbumRepository).getAssetIds(album1.id, [asset.id])).resolves.toContain(asset.id); await expect(ctx.get(AlbumRepository).getAssetIds(album2.id, [asset.id])).resolves.toContain(asset.id); @@ -279,7 +279,7 @@ describe('core plugin', () => { steps: [{ method: 'immich-plugin-core#assetAddToAlbums', config: { albumIds: [album.id] } }], }); - await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeTruthy(); + await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeTruthy(); await expect(ctx.get(AlbumRepository).getAssetIds(album.id, [asset.id])).resolves.not.toContain(asset.id); }); diff --git a/web/src/lib/components/SchemaConfiguration.svelte b/web/src/lib/components/SchemaConfiguration.svelte index e48f46a402..2cc0b68089 100644 --- a/web/src/lib/components/SchemaConfiguration.svelte +++ b/web/src/lib/components/SchemaConfiguration.svelte @@ -64,7 +64,7 @@ {/each} -{:else if schema.uiHint === 'albumId'} +{:else if schema.uiHint === 'AlbumId'} {:else if schema.enum && schema.array} diff --git a/web/src/lib/modals/PluginMethodPicker.svelte b/web/src/lib/modals/PluginMethodPicker.svelte index bbfa2ca83f..230f5a2257 100644 --- a/web/src/lib/modals/PluginMethodPicker.svelte +++ b/web/src/lib/modals/PluginMethodPicker.svelte @@ -24,7 +24,7 @@
{method.title} - {#if method.uiHints.includes('filter')} + {#if method.uiHints.includes('Filter')} {$t('plugin_method_filter_type')} diff --git a/web/src/lib/modals/WorkflowEditTrigger.svelte b/web/src/lib/modals/WorkflowEditTrigger.svelte deleted file mode 100644 index d72bff3c6d..0000000000 --- a/web/src/lib/modals/WorkflowEditTrigger.svelte +++ /dev/null @@ -1,37 +0,0 @@ - - - -
- {#each pluginManager.triggers as item (item.trigger)} - (selected = item)}> -
- {getTriggerName($t, item.trigger)} - {getTriggerDescription($t, item.trigger)} -
-
- {/each} -
-
diff --git a/web/src/lib/modals/WorkflowTemplatePicker.svelte b/web/src/lib/modals/WorkflowTemplatePickerModal.svelte similarity index 87% rename from web/src/lib/modals/WorkflowTemplatePicker.svelte rename to web/src/lib/modals/WorkflowTemplatePickerModal.svelte index 05306671fc..0f3aad0ee6 100644 --- a/web/src/lib/modals/WorkflowTemplatePicker.svelte +++ b/web/src/lib/modals/WorkflowTemplatePickerModal.svelte @@ -2,7 +2,7 @@ import { pluginManager } from '$lib/managers/plugin-manager.svelte'; import { handleCreateWorkflow } from '$lib/services/workflow.service'; import { type PluginTemplateResponseDto } from '@immich/sdk'; - import { FormModal, Icon, ListButton, Text } from '@immich/ui'; + import { Badge, FormModal, Icon, ListButton, Text } from '@immich/ui'; import { mdiFlashOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -59,6 +59,11 @@ {template.title} {template.description}
+ {#if template.uiHints.includes('SmartAlbum')} +
+ {$t('smart_album')} +
+ {/if} {/each} diff --git a/web/src/lib/services/workflow.service.ts b/web/src/lib/services/workflow.service.ts index 79358b9ee4..c81b1b2706 100644 --- a/web/src/lib/services/workflow.service.ts +++ b/web/src/lib/services/workflow.service.ts @@ -26,7 +26,7 @@ import type { MessageFormatter } from 'svelte-i18n'; import { goto } from '$app/navigation'; import { eventManager } from '$lib/managers/event-manager.svelte'; import WorkflowDuplicateModal from '$lib/modals/WorkflowDuplicateModal.svelte'; -import WorkflowTemplatePicker from '$lib/modals/WorkflowTemplatePicker.svelte'; +import WorkflowTemplatePickerModal from '$lib/modals/WorkflowTemplatePickerModal.svelte'; import { Route } from '$lib/route'; import { copyToClipboard, downloadJson } from '$lib/utils'; import { handleError } from '$lib/utils/handle-error'; @@ -50,7 +50,7 @@ export const getWorkflowsActions = ($t: MessageFormatter) => { const UseTemplate: ActionItem = { title: $t('browse_templates'), icon: mdiFileDocumentMultipleOutline, - onAction: () => modalManager.show(WorkflowTemplatePicker, {}), + onAction: () => modalManager.show(WorkflowTemplatePickerModal, {}), }; return { Create, UseTemplate }; diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index b0e1466da1..ef69a6b08e 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -104,7 +104,7 @@ export type JSONSchemaProperty = { array?: boolean; properties?: Record; required?: string[]; - uiHint?: 'albumId' | 'assetId' | 'personId'; + uiHint?: 'AlbumId' | 'AssetId' | 'PersonId'; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/web/src/routes/(user)/workflows/[workflowId]/WorkflowStepCard.svelte b/web/src/routes/(user)/workflows/[workflowId]/WorkflowStepCard.svelte index 661b90bd73..d6b20d3cc8 100644 --- a/web/src/routes/(user)/workflows/[workflowId]/WorkflowStepCard.svelte +++ b/web/src/routes/(user)/workflows/[workflowId]/WorkflowStepCard.svelte @@ -26,7 +26,7 @@ let { step, index, onEdit, onDelete, onInsertBefore, onDrop }: Props = $props(); const method = $derived(pluginManager.getMethod(step.method)); - const isFilter = $derived(method?.uiHints?.includes('filter') ?? false); + const isFilter = $derived(method?.uiHints?.includes('Filter') ?? false); const configEntries = $derived( Object.entries(step.config ?? {}).filter(([, value]) => value !== null && value !== undefined && value !== ''), ); @@ -75,7 +75,7 @@ target: document.body, props: { description: method?.description, - isFilter: method?.uiHints?.includes('filter') ?? false, + isFilter: method?.uiHints?.includes('Filter') ?? false, label: step ? pluginManager.getMethodLabel(step.method) : '', stepNumber: index + 1, }, diff --git a/web/src/routes/(user)/workflows/[workflowId]/WorkflowSummary.svelte b/web/src/routes/(user)/workflows/[workflowId]/WorkflowSummary.svelte index 8b44e45219..3d6e29e896 100644 --- a/web/src/routes/(user)/workflows/[workflowId]/WorkflowSummary.svelte +++ b/web/src/routes/(user)/workflows/[workflowId]/WorkflowSummary.svelte @@ -67,7 +67,7 @@ for (const [i, step] of workflow.steps.entries()) { const method = pluginManager.getMethod(step.method); - const isFilter = method?.uiHints?.includes('filter') ?? false; + const isFilter = method?.uiHints?.includes('Filter') ?? false; const type = isFilter ? $t('filter') : $t('action'); const label = pluginManager.getMethodLabel(step.method); lines.push(` [${i + 1}] ${type.toUpperCase()} ยท ${label}`); From 55947cb2279e734adf60df2d8a70c9993b76c4a0 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Fri, 29 May 2026 03:12:59 +0530 Subject: [PATCH 03/50] refactor: drop metadata scope (#28668) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../lib/domain/models/config/app_config.dart | 9 ++----- mobile/lib/domain/models/metadata_key.dart | 25 +++++------------- .../repositories/metadata.repository.dart | 26 ++++++++++++++----- mobile/lib/services/auth.service.dart | 8 +++++- 4 files changed, 35 insertions(+), 33 deletions(-) diff --git a/mobile/lib/domain/models/config/app_config.dart b/mobile/lib/domain/models/config/app_config.dart index dc1bf96679..d881f67cf8 100644 --- a/mobile/lib/domain/models/config/app_config.dart +++ b/mobile/lib/domain/models/config/app_config.dart @@ -143,13 +143,8 @@ class AppConfig { }) as T; - factory AppConfig.fromEntries(Map, Object> entries) { - var config = const AppConfig(); - for (final MapEntry(key: key, value: value) in entries.entries) { - config = config.write(key, value); - } - return config; - } + factory AppConfig.fromEntries(Map, Object> overrides) => + overrides.entries.fold(const AppConfig(), (config, entry) => config.write(entry.key, entry.value)); AppConfig write(MetadataKey key, T value) { return switch (key) { diff --git a/mobile/lib/domain/models/metadata_key.dart b/mobile/lib/domain/models/metadata_key.dart index f68af0be84..c8822c28e9 100644 --- a/mobile/lib/domain/models/metadata_key.dart +++ b/mobile/lib/domain/models/metadata_key.dart @@ -7,13 +7,6 @@ import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; -enum MetadataScope { - user, // keys with this scope are deleted on logout - system; - - const MetadataScope(); -} - enum MetadataKey { // Theme themePrimaryColor(codec: _EnumCodec(ImmichColorPreset.values)), @@ -32,14 +25,11 @@ enum MetadataKey { viewerTapToNavigate(), // Network - networkAutoEndpointSwitching(scope: .system), - networkPreferredWifiName(scope: .system), - networkLocalEndpoint(scope: .system), - networkExternalEndpointList>(scope: .system, codec: _ListCodec(_PrimitiveCodec.string)), - networkCustomHeaders>( - scope: .system, - codec: _MapCodec(_PrimitiveCodec.string, _PrimitiveCodec.string), - ), + networkAutoEndpointSwitching(), + networkPreferredWifiName(), + networkLocalEndpoint(), + networkExternalEndpointList>(codec: _ListCodec(_PrimitiveCodec.string)), + networkCustomHeaders>(codec: _MapCodec(_PrimitiveCodec.string, _PrimitiveCodec.string)), // Album albumSortMode(codec: _EnumCodec(AlbumSortMode.values)), @@ -60,7 +50,7 @@ enum MetadataKey { timelineStorageIndicator(), // Log - logLevel(scope: .system, codec: _EnumCodec(LogLevel.values)), + logLevel(codec: _EnumCodec(LogLevel.values)), // Map mapShowFavoriteOnly(), @@ -83,10 +73,9 @@ enum MetadataKey { slideshowLook(codec: _EnumCodec(SlideshowLook.values)), slideshowDirection(codec: _EnumCodec(SlideshowDirection.values)); - final MetadataScope scope; final _MetadataCodec? _codecOverride; - const MetadataKey({this.scope = .user, _MetadataCodec? codec}) : _codecOverride = codec; + const MetadataKey({_MetadataCodec? codec}) : _codecOverride = codec; _MetadataCodec get _codec => _codecOverride ?? _MetadataCodec.forType(T); diff --git a/mobile/lib/infrastructure/repositories/metadata.repository.dart b/mobile/lib/infrastructure/repositories/metadata.repository.dart index d43f67fe87..1300376464 100644 --- a/mobile/lib/infrastructure/repositories/metadata.repository.dart +++ b/mobile/lib/infrastructure/repositories/metadata.repository.dart @@ -34,21 +34,33 @@ class MetadataRepository extends DriftDatabaseRepository { Future refresh() async => _applyOverrides(await _db.select(_db.metadataEntity).get()); + Future clear(Iterable keys) async { + if (keys.isEmpty) { + return; + } + + final names = keys.map((key) => key.name).toList(); + await (_db.delete(_db.metadataEntity)..where((row) => row.key.isIn(names))).go(); + + for (final key in keys) { + _appConfig = _appConfig.write(key, defaultConfig.read(key)); + } + } + Future write(MetadataKey key, U value) async { if (value == _appConfig.read(key)) { return; } if (value == defaultConfig.read(key)) { - await (_db.delete(_db.metadataEntity)..where((t) => t.key.equals(key.name))).go(); - } else { - await _db - .into(_db.metadataEntity) - .insertOnConflictUpdate( - MetadataEntityCompanion.insert(key: key.name, value: key.encode(value), updatedAt: Value(DateTime.now())), - ); + return clear([key]); } + await _db + .into(_db.metadataEntity) + .insertOnConflictUpdate( + MetadataEntityCompanion.insert(key: key.name, value: key.encode(value), updatedAt: Value(DateTime.now())), + ); _appConfig = _appConfig.write(key, value); } diff --git a/mobile/lib/services/auth.service.dart b/mobile/lib/services/auth.service.dart index 7d470ecd7a..fff72a710e 100644 --- a/mobile/lib/services/auth.service.dart +++ b/mobile/lib/services/auth.service.dart @@ -110,7 +110,7 @@ class AuthService { /// - Authentication repository data /// - Current user information /// - Access token - /// - Asset ETag + /// - Server-specific endpoint configuration /// /// All deletions are executed in parallel using [Future.wait]. Future clearLocalData() async { @@ -120,6 +120,12 @@ class AuthService { _authRepository.clearLocalData(), Store.delete(StoreKey.currentUser), Store.delete(StoreKey.accessToken), + MetadataRepository.instance.clear(const [ + .networkAutoEndpointSwitching, + .networkPreferredWifiName, + .networkLocalEndpoint, + .networkExternalEndpointList, + ]), ]); } From 0d6cce4a5b6fca50d011a388f9c720a226c4fd5c Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Fri, 29 May 2026 03:14:11 +0530 Subject: [PATCH 04/50] fix: api repositories using stale endpoint (#28667) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- mobile/lib/services/api.service.dart | 4 ++-- mobile/openapi/lib/api_client.dart | 2 +- open-api/patch/api_client.dart.patch | 9 +++++++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/mobile/lib/services/api.service.dart b/mobile/lib/services/api.service.dart index 99f618b832..b6a2cb1e4b 100644 --- a/mobile/lib/services/api.service.dart +++ b/mobile/lib/services/api.service.dart @@ -13,7 +13,7 @@ import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; class ApiService { - late ApiClient _apiClient; + final ApiClient _apiClient = ApiClient(basePath: ''); late UsersApi usersApi; late AuthenticationApi authenticationApi; @@ -54,7 +54,7 @@ class ApiService { } setEndpoint(String endpoint) { - _apiClient = ApiClient(basePath: endpoint); + _apiClient.basePath = endpoint; _apiClient.client = NetworkRepository.client; usersApi = UsersApi(_apiClient); authenticationApi = AuthenticationApi(_apiClient); diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index a3c2369c1d..fd0d73aebb 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -13,7 +13,7 @@ part of openapi.api; class ApiClient { ApiClient({this.basePath = '/api', this.authentication,}); - final String basePath; + String basePath; final Authentication? authentication; var _client = Client(); diff --git a/open-api/patch/api_client.dart.patch b/open-api/patch/api_client.dart.patch index 8996e79413..a813c1c033 100644 --- a/open-api/patch/api_client.dart.patch +++ b/open-api/patch/api_client.dart.patch @@ -1,3 +1,12 @@ +@@ -13,7 +13,7 @@ + class ApiClient { + ApiClient({this.basePath = '/api', this.authentication,}); + +- final String basePath; ++ String basePath; + final Authentication? authentication; + + var _client = Client(); @@ -143,19 +143,19 @@ ); } From 96923f61151f8b0f6c3f9e44202c5dab71f7667d Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 28 May 2026 18:04:15 -0400 Subject: [PATCH 05/50] refactor: plugin sdk types (#28674) --- mise.toml | 6 +++--- packages/plugin-sdk/package.json | 1 + packages/plugin-sdk/src/host-functions.ts | 9 ++++++++- pnpm-lock.yaml | 3 +++ server/Dockerfile | 6 ++++-- server/mise.toml | 1 + server/src/services/workflow-execution.service.ts | 1 - 7 files changed, 20 insertions(+), 7 deletions(-) diff --git a/mise.toml b/mise.toml index 5659aacb6c..8cff3993f7 100644 --- a/mise.toml +++ b/mise.toml @@ -54,8 +54,8 @@ lockfile = true [tasks.plugins] run = [ - "pnpm --filter @immich/plugin-sdk --filter @immich/plugin-core install --frozen-lockfile", - "pnpm --filter @immich/plugin-sdk --filter @immich/plugin-core build", + "pnpm --filter @immich/sdk --filter @immich/plugin-sdk --filter @immich/plugin-core install --frozen-lockfile", + "pnpm --filter @immich/sdk --filter @immich/plugin-sdk --filter @immich/plugin-core build", ] [tasks.open-api-typescript] @@ -108,7 +108,7 @@ depends = "//:plugins" dir = "docker" interactive = true env = { COMPOSE_BAKE = true } -run = "docker compose -f ./docker-compose.prod.yml up --remove-orphans" +run = "docker compose -f ./docker-compose.prod.yml up --build --remove-orphans" depends_post = "//:prod-down" [tasks.prod-scale] diff --git a/packages/plugin-sdk/package.json b/packages/plugin-sdk/package.json index f505a6cc0e..1121e42f59 100644 --- a/packages/plugin-sdk/package.json +++ b/packages/plugin-sdk/package.json @@ -27,6 +27,7 @@ "packageManager": "pnpm@10.33.4", "devDependencies": { "@extism/js-pdk": "^1.1.1", + "@immich/sdk": "workspace:*", "@types/node": "^24.12.4", "esbuild": "^0.28.0", "tsc-alias": "^1.8.16", diff --git a/packages/plugin-sdk/src/host-functions.ts b/packages/plugin-sdk/src/host-functions.ts index d0f8a3ef17..ab76218551 100644 --- a/packages/plugin-sdk/src/host-functions.ts +++ b/packages/plugin-sdk/src/host-functions.ts @@ -1,3 +1,6 @@ +import { type BulkIdResponseDto, type BulkIdsDto } from '@immich/sdk'; + +// keep in sync with plugin-core/src/index.d.ts'; declare module 'extism:host' { interface user { albumAddAssets(ptr: PTR): I64; @@ -45,7 +48,11 @@ type AlbumsToAssets = { export const hostFunctions = (authToken: string) => ({ albumAddAssets: (albumId: string, assetIds: string[]) => - call('albumAddAssets', authToken, [albumId, { ids: assetIds }]), + call<[string, BulkIdsDto], BulkIdResponseDto[]>( + 'albumAddAssets', + authToken, + [albumId, { ids: assetIds }], + ), addAssetsToAlbums: ({ assetIds, albumIds }: AlbumsToAssets) => call('addAssetsToAlbums', authToken, [{ albumIds, assetIds }]), }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64cf0d3d85..4eef7170f6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -332,6 +332,9 @@ importers: '@extism/js-pdk': specifier: ^1.1.1 version: 1.1.1 + '@immich/sdk': + specifier: workspace:* + version: link:../sdk '@types/node': specifier: ^24.12.4 version: 24.12.4 diff --git a/server/Dockerfile b/server/Dockerfile index 6078a97504..3d074ba17f 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -13,14 +13,15 @@ FROM builder AS server WORKDIR /usr/src/app COPY ./server ./server/ +COPY ./packages/sdk ./packages/sdk/ COPY ./packages/plugin-sdk ./packages/plugin-sdk/ RUN --mount=type=cache,id=pnpm-server,target=/buildcache/pnpm-store \ --mount=type=bind,source=package.json,target=package.json \ --mount=type=bind,source=.pnpmfile.cjs,target=.pnpmfile.cjs \ --mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \ --mount=type=bind,source=pnpm-workspace.yaml,target=pnpm-workspace.yaml \ - SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter immich --filter @immich/plugin-sdk --frozen-lockfile build && \ - SHARP_FORCE_GLOBAL_LIBVIPS=true pnpm --filter immich --frozen-lockfile --prod --no-optional deploy /output/server-pruned + SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter @immich/sdk --filter @immich/plugin-sdk --filter immich --frozen-lockfile build && \ + SHARP_FORCE_GLOBAL_LIBVIPS=true pnpm --filter immich --frozen-lockfile --prod --no-optional deploy /output/server-pruned FROM builder AS web @@ -66,6 +67,7 @@ ENV MISE_DISABLE_TOOLS=flutter RUN --mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \ mise install +COPY ./packages/sdk ./packages/sdk/ COPY ./packages/plugin-core ./packages/plugin-core/ COPY ./packages/plugin-sdk ./packages/plugin-sdk/ diff --git a/server/mise.toml b/server/mise.toml index f7e4f92a26..1462af0c52 100644 --- a/server/mise.toml +++ b/server/mise.toml @@ -68,6 +68,7 @@ run = [ [tasks.ci-medium] run = [ { task = ":install" }, + { task = "//:plugins" }, { task = "//packages/plugin-core:build" }, { task = ":test-medium --run" }, ] diff --git a/server/src/services/workflow-execution.service.ts b/server/src/services/workflow-execution.service.ts index 0f600e117b..0ada9e7e8f 100644 --- a/server/src/services/workflow-execution.service.ts +++ b/server/src/services/workflow-execution.service.ts @@ -359,7 +359,6 @@ export class WorkflowExecutionService extends BaseService { }, session: { id: DummyValue.UUID, - // TODO move this to auth.elevated or similar hasElevatedPermission: true, }, } as AuthDto, From b189fc571cf78bb91b6529dbed67ceb46c7e59d9 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Thu, 28 May 2026 18:04:25 -0400 Subject: [PATCH 06/50] fix: make ts a peer dependency for swagger (#28677) make ts a peer dependency --- pnpm-lock.yaml | 14 ++++++++------ pnpm-workspace.yaml | 3 +++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4eef7170f6..600c0b3cfe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ overrides: sharp: ^0.34.5 webpackbar: ^7.0.0 -packageExtensionsChecksum: sha256-3l4AQg4iuprBDup+q+2JaPvbPg/7XodWCE0ZteH+s54= +packageExtensionsChecksum: sha256-W6pFzyf+6QXnV91iA6oob0OGVkergPXDN1afLgoF53k= pnpmfileChecksum: sha256-un98do36L0wZyqsjcLozQ3YUadCAn2yz5bXcBbOuyDA= @@ -392,7 +392,7 @@ importers: version: 6.1.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21) '@nestjs/swagger': specifier: ^11.4.2 - version: 11.4.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2) + version: 11.4.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2)(typescript@6.0.3) '@nestjs/websockets': specifier: ^11.0.4 version: 11.1.21(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(@nestjs/platform-socket.io@11.1.21)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -539,7 +539,7 @@ importers: version: 8.0.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21) nestjs-zod: specifier: ^5.3.0 - version: 5.4.0(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.4.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6) + version: 5.4.0(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.4.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2)(typescript@6.0.3))(rxjs@7.8.2)(zod@4.3.6) nodemailer: specifier: ^8.0.0 version: 8.0.7 @@ -3798,6 +3798,7 @@ packages: class-transformer: '*' class-validator: '*' reflect-metadata: ^0.1.12 || ^0.2.0 + typescript: '*' peerDependenciesMeta: '@fastify/static': optional: true @@ -16573,7 +16574,7 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/swagger@11.4.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2)': + '@nestjs/swagger@11.4.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2)(typescript@6.0.3)': dependencies: '@microsoft/tsdoc': 0.16.0 '@nestjs/common': 11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -16584,6 +16585,7 @@ snapshots: path-to-regexp: 8.4.2 reflect-metadata: 0.2.2 swagger-ui-dist: 5.32.6 + typescript: 6.0.3 '@nestjs/testing@11.1.21(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(@nestjs/platform-express@11.1.21)': dependencies: @@ -23232,14 +23234,14 @@ snapshots: '@opentelemetry/host-metrics': 0.38.3(@opentelemetry/api@1.9.1) tslib: 2.8.1 - nestjs-zod@5.4.0(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.4.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6): + nestjs-zod@5.4.0(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.4.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2)(typescript@6.0.3))(rxjs@7.8.2)(zod@4.3.6): dependencies: '@nestjs/common': 11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2) deepmerge: 4.3.1 rxjs: 7.8.2 zod: 4.3.6 optionalDependencies: - '@nestjs/swagger': 11.4.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2) + '@nestjs/swagger': 11.4.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2)(typescript@6.0.3) next-tick@1.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f53cb0d406..a848deca82 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -60,6 +60,9 @@ packageExtensions: dependencies: node-addon-api: '*' node-gyp: '*' + '@nestjs/swagger': + peerDependencies: + typescript: '*' dedupePeerDependents: false preferWorkspacePackages: true injectWorkspacePackages: true From a838167f110deaa6c7c7c2361e01287f05219bfa Mon Sep 17 00:00:00 2001 From: pneuly Date: Fri, 29 May 2026 11:54:04 +0900 Subject: [PATCH 07/50] fix(ml): pass model_root_dir to OcrOptions for RapidOCR compatibility (#28610) * fix(ml): pass model_root_dir to OcrOptions for RapidOCR compatibility Fix a TypeError (Path(None)) when the OCR model is invoked, caused by an upstream change in RapidOCR v3.8.1 (RapidAI/RapidOCR@8ea9626). RapidOCR now internally calls `Path(cfg.get("model_root_dir"))`. Since `model_root_dir` was missing from `OcrOptions`, it evaluated to `None` and triggered a `TypeError: argument should be a str or an os.PathLike`. This fix adds the missing `model_root_dir` argument to prevent the error. Ref: #28331 * fix(ml-test): update OCR tests for RapidOCR schema change * chore(ml-test): remove unused `cache_dir` parameter from `TextRecognizer` * Revert "chore(ml-test): remove unused `cache_dir` parameter from `TextRecognizer`" This reverts commit 007ad7b3f22b0c93cfc237509b460eb6d5c51075. * fix(ml): use self.cache_dir for model_root_dir in OcrOptions --- .../immich_ml/models/ocr/recognition.py | 1 + machine-learning/test_main.py | 21 ++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/machine-learning/immich_ml/models/ocr/recognition.py b/machine-learning/immich_ml/models/ocr/recognition.py index 6408e4818f..94f40c9285 100644 --- a/machine-learning/immich_ml/models/ocr/recognition.py +++ b/machine-learning/immich_ml/models/ocr/recognition.py @@ -64,6 +64,7 @@ class TextRecognizer(InferenceModel): rec_batch_num=max_batch_size if max_batch_size else 6, rec_img_shape=(3, 48, 320), lang_type=self.language, + model_root_dir=self.cache_dir, ) ) return session diff --git a/machine-learning/test_main.py b/machine-learning/test_main.py index b281c0d417..5145be0045 100644 --- a/machine-learning/test_main.py +++ b/machine-learning/test_main.py @@ -1028,7 +1028,12 @@ class TestOcr: text_recognizer.load() rapid_recognizer.assert_called_once_with( - OcrOptions(session=ort_session.return_value, rec_batch_num=6, rec_img_shape=(3, 48, 320)) + OcrOptions( + session=ort_session.return_value, + rec_batch_num=6, + rec_img_shape=(3, 48, 320), + model_root_dir=text_recognizer.cache_dir, + ) ) def test_set_custom_max_batch_size(self, ort_session: mock.Mock, path: mock.Mock, mocker: MockerFixture) -> None: @@ -1041,7 +1046,12 @@ class TestOcr: text_recognizer.load() rapid_recognizer.assert_called_once_with( - OcrOptions(session=ort_session.return_value, rec_batch_num=4, rec_img_shape=(3, 48, 320)) + OcrOptions( + session=ort_session.return_value, + rec_batch_num=4, + rec_img_shape=(3, 48, 320), + model_root_dir=text_recognizer.cache_dir, + ) ) def test_ignore_other_custom_max_batch_size( @@ -1056,7 +1066,12 @@ class TestOcr: text_recognizer.load() rapid_recognizer.assert_called_once_with( - OcrOptions(session=ort_session.return_value, rec_batch_num=6, rec_img_shape=(3, 48, 320)) + OcrOptions( + session=ort_session.return_value, + rec_batch_num=6, + rec_img_shape=(3, 48, 320), + model_root_dir=text_recognizer.cache_dir, + ) ) From 58586483dc6bd34c80be112ef1f6a6ace4fa8b4b Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 29 May 2026 10:37:37 -0500 Subject: [PATCH 08/50] feat: render album's name in workflow step card (#28680) * feat: render album name in step card body * clean up * i18n --- .../[workflowId]/WorkflowStepCard.svelte | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/web/src/routes/(user)/workflows/[workflowId]/WorkflowStepCard.svelte b/web/src/routes/(user)/workflows/[workflowId]/WorkflowStepCard.svelte index d6b20d3cc8..6110572e5b 100644 --- a/web/src/routes/(user)/workflows/[workflowId]/WorkflowStepCard.svelte +++ b/web/src/routes/(user)/workflows/[workflowId]/WorkflowStepCard.svelte @@ -1,5 +1,25 @@ + + - - diff --git a/mobile/packages/ui/showcase/web/manifest.json b/mobile/packages/ui/showcase/web/manifest.json deleted file mode 100644 index 25b44bd1ae..0000000000 --- a/mobile/packages/ui/showcase/web/manifest.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "@immich/ui Showcase", - "short_name": "@immich/ui", - "start_url": ".", - "display": "standalone", - "background_color": "#FCFCFD", - "theme_color": "#4250AF", - "description": "Immich UI component library showcase and documentation", - "orientation": "landscape", - "prefer_related_applications": false, - "icons": [ - { - "src": "icons/Icon-maskable-192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "any" - }, - { - "src": "icons/Icon-maskable-192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "icons/Icon-maskable-512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "any" - }, - { - "src": "icons/Icon-maskable-512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - } - ] -} From 14aff51da9d57164fb46790eaa878254bf45f2a5 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Sat, 30 May 2026 20:57:55 +0530 Subject: [PATCH 11/50] refactor: rename metadata to settings (#28691) rename metadata to settings Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../drift_schemas/main/drift_schema_v27.json | 3368 ++++++ .../lib/domain/models/config/app_config.dart | 8 +- .../{metadata_key.dart => settings_key.dart} | 36 +- .../services/background_worker.service.dart | 6 +- mobile/lib/domain/services/log.service.dart | 22 +- .../lib/domain/services/timeline.service.dart | 8 +- ...adata.entity.dart => settings.entity.dart} | 6 +- ....drift.dart => settings.entity.drift.dart} | 148 +- .../repositories/db.repository.dart | 9 +- .../repositories/db.repository.drift.dart | 10 +- .../repositories/db.repository.steps.dart | 552 + ...pository.dart => settings.repository.dart} | 36 +- mobile/lib/main.dart | 2 +- .../drift_backup_album_selection.page.dart | 6 +- .../backup/drift_backup_options.page.dart | 8 +- .../pages/common/headers_settings.page.dart | 7 +- .../lib/pages/common/splash_screen.page.dart | 6 +- .../pages/drift_slideshow.page.dart | 2 +- .../widgets/album/album_selector.widget.dart | 13 +- .../asset_viewer/asset_page.widget.dart | 4 +- .../asset_viewer/video_viewer.widget.dart | 8 +- .../viewer_kebab_menu.widget.dart | 3 +- .../backup/backup_toggle_button.widget.dart | 7 +- .../widgets/images/image_provider.dart | 4 +- .../widgets/images/local_image_provider.dart | 4 +- .../widgets/images/remote_image_provider.dart | 4 +- .../widgets/images/thumbnail_tile.widget.dart | 2 +- .../presentation/widgets/map/map.state.dart | 11 +- .../widgets/timeline/timeline.state.dart | 2 +- .../widgets/timeline/timeline.widget.dart | 5 +- .../providers/app_life_cycle.provider.dart | 6 +- mobile/lib/providers/auth.provider.dart | 13 +- mobile/lib/providers/cleanup.provider.dart | 24 +- ...a.provider.dart => settings.provider.dart} | 6 +- .../infrastructure/timeline.provider.dart | 4 +- .../lib/providers/map/map_state.provider.dart | 13 +- mobile/lib/providers/theme.provider.dart | 2 +- mobile/lib/providers/websocket.provider.dart | 6 +- mobile/lib/repositories/auth.repository.dart | 2 +- mobile/lib/services/api.service.dart | 6 +- mobile/lib/services/auth.service.dart | 8 +- .../services/background_upload.service.dart | 4 +- .../services/foreground_upload.service.dart | 4 +- mobile/lib/utils/bootstrap.dart | 6 +- mobile/lib/utils/migration.dart | 98 +- .../widgets/common/immich_sliver_app_bar.dart | 2 +- .../lib/widgets/forms/login/login_form.dart | 4 +- .../widgets/settings/advanced_settings.dart | 4 +- .../asset_list_group_settings.dart | 5 +- .../asset_list_layout_settings.dart | 5 +- .../asset_list_settings.dart | 6 +- .../image_viewer_quality_setting.dart | 4 +- .../image_viewer_tap_to_navigate_setting.dart | 4 +- .../slideshow_settings.dart | 12 +- .../video_viewer_settings.dart | 8 +- .../drift_backup_settings.dart | 20 +- .../external_network_preference.dart | 6 +- .../networking_settings.dart | 4 +- .../primary_color_setting.dart | 8 +- .../preference_settings/theme_setting.dart | 8 +- .../domain/services/log_service_test.dart | 24 +- mobile/test/drift/main/generated/schema.dart | 4 + .../test/drift/main/generated/schema_v27.dart | 9384 +++++++++++++++++ .../test/infrastructure/repository.mock.dart | 4 +- ...est.dart => settings_repository_test.dart} | 40 +- .../background_upload.service_test.dart | 4 +- ...est.dart => settings_repository_test.dart} | 6 +- 67 files changed, 13687 insertions(+), 388 deletions(-) create mode 100644 mobile/drift_schemas/main/drift_schema_v27.json rename mobile/lib/domain/models/{metadata_key.dart => settings_key.dart} (85%) rename mobile/lib/infrastructure/entities/{metadata.entity.dart => settings.entity.dart} (72%) rename mobile/lib/infrastructure/entities/{metadata.entity.drift.dart => settings.entity.drift.dart} (74%) rename mobile/lib/infrastructure/repositories/{metadata.repository.dart => settings.repository.dart} (61%) rename mobile/lib/providers/infrastructure/{metadata.provider.dart => settings.provider.dart} (59%) create mode 100644 mobile/test/drift/main/generated/schema_v27.dart rename mobile/test/medium/repositories/{metadata_repository_test.dart => settings_repository_test.dart} (74%) rename mobile/test/unit/repositories/{metadata_repository_test.dart => settings_repository_test.dart} (76%) diff --git a/mobile/drift_schemas/main/drift_schema_v27.json b/mobile/drift_schemas/main/drift_schema_v27.json new file mode 100644 index 0000000000..4df6ef8389 --- /dev/null +++ b/mobile/drift_schemas/main/drift_schema_v27.json @@ -0,0 +1,3368 @@ +{ + "_meta": { + "description": "This file contains a serialized version of schema entities for drift.", + "version": "1.3.0" + }, + "options": { + "store_date_time_values_as_text": true + }, + "entities": [ + { + "id": 0, + "references": [], + "type": "table", + "data": { + "name": "user_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "email", + "getter_name": "email", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "has_profile_image", + "getter_name": "hasProfileImage", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"has_profile_image\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"has_profile_image\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "profile_changed_at", + "getter_name": "profileChangedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "avatar_color", + "getter_name": "avatarColor", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AvatarColor.values)", + "dart_type_name": "AvatarColor" + } + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 1, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "remote_asset_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AssetType.values)", + "dart_type_name": "AssetType" + } + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "width", + "getter_name": "width", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "height", + "getter_name": "height", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "duration_ms", + "getter_name": "durationMs", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "checksum", + "getter_name": "checksum", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_favorite", + "getter_name": "isFavorite", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_favorite\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_favorite\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "owner_id", + "getter_name": "ownerId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "local_date_time", + "getter_name": "localDateTime", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "thumb_hash", + "getter_name": "thumbHash", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "deleted_at", + "getter_name": "deletedAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "uploaded_at", + "getter_name": "uploadedAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "live_photo_video_id", + "getter_name": "livePhotoVideoId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "visibility", + "getter_name": "visibility", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AssetVisibility.values)", + "dart_type_name": "AssetVisibility" + } + }, + { + "name": "stack_id", + "getter_name": "stackId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "library_id", + "getter_name": "libraryId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_edited", + "getter_name": "isEdited", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_edited\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_edited\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 2, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "stack_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "owner_id", + "getter_name": "ownerId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "primary_asset_id", + "getter_name": "primaryAssetId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 3, + "references": [], + "type": "table", + "data": { + "name": "local_asset_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AssetType.values)", + "dart_type_name": "AssetType" + } + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "width", + "getter_name": "width", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "height", + "getter_name": "height", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "duration_ms", + "getter_name": "durationMs", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "checksum", + "getter_name": "checksum", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_favorite", + "getter_name": "isFavorite", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_favorite\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_favorite\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "orientation", + "getter_name": "orientation", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "i_cloud_id", + "getter_name": "iCloudId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "adjustment_time", + "getter_name": "adjustmentTime", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "latitude", + "getter_name": "latitude", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "longitude", + "getter_name": "longitude", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "playback_style", + "getter_name": "playbackStyle", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AssetPlaybackStyle.values)", + "dart_type_name": "AssetPlaybackStyle" + } + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 4, + "references": [ + 1 + ], + "type": "table", + "data": { + "name": "remote_album_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "description", + "getter_name": "description", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('\\'\\'')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "thumbnail_asset_id", + "getter_name": "thumbnailAssetId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE SET NULL", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE SET NULL" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "setNull" + } + } + ] + }, + { + "name": "is_activity_enabled", + "getter_name": "isActivityEnabled", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_activity_enabled\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_activity_enabled\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "order", + "getter_name": "order", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AlbumAssetOrder.values)", + "dart_type_name": "AlbumAssetOrder" + } + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 5, + "references": [ + 4 + ], + "type": "table", + "data": { + "name": "local_album_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "backup_selection", + "getter_name": "backupSelection", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(BackupSelection.values)", + "dart_type_name": "BackupSelection" + } + }, + { + "name": "is_ios_shared_album", + "getter_name": "isIosSharedAlbum", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_ios_shared_album\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_ios_shared_album\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "linked_remote_album_id", + "getter_name": "linkedRemoteAlbumId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_album_entity (id) ON DELETE SET NULL", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_album_entity (id) ON DELETE SET NULL" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_album_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "setNull" + } + } + ] + }, + { + "name": "marker", + "getter_name": "marker_", + "moor_type": "bool", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "CHECK (\"marker\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"marker\" IN (0, 1))" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 6, + "references": [ + 3, + 5 + ], + "type": "table", + "data": { + "name": "local_album_asset_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "asset_id", + "getter_name": "assetId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES local_asset_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES local_asset_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "local_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "album_id", + "getter_name": "albumId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES local_album_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES local_album_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "local_album_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "marker", + "getter_name": "marker_", + "moor_type": "bool", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "CHECK (\"marker\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"marker\" IN (0, 1))" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "asset_id", + "album_id" + ] + } + }, + { + "id": 7, + "references": [ + 6 + ], + "type": "index", + "data": { + "on": 6, + "name": "idx_local_album_asset_album_asset", + "sql": "CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 8, + "references": [ + 3 + ], + "type": "index", + "data": { + "on": 3, + "name": "idx_local_asset_checksum", + "sql": "CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)", + "unique": false, + "columns": [] + } + }, + { + "id": 9, + "references": [ + 3 + ], + "type": "index", + "data": { + "on": 3, + "name": "idx_local_asset_cloud_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 10, + "references": [ + 2 + ], + "type": "index", + "data": { + "on": 2, + "name": "idx_stack_primary_asset_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 11, + "references": [ + 1 + ], + "type": "index", + "data": { + "on": 1, + "name": "UQ_remote_assets_owner_checksum", + "sql": "CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum\nON remote_asset_entity (owner_id, checksum)\nWHERE (library_id IS NULL);\n", + "unique": true, + "columns": [] + } + }, + { + "id": 12, + "references": [ + 1 + ], + "type": "index", + "data": { + "on": 1, + "name": "UQ_remote_assets_owner_library_checksum", + "sql": "CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum\nON remote_asset_entity (owner_id, library_id, checksum)\nWHERE (library_id IS NOT NULL);\n", + "unique": true, + "columns": [] + } + }, + { + "id": 13, + "references": [ + 1 + ], + "type": "index", + "data": { + "on": 1, + "name": "idx_remote_asset_checksum", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)", + "unique": false, + "columns": [] + } + }, + { + "id": 14, + "references": [ + 1 + ], + "type": "index", + "data": { + "on": 1, + "name": "idx_remote_asset_stack_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 15, + "references": [ + 1 + ], + "type": "index", + "data": { + "on": 1, + "name": "idx_remote_asset_owner_visibility_deleted_created", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created\nON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)\n", + "unique": false, + "columns": [] + } + }, + { + "id": 16, + "references": [], + "type": "table", + "data": { + "name": "auth_user_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "email", + "getter_name": "email", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_admin", + "getter_name": "isAdmin", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_admin\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_admin\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "has_profile_image", + "getter_name": "hasProfileImage", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"has_profile_image\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"has_profile_image\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "profile_changed_at", + "getter_name": "profileChangedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "avatar_color", + "getter_name": "avatarColor", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AvatarColor.values)", + "dart_type_name": "AvatarColor" + } + }, + { + "name": "quota_size_in_bytes", + "getter_name": "quotaSizeInBytes", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quota_usage_in_bytes", + "getter_name": "quotaUsageInBytes", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "pin_code", + "getter_name": "pinCode", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 17, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "user_metadata_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "user_id", + "getter_name": "userId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "key", + "getter_name": "key", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(UserMetadataKey.values)", + "dart_type_name": "UserMetadataKey" + } + }, + { + "name": "value", + "getter_name": "value", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "userMetadataConverter", + "dart_type_name": "Map" + } + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "user_id", + "key" + ] + } + }, + { + "id": 18, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "partner_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "shared_by_id", + "getter_name": "sharedById", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "shared_with_id", + "getter_name": "sharedWithId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "in_timeline", + "getter_name": "inTimeline", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"in_timeline\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"in_timeline\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "shared_by_id", + "shared_with_id" + ] + } + }, + { + "id": 19, + "references": [ + 1 + ], + "type": "table", + "data": { + "name": "remote_exif_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "asset_id", + "getter_name": "assetId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "city", + "getter_name": "city", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "state", + "getter_name": "state", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "country", + "getter_name": "country", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "date_time_original", + "getter_name": "dateTimeOriginal", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "description", + "getter_name": "description", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "height", + "getter_name": "height", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "width", + "getter_name": "width", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "exposure_time", + "getter_name": "exposureTime", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "f_number", + "getter_name": "fNumber", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "file_size", + "getter_name": "fileSize", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "focal_length", + "getter_name": "focalLength", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "latitude", + "getter_name": "latitude", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "longitude", + "getter_name": "longitude", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "iso", + "getter_name": "iso", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "make", + "getter_name": "make", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "model", + "getter_name": "model", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "lens", + "getter_name": "lens", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "orientation", + "getter_name": "orientation", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "time_zone", + "getter_name": "timeZone", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "rating", + "getter_name": "rating", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "projection_type", + "getter_name": "projectionType", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "asset_id" + ] + } + }, + { + "id": 20, + "references": [ + 1, + 4 + ], + "type": "table", + "data": { + "name": "remote_album_asset_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "asset_id", + "getter_name": "assetId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "album_id", + "getter_name": "albumId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_album_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_album_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_album_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "asset_id", + "album_id" + ] + } + }, + { + "id": 21, + "references": [ + 4, + 0 + ], + "type": "table", + "data": { + "name": "remote_album_user_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "album_id", + "getter_name": "albumId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_album_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_album_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_album_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "user_id", + "getter_name": "userId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "role", + "getter_name": "role", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AlbumUserRole.values)", + "dart_type_name": "AlbumUserRole" + } + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "album_id", + "user_id" + ] + } + }, + { + "id": 22, + "references": [ + 1 + ], + "type": "table", + "data": { + "name": "remote_asset_cloud_id_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "asset_id", + "getter_name": "assetId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "cloud_id", + "getter_name": "cloudId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "adjustment_time", + "getter_name": "adjustmentTime", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "latitude", + "getter_name": "latitude", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "longitude", + "getter_name": "longitude", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "asset_id" + ] + } + }, + { + "id": 23, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "memory_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "deleted_at", + "getter_name": "deletedAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "owner_id", + "getter_name": "ownerId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(MemoryTypeEnum.values)", + "dart_type_name": "MemoryTypeEnum" + } + }, + { + "name": "data", + "getter_name": "data", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_saved", + "getter_name": "isSaved", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_saved\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_saved\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "memory_at", + "getter_name": "memoryAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "seen_at", + "getter_name": "seenAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "show_at", + "getter_name": "showAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "hide_at", + "getter_name": "hideAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 24, + "references": [ + 1, + 23 + ], + "type": "table", + "data": { + "name": "memory_asset_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "asset_id", + "getter_name": "assetId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "memory_id", + "getter_name": "memoryId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES memory_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES memory_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "memory_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "asset_id", + "memory_id" + ] + } + }, + { + "id": 25, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "person_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "owner_id", + "getter_name": "ownerId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "face_asset_id", + "getter_name": "faceAssetId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_favorite", + "getter_name": "isFavorite", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_favorite\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_favorite\" IN (0, 1))" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_hidden", + "getter_name": "isHidden", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_hidden\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_hidden\" IN (0, 1))" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "color", + "getter_name": "color", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "birth_date", + "getter_name": "birthDate", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 26, + "references": [ + 1, + 25 + ], + "type": "table", + "data": { + "name": "asset_face_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "asset_id", + "getter_name": "assetId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "person_id", + "getter_name": "personId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES person_entity (id) ON DELETE SET NULL", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES person_entity (id) ON DELETE SET NULL" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "person_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "setNull" + } + } + ] + }, + { + "name": "image_width", + "getter_name": "imageWidth", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "image_height", + "getter_name": "imageHeight", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "bounding_box_x1", + "getter_name": "boundingBoxX1", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "bounding_box_y1", + "getter_name": "boundingBoxY1", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "bounding_box_x2", + "getter_name": "boundingBoxX2", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "bounding_box_y2", + "getter_name": "boundingBoxY2", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "source_type", + "getter_name": "sourceType", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_visible", + "getter_name": "isVisible", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_visible\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_visible\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "deleted_at", + "getter_name": "deletedAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 27, + "references": [], + "type": "table", + "data": { + "name": "store_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "string_value", + "getter_name": "stringValue", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "int_value", + "getter_name": "intValue", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 28, + "references": [], + "type": "table", + "data": { + "name": "trashed_local_asset_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AssetType.values)", + "dart_type_name": "AssetType" + } + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "width", + "getter_name": "width", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "height", + "getter_name": "height", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "duration_ms", + "getter_name": "durationMs", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "album_id", + "getter_name": "albumId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "checksum", + "getter_name": "checksum", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_favorite", + "getter_name": "isFavorite", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_favorite\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_favorite\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "orientation", + "getter_name": "orientation", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "source", + "getter_name": "source", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(TrashOrigin.values)", + "dart_type_name": "TrashOrigin" + } + }, + { + "name": "playback_style", + "getter_name": "playbackStyle", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AssetPlaybackStyle.values)", + "dart_type_name": "AssetPlaybackStyle" + } + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id", + "album_id" + ] + } + }, + { + "id": 29, + "references": [ + 1 + ], + "type": "table", + "data": { + "name": "asset_edit_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "asset_id", + "getter_name": "assetId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "action", + "getter_name": "action", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AssetEditAction.values)", + "dart_type_name": "AssetEditAction" + } + }, + { + "name": "parameters", + "getter_name": "parameters", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "editParameterConverter", + "dart_type_name": "Map" + } + }, + { + "name": "sequence", + "getter_name": "sequence", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 30, + "references": [], + "type": "table", + "data": { + "name": "settings", + "was_declared_in_moor": false, + "columns": [ + { + "name": "key", + "getter_name": "key", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "value", + "getter_name": "value", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "key" + ] + } + }, + { + "id": 31, + "references": [ + 18 + ], + "type": "index", + "data": { + "on": 18, + "name": "idx_partner_shared_with_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 32, + "references": [ + 19 + ], + "type": "index", + "data": { + "on": 19, + "name": "idx_lat_lng", + "sql": "CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)", + "unique": false, + "columns": [] + } + }, + { + "id": 33, + "references": [ + 19 + ], + "type": "index", + "data": { + "on": 19, + "name": "idx_remote_exif_city", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_exif_city\nON remote_exif_entity (city) WHERE city IS NOT NULL\n", + "unique": false, + "columns": [] + } + }, + { + "id": 34, + "references": [ + 20 + ], + "type": "index", + "data": { + "on": 20, + "name": "idx_remote_album_asset_album_asset", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 35, + "references": [ + 22 + ], + "type": "index", + "data": { + "on": 22, + "name": "idx_remote_asset_cloud_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 36, + "references": [ + 25 + ], + "type": "index", + "data": { + "on": 25, + "name": "idx_person_owner_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 37, + "references": [ + 26 + ], + "type": "index", + "data": { + "on": 26, + "name": "idx_asset_face_person_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 38, + "references": [ + 26 + ], + "type": "index", + "data": { + "on": 26, + "name": "idx_asset_face_asset_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 39, + "references": [ + 26 + ], + "type": "index", + "data": { + "on": 26, + "name": "idx_asset_face_visible_person", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person\nON asset_face_entity (person_id, asset_id)\nWHERE is_visible = 1 AND deleted_at IS NULL\n", + "unique": false, + "columns": [] + } + }, + { + "id": 40, + "references": [ + 28 + ], + "type": "index", + "data": { + "on": 28, + "name": "idx_trashed_local_asset_checksum", + "sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)", + "unique": false, + "columns": [] + } + }, + { + "id": 41, + "references": [ + 28 + ], + "type": "index", + "data": { + "on": 28, + "name": "idx_trashed_local_asset_album", + "sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 42, + "references": [ + 29 + ], + "type": "index", + "data": { + "on": 29, + "name": "idx_asset_edit_asset_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)", + "unique": false, + "columns": [] + } + } + ], + "fixed_sql": [ + { + "name": "user_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"user_entity\" (\"id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"email\" TEXT NOT NULL, \"has_profile_image\" INTEGER NOT NULL DEFAULT 0 CHECK (\"has_profile_image\" IN (0, 1)), \"profile_changed_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"avatar_color\" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "remote_asset_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"remote_asset_entity\" (\"name\" TEXT NOT NULL, \"type\" INTEGER NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"width\" INTEGER NULL, \"height\" INTEGER NULL, \"duration_ms\" INTEGER NULL, \"id\" TEXT NOT NULL, \"checksum\" TEXT NOT NULL, \"is_favorite\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_favorite\" IN (0, 1)), \"owner_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"local_date_time\" TEXT NULL, \"thumb_hash\" TEXT NULL, \"deleted_at\" TEXT NULL, \"uploaded_at\" TEXT NULL, \"live_photo_video_id\" TEXT NULL, \"visibility\" INTEGER NOT NULL, \"stack_id\" TEXT NULL, \"library_id\" TEXT NULL, \"is_edited\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_edited\" IN (0, 1)), PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "stack_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"stack_entity\" (\"id\" TEXT NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"owner_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"primary_asset_id\" TEXT NOT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "local_asset_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"local_asset_entity\" (\"name\" TEXT NOT NULL, \"type\" INTEGER NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"width\" INTEGER NULL, \"height\" INTEGER NULL, \"duration_ms\" INTEGER NULL, \"id\" TEXT NOT NULL, \"checksum\" TEXT NULL, \"is_favorite\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_favorite\" IN (0, 1)), \"orientation\" INTEGER NOT NULL DEFAULT 0, \"i_cloud_id\" TEXT NULL, \"adjustment_time\" TEXT NULL, \"latitude\" REAL NULL, \"longitude\" REAL NULL, \"playback_style\" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "remote_album_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"remote_album_entity\" (\"id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"description\" TEXT NOT NULL DEFAULT '', \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"thumbnail_asset_id\" TEXT NULL REFERENCES remote_asset_entity (id) ON DELETE SET NULL, \"is_activity_enabled\" INTEGER NOT NULL DEFAULT 1 CHECK (\"is_activity_enabled\" IN (0, 1)), \"order\" INTEGER NOT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "local_album_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"local_album_entity\" (\"id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"backup_selection\" INTEGER NOT NULL, \"is_ios_shared_album\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_ios_shared_album\" IN (0, 1)), \"linked_remote_album_id\" TEXT NULL REFERENCES remote_album_entity (id) ON DELETE SET NULL, \"marker\" INTEGER NULL CHECK (\"marker\" IN (0, 1)), PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "local_album_asset_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"local_album_asset_entity\" (\"asset_id\" TEXT NOT NULL REFERENCES local_asset_entity (id) ON DELETE CASCADE, \"album_id\" TEXT NOT NULL REFERENCES local_album_entity (id) ON DELETE CASCADE, \"marker\" INTEGER NULL CHECK (\"marker\" IN (0, 1)), PRIMARY KEY (\"asset_id\", \"album_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "idx_local_album_asset_album_asset", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)" + } + ] + }, + { + "name": "idx_local_asset_checksum", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)" + } + ] + }, + { + "name": "idx_local_asset_cloud_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)" + } + ] + }, + { + "name": "idx_stack_primary_asset_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)" + } + ] + }, + { + "name": "UQ_remote_assets_owner_checksum", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)" + } + ] + }, + { + "name": "UQ_remote_assets_owner_library_checksum", + "sql": [ + { + "dialect": "sqlite", + "sql": "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)" + } + ] + }, + { + "name": "idx_remote_asset_checksum", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)" + } + ] + }, + { + "name": "idx_remote_asset_stack_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)" + } + ] + }, + { + "name": "idx_remote_asset_owner_visibility_deleted_created", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)" + } + ] + }, + { + "name": "auth_user_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"auth_user_entity\" (\"id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"email\" TEXT NOT NULL, \"is_admin\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_admin\" IN (0, 1)), \"has_profile_image\" INTEGER NOT NULL DEFAULT 0 CHECK (\"has_profile_image\" IN (0, 1)), \"profile_changed_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"avatar_color\" INTEGER NOT NULL, \"quota_size_in_bytes\" INTEGER NOT NULL DEFAULT 0, \"quota_usage_in_bytes\" INTEGER NOT NULL DEFAULT 0, \"pin_code\" TEXT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "user_metadata_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"user_metadata_entity\" (\"user_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"key\" INTEGER NOT NULL, \"value\" BLOB NOT NULL, PRIMARY KEY (\"user_id\", \"key\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "partner_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"partner_entity\" (\"shared_by_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"shared_with_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"in_timeline\" INTEGER NOT NULL DEFAULT 0 CHECK (\"in_timeline\" IN (0, 1)), PRIMARY KEY (\"shared_by_id\", \"shared_with_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "remote_exif_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"remote_exif_entity\" (\"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"city\" TEXT NULL, \"state\" TEXT NULL, \"country\" TEXT NULL, \"date_time_original\" TEXT NULL, \"description\" TEXT NULL, \"height\" INTEGER NULL, \"width\" INTEGER NULL, \"exposure_time\" TEXT NULL, \"f_number\" REAL NULL, \"file_size\" INTEGER NULL, \"focal_length\" REAL NULL, \"latitude\" REAL NULL, \"longitude\" REAL NULL, \"iso\" INTEGER NULL, \"make\" TEXT NULL, \"model\" TEXT NULL, \"lens\" TEXT NULL, \"orientation\" TEXT NULL, \"time_zone\" TEXT NULL, \"rating\" INTEGER NULL, \"projection_type\" TEXT NULL, PRIMARY KEY (\"asset_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "remote_album_asset_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"remote_album_asset_entity\" (\"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"album_id\" TEXT NOT NULL REFERENCES remote_album_entity (id) ON DELETE CASCADE, PRIMARY KEY (\"asset_id\", \"album_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "remote_album_user_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"remote_album_user_entity\" (\"album_id\" TEXT NOT NULL REFERENCES remote_album_entity (id) ON DELETE CASCADE, \"user_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"role\" INTEGER NOT NULL, PRIMARY KEY (\"album_id\", \"user_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "remote_asset_cloud_id_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"remote_asset_cloud_id_entity\" (\"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"cloud_id\" TEXT NULL, \"created_at\" TEXT NULL, \"adjustment_time\" TEXT NULL, \"latitude\" REAL NULL, \"longitude\" REAL NULL, PRIMARY KEY (\"asset_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "memory_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"memory_entity\" (\"id\" TEXT NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"deleted_at\" TEXT NULL, \"owner_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"type\" INTEGER NOT NULL, \"data\" TEXT NOT NULL, \"is_saved\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_saved\" IN (0, 1)), \"memory_at\" TEXT NOT NULL, \"seen_at\" TEXT NULL, \"show_at\" TEXT NULL, \"hide_at\" TEXT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "memory_asset_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"memory_asset_entity\" (\"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"memory_id\" TEXT NOT NULL REFERENCES memory_entity (id) ON DELETE CASCADE, PRIMARY KEY (\"asset_id\", \"memory_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "person_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"person_entity\" (\"id\" TEXT NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"owner_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"name\" TEXT NOT NULL, \"face_asset_id\" TEXT NULL, \"is_favorite\" INTEGER NOT NULL CHECK (\"is_favorite\" IN (0, 1)), \"is_hidden\" INTEGER NOT NULL CHECK (\"is_hidden\" IN (0, 1)), \"color\" TEXT NULL, \"birth_date\" TEXT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "asset_face_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"asset_face_entity\" (\"id\" TEXT NOT NULL, \"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"person_id\" TEXT NULL REFERENCES person_entity (id) ON DELETE SET NULL, \"image_width\" INTEGER NOT NULL, \"image_height\" INTEGER NOT NULL, \"bounding_box_x1\" INTEGER NOT NULL, \"bounding_box_y1\" INTEGER NOT NULL, \"bounding_box_x2\" INTEGER NOT NULL, \"bounding_box_y2\" INTEGER NOT NULL, \"source_type\" TEXT NOT NULL, \"is_visible\" INTEGER NOT NULL DEFAULT 1 CHECK (\"is_visible\" IN (0, 1)), \"deleted_at\" TEXT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "store_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"store_entity\" (\"id\" INTEGER NOT NULL, \"string_value\" TEXT NULL, \"int_value\" INTEGER NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "trashed_local_asset_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"trashed_local_asset_entity\" (\"name\" TEXT NOT NULL, \"type\" INTEGER NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"width\" INTEGER NULL, \"height\" INTEGER NULL, \"duration_ms\" INTEGER NULL, \"id\" TEXT NOT NULL, \"album_id\" TEXT NOT NULL, \"checksum\" TEXT NULL, \"is_favorite\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_favorite\" IN (0, 1)), \"orientation\" INTEGER NOT NULL DEFAULT 0, \"source\" INTEGER NOT NULL, \"playback_style\" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (\"id\", \"album_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "asset_edit_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"asset_edit_entity\" (\"id\" TEXT NOT NULL, \"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"action\" INTEGER NOT NULL, \"parameters\" BLOB NOT NULL, \"sequence\" INTEGER NOT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "settings", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"settings\" (\"key\" TEXT NOT NULL, \"value\" TEXT NOT NULL, \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), PRIMARY KEY (\"key\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "idx_partner_shared_with_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)" + } + ] + }, + { + "name": "idx_lat_lng", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)" + } + ] + }, + { + "name": "idx_remote_exif_city", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL" + } + ] + }, + { + "name": "idx_remote_album_asset_album_asset", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)" + } + ] + }, + { + "name": "idx_remote_asset_cloud_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)" + } + ] + }, + { + "name": "idx_person_owner_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)" + } + ] + }, + { + "name": "idx_asset_face_person_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)" + } + ] + }, + { + "name": "idx_asset_face_asset_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)" + } + ] + }, + { + "name": "idx_asset_face_visible_person", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL" + } + ] + }, + { + "name": "idx_trashed_local_asset_checksum", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)" + } + ] + }, + { + "name": "idx_trashed_local_asset_album", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)" + } + ] + }, + { + "name": "idx_asset_edit_asset_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)" + } + ] + } + ] +} \ No newline at end of file diff --git a/mobile/lib/domain/models/config/app_config.dart b/mobile/lib/domain/models/config/app_config.dart index d881f67cf8..a955fb73a8 100644 --- a/mobile/lib/domain/models/config/app_config.dart +++ b/mobile/lib/domain/models/config/app_config.dart @@ -12,7 +12,7 @@ import 'package:immich_mobile/domain/models/config/theme_config.dart'; import 'package:immich_mobile/domain/models/config/timeline_config.dart'; import 'package:immich_mobile/domain/models/config/viewer_config.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; +import 'package:immich_mobile/domain/models/settings_key.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; @@ -95,7 +95,7 @@ class AppConfig { String toString() => 'AppConfig(logLevel: $logLevel, theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow, album: $album, backup: $backup, network: $network)'; - T read(MetadataKey key) => + T read(SettingsKey key) => (switch (key) { .logLevel => logLevel, .themePrimaryColor => theme.primaryColor, @@ -143,10 +143,10 @@ class AppConfig { }) as T; - factory AppConfig.fromEntries(Map, Object> overrides) => + factory AppConfig.fromEntries(Map, Object> overrides) => overrides.entries.fold(const AppConfig(), (config, entry) => config.write(entry.key, entry.value)); - AppConfig write(MetadataKey key, T value) { + AppConfig write(SettingsKey key, T value) { return switch (key) { .logLevel => copyWith(logLevel: value as LogLevel), .themePrimaryColor => copyWith(theme: theme.copyWith(primaryColor: value as ImmichColorPreset)), diff --git a/mobile/lib/domain/models/metadata_key.dart b/mobile/lib/domain/models/settings_key.dart similarity index 85% rename from mobile/lib/domain/models/metadata_key.dart rename to mobile/lib/domain/models/settings_key.dart index c8822c28e9..5277a37a60 100644 --- a/mobile/lib/domain/models/metadata_key.dart +++ b/mobile/lib/domain/models/settings_key.dart @@ -7,7 +7,7 @@ import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; -enum MetadataKey { +enum SettingsKey { // Theme themePrimaryColor(codec: _EnumCodec(ImmichColorPreset.values)), themeMode(codec: _EnumCodec(ThemeMode.values)), @@ -73,24 +73,24 @@ enum MetadataKey { slideshowLook(codec: _EnumCodec(SlideshowLook.values)), slideshowDirection(codec: _EnumCodec(SlideshowDirection.values)); - final _MetadataCodec? _codecOverride; + final _SettingsCodec? _codecOverride; - const MetadataKey({_MetadataCodec? codec}) : _codecOverride = codec; + const SettingsKey({_SettingsCodec? codec}) : _codecOverride = codec; - _MetadataCodec get _codec => _codecOverride ?? _MetadataCodec.forType(T); + _SettingsCodec get _codec => _codecOverride ?? _SettingsCodec.forType(T); String encode(T value) => _codec.encode(value); T decode(String raw) => _codec.decode(raw); } -sealed class _MetadataCodec { - const _MetadataCodec(); +sealed class _SettingsCodec { + const _SettingsCodec(); String encode(T value); T decode(String raw); - static const Map> _primitives = { + static const Map> _primitives = { int: _PrimitiveCodec.integer, double: _PrimitiveCodec.real, bool: _PrimitiveCodec.boolean, @@ -98,16 +98,16 @@ sealed class _MetadataCodec { DateTime: _DateTimeCodec(), }; - static _MetadataCodec forType(Type runtimeType) { + static _SettingsCodec forType(Type runtimeType) { final codec = _primitives[runtimeType]; if (codec == null) { - throw StateError('No primitive codec for $runtimeType. Provide an explicit codec when defining the MetadataKey.'); + throw StateError('No primitive codec for $runtimeType. Provide an explicit codec when defining the SettingsKey.'); } - return codec as _MetadataCodec; + return codec as _SettingsCodec; } } -final class _EnumCodec extends _MetadataCodec { +final class _EnumCodec extends _SettingsCodec { final List values; const _EnumCodec(this.values); @@ -119,7 +119,7 @@ final class _EnumCodec extends _MetadataCodec { T decode(String raw) => values.firstWhere((v) => v.name == raw); } -final class _DateTimeCodec extends _MetadataCodec { +final class _DateTimeCodec extends _SettingsCodec { const _DateTimeCodec(); @override @@ -129,9 +129,9 @@ final class _DateTimeCodec extends _MetadataCodec { DateTime decode(String raw) => DateTime.parse(raw); } -final class _MapCodec extends _MetadataCodec> { - final _MetadataCodec _keyCodec; - final _MetadataCodec _valueCodec; +final class _MapCodec extends _SettingsCodec> { + final _SettingsCodec _keyCodec; + final _SettingsCodec _valueCodec; const _MapCodec(this._keyCodec, this._valueCodec); @@ -167,8 +167,8 @@ final class _MapCodec extends _MetadataCodec } } -final class _ListCodec extends _MetadataCodec> { - final _MetadataCodec _elementCodec; +final class _ListCodec extends _SettingsCodec> { + final _SettingsCodec _elementCodec; const _ListCodec(this._elementCodec); @@ -197,7 +197,7 @@ final class _ListCodec extends _MetadataCodec> { } } -final class _PrimitiveCodec extends _MetadataCodec { +final class _PrimitiveCodec extends _SettingsCodec { final T Function(String) _parse; const _PrimitiveCodec._(this._parse); diff --git a/mobile/lib/domain/services/background_worker.service.dart b/mobile/lib/domain/services/background_worker.service.dart index 3411f8aa13..d28f7ff14b 100644 --- a/mobile/lib/domain/services/background_worker.service.dart +++ b/mobile/lib/domain/services/background_worker.service.dart @@ -11,7 +11,7 @@ import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/platform/background_worker_api.g.dart'; import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; @@ -39,7 +39,7 @@ class BackgroundWorkerFgService { _foregroundHostApi.saveNotificationMessage(title, body); Future configure({int? minimumDelaySeconds, bool? requireCharging}) { - final backup = MetadataRepository.instance.appConfig.backup; + final backup = SettingsRepository.instance.appConfig.backup; return _foregroundHostApi.configure( BackgroundWorkerSettings( minimumDelaySeconds: minimumDelaySeconds ?? backup.triggerDelay, @@ -67,7 +67,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { BackgroundWorkerFlutterApi.setUp(this); } - bool get _isBackupEnabled => MetadataRepository.instance.appConfig.backup.enabled; + bool get _isBackupEnabled => SettingsRepository.instance.appConfig.backup.enabled; Future init() async { try { diff --git a/mobile/lib/domain/services/log.service.dart b/mobile/lib/domain/services/log.service.dart index ac65c9bedf..216f030b12 100644 --- a/mobile/lib/domain/services/log.service.dart +++ b/mobile/lib/domain/services/log.service.dart @@ -2,9 +2,9 @@ import 'dart:async'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; +import 'package:immich_mobile/domain/models/settings_key.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:logging/logging.dart'; @@ -12,10 +12,10 @@ import 'package:logging/logging.dart'; /// /// It listens to Dart's [Logger.root], buffers logs in memory (optionally), /// writes them to a persistent [LogRepository], and manages log levels via -/// [MetadataRepository]. +/// [SettingsRepository]. class LogService { final LogRepository _logRepository; - final MetadataRepository _metadataRepository; + final SettingsRepository _settingsRepository; final List _msgBuffer = []; @@ -38,12 +38,12 @@ class LogService { static Future init({ required LogRepository logRepository, - required MetadataRepository metadataRepository, + required SettingsRepository settingsRepository, bool shouldBuffer = true, }) async { _instance ??= await create( logRepository: logRepository, - metadataRepository: metadataRepository, + settingsRepository: settingsRepository, shouldBuffer: shouldBuffer, ); return _instance!; @@ -51,17 +51,17 @@ class LogService { static Future create({ required LogRepository logRepository, - required MetadataRepository metadataRepository, + required SettingsRepository settingsRepository, bool shouldBuffer = true, }) async { - final instance = LogService._(logRepository, metadataRepository, shouldBuffer); + final instance = LogService._(logRepository, settingsRepository, shouldBuffer); await logRepository.truncate(limit: kLogTruncateLimit); - final level = instance._metadataRepository.appConfig.logLevel; + final level = instance._settingsRepository.appConfig.logLevel; Logger.root.level = Level.LEVELS.elementAtOrNull(level.index) ?? Level.INFO; return instance; } - LogService._(this._logRepository, this._metadataRepository, this._shouldBuffer) { + LogService._(this._logRepository, this._settingsRepository, this._shouldBuffer) { _logSubscription = Logger.root.onRecord.listen(_handleLogRecord); } @@ -91,7 +91,7 @@ class LogService { } Future setLogLevel(LogLevel level) async { - await _metadataRepository.write(MetadataKey.logLevel, level); + await _settingsRepository.write(SettingsKey.logLevel, level); Logger.root.level = level.toLevel(); } diff --git a/mobile/lib/domain/services/timeline.service.dart b/mobile/lib/domain/services/timeline.service.dart index c6324b356e..4cc58b0fe7 100644 --- a/mobile/lib/domain/services/timeline.service.dart +++ b/mobile/lib/domain/services/timeline.service.dart @@ -7,7 +7,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/events.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart'; import 'package:immich_mobile/utils/async_mutex.dart'; @@ -39,12 +39,12 @@ enum TimelineOrigin { class TimelineFactory { final DriftTimelineRepository _timelineRepository; - final MetadataRepository _metadataRepository; + final SettingsRepository _settingsRepository; - const TimelineFactory({required this._timelineRepository, required this._metadataRepository}); + const TimelineFactory({required this._timelineRepository, required this._settingsRepository}); GroupAssetsBy get groupBy { - final group = _metadataRepository.appConfig.timeline.groupAssetsBy; + final group = _settingsRepository.appConfig.timeline.groupAssetsBy; // We do not support auto grouping in the new timeline yet, fallback to day grouping return group == GroupAssetsBy.auto ? GroupAssetsBy.day : group; } diff --git a/mobile/lib/infrastructure/entities/metadata.entity.dart b/mobile/lib/infrastructure/entities/settings.entity.dart similarity index 72% rename from mobile/lib/infrastructure/entities/metadata.entity.dart rename to mobile/lib/infrastructure/entities/settings.entity.dart index 2908245040..36e0bcc990 100644 --- a/mobile/lib/infrastructure/entities/metadata.entity.dart +++ b/mobile/lib/infrastructure/entities/settings.entity.dart @@ -1,8 +1,8 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; -class MetadataEntity extends Table with DriftDefaultsMixin { - const MetadataEntity(); +class SettingsEntity extends Table with DriftDefaultsMixin { + const SettingsEntity(); TextColumn get key => text()(); @@ -14,5 +14,5 @@ class MetadataEntity extends Table with DriftDefaultsMixin { Set get primaryKey => {key}; @override - String get tableName => "metadata"; + String get tableName => "settings"; } diff --git a/mobile/lib/infrastructure/entities/metadata.entity.drift.dart b/mobile/lib/infrastructure/entities/settings.entity.drift.dart similarity index 74% rename from mobile/lib/infrastructure/entities/metadata.entity.drift.dart rename to mobile/lib/infrastructure/entities/settings.entity.drift.dart index 80bf7bfc43..e2cac89a5e 100644 --- a/mobile/lib/infrastructure/entities/metadata.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/settings.entity.drift.dart @@ -1,28 +1,28 @@ // dart format width=80 // ignore_for_file: type=lint import 'package:drift/drift.dart' as i0; -import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart' +import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart' as i1; -import 'package:immich_mobile/infrastructure/entities/metadata.entity.dart' +import 'package:immich_mobile/infrastructure/entities/settings.entity.dart' as i2; import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3; -typedef $$MetadataEntityTableCreateCompanionBuilder = - i1.MetadataEntityCompanion Function({ +typedef $$SettingsEntityTableCreateCompanionBuilder = + i1.SettingsEntityCompanion Function({ required String key, required String value, i0.Value updatedAt, }); -typedef $$MetadataEntityTableUpdateCompanionBuilder = - i1.MetadataEntityCompanion Function({ +typedef $$SettingsEntityTableUpdateCompanionBuilder = + i1.SettingsEntityCompanion Function({ i0.Value key, i0.Value value, i0.Value updatedAt, }); -class $$MetadataEntityTableFilterComposer - extends i0.Composer { - $$MetadataEntityTableFilterComposer({ +class $$SettingsEntityTableFilterComposer + extends i0.Composer { + $$SettingsEntityTableFilterComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -45,9 +45,9 @@ class $$MetadataEntityTableFilterComposer ); } -class $$MetadataEntityTableOrderingComposer - extends i0.Composer { - $$MetadataEntityTableOrderingComposer({ +class $$SettingsEntityTableOrderingComposer + extends i0.Composer { + $$SettingsEntityTableOrderingComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -70,9 +70,9 @@ class $$MetadataEntityTableOrderingComposer ); } -class $$MetadataEntityTableAnnotationComposer - extends i0.Composer { - $$MetadataEntityTableAnnotationComposer({ +class $$SettingsEntityTableAnnotationComposer + extends i0.Composer { + $$SettingsEntityTableAnnotationComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -89,47 +89,47 @@ class $$MetadataEntityTableAnnotationComposer $composableBuilder(column: $table.updatedAt, builder: (column) => column); } -class $$MetadataEntityTableTableManager +class $$SettingsEntityTableTableManager extends i0.RootTableManager< i0.GeneratedDatabase, - i1.$MetadataEntityTable, - i1.MetadataEntityData, - i1.$$MetadataEntityTableFilterComposer, - i1.$$MetadataEntityTableOrderingComposer, - i1.$$MetadataEntityTableAnnotationComposer, - $$MetadataEntityTableCreateCompanionBuilder, - $$MetadataEntityTableUpdateCompanionBuilder, + i1.$SettingsEntityTable, + i1.SettingsEntityData, + i1.$$SettingsEntityTableFilterComposer, + i1.$$SettingsEntityTableOrderingComposer, + i1.$$SettingsEntityTableAnnotationComposer, + $$SettingsEntityTableCreateCompanionBuilder, + $$SettingsEntityTableUpdateCompanionBuilder, ( - i1.MetadataEntityData, + i1.SettingsEntityData, i0.BaseReferences< i0.GeneratedDatabase, - i1.$MetadataEntityTable, - i1.MetadataEntityData + i1.$SettingsEntityTable, + i1.SettingsEntityData >, ), - i1.MetadataEntityData, + i1.SettingsEntityData, i0.PrefetchHooks Function() > { - $$MetadataEntityTableTableManager( + $$SettingsEntityTableTableManager( i0.GeneratedDatabase db, - i1.$MetadataEntityTable table, + i1.$SettingsEntityTable table, ) : super( i0.TableManagerState( db: db, table: table, createFilteringComposer: () => - i1.$$MetadataEntityTableFilterComposer($db: db, $table: table), + i1.$$SettingsEntityTableFilterComposer($db: db, $table: table), createOrderingComposer: () => - i1.$$MetadataEntityTableOrderingComposer($db: db, $table: table), + i1.$$SettingsEntityTableOrderingComposer($db: db, $table: table), createComputedFieldComposer: () => i1 - .$$MetadataEntityTableAnnotationComposer($db: db, $table: table), + .$$SettingsEntityTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ i0.Value key = const i0.Value.absent(), i0.Value value = const i0.Value.absent(), i0.Value updatedAt = const i0.Value.absent(), - }) => i1.MetadataEntityCompanion( + }) => i1.SettingsEntityCompanion( key: key, value: value, updatedAt: updatedAt, @@ -139,7 +139,7 @@ class $$MetadataEntityTableTableManager required String key, required String value, i0.Value updatedAt = const i0.Value.absent(), - }) => i1.MetadataEntityCompanion.insert( + }) => i1.SettingsEntityCompanion.insert( key: key, value: value, updatedAt: updatedAt, @@ -152,34 +152,34 @@ class $$MetadataEntityTableTableManager ); } -typedef $$MetadataEntityTableProcessedTableManager = +typedef $$SettingsEntityTableProcessedTableManager = i0.ProcessedTableManager< i0.GeneratedDatabase, - i1.$MetadataEntityTable, - i1.MetadataEntityData, - i1.$$MetadataEntityTableFilterComposer, - i1.$$MetadataEntityTableOrderingComposer, - i1.$$MetadataEntityTableAnnotationComposer, - $$MetadataEntityTableCreateCompanionBuilder, - $$MetadataEntityTableUpdateCompanionBuilder, + i1.$SettingsEntityTable, + i1.SettingsEntityData, + i1.$$SettingsEntityTableFilterComposer, + i1.$$SettingsEntityTableOrderingComposer, + i1.$$SettingsEntityTableAnnotationComposer, + $$SettingsEntityTableCreateCompanionBuilder, + $$SettingsEntityTableUpdateCompanionBuilder, ( - i1.MetadataEntityData, + i1.SettingsEntityData, i0.BaseReferences< i0.GeneratedDatabase, - i1.$MetadataEntityTable, - i1.MetadataEntityData + i1.$SettingsEntityTable, + i1.SettingsEntityData >, ), - i1.MetadataEntityData, + i1.SettingsEntityData, i0.PrefetchHooks Function() >; -class $MetadataEntityTable extends i2.MetadataEntity - with i0.TableInfo<$MetadataEntityTable, i1.MetadataEntityData> { +class $SettingsEntityTable extends i2.SettingsEntity + with i0.TableInfo<$SettingsEntityTable, i1.SettingsEntityData> { @override final i0.GeneratedDatabase attachedDatabase; final String? _alias; - $MetadataEntityTable(this.attachedDatabase, [this._alias]); + $SettingsEntityTable(this.attachedDatabase, [this._alias]); static const i0.VerificationMeta _keyMeta = const i0.VerificationMeta('key'); @override late final i0.GeneratedColumn key = i0.GeneratedColumn( @@ -219,10 +219,10 @@ class $MetadataEntityTable extends i2.MetadataEntity String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; - static const String $name = 'metadata'; + static const String $name = 'settings'; @override i0.VerificationContext validateIntegrity( - i0.Insertable instance, { + i0.Insertable instance, { bool isInserting = false, }) { final context = i0.VerificationContext(); @@ -255,9 +255,9 @@ class $MetadataEntityTable extends i2.MetadataEntity @override Set get $primaryKey => {key}; @override - i1.MetadataEntityData map(Map data, {String? tablePrefix}) { + i1.SettingsEntityData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return i1.MetadataEntityData( + return i1.SettingsEntityData( key: attachedDatabase.typeMapping.read( i0.DriftSqlType.string, data['${effectivePrefix}key'], @@ -274,8 +274,8 @@ class $MetadataEntityTable extends i2.MetadataEntity } @override - $MetadataEntityTable createAlias(String alias) { - return $MetadataEntityTable(attachedDatabase, alias); + $SettingsEntityTable createAlias(String alias) { + return $SettingsEntityTable(attachedDatabase, alias); } @override @@ -284,12 +284,12 @@ class $MetadataEntityTable extends i2.MetadataEntity bool get isStrict => true; } -class MetadataEntityData extends i0.DataClass - implements i0.Insertable { +class SettingsEntityData extends i0.DataClass + implements i0.Insertable { final String key; final String value; final DateTime updatedAt; - const MetadataEntityData({ + const SettingsEntityData({ required this.key, required this.value, required this.updatedAt, @@ -303,12 +303,12 @@ class MetadataEntityData extends i0.DataClass return map; } - factory MetadataEntityData.fromJson( + factory SettingsEntityData.fromJson( Map json, { i0.ValueSerializer? serializer, }) { serializer ??= i0.driftRuntimeOptions.defaultSerializer; - return MetadataEntityData( + return SettingsEntityData( key: serializer.fromJson(json['key']), value: serializer.fromJson(json['value']), updatedAt: serializer.fromJson(json['updatedAt']), @@ -324,17 +324,17 @@ class MetadataEntityData extends i0.DataClass }; } - i1.MetadataEntityData copyWith({ + i1.SettingsEntityData copyWith({ String? key, String? value, DateTime? updatedAt, - }) => i1.MetadataEntityData( + }) => i1.SettingsEntityData( key: key ?? this.key, value: value ?? this.value, updatedAt: updatedAt ?? this.updatedAt, ); - MetadataEntityData copyWithCompanion(i1.MetadataEntityCompanion data) { - return MetadataEntityData( + SettingsEntityData copyWithCompanion(i1.SettingsEntityCompanion data) { + return SettingsEntityData( key: data.key.present ? data.key.value : this.key, value: data.value.present ? data.value.value : this.value, updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, @@ -343,7 +343,7 @@ class MetadataEntityData extends i0.DataClass @override String toString() { - return (StringBuffer('MetadataEntityData(') + return (StringBuffer('SettingsEntityData(') ..write('key: $key, ') ..write('value: $value, ') ..write('updatedAt: $updatedAt') @@ -356,29 +356,29 @@ class MetadataEntityData extends i0.DataClass @override bool operator ==(Object other) => identical(this, other) || - (other is i1.MetadataEntityData && + (other is i1.SettingsEntityData && other.key == this.key && other.value == this.value && other.updatedAt == this.updatedAt); } -class MetadataEntityCompanion - extends i0.UpdateCompanion { +class SettingsEntityCompanion + extends i0.UpdateCompanion { final i0.Value key; final i0.Value value; final i0.Value updatedAt; - const MetadataEntityCompanion({ + const SettingsEntityCompanion({ this.key = const i0.Value.absent(), this.value = const i0.Value.absent(), this.updatedAt = const i0.Value.absent(), }); - MetadataEntityCompanion.insert({ + SettingsEntityCompanion.insert({ required String key, required String value, this.updatedAt = const i0.Value.absent(), }) : key = i0.Value(key), value = i0.Value(value); - static i0.Insertable custom({ + static i0.Insertable custom({ i0.Expression? key, i0.Expression? value, i0.Expression? updatedAt, @@ -390,12 +390,12 @@ class MetadataEntityCompanion }); } - i1.MetadataEntityCompanion copyWith({ + i1.SettingsEntityCompanion copyWith({ i0.Value? key, i0.Value? value, i0.Value? updatedAt, }) { - return i1.MetadataEntityCompanion( + return i1.SettingsEntityCompanion( key: key ?? this.key, value: value ?? this.value, updatedAt: updatedAt ?? this.updatedAt, @@ -419,7 +419,7 @@ class MetadataEntityCompanion @override String toString() { - return (StringBuffer('MetadataEntityCompanion(') + return (StringBuffer('SettingsEntityCompanion(') ..write('key: $key, ') ..write('value: $value, ') ..write('updatedAt: $updatedAt') diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index e81fe58ba9..6bb2e946f1 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -13,7 +13,7 @@ import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/memory.entity.dart'; import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/metadata.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/settings.entity.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.dart'; import 'package:immich_mobile/infrastructure/entities/person.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart'; @@ -55,7 +55,7 @@ import 'package:logging/logging.dart'; StoreEntity, TrashedLocalAssetEntity, AssetEditEntity, - MetadataEntity, + SettingsEntity, ], include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'}, ) @@ -98,7 +98,7 @@ class Drift extends $Drift { } @override - int get schemaVersion => 26; + int get schemaVersion => 27; @override MigrationStrategy get migration => MigrationStrategy( @@ -276,6 +276,9 @@ class Drift extends $Drift { from25To26: (m, v26) async { await m.addColumn(v26.remoteAssetEntity, v26.remoteAssetEntity.uploadedAt); }, + from26To27: (m, v27) async { + await customStatement('ALTER TABLE metadata RENAME TO settings'); + }, ), ); diff --git a/mobile/lib/infrastructure/repositories/db.repository.drift.dart b/mobile/lib/infrastructure/repositories/db.repository.drift.dart index c43a83f86a..692523219b 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.drift.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.drift.dart @@ -43,7 +43,7 @@ import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity as i20; import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart' as i21; -import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart' +import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart' as i22; import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart' as i23; @@ -91,7 +91,7 @@ abstract class $Drift extends i0.GeneratedDatabase { .$TrashedLocalAssetEntityTable(this); late final i21.$AssetEditEntityTable assetEditEntity = i21 .$AssetEditEntityTable(this); - late final i22.$MetadataEntityTable metadataEntity = i22.$MetadataEntityTable( + late final i22.$SettingsEntityTable settingsEntity = i22.$SettingsEntityTable( this, ); i23.MergedAssetDrift get mergedAssetDrift => i24.ReadDatabaseContainer( @@ -132,7 +132,7 @@ abstract class $Drift extends i0.GeneratedDatabase { storeEntity, trashedLocalAssetEntity, assetEditEntity, - metadataEntity, + settingsEntity, i10.idxPartnerSharedWithId, i11.idxLatLng, i11.idxRemoteExifCity, @@ -395,6 +395,6 @@ class $DriftManager { ); i21.$$AssetEditEntityTableTableManager get assetEditEntity => i21.$$AssetEditEntityTableTableManager(_db, _db.assetEditEntity); - i22.$$MetadataEntityTableTableManager get metadataEntity => - i22.$$MetadataEntityTableTableManager(_db, _db.metadataEntity); + i22.$$SettingsEntityTableTableManager get settingsEntity => + i22.$$SettingsEntityTableTableManager(_db, _db.settingsEntity); } diff --git a/mobile/lib/infrastructure/repositories/db.repository.steps.dart b/mobile/lib/infrastructure/repositories/db.repository.steps.dart index 1fb88de1d0..a51174d980 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.steps.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.steps.dart @@ -13539,6 +13539,550 @@ i1.GeneratedColumn _column_212(String aliasedName) => type: i1.DriftSqlType.string, $customConstraints: 'NULL', ); + +final class Schema27 extends i0.VersionedSchema { + Schema27({required super.database}) : super(version: 27); + @override + late final List entities = [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAlbumAssetAlbumAsset, + idxLocalAssetChecksum, + idxLocalAssetCloudId, + idxStackPrimaryAssetId, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + idxRemoteAssetStackId, + idxRemoteAssetOwnerVisibilityDeletedCreated, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + remoteAssetCloudIdEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + trashedLocalAssetEntity, + assetEditEntity, + settings, + idxPartnerSharedWithId, + idxLatLng, + idxRemoteExifCity, + idxRemoteAlbumAssetAlbumAsset, + idxRemoteAssetCloudId, + idxPersonOwnerId, + idxAssetFacePersonId, + idxAssetFaceAssetId, + idxAssetFaceVisiblePerson, + idxTrashedLocalAssetChecksum, + idxTrashedLocalAssetAlbum, + idxAssetEditAssetId, + ]; + late final Shape33 userEntity = Shape33( + source: i0.VersionedTable( + entityName: 'user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_107, + _column_108, + _column_109, + _column_110, + _column_111, + _column_112, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape50 remoteAssetEntity = Shape50( + source: i0.VersionedTable( + entityName: 'remote_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_108, + _column_113, + _column_114, + _column_115, + _column_116, + _column_117, + _column_118, + _column_107, + _column_119, + _column_120, + _column_121, + _column_122, + _column_123, + _column_124, + _column_212, + _column_125, + _column_126, + _column_127, + _column_128, + _column_129, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape35 stackEntity = Shape35( + source: i0.VersionedTable( + entityName: 'stack_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_107, + _column_114, + _column_115, + _column_121, + _column_130, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape36 localAssetEntity = Shape36( + source: i0.VersionedTable( + entityName: 'local_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_108, + _column_113, + _column_114, + _column_115, + _column_116, + _column_117, + _column_118, + _column_107, + _column_131, + _column_120, + _column_132, + _column_133, + _column_134, + _column_135, + _column_136, + _column_137, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape48 remoteAlbumEntity = Shape48( + source: i0.VersionedTable( + entityName: 'remote_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_107, + _column_108, + _column_138, + _column_114, + _column_115, + _column_139, + _column_140, + _column_141, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape38 localAlbumEntity = Shape38( + source: i0.VersionedTable( + entityName: 'local_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_107, + _column_108, + _column_115, + _column_142, + _column_143, + _column_144, + _column_145, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape39 localAlbumAssetEntity = Shape39( + source: i0.VersionedTable( + entityName: 'local_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_146, _column_147, _column_145], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxLocalAlbumAssetAlbumAsset = i1.Index( + 'idx_local_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)', + ); + 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 idxStackPrimaryAssetId = i1.Index( + 'idx_stack_primary_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)', + ); + 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)', + ); + final i1.Index idxRemoteAssetStackId = i1.Index( + 'idx_remote_asset_stack_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)', + ); + final i1.Index idxRemoteAssetOwnerVisibilityDeletedCreated = i1.Index( + 'idx_remote_asset_owner_visibility_deleted_created', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)', + ); + late final Shape40 authUserEntity = Shape40( + source: i0.VersionedTable( + entityName: 'auth_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_107, + _column_108, + _column_109, + _column_148, + _column_110, + _column_111, + _column_149, + _column_150, + _column_151, + _column_152, + ], + 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_153, _column_154, _column_155], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape41 partnerEntity = Shape41( + source: i0.VersionedTable( + entityName: 'partner_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'], + columns: [_column_156, _column_157, _column_158], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape42 remoteExifEntity = Shape42( + source: i0.VersionedTable( + entityName: 'remote_exif_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id)'], + columns: [ + _column_159, + _column_160, + _column_161, + _column_162, + _column_163, + _column_164, + _column_117, + _column_116, + _column_165, + _column_166, + _column_167, + _column_168, + _column_135, + _column_136, + _column_169, + _column_170, + _column_171, + _column_172, + _column_173, + _column_174, + _column_175, + _column_176, + ], + 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_159, _column_177], + 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_177, _column_153, _column_178], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape43 remoteAssetCloudIdEntity = Shape43( + source: i0.VersionedTable( + entityName: 'remote_asset_cloud_id_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id)'], + columns: [ + _column_159, + _column_179, + _column_180, + _column_134, + _column_135, + _column_136, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape44 memoryEntity = Shape44( + source: i0.VersionedTable( + entityName: 'memory_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_107, + _column_114, + _column_115, + _column_124, + _column_121, + _column_113, + _column_181, + _column_182, + _column_183, + _column_184, + _column_185, + _column_186, + ], + 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_159, _column_187], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape45 personEntity = Shape45( + source: i0.VersionedTable( + entityName: 'person_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_107, + _column_114, + _column_115, + _column_121, + _column_108, + _column_188, + _column_189, + _column_190, + _column_191, + _column_192, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape46 assetFaceEntity = Shape46( + source: i0.VersionedTable( + entityName: 'asset_face_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_107, + _column_159, + _column_193, + _column_194, + _column_195, + _column_196, + _column_197, + _column_198, + _column_199, + _column_200, + _column_201, + _column_124, + ], + 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_202, _column_203, _column_204], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape47 trashedLocalAssetEntity = Shape47( + source: i0.VersionedTable( + entityName: 'trashed_local_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id, album_id)'], + columns: [ + _column_108, + _column_113, + _column_114, + _column_115, + _column_116, + _column_117, + _column_118, + _column_107, + _column_205, + _column_131, + _column_120, + _column_132, + _column_206, + _column_137, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape32 assetEditEntity = Shape32( + source: i0.VersionedTable( + entityName: 'asset_edit_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_107, + _column_159, + _column_207, + _column_208, + _column_209, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape49 settings = Shape49( + source: i0.VersionedTable( + entityName: 'settings', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY("key")'], + columns: [_column_210, _column_211, _column_115], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxPartnerSharedWithId = i1.Index( + 'idx_partner_shared_with_id', + 'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)', + ); + 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 idxRemoteExifCity = i1.Index( + 'idx_remote_exif_city', + 'CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL', + ); + final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index( + 'idx_remote_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)', + ); + final i1.Index idxRemoteAssetCloudId = i1.Index( + 'idx_remote_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)', + ); + final i1.Index idxPersonOwnerId = i1.Index( + 'idx_person_owner_id', + 'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)', + ); + final i1.Index idxAssetFacePersonId = i1.Index( + 'idx_asset_face_person_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)', + ); + final i1.Index idxAssetFaceAssetId = i1.Index( + 'idx_asset_face_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)', + ); + final i1.Index idxAssetFaceVisiblePerson = i1.Index( + 'idx_asset_face_visible_person', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL', + ); + 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)', + ); + final i1.Index idxAssetEditAssetId = i1.Index( + 'idx_asset_edit_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)', + ); +} + i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema2 schema) from1To2, required Future Function(i1.Migrator m, Schema3 schema) from2To3, @@ -13565,6 +14109,7 @@ i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema24 schema) from23To24, required Future Function(i1.Migrator m, Schema25 schema) from24To25, required Future Function(i1.Migrator m, Schema26 schema) from25To26, + required Future Function(i1.Migrator m, Schema27 schema) from26To27, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -13693,6 +14238,11 @@ i0.MigrationStepWithVersion migrationSteps({ final migrator = i1.Migrator(database, schema); await from25To26(migrator, schema); return 26; + case 26: + final schema = Schema27(database: database); + final migrator = i1.Migrator(database, schema); + await from26To27(migrator, schema); + return 27; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -13725,6 +14275,7 @@ i1.OnUpgrade stepByStep({ required Future Function(i1.Migrator m, Schema24 schema) from23To24, required Future Function(i1.Migrator m, Schema25 schema) from24To25, required Future Function(i1.Migrator m, Schema26 schema) from25To26, + required Future Function(i1.Migrator m, Schema27 schema) from26To27, }) => i0.VersionedSchema.stepByStepHelper( step: migrationSteps( from1To2: from1To2, @@ -13752,5 +14303,6 @@ i1.OnUpgrade stepByStep({ from23To24: from23To24, from24To25: from24To25, from25To26: from25To26, + from26To27: from26To27, ), ); diff --git a/mobile/lib/infrastructure/repositories/metadata.repository.dart b/mobile/lib/infrastructure/repositories/settings.repository.dart similarity index 61% rename from mobile/lib/infrastructure/repositories/metadata.repository.dart rename to mobile/lib/infrastructure/repositories/settings.repository.dart index 1300376464..56066f543a 100644 --- a/mobile/lib/infrastructure/repositories/metadata.repository.dart +++ b/mobile/lib/infrastructure/repositories/settings.repository.dart @@ -1,21 +1,21 @@ import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/config/app_config.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; -import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart'; +import 'package:immich_mobile/domain/models/settings_key.dart'; +import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -class MetadataRepository extends DriftDatabaseRepository { +class SettingsRepository extends DriftDatabaseRepository { final Drift _db; - MetadataRepository._(this._db) : super(_db); + SettingsRepository._(this._db) : super(_db); - static MetadataRepository? _instance; + static SettingsRepository? _instance; - static MetadataRepository get instance { + static SettingsRepository get instance { final instance = _instance; if (instance == null) { - throw StateError('MetadataRepository not initialized. Call ensureInitialized() first'); + throw StateError('SettingsRepository not initialized. Call ensureInitialized() first'); } return instance; } @@ -23,31 +23,31 @@ class MetadataRepository extends DriftDatabaseRepository { AppConfig _appConfig = const .new(); AppConfig get appConfig => _appConfig; - static Future ensureInitialized(Drift db) async { + static Future ensureInitialized(Drift db) async { if (_instance == null) { - final instance = MetadataRepository._(db); + final instance = SettingsRepository._(db); await instance.refresh(); _instance = instance; } return _instance!; } - Future refresh() async => _applyOverrides(await _db.select(_db.metadataEntity).get()); + Future refresh() async => _applyOverrides(await _db.select(_db.settingsEntity).get()); - Future clear(Iterable keys) async { + Future clear(Iterable keys) async { if (keys.isEmpty) { return; } final names = keys.map((key) => key.name).toList(); - await (_db.delete(_db.metadataEntity)..where((row) => row.key.isIn(names))).go(); + await (_db.delete(_db.settingsEntity)..where((row) => row.key.isIn(names))).go(); for (final key in keys) { _appConfig = _appConfig.write(key, defaultConfig.read(key)); } } - Future write(MetadataKey key, U value) async { + Future write(SettingsKey key, U value) async { if (value == _appConfig.read(key)) { return; } @@ -57,22 +57,22 @@ class MetadataRepository extends DriftDatabaseRepository { } await _db - .into(_db.metadataEntity) + .into(_db.settingsEntity) .insertOnConflictUpdate( - MetadataEntityCompanion.insert(key: key.name, value: key.encode(value), updatedAt: Value(DateTime.now())), + SettingsEntityCompanion.insert(key: key.name, value: key.encode(value), updatedAt: Value(DateTime.now())), ); _appConfig = _appConfig.write(key, value); } - Stream watchConfig() => _db.select(_db.metadataEntity).watch().map((rows) { + Stream watchConfig() => _db.select(_db.settingsEntity).watch().map((rows) { _applyOverrides(rows); return _appConfig; }); - void _applyOverrides(List rows) { + void _applyOverrides(List rows) { _appConfig = AppConfig.fromEntries( rows.fold({}, (overrides, row) { - final metadataKey = MetadataKey.values.firstWhereOrNull((key) => key.name == row.key); + final metadataKey = SettingsKey.values.firstWhereOrNull((key) => key.name == row.key); if (metadataKey == null) { return overrides; } diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 19455be61c..cc5f131572 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -25,7 +25,7 @@ import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; diff --git a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart index c9398febc6..f999e7671f 100644 --- a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart @@ -8,11 +8,11 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/services/sync_linked_album.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/backup/drift_album_info_list_tile.dart'; @@ -103,7 +103,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState p.totalCount)); final totalChanged = currentTotalAssetCount != _initialTotalAssetCount; diff --git a/mobile/lib/pages/backup/drift_backup_options.page.dart b/mobile/lib/pages/backup/drift_backup_options.page.dart index ee26d0bf87..42643a0496 100644 --- a/mobile/lib/pages/backup/drift_backup_options.page.dart +++ b/mobile/lib/pages/backup/drift_backup_options.page.dart @@ -4,10 +4,10 @@ import 'package:auto_route/auto_route.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/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart'; import 'package:logging/logging.dart'; @@ -27,7 +27,7 @@ class DriftBackupOptionsPage extends ConsumerWidget { // There is an issue with Flutter where the pop event // can be triggered multiple times, so we guard it with _hasPopped - final currentBackup = ref.read(metadataProvider).appConfig.backup; + final currentBackup = ref.read(appConfigProvider).backup; final currentCellularForVideos = currentBackup.useCellularForVideos; final currentCellularForPhotos = currentBackup.useCellularForPhotos; @@ -45,7 +45,7 @@ class DriftBackupOptionsPage extends ConsumerWidget { } await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id); - final isBackupEnabled = MetadataRepository.instance.appConfig.backup.enabled; + final isBackupEnabled = SettingsRepository.instance.appConfig.backup.enabled; if (!isBackupEnabled) { return; } diff --git a/mobile/lib/pages/common/headers_settings.page.dart b/mobile/lib/pages/common/headers_settings.page.dart index d342add5af..9a6b602b04 100644 --- a/mobile/lib/pages/common/headers_settings.page.dart +++ b/mobile/lib/pages/common/headers_settings.page.dart @@ -3,10 +3,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/domain/models/metadata_key.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; class SettingsHeader { String key = ""; @@ -22,7 +21,7 @@ class HeaderSettingsPage extends HookConsumerWidget { final headers = useState>([]); final setInitialHeaders = useState(false); - final storedHeaders = ref.read(metadataProvider).appConfig.network.customHeaders; + final storedHeaders = ref.read(appConfigProvider).network.customHeaders; if (!setInitialHeaders.value) { storedHeaders.forEach((k, v) { final header = SettingsHeader(); @@ -94,7 +93,7 @@ class HeaderSettingsPage extends HookConsumerWidget { headersMap[key] = value; } - await ref.read(metadataProvider).write(MetadataKey.networkCustomHeaders, headersMap); + await ref.read(settingsProvider).write(.networkCustomHeaders, headersMap); await ref.read(apiServiceProvider).updateHeaders(); } } diff --git a/mobile/lib/pages/common/splash_screen.page.dart b/mobile/lib/pages/common/splash_screen.page.dart index 36fd9f5bf0..aaa9fffc05 100644 --- a/mobile/lib/pages/common/splash_screen.page.dart +++ b/mobile/lib/pages/common/splash_screen.page.dart @@ -12,7 +12,7 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart'; import 'package:immich_mobile/generated/translations.g.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; @@ -341,7 +341,7 @@ class SplashScreenPageState extends ConsumerState { await backgroundManager.hashAssets(); } - if (MetadataRepository.instance.appConfig.backup.syncAlbums) { + if (SettingsRepository.instance.appConfig.backup.syncAlbums) { await backgroundManager.syncLinkedAlbum(); } } catch (e) { @@ -370,7 +370,7 @@ class SplashScreenPageState extends ConsumerState { } Future _resumeBackup(DriftBackupNotifier notifier) async { - final isEnableBackup = MetadataRepository.instance.appConfig.backup.enabled; + final isEnableBackup = SettingsRepository.instance.appConfig.backup.enabled; if (isEnableBackup) { final currentUser = Store.tryGet(StoreKey.currentUser); diff --git a/mobile/lib/presentation/pages/drift_slideshow.page.dart b/mobile/lib/presentation/pages/drift_slideshow.page.dart index 4c4ee48cf9..3a5f95554c 100644 --- a/mobile/lib/presentation/pages/drift_slideshow.page.dart +++ b/mobile/lib/presentation/pages/drift_slideshow.page.dart @@ -17,7 +17,7 @@ import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.wid import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; import 'package:immich_mobile/widgets/photo_view/photo_view.dart'; diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index 6241623978..981fe2b1f8 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -15,12 +15,11 @@ 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/domain/models/metadata_key.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.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'; @@ -58,7 +57,7 @@ class _AlbumSelectorState extends ConsumerState { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { - final albumConfig = ref.read(metadataProvider).appConfig.album; + final albumConfig = ref.read(appConfigProvider).album; setState(() { sort = AlbumSort(mode: albumConfig.sortMode, isReverse: albumConfig.isReverse); @@ -94,7 +93,7 @@ class _AlbumSelectorState extends ConsumerState { setState(() { isGrid = !isGrid; }); - ref.read(metadataProvider).write(MetadataKey.albumIsGrid, isGrid); + ref.read(settingsProvider).write(.albumIsGrid, isGrid); } void changeFilter(QuickFilterMode mode) { @@ -110,9 +109,9 @@ class _AlbumSelectorState extends ConsumerState { this.sort = sort; }); - final metadata = ref.read(metadataProvider); - await metadata.write(MetadataKey.albumSortMode, sort.mode); - await metadata.write(MetadataKey.albumIsReverse, sort.isReverse); + final metadata = ref.read(settingsProvider); + await metadata.write(.albumSortMode, sort.mode); + await metadata.write(.albumIsReverse, sort.isReverse); await sortAlbums(); } diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart index 0afc1b781c..84edc4df65 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart @@ -19,7 +19,7 @@ import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; import 'package:immich_mobile/widgets/photo_view/photo_view.dart'; @@ -241,7 +241,7 @@ class _AssetPageState extends ConsumerState { return; } - final tapToNavigate = ref.read(metadataProvider).appConfig.viewer.tapToNavigate; + final tapToNavigate = ref.read(appConfigProvider).viewer.tapToNavigate; if (!tapToNavigate) { _viewer.toggleControls(); return; diff --git a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart index 97ca8ace10..c1e6fe10e6 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart @@ -12,7 +12,7 @@ import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.pro import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:logging/logging.dart'; import 'package:native_video_player/native_video_player.dart'; @@ -128,7 +128,7 @@ class _NativeVideoViewerState extends ConsumerState with Widg final remoteId = (videoAsset as RemoteAsset).id; final serverEndpoint = Store.get(StoreKey.serverEndpoint); - final isOriginalVideo = ref.read(metadataProvider).appConfig.viewer.loadOriginalVideo; + final isOriginalVideo = ref.read(appConfigProvider).viewer.loadOriginalVideo; final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback'; final String videoUrl = videoAsset.livePhotoVideoId != null ? '$serverEndpoint/assets/${videoAsset.livePhotoVideoId}/$postfixUrl' @@ -161,7 +161,7 @@ class _NativeVideoViewerState extends ConsumerState with Widg return; } - final autoPlayVideo = ref.read(metadataProvider).appConfig.viewer.autoPlayVideo; + final autoPlayVideo = ref.read(appConfigProvider).viewer.autoPlayVideo; if (autoPlayVideo || widget.asset.isMotionPhoto) { await _notifier.play(); } @@ -212,7 +212,7 @@ class _NativeVideoViewerState extends ConsumerState with Widg } await _notifier.load(source); - final loopVideo = ref.read(metadataProvider).appConfig.viewer.loopVideo; + final loopVideo = ref.read(appConfigProvider).viewer.loopVideo; await _notifier.setLoop(!widget.asset.isMotionPhoto && loopVideo); await _notifier.setVolume(1); } diff --git a/mobile/lib/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart index 5a79485daf..418d41e1f2 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/setting.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart'; @@ -34,7 +33,7 @@ class ViewerKebabMenu extends ConsumerWidget { final isInLockedView = ref.watch(inLockedViewProvider); final currentAlbum = ref.watch(currentRemoteAlbumProvider); final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive; - final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting); + final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(.advancedTroubleshooting); final actionContext = ActionButtonContext( asset: asset, diff --git a/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart b/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart index 708d3a9879..3bf48783ea 100644 --- a/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart +++ b/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; class BackupToggleButton extends ConsumerStatefulWidget { final VoidCallback onStart; @@ -31,7 +30,7 @@ class BackupToggleButtonState extends ConsumerState with Sin end: 1, ).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeInOut)); - _isEnabled = ref.read(metadataProvider).appConfig.backup.enabled; + _isEnabled = ref.read(appConfigProvider).backup.enabled; } @override @@ -41,7 +40,7 @@ class BackupToggleButtonState extends ConsumerState with Sin } Future _onToggle(bool value) async { - await ref.read(metadataProvider).write(MetadataKey.backupEnabled, value); + await ref.read(settingsProvider).write(.backupEnabled, value); setState(() { _isEnabled = value; diff --git a/mobile/lib/presentation/widgets/images/image_provider.dart b/mobile/lib/presentation/widgets/images/image_provider.dart index 9364fdd091..36d9678277 100644 --- a/mobile/lib/presentation/widgets/images/image_provider.dart +++ b/mobile/lib/presentation/widgets/images/image_provider.dart @@ -4,7 +4,7 @@ import 'package:async/async.dart'; import 'package:flutter/widgets.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/infrastructure/loaders/image_request.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/presentation/widgets/timeline/constants.dart'; @@ -189,5 +189,5 @@ ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnai bool _shouldUseLocalAsset(BaseAsset asset) => asset.hasLocal && - (!asset.hasRemote || !MetadataRepository.instance.appConfig.image.preferRemote) && + (!asset.hasRemote || !SettingsRepository.instance.appConfig.image.preferRemote) && !asset.isEdited; diff --git a/mobile/lib/presentation/widgets/images/local_image_provider.dart b/mobile/lib/presentation/widgets/images/local_image_provider.dart index 6376e07405..eba4f0a1cd 100644 --- a/mobile/lib/presentation/widgets/images/local_image_provider.dart +++ b/mobile/lib/presentation/widgets/images/local_image_provider.dart @@ -2,7 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/infrastructure/loaders/image_request.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/presentation/widgets/images/animated_image_stream_completer.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/one_frame_multi_image_stream_completer.dart'; @@ -104,7 +104,7 @@ class LocalFullImageProvider extends CancellableImageProvider { } void switchFavoriteOnly(bool isFavoriteOnly) { - ref.read(metadataProvider).write(MetadataKey.mapShowFavoriteOnly, isFavoriteOnly); + ref.read(settingsProvider).write(.mapShowFavoriteOnly, isFavoriteOnly); state = state.copyWith(onlyFavorites: isFavoriteOnly); EventStream.shared.emit(const MapMarkerReloadEvent()); } void switchIncludeArchived(bool isIncludeArchived) { - ref.read(metadataProvider).write(MetadataKey.mapIncludeArchived, isIncludeArchived); + ref.read(settingsProvider).write(.mapIncludeArchived, isIncludeArchived); state = state.copyWith(includeArchived: isIncludeArchived); EventStream.shared.emit(const MapMarkerReloadEvent()); } void switchWithPartners(bool isWithPartners) { - ref.read(metadataProvider).write(MetadataKey.mapWithPartners, isWithPartners); + ref.read(settingsProvider).write(.mapWithPartners, isWithPartners); state = state.copyWith(withPartners: isWithPartners); EventStream.shared.emit(const MapMarkerReloadEvent()); } void setRelativeTime(int relativeDays) { - ref.read(metadataProvider).write(MetadataKey.mapRelativeDate, relativeDays); + ref.read(settingsProvider).write(.mapRelativeDate, relativeDays); state = state.copyWith(relativeDays: relativeDays); EventStream.shared.emit(const MapMarkerReloadEvent()); } diff --git a/mobile/lib/presentation/widgets/timeline/timeline.state.dart b/mobile/lib/presentation/widgets/timeline/timeline.state.dart index 7b88800f22..8dd87f9868 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.state.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.state.dart @@ -5,7 +5,7 @@ import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/presentation/widgets/timeline/constants.dart'; import 'package:immich_mobile/presentation/widgets/timeline/fixed/segment_builder.dart'; import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; class TimelineArgs { diff --git a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart index de52c047a2..eb7a31ac8b 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart @@ -10,7 +10,6 @@ import 'package:flutter/rendering.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/events.model.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; @@ -22,7 +21,7 @@ import 'package:immich_mobile/presentation/widgets/timeline/scrubber.widget.dart import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline_drag_region.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; @@ -459,7 +458,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { _restoreAssetIndex = targetAssetIndex; }); - ref.read(metadataProvider).write(MetadataKey.timelineTilesPerRow, _perRow); + ref.read(settingsProvider).write(.timelineTilesPerRow, _perRow); } }; }, diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index 3b40ca2f63..5cd294d781 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -9,8 +9,8 @@ import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/notification_permission.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; @@ -107,7 +107,7 @@ class AppLifeCycleNotifier extends StateNotifier { await Future.delayed(const Duration(milliseconds: 500)); final backgroundManager = _ref.read(backgroundSyncProvider); - final isAlbumLinkedSyncEnable = _ref.read(metadataProvider).appConfig.backup.syncAlbums; + final isAlbumLinkedSyncEnable = _ref.read(appConfigProvider).backup.syncAlbums; try { bool syncSuccess = false; @@ -137,7 +137,7 @@ class AppLifeCycleNotifier extends StateNotifier { } Future _resumeBackup() async { - final isEnableBackup = _ref.read(metadataProvider).appConfig.backup.enabled; + final isEnableBackup = _ref.read(appConfigProvider).backup.enabled; if (isEnableBackup) { final currentUser = Store.tryGet(StoreKey.currentUser); diff --git a/mobile/lib/providers/auth.provider.dart b/mobile/lib/providers/auth.provider.dart index 0bd5783fee..8a3f503553 100644 --- a/mobile/lib/providers/auth.provider.dart +++ b/mobile/lib/providers/auth.provider.dart @@ -3,7 +3,6 @@ import 'dart:convert'; import 'package:flutter_udid/flutter_udid.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; @@ -11,7 +10,7 @@ import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/models/auth/auth_state.model.dart'; import 'package:immich_mobile/models/auth/login_response.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/auth.service.dart'; @@ -130,7 +129,7 @@ class AuthNotifier extends StateNotifier { await _apiService.updateHeaders(); final serverEndpoint = Store.get(StoreKey.serverEndpoint); - final headerMap = _ref.read(metadataProvider).appConfig.network.customHeaders; + final headerMap = _ref.read(appConfigProvider).network.customHeaders; final customHeaders = headerMap.isEmpty ? null : jsonEncode(headerMap); await _widgetService.writeCredentials(serverEndpoint, accessToken, customHeaders); @@ -179,19 +178,19 @@ class AuthNotifier extends StateNotifier { } Future saveWifiName(String wifiName) async { - await _ref.read(metadataProvider).write(MetadataKey.networkPreferredWifiName, wifiName); + await _ref.read(settingsProvider).write(.networkPreferredWifiName, wifiName); } Future saveLocalEndpoint(String url) async { - await _ref.read(metadataProvider).write(MetadataKey.networkLocalEndpoint, url); + await _ref.read(settingsProvider).write(.networkLocalEndpoint, url); } String? getSavedWifiName() { - return _ref.read(metadataProvider).appConfig.network.preferredWifiName; + return _ref.read(appConfigProvider).network.preferredWifiName; } String? getSavedLocalEndpoint() { - return _ref.read(metadataProvider).appConfig.network.localEndpoint; + return _ref.read(appConfigProvider).network.localEndpoint; } /// Returns the current server endpoint (with /api) URL from the store diff --git a/mobile/lib/providers/cleanup.provider.dart b/mobile/lib/providers/cleanup.provider.dart index e4a3d10a15..378ceb010f 100644 --- a/mobile/lib/providers/cleanup.provider.dart +++ b/mobile/lib/providers/cleanup.provider.dart @@ -1,8 +1,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/services/cleanup.service.dart'; @@ -54,21 +54,21 @@ final cleanupProvider = StateNotifierProvider((re return CleanupNotifier( ref.watch(cleanupServiceProvider), ref.watch(currentUserProvider)?.id, - ref.watch(metadataProvider), + ref.watch(settingsProvider), ); }); class CleanupNotifier extends StateNotifier { final CleanupService _cleanupService; final String? _userId; - final MetadataRepository _metadataRepository; + final SettingsRepository _settingsRepository; - CleanupNotifier(this._cleanupService, this._userId, this._metadataRepository) : super(const CleanupState()) { + CleanupNotifier(this._cleanupService, this._userId, this._settingsRepository) : super(const CleanupState()) { _loadPersistedSettings(); } void _loadPersistedSettings() { - final cleanup = _metadataRepository.appConfig.cleanup; + final cleanup = _settingsRepository.appConfig.cleanup; final keepFavorites = cleanup.keepFavorites; final keepMediaType = cleanup.keepMediaType; final keepAlbumIds = cleanup.keepAlbumIds.toSet(); @@ -87,18 +87,18 @@ class CleanupNotifier extends StateNotifier { state = state.copyWith(selectedDate: date, assetsToDelete: []); if (date != null) { final daysAgo = DateTime.now().difference(date).inDays; - _metadataRepository.write(.cleanupCutoffDaysAgo, daysAgo); + _settingsRepository.write(.cleanupCutoffDaysAgo, daysAgo); } } void setKeepMediaType(AssetKeepType keepMediaType) { state = state.copyWith(keepMediaType: keepMediaType, assetsToDelete: []); - _metadataRepository.write(.cleanupKeepMediaType, keepMediaType); + _settingsRepository.write(.cleanupKeepMediaType, keepMediaType); } void setKeepFavorites(bool keepFavorites) { state = state.copyWith(keepFavorites: keepFavorites, assetsToDelete: []); - _metadataRepository.write(.cleanupKeepFavorites, keepFavorites); + _settingsRepository.write(.cleanupKeepFavorites, keepFavorites); } void toggleKeepAlbum(String albumId) { @@ -118,7 +118,7 @@ class CleanupNotifier extends StateNotifier { } void _persistExcludedAlbumIds(Set albumIds) { - _metadataRepository.write(.cleanupKeepAlbumIds, albumIds.toList()); + _settingsRepository.write(.cleanupKeepAlbumIds, albumIds.toList()); } void cleanupStaleAlbumIds(Set existingAlbumIds) { @@ -131,7 +131,7 @@ class CleanupNotifier extends StateNotifier { } void applyDefaultAlbumSelections(List<(String id, String name)> albums) { - final isInitialized = _metadataRepository.appConfig.cleanup.defaultsInitialized; + final isInitialized = _settingsRepository.appConfig.cleanup.defaultsInitialized; if (isInitialized) { return; } @@ -144,7 +144,7 @@ class CleanupNotifier extends StateNotifier { _persistExcludedAlbumIds(keepAlbumIds); } - _metadataRepository.write(.cleanupDefaultsInitialized, true); + _settingsRepository.write(.cleanupDefaultsInitialized, true); } Future scanAssets() async { diff --git a/mobile/lib/providers/infrastructure/metadata.provider.dart b/mobile/lib/providers/infrastructure/settings.provider.dart similarity index 59% rename from mobile/lib/providers/infrastructure/metadata.provider.dart rename to mobile/lib/providers/infrastructure/settings.provider.dart index d9e6920d62..d2b9dce1d6 100644 --- a/mobile/lib/providers/infrastructure/metadata.provider.dart +++ b/mobile/lib/providers/infrastructure/settings.provider.dart @@ -1,11 +1,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/config/app_config.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; -final metadataProvider = Provider.autoDispose((_) => MetadataRepository.instance); +final settingsProvider = Provider.autoDispose((_) => SettingsRepository.instance); final appConfigProvider = Provider.autoDispose((ref) { - final repo = ref.watch(metadataProvider); + final repo = ref.watch(settingsProvider); final subscription = repo.watchConfig().listen((event) => ref.state = event); ref.onDispose(subscription.cancel); return repo.appConfig; diff --git a/mobile/lib/providers/infrastructure/timeline.provider.dart b/mobile/lib/providers/infrastructure/timeline.provider.dart index 9f2fdec519..b22c693033 100644 --- a/mobile/lib/providers/infrastructure/timeline.provider.dart +++ b/mobile/lib/providers/infrastructure/timeline.provider.dart @@ -3,7 +3,7 @@ import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; final timelineRepositoryProvider = Provider( @@ -29,7 +29,7 @@ final timelineServiceProvider = Provider( final timelineFactoryProvider = Provider( (ref) => TimelineFactory( timelineRepository: ref.watch(timelineRepositoryProvider), - metadataRepository: ref.watch(metadataProvider), + settingsRepository: ref.watch(settingsProvider), ), ); diff --git a/mobile/lib/providers/map/map_state.provider.dart b/mobile/lib/providers/map/map_state.provider.dart index b0a59f6a1e..b643264dca 100644 --- a/mobile/lib/providers/map/map_state.provider.dart +++ b/mobile/lib/providers/map/map_state.provider.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; import 'package:immich_mobile/models/map/map_state.model.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; final mapStateNotifierProvider = NotifierProvider(MapStateNotifier.new); @@ -27,12 +26,12 @@ class MapStateNotifier extends Notifier { } void switchTheme(ThemeMode mode) { - ref.read(metadataProvider).write(MetadataKey.mapThemeMode, mode); + ref.read(settingsProvider).write(.mapThemeMode, mode); state = state.copyWith(themeMode: mode); } void switchFavoriteOnly(bool isFavoriteOnly) { - ref.read(metadataProvider).write(MetadataKey.mapShowFavoriteOnly, isFavoriteOnly); + ref.read(settingsProvider).write(.mapShowFavoriteOnly, isFavoriteOnly); state = state.copyWith(showFavoriteOnly: isFavoriteOnly, shouldRefetchMarkers: true); } @@ -41,17 +40,17 @@ class MapStateNotifier extends Notifier { } void switchIncludeArchived(bool isIncludeArchived) { - ref.read(metadataProvider).write(MetadataKey.mapIncludeArchived, isIncludeArchived); + ref.read(settingsProvider).write(.mapIncludeArchived, isIncludeArchived); state = state.copyWith(includeArchived: isIncludeArchived, shouldRefetchMarkers: true); } void switchWithPartners(bool isWithPartners) { - ref.read(metadataProvider).write(MetadataKey.mapWithPartners, isWithPartners); + ref.read(settingsProvider).write(.mapWithPartners, isWithPartners); state = state.copyWith(withPartners: isWithPartners, shouldRefetchMarkers: true); } void setRelativeTime(int relativeTime) { - ref.read(metadataProvider).write(MetadataKey.mapRelativeDate, relativeTime); + ref.read(settingsProvider).write(.mapRelativeDate, relativeTime); state = state.copyWith(relativeTime: relativeTime, shouldRefetchMarkers: true); } } diff --git a/mobile/lib/providers/theme.provider.dart b/mobile/lib/providers/theme.provider.dart index 909b8137c1..962781586e 100644 --- a/mobile/lib/providers/theme.provider.dart +++ b/mobile/lib/providers/theme.provider.dart @@ -1,5 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/theme/color_scheme.dart'; import 'package:immich_mobile/theme/dynamic_theme.dart'; import 'package:immich_mobile/theme/theme_data.dart'; diff --git a/mobile/lib/providers/websocket.provider.dart b/mobile/lib/providers/websocket.provider.dart index a53f0aaaeb..8d9bd5bfe3 100644 --- a/mobile/lib/providers/websocket.provider.dart +++ b/mobile/lib/providers/websocket.provider.dart @@ -7,7 +7,7 @@ import 'package:immich_mobile/infrastructure/repositories/network.repository.dar import 'package:immich_mobile/models/server_info/server_version.model.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/utils/debounce.dart'; import 'package:immich_mobile/utils/debug_print.dart'; @@ -193,7 +193,7 @@ class WebsocketNotifier extends StateNotifier { return; } - final isSyncAlbumEnabled = _ref.read(metadataProvider).appConfig.backup.syncAlbums; + final isSyncAlbumEnabled = _ref.read(appConfigProvider).backup.syncAlbums; try { unawaited( _ref.read(backgroundSyncProvider).syncWebsocketBatchV1(_batchedAssetUploadReady.toList()).then((_) { @@ -214,7 +214,7 @@ class WebsocketNotifier extends StateNotifier { return; } - final isSyncAlbumEnabled = _ref.read(metadataProvider).appConfig.backup.syncAlbums; + final isSyncAlbumEnabled = _ref.read(appConfigProvider).backup.syncAlbums; try { unawaited( _ref.read(backgroundSyncProvider).syncWebsocketBatchV2(_batchedAssetUploadReady.toList()).then((_) { diff --git a/mobile/lib/repositories/auth.repository.dart b/mobile/lib/repositories/auth.repository.dart index 5aca8e76ac..866d587328 100644 --- a/mobile/lib/repositories/auth.repository.dart +++ b/mobile/lib/repositories/auth.repository.dart @@ -4,7 +4,7 @@ import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; final authRepositoryProvider = Provider( (ref) => AuthRepository(ref.watch(driftProvider), ref.watch(appConfigProvider)), diff --git a/mobile/lib/services/api.service.dart b/mobile/lib/services/api.service.dart index b6a2cb1e4b..a0828927ce 100644 --- a/mobile/lib/services/api.service.dart +++ b/mobile/lib/services/api.service.dart @@ -5,7 +5,7 @@ import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/utils/url_helper.dart'; @@ -177,7 +177,7 @@ class ApiService { if (serverEndpoint != null && serverEndpoint.isNotEmpty) { urls.add(serverEndpoint); } - final network = MetadataRepository.instance.appConfig.network; + final network = SettingsRepository.instance.appConfig.network; final localEndpoint = network.localEndpoint; if (localEndpoint.isNotEmpty) { urls.add(localEndpoint); @@ -191,7 +191,7 @@ class ApiService { } static Map getRequestHeaders() { - return MetadataRepository.instance.appConfig.network.customHeaders; + return SettingsRepository.instance.appConfig.network.customHeaders; } ApiClient get apiClient => _apiClient; diff --git a/mobile/lib/services/auth.service.dart b/mobile/lib/services/auth.service.dart index fff72a710e..0de22fd124 100644 --- a/mobile/lib/services/auth.service.dart +++ b/mobile/lib/services/auth.service.dart @@ -1,11 +1,11 @@ import 'dart:async'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; +import 'package:immich_mobile/domain/models/settings_key.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/models/auth/login_response.model.dart'; @@ -100,7 +100,7 @@ class AuthService { _log.severe("Error clearing local data", error, stackTrace); }); - await MetadataRepository.instance.write(MetadataKey.backupEnabled, false); + await SettingsRepository.instance.write(SettingsKey.backupEnabled, false); } } @@ -120,7 +120,7 @@ class AuthService { _authRepository.clearLocalData(), Store.delete(StoreKey.currentUser), Store.delete(StoreKey.accessToken), - MetadataRepository.instance.clear(const [ + SettingsRepository.instance.clear(const [ .networkAutoEndpointSwitching, .networkPreferredWifiName, .networkLocalEndpoint, diff --git a/mobile/lib/services/background_upload.service.dart b/mobile/lib/services/background_upload.service.dart index 37577e3666..903fd02395 100644 --- a/mobile/lib/services/background_upload.service.dart +++ b/mobile/lib/services/background_upload.service.dart @@ -13,7 +13,7 @@ import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; @@ -359,7 +359,7 @@ class BackgroundUploadService { } bool _shouldRequireWiFi(LocalAsset asset) { - final backup = MetadataRepository.instance.appConfig.backup; + final backup = SettingsRepository.instance.appConfig.backup; if (asset.isVideo && backup.useCellularForVideos) { return false; } diff --git a/mobile/lib/services/foreground_upload.service.dart b/mobile/lib/services/foreground_upload.service.dart index 2fc1a92127..ef7f32d168 100644 --- a/mobile/lib/services/foreground_upload.service.dart +++ b/mobile/lib/services/foreground_upload.service.dart @@ -11,7 +11,7 @@ import 'package:immich_mobile/extensions/network_capability_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/platform/connectivity_api.g.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; @@ -451,7 +451,7 @@ class ForegroundUploadService { } bool _shouldRequireWiFi(LocalAsset asset) { - final backup = MetadataRepository.instance.appConfig.backup; + final backup = SettingsRepository.instance.appConfig.backup; if (asset.isVideo && backup.useCellularForVideos) { return false; } diff --git a/mobile/lib/utils/bootstrap.dart b/mobile/lib/utils/bootstrap.dart index 68ebfe9c9f..9bd652381a 100644 --- a/mobile/lib/utils/bootstrap.dart +++ b/mobile/lib/utils/bootstrap.dart @@ -6,7 +6,7 @@ import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -49,11 +49,11 @@ abstract final class Bootstrap { await StoreService.init(storeRepository: storeRepo, listenUpdates: listenStoreUpdates); - final metadataRepo = await MetadataRepository.ensureInitialized(drift); + final settingsRepo = await SettingsRepository.ensureInitialized(drift); await LogService.init( logRepository: LogRepository(logDb), - metadataRepository: metadataRepo, + settingsRepository: settingsRepo, shouldBuffer: shouldBufferLogs, ); diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index e1f40c8751..3afa554e29 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -8,11 +8,11 @@ import 'package:immich_mobile/constants/colors.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/config/app_config.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; +import 'package:immich_mobile/domain/models/settings_key.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; @@ -74,65 +74,65 @@ Future _migrateTo25() async { Future _migrateTo26(Drift drift) async { final migrator = _StoreMigrator(drift); - await migrator.migrateEnumIndex(StoreKey.legacyLogLevel, MetadataKey.logLevel, LogLevel.values); + await migrator.migrateEnumIndex(StoreKey.legacyLogLevel, SettingsKey.logLevel, LogLevel.values); // Theme - await migrator.migrateEnumName(StoreKey.legacyThemeMode, MetadataKey.themeMode, ThemeMode.values); - await migrator.migrateEnumName(StoreKey.legacyPrimaryColor, MetadataKey.themePrimaryColor, ImmichColorPreset.values); - await migrator.migrateBool(StoreKey.legacyDynamicTheme, MetadataKey.themeDynamic); - await migrator.migrateBool(StoreKey.legacyColorfulInterface, MetadataKey.themeColorfulInterface); + await migrator.migrateEnumName(StoreKey.legacyThemeMode, SettingsKey.themeMode, ThemeMode.values); + await migrator.migrateEnumName(StoreKey.legacyPrimaryColor, SettingsKey.themePrimaryColor, ImmichColorPreset.values); + await migrator.migrateBool(StoreKey.legacyDynamicTheme, SettingsKey.themeDynamic); + await migrator.migrateBool(StoreKey.legacyColorfulInterface, SettingsKey.themeColorfulInterface); // Cleanup final cleanupKeepAlbumIds = await migrator.readLegacyStoreString(StoreKey.legacyCleanupKeepAlbumIds.id); if (cleanupKeepAlbumIds != null) { final ids = cleanupKeepAlbumIds.split(',').where((id) => id.isNotEmpty).toList(); - migrator.stage(StoreKey.legacyCleanupKeepAlbumIds, MetadataKey.cleanupKeepAlbumIds, ids); + migrator.stage(StoreKey.legacyCleanupKeepAlbumIds, SettingsKey.cleanupKeepAlbumIds, ids); } - await migrator.migrateBool(StoreKey.legacyCleanupKeepFavorites, MetadataKey.cleanupKeepFavorites); + await migrator.migrateBool(StoreKey.legacyCleanupKeepFavorites, SettingsKey.cleanupKeepFavorites); await migrator.migrateEnumIndex( StoreKey.legacyCleanupKeepMediaType, - MetadataKey.cleanupKeepMediaType, + SettingsKey.cleanupKeepMediaType, AssetKeepType.values, ); - await migrator.migrateInt(StoreKey.legacyCleanupCutoffDaysAgo, MetadataKey.cleanupCutoffDaysAgo); - await migrator.migrateBool(StoreKey.legacyCleanupDefaultsInitialized, MetadataKey.cleanupDefaultsInitialized); + await migrator.migrateInt(StoreKey.legacyCleanupCutoffDaysAgo, SettingsKey.cleanupCutoffDaysAgo); + await migrator.migrateBool(StoreKey.legacyCleanupDefaultsInitialized, SettingsKey.cleanupDefaultsInitialized); // Map - await migrator.migrateBool(StoreKey.legacyMapShowFavoriteOnly, MetadataKey.mapShowFavoriteOnly); - await migrator.migrateInt(StoreKey.legacyMapRelativeDate, MetadataKey.mapRelativeDate); - await migrator.migrateBool(StoreKey.legacyMapIncludeArchived, MetadataKey.mapIncludeArchived); - await migrator.migrateEnumIndex(StoreKey.legacyMapThemeMode, MetadataKey.mapThemeMode, ThemeMode.values); - await migrator.migrateBool(StoreKey.legacyMapwithPartners, MetadataKey.mapWithPartners); + await migrator.migrateBool(StoreKey.legacyMapShowFavoriteOnly, SettingsKey.mapShowFavoriteOnly); + await migrator.migrateInt(StoreKey.legacyMapRelativeDate, SettingsKey.mapRelativeDate); + await migrator.migrateBool(StoreKey.legacyMapIncludeArchived, SettingsKey.mapIncludeArchived); + await migrator.migrateEnumIndex(StoreKey.legacyMapThemeMode, SettingsKey.mapThemeMode, ThemeMode.values); + await migrator.migrateBool(StoreKey.legacyMapwithPartners, SettingsKey.mapWithPartners); // Timeline - await migrator.migrateInt(StoreKey.legacyTilesPerRow, MetadataKey.timelineTilesPerRow); + await migrator.migrateInt(StoreKey.legacyTilesPerRow, SettingsKey.timelineTilesPerRow); await migrator.migrateEnumIndex( StoreKey.legacyGroupAssetsBy, - MetadataKey.timelineGroupAssetsBy, + SettingsKey.timelineGroupAssetsBy, GroupAssetsBy.values, ); - await migrator.migrateBool(StoreKey.legacyStorageIndicator, MetadataKey.timelineStorageIndicator); + await migrator.migrateBool(StoreKey.legacyStorageIndicator, SettingsKey.timelineStorageIndicator); // Image - await migrator.migrateBool(StoreKey.legacyPreferRemoteImage, MetadataKey.imagePreferRemote); - await migrator.migrateBool(StoreKey.legacyLoadOriginal, MetadataKey.imageLoadOriginal); + await migrator.migrateBool(StoreKey.legacyPreferRemoteImage, SettingsKey.imagePreferRemote); + await migrator.migrateBool(StoreKey.legacyLoadOriginal, SettingsKey.imageLoadOriginal); // Viewer - await migrator.migrateBool(StoreKey.legacyLoopVideo, MetadataKey.viewerLoopVideo); - await migrator.migrateBool(StoreKey.legacyLoadOriginalVideo, MetadataKey.viewerLoadOriginalVideo); - await migrator.migrateBool(StoreKey.legacyAutoPlayVideo, MetadataKey.viewerAutoPlayVideo); - await migrator.migrateBool(StoreKey.legacyTapToNavigate, MetadataKey.viewerTapToNavigate); + await migrator.migrateBool(StoreKey.legacyLoopVideo, SettingsKey.viewerLoopVideo); + await migrator.migrateBool(StoreKey.legacyLoadOriginalVideo, SettingsKey.viewerLoadOriginalVideo); + await migrator.migrateBool(StoreKey.legacyAutoPlayVideo, SettingsKey.viewerAutoPlayVideo); + await migrator.migrateBool(StoreKey.legacyTapToNavigate, SettingsKey.viewerTapToNavigate); // Network - await migrator.migrateBool(StoreKey.legacyAutoEndpointSwitching, MetadataKey.networkAutoEndpointSwitching); - await migrator.migrateString(StoreKey.legacyPreferredWifiName, MetadataKey.networkPreferredWifiName); - await migrator.migrateString(StoreKey.legacyLocalEndpoint, MetadataKey.networkLocalEndpoint); + await migrator.migrateBool(StoreKey.legacyAutoEndpointSwitching, SettingsKey.networkAutoEndpointSwitching); + await migrator.migrateString(StoreKey.legacyPreferredWifiName, SettingsKey.networkPreferredWifiName); + await migrator.migrateString(StoreKey.legacyLocalEndpoint, SettingsKey.networkLocalEndpoint); await _migrateExternalEndpointList(migrator); await _migrateCustomHeaders(migrator); // Album await _migrateAlbumSortMode(migrator); - await migrator.migrateBool(StoreKey.legacySelectedAlbumSortReverse, MetadataKey.albumIsReverse); - await migrator.migrateBool(StoreKey.legacyAlbumGridView, MetadataKey.albumIsGrid); + await migrator.migrateBool(StoreKey.legacySelectedAlbumSortReverse, SettingsKey.albumIsReverse); + await migrator.migrateBool(StoreKey.legacyAlbumGridView, SettingsKey.albumIsGrid); // Backup - await migrator.migrateBool(StoreKey.legacyEnableBackup, MetadataKey.backupEnabled); - await migrator.migrateBool(StoreKey.legacyUseWifiForUploadVideos, MetadataKey.backupUseCellularForVideos); - await migrator.migrateBool(StoreKey.legacyUseWifiForUploadPhotos, MetadataKey.backupUseCellularForPhotos); - await migrator.migrateBool(StoreKey.legacyBackupRequireCharging, MetadataKey.backupRequireCharging); - await migrator.migrateInt(StoreKey.legacyBackupTriggerDelay, MetadataKey.backupTriggerDelay); - await migrator.migrateBool(StoreKey.legacySyncAlbums, MetadataKey.backupSyncAlbums); + await migrator.migrateBool(StoreKey.legacyEnableBackup, SettingsKey.backupEnabled); + await migrator.migrateBool(StoreKey.legacyUseWifiForUploadVideos, SettingsKey.backupUseCellularForVideos); + await migrator.migrateBool(StoreKey.legacyUseWifiForUploadPhotos, SettingsKey.backupUseCellularForPhotos); + await migrator.migrateBool(StoreKey.legacyBackupRequireCharging, SettingsKey.backupRequireCharging); + await migrator.migrateInt(StoreKey.legacyBackupTriggerDelay, SettingsKey.backupTriggerDelay); + await migrator.migrateBool(StoreKey.legacySyncAlbums, SettingsKey.backupSyncAlbums); await migrator.complete(); } @@ -143,7 +143,7 @@ Future _migrateAlbumSortMode(_StoreMigrator migrator) async { return; } - migrator.stage(StoreKey.legacySelectedAlbumSortOrder, MetadataKey.albumSortMode, mode); + migrator.stage(StoreKey.legacySelectedAlbumSortOrder, SettingsKey.albumSortMode, mode); } Future _migrateExternalEndpointList(_StoreMigrator migrator) async { @@ -167,7 +167,7 @@ Future _migrateExternalEndpointList(_StoreMigrator migrator) async { // ignore invalid entries } - migrator.stage(StoreKey.legacyExternalEndpointList, MetadataKey.networkExternalEndpointList, urls); + migrator.stage(StoreKey.legacyExternalEndpointList, SettingsKey.networkExternalEndpointList, urls); } Future _migrateCustomHeaders(_StoreMigrator migrator) async { @@ -190,17 +190,17 @@ Future _migrateCustomHeaders(_StoreMigrator migrator) async { // ignore invalid entries } - migrator.stage(StoreKey.legacyCustomHeaders, MetadataKey.networkCustomHeaders, headers); + migrator.stage(StoreKey.legacyCustomHeaders, SettingsKey.networkCustomHeaders, headers); } class _StoreMigrator { final Drift _db; - final Map, Object> _cache = {}; + final Map, Object> _cache = {}; final List _migratedStoreIds = []; _StoreMigrator(this._db); - Future migrateEnumIndex(StoreKey legacyKey, MetadataKey newKey, List values) async { + Future migrateEnumIndex(StoreKey legacyKey, SettingsKey newKey, List values) async { final index = await readLegacyStoreInt(legacyKey.id); if (index == null) { return; @@ -217,7 +217,7 @@ class _StoreMigrator { Future migrateEnumName( StoreKey legacyKey, - MetadataKey newKey, + SettingsKey newKey, List values, ) async { final name = await readLegacyStoreString(legacyKey.id); @@ -234,7 +234,7 @@ class _StoreMigrator { _migratedStoreIds.add(legacyKey.id); } - Future migrateBool(StoreKey legacyKey, MetadataKey newKey) async { + Future migrateBool(StoreKey legacyKey, SettingsKey newKey) async { final intValue = await readLegacyStoreInt(legacyKey.id); if (intValue == null) { return; @@ -245,7 +245,7 @@ class _StoreMigrator { _migratedStoreIds.add(legacyKey.id); } - Future migrateInt(StoreKey legacyKey, MetadataKey newKey) async { + Future migrateInt(StoreKey legacyKey, SettingsKey newKey) async { final intValue = await readLegacyStoreInt(legacyKey.id); if (intValue == null) { return; @@ -255,7 +255,7 @@ class _StoreMigrator { _migratedStoreIds.add(legacyKey.id); } - Future migrateString(StoreKey legacyKey, MetadataKey newKey) async { + Future migrateString(StoreKey legacyKey, SettingsKey newKey) async { final value = await readLegacyStoreString(legacyKey.id); if (value == null) { return; @@ -265,7 +265,7 @@ class _StoreMigrator { _migratedStoreIds.add(legacyKey.id); } - void stage(StoreKey legacyKey, MetadataKey newKey, T value) { + void stage(StoreKey legacyKey, SettingsKey newKey, T value) { _cache[newKey] = value; _migratedStoreIds.add(legacyKey.id); } @@ -277,8 +277,8 @@ class _StoreMigrator { continue; } batch.insert( - _db.metadataEntity, - MetadataEntityCompanion(key: Value(entry.key.name), value: Value(entry.key.encode(entry.value))), + _db.settingsEntity, + SettingsEntityCompanion(key: Value(entry.key.name), value: Value(entry.key.encode(entry.value))), mode: InsertMode.insertOrReplace, ); } diff --git a/mobile/lib/widgets/common/immich_sliver_app_bar.dart b/mobile/lib/widgets/common/immich_sliver_app_bar.dart index 32aa766dec..a81082753a 100644 --- a/mobile/lib/widgets/common/immich_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/immich_sliver_app_bar.dart @@ -10,7 +10,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/server_info/server_info.model.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/sync_status.provider.dart'; diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index f64e7cc197..090c9bb2b8 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -15,7 +15,7 @@ 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/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; @@ -187,7 +187,7 @@ class LoginForm extends HookConsumerWidget { await backgroundManager.syncRemote(); await backgroundManager.hashAssets(); - if (MetadataRepository.instance.appConfig.backup.syncAlbums) { + if (SettingsRepository.instance.appConfig.backup.syncAlbums) { await backgroundManager.syncLinkedAlbum(); } } diff --git a/mobile/lib/widgets/settings/advanced_settings.dart b/mobile/lib/widgets/settings/advanced_settings.dart index b4b24a43e6..542a7cc5e2 100644 --- a/mobile/lib/widgets/settings/advanced_settings.dart +++ b/mobile/lib/widgets/settings/advanced_settings.dart @@ -7,7 +7,7 @@ import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/repositories/permission.repository.dart'; @@ -35,7 +35,7 @@ class AdvancedSettings extends HookConsumerWidget { final preferRemote = useState(ref.read(appConfigProvider).image.preferRemote); useValueChanged( preferRemote.value, - (_, __) => ref.read(metadataProvider).write(.imagePreferRemote, preferRemote.value), + (_, __) => ref.read(settingsProvider).write(.imagePreferRemote, preferRemote.value), ); final readonlyModeEnabled = useAppSettingsState(AppSettingsEnum.readonlyModeEnabled); diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart index b9f81da79e..59adb335bb 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart @@ -3,11 +3,10 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_radio_list_tile.dart'; @@ -19,7 +18,7 @@ class GroupSettings extends HookConsumerWidget { final groupBy = useValueNotifier(ref.watch(appConfigProvider.select((s) => s.timeline.groupAssetsBy))); Future updateAppSettings(GroupAssetsBy groupBy) async { - await ref.read(metadataProvider).write(MetadataKey.timelineGroupAssetsBy, groupBy); + await ref.read(settingsProvider).write(.timelineGroupAssetsBy, groupBy); ref.invalidate(appSettingsServiceProvider); } diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart index 20025286f4..f915df04f8 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart @@ -2,10 +2,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/domain/models/metadata_key.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart'; @@ -16,7 +15,7 @@ class LayoutSettings extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final tilesPerRow = useState(ref.read(appConfigProvider.select((s) => s.timeline.tilesPerRow))); useValueChanged(tilesPerRow.value, (_, __) { - ref.read(metadataProvider).write(MetadataKey.timelineTilesPerRow, tilesPerRow.value); + ref.read(settingsProvider).write(.timelineTilesPerRow, tilesPerRow.value); }); return Column( diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart index 21d751c26f..3ac72d6612 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart @@ -2,10 +2,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/setting.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_group_settings.dart'; import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_layout_settings.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart'; @@ -23,7 +21,7 @@ class AssetListSettings extends HookConsumerWidget { valueNotifier: storageIndicator, title: 'theme_setting_asset_list_storage_indicator_title'.tr(), onChanged: (value) { - ref.read(metadataProvider).write(MetadataKey.timelineStorageIndicator, value); + ref.read(settingsProvider).write(.timelineStorageIndicator, value); ref.invalidate(appSettingsServiceProvider); ref.invalidate(settingsProvider); }, diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart index 7858033401..f65af6af9d 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart @@ -3,7 +3,7 @@ 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/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; @@ -14,7 +14,7 @@ class ImageViewerQualitySetting extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final isOriginal = useState(ref.read(appConfigProvider).image.loadOriginal); useValueChanged(isOriginal.value, (_, __) { - ref.read(metadataProvider).write(.imageLoadOriginal, isOriginal.value); + ref.read(settingsProvider).write(.imageLoadOriginal, isOriginal.value); }); return Column( diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart index 5af64b0be9..730521e3c1 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart @@ -2,7 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; @@ -13,7 +13,7 @@ class ImageViewerTapToNavigateSetting extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final tapToNavigate = useState(ref.read(appConfigProvider).viewer.tapToNavigate); useValueChanged(tapToNavigate.value, (_, __) { - ref.read(metadataProvider).write(.viewerTapToNavigate, tapToNavigate.value); + ref.read(settingsProvider).write(.viewerTapToNavigate, tapToNavigate.value); }); return Column( diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/slideshow_settings.dart b/mobile/lib/widgets/settings/asset_viewer_settings/slideshow_settings.dart index 4e566e6065..5f93d429b0 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/slideshow_settings.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/slideshow_settings.dart @@ -3,7 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.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_slider_list_tile.dart'; @@ -23,19 +23,19 @@ class SlideshowSettings extends HookConsumerWidget { final useDirection = useState(slideshow.direction); useValueChanged(useTransition.value, (_, __) { - ref.read(metadataProvider).write(.slideshowTransition, useTransition.value); + ref.read(settingsProvider).write(.slideshowTransition, useTransition.value); }); useValueChanged(useRepeat.value, (_, __) { - ref.read(metadataProvider).write(.slideshowRepeat, useRepeat.value); + ref.read(settingsProvider).write(.slideshowRepeat, useRepeat.value); }); useValueChanged(useDuration.value, (_, __) { - ref.read(metadataProvider).write(.slideshowDuration, useDuration.value); + ref.read(settingsProvider).write(.slideshowDuration, useDuration.value); }); useValueChanged(useLook.value, (_, __) { - ref.read(metadataProvider).write(.slideshowLook, useLook.value); + ref.read(settingsProvider).write(.slideshowLook, useLook.value); }); useValueChanged(useDirection.value, (_, __) { - ref.read(metadataProvider).write(.slideshowDirection, useDirection.value); + ref.read(settingsProvider).write(.slideshowDirection, useDirection.value); }); return Column( diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart b/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart index 8d62544dd4..81929d95b9 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart @@ -2,7 +2,7 @@ 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/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; @@ -17,13 +17,13 @@ class VideoViewerSettings extends HookConsumerWidget { final useOriginalVideo = useState(viewer.loadOriginalVideo); useValueChanged(useAutoPlayVideo.value, (_, __) { - ref.read(metadataProvider).write(.viewerAutoPlayVideo, useAutoPlayVideo.value); + ref.read(settingsProvider).write(.viewerAutoPlayVideo, useAutoPlayVideo.value); }); useValueChanged(useLoopVideo.value, (_, __) { - ref.read(metadataProvider).write(.viewerLoopVideo, useLoopVideo.value); + ref.read(settingsProvider).write(.viewerLoopVideo, useLoopVideo.value); }); useValueChanged(useOriginalVideo.value, (_, __) { - ref.read(metadataProvider).write(.viewerLoadOriginalVideo, useOriginalVideo.value); + ref.read(settingsProvider).write(.viewerLoadOriginalVideo, useOriginalVideo.value); }); return Column( diff --git a/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart b/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart index 89d6f13f43..e5debb43fe 100644 --- a/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart +++ b/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart @@ -5,15 +5,15 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/config/app_config.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; +import 'package:immich_mobile/domain/models/settings_key.dart'; import 'package:immich_mobile/domain/services/sync_linked_album.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/setting_list_tile.dart'; @@ -112,7 +112,7 @@ class _AlbumSyncActionButtonState extends ConsumerState<_AlbumSyncActionButton> trailing: Switch( value: albumSyncEnable, onChanged: (bool newValue) async { - await ref.read(metadataProvider).write(MetadataKey.backupSyncAlbums, newValue); + await ref.read(settingsProvider).write(.backupSyncAlbums, newValue); if (newValue == true) { await _manageLinkedAlbums(); @@ -158,7 +158,7 @@ class _AlbumSyncActionButtonState extends ConsumerState<_AlbumSyncActionButton> } class _BackupSwitchTile extends ConsumerWidget { - final MetadataKey metadataKey; + final SettingsKey metadataKey; final bool Function(AppConfig) selector; final String titleKey; final String subtitleKey; @@ -183,7 +183,7 @@ class _BackupSwitchTile extends ConsumerWidget { trailing: Switch( value: value, onChanged: (bool newValue) async { - await ref.read(metadataProvider).write(metadataKey, newValue); + await ref.read(settingsProvider).write(metadataKey, newValue); onChanged?.call(newValue); }, ), @@ -198,7 +198,7 @@ class _UseCellularForVideosButton extends StatelessWidget { @override Widget build(BuildContext context) { return _BackupSwitchTile( - metadataKey: MetadataKey.backupUseCellularForVideos, + metadataKey: SettingsKey.backupUseCellularForVideos, selector: (c) => c.backup.useCellularForVideos, titleKey: "videos", subtitleKey: "network_requirement_videos_upload", @@ -212,7 +212,7 @@ class _UseCellularForPhotosButton extends StatelessWidget { @override Widget build(BuildContext context) { return _BackupSwitchTile( - metadataKey: MetadataKey.backupUseCellularForPhotos, + metadataKey: SettingsKey.backupUseCellularForPhotos, selector: (c) => c.backup.useCellularForPhotos, titleKey: "photos", subtitleKey: "network_requirement_photos_upload", @@ -227,7 +227,7 @@ class _BackupOnlyWhenChargingButton extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final fgService = ref.read(backgroundWorkerFgServiceProvider); return _BackupSwitchTile( - metadataKey: MetadataKey.backupRequireCharging, + metadataKey: SettingsKey.backupRequireCharging, selector: (c) => c.backup.requireCharging, titleKey: "charging", subtitleKey: "charging_requirement_mobile_backup", @@ -282,11 +282,11 @@ class _BackupDelaySlider extends ConsumerWidget { value: currentValue.toDouble(), onChanged: (double v) async { final seconds = backupDelayToSeconds(v.toInt()); - await ref.read(metadataProvider).write(MetadataKey.backupTriggerDelay, seconds); + await ref.read(settingsProvider).write(SettingsKey.backupTriggerDelay, seconds); }, onChangeEnd: (double v) async { final seconds = backupDelayToSeconds(v.toInt()); - await ref.read(metadataProvider).write(MetadataKey.backupTriggerDelay, seconds); + await ref.read(settingsProvider).write(SettingsKey.backupTriggerDelay, seconds); }, max: 3.0, min: 0.0, diff --git a/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart b/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart index 02be04ac31..492a13de93 100644 --- a/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart +++ b/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; +import 'package:immich_mobile/domain/models/settings_key.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/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/networking_settings/endpoint_input.dart'; class ExternalNetworkPreference extends HookConsumerWidget { @@ -26,7 +26,7 @@ class ExternalNetworkPreference extends HookConsumerWidget { .map((e) => e.url) .toList(); - ref.read(metadataProvider).write(MetadataKey.networkExternalEndpointList, urls); + ref.read(settingsProvider).write(SettingsKey.networkExternalEndpointList, urls); } updateValidationStatus(String url, int index, AuxCheckStatus status) { diff --git a/mobile/lib/widgets/settings/networking_settings/networking_settings.dart b/mobile/lib/widgets/settings/networking_settings/networking_settings.dart index 037d66b076..7e6e169de7 100644 --- a/mobile/lib/widgets/settings/networking_settings/networking_settings.dart +++ b/mobile/lib/widgets/settings/networking_settings/networking_settings.dart @@ -5,7 +5,7 @@ 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/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/network.provider.dart'; import 'package:immich_mobile/utils/url_helper.dart'; import 'package:immich_mobile/widgets/settings/networking_settings/external_network_preference.dart'; @@ -21,7 +21,7 @@ class NetworkingSettings extends HookConsumerWidget { final currentEndpoint = getServerUrl(); final featureEnabled = useState(ref.read(appConfigProvider).network.autoEndpointSwitching); useValueChanged(featureEnabled.value, (_, __) { - ref.read(metadataProvider).write(.networkAutoEndpointSwitching, featureEnabled.value); + ref.read(settingsProvider).write(.networkAutoEndpointSwitching, featureEnabled.value); }); Future checkWifiReadPermission() async { diff --git a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart index 330555ed54..48d0ca672b 100644 --- a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart @@ -4,7 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/colors.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/theme.provider.dart'; import 'package:immich_mobile/theme/color_scheme.dart'; import 'package:immich_mobile/theme/dynamic_theme.dart'; @@ -26,16 +26,16 @@ class PrimaryColorSetting extends HookConsumerWidget { } onUseSystemColorChange(bool newValue) { - ref.read(metadataProvider).write(.themeDynamic, newValue); + ref.read(settingsProvider).write(.themeDynamic, newValue); popBottomSheet(); } onPrimaryColorChange(ImmichColorPreset colorPreset) { - ref.read(metadataProvider).write(.themePrimaryColor, colorPreset); + ref.read(settingsProvider).write(.themePrimaryColor, colorPreset); //turn off system color setting if (themeConfig.dynamicTheme) { - ref.read(metadataProvider).write(.themeDynamic, false); + ref.read(settingsProvider).write(.themeDynamic, false); } popBottomSheet(); } diff --git a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart index d71842d786..ffeeceae02 100644 --- a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart @@ -3,7 +3,7 @@ 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/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.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_switch_list_tile.dart'; @@ -22,7 +22,7 @@ class ThemeSetting extends HookConsumerWidget { void onThemeChange(bool isDark) { currentTheme.value = isDark ? ThemeMode.dark : ThemeMode.light; - ref.read(metadataProvider).write(.themeMode, currentTheme.value); + ref.read(settingsProvider).write(.themeMode, currentTheme.value); } void onSystemThemeChange(bool isSystem) { @@ -39,11 +39,11 @@ class ThemeSetting extends HookConsumerWidget { currentTheme.value = ThemeMode.dark; } } - ref.read(metadataProvider).write(.themeMode, currentTheme.value); + ref.read(settingsProvider).write(.themeMode, currentTheme.value); } void onSurfaceColorSettingChange(bool useColorfulInterface) { - ref.read(metadataProvider).write(.themeColorfulInterface, useColorfulInterface); + ref.read(settingsProvider).write(.themeColorfulInterface, useColorfulInterface); colorfulInterface.value = useColorfulInterface; } diff --git a/mobile/test/domain/services/log_service_test.dart b/mobile/test/domain/services/log_service_test.dart index f442b9514c..6a82d1dce3 100644 --- a/mobile/test/domain/services/log_service_test.dart +++ b/mobile/test/domain/services/log_service_test.dart @@ -3,7 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/config/app_config.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; +import 'package:immich_mobile/domain/models/settings_key.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; import 'package:logging/logging.dart'; @@ -29,23 +29,23 @@ final _kWarnLog = LogMessage( void main() { late LogService sut; late LogRepository mockLogRepo; - late MockMetadataRepository mockMetadataRepository; + late MockSettingsRepository mockSettingsRepository; setUp(() async { mockLogRepo = MockLogRepository(); - mockMetadataRepository = MockMetadataRepository(); + mockSettingsRepository = MockSettingsRepository(); registerFallbackValue(_kInfoLog); registerFallbackValue(LogLevel.info); when(() => mockLogRepo.truncate(limit: any(named: 'limit'))).thenAnswer((_) async => {}); - when(() => mockMetadataRepository.appConfig).thenReturn(const AppConfig(logLevel: LogLevel.fine)); - when(() => mockMetadataRepository.write(MetadataKey.logLevel, any())).thenAnswer((_) async {}); + when(() => mockSettingsRepository.appConfig).thenReturn(const AppConfig(logLevel: LogLevel.fine)); + when(() => mockSettingsRepository.write(SettingsKey.logLevel, any())).thenAnswer((_) async {}); when(() => mockLogRepo.getAll()).thenAnswer((_) async => []); when(() => mockLogRepo.insert(any())).thenAnswer((_) async => true); when(() => mockLogRepo.insertAll(any())).thenAnswer((_) async => true); - sut = await LogService.create(logRepository: mockLogRepo, metadataRepository: mockMetadataRepository); + sut = await LogService.create(logRepository: mockLogRepo, settingsRepository: mockSettingsRepository); }); tearDown(() async { @@ -59,7 +59,7 @@ void main() { }); test('Sets log level based on the metadata repository', () { - verify(() => mockMetadataRepository.appConfig).called(1); + verify(() => mockSettingsRepository.appConfig).called(1); expect(Logger.root.level, Level.FINE); }); }); @@ -71,7 +71,7 @@ void main() { test('Updates the log level via metadata repository', () { final captured = verify( - () => mockMetadataRepository.write(MetadataKey.logLevel, captureAny()), + () => mockSettingsRepository.write(SettingsKey.logLevel, captureAny()), ).captured.firstOrNull; expect(captured, LogLevel.shout); }); @@ -86,7 +86,7 @@ void main() { TestUtils.fakeAsync((time) async { sut = await LogService.create( logRepository: mockLogRepo, - metadataRepository: mockMetadataRepository, + settingsRepository: mockSettingsRepository, shouldBuffer: true, ); @@ -104,7 +104,7 @@ void main() { TestUtils.fakeAsync((time) async { sut = await LogService.create( logRepository: mockLogRepo, - metadataRepository: mockMetadataRepository, + settingsRepository: mockSettingsRepository, shouldBuffer: true, ); @@ -125,7 +125,7 @@ void main() { TestUtils.fakeAsync((time) async { sut = await LogService.create( logRepository: mockLogRepo, - metadataRepository: mockMetadataRepository, + settingsRepository: mockSettingsRepository, shouldBuffer: false, ); @@ -159,7 +159,7 @@ void main() { TestUtils.fakeAsync((time) async { sut = await LogService.create( logRepository: mockLogRepo, - metadataRepository: mockMetadataRepository, + settingsRepository: mockSettingsRepository, shouldBuffer: true, ); diff --git a/mobile/test/drift/main/generated/schema.dart b/mobile/test/drift/main/generated/schema.dart index a1bae8f6dd..c5d57e9a4b 100644 --- a/mobile/test/drift/main/generated/schema.dart +++ b/mobile/test/drift/main/generated/schema.dart @@ -30,6 +30,7 @@ import 'schema_v23.dart' as v23; import 'schema_v24.dart' as v24; import 'schema_v25.dart' as v25; import 'schema_v26.dart' as v26; +import 'schema_v27.dart' as v27; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -87,6 +88,8 @@ class GeneratedHelper implements SchemaInstantiationHelper { return v25.DatabaseAtV25(db); case 26: return v26.DatabaseAtV26(db); + case 27: + return v27.DatabaseAtV27(db); default: throw MissingSchemaException(version, versions); } @@ -119,5 +122,6 @@ class GeneratedHelper implements SchemaInstantiationHelper { 24, 25, 26, + 27, ]; } diff --git a/mobile/test/drift/main/generated/schema_v27.dart b/mobile/test/drift/main/generated/schema_v27.dart new file mode 100644 index 0000000000..2b02946175 --- /dev/null +++ b/mobile/test/drift/main/generated/schema_v27.dart @@ -0,0 +1,9384 @@ +// dart format width=80 +import 'dart:typed_data' as i2; +// GENERATED BY drift_dev, DO NOT MODIFY. +// ignore_for_file: type=lint,unused_import +// +import 'package:drift/drift.dart'; + +class UserEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 0 CHECK (has_profile_image IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn avatarColor = GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + id, + name, + email, + hasProfileImage, + profileChangedAt, + avatarColor, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_entity'; + @override + Set get $primaryKey => {id}; + @override + UserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + ); + } + + @override + UserEntity createAlias(String alias) { + return UserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class UserEntityData extends DataClass implements Insertable { + final String id; + final String name; + final String email; + final int hasProfileImage; + final String profileChangedAt; + final int avatarColor; + const UserEntityData({ + required this.id, + required this.name, + required this.email, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['email'] = Variable(email); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['avatar_color'] = Variable(avatarColor); + return map; + } + + factory UserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: serializer.fromJson(json['avatarColor']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson(avatarColor), + }; + } + + UserEntityData copyWith({ + String? id, + String? name, + String? email, + int? hasProfileImage, + String? profileChangedAt, + int? avatarColor, + }) => UserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + ); + UserEntityData copyWithCompanion(UserEntityCompanion data) { + return UserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + ); + } + + @override + String toString() { + return (StringBuffer('UserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + hasProfileImage, + profileChangedAt, + avatarColor, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor); +} + +class UserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value email; + final Value hasProfileImage; + final Value profileChangedAt; + final Value avatarColor; + const UserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + }); + UserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? email, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? avatarColor, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + }); + } + + UserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? email, + Value? hasProfileImage, + Value? profileChangedAt, + Value? avatarColor, + }) { + return UserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = Variable(avatarColor.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor') + ..write(')')) + .toString(); + } +} + +class RemoteAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn durationMs = GeneratedColumn( + 'duration_ms', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_favorite IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL REFERENCES user_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn localDateTime = GeneratedColumn( + 'local_date_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn thumbHash = GeneratedColumn( + 'thumb_hash', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn uploadedAt = GeneratedColumn( + 'uploaded_at', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn livePhotoVideoId = GeneratedColumn( + 'live_photo_video_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn visibility = GeneratedColumn( + 'visibility', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn stackId = GeneratedColumn( + 'stack_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn libraryId = GeneratedColumn( + 'library_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn isEdited = GeneratedColumn( + 'is_edited', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_edited IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationMs, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + uploadedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + isEdited, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetEntityData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}updated_at'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + durationMs: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_ms'], + ), + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + checksum: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}checksum'], + )!, + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_favorite'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + localDateTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_date_time'], + ), + thumbHash: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumb_hash'], + ), + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}deleted_at'], + ), + uploadedAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}uploaded_at'], + ), + livePhotoVideoId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}live_photo_video_id'], + ), + visibility: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}visibility'], + )!, + stackId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}stack_id'], + ), + libraryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}library_id'], + ), + isEdited: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_edited'], + )!, + ); + } + + @override + RemoteAssetEntity createAlias(String alias) { + return RemoteAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class RemoteAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final String createdAt; + final String updatedAt; + final int? width; + final int? height; + final int? durationMs; + final String id; + final String checksum; + final int isFavorite; + final String ownerId; + final String? localDateTime; + final String? thumbHash; + final String? deletedAt; + final String? uploadedAt; + final String? livePhotoVideoId; + final int visibility; + final String? stackId; + final String? libraryId; + final int isEdited; + const RemoteAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationMs, + required this.id, + required this.checksum, + required this.isFavorite, + required this.ownerId, + this.localDateTime, + this.thumbHash, + this.deletedAt, + this.uploadedAt, + this.livePhotoVideoId, + required this.visibility, + this.stackId, + this.libraryId, + required this.isEdited, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationMs != null) { + map['duration_ms'] = Variable(durationMs); + } + map['id'] = Variable(id); + map['checksum'] = Variable(checksum); + map['is_favorite'] = Variable(isFavorite); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || localDateTime != null) { + map['local_date_time'] = Variable(localDateTime); + } + if (!nullToAbsent || thumbHash != null) { + map['thumb_hash'] = Variable(thumbHash); + } + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + if (!nullToAbsent || uploadedAt != null) { + map['uploaded_at'] = Variable(uploadedAt); + } + if (!nullToAbsent || livePhotoVideoId != null) { + map['live_photo_video_id'] = Variable(livePhotoVideoId); + } + map['visibility'] = Variable(visibility); + if (!nullToAbsent || stackId != null) { + map['stack_id'] = Variable(stackId); + } + if (!nullToAbsent || libraryId != null) { + map['library_id'] = Variable(libraryId); + } + map['is_edited'] = Variable(isEdited); + return map; + } + + factory RemoteAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationMs: serializer.fromJson(json['durationMs']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + ownerId: serializer.fromJson(json['ownerId']), + localDateTime: serializer.fromJson(json['localDateTime']), + thumbHash: serializer.fromJson(json['thumbHash']), + deletedAt: serializer.fromJson(json['deletedAt']), + uploadedAt: serializer.fromJson(json['uploadedAt']), + livePhotoVideoId: serializer.fromJson(json['livePhotoVideoId']), + visibility: serializer.fromJson(json['visibility']), + stackId: serializer.fromJson(json['stackId']), + libraryId: serializer.fromJson(json['libraryId']), + isEdited: serializer.fromJson(json['isEdited']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationMs': serializer.toJson(durationMs), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'ownerId': serializer.toJson(ownerId), + 'localDateTime': serializer.toJson(localDateTime), + 'thumbHash': serializer.toJson(thumbHash), + 'deletedAt': serializer.toJson(deletedAt), + 'uploadedAt': serializer.toJson(uploadedAt), + 'livePhotoVideoId': serializer.toJson(livePhotoVideoId), + 'visibility': serializer.toJson(visibility), + 'stackId': serializer.toJson(stackId), + 'libraryId': serializer.toJson(libraryId), + 'isEdited': serializer.toJson(isEdited), + }; + } + + RemoteAssetEntityData copyWith({ + String? name, + int? type, + String? createdAt, + String? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationMs = const Value.absent(), + String? id, + String? checksum, + int? isFavorite, + String? ownerId, + Value localDateTime = const Value.absent(), + Value thumbHash = const Value.absent(), + Value deletedAt = const Value.absent(), + Value uploadedAt = const Value.absent(), + Value livePhotoVideoId = const Value.absent(), + int? visibility, + Value stackId = const Value.absent(), + Value libraryId = const Value.absent(), + int? isEdited, + }) => RemoteAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationMs: durationMs.present ? durationMs.value : this.durationMs, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime.present + ? localDateTime.value + : this.localDateTime, + thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + uploadedAt: uploadedAt.present ? uploadedAt.value : this.uploadedAt, + livePhotoVideoId: livePhotoVideoId.present + ? livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId.present ? stackId.value : this.stackId, + libraryId: libraryId.present ? libraryId.value : this.libraryId, + isEdited: isEdited ?? this.isEdited, + ); + RemoteAssetEntityData copyWithCompanion(RemoteAssetEntityCompanion data) { + return RemoteAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationMs: data.durationMs.present + ? data.durationMs.value + : this.durationMs, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + localDateTime: data.localDateTime.present + ? data.localDateTime.value + : this.localDateTime, + thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + uploadedAt: data.uploadedAt.present + ? data.uploadedAt.value + : this.uploadedAt, + livePhotoVideoId: data.livePhotoVideoId.present + ? data.livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: data.visibility.present + ? data.visibility.value + : 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, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationMs: $durationMs, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('uploadedAt: $uploadedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId, ') + ..write('isEdited: $isEdited') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationMs, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + uploadedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + isEdited, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationMs == this.durationMs && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.ownerId == this.ownerId && + other.localDateTime == this.localDateTime && + other.thumbHash == this.thumbHash && + other.deletedAt == this.deletedAt && + other.uploadedAt == this.uploadedAt && + other.livePhotoVideoId == this.livePhotoVideoId && + other.visibility == this.visibility && + other.stackId == this.stackId && + other.libraryId == this.libraryId && + other.isEdited == this.isEdited); +} + +class RemoteAssetEntityCompanion + extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationMs; + final Value id; + final Value checksum; + final Value isFavorite; + final Value ownerId; + final Value localDateTime; + final Value thumbHash; + final Value deletedAt; + final Value uploadedAt; + final Value livePhotoVideoId; + final Value visibility; + final Value stackId; + final Value libraryId; + final Value isEdited; + const RemoteAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationMs = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.ownerId = const Value.absent(), + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.uploadedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + this.visibility = const Value.absent(), + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + this.isEdited = const Value.absent(), + }); + RemoteAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationMs = const Value.absent(), + required String id, + required String checksum, + this.isFavorite = const Value.absent(), + required String ownerId, + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.uploadedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + required int visibility, + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + this.isEdited = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id), + checksum = Value(checksum), + ownerId = Value(ownerId), + visibility = Value(visibility); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationMs, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? ownerId, + Expression? localDateTime, + Expression? thumbHash, + Expression? deletedAt, + Expression? uploadedAt, + Expression? livePhotoVideoId, + Expression? visibility, + Expression? stackId, + Expression? libraryId, + Expression? isEdited, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationMs != null) 'duration_ms': durationMs, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (ownerId != null) 'owner_id': ownerId, + if (localDateTime != null) 'local_date_time': localDateTime, + if (thumbHash != null) 'thumb_hash': thumbHash, + if (deletedAt != null) 'deleted_at': deletedAt, + if (uploadedAt != null) 'uploaded_at': uploadedAt, + if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId, + if (visibility != null) 'visibility': visibility, + if (stackId != null) 'stack_id': stackId, + if (libraryId != null) 'library_id': libraryId, + if (isEdited != null) 'is_edited': isEdited, + }); + } + + RemoteAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationMs, + Value? id, + Value? checksum, + Value? isFavorite, + Value? ownerId, + Value? localDateTime, + Value? thumbHash, + Value? deletedAt, + Value? uploadedAt, + Value? livePhotoVideoId, + Value? visibility, + Value? stackId, + Value? libraryId, + Value? isEdited, + }) { + return RemoteAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationMs: durationMs ?? this.durationMs, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime ?? this.localDateTime, + thumbHash: thumbHash ?? this.thumbHash, + deletedAt: deletedAt ?? this.deletedAt, + uploadedAt: uploadedAt ?? this.uploadedAt, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId ?? this.stackId, + libraryId: libraryId ?? this.libraryId, + isEdited: isEdited ?? this.isEdited, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationMs.present) { + map['duration_ms'] = Variable(durationMs.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (localDateTime.present) { + map['local_date_time'] = Variable(localDateTime.value); + } + if (thumbHash.present) { + map['thumb_hash'] = Variable(thumbHash.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (uploadedAt.present) { + map['uploaded_at'] = Variable(uploadedAt.value); + } + if (livePhotoVideoId.present) { + map['live_photo_video_id'] = Variable(livePhotoVideoId.value); + } + if (visibility.present) { + map['visibility'] = Variable(visibility.value); + } + if (stackId.present) { + map['stack_id'] = Variable(stackId.value); + } + if (libraryId.present) { + map['library_id'] = Variable(libraryId.value); + } + if (isEdited.present) { + map['is_edited'] = Variable(isEdited.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationMs: $durationMs, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('uploadedAt: $uploadedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId, ') + ..write('isEdited: $isEdited') + ..write(')')) + .toString(); + } +} + +class StackEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StackEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL REFERENCES user_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn primaryAssetId = GeneratedColumn( + 'primary_asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + ownerId, + primaryAssetId, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'stack_entity'; + @override + Set get $primaryKey => {id}; + @override + StackEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StackEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + primaryAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}primary_asset_id'], + )!, + ); + } + + @override + StackEntity createAlias(String alias) { + return StackEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class StackEntityData extends DataClass implements Insertable { + final String id; + final String createdAt; + final String updatedAt; + final String ownerId; + final String primaryAssetId; + const StackEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.primaryAssetId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['primary_asset_id'] = Variable(primaryAssetId); + return map; + } + + factory StackEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StackEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + primaryAssetId: serializer.fromJson(json['primaryAssetId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'primaryAssetId': serializer.toJson(primaryAssetId), + }; + } + + StackEntityData copyWith({ + String? id, + String? createdAt, + String? updatedAt, + String? ownerId, + String? primaryAssetId, + }) => StackEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + StackEntityData copyWithCompanion(StackEntityCompanion data) { + return StackEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + primaryAssetId: data.primaryAssetId.present + ? data.primaryAssetId.value + : this.primaryAssetId, + ); + } + + @override + String toString() { + return (StringBuffer('StackEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, createdAt, updatedAt, ownerId, primaryAssetId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StackEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.primaryAssetId == this.primaryAssetId); +} + +class StackEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value primaryAssetId; + const StackEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.primaryAssetId = const Value.absent(), + }); + StackEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String primaryAssetId, + }) : id = Value(id), + ownerId = Value(ownerId), + primaryAssetId = Value(primaryAssetId); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? primaryAssetId, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (primaryAssetId != null) 'primary_asset_id': primaryAssetId, + }); + } + + StackEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? primaryAssetId, + }) { + return StackEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (primaryAssetId.present) { + map['primary_asset_id'] = Variable(primaryAssetId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StackEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } +} + +class LocalAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn durationMs = GeneratedColumn( + 'duration_ms', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_favorite IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn iCloudId = GeneratedColumn( + 'i_cloud_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn adjustmentTime = GeneratedColumn( + 'adjustment_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn playbackStyle = GeneratedColumn( + 'playback_style', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationMs, + id, + checksum, + isFavorite, + orientation, + iCloudId, + adjustmentTime, + latitude, + longitude, + playbackStyle, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAssetEntityData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}updated_at'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + durationMs: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_ms'], + ), + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + checksum: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}checksum'], + ), + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_favorite'], + )!, + orientation: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}orientation'], + )!, + iCloudId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}i_cloud_id'], + ), + adjustmentTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}adjustment_time'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + playbackStyle: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}playback_style'], + )!, + ); + } + + @override + LocalAssetEntity createAlias(String alias) { + return LocalAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class LocalAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final String createdAt; + final String updatedAt; + final int? width; + final int? height; + final int? durationMs; + final String id; + final String? checksum; + final int isFavorite; + final int orientation; + final String? iCloudId; + final String? adjustmentTime; + final double? latitude; + final double? longitude; + final int playbackStyle; + const LocalAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationMs, + required this.id, + this.checksum, + required this.isFavorite, + required this.orientation, + this.iCloudId, + this.adjustmentTime, + this.latitude, + this.longitude, + required this.playbackStyle, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationMs != null) { + map['duration_ms'] = Variable(durationMs); + } + map['id'] = Variable(id); + if (!nullToAbsent || checksum != null) { + map['checksum'] = Variable(checksum); + } + map['is_favorite'] = Variable(isFavorite); + map['orientation'] = Variable(orientation); + if (!nullToAbsent || iCloudId != null) { + map['i_cloud_id'] = Variable(iCloudId); + } + if (!nullToAbsent || adjustmentTime != null) { + map['adjustment_time'] = Variable(adjustmentTime); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + map['playback_style'] = Variable(playbackStyle); + return map; + } + + factory LocalAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationMs: serializer.fromJson(json['durationMs']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + orientation: serializer.fromJson(json['orientation']), + iCloudId: serializer.fromJson(json['iCloudId']), + adjustmentTime: serializer.fromJson(json['adjustmentTime']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + playbackStyle: serializer.fromJson(json['playbackStyle']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationMs': serializer.toJson(durationMs), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'orientation': serializer.toJson(orientation), + 'iCloudId': serializer.toJson(iCloudId), + 'adjustmentTime': serializer.toJson(adjustmentTime), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + 'playbackStyle': serializer.toJson(playbackStyle), + }; + } + + LocalAssetEntityData copyWith({ + String? name, + int? type, + String? createdAt, + String? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationMs = const Value.absent(), + String? id, + Value checksum = const Value.absent(), + int? isFavorite, + int? orientation, + Value iCloudId = const Value.absent(), + Value adjustmentTime = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + int? playbackStyle, + }) => LocalAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationMs: durationMs.present ? durationMs.value : this.durationMs, + id: id ?? this.id, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + iCloudId: iCloudId.present ? iCloudId.value : this.iCloudId, + adjustmentTime: adjustmentTime.present + ? adjustmentTime.value + : this.adjustmentTime, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + playbackStyle: playbackStyle ?? this.playbackStyle, + ); + LocalAssetEntityData copyWithCompanion(LocalAssetEntityCompanion data) { + return LocalAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationMs: data.durationMs.present + ? data.durationMs.value + : this.durationMs, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + orientation: data.orientation.present + ? data.orientation.value + : this.orientation, + iCloudId: data.iCloudId.present ? data.iCloudId.value : this.iCloudId, + adjustmentTime: data.adjustmentTime.present + ? data.adjustmentTime.value + : this.adjustmentTime, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + playbackStyle: data.playbackStyle.present + ? data.playbackStyle.value + : this.playbackStyle, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationMs: $durationMs, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('iCloudId: $iCloudId, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('playbackStyle: $playbackStyle') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationMs, + id, + checksum, + isFavorite, + orientation, + iCloudId, + adjustmentTime, + latitude, + longitude, + playbackStyle, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationMs == this.durationMs && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.orientation == this.orientation && + other.iCloudId == this.iCloudId && + other.adjustmentTime == this.adjustmentTime && + other.latitude == this.latitude && + other.longitude == this.longitude && + other.playbackStyle == this.playbackStyle); +} + +class LocalAssetEntityCompanion extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationMs; + final Value id; + final Value checksum; + final Value isFavorite; + final Value orientation; + final Value iCloudId; + final Value adjustmentTime; + final Value latitude; + final Value longitude; + final Value playbackStyle; + const LocalAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationMs = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + this.iCloudId = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.playbackStyle = const Value.absent(), + }); + LocalAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationMs = const Value.absent(), + required String id, + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + this.iCloudId = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.playbackStyle = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationMs, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? orientation, + Expression? iCloudId, + Expression? adjustmentTime, + Expression? latitude, + Expression? longitude, + Expression? playbackStyle, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationMs != null) 'duration_ms': durationMs, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (orientation != null) 'orientation': orientation, + if (iCloudId != null) 'i_cloud_id': iCloudId, + if (adjustmentTime != null) 'adjustment_time': adjustmentTime, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + if (playbackStyle != null) 'playback_style': playbackStyle, + }); + } + + LocalAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationMs, + Value? id, + Value? checksum, + Value? isFavorite, + Value? orientation, + Value? iCloudId, + Value? adjustmentTime, + Value? latitude, + Value? longitude, + Value? playbackStyle, + }) { + return LocalAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationMs: durationMs ?? this.durationMs, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + iCloudId: iCloudId ?? this.iCloudId, + adjustmentTime: adjustmentTime ?? this.adjustmentTime, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + playbackStyle: playbackStyle ?? this.playbackStyle, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationMs.present) { + map['duration_ms'] = Variable(durationMs.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (iCloudId.present) { + map['i_cloud_id'] = Variable(iCloudId.value); + } + if (adjustmentTime.present) { + map['adjustment_time'] = Variable(adjustmentTime.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + if (playbackStyle.present) { + map['playback_style'] = Variable(playbackStyle.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationMs: $durationMs, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('iCloudId: $iCloudId, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('playbackStyle: $playbackStyle') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT \'\'', + defaultValue: const CustomExpression('\'\''), + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn thumbnailAssetId = GeneratedColumn( + 'thumbnail_asset_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: + 'NULL REFERENCES remote_asset_entity(id)ON DELETE SET NULL', + ); + late final GeneratedColumn isActivityEnabled = GeneratedColumn( + 'is_activity_enabled', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 1 CHECK (is_activity_enabled IN (0, 1))', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn order = GeneratedColumn( + 'order', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [ + id, + name, + description, + createdAt, + updatedAt, + thumbnailAssetId, + isActivityEnabled, + order, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}updated_at'], + )!, + thumbnailAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumbnail_asset_id'], + ), + isActivityEnabled: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_activity_enabled'], + )!, + order: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}order'], + )!, + ); + } + + @override + RemoteAlbumEntity createAlias(String alias) { + return RemoteAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class RemoteAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String description; + final String createdAt; + final String updatedAt; + final String? thumbnailAssetId; + final int isActivityEnabled; + final int order; + const RemoteAlbumEntityData({ + required this.id, + required this.name, + required this.description, + required this.createdAt, + required this.updatedAt, + this.thumbnailAssetId, + required this.isActivityEnabled, + required this.order, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['description'] = Variable(description); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || thumbnailAssetId != null) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId); + } + map['is_activity_enabled'] = Variable(isActivityEnabled); + map['order'] = Variable(order); + return map; + } + + factory RemoteAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + thumbnailAssetId: serializer.fromJson(json['thumbnailAssetId']), + isActivityEnabled: serializer.fromJson(json['isActivityEnabled']), + order: serializer.fromJson(json['order']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'thumbnailAssetId': serializer.toJson(thumbnailAssetId), + 'isActivityEnabled': serializer.toJson(isActivityEnabled), + 'order': serializer.toJson(order), + }; + } + + RemoteAlbumEntityData copyWith({ + String? id, + String? name, + String? description, + String? createdAt, + String? updatedAt, + Value thumbnailAssetId = const Value.absent(), + int? isActivityEnabled, + int? order, + }) => RemoteAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + thumbnailAssetId: thumbnailAssetId.present + ? thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + RemoteAlbumEntityData copyWithCompanion(RemoteAlbumEntityCompanion data) { + return RemoteAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: data.description.present + ? data.description.value + : this.description, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + thumbnailAssetId: data.thumbnailAssetId.present + ? data.thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: data.isActivityEnabled.present + ? data.isActivityEnabled.value + : this.isActivityEnabled, + order: data.order.present ? data.order.value : this.order, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + description, + createdAt, + updatedAt, + thumbnailAssetId, + isActivityEnabled, + order, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.thumbnailAssetId == this.thumbnailAssetId && + other.isActivityEnabled == this.isActivityEnabled && + other.order == this.order); +} + +class RemoteAlbumEntityCompanion + extends UpdateCompanion { + final Value id; + final Value name; + final Value description; + final Value createdAt; + final Value updatedAt; + final Value thumbnailAssetId; + final Value isActivityEnabled; + final Value order; + const RemoteAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + this.order = const Value.absent(), + }); + RemoteAlbumEntityCompanion.insert({ + required String id, + required String name, + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + required int order, + }) : id = Value(id), + name = Value(name), + order = Value(order); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? description, + Expression? createdAt, + Expression? updatedAt, + Expression? thumbnailAssetId, + Expression? isActivityEnabled, + Expression? order, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (thumbnailAssetId != null) 'thumbnail_asset_id': thumbnailAssetId, + if (isActivityEnabled != null) 'is_activity_enabled': isActivityEnabled, + if (order != null) 'order': order, + }); + } + + RemoteAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? description, + Value? createdAt, + Value? updatedAt, + Value? thumbnailAssetId, + Value? isActivityEnabled, + Value? order, + }) { + return RemoteAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (thumbnailAssetId.present) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId.value); + } + if (isActivityEnabled.present) { + map['is_activity_enabled'] = Variable(isActivityEnabled.value); + } + if (order.present) { + map['order'] = Variable(order.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } +} + +class LocalAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn backupSelection = GeneratedColumn( + 'backup_selection', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isIosSharedAlbum = GeneratedColumn( + 'is_ios_shared_album', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 0 CHECK (is_ios_shared_album IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn linkedRemoteAlbumId = + GeneratedColumn( + 'linked_remote_album_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: + 'NULL REFERENCES remote_album_entity(id)ON DELETE SET NULL', + ); + late final GeneratedColumn marker = GeneratedColumn( + 'marker', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL CHECK (marker IN (0, 1))', + ); + @override + List get $columns => [ + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}updated_at'], + )!, + backupSelection: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}backup_selection'], + )!, + isIosSharedAlbum: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_ios_shared_album'], + )!, + linkedRemoteAlbumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}linked_remote_album_id'], + ), + marker: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}marker'], + ), + ); + } + + @override + LocalAlbumEntity createAlias(String alias) { + return LocalAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class LocalAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String updatedAt; + final int backupSelection; + final int isIosSharedAlbum; + final String? linkedRemoteAlbumId; + final int? marker; + const LocalAlbumEntityData({ + required this.id, + required this.name, + required this.updatedAt, + required this.backupSelection, + required this.isIosSharedAlbum, + this.linkedRemoteAlbumId, + this.marker, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['updated_at'] = Variable(updatedAt); + map['backup_selection'] = Variable(backupSelection); + map['is_ios_shared_album'] = Variable(isIosSharedAlbum); + if (!nullToAbsent || linkedRemoteAlbumId != null) { + map['linked_remote_album_id'] = Variable(linkedRemoteAlbumId); + } + if (!nullToAbsent || marker != null) { + map['marker'] = Variable(marker); + } + return map; + } + + factory LocalAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + updatedAt: serializer.fromJson(json['updatedAt']), + backupSelection: serializer.fromJson(json['backupSelection']), + isIosSharedAlbum: serializer.fromJson(json['isIosSharedAlbum']), + linkedRemoteAlbumId: serializer.fromJson( + json['linkedRemoteAlbumId'], + ), + marker: serializer.fromJson(json['marker']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'updatedAt': serializer.toJson(updatedAt), + 'backupSelection': serializer.toJson(backupSelection), + 'isIosSharedAlbum': serializer.toJson(isIosSharedAlbum), + 'linkedRemoteAlbumId': serializer.toJson(linkedRemoteAlbumId), + 'marker': serializer.toJson(marker), + }; + } + + LocalAlbumEntityData copyWith({ + String? id, + String? name, + String? updatedAt, + int? backupSelection, + int? isIosSharedAlbum, + Value linkedRemoteAlbumId = const Value.absent(), + Value marker = const Value.absent(), + }) => LocalAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId.present + ? linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + marker: marker.present ? marker.value : this.marker, + ); + LocalAlbumEntityData copyWithCompanion(LocalAlbumEntityCompanion data) { + return LocalAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + backupSelection: data.backupSelection.present + ? data.backupSelection.value + : this.backupSelection, + isIosSharedAlbum: data.isIosSharedAlbum.present + ? data.isIosSharedAlbum.value + : this.isIosSharedAlbum, + linkedRemoteAlbumId: data.linkedRemoteAlbumId.present + ? data.linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + marker: data.marker.present ? data.marker.value : this.marker, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker: $marker') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.updatedAt == this.updatedAt && + other.backupSelection == this.backupSelection && + other.isIosSharedAlbum == this.isIosSharedAlbum && + other.linkedRemoteAlbumId == this.linkedRemoteAlbumId && + other.marker == this.marker); +} + +class LocalAlbumEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value updatedAt; + final Value backupSelection; + final Value isIosSharedAlbum; + final Value linkedRemoteAlbumId; + final Value marker; + const LocalAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.updatedAt = const Value.absent(), + this.backupSelection = const Value.absent(), + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker = const Value.absent(), + }); + LocalAlbumEntityCompanion.insert({ + required String id, + required String name, + this.updatedAt = const Value.absent(), + required int backupSelection, + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker = const Value.absent(), + }) : id = Value(id), + name = Value(name), + backupSelection = Value(backupSelection); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? updatedAt, + Expression? backupSelection, + Expression? isIosSharedAlbum, + Expression? linkedRemoteAlbumId, + Expression? marker, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (updatedAt != null) 'updated_at': updatedAt, + if (backupSelection != null) 'backup_selection': backupSelection, + if (isIosSharedAlbum != null) 'is_ios_shared_album': isIosSharedAlbum, + if (linkedRemoteAlbumId != null) + 'linked_remote_album_id': linkedRemoteAlbumId, + if (marker != null) 'marker': marker, + }); + } + + LocalAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? updatedAt, + Value? backupSelection, + Value? isIosSharedAlbum, + Value? linkedRemoteAlbumId, + Value? marker, + }) { + return LocalAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId ?? this.linkedRemoteAlbumId, + marker: marker ?? this.marker, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (backupSelection.present) { + map['backup_selection'] = Variable(backupSelection.value); + } + if (isIosSharedAlbum.present) { + map['is_ios_shared_album'] = Variable(isIosSharedAlbum.value); + } + if (linkedRemoteAlbumId.present) { + map['linked_remote_album_id'] = Variable( + linkedRemoteAlbumId.value, + ); + } + if (marker.present) { + map['marker'] = Variable(marker.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker: $marker') + ..write(')')) + .toString(); + } +} + +class LocalAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES local_asset_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES local_album_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn marker = GeneratedColumn( + 'marker', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL CHECK (marker IN (0, 1))', + ); + @override + List get $columns => [assetId, albumId, marker]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + LocalAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + marker: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}marker'], + ), + ); + } + + @override + LocalAlbumAssetEntity createAlias(String alias) { + return LocalAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const [ + 'PRIMARY KEY(asset_id, album_id)', + ]; + @override + bool get dontWriteConstraints => true; +} + +class LocalAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + final int? marker; + const LocalAlbumAssetEntityData({ + required this.assetId, + required this.albumId, + this.marker, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + if (!nullToAbsent || marker != null) { + map['marker'] = Variable(marker); + } + return map; + } + + factory LocalAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + marker: serializer.fromJson(json['marker']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + 'marker': serializer.toJson(marker), + }; + } + + LocalAlbumAssetEntityData copyWith({ + String? assetId, + String? albumId, + Value marker = const Value.absent(), + }) => LocalAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + marker: marker.present ? marker.value : this.marker, + ); + LocalAlbumAssetEntityData copyWithCompanion( + LocalAlbumAssetEntityCompanion data, + ) { + return LocalAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + marker: data.marker.present ? data.marker.value : this.marker, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId, ') + ..write('marker: $marker') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId, marker); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId && + other.marker == this.marker); +} + +class LocalAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + final Value marker; + const LocalAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + this.marker = const Value.absent(), + }); + LocalAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + this.marker = const Value.absent(), + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + Expression? marker, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + if (marker != null) 'marker': marker, + }); + } + + LocalAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + Value? marker, + }) { + return LocalAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + marker: marker ?? this.marker, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (marker.present) { + map['marker'] = Variable(marker.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId, ') + ..write('marker: $marker') + ..write(')')) + .toString(); + } +} + +class AuthUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AuthUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isAdmin = GeneratedColumn( + 'is_admin', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_admin IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 0 CHECK (has_profile_image IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn avatarColor = GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn quotaSizeInBytes = GeneratedColumn( + 'quota_size_in_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn quotaUsageInBytes = GeneratedColumn( + 'quota_usage_in_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn pinCode = GeneratedColumn( + 'pin_code', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'auth_user_entity'; + @override + Set get $primaryKey => {id}; + @override + AuthUserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AuthUserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + isAdmin: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_admin'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + quotaSizeInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quota_size_in_bytes'], + )!, + quotaUsageInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quota_usage_in_bytes'], + )!, + pinCode: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}pin_code'], + ), + ); + } + + @override + AuthUserEntity createAlias(String alias) { + return AuthUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class AuthUserEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String email; + final int isAdmin; + final int hasProfileImage; + final String profileChangedAt; + final int avatarColor; + final int quotaSizeInBytes; + final int quotaUsageInBytes; + final String? pinCode; + const AuthUserEntityData({ + required this.id, + required this.name, + required this.email, + required this.isAdmin, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + required this.quotaSizeInBytes, + required this.quotaUsageInBytes, + this.pinCode, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['email'] = Variable(email); + map['is_admin'] = Variable(isAdmin); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['avatar_color'] = Variable(avatarColor); + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes); + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes); + if (!nullToAbsent || pinCode != null) { + map['pin_code'] = Variable(pinCode); + } + return map; + } + + factory AuthUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AuthUserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + isAdmin: serializer.fromJson(json['isAdmin']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: serializer.fromJson(json['avatarColor']), + quotaSizeInBytes: serializer.fromJson(json['quotaSizeInBytes']), + quotaUsageInBytes: serializer.fromJson(json['quotaUsageInBytes']), + pinCode: serializer.fromJson(json['pinCode']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'isAdmin': serializer.toJson(isAdmin), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson(avatarColor), + 'quotaSizeInBytes': serializer.toJson(quotaSizeInBytes), + 'quotaUsageInBytes': serializer.toJson(quotaUsageInBytes), + 'pinCode': serializer.toJson(pinCode), + }; + } + + AuthUserEntityData copyWith({ + String? id, + String? name, + String? email, + int? isAdmin, + int? hasProfileImage, + String? profileChangedAt, + int? avatarColor, + int? quotaSizeInBytes, + int? quotaUsageInBytes, + Value pinCode = const Value.absent(), + }) => AuthUserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode.present ? pinCode.value : this.pinCode, + ); + AuthUserEntityData copyWithCompanion(AuthUserEntityCompanion data) { + return AuthUserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + quotaSizeInBytes: data.quotaSizeInBytes.present + ? data.quotaSizeInBytes.value + : this.quotaSizeInBytes, + quotaUsageInBytes: data.quotaUsageInBytes.present + ? data.quotaUsageInBytes.value + : this.quotaUsageInBytes, + pinCode: data.pinCode.present ? data.pinCode.value : this.pinCode, + ); + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AuthUserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.isAdmin == this.isAdmin && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor && + other.quotaSizeInBytes == this.quotaSizeInBytes && + other.quotaUsageInBytes == this.quotaUsageInBytes && + other.pinCode == this.pinCode); +} + +class AuthUserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value email; + final Value isAdmin; + final Value hasProfileImage; + final Value profileChangedAt; + final Value avatarColor; + final Value quotaSizeInBytes; + final Value quotaUsageInBytes; + final Value pinCode; + const AuthUserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.isAdmin = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + this.pinCode = const Value.absent(), + }); + AuthUserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.isAdmin = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + required int avatarColor, + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + this.pinCode = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email), + avatarColor = Value(avatarColor); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? email, + Expression? isAdmin, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? avatarColor, + Expression? quotaSizeInBytes, + Expression? quotaUsageInBytes, + Expression? pinCode, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (isAdmin != null) 'is_admin': isAdmin, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + if (quotaSizeInBytes != null) 'quota_size_in_bytes': quotaSizeInBytes, + if (quotaUsageInBytes != null) 'quota_usage_in_bytes': quotaUsageInBytes, + if (pinCode != null) 'pin_code': pinCode, + }); + } + + AuthUserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? email, + Value? isAdmin, + Value? hasProfileImage, + Value? profileChangedAt, + Value? avatarColor, + Value? quotaSizeInBytes, + Value? quotaUsageInBytes, + Value? pinCode, + }) { + return AuthUserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode ?? this.pinCode, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (isAdmin.present) { + map['is_admin'] = Variable(isAdmin.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = Variable(avatarColor.value); + } + if (quotaSizeInBytes.present) { + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes.value); + } + if (quotaUsageInBytes.present) { + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes.value); + } + if (pinCode.present) { + map['pin_code'] = Variable(pinCode.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } +} + +class UserMetadataEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserMetadataEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL REFERENCES user_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn value = + GeneratedColumn( + 'value', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [userId, key, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_metadata_entity'; + @override + Set get $primaryKey => {userId, key}; + @override + UserMetadataEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserMetadataEntityData( + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + key: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}key'], + )!, + value: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}value'], + )!, + ); + } + + @override + UserMetadataEntity createAlias(String alias) { + return UserMetadataEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(user_id, "key")']; + @override + bool get dontWriteConstraints => true; +} + +class UserMetadataEntityData extends DataClass + implements Insertable { + final String userId; + final int key; + final i2.Uint8List value; + const UserMetadataEntityData({ + required this.userId, + required this.key, + required this.value, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['key'] = Variable(key); + map['value'] = Variable(value); + return map; + } + + factory UserMetadataEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserMetadataEntityData( + userId: serializer.fromJson(json['userId']), + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + }; + } + + UserMetadataEntityData copyWith({ + String? userId, + int? key, + i2.Uint8List? value, + }) => UserMetadataEntityData( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + UserMetadataEntityData copyWithCompanion(UserMetadataEntityCompanion data) { + return UserMetadataEntityData( + userId: data.userId.present ? data.userId.value : this.userId, + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityData(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(userId, key, $driftBlobEquality.hash(value)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserMetadataEntityData && + other.userId == this.userId && + other.key == this.key && + $driftBlobEquality.equals(other.value, this.value)); +} + +class UserMetadataEntityCompanion + extends UpdateCompanion { + final Value userId; + final Value key; + final Value value; + const UserMetadataEntityCompanion({ + this.userId = const Value.absent(), + this.key = const Value.absent(), + this.value = const Value.absent(), + }); + UserMetadataEntityCompanion.insert({ + required String userId, + required int key, + required i2.Uint8List value, + }) : userId = Value(userId), + key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? userId, + Expression? key, + Expression? value, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (key != null) 'key': key, + if (value != null) 'value': value, + }); + } + + UserMetadataEntityCompanion copyWith({ + Value? userId, + Value? key, + Value? value, + }) { + return UserMetadataEntityCompanion( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityCompanion(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } +} + +class PartnerEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PartnerEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn sharedById = GeneratedColumn( + 'shared_by_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL REFERENCES user_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn sharedWithId = GeneratedColumn( + 'shared_with_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL REFERENCES user_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn inTimeline = GeneratedColumn( + 'in_timeline', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (in_timeline IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [sharedById, sharedWithId, inTimeline]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'partner_entity'; + @override + Set get $primaryKey => {sharedById, sharedWithId}; + @override + PartnerEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PartnerEntityData( + sharedById: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_by_id'], + )!, + sharedWithId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_with_id'], + )!, + inTimeline: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}in_timeline'], + )!, + ); + } + + @override + PartnerEntity createAlias(String alias) { + return PartnerEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const [ + 'PRIMARY KEY(shared_by_id, shared_with_id)', + ]; + @override + bool get dontWriteConstraints => true; +} + +class PartnerEntityData extends DataClass + implements Insertable { + final String sharedById; + final String sharedWithId; + final int inTimeline; + const PartnerEntityData({ + required this.sharedById, + required this.sharedWithId, + required this.inTimeline, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['shared_by_id'] = Variable(sharedById); + map['shared_with_id'] = Variable(sharedWithId); + map['in_timeline'] = Variable(inTimeline); + return map; + } + + factory PartnerEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PartnerEntityData( + sharedById: serializer.fromJson(json['sharedById']), + sharedWithId: serializer.fromJson(json['sharedWithId']), + inTimeline: serializer.fromJson(json['inTimeline']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'sharedById': serializer.toJson(sharedById), + 'sharedWithId': serializer.toJson(sharedWithId), + 'inTimeline': serializer.toJson(inTimeline), + }; + } + + PartnerEntityData copyWith({ + String? sharedById, + String? sharedWithId, + int? inTimeline, + }) => PartnerEntityData( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + PartnerEntityData copyWithCompanion(PartnerEntityCompanion data) { + return PartnerEntityData( + sharedById: data.sharedById.present + ? data.sharedById.value + : this.sharedById, + sharedWithId: data.sharedWithId.present + ? data.sharedWithId.value + : this.sharedWithId, + inTimeline: data.inTimeline.present + ? data.inTimeline.value + : this.inTimeline, + ); + } + + @override + String toString() { + return (StringBuffer('PartnerEntityData(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(sharedById, sharedWithId, inTimeline); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PartnerEntityData && + other.sharedById == this.sharedById && + other.sharedWithId == this.sharedWithId && + other.inTimeline == this.inTimeline); +} + +class PartnerEntityCompanion extends UpdateCompanion { + final Value sharedById; + final Value sharedWithId; + final Value inTimeline; + const PartnerEntityCompanion({ + this.sharedById = const Value.absent(), + this.sharedWithId = const Value.absent(), + this.inTimeline = const Value.absent(), + }); + PartnerEntityCompanion.insert({ + required String sharedById, + required String sharedWithId, + this.inTimeline = const Value.absent(), + }) : sharedById = Value(sharedById), + sharedWithId = Value(sharedWithId); + static Insertable custom({ + Expression? sharedById, + Expression? sharedWithId, + Expression? inTimeline, + }) { + return RawValuesInsertable({ + if (sharedById != null) 'shared_by_id': sharedById, + if (sharedWithId != null) 'shared_with_id': sharedWithId, + if (inTimeline != null) 'in_timeline': inTimeline, + }); + } + + PartnerEntityCompanion copyWith({ + Value? sharedById, + Value? sharedWithId, + Value? inTimeline, + }) { + return PartnerEntityCompanion( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (sharedById.present) { + map['shared_by_id'] = Variable(sharedById.value); + } + if (sharedWithId.present) { + map['shared_with_id'] = Variable(sharedWithId.value); + } + if (inTimeline.present) { + map['in_timeline'] = Variable(inTimeline.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PartnerEntityCompanion(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } +} + +class RemoteExifEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteExifEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES remote_asset_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn city = GeneratedColumn( + 'city', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn state = GeneratedColumn( + 'state', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn country = GeneratedColumn( + 'country', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn dateTimeOriginal = GeneratedColumn( + 'date_time_original', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn exposureTime = GeneratedColumn( + 'exposure_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn fNumber = GeneratedColumn( + 'f_number', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn fileSize = GeneratedColumn( + 'file_size', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn focalLength = GeneratedColumn( + 'focal_length', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn iso = GeneratedColumn( + 'iso', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn make = GeneratedColumn( + 'make', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn model = GeneratedColumn( + 'model', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn lens = GeneratedColumn( + 'lens', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn timeZone = GeneratedColumn( + 'time_zone', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn rating = GeneratedColumn( + 'rating', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn projectionType = GeneratedColumn( + 'projection_type', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_exif_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteExifEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteExifEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + city: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}city'], + ), + state: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}state'], + ), + country: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}country'], + ), + dateTimeOriginal: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}date_time_original'], + ), + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + exposureTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}exposure_time'], + ), + fNumber: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}f_number'], + ), + fileSize: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}file_size'], + ), + focalLength: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}focal_length'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + iso: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}iso'], + ), + make: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}make'], + ), + model: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}model'], + ), + lens: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}lens'], + ), + orientation: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}orientation'], + ), + timeZone: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}time_zone'], + ), + rating: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}rating'], + ), + projectionType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}projection_type'], + ), + ); + } + + @override + RemoteExifEntity createAlias(String alias) { + return RemoteExifEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(asset_id)']; + @override + bool get dontWriteConstraints => true; +} + +class RemoteExifEntityData extends DataClass + implements Insertable { + final String assetId; + final String? city; + final String? state; + final String? country; + final String? dateTimeOriginal; + final String? description; + final int? height; + final int? width; + final String? exposureTime; + final double? fNumber; + final int? fileSize; + final double? focalLength; + final double? latitude; + final double? longitude; + final int? iso; + final String? make; + final String? model; + final String? lens; + final String? orientation; + final String? timeZone; + final int? rating; + final String? projectionType; + const RemoteExifEntityData({ + required this.assetId, + this.city, + this.state, + this.country, + this.dateTimeOriginal, + this.description, + this.height, + this.width, + this.exposureTime, + this.fNumber, + this.fileSize, + this.focalLength, + this.latitude, + this.longitude, + this.iso, + this.make, + this.model, + this.lens, + this.orientation, + this.timeZone, + this.rating, + this.projectionType, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || city != null) { + map['city'] = Variable(city); + } + if (!nullToAbsent || state != null) { + map['state'] = Variable(state); + } + if (!nullToAbsent || country != null) { + map['country'] = Variable(country); + } + if (!nullToAbsent || dateTimeOriginal != null) { + map['date_time_original'] = Variable(dateTimeOriginal); + } + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || exposureTime != null) { + map['exposure_time'] = Variable(exposureTime); + } + if (!nullToAbsent || fNumber != null) { + map['f_number'] = Variable(fNumber); + } + if (!nullToAbsent || fileSize != null) { + map['file_size'] = Variable(fileSize); + } + if (!nullToAbsent || focalLength != null) { + map['focal_length'] = Variable(focalLength); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + if (!nullToAbsent || iso != null) { + map['iso'] = Variable(iso); + } + if (!nullToAbsent || make != null) { + map['make'] = Variable(make); + } + if (!nullToAbsent || model != null) { + map['model'] = Variable(model); + } + if (!nullToAbsent || lens != null) { + map['lens'] = Variable(lens); + } + if (!nullToAbsent || orientation != null) { + map['orientation'] = Variable(orientation); + } + if (!nullToAbsent || timeZone != null) { + map['time_zone'] = Variable(timeZone); + } + if (!nullToAbsent || rating != null) { + map['rating'] = Variable(rating); + } + if (!nullToAbsent || projectionType != null) { + map['projection_type'] = Variable(projectionType); + } + return map; + } + + factory RemoteExifEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteExifEntityData( + assetId: serializer.fromJson(json['assetId']), + city: serializer.fromJson(json['city']), + state: serializer.fromJson(json['state']), + country: serializer.fromJson(json['country']), + dateTimeOriginal: serializer.fromJson(json['dateTimeOriginal']), + description: serializer.fromJson(json['description']), + height: serializer.fromJson(json['height']), + width: serializer.fromJson(json['width']), + exposureTime: serializer.fromJson(json['exposureTime']), + fNumber: serializer.fromJson(json['fNumber']), + fileSize: serializer.fromJson(json['fileSize']), + focalLength: serializer.fromJson(json['focalLength']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + iso: serializer.fromJson(json['iso']), + make: serializer.fromJson(json['make']), + model: serializer.fromJson(json['model']), + lens: serializer.fromJson(json['lens']), + orientation: serializer.fromJson(json['orientation']), + timeZone: serializer.fromJson(json['timeZone']), + rating: serializer.fromJson(json['rating']), + projectionType: serializer.fromJson(json['projectionType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'city': serializer.toJson(city), + 'state': serializer.toJson(state), + 'country': serializer.toJson(country), + 'dateTimeOriginal': serializer.toJson(dateTimeOriginal), + 'description': serializer.toJson(description), + 'height': serializer.toJson(height), + 'width': serializer.toJson(width), + 'exposureTime': serializer.toJson(exposureTime), + 'fNumber': serializer.toJson(fNumber), + 'fileSize': serializer.toJson(fileSize), + 'focalLength': serializer.toJson(focalLength), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + 'iso': serializer.toJson(iso), + 'make': serializer.toJson(make), + 'model': serializer.toJson(model), + 'lens': serializer.toJson(lens), + 'orientation': serializer.toJson(orientation), + 'timeZone': serializer.toJson(timeZone), + 'rating': serializer.toJson(rating), + 'projectionType': serializer.toJson(projectionType), + }; + } + + RemoteExifEntityData copyWith({ + String? assetId, + Value city = const Value.absent(), + Value state = const Value.absent(), + Value country = const Value.absent(), + Value dateTimeOriginal = const Value.absent(), + Value description = const Value.absent(), + Value height = const Value.absent(), + Value width = const Value.absent(), + Value exposureTime = const Value.absent(), + Value fNumber = const Value.absent(), + Value fileSize = const Value.absent(), + Value focalLength = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + Value iso = const Value.absent(), + Value make = const Value.absent(), + Value model = const Value.absent(), + Value lens = const Value.absent(), + Value orientation = const Value.absent(), + Value timeZone = const Value.absent(), + Value rating = const Value.absent(), + Value projectionType = const Value.absent(), + }) => RemoteExifEntityData( + assetId: assetId ?? this.assetId, + city: city.present ? city.value : this.city, + state: state.present ? state.value : this.state, + country: country.present ? country.value : this.country, + dateTimeOriginal: dateTimeOriginal.present + ? dateTimeOriginal.value + : this.dateTimeOriginal, + description: description.present ? description.value : this.description, + height: height.present ? height.value : this.height, + width: width.present ? width.value : this.width, + exposureTime: exposureTime.present ? exposureTime.value : this.exposureTime, + fNumber: fNumber.present ? fNumber.value : this.fNumber, + fileSize: fileSize.present ? fileSize.value : this.fileSize, + focalLength: focalLength.present ? focalLength.value : this.focalLength, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + iso: iso.present ? iso.value : this.iso, + make: make.present ? make.value : this.make, + model: model.present ? model.value : this.model, + lens: lens.present ? lens.value : this.lens, + orientation: orientation.present ? orientation.value : this.orientation, + timeZone: timeZone.present ? timeZone.value : this.timeZone, + rating: rating.present ? rating.value : this.rating, + projectionType: projectionType.present + ? projectionType.value + : this.projectionType, + ); + RemoteExifEntityData copyWithCompanion(RemoteExifEntityCompanion data) { + return RemoteExifEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + city: data.city.present ? data.city.value : this.city, + state: data.state.present ? data.state.value : this.state, + country: data.country.present ? data.country.value : this.country, + dateTimeOriginal: data.dateTimeOriginal.present + ? data.dateTimeOriginal.value + : this.dateTimeOriginal, + description: data.description.present + ? data.description.value + : this.description, + height: data.height.present ? data.height.value : this.height, + width: data.width.present ? data.width.value : this.width, + exposureTime: data.exposureTime.present + ? data.exposureTime.value + : this.exposureTime, + fNumber: data.fNumber.present ? data.fNumber.value : this.fNumber, + fileSize: data.fileSize.present ? data.fileSize.value : this.fileSize, + focalLength: data.focalLength.present + ? data.focalLength.value + : this.focalLength, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + iso: data.iso.present ? data.iso.value : this.iso, + make: data.make.present ? data.make.value : this.make, + model: data.model.present ? data.model.value : this.model, + lens: data.lens.present ? data.lens.value : this.lens, + orientation: data.orientation.present + ? data.orientation.value + : this.orientation, + timeZone: data.timeZone.present ? data.timeZone.value : this.timeZone, + rating: data.rating.present ? data.rating.value : this.rating, + projectionType: data.projectionType.present + ? data.projectionType.value + : this.projectionType, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityData(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType, + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteExifEntityData && + other.assetId == this.assetId && + other.city == this.city && + other.state == this.state && + other.country == this.country && + other.dateTimeOriginal == this.dateTimeOriginal && + other.description == this.description && + other.height == this.height && + other.width == this.width && + other.exposureTime == this.exposureTime && + other.fNumber == this.fNumber && + other.fileSize == this.fileSize && + other.focalLength == this.focalLength && + other.latitude == this.latitude && + other.longitude == this.longitude && + other.iso == this.iso && + other.make == this.make && + other.model == this.model && + other.lens == this.lens && + other.orientation == this.orientation && + other.timeZone == this.timeZone && + other.rating == this.rating && + other.projectionType == this.projectionType); +} + +class RemoteExifEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value city; + final Value state; + final Value country; + final Value dateTimeOriginal; + final Value description; + final Value height; + final Value width; + final Value exposureTime; + final Value fNumber; + final Value fileSize; + final Value focalLength; + final Value latitude; + final Value longitude; + final Value iso; + final Value make; + final Value model; + final Value lens; + final Value orientation; + final Value timeZone; + final Value rating; + final Value projectionType; + const RemoteExifEntityCompanion({ + this.assetId = const Value.absent(), + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }); + RemoteExifEntityCompanion.insert({ + required String assetId, + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? city, + Expression? state, + Expression? country, + Expression? dateTimeOriginal, + Expression? description, + Expression? height, + Expression? width, + Expression? exposureTime, + Expression? fNumber, + Expression? fileSize, + Expression? focalLength, + Expression? latitude, + Expression? longitude, + Expression? iso, + Expression? make, + Expression? model, + Expression? lens, + Expression? orientation, + Expression? timeZone, + Expression? rating, + Expression? projectionType, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (city != null) 'city': city, + if (state != null) 'state': state, + if (country != null) 'country': country, + if (dateTimeOriginal != null) 'date_time_original': dateTimeOriginal, + if (description != null) 'description': description, + if (height != null) 'height': height, + if (width != null) 'width': width, + if (exposureTime != null) 'exposure_time': exposureTime, + if (fNumber != null) 'f_number': fNumber, + if (fileSize != null) 'file_size': fileSize, + if (focalLength != null) 'focal_length': focalLength, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + if (iso != null) 'iso': iso, + if (make != null) 'make': make, + if (model != null) 'model': model, + if (lens != null) 'lens': lens, + if (orientation != null) 'orientation': orientation, + if (timeZone != null) 'time_zone': timeZone, + if (rating != null) 'rating': rating, + if (projectionType != null) 'projection_type': projectionType, + }); + } + + RemoteExifEntityCompanion copyWith({ + Value? assetId, + Value? city, + Value? state, + Value? country, + Value? dateTimeOriginal, + Value? description, + Value? height, + Value? width, + Value? exposureTime, + Value? fNumber, + Value? fileSize, + Value? focalLength, + Value? latitude, + Value? longitude, + Value? iso, + Value? make, + Value? model, + Value? lens, + Value? orientation, + Value? timeZone, + Value? rating, + Value? projectionType, + }) { + return RemoteExifEntityCompanion( + assetId: assetId ?? this.assetId, + city: city ?? this.city, + state: state ?? this.state, + country: country ?? this.country, + dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal, + description: description ?? this.description, + height: height ?? this.height, + width: width ?? this.width, + exposureTime: exposureTime ?? this.exposureTime, + fNumber: fNumber ?? this.fNumber, + fileSize: fileSize ?? this.fileSize, + focalLength: focalLength ?? this.focalLength, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + iso: iso ?? this.iso, + make: make ?? this.make, + model: model ?? this.model, + lens: lens ?? this.lens, + orientation: orientation ?? this.orientation, + timeZone: timeZone ?? this.timeZone, + rating: rating ?? this.rating, + projectionType: projectionType ?? this.projectionType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (city.present) { + map['city'] = Variable(city.value); + } + if (state.present) { + map['state'] = Variable(state.value); + } + if (country.present) { + map['country'] = Variable(country.value); + } + if (dateTimeOriginal.present) { + map['date_time_original'] = Variable(dateTimeOriginal.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (exposureTime.present) { + map['exposure_time'] = Variable(exposureTime.value); + } + if (fNumber.present) { + map['f_number'] = Variable(fNumber.value); + } + if (fileSize.present) { + map['file_size'] = Variable(fileSize.value); + } + if (focalLength.present) { + map['focal_length'] = Variable(focalLength.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + if (iso.present) { + map['iso'] = Variable(iso.value); + } + if (make.present) { + map['make'] = Variable(make.value); + } + if (model.present) { + map['model'] = Variable(model.value); + } + if (lens.present) { + map['lens'] = Variable(lens.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (timeZone.present) { + map['time_zone'] = Variable(timeZone.value); + } + if (rating.present) { + map['rating'] = Variable(rating.value); + } + if (projectionType.present) { + map['projection_type'] = Variable(projectionType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES remote_asset_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES remote_album_entity(id)ON DELETE CASCADE', + ); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + RemoteAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + ); + } + + @override + RemoteAlbumAssetEntity createAlias(String alias) { + return RemoteAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const [ + 'PRIMARY KEY(asset_id, album_id)', + ]; + @override + bool get dontWriteConstraints => true; +} + +class RemoteAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + const RemoteAlbumAssetEntityData({ + required this.assetId, + required this.albumId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory RemoteAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + RemoteAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => + RemoteAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + RemoteAlbumAssetEntityData copyWithCompanion( + RemoteAlbumAssetEntityCompanion data, + ) { + return RemoteAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId); +} + +class RemoteAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + const RemoteAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + RemoteAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + RemoteAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + }) { + return RemoteAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES remote_album_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL REFERENCES user_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn role = GeneratedColumn( + 'role', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [albumId, userId, role]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_user_entity'; + @override + Set get $primaryKey => {albumId, userId}; + @override + RemoteAlbumUserEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumUserEntityData( + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + role: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}role'], + )!, + ); + } + + @override + RemoteAlbumUserEntity createAlias(String alias) { + return RemoteAlbumUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const [ + 'PRIMARY KEY(album_id, user_id)', + ]; + @override + bool get dontWriteConstraints => true; +} + +class RemoteAlbumUserEntityData extends DataClass + implements Insertable { + final String albumId; + final String userId; + final int role; + const RemoteAlbumUserEntityData({ + required this.albumId, + required this.userId, + required this.role, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['album_id'] = Variable(albumId); + map['user_id'] = Variable(userId); + map['role'] = Variable(role); + return map; + } + + factory RemoteAlbumUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumUserEntityData( + albumId: serializer.fromJson(json['albumId']), + userId: serializer.fromJson(json['userId']), + role: serializer.fromJson(json['role']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'albumId': serializer.toJson(albumId), + 'userId': serializer.toJson(userId), + 'role': serializer.toJson(role), + }; + } + + RemoteAlbumUserEntityData copyWith({ + String? albumId, + String? userId, + int? role, + }) => RemoteAlbumUserEntityData( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + RemoteAlbumUserEntityData copyWithCompanion( + RemoteAlbumUserEntityCompanion data, + ) { + return RemoteAlbumUserEntityData( + albumId: data.albumId.present ? data.albumId.value : this.albumId, + userId: data.userId.present ? data.userId.value : this.userId, + role: data.role.present ? data.role.value : this.role, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityData(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(albumId, userId, role); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumUserEntityData && + other.albumId == this.albumId && + other.userId == this.userId && + other.role == this.role); +} + +class RemoteAlbumUserEntityCompanion + extends UpdateCompanion { + final Value albumId; + final Value userId; + final Value role; + const RemoteAlbumUserEntityCompanion({ + this.albumId = const Value.absent(), + this.userId = const Value.absent(), + this.role = const Value.absent(), + }); + RemoteAlbumUserEntityCompanion.insert({ + required String albumId, + required String userId, + required int role, + }) : albumId = Value(albumId), + userId = Value(userId), + role = Value(role); + static Insertable custom({ + Expression? albumId, + Expression? userId, + Expression? role, + }) { + return RawValuesInsertable({ + if (albumId != null) 'album_id': albumId, + if (userId != null) 'user_id': userId, + if (role != null) 'role': role, + }); + } + + RemoteAlbumUserEntityCompanion copyWith({ + Value? albumId, + Value? userId, + Value? role, + }) { + return RemoteAlbumUserEntityCompanion( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (role.present) { + map['role'] = Variable(role.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityCompanion(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } +} + +class RemoteAssetCloudIdEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetCloudIdEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES remote_asset_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn cloudId = GeneratedColumn( + 'cloud_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn adjustmentTime = GeneratedColumn( + 'adjustment_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + assetId, + cloudId, + createdAt, + adjustmentTime, + latitude, + longitude, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_cloud_id_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteAssetCloudIdEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetCloudIdEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + cloudId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}cloud_id'], + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}created_at'], + ), + adjustmentTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}adjustment_time'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + ); + } + + @override + RemoteAssetCloudIdEntity createAlias(String alias) { + return RemoteAssetCloudIdEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(asset_id)']; + @override + bool get dontWriteConstraints => true; +} + +class RemoteAssetCloudIdEntityData extends DataClass + implements Insertable { + final String assetId; + final String? cloudId; + final String? createdAt; + final String? adjustmentTime; + final double? latitude; + final double? longitude; + const RemoteAssetCloudIdEntityData({ + required this.assetId, + this.cloudId, + this.createdAt, + this.adjustmentTime, + this.latitude, + this.longitude, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || cloudId != null) { + map['cloud_id'] = Variable(cloudId); + } + if (!nullToAbsent || createdAt != null) { + map['created_at'] = Variable(createdAt); + } + if (!nullToAbsent || adjustmentTime != null) { + map['adjustment_time'] = Variable(adjustmentTime); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + return map; + } + + factory RemoteAssetCloudIdEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetCloudIdEntityData( + assetId: serializer.fromJson(json['assetId']), + cloudId: serializer.fromJson(json['cloudId']), + createdAt: serializer.fromJson(json['createdAt']), + adjustmentTime: serializer.fromJson(json['adjustmentTime']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'cloudId': serializer.toJson(cloudId), + 'createdAt': serializer.toJson(createdAt), + 'adjustmentTime': serializer.toJson(adjustmentTime), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + }; + } + + RemoteAssetCloudIdEntityData copyWith({ + String? assetId, + Value cloudId = const Value.absent(), + Value createdAt = const Value.absent(), + Value adjustmentTime = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + }) => RemoteAssetCloudIdEntityData( + assetId: assetId ?? this.assetId, + cloudId: cloudId.present ? cloudId.value : this.cloudId, + createdAt: createdAt.present ? createdAt.value : this.createdAt, + adjustmentTime: adjustmentTime.present + ? adjustmentTime.value + : this.adjustmentTime, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + ); + RemoteAssetCloudIdEntityData copyWithCompanion( + RemoteAssetCloudIdEntityCompanion data, + ) { + return RemoteAssetCloudIdEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + cloudId: data.cloudId.present ? data.cloudId.value : this.cloudId, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + adjustmentTime: data.adjustmentTime.present + ? data.adjustmentTime.value + : this.adjustmentTime, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetCloudIdEntityData(') + ..write('assetId: $assetId, ') + ..write('cloudId: $cloudId, ') + ..write('createdAt: $createdAt, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + assetId, + cloudId, + createdAt, + adjustmentTime, + latitude, + longitude, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetCloudIdEntityData && + other.assetId == this.assetId && + other.cloudId == this.cloudId && + other.createdAt == this.createdAt && + other.adjustmentTime == this.adjustmentTime && + other.latitude == this.latitude && + other.longitude == this.longitude); +} + +class RemoteAssetCloudIdEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value cloudId; + final Value createdAt; + final Value adjustmentTime; + final Value latitude; + final Value longitude; + const RemoteAssetCloudIdEntityCompanion({ + this.assetId = const Value.absent(), + this.cloudId = const Value.absent(), + this.createdAt = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + }); + RemoteAssetCloudIdEntityCompanion.insert({ + required String assetId, + this.cloudId = const Value.absent(), + this.createdAt = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? cloudId, + Expression? createdAt, + Expression? adjustmentTime, + Expression? latitude, + Expression? longitude, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (cloudId != null) 'cloud_id': cloudId, + if (createdAt != null) 'created_at': createdAt, + if (adjustmentTime != null) 'adjustment_time': adjustmentTime, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + }); + } + + RemoteAssetCloudIdEntityCompanion copyWith({ + Value? assetId, + Value? cloudId, + Value? createdAt, + Value? adjustmentTime, + Value? latitude, + Value? longitude, + }) { + return RemoteAssetCloudIdEntityCompanion( + assetId: assetId ?? this.assetId, + cloudId: cloudId ?? this.cloudId, + createdAt: createdAt ?? this.createdAt, + adjustmentTime: adjustmentTime ?? this.adjustmentTime, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (cloudId.present) { + map['cloud_id'] = Variable(cloudId.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (adjustmentTime.present) { + map['adjustment_time'] = Variable(adjustmentTime.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetCloudIdEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('cloudId: $cloudId, ') + ..write('createdAt: $createdAt, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } +} + +class MemoryEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL REFERENCES user_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn data = GeneratedColumn( + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isSaved = GeneratedColumn( + 'is_saved', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_saved IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn memoryAt = GeneratedColumn( + 'memory_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn seenAt = GeneratedColumn( + 'seen_at', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn showAt = GeneratedColumn( + 'show_at', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn hideAt = GeneratedColumn( + 'hide_at', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_entity'; + @override + Set get $primaryKey => {id}; + @override + MemoryEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}updated_at'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}deleted_at'], + ), + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + data: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + isSaved: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_saved'], + )!, + memoryAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}memory_at'], + )!, + seenAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}seen_at'], + ), + showAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}show_at'], + ), + hideAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}hide_at'], + ), + ); + } + + @override + MemoryEntity createAlias(String alias) { + return MemoryEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class MemoryEntityData extends DataClass + implements Insertable { + final String id; + final String createdAt; + final String updatedAt; + final String? deletedAt; + final String ownerId; + final int type; + final String data; + final int isSaved; + final String memoryAt; + final String? seenAt; + final String? showAt; + final String? hideAt; + const MemoryEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.ownerId, + required this.type, + required this.data, + required this.isSaved, + required this.memoryAt, + this.seenAt, + this.showAt, + this.hideAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + map['owner_id'] = Variable(ownerId); + map['type'] = Variable(type); + map['data'] = Variable(data); + map['is_saved'] = Variable(isSaved); + map['memory_at'] = Variable(memoryAt); + if (!nullToAbsent || seenAt != null) { + map['seen_at'] = Variable(seenAt); + } + if (!nullToAbsent || showAt != null) { + map['show_at'] = Variable(showAt); + } + if (!nullToAbsent || hideAt != null) { + map['hide_at'] = Variable(hideAt); + } + return map; + } + + factory MemoryEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ownerId: serializer.fromJson(json['ownerId']), + type: serializer.fromJson(json['type']), + data: serializer.fromJson(json['data']), + isSaved: serializer.fromJson(json['isSaved']), + memoryAt: serializer.fromJson(json['memoryAt']), + seenAt: serializer.fromJson(json['seenAt']), + showAt: serializer.fromJson(json['showAt']), + hideAt: serializer.fromJson(json['hideAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'ownerId': serializer.toJson(ownerId), + 'type': serializer.toJson(type), + 'data': serializer.toJson(data), + 'isSaved': serializer.toJson(isSaved), + 'memoryAt': serializer.toJson(memoryAt), + 'seenAt': serializer.toJson(seenAt), + 'showAt': serializer.toJson(showAt), + 'hideAt': serializer.toJson(hideAt), + }; + } + + MemoryEntityData copyWith({ + String? id, + String? createdAt, + String? updatedAt, + Value deletedAt = const Value.absent(), + String? ownerId, + int? type, + String? data, + int? isSaved, + String? memoryAt, + Value seenAt = const Value.absent(), + Value showAt = const Value.absent(), + Value hideAt = const Value.absent(), + }) => MemoryEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt.present ? seenAt.value : this.seenAt, + showAt: showAt.present ? showAt.value : this.showAt, + hideAt: hideAt.present ? hideAt.value : this.hideAt, + ); + MemoryEntityData copyWithCompanion(MemoryEntityCompanion data) { + return MemoryEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + type: data.type.present ? data.type.value : this.type, + data: data.data.present ? data.data.value : this.data, + isSaved: data.isSaved.present ? data.isSaved.value : this.isSaved, + memoryAt: data.memoryAt.present ? data.memoryAt.value : this.memoryAt, + seenAt: data.seenAt.present ? data.seenAt.value : this.seenAt, + showAt: data.showAt.present ? data.showAt.value : this.showAt, + hideAt: data.hideAt.present ? data.hideAt.value : this.hideAt, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.ownerId == this.ownerId && + other.type == this.type && + other.data == this.data && + other.isSaved == this.isSaved && + other.memoryAt == this.memoryAt && + other.seenAt == this.seenAt && + other.showAt == this.showAt && + other.hideAt == this.hideAt); +} + +class MemoryEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value ownerId; + final Value type; + final Value data; + final Value isSaved; + final Value memoryAt; + final Value seenAt; + final Value showAt; + final Value hideAt; + const MemoryEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.type = const Value.absent(), + this.data = const Value.absent(), + this.isSaved = const Value.absent(), + this.memoryAt = const Value.absent(), + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }); + MemoryEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + required String ownerId, + required int type, + required String data, + this.isSaved = const Value.absent(), + required String memoryAt, + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + type = Value(type), + data = Value(data), + memoryAt = Value(memoryAt); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? ownerId, + Expression? type, + Expression? data, + Expression? isSaved, + Expression? memoryAt, + Expression? seenAt, + Expression? showAt, + Expression? hideAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (ownerId != null) 'owner_id': ownerId, + if (type != null) 'type': type, + if (data != null) 'data': data, + if (isSaved != null) 'is_saved': isSaved, + if (memoryAt != null) 'memory_at': memoryAt, + if (seenAt != null) 'seen_at': seenAt, + if (showAt != null) 'show_at': showAt, + if (hideAt != null) 'hide_at': hideAt, + }); + } + + MemoryEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? ownerId, + Value? type, + Value? data, + Value? isSaved, + Value? memoryAt, + Value? seenAt, + Value? showAt, + Value? hideAt, + }) { + return MemoryEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt ?? this.seenAt, + showAt: showAt ?? this.showAt, + hideAt: hideAt ?? this.hideAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (isSaved.present) { + map['is_saved'] = Variable(isSaved.value); + } + if (memoryAt.present) { + map['memory_at'] = Variable(memoryAt.value); + } + if (seenAt.present) { + map['seen_at'] = Variable(seenAt.value); + } + if (showAt.present) { + map['show_at'] = Variable(showAt.value); + } + if (hideAt.present) { + map['hide_at'] = Variable(hideAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } +} + +class MemoryAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES remote_asset_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn memoryId = GeneratedColumn( + 'memory_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES memory_entity(id)ON DELETE CASCADE', + ); + @override + List get $columns => [assetId, memoryId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_asset_entity'; + @override + Set get $primaryKey => {assetId, memoryId}; + @override + MemoryAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + memoryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}memory_id'], + )!, + ); + } + + @override + MemoryAssetEntity createAlias(String alias) { + return MemoryAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const [ + 'PRIMARY KEY(asset_id, memory_id)', + ]; + @override + bool get dontWriteConstraints => true; +} + +class MemoryAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String memoryId; + const MemoryAssetEntityData({required this.assetId, required this.memoryId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['memory_id'] = Variable(memoryId); + return map; + } + + factory MemoryAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + memoryId: serializer.fromJson(json['memoryId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'memoryId': serializer.toJson(memoryId), + }; + } + + MemoryAssetEntityData copyWith({String? assetId, String? memoryId}) => + MemoryAssetEntityData( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + MemoryAssetEntityData copyWithCompanion(MemoryAssetEntityCompanion data) { + return MemoryAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + memoryId: data.memoryId.present ? data.memoryId.value : this.memoryId, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, memoryId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryAssetEntityData && + other.assetId == this.assetId && + other.memoryId == this.memoryId); +} + +class MemoryAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value memoryId; + const MemoryAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.memoryId = const Value.absent(), + }); + MemoryAssetEntityCompanion.insert({ + required String assetId, + required String memoryId, + }) : assetId = Value(assetId), + memoryId = Value(memoryId); + static Insertable custom({ + Expression? assetId, + Expression? memoryId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (memoryId != null) 'memory_id': memoryId, + }); + } + + MemoryAssetEntityCompanion copyWith({ + Value? assetId, + Value? memoryId, + }) { + return MemoryAssetEntityCompanion( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (memoryId.present) { + map['memory_id'] = Variable(memoryId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } +} + +class PersonEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PersonEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL REFERENCES user_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn faceAssetId = GeneratedColumn( + 'face_asset_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL CHECK (is_favorite IN (0, 1))', + ); + late final GeneratedColumn isHidden = GeneratedColumn( + 'is_hidden', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL CHECK (is_hidden IN (0, 1))', + ); + late final GeneratedColumn color = GeneratedColumn( + 'color', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn birthDate = GeneratedColumn( + 'birth_date', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + ownerId, + name, + faceAssetId, + isFavorite, + isHidden, + color, + birthDate, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'person_entity'; + @override + Set get $primaryKey => {id}; + @override + PersonEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PersonEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + faceAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}face_asset_id'], + ), + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_favorite'], + )!, + isHidden: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_hidden'], + )!, + color: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}color'], + ), + birthDate: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}birth_date'], + ), + ); + } + + @override + PersonEntity createAlias(String alias) { + return PersonEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class PersonEntityData extends DataClass + implements Insertable { + final String id; + final String createdAt; + final String updatedAt; + final String ownerId; + final String name; + final String? faceAssetId; + final int isFavorite; + final int isHidden; + final String? color; + final String? birthDate; + const PersonEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.name, + this.faceAssetId, + required this.isFavorite, + required this.isHidden, + this.color, + this.birthDate, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['name'] = Variable(name); + if (!nullToAbsent || faceAssetId != null) { + map['face_asset_id'] = Variable(faceAssetId); + } + map['is_favorite'] = Variable(isFavorite); + map['is_hidden'] = Variable(isHidden); + if (!nullToAbsent || color != null) { + map['color'] = Variable(color); + } + if (!nullToAbsent || birthDate != null) { + map['birth_date'] = Variable(birthDate); + } + return map; + } + + factory PersonEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PersonEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + name: serializer.fromJson(json['name']), + faceAssetId: serializer.fromJson(json['faceAssetId']), + isFavorite: serializer.fromJson(json['isFavorite']), + isHidden: serializer.fromJson(json['isHidden']), + color: serializer.fromJson(json['color']), + birthDate: serializer.fromJson(json['birthDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'name': serializer.toJson(name), + 'faceAssetId': serializer.toJson(faceAssetId), + 'isFavorite': serializer.toJson(isFavorite), + 'isHidden': serializer.toJson(isHidden), + 'color': serializer.toJson(color), + 'birthDate': serializer.toJson(birthDate), + }; + } + + PersonEntityData copyWith({ + String? id, + String? createdAt, + String? updatedAt, + String? ownerId, + String? name, + Value faceAssetId = const Value.absent(), + int? isFavorite, + int? isHidden, + Value color = const Value.absent(), + Value birthDate = const Value.absent(), + }) => PersonEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId.present ? faceAssetId.value : this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color.present ? color.value : this.color, + birthDate: birthDate.present ? birthDate.value : this.birthDate, + ); + PersonEntityData copyWithCompanion(PersonEntityCompanion data) { + return PersonEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + name: data.name.present ? data.name.value : this.name, + faceAssetId: data.faceAssetId.present + ? data.faceAssetId.value + : this.faceAssetId, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden, + color: data.color.present ? data.color.value : this.color, + birthDate: data.birthDate.present ? data.birthDate.value : this.birthDate, + ); + } + + @override + String toString() { + return (StringBuffer('PersonEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + createdAt, + updatedAt, + ownerId, + name, + faceAssetId, + isFavorite, + isHidden, + color, + birthDate, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PersonEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.name == this.name && + other.faceAssetId == this.faceAssetId && + other.isFavorite == this.isFavorite && + other.isHidden == this.isHidden && + other.color == this.color && + other.birthDate == this.birthDate); +} + +class PersonEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value name; + final Value faceAssetId; + final Value isFavorite; + final Value isHidden; + final Value color; + final Value birthDate; + const PersonEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.name = const Value.absent(), + this.faceAssetId = const Value.absent(), + this.isFavorite = const Value.absent(), + this.isHidden = const Value.absent(), + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }); + PersonEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String name, + this.faceAssetId = const Value.absent(), + required int isFavorite, + required int isHidden, + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + name = Value(name), + isFavorite = Value(isFavorite), + isHidden = Value(isHidden); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? name, + Expression? faceAssetId, + Expression? isFavorite, + Expression? isHidden, + Expression? color, + Expression? birthDate, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (name != null) 'name': name, + if (faceAssetId != null) 'face_asset_id': faceAssetId, + if (isFavorite != null) 'is_favorite': isFavorite, + if (isHidden != null) 'is_hidden': isHidden, + if (color != null) 'color': color, + if (birthDate != null) 'birth_date': birthDate, + }); + } + + PersonEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? name, + Value? faceAssetId, + Value? isFavorite, + Value? isHidden, + Value? color, + Value? birthDate, + }) { + return PersonEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId ?? this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color ?? this.color, + birthDate: birthDate ?? this.birthDate, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (faceAssetId.present) { + map['face_asset_id'] = Variable(faceAssetId.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (isHidden.present) { + map['is_hidden'] = Variable(isHidden.value); + } + if (color.present) { + map['color'] = Variable(color.value); + } + if (birthDate.present) { + map['birth_date'] = Variable(birthDate.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PersonEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } +} + +class AssetFaceEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AssetFaceEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES remote_asset_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn personId = GeneratedColumn( + 'person_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL REFERENCES person_entity(id)ON DELETE SET NULL', + ); + late final GeneratedColumn imageWidth = GeneratedColumn( + 'image_width', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn imageHeight = GeneratedColumn( + 'image_height', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn boundingBoxX1 = GeneratedColumn( + 'bounding_box_x1', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn boundingBoxY1 = GeneratedColumn( + 'bounding_box_y1', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn boundingBoxX2 = GeneratedColumn( + 'bounding_box_x2', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn boundingBoxY2 = GeneratedColumn( + 'bounding_box_y2', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn sourceType = GeneratedColumn( + 'source_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isVisible = GeneratedColumn( + 'is_visible', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1 CHECK (is_visible IN (0, 1))', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + isVisible, + deletedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'asset_face_entity'; + @override + Set get $primaryKey => {id}; + @override + AssetFaceEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AssetFaceEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + personId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}person_id'], + ), + imageWidth: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}image_width'], + )!, + imageHeight: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}image_height'], + )!, + boundingBoxX1: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_x1'], + )!, + boundingBoxY1: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_y1'], + )!, + boundingBoxX2: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_x2'], + )!, + boundingBoxY2: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_y2'], + )!, + sourceType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}source_type'], + )!, + isVisible: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_visible'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}deleted_at'], + ), + ); + } + + @override + AssetFaceEntity createAlias(String alias) { + return AssetFaceEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class AssetFaceEntityData extends DataClass + implements Insertable { + final String id; + final String assetId; + final String? personId; + final int imageWidth; + final int imageHeight; + final int boundingBoxX1; + final int boundingBoxY1; + final int boundingBoxX2; + final int boundingBoxY2; + final String sourceType; + final int isVisible; + final String? deletedAt; + const AssetFaceEntityData({ + required this.id, + required this.assetId, + this.personId, + required this.imageWidth, + required this.imageHeight, + required this.boundingBoxX1, + required this.boundingBoxY1, + required this.boundingBoxX2, + required this.boundingBoxY2, + required this.sourceType, + required this.isVisible, + this.deletedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || personId != null) { + map['person_id'] = Variable(personId); + } + map['image_width'] = Variable(imageWidth); + map['image_height'] = Variable(imageHeight); + map['bounding_box_x1'] = Variable(boundingBoxX1); + map['bounding_box_y1'] = Variable(boundingBoxY1); + map['bounding_box_x2'] = Variable(boundingBoxX2); + map['bounding_box_y2'] = Variable(boundingBoxY2); + map['source_type'] = Variable(sourceType); + map['is_visible'] = Variable(isVisible); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + return map; + } + + factory AssetFaceEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AssetFaceEntityData( + id: serializer.fromJson(json['id']), + assetId: serializer.fromJson(json['assetId']), + personId: serializer.fromJson(json['personId']), + imageWidth: serializer.fromJson(json['imageWidth']), + imageHeight: serializer.fromJson(json['imageHeight']), + boundingBoxX1: serializer.fromJson(json['boundingBoxX1']), + boundingBoxY1: serializer.fromJson(json['boundingBoxY1']), + boundingBoxX2: serializer.fromJson(json['boundingBoxX2']), + boundingBoxY2: serializer.fromJson(json['boundingBoxY2']), + sourceType: serializer.fromJson(json['sourceType']), + isVisible: serializer.fromJson(json['isVisible']), + deletedAt: serializer.fromJson(json['deletedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'assetId': serializer.toJson(assetId), + 'personId': serializer.toJson(personId), + 'imageWidth': serializer.toJson(imageWidth), + 'imageHeight': serializer.toJson(imageHeight), + 'boundingBoxX1': serializer.toJson(boundingBoxX1), + 'boundingBoxY1': serializer.toJson(boundingBoxY1), + 'boundingBoxX2': serializer.toJson(boundingBoxX2), + 'boundingBoxY2': serializer.toJson(boundingBoxY2), + 'sourceType': serializer.toJson(sourceType), + 'isVisible': serializer.toJson(isVisible), + 'deletedAt': serializer.toJson(deletedAt), + }; + } + + AssetFaceEntityData copyWith({ + String? id, + String? assetId, + Value personId = const Value.absent(), + int? imageWidth, + int? imageHeight, + int? boundingBoxX1, + int? boundingBoxY1, + int? boundingBoxX2, + int? boundingBoxY2, + String? sourceType, + int? isVisible, + Value deletedAt = const Value.absent(), + }) => AssetFaceEntityData( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId.present ? personId.value : this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + isVisible: isVisible ?? this.isVisible, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ); + AssetFaceEntityData copyWithCompanion(AssetFaceEntityCompanion data) { + return AssetFaceEntityData( + id: data.id.present ? data.id.value : this.id, + assetId: data.assetId.present ? data.assetId.value : this.assetId, + personId: data.personId.present ? data.personId.value : this.personId, + imageWidth: data.imageWidth.present + ? data.imageWidth.value + : this.imageWidth, + imageHeight: data.imageHeight.present + ? data.imageHeight.value + : this.imageHeight, + boundingBoxX1: data.boundingBoxX1.present + ? data.boundingBoxX1.value + : this.boundingBoxX1, + boundingBoxY1: data.boundingBoxY1.present + ? data.boundingBoxY1.value + : this.boundingBoxY1, + boundingBoxX2: data.boundingBoxX2.present + ? data.boundingBoxX2.value + : this.boundingBoxX2, + boundingBoxY2: data.boundingBoxY2.present + ? data.boundingBoxY2.value + : this.boundingBoxY2, + sourceType: data.sourceType.present + ? data.sourceType.value + : this.sourceType, + isVisible: data.isVisible.present ? data.isVisible.value : this.isVisible, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ); + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityData(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType, ') + ..write('isVisible: $isVisible, ') + ..write('deletedAt: $deletedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + isVisible, + deletedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AssetFaceEntityData && + other.id == this.id && + other.assetId == this.assetId && + other.personId == this.personId && + other.imageWidth == this.imageWidth && + other.imageHeight == this.imageHeight && + other.boundingBoxX1 == this.boundingBoxX1 && + other.boundingBoxY1 == this.boundingBoxY1 && + other.boundingBoxX2 == this.boundingBoxX2 && + other.boundingBoxY2 == this.boundingBoxY2 && + other.sourceType == this.sourceType && + other.isVisible == this.isVisible && + other.deletedAt == this.deletedAt); +} + +class AssetFaceEntityCompanion extends UpdateCompanion { + final Value id; + final Value assetId; + final Value personId; + final Value imageWidth; + final Value imageHeight; + final Value boundingBoxX1; + final Value boundingBoxY1; + final Value boundingBoxX2; + final Value boundingBoxY2; + final Value sourceType; + final Value isVisible; + final Value deletedAt; + const AssetFaceEntityCompanion({ + this.id = const Value.absent(), + this.assetId = const Value.absent(), + this.personId = const Value.absent(), + this.imageWidth = const Value.absent(), + this.imageHeight = const Value.absent(), + this.boundingBoxX1 = const Value.absent(), + this.boundingBoxY1 = const Value.absent(), + this.boundingBoxX2 = const Value.absent(), + this.boundingBoxY2 = const Value.absent(), + this.sourceType = const Value.absent(), + this.isVisible = const Value.absent(), + this.deletedAt = const Value.absent(), + }); + AssetFaceEntityCompanion.insert({ + required String id, + required String assetId, + this.personId = const Value.absent(), + required int imageWidth, + required int imageHeight, + required int boundingBoxX1, + required int boundingBoxY1, + required int boundingBoxX2, + required int boundingBoxY2, + required String sourceType, + this.isVisible = const Value.absent(), + this.deletedAt = const Value.absent(), + }) : id = Value(id), + assetId = Value(assetId), + imageWidth = Value(imageWidth), + imageHeight = Value(imageHeight), + boundingBoxX1 = Value(boundingBoxX1), + boundingBoxY1 = Value(boundingBoxY1), + boundingBoxX2 = Value(boundingBoxX2), + boundingBoxY2 = Value(boundingBoxY2), + sourceType = Value(sourceType); + static Insertable custom({ + Expression? id, + Expression? assetId, + Expression? personId, + Expression? imageWidth, + Expression? imageHeight, + Expression? boundingBoxX1, + Expression? boundingBoxY1, + Expression? boundingBoxX2, + Expression? boundingBoxY2, + Expression? sourceType, + Expression? isVisible, + Expression? deletedAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (assetId != null) 'asset_id': assetId, + if (personId != null) 'person_id': personId, + if (imageWidth != null) 'image_width': imageWidth, + if (imageHeight != null) 'image_height': imageHeight, + if (boundingBoxX1 != null) 'bounding_box_x1': boundingBoxX1, + if (boundingBoxY1 != null) 'bounding_box_y1': boundingBoxY1, + if (boundingBoxX2 != null) 'bounding_box_x2': boundingBoxX2, + if (boundingBoxY2 != null) 'bounding_box_y2': boundingBoxY2, + if (sourceType != null) 'source_type': sourceType, + if (isVisible != null) 'is_visible': isVisible, + if (deletedAt != null) 'deleted_at': deletedAt, + }); + } + + AssetFaceEntityCompanion copyWith({ + Value? id, + Value? assetId, + Value? personId, + Value? imageWidth, + Value? imageHeight, + Value? boundingBoxX1, + Value? boundingBoxY1, + Value? boundingBoxX2, + Value? boundingBoxY2, + Value? sourceType, + Value? isVisible, + Value? deletedAt, + }) { + return AssetFaceEntityCompanion( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId ?? this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + isVisible: isVisible ?? this.isVisible, + deletedAt: deletedAt ?? this.deletedAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (personId.present) { + map['person_id'] = Variable(personId.value); + } + if (imageWidth.present) { + map['image_width'] = Variable(imageWidth.value); + } + if (imageHeight.present) { + map['image_height'] = Variable(imageHeight.value); + } + if (boundingBoxX1.present) { + map['bounding_box_x1'] = Variable(boundingBoxX1.value); + } + if (boundingBoxY1.present) { + map['bounding_box_y1'] = Variable(boundingBoxY1.value); + } + if (boundingBoxX2.present) { + map['bounding_box_x2'] = Variable(boundingBoxX2.value); + } + if (boundingBoxY2.present) { + map['bounding_box_y2'] = Variable(boundingBoxY2.value); + } + if (sourceType.present) { + map['source_type'] = Variable(sourceType.value); + } + if (isVisible.present) { + map['is_visible'] = Variable(isVisible.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityCompanion(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType, ') + ..write('isVisible: $isVisible, ') + ..write('deletedAt: $deletedAt') + ..write(')')) + .toString(); + } +} + +class StoreEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StoreEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn stringValue = GeneratedColumn( + 'string_value', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn intValue = GeneratedColumn( + 'int_value', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [id, stringValue, intValue]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'store_entity'; + @override + Set get $primaryKey => {id}; + @override + StoreEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StoreEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + stringValue: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}string_value'], + ), + intValue: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}int_value'], + ), + ); + } + + @override + StoreEntity createAlias(String alias) { + return StoreEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class StoreEntityData extends DataClass implements Insertable { + final int id; + final String? stringValue; + final int? intValue; + const StoreEntityData({required this.id, this.stringValue, this.intValue}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + if (!nullToAbsent || stringValue != null) { + map['string_value'] = Variable(stringValue); + } + if (!nullToAbsent || intValue != null) { + map['int_value'] = Variable(intValue); + } + return map; + } + + factory StoreEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StoreEntityData( + id: serializer.fromJson(json['id']), + stringValue: serializer.fromJson(json['stringValue']), + intValue: serializer.fromJson(json['intValue']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'stringValue': serializer.toJson(stringValue), + 'intValue': serializer.toJson(intValue), + }; + } + + StoreEntityData copyWith({ + int? id, + Value stringValue = const Value.absent(), + Value intValue = const Value.absent(), + }) => StoreEntityData( + id: id ?? this.id, + stringValue: stringValue.present ? stringValue.value : this.stringValue, + intValue: intValue.present ? intValue.value : this.intValue, + ); + StoreEntityData copyWithCompanion(StoreEntityCompanion data) { + return StoreEntityData( + id: data.id.present ? data.id.value : this.id, + stringValue: data.stringValue.present + ? data.stringValue.value + : this.stringValue, + intValue: data.intValue.present ? data.intValue.value : this.intValue, + ); + } + + @override + String toString() { + return (StringBuffer('StoreEntityData(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, stringValue, intValue); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StoreEntityData && + other.id == this.id && + other.stringValue == this.stringValue && + other.intValue == this.intValue); +} + +class StoreEntityCompanion extends UpdateCompanion { + final Value id; + final Value stringValue; + final Value intValue; + const StoreEntityCompanion({ + this.id = const Value.absent(), + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }); + StoreEntityCompanion.insert({ + required int id, + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }) : id = Value(id); + static Insertable custom({ + Expression? id, + Expression? stringValue, + Expression? intValue, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (stringValue != null) 'string_value': stringValue, + if (intValue != null) 'int_value': intValue, + }); + } + + StoreEntityCompanion copyWith({ + Value? id, + Value? stringValue, + Value? intValue, + }) { + return StoreEntityCompanion( + id: id ?? this.id, + stringValue: stringValue ?? this.stringValue, + intValue: intValue ?? this.intValue, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (stringValue.present) { + map['string_value'] = Variable(stringValue.value); + } + if (intValue.present) { + map['int_value'] = Variable(intValue.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StoreEntityCompanion(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } +} + +class TrashedLocalAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + TrashedLocalAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn durationMs = GeneratedColumn( + 'duration_ms', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_favorite IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn source = GeneratedColumn( + 'source', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn playbackStyle = GeneratedColumn( + 'playback_style', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationMs, + id, + albumId, + checksum, + isFavorite, + orientation, + source, + playbackStyle, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'trashed_local_asset_entity'; + @override + Set get $primaryKey => {id, albumId}; + @override + TrashedLocalAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TrashedLocalAssetEntityData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}updated_at'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + durationMs: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_ms'], + ), + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + checksum: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}checksum'], + ), + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_favorite'], + )!, + orientation: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}orientation'], + )!, + source: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}source'], + )!, + playbackStyle: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}playback_style'], + )!, + ); + } + + @override + TrashedLocalAssetEntity createAlias(String alias) { + return TrashedLocalAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(id, album_id)']; + @override + bool get dontWriteConstraints => true; +} + +class TrashedLocalAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final String createdAt; + final String updatedAt; + final int? width; + final int? height; + final int? durationMs; + final String id; + final String albumId; + final String? checksum; + final int isFavorite; + final int orientation; + final int source; + final int playbackStyle; + const TrashedLocalAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationMs, + required this.id, + required this.albumId, + this.checksum, + required this.isFavorite, + required this.orientation, + required this.source, + required this.playbackStyle, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationMs != null) { + map['duration_ms'] = Variable(durationMs); + } + map['id'] = Variable(id); + map['album_id'] = Variable(albumId); + if (!nullToAbsent || checksum != null) { + map['checksum'] = Variable(checksum); + } + map['is_favorite'] = Variable(isFavorite); + map['orientation'] = Variable(orientation); + map['source'] = Variable(source); + map['playback_style'] = Variable(playbackStyle); + return map; + } + + factory TrashedLocalAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TrashedLocalAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationMs: serializer.fromJson(json['durationMs']), + id: serializer.fromJson(json['id']), + albumId: serializer.fromJson(json['albumId']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + orientation: serializer.fromJson(json['orientation']), + source: serializer.fromJson(json['source']), + playbackStyle: serializer.fromJson(json['playbackStyle']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationMs': serializer.toJson(durationMs), + 'id': serializer.toJson(id), + 'albumId': serializer.toJson(albumId), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'orientation': serializer.toJson(orientation), + 'source': serializer.toJson(source), + 'playbackStyle': serializer.toJson(playbackStyle), + }; + } + + TrashedLocalAssetEntityData copyWith({ + String? name, + int? type, + String? createdAt, + String? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationMs = const Value.absent(), + String? id, + String? albumId, + Value checksum = const Value.absent(), + int? isFavorite, + int? orientation, + int? source, + int? playbackStyle, + }) => TrashedLocalAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationMs: durationMs.present ? durationMs.value : this.durationMs, + id: id ?? this.id, + albumId: albumId ?? this.albumId, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + source: source ?? this.source, + playbackStyle: playbackStyle ?? this.playbackStyle, + ); + TrashedLocalAssetEntityData copyWithCompanion( + TrashedLocalAssetEntityCompanion data, + ) { + return TrashedLocalAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationMs: data.durationMs.present + ? data.durationMs.value + : this.durationMs, + id: data.id.present ? data.id.value : this.id, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + orientation: data.orientation.present + ? data.orientation.value + : this.orientation, + source: data.source.present ? data.source.value : this.source, + playbackStyle: data.playbackStyle.present + ? data.playbackStyle.value + : this.playbackStyle, + ); + } + + @override + String toString() { + return (StringBuffer('TrashedLocalAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationMs: $durationMs, ') + ..write('id: $id, ') + ..write('albumId: $albumId, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('source: $source, ') + ..write('playbackStyle: $playbackStyle') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationMs, + id, + albumId, + checksum, + isFavorite, + orientation, + source, + playbackStyle, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TrashedLocalAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationMs == this.durationMs && + other.id == this.id && + other.albumId == this.albumId && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.orientation == this.orientation && + other.source == this.source && + other.playbackStyle == this.playbackStyle); +} + +class TrashedLocalAssetEntityCompanion + extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationMs; + final Value id; + final Value albumId; + final Value checksum; + final Value isFavorite; + final Value orientation; + final Value source; + final Value playbackStyle; + const TrashedLocalAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationMs = const Value.absent(), + this.id = const Value.absent(), + this.albumId = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + this.source = const Value.absent(), + this.playbackStyle = const Value.absent(), + }); + TrashedLocalAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationMs = const Value.absent(), + required String id, + required String albumId, + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + required int source, + this.playbackStyle = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id), + albumId = Value(albumId), + source = Value(source); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationMs, + Expression? id, + Expression? albumId, + Expression? checksum, + Expression? isFavorite, + Expression? orientation, + Expression? source, + Expression? playbackStyle, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationMs != null) 'duration_ms': durationMs, + if (id != null) 'id': id, + if (albumId != null) 'album_id': albumId, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (orientation != null) 'orientation': orientation, + if (source != null) 'source': source, + if (playbackStyle != null) 'playback_style': playbackStyle, + }); + } + + TrashedLocalAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationMs, + Value? id, + Value? albumId, + Value? checksum, + Value? isFavorite, + Value? orientation, + Value? source, + Value? playbackStyle, + }) { + return TrashedLocalAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationMs: durationMs ?? this.durationMs, + id: id ?? this.id, + albumId: albumId ?? this.albumId, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + source: source ?? this.source, + playbackStyle: playbackStyle ?? this.playbackStyle, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationMs.present) { + map['duration_ms'] = Variable(durationMs.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (source.present) { + map['source'] = Variable(source.value); + } + if (playbackStyle.present) { + map['playback_style'] = Variable(playbackStyle.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TrashedLocalAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationMs: $durationMs, ') + ..write('id: $id, ') + ..write('albumId: $albumId, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('source: $source, ') + ..write('playbackStyle: $playbackStyle') + ..write(')')) + .toString(); + } +} + +class AssetEditEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AssetEditEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES remote_asset_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn action = GeneratedColumn( + 'action', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn parameters = + GeneratedColumn( + 'parameters', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn sequence = GeneratedColumn( + 'sequence', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [ + id, + assetId, + action, + parameters, + sequence, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'asset_edit_entity'; + @override + Set get $primaryKey => {id}; + @override + AssetEditEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AssetEditEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + action: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}action'], + )!, + parameters: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}parameters'], + )!, + sequence: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}sequence'], + )!, + ); + } + + @override + AssetEditEntity createAlias(String alias) { + return AssetEditEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class AssetEditEntityData extends DataClass + implements Insertable { + final String id; + final String assetId; + final int action; + final i2.Uint8List parameters; + final int sequence; + const AssetEditEntityData({ + required this.id, + required this.assetId, + required this.action, + required this.parameters, + required this.sequence, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['asset_id'] = Variable(assetId); + map['action'] = Variable(action); + map['parameters'] = Variable(parameters); + map['sequence'] = Variable(sequence); + return map; + } + + factory AssetEditEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AssetEditEntityData( + id: serializer.fromJson(json['id']), + assetId: serializer.fromJson(json['assetId']), + action: serializer.fromJson(json['action']), + parameters: serializer.fromJson(json['parameters']), + sequence: serializer.fromJson(json['sequence']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'assetId': serializer.toJson(assetId), + 'action': serializer.toJson(action), + 'parameters': serializer.toJson(parameters), + 'sequence': serializer.toJson(sequence), + }; + } + + AssetEditEntityData copyWith({ + String? id, + String? assetId, + int? action, + i2.Uint8List? parameters, + int? sequence, + }) => AssetEditEntityData( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + action: action ?? this.action, + parameters: parameters ?? this.parameters, + sequence: sequence ?? this.sequence, + ); + AssetEditEntityData copyWithCompanion(AssetEditEntityCompanion data) { + return AssetEditEntityData( + id: data.id.present ? data.id.value : this.id, + assetId: data.assetId.present ? data.assetId.value : this.assetId, + action: data.action.present ? data.action.value : this.action, + parameters: data.parameters.present + ? data.parameters.value + : this.parameters, + sequence: data.sequence.present ? data.sequence.value : this.sequence, + ); + } + + @override + String toString() { + return (StringBuffer('AssetEditEntityData(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('action: $action, ') + ..write('parameters: $parameters, ') + ..write('sequence: $sequence') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + assetId, + action, + $driftBlobEquality.hash(parameters), + sequence, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AssetEditEntityData && + other.id == this.id && + other.assetId == this.assetId && + other.action == this.action && + $driftBlobEquality.equals(other.parameters, this.parameters) && + other.sequence == this.sequence); +} + +class AssetEditEntityCompanion extends UpdateCompanion { + final Value id; + final Value assetId; + final Value action; + final Value parameters; + final Value sequence; + const AssetEditEntityCompanion({ + this.id = const Value.absent(), + this.assetId = const Value.absent(), + this.action = const Value.absent(), + this.parameters = const Value.absent(), + this.sequence = const Value.absent(), + }); + AssetEditEntityCompanion.insert({ + required String id, + required String assetId, + required int action, + required i2.Uint8List parameters, + required int sequence, + }) : id = Value(id), + assetId = Value(assetId), + action = Value(action), + parameters = Value(parameters), + sequence = Value(sequence); + static Insertable custom({ + Expression? id, + Expression? assetId, + Expression? action, + Expression? parameters, + Expression? sequence, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (assetId != null) 'asset_id': assetId, + if (action != null) 'action': action, + if (parameters != null) 'parameters': parameters, + if (sequence != null) 'sequence': sequence, + }); + } + + AssetEditEntityCompanion copyWith({ + Value? id, + Value? assetId, + Value? action, + Value? parameters, + Value? sequence, + }) { + return AssetEditEntityCompanion( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + action: action ?? this.action, + parameters: parameters ?? this.parameters, + sequence: sequence ?? this.sequence, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (action.present) { + map['action'] = Variable(action.value); + } + if (parameters.present) { + map['parameters'] = Variable(parameters.value); + } + if (sequence.present) { + map['sequence'] = Variable(sequence.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AssetEditEntityCompanion(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('action: $action, ') + ..write('parameters: $parameters, ') + ..write('sequence: $sequence') + ..write(')')) + .toString(); + } +} + +class Settings extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Settings(this.attachedDatabase, [this._alias]); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + @override + List get $columns => [key, value, updatedAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'settings'; + @override + Set get $primaryKey => {key}; + @override + SettingsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SettingsData( + key: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}key'], + )!, + value: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}value'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}updated_at'], + )!, + ); + } + + @override + Settings createAlias(String alias) { + return Settings(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY("key")']; + @override + bool get dontWriteConstraints => true; +} + +class SettingsData extends DataClass implements Insertable { + final String key; + final String value; + final String updatedAt; + const SettingsData({ + required this.key, + required this.value, + required this.updatedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['key'] = Variable(key); + map['value'] = Variable(value); + map['updated_at'] = Variable(updatedAt); + return map; + } + + factory SettingsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SettingsData( + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + updatedAt: serializer.fromJson(json['updatedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + 'updatedAt': serializer.toJson(updatedAt), + }; + } + + SettingsData copyWith({String? key, String? value, String? updatedAt}) => + SettingsData( + key: key ?? this.key, + value: value ?? this.value, + updatedAt: updatedAt ?? this.updatedAt, + ); + SettingsData copyWithCompanion(SettingsCompanion data) { + return SettingsData( + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ); + } + + @override + String toString() { + return (StringBuffer('SettingsData(') + ..write('key: $key, ') + ..write('value: $value, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(key, value, updatedAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SettingsData && + other.key == this.key && + other.value == this.value && + other.updatedAt == this.updatedAt); +} + +class SettingsCompanion extends UpdateCompanion { + final Value key; + final Value value; + final Value updatedAt; + const SettingsCompanion({ + this.key = const Value.absent(), + this.value = const Value.absent(), + this.updatedAt = const Value.absent(), + }); + SettingsCompanion.insert({ + required String key, + required String value, + this.updatedAt = const Value.absent(), + }) : key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? key, + Expression? value, + Expression? updatedAt, + }) { + return RawValuesInsertable({ + if (key != null) 'key': key, + if (value != null) 'value': value, + if (updatedAt != null) 'updated_at': updatedAt, + }); + } + + SettingsCompanion copyWith({ + Value? key, + Value? value, + Value? updatedAt, + }) { + return SettingsCompanion( + key: key ?? this.key, + value: value ?? this.value, + updatedAt: updatedAt ?? this.updatedAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SettingsCompanion(') + ..write('key: $key, ') + ..write('value: $value, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV27 extends GeneratedDatabase { + DatabaseAtV27(QueryExecutor e) : super(e); + late final UserEntity userEntity = UserEntity(this); + late final RemoteAssetEntity remoteAssetEntity = RemoteAssetEntity(this); + late final StackEntity stackEntity = StackEntity(this); + late final LocalAssetEntity localAssetEntity = LocalAssetEntity(this); + late final RemoteAlbumEntity remoteAlbumEntity = RemoteAlbumEntity(this); + late final LocalAlbumEntity localAlbumEntity = LocalAlbumEntity(this); + late final LocalAlbumAssetEntity localAlbumAssetEntity = + LocalAlbumAssetEntity(this); + late final Index idxLocalAlbumAssetAlbumAsset = Index( + 'idx_local_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)', + ); + late final Index idxLocalAssetChecksum = Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + late final Index idxLocalAssetCloudId = Index( + 'idx_local_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)', + ); + late final Index idxStackPrimaryAssetId = Index( + 'idx_stack_primary_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)', + ); + late final Index uQRemoteAssetsOwnerChecksum = 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)', + ); + late final Index uQRemoteAssetsOwnerLibraryChecksum = 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)', + ); + late final Index idxRemoteAssetChecksum = Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + late final Index idxRemoteAssetStackId = Index( + 'idx_remote_asset_stack_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)', + ); + late final Index idxRemoteAssetOwnerVisibilityDeletedCreated = Index( + 'idx_remote_asset_owner_visibility_deleted_created', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)', + ); + late final AuthUserEntity authUserEntity = AuthUserEntity(this); + late final UserMetadataEntity userMetadataEntity = UserMetadataEntity(this); + late final PartnerEntity partnerEntity = PartnerEntity(this); + late final RemoteExifEntity remoteExifEntity = RemoteExifEntity(this); + late final RemoteAlbumAssetEntity remoteAlbumAssetEntity = + RemoteAlbumAssetEntity(this); + late final RemoteAlbumUserEntity remoteAlbumUserEntity = + RemoteAlbumUserEntity(this); + late final RemoteAssetCloudIdEntity remoteAssetCloudIdEntity = + RemoteAssetCloudIdEntity(this); + late final MemoryEntity memoryEntity = MemoryEntity(this); + late final MemoryAssetEntity memoryAssetEntity = MemoryAssetEntity(this); + late final PersonEntity personEntity = PersonEntity(this); + late final AssetFaceEntity assetFaceEntity = AssetFaceEntity(this); + late final StoreEntity storeEntity = StoreEntity(this); + late final TrashedLocalAssetEntity trashedLocalAssetEntity = + TrashedLocalAssetEntity(this); + late final AssetEditEntity assetEditEntity = AssetEditEntity(this); + late final Settings settings = Settings(this); + late final Index idxPartnerSharedWithId = Index( + 'idx_partner_shared_with_id', + 'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)', + ); + late final Index idxLatLng = Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); + late final Index idxRemoteExifCity = Index( + 'idx_remote_exif_city', + 'CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL', + ); + late final Index idxRemoteAlbumAssetAlbumAsset = Index( + 'idx_remote_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)', + ); + late final Index idxRemoteAssetCloudId = Index( + 'idx_remote_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)', + ); + late final Index idxPersonOwnerId = Index( + 'idx_person_owner_id', + 'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)', + ); + late final Index idxAssetFacePersonId = Index( + 'idx_asset_face_person_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)', + ); + late final Index idxAssetFaceAssetId = Index( + 'idx_asset_face_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)', + ); + late final Index idxAssetFaceVisiblePerson = Index( + 'idx_asset_face_visible_person', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL', + ); + late final Index idxTrashedLocalAssetChecksum = Index( + 'idx_trashed_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)', + ); + late final Index idxTrashedLocalAssetAlbum = Index( + 'idx_trashed_local_asset_album', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)', + ); + late final Index idxAssetEditAssetId = Index( + 'idx_asset_edit_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)', + ); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAlbumAssetAlbumAsset, + idxLocalAssetChecksum, + idxLocalAssetCloudId, + idxStackPrimaryAssetId, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + idxRemoteAssetStackId, + idxRemoteAssetOwnerVisibilityDeletedCreated, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + remoteAssetCloudIdEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + trashedLocalAssetEntity, + assetEditEntity, + settings, + idxPartnerSharedWithId, + idxLatLng, + idxRemoteExifCity, + idxRemoteAlbumAssetAlbumAsset, + idxRemoteAssetCloudId, + idxPersonOwnerId, + idxAssetFacePersonId, + idxAssetFaceAssetId, + idxAssetFaceVisiblePerson, + idxTrashedLocalAssetChecksum, + idxTrashedLocalAssetAlbum, + idxAssetEditAssetId, + ]; + @override + StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules([ + WritePropagation( + on: TableUpdateQuery.onTableName( + 'user_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('remote_asset_entity', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'user_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('stack_entity', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'remote_asset_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('remote_album_entity', kind: UpdateKind.update)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'remote_album_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('local_album_entity', kind: UpdateKind.update)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'local_asset_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [ + TableUpdate('local_album_asset_entity', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'local_album_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [ + TableUpdate('local_album_asset_entity', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'user_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('user_metadata_entity', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'user_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('partner_entity', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'user_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('partner_entity', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'remote_asset_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('remote_exif_entity', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'remote_asset_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [ + TableUpdate('remote_album_asset_entity', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'remote_album_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [ + TableUpdate('remote_album_asset_entity', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'remote_album_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [ + TableUpdate('remote_album_user_entity', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'user_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [ + TableUpdate('remote_album_user_entity', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'remote_asset_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [ + TableUpdate('remote_asset_cloud_id_entity', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'user_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('memory_entity', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'remote_asset_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('memory_asset_entity', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'memory_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('memory_asset_entity', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'user_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('person_entity', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'remote_asset_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('asset_face_entity', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'person_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('asset_face_entity', kind: UpdateKind.update)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'remote_asset_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('asset_edit_entity', kind: UpdateKind.delete)], + ), + ]); + @override + int get schemaVersion => 27; + @override + DriftDatabaseOptions get options => + const DriftDatabaseOptions(storeDateTimeAsText: true); +} diff --git a/mobile/test/infrastructure/repository.mock.dart b/mobile/test/infrastructure/repository.mock.dart index 74ecf39038..9c1cdae416 100644 --- a/mobile/test/infrastructure/repository.mock.dart +++ b/mobile/test/infrastructure/repository.mock.dart @@ -2,7 +2,7 @@ import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; @@ -18,7 +18,7 @@ import 'package:mocktail/mocktail.dart'; class MockDriftStoreRepository extends Mock implements DriftStoreRepository {} -class MockMetadataRepository extends Mock implements MetadataRepository {} +class MockSettingsRepository extends Mock implements SettingsRepository {} class MockLogRepository extends Mock implements LogRepository {} diff --git a/mobile/test/medium/repositories/metadata_repository_test.dart b/mobile/test/medium/repositories/settings_repository_test.dart similarity index 74% rename from mobile/test/medium/repositories/metadata_repository_test.dart rename to mobile/test/medium/repositories/settings_repository_test.dart index 8662e8bdd0..6a3f79badb 100644 --- a/mobile/test/medium/repositories/metadata_repository_test.dart +++ b/mobile/test/medium/repositories/settings_repository_test.dart @@ -2,19 +2,19 @@ import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; -import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/domain/models/settings_key.dart'; +import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import '../repository_context.dart'; void main() { late MediumRepositoryContext ctx; - late MetadataRepository sut; + late SettingsRepository sut; setUpAll(() async { ctx = MediumRepositoryContext(); - sut = await MetadataRepository.ensureInitialized(ctx.db); + sut = await SettingsRepository.ensureInitialized(ctx.db); }); tearDownAll(() async { @@ -22,8 +22,8 @@ void main() { }); setUp(() async { - await ctx.db.delete(ctx.db.metadataEntity).go(); - await MetadataRepository.instance.refresh(); + await ctx.db.delete(ctx.db.settingsEntity).go(); + await SettingsRepository.instance.refresh(); }); group('defaults', () { @@ -56,7 +56,7 @@ void main() { await sut.write(.themeMode, ThemeMode.system); expect(sut.appConfig.theme.mode, ThemeMode.system); - final rows = await ctx.db.select(ctx.db.metadataEntity).get(); + final rows = await ctx.db.select(ctx.db.settingsEntity).get(); expect(rows, isEmpty); }); }); @@ -66,10 +66,10 @@ void main() { group('sync', () { test('picks up rows that were inserted directly into the DB', () async { await ctx.db - .into(ctx.db.metadataEntity) + .into(ctx.db.settingsEntity) .insert( - MetadataEntityCompanion.insert( - key: MetadataKey.themeMode.name, + SettingsEntityCompanion.insert( + key: SettingsKey.themeMode.name, value: ThemeMode.dark.name, updatedAt: Value(DateTime.now()), ), @@ -78,32 +78,32 @@ void main() { // Cache hasn't seen this row yet โ€” view still returns the default. expect(sut.appConfig.theme.mode, ThemeMode.system); - await MetadataRepository.instance.refresh(); + await SettingsRepository.instance.refresh(); expect(sut.appConfig.theme.mode, ThemeMode.dark); }); test('drops cached values for rows that were deleted out from under the repo', () async { await sut.write(.themeMode, ThemeMode.dark); // Wipe the row directly. Cache still holds the old value. - await ctx.db.delete(ctx.db.metadataEntity).go(); + await ctx.db.delete(ctx.db.settingsEntity).go(); expect(sut.appConfig.theme.mode, ThemeMode.dark); - await MetadataRepository.instance.refresh(); + await SettingsRepository.instance.refresh(); expect(sut.appConfig.theme.mode, ThemeMode.system); }); - test('skips rows whose key is unknown to MetadataKey', () async { + test('skips rows whose key is unknown to SettingsKey', () async { await ctx.db - .into(ctx.db.metadataEntity) + .into(ctx.db.settingsEntity) .insert( - MetadataEntityCompanion.insert( + SettingsEntityCompanion.insert( key: 'app-config.unknown.future-key', value: 'whatever', updatedAt: Value(DateTime.now()), ), ); - await MetadataRepository.instance.refresh(); + await SettingsRepository.instance.refresh(); expect(sut.appConfig.theme.mode, ThemeMode.system); }); }); @@ -111,13 +111,13 @@ void main() { group('watch', () { test('watchAppConfig emits the new value after a write', () async { final expectation = expectLater(sut.watchConfig().map((c) => c.theme.mode), emitsThrough(ThemeMode.dark)); - await sut.write(MetadataKey.themeMode, ThemeMode.dark); + await sut.write(SettingsKey.themeMode, ThemeMode.dark); await expectation; }); test('watchConfig emits the new value after a write', () async { final expectation = expectLater(sut.watchConfig().map((c) => c.logLevel), emitsThrough(LogLevel.warning)); - await sut.write(MetadataKey.logLevel, LogLevel.warning); + await sut.write(SettingsKey.logLevel, LogLevel.warning); await expectation; }); }); diff --git a/mobile/test/services/background_upload.service_test.dart b/mobile/test/services/background_upload.service_test.dart index dd19f2b1cc..310f2f4d49 100644 --- a/mobile/test/services/background_upload.service_test.dart +++ b/mobile/test/services/background_upload.service_test.dart @@ -11,7 +11,7 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/services/background_upload.service.dart'; import 'package:mocktail/mocktail.dart'; @@ -38,7 +38,7 @@ void main() { ); db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); await StoreService.init(storeRepository: DriftStoreRepository(db)); - await MetadataRepository.ensureInitialized(db); + await SettingsRepository.ensureInitialized(db); await Store.put(StoreKey.serverEndpoint, 'http://test-server.com'); await Store.put(StoreKey.deviceId, 'test-device-id'); diff --git a/mobile/test/unit/repositories/metadata_repository_test.dart b/mobile/test/unit/repositories/settings_repository_test.dart similarity index 76% rename from mobile/test/unit/repositories/metadata_repository_test.dart rename to mobile/test/unit/repositories/settings_repository_test.dart index e51b21f238..80214dd298 100644 --- a/mobile/test/unit/repositories/metadata_repository_test.dart +++ b/mobile/test/unit/repositories/settings_repository_test.dart @@ -1,10 +1,10 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/models/config/app_config.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; +import 'package:immich_mobile/domain/models/settings_key.dart'; void main() { - group('MetadataKey', () { - for (final key in MetadataKey.values) { + group('SettingsKey', () { + for (final key in SettingsKey.values) { test('verify codec for $key', () { final defaultValue = defaultConfig.read(key); final encoded = key.encode(defaultValue); From 65611bb860ff83f7931ad124c018edf750d871a4 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Sat, 30 May 2026 11:31:17 -0400 Subject: [PATCH 12/50] chore(mobile): make openapi requests abortable (#28692) make open-api requests abortable --- mobile/openapi/lib/api/activities_api.dart | 28 ++-- mobile/openapi/lib/api/albums_api.dart | 91 ++++++----- mobile/openapi/lib/api/api_keys_api.dart | 42 +++-- mobile/openapi/lib/api/assets_api.dart | 154 ++++++++++-------- .../lib/api/authentication_admin_api.dart | 7 +- .../openapi/lib/api/authentication_api.dart | 119 ++++++++------ .../lib/api/database_backups_admin_api.dart | 35 ++-- mobile/openapi/lib/api/deprecated_api.dart | 21 ++- mobile/openapi/lib/api/download_api.dart | 14 +- mobile/openapi/lib/api/duplicates_api.dart | 28 ++-- mobile/openapi/lib/api/faces_api.dart | 28 ++-- mobile/openapi/lib/api/jobs_api.dart | 21 ++- mobile/openapi/lib/api/libraries_api.dart | 56 ++++--- .../lib/api/maintenance_admin_api.dart | 28 ++-- mobile/openapi/lib/api/map_api.dart | 14 +- mobile/openapi/lib/api/memories_api.dart | 56 ++++--- .../lib/api/notifications_admin_api.dart | 21 ++- mobile/openapi/lib/api/notifications_api.dart | 42 +++-- mobile/openapi/lib/api/partners_api.dart | 35 ++-- mobile/openapi/lib/api/people_api.dart | 77 +++++---- mobile/openapi/lib/api/plugins_api.dart | 28 ++-- mobile/openapi/lib/api/queues_api.dart | 35 ++-- mobile/openapi/lib/api/search_api.dart | 70 ++++---- mobile/openapi/lib/api/server_api.dart | 98 ++++++----- mobile/openapi/lib/api/sessions_api.dart | 42 +++-- mobile/openapi/lib/api/shared_links_api.dart | 63 ++++--- mobile/openapi/lib/api/stacks_api.dart | 49 +++--- mobile/openapi/lib/api/sync_api.dart | 28 ++-- mobile/openapi/lib/api/system_config_api.dart | 28 ++-- .../openapi/lib/api/system_metadata_api.dart | 28 ++-- mobile/openapi/lib/api/tags_api.dart | 63 ++++--- mobile/openapi/lib/api/timeline_api.dart | 14 +- mobile/openapi/lib/api/trash_api.dart | 21 ++- mobile/openapi/lib/api/users_admin_api.dart | 70 ++++---- mobile/openapi/lib/api/users_api.dart | 105 +++++++----- mobile/openapi/lib/api/views_api.dart | 14 +- mobile/openapi/lib/api/workflows_api.dart | 49 +++--- mobile/openapi/lib/api_client.dart | 33 ++-- open-api/patch/api_client.dart.patch | 80 ++++++++- open-api/templates/mobile/api.mustache | 7 +- open-api/templates/mobile/api.mustache.patch | 31 +++- 41 files changed, 1104 insertions(+), 769 deletions(-) diff --git a/mobile/openapi/lib/api/activities_api.dart b/mobile/openapi/lib/api/activities_api.dart index e0a393948c..490c418785 100644 --- a/mobile/openapi/lib/api/activities_api.dart +++ b/mobile/openapi/lib/api/activities_api.dart @@ -25,7 +25,7 @@ class ActivitiesApi { /// Parameters: /// /// * [ActivityCreateDto] activityCreateDto (required): - Future createActivityWithHttpInfo(ActivityCreateDto activityCreateDto,) async { + Future createActivityWithHttpInfo(ActivityCreateDto activityCreateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/activities'; @@ -47,6 +47,7 @@ class ActivitiesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class ActivitiesApi { /// Parameters: /// /// * [ActivityCreateDto] activityCreateDto (required): - Future createActivity(ActivityCreateDto activityCreateDto,) async { - final response = await createActivityWithHttpInfo(activityCreateDto,); + Future createActivity(ActivityCreateDto activityCreateDto, { Future? abortTrigger, }) async { + final response = await createActivityWithHttpInfo(activityCreateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -81,7 +82,7 @@ class ActivitiesApi { /// Parameters: /// /// * [String] id (required): - Future deleteActivityWithHttpInfo(String id,) async { + Future deleteActivityWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/activities/{id}' .replaceAll('{id}', id); @@ -104,6 +105,7 @@ class ActivitiesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -114,8 +116,8 @@ class ActivitiesApi { /// Parameters: /// /// * [String] id (required): - Future deleteActivity(String id,) async { - final response = await deleteActivityWithHttpInfo(id,); + Future deleteActivity(String id, { Future? abortTrigger, }) async { + final response = await deleteActivityWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -141,7 +143,7 @@ class ActivitiesApi { /// /// * [String] userId: /// Filter by user ID - Future getActivitiesWithHttpInfo(String albumId, { String? assetId, ReactionLevel? level, ReactionType? type, String? userId, }) async { + Future getActivitiesWithHttpInfo(String albumId, { String? assetId, ReactionLevel? level, ReactionType? type, String? userId, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/activities'; @@ -177,6 +179,7 @@ class ActivitiesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -198,8 +201,8 @@ class ActivitiesApi { /// /// * [String] userId: /// Filter by user ID - Future?> getActivities(String albumId, { String? assetId, ReactionLevel? level, ReactionType? type, String? userId, }) async { - final response = await getActivitiesWithHttpInfo(albumId, assetId: assetId, level: level, type: type, userId: userId, ); + Future?> getActivities(String albumId, { String? assetId, ReactionLevel? level, ReactionType? type, String? userId, Future? abortTrigger, }) async { + final response = await getActivitiesWithHttpInfo(albumId, assetId: assetId, level: level, type: type, userId: userId, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -229,7 +232,7 @@ class ActivitiesApi { /// /// * [String] assetId: /// Asset ID (if activity is for an asset) - Future getActivityStatisticsWithHttpInfo(String albumId, { String? assetId, }) async { + Future getActivityStatisticsWithHttpInfo(String albumId, { String? assetId, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/activities/statistics'; @@ -256,6 +259,7 @@ class ActivitiesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -270,8 +274,8 @@ class ActivitiesApi { /// /// * [String] assetId: /// Asset ID (if activity is for an asset) - Future getActivityStatistics(String albumId, { String? assetId, }) async { - final response = await getActivityStatisticsWithHttpInfo(albumId, assetId: assetId, ); + Future getActivityStatistics(String albumId, { String? assetId, Future? abortTrigger, }) async { + final response = await getActivityStatisticsWithHttpInfo(albumId, assetId: assetId, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/albums_api.dart b/mobile/openapi/lib/api/albums_api.dart index c22a8ced07..e6e7cdbf40 100644 --- a/mobile/openapi/lib/api/albums_api.dart +++ b/mobile/openapi/lib/api/albums_api.dart @@ -27,7 +27,7 @@ class AlbumsApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - Future addAssetsToAlbumWithHttpInfo(String id, BulkIdsDto bulkIdsDto,) async { + Future addAssetsToAlbumWithHttpInfo(String id, BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums/{id}/assets' .replaceAll('{id}', id); @@ -50,6 +50,7 @@ class AlbumsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -62,8 +63,8 @@ class AlbumsApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - Future?> addAssetsToAlbum(String id, BulkIdsDto bulkIdsDto,) async { - final response = await addAssetsToAlbumWithHttpInfo(id, bulkIdsDto,); + Future?> addAssetsToAlbum(String id, BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { + final response = await addAssetsToAlbumWithHttpInfo(id, bulkIdsDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -89,7 +90,7 @@ class AlbumsApi { /// Parameters: /// /// * [AlbumsAddAssetsDto] albumsAddAssetsDto (required): - Future addAssetsToAlbumsWithHttpInfo(AlbumsAddAssetsDto albumsAddAssetsDto,) async { + Future addAssetsToAlbumsWithHttpInfo(AlbumsAddAssetsDto albumsAddAssetsDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums/assets'; @@ -111,6 +112,7 @@ class AlbumsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -121,8 +123,8 @@ class AlbumsApi { /// Parameters: /// /// * [AlbumsAddAssetsDto] albumsAddAssetsDto (required): - Future addAssetsToAlbums(AlbumsAddAssetsDto albumsAddAssetsDto,) async { - final response = await addAssetsToAlbumsWithHttpInfo(albumsAddAssetsDto,); + Future addAssetsToAlbums(AlbumsAddAssetsDto albumsAddAssetsDto, { Future? abortTrigger, }) async { + final response = await addAssetsToAlbumsWithHttpInfo(albumsAddAssetsDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -147,7 +149,7 @@ class AlbumsApi { /// * [String] id (required): /// /// * [AddUsersDto] addUsersDto (required): - Future addUsersToAlbumWithHttpInfo(String id, AddUsersDto addUsersDto,) async { + Future addUsersToAlbumWithHttpInfo(String id, AddUsersDto addUsersDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums/{id}/users' .replaceAll('{id}', id); @@ -170,6 +172,7 @@ class AlbumsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -182,8 +185,8 @@ class AlbumsApi { /// * [String] id (required): /// /// * [AddUsersDto] addUsersDto (required): - Future addUsersToAlbum(String id, AddUsersDto addUsersDto,) async { - final response = await addUsersToAlbumWithHttpInfo(id, addUsersDto,); + Future addUsersToAlbum(String id, AddUsersDto addUsersDto, { Future? abortTrigger, }) async { + final response = await addUsersToAlbumWithHttpInfo(id, addUsersDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -206,7 +209,7 @@ class AlbumsApi { /// Parameters: /// /// * [CreateAlbumDto] createAlbumDto (required): - Future createAlbumWithHttpInfo(CreateAlbumDto createAlbumDto,) async { + Future createAlbumWithHttpInfo(CreateAlbumDto createAlbumDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums'; @@ -228,6 +231,7 @@ class AlbumsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -238,8 +242,8 @@ class AlbumsApi { /// Parameters: /// /// * [CreateAlbumDto] createAlbumDto (required): - Future createAlbum(CreateAlbumDto createAlbumDto,) async { - final response = await createAlbumWithHttpInfo(createAlbumDto,); + Future createAlbum(CreateAlbumDto createAlbumDto, { Future? abortTrigger, }) async { + final response = await createAlbumWithHttpInfo(createAlbumDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -262,7 +266,7 @@ class AlbumsApi { /// Parameters: /// /// * [String] id (required): - Future deleteAlbumWithHttpInfo(String id,) async { + Future deleteAlbumWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums/{id}' .replaceAll('{id}', id); @@ -285,6 +289,7 @@ class AlbumsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -295,8 +300,8 @@ class AlbumsApi { /// Parameters: /// /// * [String] id (required): - Future deleteAlbum(String id,) async { - final response = await deleteAlbumWithHttpInfo(id,); + Future deleteAlbum(String id, { Future? abortTrigger, }) async { + final response = await deleteAlbumWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -315,7 +320,7 @@ class AlbumsApi { /// * [String] key: /// /// * [String] slug: - Future getAlbumInfoWithHttpInfo(String id, { String? key, String? slug, }) async { + Future getAlbumInfoWithHttpInfo(String id, { String? key, String? slug, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums/{id}' .replaceAll('{id}', id); @@ -345,6 +350,7 @@ class AlbumsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -359,8 +365,8 @@ class AlbumsApi { /// * [String] key: /// /// * [String] slug: - Future getAlbumInfo(String id, { String? key, String? slug, }) async { - final response = await getAlbumInfoWithHttpInfo(id, key: key, slug: slug, ); + Future getAlbumInfo(String id, { String? key, String? slug, Future? abortTrigger, }) async { + final response = await getAlbumInfoWithHttpInfo(id, key: key, slug: slug, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -387,7 +393,7 @@ class AlbumsApi { /// * [String] key: /// /// * [String] slug: - Future getAlbumMapMarkersWithHttpInfo(String id, { String? key, String? slug, }) async { + Future getAlbumMapMarkersWithHttpInfo(String id, { String? key, String? slug, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums/{id}/map-markers' .replaceAll('{id}', id); @@ -417,6 +423,7 @@ class AlbumsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -431,8 +438,8 @@ class AlbumsApi { /// * [String] key: /// /// * [String] slug: - Future?> getAlbumMapMarkers(String id, { String? key, String? slug, }) async { - final response = await getAlbumMapMarkersWithHttpInfo(id, key: key, slug: slug, ); + Future?> getAlbumMapMarkers(String id, { String? key, String? slug, Future? abortTrigger, }) async { + final response = await getAlbumMapMarkersWithHttpInfo(id, key: key, slug: slug, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -454,7 +461,7 @@ class AlbumsApi { /// Returns statistics about the albums available to the authenticated user. /// /// Note: This method returns the HTTP [Response]. - Future getAlbumStatisticsWithHttpInfo() async { + Future getAlbumStatisticsWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums/statistics'; @@ -476,14 +483,15 @@ class AlbumsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve album statistics /// /// Returns statistics about the albums available to the authenticated user. - Future getAlbumStatistics() async { - final response = await getAlbumStatisticsWithHttpInfo(); + Future getAlbumStatistics({ Future? abortTrigger, }) async { + final response = await getAlbumStatisticsWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -519,7 +527,7 @@ class AlbumsApi { /// /// * [String] name: /// Album name (exact match) - Future getAllAlbumsWithHttpInfo({ String? assetId, String? id, bool? isOwned, bool? isShared, String? name, }) async { + Future getAllAlbumsWithHttpInfo({ String? assetId, String? id, bool? isOwned, bool? isShared, String? name, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums'; @@ -557,6 +565,7 @@ class AlbumsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -580,8 +589,8 @@ class AlbumsApi { /// /// * [String] name: /// Album name (exact match) - Future?> getAllAlbums({ String? assetId, String? id, bool? isOwned, bool? isShared, String? name, }) async { - final response = await getAllAlbumsWithHttpInfo( assetId: assetId, id: id, isOwned: isOwned, isShared: isShared, name: name, ); + Future?> getAllAlbums({ String? assetId, String? id, bool? isOwned, bool? isShared, String? name, Future? abortTrigger, }) async { + final response = await getAllAlbumsWithHttpInfo(assetId: assetId, id: id, isOwned: isOwned, isShared: isShared, name: name, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -609,7 +618,7 @@ class AlbumsApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - Future removeAssetFromAlbumWithHttpInfo(String id, BulkIdsDto bulkIdsDto,) async { + Future removeAssetFromAlbumWithHttpInfo(String id, BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums/{id}/assets' .replaceAll('{id}', id); @@ -632,6 +641,7 @@ class AlbumsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -644,8 +654,8 @@ class AlbumsApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - Future?> removeAssetFromAlbum(String id, BulkIdsDto bulkIdsDto,) async { - final response = await removeAssetFromAlbumWithHttpInfo(id, bulkIdsDto,); + Future?> removeAssetFromAlbum(String id, BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { + final response = await removeAssetFromAlbumWithHttpInfo(id, bulkIdsDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -673,7 +683,7 @@ class AlbumsApi { /// * [String] id (required): /// /// * [String] userId (required): - Future removeUserFromAlbumWithHttpInfo(String id, String userId,) async { + Future removeUserFromAlbumWithHttpInfo(String id, String userId, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums/{id}/user/{userId}' .replaceAll('{id}', id) @@ -697,6 +707,7 @@ class AlbumsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -709,8 +720,8 @@ class AlbumsApi { /// * [String] id (required): /// /// * [String] userId (required): - Future removeUserFromAlbum(String id, String userId,) async { - final response = await removeUserFromAlbumWithHttpInfo(id, userId,); + Future removeUserFromAlbum(String id, String userId, { Future? abortTrigger, }) async { + final response = await removeUserFromAlbumWithHttpInfo(id, userId, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -727,7 +738,7 @@ class AlbumsApi { /// * [String] id (required): /// /// * [UpdateAlbumDto] updateAlbumDto (required): - Future updateAlbumInfoWithHttpInfo(String id, UpdateAlbumDto updateAlbumDto,) async { + Future updateAlbumInfoWithHttpInfo(String id, UpdateAlbumDto updateAlbumDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums/{id}' .replaceAll('{id}', id); @@ -750,6 +761,7 @@ class AlbumsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -762,8 +774,8 @@ class AlbumsApi { /// * [String] id (required): /// /// * [UpdateAlbumDto] updateAlbumDto (required): - Future updateAlbumInfo(String id, UpdateAlbumDto updateAlbumDto,) async { - final response = await updateAlbumInfoWithHttpInfo(id, updateAlbumDto,); + Future updateAlbumInfo(String id, UpdateAlbumDto updateAlbumDto, { Future? abortTrigger, }) async { + final response = await updateAlbumInfoWithHttpInfo(id, updateAlbumDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -790,7 +802,7 @@ class AlbumsApi { /// * [String] userId (required): /// /// * [UpdateAlbumUserDto] updateAlbumUserDto (required): - Future updateAlbumUserWithHttpInfo(String id, String userId, UpdateAlbumUserDto updateAlbumUserDto,) async { + Future updateAlbumUserWithHttpInfo(String id, String userId, UpdateAlbumUserDto updateAlbumUserDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums/{id}/user/{userId}' .replaceAll('{id}', id) @@ -814,6 +826,7 @@ class AlbumsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -828,8 +841,8 @@ class AlbumsApi { /// * [String] userId (required): /// /// * [UpdateAlbumUserDto] updateAlbumUserDto (required): - Future updateAlbumUser(String id, String userId, UpdateAlbumUserDto updateAlbumUserDto,) async { - final response = await updateAlbumUserWithHttpInfo(id, userId, updateAlbumUserDto,); + Future updateAlbumUser(String id, String userId, UpdateAlbumUserDto updateAlbumUserDto, { Future? abortTrigger, }) async { + final response = await updateAlbumUserWithHttpInfo(id, userId, updateAlbumUserDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/api_keys_api.dart b/mobile/openapi/lib/api/api_keys_api.dart index 3ca85265c4..c26ddc263d 100644 --- a/mobile/openapi/lib/api/api_keys_api.dart +++ b/mobile/openapi/lib/api/api_keys_api.dart @@ -25,7 +25,7 @@ class APIKeysApi { /// Parameters: /// /// * [ApiKeyCreateDto] apiKeyCreateDto (required): - Future createApiKeyWithHttpInfo(ApiKeyCreateDto apiKeyCreateDto,) async { + Future createApiKeyWithHttpInfo(ApiKeyCreateDto apiKeyCreateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/api-keys'; @@ -47,6 +47,7 @@ class APIKeysApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class APIKeysApi { /// Parameters: /// /// * [ApiKeyCreateDto] apiKeyCreateDto (required): - Future createApiKey(ApiKeyCreateDto apiKeyCreateDto,) async { - final response = await createApiKeyWithHttpInfo(apiKeyCreateDto,); + Future createApiKey(ApiKeyCreateDto apiKeyCreateDto, { Future? abortTrigger, }) async { + final response = await createApiKeyWithHttpInfo(apiKeyCreateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -81,7 +82,7 @@ class APIKeysApi { /// Parameters: /// /// * [String] id (required): - Future deleteApiKeyWithHttpInfo(String id,) async { + Future deleteApiKeyWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/api-keys/{id}' .replaceAll('{id}', id); @@ -104,6 +105,7 @@ class APIKeysApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -114,8 +116,8 @@ class APIKeysApi { /// Parameters: /// /// * [String] id (required): - Future deleteApiKey(String id,) async { - final response = await deleteApiKeyWithHttpInfo(id,); + Future deleteApiKey(String id, { Future? abortTrigger, }) async { + final response = await deleteApiKeyWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -130,7 +132,7 @@ class APIKeysApi { /// Parameters: /// /// * [String] id (required): - Future getApiKeyWithHttpInfo(String id,) async { + Future getApiKeyWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/api-keys/{id}' .replaceAll('{id}', id); @@ -153,6 +155,7 @@ class APIKeysApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -163,8 +166,8 @@ class APIKeysApi { /// Parameters: /// /// * [String] id (required): - Future getApiKey(String id,) async { - final response = await getApiKeyWithHttpInfo(id,); + Future getApiKey(String id, { Future? abortTrigger, }) async { + final response = await getApiKeyWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -183,7 +186,7 @@ class APIKeysApi { /// Retrieve all API keys of the current user. /// /// Note: This method returns the HTTP [Response]. - Future getApiKeysWithHttpInfo() async { + Future getApiKeysWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/api-keys'; @@ -205,14 +208,15 @@ class APIKeysApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// List all API keys /// /// Retrieve all API keys of the current user. - Future?> getApiKeys() async { - final response = await getApiKeysWithHttpInfo(); + Future?> getApiKeys({ Future? abortTrigger, }) async { + final response = await getApiKeysWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -234,7 +238,7 @@ class APIKeysApi { /// Retrieve the API key that is used to access this endpoint. /// /// Note: This method returns the HTTP [Response]. - Future getMyApiKeyWithHttpInfo() async { + Future getMyApiKeyWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/api-keys/me'; @@ -256,14 +260,15 @@ class APIKeysApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve the current API key /// /// Retrieve the API key that is used to access this endpoint. - Future getMyApiKey() async { - final response = await getMyApiKeyWithHttpInfo(); + Future getMyApiKey({ Future? abortTrigger, }) async { + final response = await getMyApiKeyWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -288,7 +293,7 @@ class APIKeysApi { /// * [String] id (required): /// /// * [ApiKeyUpdateDto] apiKeyUpdateDto (required): - Future updateApiKeyWithHttpInfo(String id, ApiKeyUpdateDto apiKeyUpdateDto,) async { + Future updateApiKeyWithHttpInfo(String id, ApiKeyUpdateDto apiKeyUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/api-keys/{id}' .replaceAll('{id}', id); @@ -311,6 +316,7 @@ class APIKeysApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -323,8 +329,8 @@ class APIKeysApi { /// * [String] id (required): /// /// * [ApiKeyUpdateDto] apiKeyUpdateDto (required): - Future updateApiKey(String id, ApiKeyUpdateDto apiKeyUpdateDto,) async { - final response = await updateApiKeyWithHttpInfo(id, apiKeyUpdateDto,); + Future updateApiKey(String id, ApiKeyUpdateDto apiKeyUpdateDto, { Future? abortTrigger, }) async { + final response = await updateApiKeyWithHttpInfo(id, apiKeyUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/assets_api.dart b/mobile/openapi/lib/api/assets_api.dart index 691c57cd3e..bb14238e02 100644 --- a/mobile/openapi/lib/api/assets_api.dart +++ b/mobile/openapi/lib/api/assets_api.dart @@ -25,7 +25,7 @@ class AssetsApi { /// Parameters: /// /// * [AssetBulkUploadCheckDto] assetBulkUploadCheckDto (required): - Future checkBulkUploadWithHttpInfo(AssetBulkUploadCheckDto assetBulkUploadCheckDto,) async { + Future checkBulkUploadWithHttpInfo(AssetBulkUploadCheckDto assetBulkUploadCheckDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/bulk-upload-check'; @@ -47,6 +47,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class AssetsApi { /// Parameters: /// /// * [AssetBulkUploadCheckDto] assetBulkUploadCheckDto (required): - Future checkBulkUpload(AssetBulkUploadCheckDto assetBulkUploadCheckDto,) async { - final response = await checkBulkUploadWithHttpInfo(assetBulkUploadCheckDto,); + Future checkBulkUpload(AssetBulkUploadCheckDto assetBulkUploadCheckDto, { Future? abortTrigger, }) async { + final response = await checkBulkUploadWithHttpInfo(assetBulkUploadCheckDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -81,7 +82,7 @@ class AssetsApi { /// Parameters: /// /// * [AssetCopyDto] assetCopyDto (required): - Future copyAssetWithHttpInfo(AssetCopyDto assetCopyDto,) async { + Future copyAssetWithHttpInfo(AssetCopyDto assetCopyDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/copy'; @@ -103,6 +104,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -113,8 +115,8 @@ class AssetsApi { /// Parameters: /// /// * [AssetCopyDto] assetCopyDto (required): - Future copyAsset(AssetCopyDto assetCopyDto,) async { - final response = await copyAssetWithHttpInfo(assetCopyDto,); + Future copyAsset(AssetCopyDto assetCopyDto, { Future? abortTrigger, }) async { + final response = await copyAssetWithHttpInfo(assetCopyDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -133,7 +135,7 @@ class AssetsApi { /// /// * [String] key (required): /// Metadata key - Future deleteAssetMetadataWithHttpInfo(String id, String key,) async { + Future deleteAssetMetadataWithHttpInfo(String id, String key, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/metadata/{key}' .replaceAll('{id}', id) @@ -157,6 +159,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -171,8 +174,8 @@ class AssetsApi { /// /// * [String] key (required): /// Metadata key - Future deleteAssetMetadata(String id, String key,) async { - final response = await deleteAssetMetadataWithHttpInfo(id, key,); + Future deleteAssetMetadata(String id, String key, { Future? abortTrigger, }) async { + final response = await deleteAssetMetadataWithHttpInfo(id, key, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -187,7 +190,7 @@ class AssetsApi { /// Parameters: /// /// * [AssetBulkDeleteDto] assetBulkDeleteDto (required): - Future deleteAssetsWithHttpInfo(AssetBulkDeleteDto assetBulkDeleteDto,) async { + Future deleteAssetsWithHttpInfo(AssetBulkDeleteDto assetBulkDeleteDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets'; @@ -209,6 +212,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -219,8 +223,8 @@ class AssetsApi { /// Parameters: /// /// * [AssetBulkDeleteDto] assetBulkDeleteDto (required): - Future deleteAssets(AssetBulkDeleteDto assetBulkDeleteDto,) async { - final response = await deleteAssetsWithHttpInfo(assetBulkDeleteDto,); + Future deleteAssets(AssetBulkDeleteDto assetBulkDeleteDto, { Future? abortTrigger, }) async { + final response = await deleteAssetsWithHttpInfo(assetBulkDeleteDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -235,7 +239,7 @@ class AssetsApi { /// Parameters: /// /// * [AssetMetadataBulkDeleteDto] assetMetadataBulkDeleteDto (required): - Future deleteBulkAssetMetadataWithHttpInfo(AssetMetadataBulkDeleteDto assetMetadataBulkDeleteDto,) async { + Future deleteBulkAssetMetadataWithHttpInfo(AssetMetadataBulkDeleteDto assetMetadataBulkDeleteDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/metadata'; @@ -257,6 +261,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -267,8 +272,8 @@ class AssetsApi { /// Parameters: /// /// * [AssetMetadataBulkDeleteDto] assetMetadataBulkDeleteDto (required): - Future deleteBulkAssetMetadata(AssetMetadataBulkDeleteDto assetMetadataBulkDeleteDto,) async { - final response = await deleteBulkAssetMetadataWithHttpInfo(assetMetadataBulkDeleteDto,); + Future deleteBulkAssetMetadata(AssetMetadataBulkDeleteDto assetMetadataBulkDeleteDto, { Future? abortTrigger, }) async { + final response = await deleteBulkAssetMetadataWithHttpInfo(assetMetadataBulkDeleteDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -290,7 +295,7 @@ class AssetsApi { /// * [String] key: /// /// * [String] slug: - Future downloadAssetWithHttpInfo(String id, { bool? edited, String? key, String? slug, }) async { + Future downloadAssetWithHttpInfo(String id, { bool? edited, String? key, String? slug, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/original' .replaceAll('{id}', id); @@ -323,6 +328,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -340,8 +346,8 @@ class AssetsApi { /// * [String] key: /// /// * [String] slug: - Future downloadAsset(String id, { bool? edited, String? key, String? slug, }) async { - final response = await downloadAssetWithHttpInfo(id, edited: edited, key: key, slug: slug, ); + Future downloadAsset(String id, { bool? edited, String? key, String? slug, Future? abortTrigger, }) async { + final response = await downloadAssetWithHttpInfo(id, edited: edited, key: key, slug: slug, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -366,7 +372,7 @@ class AssetsApi { /// * [String] id (required): /// /// * [AssetEditsCreateDto] assetEditsCreateDto (required): - Future editAssetWithHttpInfo(String id, AssetEditsCreateDto assetEditsCreateDto,) async { + Future editAssetWithHttpInfo(String id, AssetEditsCreateDto assetEditsCreateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/edits' .replaceAll('{id}', id); @@ -389,6 +395,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -401,8 +408,8 @@ class AssetsApi { /// * [String] id (required): /// /// * [AssetEditsCreateDto] assetEditsCreateDto (required): - Future editAsset(String id, AssetEditsCreateDto assetEditsCreateDto,) async { - final response = await editAssetWithHttpInfo(id, assetEditsCreateDto,); + Future editAsset(String id, AssetEditsCreateDto assetEditsCreateDto, { Future? abortTrigger, }) async { + final response = await editAssetWithHttpInfo(id, assetEditsCreateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -425,7 +432,7 @@ class AssetsApi { /// Parameters: /// /// * [String] id (required): - Future getAssetEditsWithHttpInfo(String id,) async { + Future getAssetEditsWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/edits' .replaceAll('{id}', id); @@ -448,6 +455,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -458,8 +466,8 @@ class AssetsApi { /// Parameters: /// /// * [String] id (required): - Future getAssetEdits(String id,) async { - final response = await getAssetEditsWithHttpInfo(id,); + Future getAssetEdits(String id, { Future? abortTrigger, }) async { + final response = await getAssetEditsWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -486,7 +494,7 @@ class AssetsApi { /// * [String] key: /// /// * [String] slug: - Future getAssetInfoWithHttpInfo(String id, { String? key, String? slug, }) async { + Future getAssetInfoWithHttpInfo(String id, { String? key, String? slug, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}' .replaceAll('{id}', id); @@ -516,6 +524,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -530,8 +539,8 @@ class AssetsApi { /// * [String] key: /// /// * [String] slug: - Future getAssetInfo(String id, { String? key, String? slug, }) async { - final response = await getAssetInfoWithHttpInfo(id, key: key, slug: slug, ); + Future getAssetInfo(String id, { String? key, String? slug, Future? abortTrigger, }) async { + final response = await getAssetInfoWithHttpInfo(id, key: key, slug: slug, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -554,7 +563,7 @@ class AssetsApi { /// Parameters: /// /// * [String] id (required): - Future getAssetMetadataWithHttpInfo(String id,) async { + Future getAssetMetadataWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/metadata' .replaceAll('{id}', id); @@ -577,6 +586,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -587,8 +597,8 @@ class AssetsApi { /// Parameters: /// /// * [String] id (required): - Future?> getAssetMetadata(String id,) async { - final response = await getAssetMetadataWithHttpInfo(id,); + Future?> getAssetMetadata(String id, { Future? abortTrigger, }) async { + final response = await getAssetMetadataWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -618,7 +628,7 @@ class AssetsApi { /// /// * [String] key (required): /// Metadata key - Future getAssetMetadataByKeyWithHttpInfo(String id, String key,) async { + Future getAssetMetadataByKeyWithHttpInfo(String id, String key, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/metadata/{key}' .replaceAll('{id}', id) @@ -642,6 +652,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -656,8 +667,8 @@ class AssetsApi { /// /// * [String] key (required): /// Metadata key - Future getAssetMetadataByKey(String id, String key,) async { - final response = await getAssetMetadataByKeyWithHttpInfo(id, key,); + Future getAssetMetadataByKey(String id, String key, { Future? abortTrigger, }) async { + final response = await getAssetMetadataByKeyWithHttpInfo(id, key, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -680,7 +691,7 @@ class AssetsApi { /// Parameters: /// /// * [String] id (required): - Future getAssetOcrWithHttpInfo(String id,) async { + Future getAssetOcrWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/ocr' .replaceAll('{id}', id); @@ -703,6 +714,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -713,8 +725,8 @@ class AssetsApi { /// Parameters: /// /// * [String] id (required): - Future?> getAssetOcr(String id,) async { - final response = await getAssetOcrWithHttpInfo(id,); + Future?> getAssetOcr(String id, { Future? abortTrigger, }) async { + final response = await getAssetOcrWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -746,7 +758,7 @@ class AssetsApi { /// Filter by trash status /// /// * [AssetVisibility] visibility: - Future getAssetStatisticsWithHttpInfo({ bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { + Future getAssetStatisticsWithHttpInfo({ bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/statistics'; @@ -778,6 +790,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -794,8 +807,8 @@ class AssetsApi { /// Filter by trash status /// /// * [AssetVisibility] visibility: - Future getAssetStatistics({ bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { - final response = await getAssetStatisticsWithHttpInfo( isFavorite: isFavorite, isTrashed: isTrashed, visibility: visibility, ); + Future getAssetStatistics({ bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, Future? abortTrigger, }) async { + final response = await getAssetStatisticsWithHttpInfo(isFavorite: isFavorite, isTrashed: isTrashed, visibility: visibility, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -822,7 +835,7 @@ class AssetsApi { /// * [String] key: /// /// * [String] slug: - Future playAssetVideoWithHttpInfo(String id, { String? key, String? slug, }) async { + Future playAssetVideoWithHttpInfo(String id, { String? key, String? slug, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/video/playback' .replaceAll('{id}', id); @@ -852,6 +865,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -866,8 +880,8 @@ class AssetsApi { /// * [String] key: /// /// * [String] slug: - Future playAssetVideo(String id, { String? key, String? slug, }) async { - final response = await playAssetVideoWithHttpInfo(id, key: key, slug: slug, ); + Future playAssetVideo(String id, { String? key, String? slug, Future? abortTrigger, }) async { + final response = await playAssetVideoWithHttpInfo(id, key: key, slug: slug, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -890,7 +904,7 @@ class AssetsApi { /// Parameters: /// /// * [String] id (required): - Future removeAssetEditsWithHttpInfo(String id,) async { + Future removeAssetEditsWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/edits' .replaceAll('{id}', id); @@ -913,6 +927,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -923,8 +938,8 @@ class AssetsApi { /// Parameters: /// /// * [String] id (required): - Future removeAssetEdits(String id,) async { - final response = await removeAssetEditsWithHttpInfo(id,); + Future removeAssetEdits(String id, { Future? abortTrigger, }) async { + final response = await removeAssetEditsWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -939,7 +954,7 @@ class AssetsApi { /// Parameters: /// /// * [AssetJobsDto] assetJobsDto (required): - Future runAssetJobsWithHttpInfo(AssetJobsDto assetJobsDto,) async { + Future runAssetJobsWithHttpInfo(AssetJobsDto assetJobsDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/jobs'; @@ -961,6 +976,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -971,8 +987,8 @@ class AssetsApi { /// Parameters: /// /// * [AssetJobsDto] assetJobsDto (required): - Future runAssetJobs(AssetJobsDto assetJobsDto,) async { - final response = await runAssetJobsWithHttpInfo(assetJobsDto,); + Future runAssetJobs(AssetJobsDto assetJobsDto, { Future? abortTrigger, }) async { + final response = await runAssetJobsWithHttpInfo(assetJobsDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -989,7 +1005,7 @@ class AssetsApi { /// * [String] id (required): /// /// * [UpdateAssetDto] updateAssetDto (required): - Future updateAssetWithHttpInfo(String id, UpdateAssetDto updateAssetDto,) async { + Future updateAssetWithHttpInfo(String id, UpdateAssetDto updateAssetDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}' .replaceAll('{id}', id); @@ -1012,6 +1028,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -1024,8 +1041,8 @@ class AssetsApi { /// * [String] id (required): /// /// * [UpdateAssetDto] updateAssetDto (required): - Future updateAsset(String id, UpdateAssetDto updateAssetDto,) async { - final response = await updateAssetWithHttpInfo(id, updateAssetDto,); + Future updateAsset(String id, UpdateAssetDto updateAssetDto, { Future? abortTrigger, }) async { + final response = await updateAssetWithHttpInfo(id, updateAssetDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -1050,7 +1067,7 @@ class AssetsApi { /// * [String] id (required): /// /// * [AssetMetadataUpsertDto] assetMetadataUpsertDto (required): - Future updateAssetMetadataWithHttpInfo(String id, AssetMetadataUpsertDto assetMetadataUpsertDto,) async { + Future updateAssetMetadataWithHttpInfo(String id, AssetMetadataUpsertDto assetMetadataUpsertDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/metadata' .replaceAll('{id}', id); @@ -1073,6 +1090,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -1085,8 +1103,8 @@ class AssetsApi { /// * [String] id (required): /// /// * [AssetMetadataUpsertDto] assetMetadataUpsertDto (required): - Future?> updateAssetMetadata(String id, AssetMetadataUpsertDto assetMetadataUpsertDto,) async { - final response = await updateAssetMetadataWithHttpInfo(id, assetMetadataUpsertDto,); + Future?> updateAssetMetadata(String id, AssetMetadataUpsertDto assetMetadataUpsertDto, { Future? abortTrigger, }) async { + final response = await updateAssetMetadataWithHttpInfo(id, assetMetadataUpsertDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -1112,7 +1130,7 @@ class AssetsApi { /// Parameters: /// /// * [AssetBulkUpdateDto] assetBulkUpdateDto (required): - Future updateAssetsWithHttpInfo(AssetBulkUpdateDto assetBulkUpdateDto,) async { + Future updateAssetsWithHttpInfo(AssetBulkUpdateDto assetBulkUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets'; @@ -1134,6 +1152,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -1144,8 +1163,8 @@ class AssetsApi { /// Parameters: /// /// * [AssetBulkUpdateDto] assetBulkUpdateDto (required): - Future updateAssets(AssetBulkUpdateDto assetBulkUpdateDto,) async { - final response = await updateAssetsWithHttpInfo(assetBulkUpdateDto,); + Future updateAssets(AssetBulkUpdateDto assetBulkUpdateDto, { Future? abortTrigger, }) async { + final response = await updateAssetsWithHttpInfo(assetBulkUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -1160,7 +1179,7 @@ class AssetsApi { /// Parameters: /// /// * [AssetMetadataBulkUpsertDto] assetMetadataBulkUpsertDto (required): - Future updateBulkAssetMetadataWithHttpInfo(AssetMetadataBulkUpsertDto assetMetadataBulkUpsertDto,) async { + Future updateBulkAssetMetadataWithHttpInfo(AssetMetadataBulkUpsertDto assetMetadataBulkUpsertDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/metadata'; @@ -1182,6 +1201,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -1192,8 +1212,8 @@ class AssetsApi { /// Parameters: /// /// * [AssetMetadataBulkUpsertDto] assetMetadataBulkUpsertDto (required): - Future?> updateBulkAssetMetadata(AssetMetadataBulkUpsertDto assetMetadataBulkUpsertDto,) async { - final response = await updateBulkAssetMetadataWithHttpInfo(assetMetadataBulkUpsertDto,); + Future?> updateBulkAssetMetadata(AssetMetadataBulkUpsertDto assetMetadataBulkUpsertDto, { Future? abortTrigger, }) async { + final response = await updateBulkAssetMetadataWithHttpInfo(assetMetadataBulkUpsertDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -1253,7 +1273,7 @@ class AssetsApi { /// Sidecar file data /// /// * [AssetVisibility] visibility: - Future uploadAssetWithHttpInfo(MultipartFile assetData, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, int? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { + Future uploadAssetWithHttpInfo(MultipartFile assetData, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, int? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets'; @@ -1333,6 +1353,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -1377,8 +1398,8 @@ class AssetsApi { /// Sidecar file data /// /// * [AssetVisibility] visibility: - Future uploadAsset(MultipartFile assetData, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, int? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { - final response = await uploadAssetWithHttpInfo(assetData, fileCreatedAt, fileModifiedAt, key: key, slug: slug, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, metadata: metadata, sidecarData: sidecarData, visibility: visibility, ); + Future uploadAsset(MultipartFile assetData, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, int? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, Future? abortTrigger, }) async { + final response = await uploadAssetWithHttpInfo(assetData, fileCreatedAt, fileModifiedAt, key: key, slug: slug, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, metadata: metadata, sidecarData: sidecarData, visibility: visibility, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -1410,7 +1431,7 @@ class AssetsApi { /// * [AssetMediaSize] size: /// /// * [String] slug: - Future viewAssetWithHttpInfo(String id, { bool? edited, String? key, AssetMediaSize? size, String? slug, }) async { + Future viewAssetWithHttpInfo(String id, { bool? edited, String? key, AssetMediaSize? size, String? slug, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/thumbnail' .replaceAll('{id}', id); @@ -1446,6 +1467,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -1465,8 +1487,8 @@ class AssetsApi { /// * [AssetMediaSize] size: /// /// * [String] slug: - Future viewAsset(String id, { bool? edited, String? key, AssetMediaSize? size, String? slug, }) async { - final response = await viewAssetWithHttpInfo(id, edited: edited, key: key, size: size, slug: slug, ); + Future viewAsset(String id, { bool? edited, String? key, AssetMediaSize? size, String? slug, Future? abortTrigger, }) async { + final response = await viewAssetWithHttpInfo(id, edited: edited, key: key, size: size, slug: slug, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/authentication_admin_api.dart b/mobile/openapi/lib/api/authentication_admin_api.dart index 0a4b91ebc3..2c107891b3 100644 --- a/mobile/openapi/lib/api/authentication_admin_api.dart +++ b/mobile/openapi/lib/api/authentication_admin_api.dart @@ -21,7 +21,7 @@ class AuthenticationAdminApi { /// Unlinks all OAuth accounts associated with user accounts in the system. /// /// Note: This method returns the HTTP [Response]. - Future unlinkAllOAuthAccountsAdminWithHttpInfo() async { + Future unlinkAllOAuthAccountsAdminWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/auth/unlink-all'; @@ -43,14 +43,15 @@ class AuthenticationAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Unlink all OAuth accounts /// /// Unlinks all OAuth accounts associated with user accounts in the system. - Future unlinkAllOAuthAccountsAdmin() async { - final response = await unlinkAllOAuthAccountsAdminWithHttpInfo(); + Future unlinkAllOAuthAccountsAdmin({ Future? abortTrigger, }) async { + final response = await unlinkAllOAuthAccountsAdminWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/authentication_api.dart b/mobile/openapi/lib/api/authentication_api.dart index e1219f2c03..8e088d040b 100644 --- a/mobile/openapi/lib/api/authentication_api.dart +++ b/mobile/openapi/lib/api/authentication_api.dart @@ -25,7 +25,7 @@ class AuthenticationApi { /// Parameters: /// /// * [ChangePasswordDto] changePasswordDto (required): - Future changePasswordWithHttpInfo(ChangePasswordDto changePasswordDto,) async { + Future changePasswordWithHttpInfo(ChangePasswordDto changePasswordDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/auth/change-password'; @@ -47,6 +47,7 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class AuthenticationApi { /// Parameters: /// /// * [ChangePasswordDto] changePasswordDto (required): - Future changePassword(ChangePasswordDto changePasswordDto,) async { - final response = await changePasswordWithHttpInfo(changePasswordDto,); + Future changePassword(ChangePasswordDto changePasswordDto, { Future? abortTrigger, }) async { + final response = await changePasswordWithHttpInfo(changePasswordDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -81,7 +82,7 @@ class AuthenticationApi { /// Parameters: /// /// * [PinCodeChangeDto] pinCodeChangeDto (required): - Future changePinCodeWithHttpInfo(PinCodeChangeDto pinCodeChangeDto,) async { + Future changePinCodeWithHttpInfo(PinCodeChangeDto pinCodeChangeDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/auth/pin-code'; @@ -103,6 +104,7 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -113,8 +115,8 @@ class AuthenticationApi { /// Parameters: /// /// * [PinCodeChangeDto] pinCodeChangeDto (required): - Future changePinCode(PinCodeChangeDto pinCodeChangeDto,) async { - final response = await changePinCodeWithHttpInfo(pinCodeChangeDto,); + Future changePinCode(PinCodeChangeDto pinCodeChangeDto, { Future? abortTrigger, }) async { + final response = await changePinCodeWithHttpInfo(pinCodeChangeDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -129,7 +131,7 @@ class AuthenticationApi { /// Parameters: /// /// * [OAuthCallbackDto] oAuthCallbackDto (required): - Future finishOAuthWithHttpInfo(OAuthCallbackDto oAuthCallbackDto,) async { + Future finishOAuthWithHttpInfo(OAuthCallbackDto oAuthCallbackDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/oauth/callback'; @@ -151,6 +153,7 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -161,8 +164,8 @@ class AuthenticationApi { /// Parameters: /// /// * [OAuthCallbackDto] oAuthCallbackDto (required): - Future finishOAuth(OAuthCallbackDto oAuthCallbackDto,) async { - final response = await finishOAuthWithHttpInfo(oAuthCallbackDto,); + Future finishOAuth(OAuthCallbackDto oAuthCallbackDto, { Future? abortTrigger, }) async { + final response = await finishOAuthWithHttpInfo(oAuthCallbackDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -181,7 +184,7 @@ class AuthenticationApi { /// Get information about the current session, including whether the user has a password, and if the session can access locked assets. /// /// Note: This method returns the HTTP [Response]. - Future getAuthStatusWithHttpInfo() async { + Future getAuthStatusWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/auth/status'; @@ -203,14 +206,15 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve auth status /// /// Get information about the current session, including whether the user has a password, and if the session can access locked assets. - Future getAuthStatus() async { - final response = await getAuthStatusWithHttpInfo(); + Future getAuthStatus({ Future? abortTrigger, }) async { + final response = await getAuthStatusWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -233,7 +237,7 @@ class AuthenticationApi { /// Parameters: /// /// * [OAuthCallbackDto] oAuthCallbackDto (required): - Future linkOAuthAccountWithHttpInfo(OAuthCallbackDto oAuthCallbackDto,) async { + Future linkOAuthAccountWithHttpInfo(OAuthCallbackDto oAuthCallbackDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/oauth/link'; @@ -255,6 +259,7 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -265,8 +270,8 @@ class AuthenticationApi { /// Parameters: /// /// * [OAuthCallbackDto] oAuthCallbackDto (required): - Future linkOAuthAccount(OAuthCallbackDto oAuthCallbackDto,) async { - final response = await linkOAuthAccountWithHttpInfo(oAuthCallbackDto,); + Future linkOAuthAccount(OAuthCallbackDto oAuthCallbackDto, { Future? abortTrigger, }) async { + final response = await linkOAuthAccountWithHttpInfo(oAuthCallbackDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -285,7 +290,7 @@ class AuthenticationApi { /// Remove elevated access to locked assets from the current session. /// /// Note: This method returns the HTTP [Response]. - Future lockAuthSessionWithHttpInfo() async { + Future lockAuthSessionWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/auth/session/lock'; @@ -307,14 +312,15 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Lock auth session /// /// Remove elevated access to locked assets from the current session. - Future lockAuthSession() async { - final response = await lockAuthSessionWithHttpInfo(); + Future lockAuthSession({ Future? abortTrigger, }) async { + final response = await lockAuthSessionWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -329,7 +335,7 @@ class AuthenticationApi { /// Parameters: /// /// * [LoginCredentialDto] loginCredentialDto (required): - Future loginWithHttpInfo(LoginCredentialDto loginCredentialDto,) async { + Future loginWithHttpInfo(LoginCredentialDto loginCredentialDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/auth/login'; @@ -351,6 +357,7 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -361,8 +368,8 @@ class AuthenticationApi { /// Parameters: /// /// * [LoginCredentialDto] loginCredentialDto (required): - Future login(LoginCredentialDto loginCredentialDto,) async { - final response = await loginWithHttpInfo(loginCredentialDto,); + Future login(LoginCredentialDto loginCredentialDto, { Future? abortTrigger, }) async { + final response = await loginWithHttpInfo(loginCredentialDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -381,7 +388,7 @@ class AuthenticationApi { /// Logout the current user and invalidate the session token. /// /// Note: This method returns the HTTP [Response]. - Future logoutWithHttpInfo() async { + Future logoutWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/auth/logout'; @@ -403,14 +410,15 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Logout /// /// Logout the current user and invalidate the session token. - Future logout() async { - final response = await logoutWithHttpInfo(); + Future logout({ Future? abortTrigger, }) async { + final response = await logoutWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -434,7 +442,7 @@ class AuthenticationApi { /// /// * [String] logoutToken (required): /// OAuth logout token - Future logoutOAuthWithHttpInfo(String logoutToken,) async { + Future logoutOAuthWithHttpInfo(String logoutToken, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/oauth/backchannel-logout'; @@ -459,6 +467,7 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -470,8 +479,8 @@ class AuthenticationApi { /// /// * [String] logoutToken (required): /// OAuth logout token - Future logoutOAuth(String logoutToken,) async { - final response = await logoutOAuthWithHttpInfo(logoutToken,); + Future logoutOAuth(String logoutToken, { Future? abortTrigger, }) async { + final response = await logoutOAuthWithHttpInfo(logoutToken, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -482,7 +491,7 @@ class AuthenticationApi { /// Requests to this URL are automatically forwarded to the mobile app, and is used in some cases for OAuth redirecting. /// /// Note: This method returns the HTTP [Response]. - Future redirectOAuthToMobileWithHttpInfo() async { + Future redirectOAuthToMobileWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/oauth/mobile-redirect'; @@ -504,14 +513,15 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Redirect OAuth to mobile /// /// Requests to this URL are automatically forwarded to the mobile app, and is used in some cases for OAuth redirecting. - Future redirectOAuthToMobile() async { - final response = await redirectOAuthToMobileWithHttpInfo(); + Future redirectOAuthToMobile({ Future? abortTrigger, }) async { + final response = await redirectOAuthToMobileWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -526,7 +536,7 @@ class AuthenticationApi { /// Parameters: /// /// * [PinCodeResetDto] pinCodeResetDto (required): - Future resetPinCodeWithHttpInfo(PinCodeResetDto pinCodeResetDto,) async { + Future resetPinCodeWithHttpInfo(PinCodeResetDto pinCodeResetDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/auth/pin-code'; @@ -548,6 +558,7 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -558,8 +569,8 @@ class AuthenticationApi { /// Parameters: /// /// * [PinCodeResetDto] pinCodeResetDto (required): - Future resetPinCode(PinCodeResetDto pinCodeResetDto,) async { - final response = await resetPinCodeWithHttpInfo(pinCodeResetDto,); + Future resetPinCode(PinCodeResetDto pinCodeResetDto, { Future? abortTrigger, }) async { + final response = await resetPinCodeWithHttpInfo(pinCodeResetDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -574,7 +585,7 @@ class AuthenticationApi { /// Parameters: /// /// * [PinCodeSetupDto] pinCodeSetupDto (required): - Future setupPinCodeWithHttpInfo(PinCodeSetupDto pinCodeSetupDto,) async { + Future setupPinCodeWithHttpInfo(PinCodeSetupDto pinCodeSetupDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/auth/pin-code'; @@ -596,6 +607,7 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -606,8 +618,8 @@ class AuthenticationApi { /// Parameters: /// /// * [PinCodeSetupDto] pinCodeSetupDto (required): - Future setupPinCode(PinCodeSetupDto pinCodeSetupDto,) async { - final response = await setupPinCodeWithHttpInfo(pinCodeSetupDto,); + Future setupPinCode(PinCodeSetupDto pinCodeSetupDto, { Future? abortTrigger, }) async { + final response = await setupPinCodeWithHttpInfo(pinCodeSetupDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -622,7 +634,7 @@ class AuthenticationApi { /// Parameters: /// /// * [SignUpDto] signUpDto (required): - Future signUpAdminWithHttpInfo(SignUpDto signUpDto,) async { + Future signUpAdminWithHttpInfo(SignUpDto signUpDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/auth/admin-sign-up'; @@ -644,6 +656,7 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -654,8 +667,8 @@ class AuthenticationApi { /// Parameters: /// /// * [SignUpDto] signUpDto (required): - Future signUpAdmin(SignUpDto signUpDto,) async { - final response = await signUpAdminWithHttpInfo(signUpDto,); + Future signUpAdmin(SignUpDto signUpDto, { Future? abortTrigger, }) async { + final response = await signUpAdminWithHttpInfo(signUpDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -678,7 +691,7 @@ class AuthenticationApi { /// Parameters: /// /// * [OAuthConfigDto] oAuthConfigDto (required): - Future startOAuthWithHttpInfo(OAuthConfigDto oAuthConfigDto,) async { + Future startOAuthWithHttpInfo(OAuthConfigDto oAuthConfigDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/oauth/authorize'; @@ -700,6 +713,7 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -710,8 +724,8 @@ class AuthenticationApi { /// Parameters: /// /// * [OAuthConfigDto] oAuthConfigDto (required): - Future startOAuth(OAuthConfigDto oAuthConfigDto,) async { - final response = await startOAuthWithHttpInfo(oAuthConfigDto,); + Future startOAuth(OAuthConfigDto oAuthConfigDto, { Future? abortTrigger, }) async { + final response = await startOAuthWithHttpInfo(oAuthConfigDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -730,7 +744,7 @@ class AuthenticationApi { /// Unlink the OAuth account from the authenticated user. /// /// Note: This method returns the HTTP [Response]. - Future unlinkOAuthAccountWithHttpInfo() async { + Future unlinkOAuthAccountWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/oauth/unlink'; @@ -752,14 +766,15 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Unlink OAuth account /// /// Unlink the OAuth account from the authenticated user. - Future unlinkOAuthAccount() async { - final response = await unlinkOAuthAccountWithHttpInfo(); + Future unlinkOAuthAccount({ Future? abortTrigger, }) async { + final response = await unlinkOAuthAccountWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -782,7 +797,7 @@ class AuthenticationApi { /// Parameters: /// /// * [SessionUnlockDto] sessionUnlockDto (required): - Future unlockAuthSessionWithHttpInfo(SessionUnlockDto sessionUnlockDto,) async { + Future unlockAuthSessionWithHttpInfo(SessionUnlockDto sessionUnlockDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/auth/session/unlock'; @@ -804,6 +819,7 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -814,8 +830,8 @@ class AuthenticationApi { /// Parameters: /// /// * [SessionUnlockDto] sessionUnlockDto (required): - Future unlockAuthSession(SessionUnlockDto sessionUnlockDto,) async { - final response = await unlockAuthSessionWithHttpInfo(sessionUnlockDto,); + Future unlockAuthSession(SessionUnlockDto sessionUnlockDto, { Future? abortTrigger, }) async { + final response = await unlockAuthSessionWithHttpInfo(sessionUnlockDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -826,7 +842,7 @@ class AuthenticationApi { /// Validate the current authorization method is still valid. /// /// Note: This method returns the HTTP [Response]. - Future validateAccessTokenWithHttpInfo() async { + Future validateAccessTokenWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/auth/validateToken'; @@ -848,14 +864,15 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Validate access token /// /// Validate the current authorization method is still valid. - Future validateAccessToken() async { - final response = await validateAccessTokenWithHttpInfo(); + Future validateAccessToken({ Future? abortTrigger, }) async { + final response = await validateAccessTokenWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/database_backups_admin_api.dart b/mobile/openapi/lib/api/database_backups_admin_api.dart index 768185db1e..ba393833b5 100644 --- a/mobile/openapi/lib/api/database_backups_admin_api.dart +++ b/mobile/openapi/lib/api/database_backups_admin_api.dart @@ -25,7 +25,7 @@ class DatabaseBackupsAdminApi { /// Parameters: /// /// * [DatabaseBackupDeleteDto] databaseBackupDeleteDto (required): - Future deleteDatabaseBackupWithHttpInfo(DatabaseBackupDeleteDto databaseBackupDeleteDto,) async { + Future deleteDatabaseBackupWithHttpInfo(DatabaseBackupDeleteDto databaseBackupDeleteDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/database-backups'; @@ -47,6 +47,7 @@ class DatabaseBackupsAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class DatabaseBackupsAdminApi { /// Parameters: /// /// * [DatabaseBackupDeleteDto] databaseBackupDeleteDto (required): - Future deleteDatabaseBackup(DatabaseBackupDeleteDto databaseBackupDeleteDto,) async { - final response = await deleteDatabaseBackupWithHttpInfo(databaseBackupDeleteDto,); + Future deleteDatabaseBackup(DatabaseBackupDeleteDto databaseBackupDeleteDto, { Future? abortTrigger, }) async { + final response = await deleteDatabaseBackupWithHttpInfo(databaseBackupDeleteDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -73,7 +74,7 @@ class DatabaseBackupsAdminApi { /// Parameters: /// /// * [String] filename (required): - Future downloadDatabaseBackupWithHttpInfo(String filename,) async { + Future downloadDatabaseBackupWithHttpInfo(String filename, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/database-backups/{filename}' .replaceAll('{filename}', filename); @@ -96,6 +97,7 @@ class DatabaseBackupsAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -106,8 +108,8 @@ class DatabaseBackupsAdminApi { /// Parameters: /// /// * [String] filename (required): - Future downloadDatabaseBackup(String filename,) async { - final response = await downloadDatabaseBackupWithHttpInfo(filename,); + Future downloadDatabaseBackup(String filename, { Future? abortTrigger, }) async { + final response = await downloadDatabaseBackupWithHttpInfo(filename, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -126,7 +128,7 @@ class DatabaseBackupsAdminApi { /// Get the list of the successful and failed backups /// /// Note: This method returns the HTTP [Response]. - Future listDatabaseBackupsWithHttpInfo() async { + Future listDatabaseBackupsWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/database-backups'; @@ -148,14 +150,15 @@ class DatabaseBackupsAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// List database backups /// /// Get the list of the successful and failed backups - Future listDatabaseBackups() async { - final response = await listDatabaseBackupsWithHttpInfo(); + Future listDatabaseBackups({ Future? abortTrigger, }) async { + final response = await listDatabaseBackupsWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -174,7 +177,7 @@ class DatabaseBackupsAdminApi { /// Put Immich into maintenance mode to restore a backup (Immich must not be configured) /// /// Note: This method returns the HTTP [Response]. - Future startDatabaseRestoreFlowWithHttpInfo() async { + Future startDatabaseRestoreFlowWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/database-backups/start-restore'; @@ -196,14 +199,15 @@ class DatabaseBackupsAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Start database backup restore flow /// /// Put Immich into maintenance mode to restore a backup (Immich must not be configured) - Future startDatabaseRestoreFlow() async { - final response = await startDatabaseRestoreFlowWithHttpInfo(); + Future startDatabaseRestoreFlow({ Future? abortTrigger, }) async { + final response = await startDatabaseRestoreFlowWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -219,7 +223,7 @@ class DatabaseBackupsAdminApi { /// /// * [MultipartFile] file: /// Database backup file - Future uploadDatabaseBackupWithHttpInfo({ MultipartFile? file, }) async { + Future uploadDatabaseBackupWithHttpInfo({ MultipartFile? file, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/database-backups/upload'; @@ -251,6 +255,7 @@ class DatabaseBackupsAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -262,8 +267,8 @@ class DatabaseBackupsAdminApi { /// /// * [MultipartFile] file: /// Database backup file - Future uploadDatabaseBackup({ MultipartFile? file, }) async { - final response = await uploadDatabaseBackupWithHttpInfo( file: file, ); + Future uploadDatabaseBackup({ MultipartFile? file, Future? abortTrigger, }) async { + final response = await uploadDatabaseBackupWithHttpInfo(file: file, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/deprecated_api.dart b/mobile/openapi/lib/api/deprecated_api.dart index a437cd5837..a8e2deab44 100644 --- a/mobile/openapi/lib/api/deprecated_api.dart +++ b/mobile/openapi/lib/api/deprecated_api.dart @@ -25,7 +25,7 @@ class DeprecatedApi { /// Parameters: /// /// * [String] id (required): - Future createPartnerDeprecatedWithHttpInfo(String id,) async { + Future createPartnerDeprecatedWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/partners/{id}' .replaceAll('{id}', id); @@ -48,6 +48,7 @@ class DeprecatedApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -58,8 +59,8 @@ class DeprecatedApi { /// Parameters: /// /// * [String] id (required): - Future createPartnerDeprecated(String id,) async { - final response = await createPartnerDeprecatedWithHttpInfo(id,); + Future createPartnerDeprecated(String id, { Future? abortTrigger, }) async { + final response = await createPartnerDeprecatedWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -78,7 +79,7 @@ class DeprecatedApi { /// Retrieve the counts of the current queue, as well as the current status. /// /// Note: This method returns the HTTP [Response]. - Future getQueuesLegacyWithHttpInfo() async { + Future getQueuesLegacyWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/jobs'; @@ -100,14 +101,15 @@ class DeprecatedApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve queue counts and status /// /// Retrieve the counts of the current queue, as well as the current status. - Future getQueuesLegacy() async { - final response = await getQueuesLegacyWithHttpInfo(); + Future getQueuesLegacy({ Future? abortTrigger, }) async { + final response = await getQueuesLegacyWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -132,7 +134,7 @@ class DeprecatedApi { /// * [QueueName] name (required): /// /// * [QueueCommandDto] queueCommandDto (required): - Future runQueueCommandLegacyWithHttpInfo(QueueName name, QueueCommandDto queueCommandDto,) async { + Future runQueueCommandLegacyWithHttpInfo(QueueName name, QueueCommandDto queueCommandDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/jobs/{name}' .replaceAll('{name}', name.toString()); @@ -155,6 +157,7 @@ class DeprecatedApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -167,8 +170,8 @@ class DeprecatedApi { /// * [QueueName] name (required): /// /// * [QueueCommandDto] queueCommandDto (required): - Future runQueueCommandLegacy(QueueName name, QueueCommandDto queueCommandDto,) async { - final response = await runQueueCommandLegacyWithHttpInfo(name, queueCommandDto,); + Future runQueueCommandLegacy(QueueName name, QueueCommandDto queueCommandDto, { Future? abortTrigger, }) async { + final response = await runQueueCommandLegacyWithHttpInfo(name, queueCommandDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/download_api.dart b/mobile/openapi/lib/api/download_api.dart index 4d0c5c8165..ac26259277 100644 --- a/mobile/openapi/lib/api/download_api.dart +++ b/mobile/openapi/lib/api/download_api.dart @@ -29,7 +29,7 @@ class DownloadApi { /// * [String] key: /// /// * [String] slug: - Future downloadArchiveWithHttpInfo(DownloadArchiveDto downloadArchiveDto, { String? key, String? slug, }) async { + Future downloadArchiveWithHttpInfo(DownloadArchiveDto downloadArchiveDto, { String? key, String? slug, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/download/archive'; @@ -58,6 +58,7 @@ class DownloadApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -72,8 +73,8 @@ class DownloadApi { /// * [String] key: /// /// * [String] slug: - Future downloadArchive(DownloadArchiveDto downloadArchiveDto, { String? key, String? slug, }) async { - final response = await downloadArchiveWithHttpInfo(downloadArchiveDto, key: key, slug: slug, ); + Future downloadArchive(DownloadArchiveDto downloadArchiveDto, { String? key, String? slug, Future? abortTrigger, }) async { + final response = await downloadArchiveWithHttpInfo(downloadArchiveDto, key: key, slug: slug, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -100,7 +101,7 @@ class DownloadApi { /// * [String] key: /// /// * [String] slug: - Future getDownloadInfoWithHttpInfo(DownloadInfoDto downloadInfoDto, { String? key, String? slug, }) async { + Future getDownloadInfoWithHttpInfo(DownloadInfoDto downloadInfoDto, { String? key, String? slug, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/download/info'; @@ -129,6 +130,7 @@ class DownloadApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -143,8 +145,8 @@ class DownloadApi { /// * [String] key: /// /// * [String] slug: - Future getDownloadInfo(DownloadInfoDto downloadInfoDto, { String? key, String? slug, }) async { - final response = await getDownloadInfoWithHttpInfo(downloadInfoDto, key: key, slug: slug, ); + Future getDownloadInfo(DownloadInfoDto downloadInfoDto, { String? key, String? slug, Future? abortTrigger, }) async { + final response = await getDownloadInfoWithHttpInfo(downloadInfoDto, key: key, slug: slug, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/duplicates_api.dart b/mobile/openapi/lib/api/duplicates_api.dart index 9bd01281b3..357947b889 100644 --- a/mobile/openapi/lib/api/duplicates_api.dart +++ b/mobile/openapi/lib/api/duplicates_api.dart @@ -25,7 +25,7 @@ class DuplicatesApi { /// Parameters: /// /// * [String] id (required): - Future deleteDuplicateWithHttpInfo(String id,) async { + Future deleteDuplicateWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/duplicates/{id}' .replaceAll('{id}', id); @@ -48,6 +48,7 @@ class DuplicatesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -58,8 +59,8 @@ class DuplicatesApi { /// Parameters: /// /// * [String] id (required): - Future deleteDuplicate(String id,) async { - final response = await deleteDuplicateWithHttpInfo(id,); + Future deleteDuplicate(String id, { Future? abortTrigger, }) async { + final response = await deleteDuplicateWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -74,7 +75,7 @@ class DuplicatesApi { /// Parameters: /// /// * [BulkIdsDto] bulkIdsDto (required): - Future deleteDuplicatesWithHttpInfo(BulkIdsDto bulkIdsDto,) async { + Future deleteDuplicatesWithHttpInfo(BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/duplicates'; @@ -96,6 +97,7 @@ class DuplicatesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -106,8 +108,8 @@ class DuplicatesApi { /// Parameters: /// /// * [BulkIdsDto] bulkIdsDto (required): - Future deleteDuplicates(BulkIdsDto bulkIdsDto,) async { - final response = await deleteDuplicatesWithHttpInfo(bulkIdsDto,); + Future deleteDuplicates(BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { + final response = await deleteDuplicatesWithHttpInfo(bulkIdsDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -118,7 +120,7 @@ class DuplicatesApi { /// Retrieve a list of duplicate assets available to the authenticated user. /// /// Note: This method returns the HTTP [Response]. - Future getAssetDuplicatesWithHttpInfo() async { + Future getAssetDuplicatesWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/duplicates'; @@ -140,14 +142,15 @@ class DuplicatesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve duplicates /// /// Retrieve a list of duplicate assets available to the authenticated user. - Future?> getAssetDuplicates() async { - final response = await getAssetDuplicatesWithHttpInfo(); + Future?> getAssetDuplicates({ Future? abortTrigger, }) async { + final response = await getAssetDuplicatesWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -173,7 +176,7 @@ class DuplicatesApi { /// Parameters: /// /// * [DuplicateResolveDto] duplicateResolveDto (required): - Future resolveDuplicatesWithHttpInfo(DuplicateResolveDto duplicateResolveDto,) async { + Future resolveDuplicatesWithHttpInfo(DuplicateResolveDto duplicateResolveDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/duplicates/resolve'; @@ -195,6 +198,7 @@ class DuplicatesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -205,8 +209,8 @@ class DuplicatesApi { /// Parameters: /// /// * [DuplicateResolveDto] duplicateResolveDto (required): - Future?> resolveDuplicates(DuplicateResolveDto duplicateResolveDto,) async { - final response = await resolveDuplicatesWithHttpInfo(duplicateResolveDto,); + Future?> resolveDuplicates(DuplicateResolveDto duplicateResolveDto, { Future? abortTrigger, }) async { + final response = await resolveDuplicatesWithHttpInfo(duplicateResolveDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/faces_api.dart b/mobile/openapi/lib/api/faces_api.dart index 43d63b47b9..2a71bbbaca 100644 --- a/mobile/openapi/lib/api/faces_api.dart +++ b/mobile/openapi/lib/api/faces_api.dart @@ -25,7 +25,7 @@ class FacesApi { /// Parameters: /// /// * [AssetFaceCreateDto] assetFaceCreateDto (required): - Future createFaceWithHttpInfo(AssetFaceCreateDto assetFaceCreateDto,) async { + Future createFaceWithHttpInfo(AssetFaceCreateDto assetFaceCreateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/faces'; @@ -47,6 +47,7 @@ class FacesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class FacesApi { /// Parameters: /// /// * [AssetFaceCreateDto] assetFaceCreateDto (required): - Future createFace(AssetFaceCreateDto assetFaceCreateDto,) async { - final response = await createFaceWithHttpInfo(assetFaceCreateDto,); + Future createFace(AssetFaceCreateDto assetFaceCreateDto, { Future? abortTrigger, }) async { + final response = await createFaceWithHttpInfo(assetFaceCreateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -75,7 +76,7 @@ class FacesApi { /// * [String] id (required): /// /// * [AssetFaceDeleteDto] assetFaceDeleteDto (required): - Future deleteFaceWithHttpInfo(String id, AssetFaceDeleteDto assetFaceDeleteDto,) async { + Future deleteFaceWithHttpInfo(String id, AssetFaceDeleteDto assetFaceDeleteDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/faces/{id}' .replaceAll('{id}', id); @@ -98,6 +99,7 @@ class FacesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -110,8 +112,8 @@ class FacesApi { /// * [String] id (required): /// /// * [AssetFaceDeleteDto] assetFaceDeleteDto (required): - Future deleteFace(String id, AssetFaceDeleteDto assetFaceDeleteDto,) async { - final response = await deleteFaceWithHttpInfo(id, assetFaceDeleteDto,); + Future deleteFace(String id, AssetFaceDeleteDto assetFaceDeleteDto, { Future? abortTrigger, }) async { + final response = await deleteFaceWithHttpInfo(id, assetFaceDeleteDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -127,7 +129,7 @@ class FacesApi { /// /// * [String] id (required): /// Face ID - Future getFacesWithHttpInfo(String id,) async { + Future getFacesWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/faces'; @@ -151,6 +153,7 @@ class FacesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -162,8 +165,8 @@ class FacesApi { /// /// * [String] id (required): /// Face ID - Future?> getFaces(String id,) async { - final response = await getFacesWithHttpInfo(id,); + Future?> getFaces(String id, { Future? abortTrigger, }) async { + final response = await getFacesWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -191,7 +194,7 @@ class FacesApi { /// * [String] id (required): /// /// * [FaceDto] faceDto (required): - Future reassignFacesByIdWithHttpInfo(String id, FaceDto faceDto,) async { + Future reassignFacesByIdWithHttpInfo(String id, FaceDto faceDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/faces/{id}' .replaceAll('{id}', id); @@ -214,6 +217,7 @@ class FacesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -226,8 +230,8 @@ class FacesApi { /// * [String] id (required): /// /// * [FaceDto] faceDto (required): - Future reassignFacesById(String id, FaceDto faceDto,) async { - final response = await reassignFacesByIdWithHttpInfo(id, faceDto,); + Future reassignFacesById(String id, FaceDto faceDto, { Future? abortTrigger, }) async { + final response = await reassignFacesByIdWithHttpInfo(id, faceDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/jobs_api.dart b/mobile/openapi/lib/api/jobs_api.dart index 9dda59a883..287432ad9a 100644 --- a/mobile/openapi/lib/api/jobs_api.dart +++ b/mobile/openapi/lib/api/jobs_api.dart @@ -25,7 +25,7 @@ class JobsApi { /// Parameters: /// /// * [JobCreateDto] jobCreateDto (required): - Future createJobWithHttpInfo(JobCreateDto jobCreateDto,) async { + Future createJobWithHttpInfo(JobCreateDto jobCreateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/jobs'; @@ -47,6 +47,7 @@ class JobsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class JobsApi { /// Parameters: /// /// * [JobCreateDto] jobCreateDto (required): - Future createJob(JobCreateDto jobCreateDto,) async { - final response = await createJobWithHttpInfo(jobCreateDto,); + Future createJob(JobCreateDto jobCreateDto, { Future? abortTrigger, }) async { + final response = await createJobWithHttpInfo(jobCreateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -69,7 +70,7 @@ class JobsApi { /// Retrieve the counts of the current queue, as well as the current status. /// /// Note: This method returns the HTTP [Response]. - Future getQueuesLegacyWithHttpInfo() async { + Future getQueuesLegacyWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/jobs'; @@ -91,14 +92,15 @@ class JobsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve queue counts and status /// /// Retrieve the counts of the current queue, as well as the current status. - Future getQueuesLegacy() async { - final response = await getQueuesLegacyWithHttpInfo(); + Future getQueuesLegacy({ Future? abortTrigger, }) async { + final response = await getQueuesLegacyWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -123,7 +125,7 @@ class JobsApi { /// * [QueueName] name (required): /// /// * [QueueCommandDto] queueCommandDto (required): - Future runQueueCommandLegacyWithHttpInfo(QueueName name, QueueCommandDto queueCommandDto,) async { + Future runQueueCommandLegacyWithHttpInfo(QueueName name, QueueCommandDto queueCommandDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/jobs/{name}' .replaceAll('{name}', name.toString()); @@ -146,6 +148,7 @@ class JobsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -158,8 +161,8 @@ class JobsApi { /// * [QueueName] name (required): /// /// * [QueueCommandDto] queueCommandDto (required): - Future runQueueCommandLegacy(QueueName name, QueueCommandDto queueCommandDto,) async { - final response = await runQueueCommandLegacyWithHttpInfo(name, queueCommandDto,); + Future runQueueCommandLegacy(QueueName name, QueueCommandDto queueCommandDto, { Future? abortTrigger, }) async { + final response = await runQueueCommandLegacyWithHttpInfo(name, queueCommandDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/libraries_api.dart b/mobile/openapi/lib/api/libraries_api.dart index ca59f823fe..a3b3086994 100644 --- a/mobile/openapi/lib/api/libraries_api.dart +++ b/mobile/openapi/lib/api/libraries_api.dart @@ -25,7 +25,7 @@ class LibrariesApi { /// Parameters: /// /// * [CreateLibraryDto] createLibraryDto (required): - Future createLibraryWithHttpInfo(CreateLibraryDto createLibraryDto,) async { + Future createLibraryWithHttpInfo(CreateLibraryDto createLibraryDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/libraries'; @@ -47,6 +47,7 @@ class LibrariesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class LibrariesApi { /// Parameters: /// /// * [CreateLibraryDto] createLibraryDto (required): - Future createLibrary(CreateLibraryDto createLibraryDto,) async { - final response = await createLibraryWithHttpInfo(createLibraryDto,); + Future createLibrary(CreateLibraryDto createLibraryDto, { Future? abortTrigger, }) async { + final response = await createLibraryWithHttpInfo(createLibraryDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -81,7 +82,7 @@ class LibrariesApi { /// Parameters: /// /// * [String] id (required): - Future deleteLibraryWithHttpInfo(String id,) async { + Future deleteLibraryWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/libraries/{id}' .replaceAll('{id}', id); @@ -104,6 +105,7 @@ class LibrariesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -114,8 +116,8 @@ class LibrariesApi { /// Parameters: /// /// * [String] id (required): - Future deleteLibrary(String id,) async { - final response = await deleteLibraryWithHttpInfo(id,); + Future deleteLibrary(String id, { Future? abortTrigger, }) async { + final response = await deleteLibraryWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -126,7 +128,7 @@ class LibrariesApi { /// Retrieve a list of external libraries. /// /// Note: This method returns the HTTP [Response]. - Future getAllLibrariesWithHttpInfo() async { + Future getAllLibrariesWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/libraries'; @@ -148,14 +150,15 @@ class LibrariesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve libraries /// /// Retrieve a list of external libraries. - Future?> getAllLibraries() async { - final response = await getAllLibrariesWithHttpInfo(); + Future?> getAllLibraries({ Future? abortTrigger, }) async { + final response = await getAllLibrariesWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -181,7 +184,7 @@ class LibrariesApi { /// Parameters: /// /// * [String] id (required): - Future getLibraryWithHttpInfo(String id,) async { + Future getLibraryWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/libraries/{id}' .replaceAll('{id}', id); @@ -204,6 +207,7 @@ class LibrariesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -214,8 +218,8 @@ class LibrariesApi { /// Parameters: /// /// * [String] id (required): - Future getLibrary(String id,) async { - final response = await getLibraryWithHttpInfo(id,); + Future getLibrary(String id, { Future? abortTrigger, }) async { + final response = await getLibraryWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -238,7 +242,7 @@ class LibrariesApi { /// Parameters: /// /// * [String] id (required): - Future getLibraryStatisticsWithHttpInfo(String id,) async { + Future getLibraryStatisticsWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/libraries/{id}/statistics' .replaceAll('{id}', id); @@ -261,6 +265,7 @@ class LibrariesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -271,8 +276,8 @@ class LibrariesApi { /// Parameters: /// /// * [String] id (required): - Future getLibraryStatistics(String id,) async { - final response = await getLibraryStatisticsWithHttpInfo(id,); + Future getLibraryStatistics(String id, { Future? abortTrigger, }) async { + final response = await getLibraryStatisticsWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -295,7 +300,7 @@ class LibrariesApi { /// Parameters: /// /// * [String] id (required): - Future scanLibraryWithHttpInfo(String id,) async { + Future scanLibraryWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/libraries/{id}/scan' .replaceAll('{id}', id); @@ -318,6 +323,7 @@ class LibrariesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -328,8 +334,8 @@ class LibrariesApi { /// Parameters: /// /// * [String] id (required): - Future scanLibrary(String id,) async { - final response = await scanLibraryWithHttpInfo(id,); + Future scanLibrary(String id, { Future? abortTrigger, }) async { + final response = await scanLibraryWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -346,7 +352,7 @@ class LibrariesApi { /// * [String] id (required): /// /// * [UpdateLibraryDto] updateLibraryDto (required): - Future updateLibraryWithHttpInfo(String id, UpdateLibraryDto updateLibraryDto,) async { + Future updateLibraryWithHttpInfo(String id, UpdateLibraryDto updateLibraryDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/libraries/{id}' .replaceAll('{id}', id); @@ -369,6 +375,7 @@ class LibrariesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -381,8 +388,8 @@ class LibrariesApi { /// * [String] id (required): /// /// * [UpdateLibraryDto] updateLibraryDto (required): - Future updateLibrary(String id, UpdateLibraryDto updateLibraryDto,) async { - final response = await updateLibraryWithHttpInfo(id, updateLibraryDto,); + Future updateLibrary(String id, UpdateLibraryDto updateLibraryDto, { Future? abortTrigger, }) async { + final response = await updateLibraryWithHttpInfo(id, updateLibraryDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -407,7 +414,7 @@ class LibrariesApi { /// * [String] id (required): /// /// * [ValidateLibraryDto] validateLibraryDto (required): - Future validateWithHttpInfo(String id, ValidateLibraryDto validateLibraryDto,) async { + Future validateWithHttpInfo(String id, ValidateLibraryDto validateLibraryDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/libraries/{id}/validate' .replaceAll('{id}', id); @@ -430,6 +437,7 @@ class LibrariesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -442,8 +450,8 @@ class LibrariesApi { /// * [String] id (required): /// /// * [ValidateLibraryDto] validateLibraryDto (required): - Future validate(String id, ValidateLibraryDto validateLibraryDto,) async { - final response = await validateWithHttpInfo(id, validateLibraryDto,); + Future validate(String id, ValidateLibraryDto validateLibraryDto, { Future? abortTrigger, }) async { + final response = await validateWithHttpInfo(id, validateLibraryDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/maintenance_admin_api.dart b/mobile/openapi/lib/api/maintenance_admin_api.dart index 0f953f1634..8bab193ddf 100644 --- a/mobile/openapi/lib/api/maintenance_admin_api.dart +++ b/mobile/openapi/lib/api/maintenance_admin_api.dart @@ -21,7 +21,7 @@ class MaintenanceAdminApi { /// Collect integrity checks and other heuristics about local data. /// /// Note: This method returns the HTTP [Response]. - Future detectPriorInstallWithHttpInfo() async { + Future detectPriorInstallWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/maintenance/detect-install'; @@ -43,14 +43,15 @@ class MaintenanceAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Detect existing install /// /// Collect integrity checks and other heuristics about local data. - Future detectPriorInstall() async { - final response = await detectPriorInstallWithHttpInfo(); + Future detectPriorInstall({ Future? abortTrigger, }) async { + final response = await detectPriorInstallWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -69,7 +70,7 @@ class MaintenanceAdminApi { /// Fetch information about the currently running maintenance action. /// /// Note: This method returns the HTTP [Response]. - Future getMaintenanceStatusWithHttpInfo() async { + Future getMaintenanceStatusWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/maintenance/status'; @@ -91,14 +92,15 @@ class MaintenanceAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get maintenance mode status /// /// Fetch information about the currently running maintenance action. - Future getMaintenanceStatus() async { - final response = await getMaintenanceStatusWithHttpInfo(); + Future getMaintenanceStatus({ Future? abortTrigger, }) async { + final response = await getMaintenanceStatusWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -121,7 +123,7 @@ class MaintenanceAdminApi { /// Parameters: /// /// * [MaintenanceLoginDto] maintenanceLoginDto (required): - Future maintenanceLoginWithHttpInfo(MaintenanceLoginDto maintenanceLoginDto,) async { + Future maintenanceLoginWithHttpInfo(MaintenanceLoginDto maintenanceLoginDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/maintenance/login'; @@ -143,6 +145,7 @@ class MaintenanceAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -153,8 +156,8 @@ class MaintenanceAdminApi { /// Parameters: /// /// * [MaintenanceLoginDto] maintenanceLoginDto (required): - Future maintenanceLogin(MaintenanceLoginDto maintenanceLoginDto,) async { - final response = await maintenanceLoginWithHttpInfo(maintenanceLoginDto,); + Future maintenanceLogin(MaintenanceLoginDto maintenanceLoginDto, { Future? abortTrigger, }) async { + final response = await maintenanceLoginWithHttpInfo(maintenanceLoginDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -177,7 +180,7 @@ class MaintenanceAdminApi { /// Parameters: /// /// * [SetMaintenanceModeDto] setMaintenanceModeDto (required): - Future setMaintenanceModeWithHttpInfo(SetMaintenanceModeDto setMaintenanceModeDto,) async { + Future setMaintenanceModeWithHttpInfo(SetMaintenanceModeDto setMaintenanceModeDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/maintenance'; @@ -199,6 +202,7 @@ class MaintenanceAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -209,8 +213,8 @@ class MaintenanceAdminApi { /// Parameters: /// /// * [SetMaintenanceModeDto] setMaintenanceModeDto (required): - Future setMaintenanceMode(SetMaintenanceModeDto setMaintenanceModeDto,) async { - final response = await setMaintenanceModeWithHttpInfo(setMaintenanceModeDto,); + Future setMaintenanceMode(SetMaintenanceModeDto setMaintenanceModeDto, { Future? abortTrigger, }) async { + final response = await setMaintenanceModeWithHttpInfo(setMaintenanceModeDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/map_api.dart b/mobile/openapi/lib/api/map_api.dart index 4ce62bd96c..7e1618a875 100644 --- a/mobile/openapi/lib/api/map_api.dart +++ b/mobile/openapi/lib/api/map_api.dart @@ -41,7 +41,7 @@ class MapApi { /// /// * [bool] withSharedAlbums: /// Include shared album assets - Future getMapMarkersWithHttpInfo({ DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, bool? isArchived, bool? isFavorite, bool? withPartners, bool? withSharedAlbums, }) async { + Future getMapMarkersWithHttpInfo({ DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, bool? isArchived, bool? isFavorite, bool? withPartners, bool? withSharedAlbums, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/map/markers'; @@ -82,6 +82,7 @@ class MapApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -108,8 +109,8 @@ class MapApi { /// /// * [bool] withSharedAlbums: /// Include shared album assets - Future?> getMapMarkers({ DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, bool? isArchived, bool? isFavorite, bool? withPartners, bool? withSharedAlbums, }) async { - final response = await getMapMarkersWithHttpInfo( fileCreatedAfter: fileCreatedAfter, fileCreatedBefore: fileCreatedBefore, isArchived: isArchived, isFavorite: isFavorite, withPartners: withPartners, withSharedAlbums: withSharedAlbums, ); + Future?> getMapMarkers({ DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, bool? isArchived, bool? isFavorite, bool? withPartners, bool? withSharedAlbums, Future? abortTrigger, }) async { + final response = await getMapMarkersWithHttpInfo(fileCreatedAfter: fileCreatedAfter, fileCreatedBefore: fileCreatedBefore, isArchived: isArchived, isFavorite: isFavorite, withPartners: withPartners, withSharedAlbums: withSharedAlbums, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -139,7 +140,7 @@ class MapApi { /// /// * [double] lon (required): /// Longitude (-180 to 180) - Future reverseGeocodeWithHttpInfo(double lat, double lon,) async { + Future reverseGeocodeWithHttpInfo(double lat, double lon, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/map/reverse-geocode'; @@ -164,6 +165,7 @@ class MapApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -178,8 +180,8 @@ class MapApi { /// /// * [double] lon (required): /// Longitude (-180 to 180) - Future?> reverseGeocode(double lat, double lon,) async { - final response = await reverseGeocodeWithHttpInfo(lat, lon,); + Future?> reverseGeocode(double lat, double lon, { Future? abortTrigger, }) async { + final response = await reverseGeocodeWithHttpInfo(lat, lon, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/memories_api.dart b/mobile/openapi/lib/api/memories_api.dart index 0cd96ac442..f5c653765a 100644 --- a/mobile/openapi/lib/api/memories_api.dart +++ b/mobile/openapi/lib/api/memories_api.dart @@ -27,7 +27,7 @@ class MemoriesApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - Future addMemoryAssetsWithHttpInfo(String id, BulkIdsDto bulkIdsDto,) async { + Future addMemoryAssetsWithHttpInfo(String id, BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/memories/{id}/assets' .replaceAll('{id}', id); @@ -50,6 +50,7 @@ class MemoriesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -62,8 +63,8 @@ class MemoriesApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - Future?> addMemoryAssets(String id, BulkIdsDto bulkIdsDto,) async { - final response = await addMemoryAssetsWithHttpInfo(id, bulkIdsDto,); + Future?> addMemoryAssets(String id, BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { + final response = await addMemoryAssetsWithHttpInfo(id, bulkIdsDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -89,7 +90,7 @@ class MemoriesApi { /// Parameters: /// /// * [MemoryCreateDto] memoryCreateDto (required): - Future createMemoryWithHttpInfo(MemoryCreateDto memoryCreateDto,) async { + Future createMemoryWithHttpInfo(MemoryCreateDto memoryCreateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/memories'; @@ -111,6 +112,7 @@ class MemoriesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -121,8 +123,8 @@ class MemoriesApi { /// Parameters: /// /// * [MemoryCreateDto] memoryCreateDto (required): - Future createMemory(MemoryCreateDto memoryCreateDto,) async { - final response = await createMemoryWithHttpInfo(memoryCreateDto,); + Future createMemory(MemoryCreateDto memoryCreateDto, { Future? abortTrigger, }) async { + final response = await createMemoryWithHttpInfo(memoryCreateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -145,7 +147,7 @@ class MemoriesApi { /// Parameters: /// /// * [String] id (required): - Future deleteMemoryWithHttpInfo(String id,) async { + Future deleteMemoryWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/memories/{id}' .replaceAll('{id}', id); @@ -168,6 +170,7 @@ class MemoriesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -178,8 +181,8 @@ class MemoriesApi { /// Parameters: /// /// * [String] id (required): - Future deleteMemory(String id,) async { - final response = await deleteMemoryWithHttpInfo(id,); + Future deleteMemory(String id, { Future? abortTrigger, }) async { + final response = await deleteMemoryWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -194,7 +197,7 @@ class MemoriesApi { /// Parameters: /// /// * [String] id (required): - Future getMemoryWithHttpInfo(String id,) async { + Future getMemoryWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/memories/{id}' .replaceAll('{id}', id); @@ -217,6 +220,7 @@ class MemoriesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -227,8 +231,8 @@ class MemoriesApi { /// Parameters: /// /// * [String] id (required): - Future getMemory(String id,) async { - final response = await getMemoryWithHttpInfo(id,); + Future getMemory(String id, { Future? abortTrigger, }) async { + final response = await getMemoryWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -265,7 +269,7 @@ class MemoriesApi { /// Number of memories to return /// /// * [MemoryType] type: - Future memoriesStatisticsWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { + Future memoriesStatisticsWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/memories/statistics'; @@ -306,6 +310,7 @@ class MemoriesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -330,8 +335,8 @@ class MemoriesApi { /// Number of memories to return /// /// * [MemoryType] type: - Future memoriesStatistics({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { - final response = await memoriesStatisticsWithHttpInfo( for_: for_, isSaved: isSaved, isTrashed: isTrashed, order: order, size: size, type: type, ); + Future memoriesStatistics({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, Future? abortTrigger, }) async { + final response = await memoriesStatisticsWithHttpInfo(for_: for_, isSaved: isSaved, isTrashed: isTrashed, order: order, size: size, type: type, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -356,7 +361,7 @@ class MemoriesApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - Future removeMemoryAssetsWithHttpInfo(String id, BulkIdsDto bulkIdsDto,) async { + Future removeMemoryAssetsWithHttpInfo(String id, BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/memories/{id}/assets' .replaceAll('{id}', id); @@ -379,6 +384,7 @@ class MemoriesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -391,8 +397,8 @@ class MemoriesApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - Future?> removeMemoryAssets(String id, BulkIdsDto bulkIdsDto,) async { - final response = await removeMemoryAssetsWithHttpInfo(id, bulkIdsDto,); + Future?> removeMemoryAssets(String id, BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { + final response = await removeMemoryAssetsWithHttpInfo(id, bulkIdsDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -432,7 +438,7 @@ class MemoriesApi { /// Number of memories to return /// /// * [MemoryType] type: - Future searchMemoriesWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { + Future searchMemoriesWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/memories'; @@ -473,6 +479,7 @@ class MemoriesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -497,8 +504,8 @@ class MemoriesApi { /// Number of memories to return /// /// * [MemoryType] type: - Future?> searchMemories({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { - final response = await searchMemoriesWithHttpInfo( for_: for_, isSaved: isSaved, isTrashed: isTrashed, order: order, size: size, type: type, ); + Future?> searchMemories({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, Future? abortTrigger, }) async { + final response = await searchMemoriesWithHttpInfo(for_: for_, isSaved: isSaved, isTrashed: isTrashed, order: order, size: size, type: type, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -526,7 +533,7 @@ class MemoriesApi { /// * [String] id (required): /// /// * [MemoryUpdateDto] memoryUpdateDto (required): - Future updateMemoryWithHttpInfo(String id, MemoryUpdateDto memoryUpdateDto,) async { + Future updateMemoryWithHttpInfo(String id, MemoryUpdateDto memoryUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/memories/{id}' .replaceAll('{id}', id); @@ -549,6 +556,7 @@ class MemoriesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -561,8 +569,8 @@ class MemoriesApi { /// * [String] id (required): /// /// * [MemoryUpdateDto] memoryUpdateDto (required): - Future updateMemory(String id, MemoryUpdateDto memoryUpdateDto,) async { - final response = await updateMemoryWithHttpInfo(id, memoryUpdateDto,); + Future updateMemory(String id, MemoryUpdateDto memoryUpdateDto, { Future? abortTrigger, }) async { + final response = await updateMemoryWithHttpInfo(id, memoryUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/notifications_admin_api.dart b/mobile/openapi/lib/api/notifications_admin_api.dart index 7821553d30..e9e18e791e 100644 --- a/mobile/openapi/lib/api/notifications_admin_api.dart +++ b/mobile/openapi/lib/api/notifications_admin_api.dart @@ -25,7 +25,7 @@ class NotificationsAdminApi { /// Parameters: /// /// * [NotificationCreateDto] notificationCreateDto (required): - Future createNotificationWithHttpInfo(NotificationCreateDto notificationCreateDto,) async { + Future createNotificationWithHttpInfo(NotificationCreateDto notificationCreateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/notifications'; @@ -47,6 +47,7 @@ class NotificationsAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class NotificationsAdminApi { /// Parameters: /// /// * [NotificationCreateDto] notificationCreateDto (required): - Future createNotification(NotificationCreateDto notificationCreateDto,) async { - final response = await createNotificationWithHttpInfo(notificationCreateDto,); + Future createNotification(NotificationCreateDto notificationCreateDto, { Future? abortTrigger, }) async { + final response = await createNotificationWithHttpInfo(notificationCreateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -83,7 +84,7 @@ class NotificationsAdminApi { /// * [String] name (required): /// /// * [TemplateDto] templateDto (required): - Future getNotificationTemplateAdminWithHttpInfo(String name, TemplateDto templateDto,) async { + Future getNotificationTemplateAdminWithHttpInfo(String name, TemplateDto templateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/notifications/templates/{name}' .replaceAll('{name}', name); @@ -106,6 +107,7 @@ class NotificationsAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -118,8 +120,8 @@ class NotificationsAdminApi { /// * [String] name (required): /// /// * [TemplateDto] templateDto (required): - Future getNotificationTemplateAdmin(String name, TemplateDto templateDto,) async { - final response = await getNotificationTemplateAdminWithHttpInfo(name, templateDto,); + Future getNotificationTemplateAdmin(String name, TemplateDto templateDto, { Future? abortTrigger, }) async { + final response = await getNotificationTemplateAdminWithHttpInfo(name, templateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -142,7 +144,7 @@ class NotificationsAdminApi { /// Parameters: /// /// * [SystemConfigSmtpDto] systemConfigSmtpDto (required): - Future sendTestEmailAdminWithHttpInfo(SystemConfigSmtpDto systemConfigSmtpDto,) async { + Future sendTestEmailAdminWithHttpInfo(SystemConfigSmtpDto systemConfigSmtpDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/notifications/test-email'; @@ -164,6 +166,7 @@ class NotificationsAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -174,8 +177,8 @@ class NotificationsAdminApi { /// Parameters: /// /// * [SystemConfigSmtpDto] systemConfigSmtpDto (required): - Future sendTestEmailAdmin(SystemConfigSmtpDto systemConfigSmtpDto,) async { - final response = await sendTestEmailAdminWithHttpInfo(systemConfigSmtpDto,); + Future sendTestEmailAdmin(SystemConfigSmtpDto systemConfigSmtpDto, { Future? abortTrigger, }) async { + final response = await sendTestEmailAdminWithHttpInfo(systemConfigSmtpDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/notifications_api.dart b/mobile/openapi/lib/api/notifications_api.dart index ab0be3e8f3..6b4f213bcd 100644 --- a/mobile/openapi/lib/api/notifications_api.dart +++ b/mobile/openapi/lib/api/notifications_api.dart @@ -25,7 +25,7 @@ class NotificationsApi { /// Parameters: /// /// * [String] id (required): - Future deleteNotificationWithHttpInfo(String id,) async { + Future deleteNotificationWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/notifications/{id}' .replaceAll('{id}', id); @@ -48,6 +48,7 @@ class NotificationsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -58,8 +59,8 @@ class NotificationsApi { /// Parameters: /// /// * [String] id (required): - Future deleteNotification(String id,) async { - final response = await deleteNotificationWithHttpInfo(id,); + Future deleteNotification(String id, { Future? abortTrigger, }) async { + final response = await deleteNotificationWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -74,7 +75,7 @@ class NotificationsApi { /// Parameters: /// /// * [NotificationDeleteAllDto] notificationDeleteAllDto (required): - Future deleteNotificationsWithHttpInfo(NotificationDeleteAllDto notificationDeleteAllDto,) async { + Future deleteNotificationsWithHttpInfo(NotificationDeleteAllDto notificationDeleteAllDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/notifications'; @@ -96,6 +97,7 @@ class NotificationsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -106,8 +108,8 @@ class NotificationsApi { /// Parameters: /// /// * [NotificationDeleteAllDto] notificationDeleteAllDto (required): - Future deleteNotifications(NotificationDeleteAllDto notificationDeleteAllDto,) async { - final response = await deleteNotificationsWithHttpInfo(notificationDeleteAllDto,); + Future deleteNotifications(NotificationDeleteAllDto notificationDeleteAllDto, { Future? abortTrigger, }) async { + final response = await deleteNotificationsWithHttpInfo(notificationDeleteAllDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -122,7 +124,7 @@ class NotificationsApi { /// Parameters: /// /// * [String] id (required): - Future getNotificationWithHttpInfo(String id,) async { + Future getNotificationWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/notifications/{id}' .replaceAll('{id}', id); @@ -145,6 +147,7 @@ class NotificationsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -155,8 +158,8 @@ class NotificationsApi { /// Parameters: /// /// * [String] id (required): - Future getNotification(String id,) async { - final response = await getNotificationWithHttpInfo(id,); + Future getNotification(String id, { Future? abortTrigger, }) async { + final response = await getNotificationWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -187,7 +190,7 @@ class NotificationsApi { /// /// * [bool] unread: /// Filter by unread status - Future getNotificationsWithHttpInfo({ String? id, NotificationLevel? level, NotificationType? type, bool? unread, }) async { + Future getNotificationsWithHttpInfo({ String? id, NotificationLevel? level, NotificationType? type, bool? unread, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/notifications'; @@ -222,6 +225,7 @@ class NotificationsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -240,8 +244,8 @@ class NotificationsApi { /// /// * [bool] unread: /// Filter by unread status - Future?> getNotifications({ String? id, NotificationLevel? level, NotificationType? type, bool? unread, }) async { - final response = await getNotificationsWithHttpInfo( id: id, level: level, type: type, unread: unread, ); + Future?> getNotifications({ String? id, NotificationLevel? level, NotificationType? type, bool? unread, Future? abortTrigger, }) async { + final response = await getNotificationsWithHttpInfo(id: id, level: level, type: type, unread: unread, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -269,7 +273,7 @@ class NotificationsApi { /// * [String] id (required): /// /// * [NotificationUpdateDto] notificationUpdateDto (required): - Future updateNotificationWithHttpInfo(String id, NotificationUpdateDto notificationUpdateDto,) async { + Future updateNotificationWithHttpInfo(String id, NotificationUpdateDto notificationUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/notifications/{id}' .replaceAll('{id}', id); @@ -292,6 +296,7 @@ class NotificationsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -304,8 +309,8 @@ class NotificationsApi { /// * [String] id (required): /// /// * [NotificationUpdateDto] notificationUpdateDto (required): - Future updateNotification(String id, NotificationUpdateDto notificationUpdateDto,) async { - final response = await updateNotificationWithHttpInfo(id, notificationUpdateDto,); + Future updateNotification(String id, NotificationUpdateDto notificationUpdateDto, { Future? abortTrigger, }) async { + final response = await updateNotificationWithHttpInfo(id, notificationUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -328,7 +333,7 @@ class NotificationsApi { /// Parameters: /// /// * [NotificationUpdateAllDto] notificationUpdateAllDto (required): - Future updateNotificationsWithHttpInfo(NotificationUpdateAllDto notificationUpdateAllDto,) async { + Future updateNotificationsWithHttpInfo(NotificationUpdateAllDto notificationUpdateAllDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/notifications'; @@ -350,6 +355,7 @@ class NotificationsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -360,8 +366,8 @@ class NotificationsApi { /// Parameters: /// /// * [NotificationUpdateAllDto] notificationUpdateAllDto (required): - Future updateNotifications(NotificationUpdateAllDto notificationUpdateAllDto,) async { - final response = await updateNotificationsWithHttpInfo(notificationUpdateAllDto,); + Future updateNotifications(NotificationUpdateAllDto notificationUpdateAllDto, { Future? abortTrigger, }) async { + final response = await updateNotificationsWithHttpInfo(notificationUpdateAllDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/partners_api.dart b/mobile/openapi/lib/api/partners_api.dart index 7d18f6d867..45bcdcd085 100644 --- a/mobile/openapi/lib/api/partners_api.dart +++ b/mobile/openapi/lib/api/partners_api.dart @@ -25,7 +25,7 @@ class PartnersApi { /// Parameters: /// /// * [PartnerCreateDto] partnerCreateDto (required): - Future createPartnerWithHttpInfo(PartnerCreateDto partnerCreateDto,) async { + Future createPartnerWithHttpInfo(PartnerCreateDto partnerCreateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/partners'; @@ -47,6 +47,7 @@ class PartnersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class PartnersApi { /// Parameters: /// /// * [PartnerCreateDto] partnerCreateDto (required): - Future createPartner(PartnerCreateDto partnerCreateDto,) async { - final response = await createPartnerWithHttpInfo(partnerCreateDto,); + Future createPartner(PartnerCreateDto partnerCreateDto, { Future? abortTrigger, }) async { + final response = await createPartnerWithHttpInfo(partnerCreateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -81,7 +82,7 @@ class PartnersApi { /// Parameters: /// /// * [String] id (required): - Future createPartnerDeprecatedWithHttpInfo(String id,) async { + Future createPartnerDeprecatedWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/partners/{id}' .replaceAll('{id}', id); @@ -104,6 +105,7 @@ class PartnersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -114,8 +116,8 @@ class PartnersApi { /// Parameters: /// /// * [String] id (required): - Future createPartnerDeprecated(String id,) async { - final response = await createPartnerDeprecatedWithHttpInfo(id,); + Future createPartnerDeprecated(String id, { Future? abortTrigger, }) async { + final response = await createPartnerDeprecatedWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -138,7 +140,7 @@ class PartnersApi { /// Parameters: /// /// * [PartnerDirection] direction (required): - Future getPartnersWithHttpInfo(PartnerDirection direction,) async { + Future getPartnersWithHttpInfo(PartnerDirection direction, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/partners'; @@ -162,6 +164,7 @@ class PartnersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -172,8 +175,8 @@ class PartnersApi { /// Parameters: /// /// * [PartnerDirection] direction (required): - Future?> getPartners(PartnerDirection direction,) async { - final response = await getPartnersWithHttpInfo(direction,); + Future?> getPartners(PartnerDirection direction, { Future? abortTrigger, }) async { + final response = await getPartnersWithHttpInfo(direction, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -199,7 +202,7 @@ class PartnersApi { /// Parameters: /// /// * [String] id (required): - Future removePartnerWithHttpInfo(String id,) async { + Future removePartnerWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/partners/{id}' .replaceAll('{id}', id); @@ -222,6 +225,7 @@ class PartnersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -232,8 +236,8 @@ class PartnersApi { /// Parameters: /// /// * [String] id (required): - Future removePartner(String id,) async { - final response = await removePartnerWithHttpInfo(id,); + Future removePartner(String id, { Future? abortTrigger, }) async { + final response = await removePartnerWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -250,7 +254,7 @@ class PartnersApi { /// * [String] id (required): /// /// * [PartnerUpdateDto] partnerUpdateDto (required): - Future updatePartnerWithHttpInfo(String id, PartnerUpdateDto partnerUpdateDto,) async { + Future updatePartnerWithHttpInfo(String id, PartnerUpdateDto partnerUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/partners/{id}' .replaceAll('{id}', id); @@ -273,6 +277,7 @@ class PartnersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -285,8 +290,8 @@ class PartnersApi { /// * [String] id (required): /// /// * [PartnerUpdateDto] partnerUpdateDto (required): - Future updatePartner(String id, PartnerUpdateDto partnerUpdateDto,) async { - final response = await updatePartnerWithHttpInfo(id, partnerUpdateDto,); + Future updatePartner(String id, PartnerUpdateDto partnerUpdateDto, { Future? abortTrigger, }) async { + final response = await updatePartnerWithHttpInfo(id, partnerUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/people_api.dart b/mobile/openapi/lib/api/people_api.dart index 99821f31aa..c35491e110 100644 --- a/mobile/openapi/lib/api/people_api.dart +++ b/mobile/openapi/lib/api/people_api.dart @@ -25,7 +25,7 @@ class PeopleApi { /// Parameters: /// /// * [PersonCreateDto] personCreateDto (required): - Future createPersonWithHttpInfo(PersonCreateDto personCreateDto,) async { + Future createPersonWithHttpInfo(PersonCreateDto personCreateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/people'; @@ -47,6 +47,7 @@ class PeopleApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class PeopleApi { /// Parameters: /// /// * [PersonCreateDto] personCreateDto (required): - Future createPerson(PersonCreateDto personCreateDto,) async { - final response = await createPersonWithHttpInfo(personCreateDto,); + Future createPerson(PersonCreateDto personCreateDto, { Future? abortTrigger, }) async { + final response = await createPersonWithHttpInfo(personCreateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -81,7 +82,7 @@ class PeopleApi { /// Parameters: /// /// * [BulkIdsDto] bulkIdsDto (required): - Future deletePeopleWithHttpInfo(BulkIdsDto bulkIdsDto,) async { + Future deletePeopleWithHttpInfo(BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/people'; @@ -103,6 +104,7 @@ class PeopleApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -113,8 +115,8 @@ class PeopleApi { /// Parameters: /// /// * [BulkIdsDto] bulkIdsDto (required): - Future deletePeople(BulkIdsDto bulkIdsDto,) async { - final response = await deletePeopleWithHttpInfo(bulkIdsDto,); + Future deletePeople(BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { + final response = await deletePeopleWithHttpInfo(bulkIdsDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -129,7 +131,7 @@ class PeopleApi { /// Parameters: /// /// * [String] id (required): - Future deletePersonWithHttpInfo(String id,) async { + Future deletePersonWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/people/{id}' .replaceAll('{id}', id); @@ -152,6 +154,7 @@ class PeopleApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -162,8 +165,8 @@ class PeopleApi { /// Parameters: /// /// * [String] id (required): - Future deletePerson(String id,) async { - final response = await deletePersonWithHttpInfo(id,); + Future deletePerson(String id, { Future? abortTrigger, }) async { + final response = await deletePersonWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -191,7 +194,7 @@ class PeopleApi { /// /// * [bool] withHidden: /// Include hidden people - Future getAllPeopleWithHttpInfo({ String? closestAssetId, String? closestPersonId, int? page, int? size, bool? withHidden, }) async { + Future getAllPeopleWithHttpInfo({ String? closestAssetId, String? closestPersonId, int? page, int? size, bool? withHidden, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/people'; @@ -229,6 +232,7 @@ class PeopleApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -252,8 +256,8 @@ class PeopleApi { /// /// * [bool] withHidden: /// Include hidden people - Future getAllPeople({ String? closestAssetId, String? closestPersonId, int? page, int? size, bool? withHidden, }) async { - final response = await getAllPeopleWithHttpInfo( closestAssetId: closestAssetId, closestPersonId: closestPersonId, page: page, size: size, withHidden: withHidden, ); + Future getAllPeople({ String? closestAssetId, String? closestPersonId, int? page, int? size, bool? withHidden, Future? abortTrigger, }) async { + final response = await getAllPeopleWithHttpInfo(closestAssetId: closestAssetId, closestPersonId: closestPersonId, page: page, size: size, withHidden: withHidden, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -276,7 +280,7 @@ class PeopleApi { /// Parameters: /// /// * [String] id (required): - Future getPersonWithHttpInfo(String id,) async { + Future getPersonWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/people/{id}' .replaceAll('{id}', id); @@ -299,6 +303,7 @@ class PeopleApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -309,8 +314,8 @@ class PeopleApi { /// Parameters: /// /// * [String] id (required): - Future getPerson(String id,) async { - final response = await getPersonWithHttpInfo(id,); + Future getPerson(String id, { Future? abortTrigger, }) async { + final response = await getPersonWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -333,7 +338,7 @@ class PeopleApi { /// Parameters: /// /// * [String] id (required): - Future getPersonStatisticsWithHttpInfo(String id,) async { + Future getPersonStatisticsWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/people/{id}/statistics' .replaceAll('{id}', id); @@ -356,6 +361,7 @@ class PeopleApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -366,8 +372,8 @@ class PeopleApi { /// Parameters: /// /// * [String] id (required): - Future getPersonStatistics(String id,) async { - final response = await getPersonStatisticsWithHttpInfo(id,); + Future getPersonStatistics(String id, { Future? abortTrigger, }) async { + final response = await getPersonStatisticsWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -390,7 +396,7 @@ class PeopleApi { /// Parameters: /// /// * [String] id (required): - Future getPersonThumbnailWithHttpInfo(String id,) async { + Future getPersonThumbnailWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/people/{id}/thumbnail' .replaceAll('{id}', id); @@ -413,6 +419,7 @@ class PeopleApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -423,8 +430,8 @@ class PeopleApi { /// Parameters: /// /// * [String] id (required): - Future getPersonThumbnail(String id,) async { - final response = await getPersonThumbnailWithHttpInfo(id,); + Future getPersonThumbnail(String id, { Future? abortTrigger, }) async { + final response = await getPersonThumbnailWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -449,7 +456,7 @@ class PeopleApi { /// * [String] id (required): /// /// * [MergePersonDto] mergePersonDto (required): - Future mergePersonWithHttpInfo(String id, MergePersonDto mergePersonDto,) async { + Future mergePersonWithHttpInfo(String id, MergePersonDto mergePersonDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/people/{id}/merge' .replaceAll('{id}', id); @@ -472,6 +479,7 @@ class PeopleApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -484,8 +492,8 @@ class PeopleApi { /// * [String] id (required): /// /// * [MergePersonDto] mergePersonDto (required): - Future?> mergePerson(String id, MergePersonDto mergePersonDto,) async { - final response = await mergePersonWithHttpInfo(id, mergePersonDto,); + Future?> mergePerson(String id, MergePersonDto mergePersonDto, { Future? abortTrigger, }) async { + final response = await mergePersonWithHttpInfo(id, mergePersonDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -513,7 +521,7 @@ class PeopleApi { /// * [String] id (required): /// /// * [AssetFaceUpdateDto] assetFaceUpdateDto (required): - Future reassignFacesWithHttpInfo(String id, AssetFaceUpdateDto assetFaceUpdateDto,) async { + Future reassignFacesWithHttpInfo(String id, AssetFaceUpdateDto assetFaceUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/people/{id}/reassign' .replaceAll('{id}', id); @@ -536,6 +544,7 @@ class PeopleApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -548,8 +557,8 @@ class PeopleApi { /// * [String] id (required): /// /// * [AssetFaceUpdateDto] assetFaceUpdateDto (required): - Future?> reassignFaces(String id, AssetFaceUpdateDto assetFaceUpdateDto,) async { - final response = await reassignFacesWithHttpInfo(id, assetFaceUpdateDto,); + Future?> reassignFaces(String id, AssetFaceUpdateDto assetFaceUpdateDto, { Future? abortTrigger, }) async { + final response = await reassignFacesWithHttpInfo(id, assetFaceUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -575,7 +584,7 @@ class PeopleApi { /// Parameters: /// /// * [PeopleUpdateDto] peopleUpdateDto (required): - Future updatePeopleWithHttpInfo(PeopleUpdateDto peopleUpdateDto,) async { + Future updatePeopleWithHttpInfo(PeopleUpdateDto peopleUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/people'; @@ -597,6 +606,7 @@ class PeopleApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -607,8 +617,8 @@ class PeopleApi { /// Parameters: /// /// * [PeopleUpdateDto] peopleUpdateDto (required): - Future?> updatePeople(PeopleUpdateDto peopleUpdateDto,) async { - final response = await updatePeopleWithHttpInfo(peopleUpdateDto,); + Future?> updatePeople(PeopleUpdateDto peopleUpdateDto, { Future? abortTrigger, }) async { + final response = await updatePeopleWithHttpInfo(peopleUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -636,7 +646,7 @@ class PeopleApi { /// * [String] id (required): /// /// * [PersonUpdateDto] personUpdateDto (required): - Future updatePersonWithHttpInfo(String id, PersonUpdateDto personUpdateDto,) async { + Future updatePersonWithHttpInfo(String id, PersonUpdateDto personUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/people/{id}' .replaceAll('{id}', id); @@ -659,6 +669,7 @@ class PeopleApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -671,8 +682,8 @@ class PeopleApi { /// * [String] id (required): /// /// * [PersonUpdateDto] personUpdateDto (required): - Future updatePerson(String id, PersonUpdateDto personUpdateDto,) async { - final response = await updatePersonWithHttpInfo(id, personUpdateDto,); + Future updatePerson(String id, PersonUpdateDto personUpdateDto, { Future? abortTrigger, }) async { + final response = await updatePersonWithHttpInfo(id, personUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/plugins_api.dart b/mobile/openapi/lib/api/plugins_api.dart index a29f597dc4..40892b8a67 100644 --- a/mobile/openapi/lib/api/plugins_api.dart +++ b/mobile/openapi/lib/api/plugins_api.dart @@ -25,7 +25,7 @@ class PluginsApi { /// Parameters: /// /// * [String] id (required): - Future getPluginWithHttpInfo(String id,) async { + Future getPluginWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/plugins/{id}' .replaceAll('{id}', id); @@ -48,6 +48,7 @@ class PluginsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -58,8 +59,8 @@ class PluginsApi { /// Parameters: /// /// * [String] id (required): - Future getPlugin(String id,) async { - final response = await getPluginWithHttpInfo(id,); + Future getPlugin(String id, { Future? abortTrigger, }) async { + final response = await getPluginWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -104,7 +105,7 @@ class PluginsApi { /// /// * [WorkflowType] type: /// Workflow types - Future searchPluginMethodsWithHttpInfo({ String? description, bool? enabled, String? id, String? name, String? pluginName, String? pluginVersion, String? title, WorkflowTrigger? trigger, WorkflowType? type, }) async { + Future searchPluginMethodsWithHttpInfo({ String? description, bool? enabled, String? id, String? name, String? pluginName, String? pluginVersion, String? title, WorkflowTrigger? trigger, WorkflowType? type, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/plugins/methods'; @@ -154,6 +155,7 @@ class PluginsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -186,8 +188,8 @@ class PluginsApi { /// /// * [WorkflowType] type: /// Workflow types - Future?> searchPluginMethods({ String? description, bool? enabled, String? id, String? name, String? pluginName, String? pluginVersion, String? title, WorkflowTrigger? trigger, WorkflowType? type, }) async { - final response = await searchPluginMethodsWithHttpInfo( description: description, enabled: enabled, id: id, name: name, pluginName: pluginName, pluginVersion: pluginVersion, title: title, trigger: trigger, type: type, ); + Future?> searchPluginMethods({ String? description, bool? enabled, String? id, String? name, String? pluginName, String? pluginVersion, String? title, WorkflowTrigger? trigger, WorkflowType? type, Future? abortTrigger, }) async { + final response = await searchPluginMethodsWithHttpInfo(description: description, enabled: enabled, id: id, name: name, pluginName: pluginName, pluginVersion: pluginVersion, title: title, trigger: trigger, type: type, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -209,7 +211,7 @@ class PluginsApi { /// Retrieve workflow templates provided by installed plugins /// /// Note: This method returns the HTTP [Response]. - Future searchPluginTemplatesWithHttpInfo() async { + Future searchPluginTemplatesWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/plugins/templates'; @@ -231,14 +233,15 @@ class PluginsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve workflow templates /// /// Retrieve workflow templates provided by installed plugins - Future?> searchPluginTemplates() async { - final response = await searchPluginTemplatesWithHttpInfo(); + Future?> searchPluginTemplates({ Future? abortTrigger, }) async { + final response = await searchPluginTemplatesWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -276,7 +279,7 @@ class PluginsApi { /// * [String] title: /// /// * [String] version: - Future searchPluginsWithHttpInfo({ String? description, bool? enabled, String? id, String? name, String? title, String? version, }) async { + Future searchPluginsWithHttpInfo({ String? description, bool? enabled, String? id, String? name, String? title, String? version, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/plugins'; @@ -317,6 +320,7 @@ class PluginsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -339,8 +343,8 @@ class PluginsApi { /// * [String] title: /// /// * [String] version: - Future?> searchPlugins({ String? description, bool? enabled, String? id, String? name, String? title, String? version, }) async { - final response = await searchPluginsWithHttpInfo( description: description, enabled: enabled, id: id, name: name, title: title, version: version, ); + Future?> searchPlugins({ String? description, bool? enabled, String? id, String? name, String? title, String? version, Future? abortTrigger, }) async { + final response = await searchPluginsWithHttpInfo(description: description, enabled: enabled, id: id, name: name, title: title, version: version, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/queues_api.dart b/mobile/openapi/lib/api/queues_api.dart index 1312cb5952..39386c23f9 100644 --- a/mobile/openapi/lib/api/queues_api.dart +++ b/mobile/openapi/lib/api/queues_api.dart @@ -27,7 +27,7 @@ class QueuesApi { /// * [QueueName] name (required): /// /// * [QueueDeleteDto] queueDeleteDto (required): - Future emptyQueueWithHttpInfo(QueueName name, QueueDeleteDto queueDeleteDto,) async { + Future emptyQueueWithHttpInfo(QueueName name, QueueDeleteDto queueDeleteDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/queues/{name}/jobs' .replaceAll('{name}', name.toString()); @@ -50,6 +50,7 @@ class QueuesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -62,8 +63,8 @@ class QueuesApi { /// * [QueueName] name (required): /// /// * [QueueDeleteDto] queueDeleteDto (required): - Future emptyQueue(QueueName name, QueueDeleteDto queueDeleteDto,) async { - final response = await emptyQueueWithHttpInfo(name, queueDeleteDto,); + Future emptyQueue(QueueName name, QueueDeleteDto queueDeleteDto, { Future? abortTrigger, }) async { + final response = await emptyQueueWithHttpInfo(name, queueDeleteDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -78,7 +79,7 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - Future getQueueWithHttpInfo(QueueName name,) async { + Future getQueueWithHttpInfo(QueueName name, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/queues/{name}' .replaceAll('{name}', name.toString()); @@ -101,6 +102,7 @@ class QueuesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -111,8 +113,8 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - Future getQueue(QueueName name,) async { - final response = await getQueueWithHttpInfo(name,); + Future getQueue(QueueName name, { Future? abortTrigger, }) async { + final response = await getQueueWithHttpInfo(name, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -138,7 +140,7 @@ class QueuesApi { /// /// * [List] status: /// Filter jobs by status - Future getQueueJobsWithHttpInfo(QueueName name, { List? status, }) async { + Future getQueueJobsWithHttpInfo(QueueName name, { List? status, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/queues/{name}/jobs' .replaceAll('{name}', name.toString()); @@ -165,6 +167,7 @@ class QueuesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -178,8 +181,8 @@ class QueuesApi { /// /// * [List] status: /// Filter jobs by status - Future?> getQueueJobs(QueueName name, { List? status, }) async { - final response = await getQueueJobsWithHttpInfo(name, status: status, ); + Future?> getQueueJobs(QueueName name, { List? status, Future? abortTrigger, }) async { + final response = await getQueueJobsWithHttpInfo(name, status: status, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -201,7 +204,7 @@ class QueuesApi { /// Retrieves a list of queues. /// /// Note: This method returns the HTTP [Response]. - Future getQueuesWithHttpInfo() async { + Future getQueuesWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/queues'; @@ -223,14 +226,15 @@ class QueuesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// List all queues /// /// Retrieves a list of queues. - Future?> getQueues() async { - final response = await getQueuesWithHttpInfo(); + Future?> getQueues({ Future? abortTrigger, }) async { + final response = await getQueuesWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -258,7 +262,7 @@ class QueuesApi { /// * [QueueName] name (required): /// /// * [QueueUpdateDto] queueUpdateDto (required): - Future updateQueueWithHttpInfo(QueueName name, QueueUpdateDto queueUpdateDto,) async { + Future updateQueueWithHttpInfo(QueueName name, QueueUpdateDto queueUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/queues/{name}' .replaceAll('{name}', name.toString()); @@ -281,6 +285,7 @@ class QueuesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -293,8 +298,8 @@ class QueuesApi { /// * [QueueName] name (required): /// /// * [QueueUpdateDto] queueUpdateDto (required): - Future updateQueue(QueueName name, QueueUpdateDto queueUpdateDto,) async { - final response = await updateQueueWithHttpInfo(name, queueUpdateDto,); + Future updateQueue(QueueName name, QueueUpdateDto queueUpdateDto, { Future? abortTrigger, }) async { + final response = await updateQueueWithHttpInfo(name, queueUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/search_api.dart b/mobile/openapi/lib/api/search_api.dart index 6f8a4df902..0118cabdba 100644 --- a/mobile/openapi/lib/api/search_api.dart +++ b/mobile/openapi/lib/api/search_api.dart @@ -21,7 +21,7 @@ class SearchApi { /// Retrieve a list of assets with each asset belonging to a different city. This endpoint is used on the places pages to show a single thumbnail for each city the user has assets in. /// /// Note: This method returns the HTTP [Response]. - Future getAssetsByCityWithHttpInfo() async { + Future getAssetsByCityWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/search/cities'; @@ -43,14 +43,15 @@ class SearchApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve assets by city /// /// Retrieve a list of assets with each asset belonging to a different city. This endpoint is used on the places pages to show a single thumbnail for each city the user has assets in. - Future?> getAssetsByCity() async { - final response = await getAssetsByCityWithHttpInfo(); + Future?> getAssetsByCity({ Future? abortTrigger, }) async { + final response = await getAssetsByCityWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -72,7 +73,7 @@ class SearchApi { /// Retrieve data for the explore section, such as popular people and places. /// /// Note: This method returns the HTTP [Response]. - Future getExploreDataWithHttpInfo() async { + Future getExploreDataWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/search/explore'; @@ -94,14 +95,15 @@ class SearchApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve explore data /// /// Retrieve data for the explore section, such as popular people and places. - Future?> getExploreData() async { - final response = await getExploreDataWithHttpInfo(); + Future?> getExploreData({ Future? abortTrigger, }) async { + final response = await getExploreDataWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -145,7 +147,7 @@ class SearchApi { /// /// * [String] state: /// Filter by state/province - Future getSearchSuggestionsWithHttpInfo(SearchSuggestionType type, { String? country, bool? includeNull, String? lensModel, String? make, String? model, String? state, }) async { + Future getSearchSuggestionsWithHttpInfo(SearchSuggestionType type, { String? country, bool? includeNull, String? lensModel, String? make, String? model, String? state, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/search/suggestions'; @@ -187,6 +189,7 @@ class SearchApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -215,8 +218,8 @@ class SearchApi { /// /// * [String] state: /// Filter by state/province - Future?> getSearchSuggestions(SearchSuggestionType type, { String? country, bool? includeNull, String? lensModel, String? make, String? model, String? state, }) async { - final response = await getSearchSuggestionsWithHttpInfo(type, country: country, includeNull: includeNull, lensModel: lensModel, make: make, model: model, state: state, ); + Future?> getSearchSuggestions(SearchSuggestionType type, { String? country, bool? includeNull, String? lensModel, String? make, String? model, String? state, Future? abortTrigger, }) async { + final response = await getSearchSuggestionsWithHttpInfo(type, country: country, includeNull: includeNull, lensModel: lensModel, make: make, model: model, state: state, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -242,7 +245,7 @@ class SearchApi { /// Parameters: /// /// * [StatisticsSearchDto] statisticsSearchDto (required): - Future searchAssetStatisticsWithHttpInfo(StatisticsSearchDto statisticsSearchDto,) async { + Future searchAssetStatisticsWithHttpInfo(StatisticsSearchDto statisticsSearchDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/search/statistics'; @@ -264,6 +267,7 @@ class SearchApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -274,8 +278,8 @@ class SearchApi { /// Parameters: /// /// * [StatisticsSearchDto] statisticsSearchDto (required): - Future searchAssetStatistics(StatisticsSearchDto statisticsSearchDto,) async { - final response = await searchAssetStatisticsWithHttpInfo(statisticsSearchDto,); + Future searchAssetStatistics(StatisticsSearchDto statisticsSearchDto, { Future? abortTrigger, }) async { + final response = await searchAssetStatisticsWithHttpInfo(statisticsSearchDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -298,7 +302,7 @@ class SearchApi { /// Parameters: /// /// * [MetadataSearchDto] metadataSearchDto (required): - Future searchAssetsWithHttpInfo(MetadataSearchDto metadataSearchDto,) async { + Future searchAssetsWithHttpInfo(MetadataSearchDto metadataSearchDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/search/metadata'; @@ -320,6 +324,7 @@ class SearchApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -330,8 +335,8 @@ class SearchApi { /// Parameters: /// /// * [MetadataSearchDto] metadataSearchDto (required): - Future searchAssets(MetadataSearchDto metadataSearchDto,) async { - final response = await searchAssetsWithHttpInfo(metadataSearchDto,); + Future searchAssets(MetadataSearchDto metadataSearchDto, { Future? abortTrigger, }) async { + final response = await searchAssetsWithHttpInfo(metadataSearchDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -443,7 +448,7 @@ class SearchApi { /// /// * [bool] withExif: /// Include EXIF data in response - Future searchLargeAssetsWithHttpInfo({ List? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List? personIds, int? rating, int? size, String? state, List? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async { + Future searchLargeAssetsWithHttpInfo({ List? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List? personIds, int? rating, int? size, String? state, List? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/search/large-assets'; @@ -559,6 +564,7 @@ class SearchApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -658,8 +664,8 @@ class SearchApi { /// /// * [bool] withExif: /// Include EXIF data in response - Future?> searchLargeAssets({ List? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List? personIds, int? rating, int? size, String? state, List? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async { - final response = await searchLargeAssetsWithHttpInfo( albumIds: albumIds, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, isEncoded: isEncoded, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, lensModel: lensModel, libraryId: libraryId, make: make, minFileSize: minFileSize, model: model, ocr: ocr, personIds: personIds, rating: rating, size: size, state: state, tagIds: tagIds, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, visibility: visibility, withDeleted: withDeleted, withExif: withExif, ); + Future?> searchLargeAssets({ List? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List? personIds, int? rating, int? size, String? state, List? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, Future? abortTrigger, }) async { + final response = await searchLargeAssetsWithHttpInfo(albumIds: albumIds, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, isEncoded: isEncoded, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, lensModel: lensModel, libraryId: libraryId, make: make, minFileSize: minFileSize, model: model, ocr: ocr, personIds: personIds, rating: rating, size: size, state: state, tagIds: tagIds, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, visibility: visibility, withDeleted: withDeleted, withExif: withExif, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -689,7 +695,7 @@ class SearchApi { /// /// * [bool] withHidden: /// Include hidden people - Future searchPersonWithHttpInfo(String name, { bool? withHidden, }) async { + Future searchPersonWithHttpInfo(String name, { bool? withHidden, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/search/person'; @@ -716,6 +722,7 @@ class SearchApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -730,8 +737,8 @@ class SearchApi { /// /// * [bool] withHidden: /// Include hidden people - Future?> searchPerson(String name, { bool? withHidden, }) async { - final response = await searchPersonWithHttpInfo(name, withHidden: withHidden, ); + Future?> searchPerson(String name, { bool? withHidden, Future? abortTrigger, }) async { + final response = await searchPersonWithHttpInfo(name, withHidden: withHidden, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -758,7 +765,7 @@ class SearchApi { /// /// * [String] name (required): /// Place name to search for - Future searchPlacesWithHttpInfo(String name,) async { + Future searchPlacesWithHttpInfo(String name, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/search/places'; @@ -782,6 +789,7 @@ class SearchApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -793,8 +801,8 @@ class SearchApi { /// /// * [String] name (required): /// Place name to search for - Future?> searchPlaces(String name,) async { - final response = await searchPlacesWithHttpInfo(name,); + Future?> searchPlaces(String name, { Future? abortTrigger, }) async { + final response = await searchPlacesWithHttpInfo(name, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -820,7 +828,7 @@ class SearchApi { /// Parameters: /// /// * [RandomSearchDto] randomSearchDto (required): - Future searchRandomWithHttpInfo(RandomSearchDto randomSearchDto,) async { + Future searchRandomWithHttpInfo(RandomSearchDto randomSearchDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/search/random'; @@ -842,6 +850,7 @@ class SearchApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -852,8 +861,8 @@ class SearchApi { /// Parameters: /// /// * [RandomSearchDto] randomSearchDto (required): - Future?> searchRandom(RandomSearchDto randomSearchDto,) async { - final response = await searchRandomWithHttpInfo(randomSearchDto,); + Future?> searchRandom(RandomSearchDto randomSearchDto, { Future? abortTrigger, }) async { + final response = await searchRandomWithHttpInfo(randomSearchDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -879,7 +888,7 @@ class SearchApi { /// Parameters: /// /// * [SmartSearchDto] smartSearchDto (required): - Future searchSmartWithHttpInfo(SmartSearchDto smartSearchDto,) async { + Future searchSmartWithHttpInfo(SmartSearchDto smartSearchDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/search/smart'; @@ -901,6 +910,7 @@ class SearchApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -911,8 +921,8 @@ class SearchApi { /// Parameters: /// /// * [SmartSearchDto] smartSearchDto (required): - Future searchSmart(SmartSearchDto smartSearchDto,) async { - final response = await searchSmartWithHttpInfo(smartSearchDto,); + Future searchSmart(SmartSearchDto smartSearchDto, { Future? abortTrigger, }) async { + final response = await searchSmartWithHttpInfo(smartSearchDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/server_api.dart b/mobile/openapi/lib/api/server_api.dart index dd38ade167..1a46a86188 100644 --- a/mobile/openapi/lib/api/server_api.dart +++ b/mobile/openapi/lib/api/server_api.dart @@ -21,7 +21,7 @@ class ServerApi { /// Delete the currently set server product key. /// /// Note: This method returns the HTTP [Response]. - Future deleteServerLicenseWithHttpInfo() async { + Future deleteServerLicenseWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/server/license'; @@ -43,14 +43,15 @@ class ServerApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Delete server product key /// /// Delete the currently set server product key. - Future deleteServerLicense() async { - final response = await deleteServerLicenseWithHttpInfo(); + Future deleteServerLicense({ Future? abortTrigger, }) async { + final response = await deleteServerLicenseWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -61,7 +62,7 @@ class ServerApi { /// Retrieve a list of information about the server. /// /// Note: This method returns the HTTP [Response]. - Future getAboutInfoWithHttpInfo() async { + Future getAboutInfoWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/server/about'; @@ -83,14 +84,15 @@ class ServerApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get server information /// /// Retrieve a list of information about the server. - Future getAboutInfo() async { - final response = await getAboutInfoWithHttpInfo(); + Future getAboutInfo({ Future? abortTrigger, }) async { + final response = await getAboutInfoWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -109,7 +111,7 @@ class ServerApi { /// Retrieve links to the APKs for the current server version. /// /// Note: This method returns the HTTP [Response]. - Future getApkLinksWithHttpInfo() async { + Future getApkLinksWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/server/apk-links'; @@ -131,14 +133,15 @@ class ServerApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get APK links /// /// Retrieve links to the APKs for the current server version. - Future getApkLinks() async { - final response = await getApkLinksWithHttpInfo(); + Future getApkLinks({ Future? abortTrigger, }) async { + final response = await getApkLinksWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -157,7 +160,7 @@ class ServerApi { /// Retrieve the current server configuration. /// /// Note: This method returns the HTTP [Response]. - Future getServerConfigWithHttpInfo() async { + Future getServerConfigWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/server/config'; @@ -179,14 +182,15 @@ class ServerApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get config /// /// Retrieve the current server configuration. - Future getServerConfig() async { - final response = await getServerConfigWithHttpInfo(); + Future getServerConfig({ Future? abortTrigger, }) async { + final response = await getServerConfigWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -205,7 +209,7 @@ class ServerApi { /// Retrieve available features supported by this server. /// /// Note: This method returns the HTTP [Response]. - Future getServerFeaturesWithHttpInfo() async { + Future getServerFeaturesWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/server/features'; @@ -227,14 +231,15 @@ class ServerApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get features /// /// Retrieve available features supported by this server. - Future getServerFeatures() async { - final response = await getServerFeaturesWithHttpInfo(); + Future getServerFeatures({ Future? abortTrigger, }) async { + final response = await getServerFeaturesWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -253,7 +258,7 @@ class ServerApi { /// Retrieve information about whether the server currently has a product key registered. /// /// Note: This method returns the HTTP [Response]. - Future getServerLicenseWithHttpInfo() async { + Future getServerLicenseWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/server/license'; @@ -275,14 +280,15 @@ class ServerApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get product key /// /// Retrieve information about whether the server currently has a product key registered. - Future getServerLicense() async { - final response = await getServerLicenseWithHttpInfo(); + Future getServerLicense({ Future? abortTrigger, }) async { + final response = await getServerLicenseWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -301,7 +307,7 @@ class ServerApi { /// Retrieve statistics about the entire Immich instance such as asset counts. /// /// Note: This method returns the HTTP [Response]. - Future getServerStatisticsWithHttpInfo() async { + Future getServerStatisticsWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/server/statistics'; @@ -323,14 +329,15 @@ class ServerApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get statistics /// /// Retrieve statistics about the entire Immich instance such as asset counts. - Future getServerStatistics() async { - final response = await getServerStatisticsWithHttpInfo(); + Future getServerStatistics({ Future? abortTrigger, }) async { + final response = await getServerStatisticsWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -349,7 +356,7 @@ class ServerApi { /// Retrieve the current server version in semantic versioning (semver) format. /// /// Note: This method returns the HTTP [Response]. - Future getServerVersionWithHttpInfo() async { + Future getServerVersionWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/server/version'; @@ -371,14 +378,15 @@ class ServerApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get server version /// /// Retrieve the current server version in semantic versioning (semver) format. - Future getServerVersion() async { - final response = await getServerVersionWithHttpInfo(); + Future getServerVersion({ Future? abortTrigger, }) async { + final response = await getServerVersionWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -397,7 +405,7 @@ class ServerApi { /// Retrieve the current storage utilization information of the server. /// /// Note: This method returns the HTTP [Response]. - Future getStorageWithHttpInfo() async { + Future getStorageWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/server/storage'; @@ -419,14 +427,15 @@ class ServerApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get storage /// /// Retrieve the current storage utilization information of the server. - Future getStorage() async { - final response = await getStorageWithHttpInfo(); + Future getStorage({ Future? abortTrigger, }) async { + final response = await getStorageWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -445,7 +454,7 @@ class ServerApi { /// Retrieve all media types supported by the server. /// /// Note: This method returns the HTTP [Response]. - Future getSupportedMediaTypesWithHttpInfo() async { + Future getSupportedMediaTypesWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/server/media-types'; @@ -467,14 +476,15 @@ class ServerApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get supported media types /// /// Retrieve all media types supported by the server. - Future getSupportedMediaTypes() async { - final response = await getSupportedMediaTypesWithHttpInfo(); + Future getSupportedMediaTypes({ Future? abortTrigger, }) async { + final response = await getSupportedMediaTypesWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -493,7 +503,7 @@ class ServerApi { /// Retrieve information about the last time the version check ran. /// /// Note: This method returns the HTTP [Response]. - Future getVersionCheckWithHttpInfo() async { + Future getVersionCheckWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/server/version-check'; @@ -515,14 +525,15 @@ class ServerApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get version check status /// /// Retrieve information about the last time the version check ran. - Future getVersionCheck() async { - final response = await getVersionCheckWithHttpInfo(); + Future getVersionCheck({ Future? abortTrigger, }) async { + final response = await getVersionCheckWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -541,7 +552,7 @@ class ServerApi { /// Retrieve a list of past versions the server has been on. /// /// Note: This method returns the HTTP [Response]. - Future getVersionHistoryWithHttpInfo() async { + Future getVersionHistoryWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/server/version-history'; @@ -563,14 +574,15 @@ class ServerApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get version history /// /// Retrieve a list of past versions the server has been on. - Future?> getVersionHistory() async { - final response = await getVersionHistoryWithHttpInfo(); + Future?> getVersionHistory({ Future? abortTrigger, }) async { + final response = await getVersionHistoryWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -592,7 +604,7 @@ class ServerApi { /// Pong /// /// Note: This method returns the HTTP [Response]. - Future pingServerWithHttpInfo() async { + Future pingServerWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/server/ping'; @@ -614,14 +626,15 @@ class ServerApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Ping /// /// Pong - Future pingServer() async { - final response = await pingServerWithHttpInfo(); + Future pingServer({ Future? abortTrigger, }) async { + final response = await pingServerWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -644,7 +657,7 @@ class ServerApi { /// Parameters: /// /// * [LicenseKeyDto] licenseKeyDto (required): - Future setServerLicenseWithHttpInfo(LicenseKeyDto licenseKeyDto,) async { + Future setServerLicenseWithHttpInfo(LicenseKeyDto licenseKeyDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/server/license'; @@ -666,6 +679,7 @@ class ServerApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -676,8 +690,8 @@ class ServerApi { /// Parameters: /// /// * [LicenseKeyDto] licenseKeyDto (required): - Future setServerLicense(LicenseKeyDto licenseKeyDto,) async { - final response = await setServerLicenseWithHttpInfo(licenseKeyDto,); + Future setServerLicense(LicenseKeyDto licenseKeyDto, { Future? abortTrigger, }) async { + final response = await setServerLicenseWithHttpInfo(licenseKeyDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/sessions_api.dart b/mobile/openapi/lib/api/sessions_api.dart index da508059bc..fdd6c09266 100644 --- a/mobile/openapi/lib/api/sessions_api.dart +++ b/mobile/openapi/lib/api/sessions_api.dart @@ -25,7 +25,7 @@ class SessionsApi { /// Parameters: /// /// * [SessionCreateDto] sessionCreateDto (required): - Future createSessionWithHttpInfo(SessionCreateDto sessionCreateDto,) async { + Future createSessionWithHttpInfo(SessionCreateDto sessionCreateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/sessions'; @@ -47,6 +47,7 @@ class SessionsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class SessionsApi { /// Parameters: /// /// * [SessionCreateDto] sessionCreateDto (required): - Future createSession(SessionCreateDto sessionCreateDto,) async { - final response = await createSessionWithHttpInfo(sessionCreateDto,); + Future createSession(SessionCreateDto sessionCreateDto, { Future? abortTrigger, }) async { + final response = await createSessionWithHttpInfo(sessionCreateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -77,7 +78,7 @@ class SessionsApi { /// Delete all sessions for the user. This will not delete the current session. /// /// Note: This method returns the HTTP [Response]. - Future deleteAllSessionsWithHttpInfo() async { + Future deleteAllSessionsWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/sessions'; @@ -99,14 +100,15 @@ class SessionsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Delete all sessions /// /// Delete all sessions for the user. This will not delete the current session. - Future deleteAllSessions() async { - final response = await deleteAllSessionsWithHttpInfo(); + Future deleteAllSessions({ Future? abortTrigger, }) async { + final response = await deleteAllSessionsWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -121,7 +123,7 @@ class SessionsApi { /// Parameters: /// /// * [String] id (required): - Future deleteSessionWithHttpInfo(String id,) async { + Future deleteSessionWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/sessions/{id}' .replaceAll('{id}', id); @@ -144,6 +146,7 @@ class SessionsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -154,8 +157,8 @@ class SessionsApi { /// Parameters: /// /// * [String] id (required): - Future deleteSession(String id,) async { - final response = await deleteSessionWithHttpInfo(id,); + Future deleteSession(String id, { Future? abortTrigger, }) async { + final response = await deleteSessionWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -166,7 +169,7 @@ class SessionsApi { /// Retrieve a list of sessions for the user. /// /// Note: This method returns the HTTP [Response]. - Future getSessionsWithHttpInfo() async { + Future getSessionsWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/sessions'; @@ -188,14 +191,15 @@ class SessionsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve sessions /// /// Retrieve a list of sessions for the user. - Future?> getSessions() async { - final response = await getSessionsWithHttpInfo(); + Future?> getSessions({ Future? abortTrigger, }) async { + final response = await getSessionsWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -221,7 +225,7 @@ class SessionsApi { /// Parameters: /// /// * [String] id (required): - Future lockSessionWithHttpInfo(String id,) async { + Future lockSessionWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/sessions/{id}/lock' .replaceAll('{id}', id); @@ -244,6 +248,7 @@ class SessionsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -254,8 +259,8 @@ class SessionsApi { /// Parameters: /// /// * [String] id (required): - Future lockSession(String id,) async { - final response = await lockSessionWithHttpInfo(id,); + Future lockSession(String id, { Future? abortTrigger, }) async { + final response = await lockSessionWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -272,7 +277,7 @@ class SessionsApi { /// * [String] id (required): /// /// * [SessionUpdateDto] sessionUpdateDto (required): - Future updateSessionWithHttpInfo(String id, SessionUpdateDto sessionUpdateDto,) async { + Future updateSessionWithHttpInfo(String id, SessionUpdateDto sessionUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/sessions/{id}' .replaceAll('{id}', id); @@ -295,6 +300,7 @@ class SessionsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -307,8 +313,8 @@ class SessionsApi { /// * [String] id (required): /// /// * [SessionUpdateDto] sessionUpdateDto (required): - Future updateSession(String id, SessionUpdateDto sessionUpdateDto,) async { - final response = await updateSessionWithHttpInfo(id, sessionUpdateDto,); + Future updateSession(String id, SessionUpdateDto sessionUpdateDto, { Future? abortTrigger, }) async { + final response = await updateSessionWithHttpInfo(id, sessionUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/shared_links_api.dart b/mobile/openapi/lib/api/shared_links_api.dart index 4750442287..5bd548d7d2 100644 --- a/mobile/openapi/lib/api/shared_links_api.dart +++ b/mobile/openapi/lib/api/shared_links_api.dart @@ -27,7 +27,7 @@ class SharedLinksApi { /// * [String] id (required): /// /// * [AssetIdsDto] assetIdsDto (required): - Future addSharedLinkAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto,) async { + Future addSharedLinkAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/shared-links/{id}/assets' .replaceAll('{id}', id); @@ -50,6 +50,7 @@ class SharedLinksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -62,8 +63,8 @@ class SharedLinksApi { /// * [String] id (required): /// /// * [AssetIdsDto] assetIdsDto (required): - Future?> addSharedLinkAssets(String id, AssetIdsDto assetIdsDto,) async { - final response = await addSharedLinkAssetsWithHttpInfo(id, assetIdsDto,); + Future?> addSharedLinkAssets(String id, AssetIdsDto assetIdsDto, { Future? abortTrigger, }) async { + final response = await addSharedLinkAssetsWithHttpInfo(id, assetIdsDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -89,7 +90,7 @@ class SharedLinksApi { /// Parameters: /// /// * [SharedLinkCreateDto] sharedLinkCreateDto (required): - Future createSharedLinkWithHttpInfo(SharedLinkCreateDto sharedLinkCreateDto,) async { + Future createSharedLinkWithHttpInfo(SharedLinkCreateDto sharedLinkCreateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/shared-links'; @@ -111,6 +112,7 @@ class SharedLinksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -121,8 +123,8 @@ class SharedLinksApi { /// Parameters: /// /// * [SharedLinkCreateDto] sharedLinkCreateDto (required): - Future createSharedLink(SharedLinkCreateDto sharedLinkCreateDto,) async { - final response = await createSharedLinkWithHttpInfo(sharedLinkCreateDto,); + Future createSharedLink(SharedLinkCreateDto sharedLinkCreateDto, { Future? abortTrigger, }) async { + final response = await createSharedLinkWithHttpInfo(sharedLinkCreateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -149,7 +151,7 @@ class SharedLinksApi { /// /// * [String] id: /// Filter by shared link ID - Future getAllSharedLinksWithHttpInfo({ String? albumId, String? id, }) async { + Future getAllSharedLinksWithHttpInfo({ String? albumId, String? id, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/shared-links'; @@ -178,6 +180,7 @@ class SharedLinksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -192,8 +195,8 @@ class SharedLinksApi { /// /// * [String] id: /// Filter by shared link ID - Future?> getAllSharedLinks({ String? albumId, String? id, }) async { - final response = await getAllSharedLinksWithHttpInfo( albumId: albumId, id: id, ); + Future?> getAllSharedLinks({ String? albumId, String? id, Future? abortTrigger, }) async { + final response = await getAllSharedLinksWithHttpInfo(albumId: albumId, id: id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -221,7 +224,7 @@ class SharedLinksApi { /// * [String] key: /// /// * [String] slug: - Future getMySharedLinkWithHttpInfo({ String? key, String? slug, }) async { + Future getMySharedLinkWithHttpInfo({ String? key, String? slug, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/shared-links/me'; @@ -250,6 +253,7 @@ class SharedLinksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -262,8 +266,8 @@ class SharedLinksApi { /// * [String] key: /// /// * [String] slug: - Future getMySharedLink({ String? key, String? slug, }) async { - final response = await getMySharedLinkWithHttpInfo( key: key, slug: slug, ); + Future getMySharedLink({ String? key, String? slug, Future? abortTrigger, }) async { + final response = await getMySharedLinkWithHttpInfo(key: key, slug: slug, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -286,7 +290,7 @@ class SharedLinksApi { /// Parameters: /// /// * [String] id (required): - Future getSharedLinkByIdWithHttpInfo(String id,) async { + Future getSharedLinkByIdWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/shared-links/{id}' .replaceAll('{id}', id); @@ -309,6 +313,7 @@ class SharedLinksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -319,8 +324,8 @@ class SharedLinksApi { /// Parameters: /// /// * [String] id (required): - Future getSharedLinkById(String id,) async { - final response = await getSharedLinkByIdWithHttpInfo(id,); + Future getSharedLinkById(String id, { Future? abortTrigger, }) async { + final response = await getSharedLinkByIdWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -343,7 +348,7 @@ class SharedLinksApi { /// Parameters: /// /// * [String] id (required): - Future removeSharedLinkWithHttpInfo(String id,) async { + Future removeSharedLinkWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/shared-links/{id}' .replaceAll('{id}', id); @@ -366,6 +371,7 @@ class SharedLinksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -376,8 +382,8 @@ class SharedLinksApi { /// Parameters: /// /// * [String] id (required): - Future removeSharedLink(String id,) async { - final response = await removeSharedLinkWithHttpInfo(id,); + Future removeSharedLink(String id, { Future? abortTrigger, }) async { + final response = await removeSharedLinkWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -394,7 +400,7 @@ class SharedLinksApi { /// * [String] id (required): /// /// * [AssetIdsDto] assetIdsDto (required): - Future removeSharedLinkAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto,) async { + Future removeSharedLinkAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/shared-links/{id}/assets' .replaceAll('{id}', id); @@ -417,6 +423,7 @@ class SharedLinksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -429,8 +436,8 @@ class SharedLinksApi { /// * [String] id (required): /// /// * [AssetIdsDto] assetIdsDto (required): - Future?> removeSharedLinkAssets(String id, AssetIdsDto assetIdsDto,) async { - final response = await removeSharedLinkAssetsWithHttpInfo(id, assetIdsDto,); + Future?> removeSharedLinkAssets(String id, AssetIdsDto assetIdsDto, { Future? abortTrigger, }) async { + final response = await removeSharedLinkAssetsWithHttpInfo(id, assetIdsDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -460,7 +467,7 @@ class SharedLinksApi { /// * [String] key: /// /// * [String] slug: - Future sharedLinkLoginWithHttpInfo(SharedLinkLoginDto sharedLinkLoginDto, { String? key, String? slug, }) async { + Future sharedLinkLoginWithHttpInfo(SharedLinkLoginDto sharedLinkLoginDto, { String? key, String? slug, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/shared-links/login'; @@ -489,6 +496,7 @@ class SharedLinksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -503,8 +511,8 @@ class SharedLinksApi { /// * [String] key: /// /// * [String] slug: - Future sharedLinkLogin(SharedLinkLoginDto sharedLinkLoginDto, { String? key, String? slug, }) async { - final response = await sharedLinkLoginWithHttpInfo(sharedLinkLoginDto, key: key, slug: slug, ); + Future sharedLinkLogin(SharedLinkLoginDto sharedLinkLoginDto, { String? key, String? slug, Future? abortTrigger, }) async { + final response = await sharedLinkLoginWithHttpInfo(sharedLinkLoginDto, key: key, slug: slug, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -529,7 +537,7 @@ class SharedLinksApi { /// * [String] id (required): /// /// * [SharedLinkEditDto] sharedLinkEditDto (required): - Future updateSharedLinkWithHttpInfo(String id, SharedLinkEditDto sharedLinkEditDto,) async { + Future updateSharedLinkWithHttpInfo(String id, SharedLinkEditDto sharedLinkEditDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/shared-links/{id}' .replaceAll('{id}', id); @@ -552,6 +560,7 @@ class SharedLinksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -564,8 +573,8 @@ class SharedLinksApi { /// * [String] id (required): /// /// * [SharedLinkEditDto] sharedLinkEditDto (required): - Future updateSharedLink(String id, SharedLinkEditDto sharedLinkEditDto,) async { - final response = await updateSharedLinkWithHttpInfo(id, sharedLinkEditDto,); + Future updateSharedLink(String id, SharedLinkEditDto sharedLinkEditDto, { Future? abortTrigger, }) async { + final response = await updateSharedLinkWithHttpInfo(id, sharedLinkEditDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/stacks_api.dart b/mobile/openapi/lib/api/stacks_api.dart index a691af2a7d..a99ebe0600 100644 --- a/mobile/openapi/lib/api/stacks_api.dart +++ b/mobile/openapi/lib/api/stacks_api.dart @@ -25,7 +25,7 @@ class StacksApi { /// Parameters: /// /// * [StackCreateDto] stackCreateDto (required): - Future createStackWithHttpInfo(StackCreateDto stackCreateDto,) async { + Future createStackWithHttpInfo(StackCreateDto stackCreateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/stacks'; @@ -47,6 +47,7 @@ class StacksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class StacksApi { /// Parameters: /// /// * [StackCreateDto] stackCreateDto (required): - Future createStack(StackCreateDto stackCreateDto,) async { - final response = await createStackWithHttpInfo(stackCreateDto,); + Future createStack(StackCreateDto stackCreateDto, { Future? abortTrigger, }) async { + final response = await createStackWithHttpInfo(stackCreateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -81,7 +82,7 @@ class StacksApi { /// Parameters: /// /// * [String] id (required): - Future deleteStackWithHttpInfo(String id,) async { + Future deleteStackWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/stacks/{id}' .replaceAll('{id}', id); @@ -104,6 +105,7 @@ class StacksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -114,8 +116,8 @@ class StacksApi { /// Parameters: /// /// * [String] id (required): - Future deleteStack(String id,) async { - final response = await deleteStackWithHttpInfo(id,); + Future deleteStack(String id, { Future? abortTrigger, }) async { + final response = await deleteStackWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -130,7 +132,7 @@ class StacksApi { /// Parameters: /// /// * [BulkIdsDto] bulkIdsDto (required): - Future deleteStacksWithHttpInfo(BulkIdsDto bulkIdsDto,) async { + Future deleteStacksWithHttpInfo(BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/stacks'; @@ -152,6 +154,7 @@ class StacksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -162,8 +165,8 @@ class StacksApi { /// Parameters: /// /// * [BulkIdsDto] bulkIdsDto (required): - Future deleteStacks(BulkIdsDto bulkIdsDto,) async { - final response = await deleteStacksWithHttpInfo(bulkIdsDto,); + Future deleteStacks(BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { + final response = await deleteStacksWithHttpInfo(bulkIdsDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -178,7 +181,7 @@ class StacksApi { /// Parameters: /// /// * [String] id (required): - Future getStackWithHttpInfo(String id,) async { + Future getStackWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/stacks/{id}' .replaceAll('{id}', id); @@ -201,6 +204,7 @@ class StacksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -211,8 +215,8 @@ class StacksApi { /// Parameters: /// /// * [String] id (required): - Future getStack(String id,) async { - final response = await getStackWithHttpInfo(id,); + Future getStack(String id, { Future? abortTrigger, }) async { + final response = await getStackWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -237,7 +241,7 @@ class StacksApi { /// * [String] assetId (required): /// /// * [String] id (required): - Future removeAssetFromStackWithHttpInfo(String assetId, String id,) async { + Future removeAssetFromStackWithHttpInfo(String assetId, String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/stacks/{id}/assets/{assetId}' .replaceAll('{assetId}', assetId) @@ -261,6 +265,7 @@ class StacksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -273,8 +278,8 @@ class StacksApi { /// * [String] assetId (required): /// /// * [String] id (required): - Future removeAssetFromStack(String assetId, String id,) async { - final response = await removeAssetFromStackWithHttpInfo(assetId, id,); + Future removeAssetFromStack(String assetId, String id, { Future? abortTrigger, }) async { + final response = await removeAssetFromStackWithHttpInfo(assetId, id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -290,7 +295,7 @@ class StacksApi { /// /// * [String] primaryAssetId: /// Filter by primary asset ID - Future searchStacksWithHttpInfo({ String? primaryAssetId, }) async { + Future searchStacksWithHttpInfo({ String? primaryAssetId, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/stacks'; @@ -316,6 +321,7 @@ class StacksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -327,8 +333,8 @@ class StacksApi { /// /// * [String] primaryAssetId: /// Filter by primary asset ID - Future?> searchStacks({ String? primaryAssetId, }) async { - final response = await searchStacksWithHttpInfo( primaryAssetId: primaryAssetId, ); + Future?> searchStacks({ String? primaryAssetId, Future? abortTrigger, }) async { + final response = await searchStacksWithHttpInfo(primaryAssetId: primaryAssetId, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -356,7 +362,7 @@ class StacksApi { /// * [String] id (required): /// /// * [StackUpdateDto] stackUpdateDto (required): - Future updateStackWithHttpInfo(String id, StackUpdateDto stackUpdateDto,) async { + Future updateStackWithHttpInfo(String id, StackUpdateDto stackUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/stacks/{id}' .replaceAll('{id}', id); @@ -379,6 +385,7 @@ class StacksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -391,8 +398,8 @@ class StacksApi { /// * [String] id (required): /// /// * [StackUpdateDto] stackUpdateDto (required): - Future updateStack(String id, StackUpdateDto stackUpdateDto,) async { - final response = await updateStackWithHttpInfo(id, stackUpdateDto,); + Future updateStack(String id, StackUpdateDto stackUpdateDto, { Future? abortTrigger, }) async { + final response = await updateStackWithHttpInfo(id, stackUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/sync_api.dart b/mobile/openapi/lib/api/sync_api.dart index e7bc822ace..c2a57c3395 100644 --- a/mobile/openapi/lib/api/sync_api.dart +++ b/mobile/openapi/lib/api/sync_api.dart @@ -25,7 +25,7 @@ class SyncApi { /// Parameters: /// /// * [SyncAckDeleteDto] syncAckDeleteDto (required): - Future deleteSyncAckWithHttpInfo(SyncAckDeleteDto syncAckDeleteDto,) async { + Future deleteSyncAckWithHttpInfo(SyncAckDeleteDto syncAckDeleteDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/sync/ack'; @@ -47,6 +47,7 @@ class SyncApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class SyncApi { /// Parameters: /// /// * [SyncAckDeleteDto] syncAckDeleteDto (required): - Future deleteSyncAck(SyncAckDeleteDto syncAckDeleteDto,) async { - final response = await deleteSyncAckWithHttpInfo(syncAckDeleteDto,); + Future deleteSyncAck(SyncAckDeleteDto syncAckDeleteDto, { Future? abortTrigger, }) async { + final response = await deleteSyncAckWithHttpInfo(syncAckDeleteDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -69,7 +70,7 @@ class SyncApi { /// Retrieve the synchronization acknowledgments for the current session. /// /// Note: This method returns the HTTP [Response]. - Future getSyncAckWithHttpInfo() async { + Future getSyncAckWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/sync/ack'; @@ -91,14 +92,15 @@ class SyncApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve acknowledgements /// /// Retrieve the synchronization acknowledgments for the current session. - Future?> getSyncAck() async { - final response = await getSyncAckWithHttpInfo(); + Future?> getSyncAck({ Future? abortTrigger, }) async { + final response = await getSyncAckWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -124,7 +126,7 @@ class SyncApi { /// Parameters: /// /// * [SyncStreamDto] syncStreamDto (required): - Future getSyncStreamWithHttpInfo(SyncStreamDto syncStreamDto,) async { + Future getSyncStreamWithHttpInfo(SyncStreamDto syncStreamDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/sync/stream'; @@ -146,6 +148,7 @@ class SyncApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -156,8 +159,8 @@ class SyncApi { /// Parameters: /// /// * [SyncStreamDto] syncStreamDto (required): - Future getSyncStream(SyncStreamDto syncStreamDto,) async { - final response = await getSyncStreamWithHttpInfo(syncStreamDto,); + Future getSyncStream(SyncStreamDto syncStreamDto, { Future? abortTrigger, }) async { + final response = await getSyncStreamWithHttpInfo(syncStreamDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -172,7 +175,7 @@ class SyncApi { /// Parameters: /// /// * [SyncAckSetDto] syncAckSetDto (required): - Future sendSyncAckWithHttpInfo(SyncAckSetDto syncAckSetDto,) async { + Future sendSyncAckWithHttpInfo(SyncAckSetDto syncAckSetDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/sync/ack'; @@ -194,6 +197,7 @@ class SyncApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -204,8 +208,8 @@ class SyncApi { /// Parameters: /// /// * [SyncAckSetDto] syncAckSetDto (required): - Future sendSyncAck(SyncAckSetDto syncAckSetDto,) async { - final response = await sendSyncAckWithHttpInfo(syncAckSetDto,); + Future sendSyncAck(SyncAckSetDto syncAckSetDto, { Future? abortTrigger, }) async { + final response = await sendSyncAckWithHttpInfo(syncAckSetDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/system_config_api.dart b/mobile/openapi/lib/api/system_config_api.dart index b04da71273..ba5b82263a 100644 --- a/mobile/openapi/lib/api/system_config_api.dart +++ b/mobile/openapi/lib/api/system_config_api.dart @@ -21,7 +21,7 @@ class SystemConfigApi { /// Retrieve the current system configuration. /// /// Note: This method returns the HTTP [Response]. - Future getConfigWithHttpInfo() async { + Future getConfigWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/system-config'; @@ -43,14 +43,15 @@ class SystemConfigApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get system configuration /// /// Retrieve the current system configuration. - Future getConfig() async { - final response = await getConfigWithHttpInfo(); + Future getConfig({ Future? abortTrigger, }) async { + final response = await getConfigWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -69,7 +70,7 @@ class SystemConfigApi { /// Retrieve the default values for the system configuration. /// /// Note: This method returns the HTTP [Response]. - Future getConfigDefaultsWithHttpInfo() async { + Future getConfigDefaultsWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/system-config/defaults'; @@ -91,14 +92,15 @@ class SystemConfigApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get system configuration defaults /// /// Retrieve the default values for the system configuration. - Future getConfigDefaults() async { - final response = await getConfigDefaultsWithHttpInfo(); + Future getConfigDefaults({ Future? abortTrigger, }) async { + final response = await getConfigDefaultsWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -117,7 +119,7 @@ class SystemConfigApi { /// Retrieve exemplary storage template options. /// /// Note: This method returns the HTTP [Response]. - Future getStorageTemplateOptionsWithHttpInfo() async { + Future getStorageTemplateOptionsWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/system-config/storage-template-options'; @@ -139,14 +141,15 @@ class SystemConfigApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get storage template options /// /// Retrieve exemplary storage template options. - Future getStorageTemplateOptions() async { - final response = await getStorageTemplateOptionsWithHttpInfo(); + Future getStorageTemplateOptions({ Future? abortTrigger, }) async { + final response = await getStorageTemplateOptionsWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -169,7 +172,7 @@ class SystemConfigApi { /// Parameters: /// /// * [SystemConfigDto] systemConfigDto (required): - Future updateConfigWithHttpInfo(SystemConfigDto systemConfigDto,) async { + Future updateConfigWithHttpInfo(SystemConfigDto systemConfigDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/system-config'; @@ -191,6 +194,7 @@ class SystemConfigApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -201,8 +205,8 @@ class SystemConfigApi { /// Parameters: /// /// * [SystemConfigDto] systemConfigDto (required): - Future updateConfig(SystemConfigDto systemConfigDto,) async { - final response = await updateConfigWithHttpInfo(systemConfigDto,); + Future updateConfig(SystemConfigDto systemConfigDto, { Future? abortTrigger, }) async { + final response = await updateConfigWithHttpInfo(systemConfigDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/system_metadata_api.dart b/mobile/openapi/lib/api/system_metadata_api.dart index 63fd7628ec..a1429b54b0 100644 --- a/mobile/openapi/lib/api/system_metadata_api.dart +++ b/mobile/openapi/lib/api/system_metadata_api.dart @@ -21,7 +21,7 @@ class SystemMetadataApi { /// Retrieve the current admin onboarding status. /// /// Note: This method returns the HTTP [Response]. - Future getAdminOnboardingWithHttpInfo() async { + Future getAdminOnboardingWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/system-metadata/admin-onboarding'; @@ -43,14 +43,15 @@ class SystemMetadataApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve admin onboarding /// /// Retrieve the current admin onboarding status. - Future getAdminOnboarding() async { - final response = await getAdminOnboardingWithHttpInfo(); + Future getAdminOnboarding({ Future? abortTrigger, }) async { + final response = await getAdminOnboardingWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -69,7 +70,7 @@ class SystemMetadataApi { /// Retrieve the current state of the reverse geocoding import. /// /// Note: This method returns the HTTP [Response]. - Future getReverseGeocodingStateWithHttpInfo() async { + Future getReverseGeocodingStateWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/system-metadata/reverse-geocoding-state'; @@ -91,14 +92,15 @@ class SystemMetadataApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve reverse geocoding state /// /// Retrieve the current state of the reverse geocoding import. - Future getReverseGeocodingState() async { - final response = await getReverseGeocodingStateWithHttpInfo(); + Future getReverseGeocodingState({ Future? abortTrigger, }) async { + final response = await getReverseGeocodingStateWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -117,7 +119,7 @@ class SystemMetadataApi { /// Retrieve the current state of the version check process. /// /// Note: This method returns the HTTP [Response]. - Future getVersionCheckStateWithHttpInfo() async { + Future getVersionCheckStateWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/system-metadata/version-check-state'; @@ -139,14 +141,15 @@ class SystemMetadataApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve version check state /// /// Retrieve the current state of the version check process. - Future getVersionCheckState() async { - final response = await getVersionCheckStateWithHttpInfo(); + Future getVersionCheckState({ Future? abortTrigger, }) async { + final response = await getVersionCheckStateWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -169,7 +172,7 @@ class SystemMetadataApi { /// Parameters: /// /// * [AdminOnboardingUpdateDto] adminOnboardingUpdateDto (required): - Future updateAdminOnboardingWithHttpInfo(AdminOnboardingUpdateDto adminOnboardingUpdateDto,) async { + Future updateAdminOnboardingWithHttpInfo(AdminOnboardingUpdateDto adminOnboardingUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/system-metadata/admin-onboarding'; @@ -191,6 +194,7 @@ class SystemMetadataApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -201,8 +205,8 @@ class SystemMetadataApi { /// Parameters: /// /// * [AdminOnboardingUpdateDto] adminOnboardingUpdateDto (required): - Future updateAdminOnboarding(AdminOnboardingUpdateDto adminOnboardingUpdateDto,) async { - final response = await updateAdminOnboardingWithHttpInfo(adminOnboardingUpdateDto,); + Future updateAdminOnboarding(AdminOnboardingUpdateDto adminOnboardingUpdateDto, { Future? abortTrigger, }) async { + final response = await updateAdminOnboardingWithHttpInfo(adminOnboardingUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/tags_api.dart b/mobile/openapi/lib/api/tags_api.dart index a6840f9483..c3cf9f545c 100644 --- a/mobile/openapi/lib/api/tags_api.dart +++ b/mobile/openapi/lib/api/tags_api.dart @@ -25,7 +25,7 @@ class TagsApi { /// Parameters: /// /// * [TagBulkAssetsDto] tagBulkAssetsDto (required): - Future bulkTagAssetsWithHttpInfo(TagBulkAssetsDto tagBulkAssetsDto,) async { + Future bulkTagAssetsWithHttpInfo(TagBulkAssetsDto tagBulkAssetsDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/tags/assets'; @@ -47,6 +47,7 @@ class TagsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class TagsApi { /// Parameters: /// /// * [TagBulkAssetsDto] tagBulkAssetsDto (required): - Future bulkTagAssets(TagBulkAssetsDto tagBulkAssetsDto,) async { - final response = await bulkTagAssetsWithHttpInfo(tagBulkAssetsDto,); + Future bulkTagAssets(TagBulkAssetsDto tagBulkAssetsDto, { Future? abortTrigger, }) async { + final response = await bulkTagAssetsWithHttpInfo(tagBulkAssetsDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -81,7 +82,7 @@ class TagsApi { /// Parameters: /// /// * [TagCreateDto] tagCreateDto (required): - Future createTagWithHttpInfo(TagCreateDto tagCreateDto,) async { + Future createTagWithHttpInfo(TagCreateDto tagCreateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/tags'; @@ -103,6 +104,7 @@ class TagsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -113,8 +115,8 @@ class TagsApi { /// Parameters: /// /// * [TagCreateDto] tagCreateDto (required): - Future createTag(TagCreateDto tagCreateDto,) async { - final response = await createTagWithHttpInfo(tagCreateDto,); + Future createTag(TagCreateDto tagCreateDto, { Future? abortTrigger, }) async { + final response = await createTagWithHttpInfo(tagCreateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -137,7 +139,7 @@ class TagsApi { /// Parameters: /// /// * [String] id (required): - Future deleteTagWithHttpInfo(String id,) async { + Future deleteTagWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/tags/{id}' .replaceAll('{id}', id); @@ -160,6 +162,7 @@ class TagsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -170,8 +173,8 @@ class TagsApi { /// Parameters: /// /// * [String] id (required): - Future deleteTag(String id,) async { - final response = await deleteTagWithHttpInfo(id,); + Future deleteTag(String id, { Future? abortTrigger, }) async { + final response = await deleteTagWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -182,7 +185,7 @@ class TagsApi { /// Retrieve a list of all tags. /// /// Note: This method returns the HTTP [Response]. - Future getAllTagsWithHttpInfo() async { + Future getAllTagsWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/tags'; @@ -204,14 +207,15 @@ class TagsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve tags /// /// Retrieve a list of all tags. - Future?> getAllTags() async { - final response = await getAllTagsWithHttpInfo(); + Future?> getAllTags({ Future? abortTrigger, }) async { + final response = await getAllTagsWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -237,7 +241,7 @@ class TagsApi { /// Parameters: /// /// * [String] id (required): - Future getTagByIdWithHttpInfo(String id,) async { + Future getTagByIdWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/tags/{id}' .replaceAll('{id}', id); @@ -260,6 +264,7 @@ class TagsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -270,8 +275,8 @@ class TagsApi { /// Parameters: /// /// * [String] id (required): - Future getTagById(String id,) async { - final response = await getTagByIdWithHttpInfo(id,); + Future getTagById(String id, { Future? abortTrigger, }) async { + final response = await getTagByIdWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -296,7 +301,7 @@ class TagsApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - Future tagAssetsWithHttpInfo(String id, BulkIdsDto bulkIdsDto,) async { + Future tagAssetsWithHttpInfo(String id, BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/tags/{id}/assets' .replaceAll('{id}', id); @@ -319,6 +324,7 @@ class TagsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -331,8 +337,8 @@ class TagsApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - Future?> tagAssets(String id, BulkIdsDto bulkIdsDto,) async { - final response = await tagAssetsWithHttpInfo(id, bulkIdsDto,); + Future?> tagAssets(String id, BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { + final response = await tagAssetsWithHttpInfo(id, bulkIdsDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -360,7 +366,7 @@ class TagsApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - Future untagAssetsWithHttpInfo(String id, BulkIdsDto bulkIdsDto,) async { + Future untagAssetsWithHttpInfo(String id, BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/tags/{id}/assets' .replaceAll('{id}', id); @@ -383,6 +389,7 @@ class TagsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -395,8 +402,8 @@ class TagsApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - Future?> untagAssets(String id, BulkIdsDto bulkIdsDto,) async { - final response = await untagAssetsWithHttpInfo(id, bulkIdsDto,); + Future?> untagAssets(String id, BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { + final response = await untagAssetsWithHttpInfo(id, bulkIdsDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -424,7 +431,7 @@ class TagsApi { /// * [String] id (required): /// /// * [TagUpdateDto] tagUpdateDto (required): - Future updateTagWithHttpInfo(String id, TagUpdateDto tagUpdateDto,) async { + Future updateTagWithHttpInfo(String id, TagUpdateDto tagUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/tags/{id}' .replaceAll('{id}', id); @@ -447,6 +454,7 @@ class TagsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -459,8 +467,8 @@ class TagsApi { /// * [String] id (required): /// /// * [TagUpdateDto] tagUpdateDto (required): - Future updateTag(String id, TagUpdateDto tagUpdateDto,) async { - final response = await updateTagWithHttpInfo(id, tagUpdateDto,); + Future updateTag(String id, TagUpdateDto tagUpdateDto, { Future? abortTrigger, }) async { + final response = await updateTagWithHttpInfo(id, tagUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -483,7 +491,7 @@ class TagsApi { /// Parameters: /// /// * [TagUpsertDto] tagUpsertDto (required): - Future upsertTagsWithHttpInfo(TagUpsertDto tagUpsertDto,) async { + Future upsertTagsWithHttpInfo(TagUpsertDto tagUpsertDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/tags'; @@ -505,6 +513,7 @@ class TagsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -515,8 +524,8 @@ class TagsApi { /// Parameters: /// /// * [TagUpsertDto] tagUpsertDto (required): - Future?> upsertTags(TagUpsertDto tagUpsertDto,) async { - final response = await upsertTagsWithHttpInfo(tagUpsertDto,); + Future?> upsertTags(TagUpsertDto tagUpsertDto, { Future? abortTrigger, }) async { + final response = await upsertTagsWithHttpInfo(tagUpsertDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/timeline_api.dart b/mobile/openapi/lib/api/timeline_api.dart index 6c72f62604..a85aee2d7a 100644 --- a/mobile/openapi/lib/api/timeline_api.dart +++ b/mobile/openapi/lib/api/timeline_api.dart @@ -69,7 +69,7 @@ class TimelineApi { /// /// * [bool] withStacked: /// Include stacked assets in the response. When true, only primary assets from stacks are returned. - Future getTimeBucketWithHttpInfo(String timeBucket, { String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, AssetOrderBy? orderBy, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async { + Future getTimeBucketWithHttpInfo(String timeBucket, { String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, AssetOrderBy? orderBy, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/timeline/bucket'; @@ -138,6 +138,7 @@ class TimelineApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -192,8 +193,8 @@ class TimelineApi { /// /// * [bool] withStacked: /// Include stacked assets in the response. When true, only primary assets from stacks are returned. - Future getTimeBucket(String timeBucket, { String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, AssetOrderBy? orderBy, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async { - final response = await getTimeBucketWithHttpInfo(timeBucket, albumId: albumId, bbox: bbox, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, orderBy: orderBy, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, ); + Future getTimeBucket(String timeBucket, { String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, AssetOrderBy? orderBy, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, Future? abortTrigger, }) async { + final response = await getTimeBucketWithHttpInfo(timeBucket, albumId: albumId, bbox: bbox, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, orderBy: orderBy, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -257,7 +258,7 @@ class TimelineApi { /// /// * [bool] withStacked: /// Include stacked assets in the response. When true, only primary assets from stacks are returned. - Future getTimeBucketsWithHttpInfo({ String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, AssetOrderBy? orderBy, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async { + Future getTimeBucketsWithHttpInfo({ String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, AssetOrderBy? orderBy, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/timeline/buckets'; @@ -325,6 +326,7 @@ class TimelineApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -376,8 +378,8 @@ class TimelineApi { /// /// * [bool] withStacked: /// Include stacked assets in the response. When true, only primary assets from stacks are returned. - Future?> getTimeBuckets({ String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, AssetOrderBy? orderBy, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async { - final response = await getTimeBucketsWithHttpInfo( albumId: albumId, bbox: bbox, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, orderBy: orderBy, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, ); + Future?> getTimeBuckets({ String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, AssetOrderBy? orderBy, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, Future? abortTrigger, }) async { + final response = await getTimeBucketsWithHttpInfo(albumId: albumId, bbox: bbox, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, orderBy: orderBy, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/trash_api.dart b/mobile/openapi/lib/api/trash_api.dart index f1dcbb8896..7b593e5111 100644 --- a/mobile/openapi/lib/api/trash_api.dart +++ b/mobile/openapi/lib/api/trash_api.dart @@ -21,7 +21,7 @@ class TrashApi { /// Permanently delete all items in the trash. /// /// Note: This method returns the HTTP [Response]. - Future emptyTrashWithHttpInfo() async { + Future emptyTrashWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/trash/empty'; @@ -43,14 +43,15 @@ class TrashApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Empty trash /// /// Permanently delete all items in the trash. - Future emptyTrash() async { - final response = await emptyTrashWithHttpInfo(); + Future emptyTrash({ Future? abortTrigger, }) async { + final response = await emptyTrashWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -73,7 +74,7 @@ class TrashApi { /// Parameters: /// /// * [BulkIdsDto] bulkIdsDto (required): - Future restoreAssetsWithHttpInfo(BulkIdsDto bulkIdsDto,) async { + Future restoreAssetsWithHttpInfo(BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/trash/restore/assets'; @@ -95,6 +96,7 @@ class TrashApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -105,8 +107,8 @@ class TrashApi { /// Parameters: /// /// * [BulkIdsDto] bulkIdsDto (required): - Future restoreAssets(BulkIdsDto bulkIdsDto,) async { - final response = await restoreAssetsWithHttpInfo(bulkIdsDto,); + Future restoreAssets(BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { + final response = await restoreAssetsWithHttpInfo(bulkIdsDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -125,7 +127,7 @@ class TrashApi { /// Restore all items in the trash. /// /// Note: This method returns the HTTP [Response]. - Future restoreTrashWithHttpInfo() async { + Future restoreTrashWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/trash/restore'; @@ -147,14 +149,15 @@ class TrashApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Restore trash /// /// Restore all items in the trash. - Future restoreTrash() async { - final response = await restoreTrashWithHttpInfo(); + Future restoreTrash({ Future? abortTrigger, }) async { + final response = await restoreTrashWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/users_admin_api.dart b/mobile/openapi/lib/api/users_admin_api.dart index 5e165ffd5d..fd6b43d9ce 100644 --- a/mobile/openapi/lib/api/users_admin_api.dart +++ b/mobile/openapi/lib/api/users_admin_api.dart @@ -25,7 +25,7 @@ class UsersAdminApi { /// Parameters: /// /// * [UserAdminCreateDto] userAdminCreateDto (required): - Future createUserAdminWithHttpInfo(UserAdminCreateDto userAdminCreateDto,) async { + Future createUserAdminWithHttpInfo(UserAdminCreateDto userAdminCreateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/users'; @@ -47,6 +47,7 @@ class UsersAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class UsersAdminApi { /// Parameters: /// /// * [UserAdminCreateDto] userAdminCreateDto (required): - Future createUserAdmin(UserAdminCreateDto userAdminCreateDto,) async { - final response = await createUserAdminWithHttpInfo(userAdminCreateDto,); + Future createUserAdmin(UserAdminCreateDto userAdminCreateDto, { Future? abortTrigger, }) async { + final response = await createUserAdminWithHttpInfo(userAdminCreateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -83,7 +84,7 @@ class UsersAdminApi { /// * [String] id (required): /// /// * [UserAdminDeleteDto] userAdminDeleteDto (required): - Future deleteUserAdminWithHttpInfo(String id, UserAdminDeleteDto userAdminDeleteDto,) async { + Future deleteUserAdminWithHttpInfo(String id, UserAdminDeleteDto userAdminDeleteDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/users/{id}' .replaceAll('{id}', id); @@ -106,6 +107,7 @@ class UsersAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -118,8 +120,8 @@ class UsersAdminApi { /// * [String] id (required): /// /// * [UserAdminDeleteDto] userAdminDeleteDto (required): - Future deleteUserAdmin(String id, UserAdminDeleteDto userAdminDeleteDto,) async { - final response = await deleteUserAdminWithHttpInfo(id, userAdminDeleteDto,); + Future deleteUserAdmin(String id, UserAdminDeleteDto userAdminDeleteDto, { Future? abortTrigger, }) async { + final response = await deleteUserAdminWithHttpInfo(id, userAdminDeleteDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -142,7 +144,7 @@ class UsersAdminApi { /// Parameters: /// /// * [String] id (required): - Future getUserAdminWithHttpInfo(String id,) async { + Future getUserAdminWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/users/{id}' .replaceAll('{id}', id); @@ -165,6 +167,7 @@ class UsersAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -175,8 +178,8 @@ class UsersAdminApi { /// Parameters: /// /// * [String] id (required): - Future getUserAdmin(String id,) async { - final response = await getUserAdminWithHttpInfo(id,); + Future getUserAdmin(String id, { Future? abortTrigger, }) async { + final response = await getUserAdminWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -199,7 +202,7 @@ class UsersAdminApi { /// Parameters: /// /// * [String] id (required): - Future getUserPreferencesAdminWithHttpInfo(String id,) async { + Future getUserPreferencesAdminWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/users/{id}/preferences' .replaceAll('{id}', id); @@ -222,6 +225,7 @@ class UsersAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -232,8 +236,8 @@ class UsersAdminApi { /// Parameters: /// /// * [String] id (required): - Future getUserPreferencesAdmin(String id,) async { - final response = await getUserPreferencesAdminWithHttpInfo(id,); + Future getUserPreferencesAdmin(String id, { Future? abortTrigger, }) async { + final response = await getUserPreferencesAdminWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -256,7 +260,7 @@ class UsersAdminApi { /// Parameters: /// /// * [String] id (required): - Future getUserSessionsAdminWithHttpInfo(String id,) async { + Future getUserSessionsAdminWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/users/{id}/sessions' .replaceAll('{id}', id); @@ -279,6 +283,7 @@ class UsersAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -289,8 +294,8 @@ class UsersAdminApi { /// Parameters: /// /// * [String] id (required): - Future?> getUserSessionsAdmin(String id,) async { - final response = await getUserSessionsAdminWithHttpInfo(id,); + Future?> getUserSessionsAdmin(String id, { Future? abortTrigger, }) async { + final response = await getUserSessionsAdminWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -324,7 +329,7 @@ class UsersAdminApi { /// Filter by trash status /// /// * [AssetVisibility] visibility: - Future getUserStatisticsAdminWithHttpInfo(String id, { bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { + Future getUserStatisticsAdminWithHttpInfo(String id, { bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/users/{id}/statistics' .replaceAll('{id}', id); @@ -357,6 +362,7 @@ class UsersAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -375,8 +381,8 @@ class UsersAdminApi { /// Filter by trash status /// /// * [AssetVisibility] visibility: - Future getUserStatisticsAdmin(String id, { bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { - final response = await getUserStatisticsAdminWithHttpInfo(id, isFavorite: isFavorite, isTrashed: isTrashed, visibility: visibility, ); + Future getUserStatisticsAdmin(String id, { bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, Future? abortTrigger, }) async { + final response = await getUserStatisticsAdminWithHttpInfo(id, isFavorite: isFavorite, isTrashed: isTrashed, visibility: visibility, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -399,7 +405,7 @@ class UsersAdminApi { /// Parameters: /// /// * [String] id (required): - Future restoreUserAdminWithHttpInfo(String id,) async { + Future restoreUserAdminWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/users/{id}/restore' .replaceAll('{id}', id); @@ -422,6 +428,7 @@ class UsersAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -432,8 +439,8 @@ class UsersAdminApi { /// Parameters: /// /// * [String] id (required): - Future restoreUserAdmin(String id,) async { - final response = await restoreUserAdminWithHttpInfo(id,); + Future restoreUserAdmin(String id, { Future? abortTrigger, }) async { + final response = await restoreUserAdminWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -460,7 +467,7 @@ class UsersAdminApi { /// /// * [bool] withDeleted: /// Include deleted users - Future searchUsersAdminWithHttpInfo({ String? id, bool? withDeleted, }) async { + Future searchUsersAdminWithHttpInfo({ String? id, bool? withDeleted, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/users'; @@ -489,6 +496,7 @@ class UsersAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -503,8 +511,8 @@ class UsersAdminApi { /// /// * [bool] withDeleted: /// Include deleted users - Future?> searchUsersAdmin({ String? id, bool? withDeleted, }) async { - final response = await searchUsersAdminWithHttpInfo( id: id, withDeleted: withDeleted, ); + Future?> searchUsersAdmin({ String? id, bool? withDeleted, Future? abortTrigger, }) async { + final response = await searchUsersAdminWithHttpInfo(id: id, withDeleted: withDeleted, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -532,7 +540,7 @@ class UsersAdminApi { /// * [String] id (required): /// /// * [UserAdminUpdateDto] userAdminUpdateDto (required): - Future updateUserAdminWithHttpInfo(String id, UserAdminUpdateDto userAdminUpdateDto,) async { + Future updateUserAdminWithHttpInfo(String id, UserAdminUpdateDto userAdminUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/users/{id}' .replaceAll('{id}', id); @@ -555,6 +563,7 @@ class UsersAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -567,8 +576,8 @@ class UsersAdminApi { /// * [String] id (required): /// /// * [UserAdminUpdateDto] userAdminUpdateDto (required): - Future updateUserAdmin(String id, UserAdminUpdateDto userAdminUpdateDto,) async { - final response = await updateUserAdminWithHttpInfo(id, userAdminUpdateDto,); + Future updateUserAdmin(String id, UserAdminUpdateDto userAdminUpdateDto, { Future? abortTrigger, }) async { + final response = await updateUserAdminWithHttpInfo(id, userAdminUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -593,7 +602,7 @@ class UsersAdminApi { /// * [String] id (required): /// /// * [UserPreferencesUpdateDto] userPreferencesUpdateDto (required): - Future updateUserPreferencesAdminWithHttpInfo(String id, UserPreferencesUpdateDto userPreferencesUpdateDto,) async { + Future updateUserPreferencesAdminWithHttpInfo(String id, UserPreferencesUpdateDto userPreferencesUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/users/{id}/preferences' .replaceAll('{id}', id); @@ -616,6 +625,7 @@ class UsersAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -628,8 +638,8 @@ class UsersAdminApi { /// * [String] id (required): /// /// * [UserPreferencesUpdateDto] userPreferencesUpdateDto (required): - Future updateUserPreferencesAdmin(String id, UserPreferencesUpdateDto userPreferencesUpdateDto,) async { - final response = await updateUserPreferencesAdminWithHttpInfo(id, userPreferencesUpdateDto,); + Future updateUserPreferencesAdmin(String id, UserPreferencesUpdateDto userPreferencesUpdateDto, { Future? abortTrigger, }) async { + final response = await updateUserPreferencesAdminWithHttpInfo(id, userPreferencesUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/users_api.dart b/mobile/openapi/lib/api/users_api.dart index 401cf4e94b..a7fac3ea66 100644 --- a/mobile/openapi/lib/api/users_api.dart +++ b/mobile/openapi/lib/api/users_api.dart @@ -26,7 +26,7 @@ class UsersApi { /// /// * [MultipartFile] file (required): /// Profile image file - Future createProfileImageWithHttpInfo(MultipartFile file,) async { + Future createProfileImageWithHttpInfo(MultipartFile file, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/users/profile-image'; @@ -58,6 +58,7 @@ class UsersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -69,8 +70,8 @@ class UsersApi { /// /// * [MultipartFile] file (required): /// Profile image file - Future createProfileImage(MultipartFile file,) async { - final response = await createProfileImageWithHttpInfo(file,); + Future createProfileImage(MultipartFile file, { Future? abortTrigger, }) async { + final response = await createProfileImageWithHttpInfo(file, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -89,7 +90,7 @@ class UsersApi { /// Delete the profile image of the current user. /// /// Note: This method returns the HTTP [Response]. - Future deleteProfileImageWithHttpInfo() async { + Future deleteProfileImageWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/users/profile-image'; @@ -111,14 +112,15 @@ class UsersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Delete user profile image /// /// Delete the profile image of the current user. - Future deleteProfileImage() async { - final response = await deleteProfileImageWithHttpInfo(); + Future deleteProfileImage({ Future? abortTrigger, }) async { + final response = await deleteProfileImageWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -129,7 +131,7 @@ class UsersApi { /// Delete the registered product key for the current user. /// /// Note: This method returns the HTTP [Response]. - Future deleteUserLicenseWithHttpInfo() async { + Future deleteUserLicenseWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/users/me/license'; @@ -151,14 +153,15 @@ class UsersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Delete user product key /// /// Delete the registered product key for the current user. - Future deleteUserLicense() async { - final response = await deleteUserLicenseWithHttpInfo(); + Future deleteUserLicense({ Future? abortTrigger, }) async { + final response = await deleteUserLicenseWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -169,7 +172,7 @@ class UsersApi { /// Delete the onboarding status of the current user. /// /// Note: This method returns the HTTP [Response]. - Future deleteUserOnboardingWithHttpInfo() async { + Future deleteUserOnboardingWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/users/me/onboarding'; @@ -191,14 +194,15 @@ class UsersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Delete user onboarding /// /// Delete the onboarding status of the current user. - Future deleteUserOnboarding() async { - final response = await deleteUserOnboardingWithHttpInfo(); + Future deleteUserOnboarding({ Future? abortTrigger, }) async { + final response = await deleteUserOnboardingWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -209,7 +213,7 @@ class UsersApi { /// Retrieve the preferences for the current user. /// /// Note: This method returns the HTTP [Response]. - Future getMyPreferencesWithHttpInfo() async { + Future getMyPreferencesWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/users/me/preferences'; @@ -231,14 +235,15 @@ class UsersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get my preferences /// /// Retrieve the preferences for the current user. - Future getMyPreferences() async { - final response = await getMyPreferencesWithHttpInfo(); + Future getMyPreferences({ Future? abortTrigger, }) async { + final response = await getMyPreferencesWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -257,7 +262,7 @@ class UsersApi { /// Retrieve information about the user making the API request. /// /// Note: This method returns the HTTP [Response]. - Future getMyUserWithHttpInfo() async { + Future getMyUserWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/users/me'; @@ -279,14 +284,15 @@ class UsersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get current user /// /// Retrieve information about the user making the API request. - Future getMyUser() async { - final response = await getMyUserWithHttpInfo(); + Future getMyUser({ Future? abortTrigger, }) async { + final response = await getMyUserWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -309,7 +315,7 @@ class UsersApi { /// Parameters: /// /// * [String] id (required): - Future getProfileImageWithHttpInfo(String id,) async { + Future getProfileImageWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/users/{id}/profile-image' .replaceAll('{id}', id); @@ -332,6 +338,7 @@ class UsersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -342,8 +349,8 @@ class UsersApi { /// Parameters: /// /// * [String] id (required): - Future getProfileImage(String id,) async { - final response = await getProfileImageWithHttpInfo(id,); + Future getProfileImage(String id, { Future? abortTrigger, }) async { + final response = await getProfileImageWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -366,7 +373,7 @@ class UsersApi { /// Parameters: /// /// * [String] id (required): - Future getUserWithHttpInfo(String id,) async { + Future getUserWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/users/{id}' .replaceAll('{id}', id); @@ -389,6 +396,7 @@ class UsersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -399,8 +407,8 @@ class UsersApi { /// Parameters: /// /// * [String] id (required): - Future getUser(String id,) async { - final response = await getUserWithHttpInfo(id,); + Future getUser(String id, { Future? abortTrigger, }) async { + final response = await getUserWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -419,7 +427,7 @@ class UsersApi { /// Retrieve information about whether the current user has a registered product key. /// /// Note: This method returns the HTTP [Response]. - Future getUserLicenseWithHttpInfo() async { + Future getUserLicenseWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/users/me/license'; @@ -441,14 +449,15 @@ class UsersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve user product key /// /// Retrieve information about whether the current user has a registered product key. - Future getUserLicense() async { - final response = await getUserLicenseWithHttpInfo(); + Future getUserLicense({ Future? abortTrigger, }) async { + final response = await getUserLicenseWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -467,7 +476,7 @@ class UsersApi { /// Retrieve the onboarding status of the current user. /// /// Note: This method returns the HTTP [Response]. - Future getUserOnboardingWithHttpInfo() async { + Future getUserOnboardingWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/users/me/onboarding'; @@ -489,14 +498,15 @@ class UsersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve user onboarding /// /// Retrieve the onboarding status of the current user. - Future getUserOnboarding() async { - final response = await getUserOnboardingWithHttpInfo(); + Future getUserOnboarding({ Future? abortTrigger, }) async { + final response = await getUserOnboardingWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -515,7 +525,7 @@ class UsersApi { /// Retrieve a list of all users on the server. /// /// Note: This method returns the HTTP [Response]. - Future searchUsersWithHttpInfo() async { + Future searchUsersWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/users'; @@ -537,14 +547,15 @@ class UsersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get all users /// /// Retrieve a list of all users on the server. - Future?> searchUsers() async { - final response = await searchUsersWithHttpInfo(); + Future?> searchUsers({ Future? abortTrigger, }) async { + final response = await searchUsersWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -570,7 +581,7 @@ class UsersApi { /// Parameters: /// /// * [LicenseKeyDto] licenseKeyDto (required): - Future setUserLicenseWithHttpInfo(LicenseKeyDto licenseKeyDto,) async { + Future setUserLicenseWithHttpInfo(LicenseKeyDto licenseKeyDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/users/me/license'; @@ -592,6 +603,7 @@ class UsersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -602,8 +614,8 @@ class UsersApi { /// Parameters: /// /// * [LicenseKeyDto] licenseKeyDto (required): - Future setUserLicense(LicenseKeyDto licenseKeyDto,) async { - final response = await setUserLicenseWithHttpInfo(licenseKeyDto,); + Future setUserLicense(LicenseKeyDto licenseKeyDto, { Future? abortTrigger, }) async { + final response = await setUserLicenseWithHttpInfo(licenseKeyDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -626,7 +638,7 @@ class UsersApi { /// Parameters: /// /// * [OnboardingDto] onboardingDto (required): - Future setUserOnboardingWithHttpInfo(OnboardingDto onboardingDto,) async { + Future setUserOnboardingWithHttpInfo(OnboardingDto onboardingDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/users/me/onboarding'; @@ -648,6 +660,7 @@ class UsersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -658,8 +671,8 @@ class UsersApi { /// Parameters: /// /// * [OnboardingDto] onboardingDto (required): - Future setUserOnboarding(OnboardingDto onboardingDto,) async { - final response = await setUserOnboardingWithHttpInfo(onboardingDto,); + Future setUserOnboarding(OnboardingDto onboardingDto, { Future? abortTrigger, }) async { + final response = await setUserOnboardingWithHttpInfo(onboardingDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -682,7 +695,7 @@ class UsersApi { /// Parameters: /// /// * [UserPreferencesUpdateDto] userPreferencesUpdateDto (required): - Future updateMyPreferencesWithHttpInfo(UserPreferencesUpdateDto userPreferencesUpdateDto,) async { + Future updateMyPreferencesWithHttpInfo(UserPreferencesUpdateDto userPreferencesUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/users/me/preferences'; @@ -704,6 +717,7 @@ class UsersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -714,8 +728,8 @@ class UsersApi { /// Parameters: /// /// * [UserPreferencesUpdateDto] userPreferencesUpdateDto (required): - Future updateMyPreferences(UserPreferencesUpdateDto userPreferencesUpdateDto,) async { - final response = await updateMyPreferencesWithHttpInfo(userPreferencesUpdateDto,); + Future updateMyPreferences(UserPreferencesUpdateDto userPreferencesUpdateDto, { Future? abortTrigger, }) async { + final response = await updateMyPreferencesWithHttpInfo(userPreferencesUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -738,7 +752,7 @@ class UsersApi { /// Parameters: /// /// * [UserUpdateMeDto] userUpdateMeDto (required): - Future updateMyUserWithHttpInfo(UserUpdateMeDto userUpdateMeDto,) async { + Future updateMyUserWithHttpInfo(UserUpdateMeDto userUpdateMeDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/users/me'; @@ -760,6 +774,7 @@ class UsersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -770,8 +785,8 @@ class UsersApi { /// Parameters: /// /// * [UserUpdateMeDto] userUpdateMeDto (required): - Future updateMyUser(UserUpdateMeDto userUpdateMeDto,) async { - final response = await updateMyUserWithHttpInfo(userUpdateMeDto,); + Future updateMyUser(UserUpdateMeDto userUpdateMeDto, { Future? abortTrigger, }) async { + final response = await updateMyUserWithHttpInfo(userUpdateMeDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/views_api.dart b/mobile/openapi/lib/api/views_api.dart index a45e89d58f..3ccbacb650 100644 --- a/mobile/openapi/lib/api/views_api.dart +++ b/mobile/openapi/lib/api/views_api.dart @@ -25,7 +25,7 @@ class ViewsApi { /// Parameters: /// /// * [String] path (required): - Future getAssetsByOriginalPathWithHttpInfo(String path,) async { + Future getAssetsByOriginalPathWithHttpInfo(String path, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/view/folder'; @@ -49,6 +49,7 @@ class ViewsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -59,8 +60,8 @@ class ViewsApi { /// Parameters: /// /// * [String] path (required): - Future?> getAssetsByOriginalPath(String path,) async { - final response = await getAssetsByOriginalPathWithHttpInfo(path,); + Future?> getAssetsByOriginalPath(String path, { Future? abortTrigger, }) async { + final response = await getAssetsByOriginalPathWithHttpInfo(path, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -82,7 +83,7 @@ class ViewsApi { /// Retrieve a list of unique folder paths from asset original paths. /// /// Note: This method returns the HTTP [Response]. - Future getUniqueOriginalPathsWithHttpInfo() async { + Future getUniqueOriginalPathsWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/view/folder/unique-paths'; @@ -104,14 +105,15 @@ class ViewsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve unique paths /// /// Retrieve a list of unique folder paths from asset original paths. - Future?> getUniqueOriginalPaths() async { - final response = await getUniqueOriginalPathsWithHttpInfo(); + Future?> getUniqueOriginalPaths({ Future? abortTrigger, }) async { + final response = await getUniqueOriginalPathsWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/workflows_api.dart b/mobile/openapi/lib/api/workflows_api.dart index 12b33b7238..4b27acd624 100644 --- a/mobile/openapi/lib/api/workflows_api.dart +++ b/mobile/openapi/lib/api/workflows_api.dart @@ -25,7 +25,7 @@ class WorkflowsApi { /// Parameters: /// /// * [WorkflowCreateDto] workflowCreateDto (required): - Future createWorkflowWithHttpInfo(WorkflowCreateDto workflowCreateDto,) async { + Future createWorkflowWithHttpInfo(WorkflowCreateDto workflowCreateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/workflows'; @@ -47,6 +47,7 @@ class WorkflowsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class WorkflowsApi { /// Parameters: /// /// * [WorkflowCreateDto] workflowCreateDto (required): - Future createWorkflow(WorkflowCreateDto workflowCreateDto,) async { - final response = await createWorkflowWithHttpInfo(workflowCreateDto,); + Future createWorkflow(WorkflowCreateDto workflowCreateDto, { Future? abortTrigger, }) async { + final response = await createWorkflowWithHttpInfo(workflowCreateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -81,7 +82,7 @@ class WorkflowsApi { /// Parameters: /// /// * [String] id (required): - Future deleteWorkflowWithHttpInfo(String id,) async { + Future deleteWorkflowWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/workflows/{id}' .replaceAll('{id}', id); @@ -104,6 +105,7 @@ class WorkflowsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -114,8 +116,8 @@ class WorkflowsApi { /// Parameters: /// /// * [String] id (required): - Future deleteWorkflow(String id,) async { - final response = await deleteWorkflowWithHttpInfo(id,); + Future deleteWorkflow(String id, { Future? abortTrigger, }) async { + final response = await deleteWorkflowWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -130,7 +132,7 @@ class WorkflowsApi { /// Parameters: /// /// * [String] id (required): - Future getWorkflowWithHttpInfo(String id,) async { + Future getWorkflowWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/workflows/{id}' .replaceAll('{id}', id); @@ -153,6 +155,7 @@ class WorkflowsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -163,8 +166,8 @@ class WorkflowsApi { /// Parameters: /// /// * [String] id (required): - Future getWorkflow(String id,) async { - final response = await getWorkflowWithHttpInfo(id,); + Future getWorkflow(String id, { Future? abortTrigger, }) async { + final response = await getWorkflowWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -187,7 +190,7 @@ class WorkflowsApi { /// Parameters: /// /// * [String] id (required): - Future getWorkflowForShareWithHttpInfo(String id,) async { + Future getWorkflowForShareWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/workflows/{id}/share' .replaceAll('{id}', id); @@ -210,6 +213,7 @@ class WorkflowsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -220,8 +224,8 @@ class WorkflowsApi { /// Parameters: /// /// * [String] id (required): - Future getWorkflowForShare(String id,) async { - final response = await getWorkflowForShareWithHttpInfo(id,); + Future getWorkflowForShare(String id, { Future? abortTrigger, }) async { + final response = await getWorkflowForShareWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -240,7 +244,7 @@ class WorkflowsApi { /// Retrieve a list of all available workflow triggers. /// /// Note: This method returns the HTTP [Response]. - Future getWorkflowTriggersWithHttpInfo() async { + Future getWorkflowTriggersWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/workflows/triggers'; @@ -262,14 +266,15 @@ class WorkflowsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// List all workflow triggers /// /// Retrieve a list of all available workflow triggers. - Future?> getWorkflowTriggers() async { - final response = await getWorkflowTriggersWithHttpInfo(); + Future?> getWorkflowTriggers({ Future? abortTrigger, }) async { + final response = await getWorkflowTriggersWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -308,7 +313,7 @@ class WorkflowsApi { /// /// * [WorkflowTrigger] trigger: /// Workflow trigger type - Future searchWorkflowsWithHttpInfo({ String? description, bool? enabled, String? id, String? name, WorkflowTrigger? trigger, }) async { + Future searchWorkflowsWithHttpInfo({ String? description, bool? enabled, String? id, String? name, WorkflowTrigger? trigger, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/workflows'; @@ -346,6 +351,7 @@ class WorkflowsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -369,8 +375,8 @@ class WorkflowsApi { /// /// * [WorkflowTrigger] trigger: /// Workflow trigger type - Future?> searchWorkflows({ String? description, bool? enabled, String? id, String? name, WorkflowTrigger? trigger, }) async { - final response = await searchWorkflowsWithHttpInfo( description: description, enabled: enabled, id: id, name: name, trigger: trigger, ); + Future?> searchWorkflows({ String? description, bool? enabled, String? id, String? name, WorkflowTrigger? trigger, Future? abortTrigger, }) async { + final response = await searchWorkflowsWithHttpInfo(description: description, enabled: enabled, id: id, name: name, trigger: trigger, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -398,7 +404,7 @@ class WorkflowsApi { /// * [String] id (required): /// /// * [WorkflowUpdateDto] workflowUpdateDto (required): - Future updateWorkflowWithHttpInfo(String id, WorkflowUpdateDto workflowUpdateDto,) async { + Future updateWorkflowWithHttpInfo(String id, WorkflowUpdateDto workflowUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/workflows/{id}' .replaceAll('{id}', id); @@ -421,6 +427,7 @@ class WorkflowsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -433,8 +440,8 @@ class WorkflowsApi { /// * [String] id (required): /// /// * [WorkflowUpdateDto] workflowUpdateDto (required): - Future updateWorkflow(String id, WorkflowUpdateDto workflowUpdateDto,) async { - final response = await updateWorkflowWithHttpInfo(id, workflowUpdateDto,); + Future updateWorkflow(String id, WorkflowUpdateDto workflowUpdateDto, { Future? abortTrigger, }) async { + final response = await updateWorkflowWithHttpInfo(id, workflowUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index fd0d73aebb..3145b7faf4 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -44,8 +44,9 @@ class ApiClient { Object? body, Map headerParams, Map formParams, - String? contentType, - ) async { + String? contentType, { + Future? abortTrigger, + }) async { await authentication?.applyToParams(queryParams, headerParams); headerParams.addAll(_defaultHeaderMap); @@ -63,7 +64,7 @@ class ApiClient { body is MultipartFile && (contentType == null || !contentType.toLowerCase().startsWith('multipart/form-data')) ) { - final request = StreamedRequest(method, uri); + final request = AbortableStreamedRequest(method, uri, abortTrigger: abortTrigger); request.headers.addAll(headerParams); request.contentLength = body.length; body.finalize().listen( @@ -78,7 +79,7 @@ class ApiClient { } if (body is MultipartRequest) { - final request = MultipartRequest(method, uri); + final request = AbortableMultipartRequest(method, uri, abortTrigger: abortTrigger); request.fields.addAll(body.fields); request.files.addAll(body.files); request.headers.addAll(body.headers); @@ -92,14 +93,19 @@ class ApiClient { : await serializeAsync(body); final nullableHeaderParams = headerParams.isEmpty ? null : headerParams; - switch(method) { - case 'POST': return await _client.post(uri, headers: nullableHeaderParams, body: msgBody,); - case 'PUT': return await _client.put(uri, headers: nullableHeaderParams, body: msgBody,); - case 'DELETE': return await _client.delete(uri, headers: nullableHeaderParams, body: msgBody,); - case 'PATCH': return await _client.patch(uri, headers: nullableHeaderParams, body: msgBody,); - case 'HEAD': return await _client.head(uri, headers: nullableHeaderParams,); - case 'GET': return await _client.get(uri, headers: nullableHeaderParams,); + final request = AbortableRequest(method, uri, abortTrigger: abortTrigger); + if (nullableHeaderParams != null) { + request.headers.addAll(nullableHeaderParams); } + if (msgBody is String) { + request.body = msgBody; + } else if (msgBody is List) { + request.bodyBytes = msgBody; + } else if (msgBody is Map) { + request.bodyFields = msgBody; + } + final response = await _client.send(request); + return Response.fromStream(response); } on SocketException catch (error, trace) { throw ApiException.withInner( HttpStatus.badRequest, @@ -136,11 +142,6 @@ class ApiClient { trace, ); } - - throw ApiException( - HttpStatus.badRequest, - 'Invalid HTTP operation: $method $path', - ); } Future deserializeAsync(String value, String targetType, {bool growable = false,}) => diff --git a/open-api/patch/api_client.dart.patch b/open-api/patch/api_client.dart.patch index a813c1c033..55acb0d3cd 100644 --- a/open-api/patch/api_client.dart.patch +++ b/open-api/patch/api_client.dart.patch @@ -1,30 +1,96 @@ @@ -13,7 +13,7 @@ class ApiClient { ApiClient({this.basePath = '/api', this.authentication,}); - + - final String basePath; + String basePath; final Authentication? authentication; - + var _client = Client(); -@@ -143,19 +143,19 @@ - ); +@@ -44,8 +44,9 @@ + Object? body, + Map headerParams, + Map formParams, +- String? contentType, +- ) async { ++ String? contentType, { ++ Future? abortTrigger, ++ }) async { + await authentication?.applyToParams(queryParams, headerParams); + + headerParams.addAll(_defaultHeaderMap); +@@ -63,7 +64,7 @@ + body is MultipartFile && (contentType == null || + !contentType.toLowerCase().startsWith('multipart/form-data')) + ) { +- final request = StreamedRequest(method, uri); ++ final request = AbortableStreamedRequest(method, uri, abortTrigger: abortTrigger); + request.headers.addAll(headerParams); + request.contentLength = body.length; + body.finalize().listen( +@@ -78,7 +79,7 @@ + } + + if (body is MultipartRequest) { +- final request = MultipartRequest(method, uri); ++ final request = AbortableMultipartRequest(method, uri, abortTrigger: abortTrigger); + request.fields.addAll(body.fields); + request.files.addAll(body.files); + request.headers.addAll(body.headers); +@@ -92,14 +93,19 @@ + : await serializeAsync(body); + final nullableHeaderParams = headerParams.isEmpty ? null : headerParams; + +- switch(method) { +- case 'POST': return await _client.post(uri, headers: nullableHeaderParams, body: msgBody,); +- case 'PUT': return await _client.put(uri, headers: nullableHeaderParams, body: msgBody,); +- case 'DELETE': return await _client.delete(uri, headers: nullableHeaderParams, body: msgBody,); +- case 'PATCH': return await _client.patch(uri, headers: nullableHeaderParams, body: msgBody,); +- case 'HEAD': return await _client.head(uri, headers: nullableHeaderParams,); +- case 'GET': return await _client.get(uri, headers: nullableHeaderParams,); ++ final request = AbortableRequest(method, uri, abortTrigger: abortTrigger); ++ if (nullableHeaderParams != null) { ++ request.headers.addAll(nullableHeaderParams); + } ++ if (msgBody is String) { ++ request.body = msgBody; ++ } else if (msgBody is List) { ++ request.bodyBytes = msgBody; ++ } else if (msgBody is Map) { ++ request.bodyFields = msgBody; ++ } ++ final response = await _client.send(request); ++ return Response.fromStream(response); + } on SocketException catch (error, trace) { + throw ApiException.withInner( + HttpStatus.badRequest, +@@ -136,26 +146,21 @@ + trace, + ); + } +- +- throw ApiException( +- HttpStatus.badRequest, +- 'Invalid HTTP operation: $method $path', +- ); } - + - Future deserializeAsync(String value, String targetType, {bool growable = false,}) async => + Future deserializeAsync(String value, String targetType, {bool growable = false,}) => // ignore: deprecated_member_use_from_same_package deserialize(value, targetType, growable: growable); - + @Deprecated('Scheduled for removal in OpenAPI Generator 6.x. Use deserializeAsync() instead.') - dynamic deserialize(String value, String targetType, {bool growable = false,}) { + Future deserialize(String value, String targetType, {bool growable = false,}) async { // Remove all spaces. Necessary for regular expressions as well. targetType = targetType.replaceAll(' ', ''); // ignore: parameter_assignments - + // If the expected target type is String, nothing to do... return targetType == 'String' ? value - : fromJson(json.decode(value), targetType, growable: growable); + : fromJson(await compute((String j) => json.decode(j), value), targetType, growable: growable); } + + // ignore: deprecated_member_use_from_same_package diff --git a/open-api/templates/mobile/api.mustache b/open-api/templates/mobile/api.mustache index ac32571123..2cd4c0f04e 100644 --- a/open-api/templates/mobile/api.mustache +++ b/open-api/templates/mobile/api.mustache @@ -49,7 +49,7 @@ class {{{classname}}} { /// {{/-last}} {{/allParams}} - Future {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} }{{/hasOptionalParams}}) async { + Future {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}}, {{/required}}{{/allParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}}, {{/required}}{{/allParams}}Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'{{{path}}}'{{#pathParams}} .replaceAll({{=<% %>=}}'{<% baseName %>}'<%={{ }}=%>, {{{paramName}}}{{^isString}}.toString(){{/isString}}){{/pathParams}}; @@ -128,6 +128,7 @@ class {{{classname}}} { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -161,8 +162,8 @@ class {{{classname}}} { /// {{/-last}} {{/allParams}} - Future<{{#returnType}}{{{.}}}?{{/returnType}}{{^returnType}}void{{/returnType}}> {{{nickname}}}({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} }{{/hasOptionalParams}}) async { - final response = await {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}} {{#allParams}}{{^required}}{{{paramName}}}: {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} {{/hasOptionalParams}}); + Future<{{#returnType}}{{{.}}}?{{/returnType}}{{^returnType}}void{{/returnType}}> {{{nickname}}}({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}}, {{/required}}{{/allParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}}, {{/required}}{{/allParams}}Future? abortTrigger, }) async { + final response = await {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{paramName}}}, {{/required}}{{/allParams}}{{#allParams}}{{^required}}{{{paramName}}}: {{{paramName}}}, {{/required}}{{/allParams}}abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/open-api/templates/mobile/api.mustache.patch b/open-api/templates/mobile/api.mustache.patch index e3f888d6d7..feb5f40047 100644 --- a/open-api/templates/mobile/api.mustache.patch +++ b/open-api/templates/mobile/api.mustache.patch @@ -1,8 +1,11 @@ ---- api.mustache 2025-01-22 05:50:25 -+++ api.mustache.modified 2025-01-22 05:52:23 -@@ -51,7 +51,7 @@ +--- api.mustache ++++ api.mustache.modified +@@ -49,9 +49,9 @@ + /// + {{/-last}} {{/allParams}} - Future {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} }{{/hasOptionalParams}}) async { +- Future {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} }{{/hasOptionalParams}}) async { ++ Future {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}}, {{/required}}{{/allParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}}, {{/required}}{{/allParams}}Future? abortTrigger, }) async { // ignore: prefer_const_declarations - final path = r'{{{path}}}'{{#pathParams}} + final apiPath = r'{{{path}}}'{{#pathParams}} @@ -18,7 +21,7 @@ {{#formParams}} {{^isFile}} if ({{{paramName}}} != null) { -@@ -121,7 +121,7 @@ +@@ -121,13 +121,14 @@ {{/isMultipart}} return apiClient.invokeAPI( @@ -27,3 +30,21 @@ '{{{httpMethod}}}', queryParams, postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, ++ abortTrigger: abortTrigger, + ); + } + +@@ -161,8 +162,8 @@ + /// + {{/-last}} + {{/allParams}} +- Future<{{#returnType}}{{{.}}}?{{/returnType}}{{^returnType}}void{{/returnType}}> {{{nickname}}}({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} }{{/hasOptionalParams}}) async { +- final response = await {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}} {{#allParams}}{{^required}}{{{paramName}}}: {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} {{/hasOptionalParams}}); ++ Future<{{#returnType}}{{{.}}}?{{/returnType}}{{^returnType}}void{{/returnType}}> {{{nickname}}}({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}}, {{/required}}{{/allParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}}, {{/required}}{{/allParams}}Future? abortTrigger, }) async { ++ final response = await {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{paramName}}}, {{/required}}{{/allParams}}{{#allParams}}{{^required}}{{{paramName}}}: {{{paramName}}}, {{/required}}{{/allParams}}abortTrigger: abortTrigger,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } From 206992605e28ca492665744077aef63031f944c0 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 30 May 2026 13:14:49 -0500 Subject: [PATCH 13/50] feat: upload local assets to album from bottom sheet (#28531) * feat: upload local assets to album from bottom sheet * Cancel token * refactor * refactor * Update mobile/lib/domain/services/remote_album.service.dart Co-authored-by: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> --------- Co-authored-by: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> --- .../domain/services/remote_album.service.dart | 37 +++----- .../add_action_button.widget.dart | 12 ++- .../widgets/album/album_selector.widget.dart | 7 +- .../album/pending_uploads_banner.widget.dart | 19 +++- .../general_bottom_sheet.widget.dart | 54 ++++------- .../local_album_bottom_sheet.widget.dart | 63 +++++++++++-- .../remote_album_bottom_sheet.widget.dart | 37 ++++---- .../album/pending_album_uploads.provider.dart | 5 ++ .../infrastructure/action.provider.dart | 90 ++++++++++++++++++- .../infrastructure/remote_album.provider.dart | 33 +++++++ 10 files changed, 258 insertions(+), 99 deletions(-) diff --git a/mobile/lib/domain/services/remote_album.service.dart b/mobile/lib/domain/services/remote_album.service.dart index e873a7631f..35a8f899a8 100644 --- a/mobile/lib/domain/services/remote_album.service.dart +++ b/mobile/lib/domain/services/remote_album.service.dart @@ -192,43 +192,30 @@ class RemoteAlbumService { required UserDto uploader, required AlbumAssetCandidates candidates, UploadCallbacks uploadCallbacks = const UploadCallbacks(), + Completer? cancelToken, }) async { int addedCount = 0; if (candidates.remoteAssetIds.isNotEmpty) { addedCount += await addAssets(albumId: albumId, assetIds: candidates.remoteAssetIds); } if (candidates.localAssetsToUpload.isNotEmpty) { - addedCount += await _uploadAndAddLocals(albumId, uploader, candidates.localAssetsToUpload, uploadCallbacks); + addedCount += await _uploadAndAddLocals( + albumId, + uploader, + candidates.localAssetsToUpload, + uploadCallbacks, + cancelToken, + ); } return addedCount; } - /// Creates an album, seeding it with already-remote asset IDs, then uploads - /// local-only assets and links each one as it finishes. - Future createAlbumWithAssets({ - required String title, - required UserDto owner, - String? description, - AlbumAssetCandidates candidates = const AlbumAssetCandidates(remoteAssetIds: [], localAssetsToUpload: []), - UploadCallbacks uploadCallbacks = const UploadCallbacks(), - }) async { - final album = await createAlbum( - title: title, - owner: owner, - description: description, - assetIds: candidates.remoteAssetIds, - ); - if (candidates.localAssetsToUpload.isNotEmpty) { - await _uploadAndAddLocals(album.id, owner, candidates.localAssetsToUpload, uploadCallbacks); - } - return album; - } - Future _uploadAndAddLocals( String albumId, UserDto uploader, List localAssets, UploadCallbacks userCallbacks, + Completer? cancelToken, ) async { int addedCount = 0; final pendingAdds = >[]; @@ -258,7 +245,7 @@ class RemoteAlbumService { return; } pendingAdds.add( - _linkUploadedAssetToAlbum(albumId, remoteId, uploader, source) + linkUploadedAssetToAlbum(albumId, remoteId, uploader, source) .then((added) { addedCount += added; }) @@ -269,7 +256,7 @@ class RemoteAlbumService { }, ); - await _uploadService.uploadManual(localAssets, callbacks: wrappedCallbacks); + await _uploadService.uploadManual(localAssets, callbacks: wrappedCallbacks, cancelToken: cancelToken); await Future.wait(pendingAdds); return addedCount; } @@ -288,7 +275,7 @@ class RemoteAlbumService { /// `remote_asset_entity` row from the local source so the FK-protected /// junction insert succeeds. Sync overwrites the placeholder later with /// the authoritative server data. - Future _linkUploadedAssetToAlbum(String albumId, String remoteId, UserDto uploader, LocalAsset source) async { + Future linkUploadedAssetToAlbum(String albumId, String remoteId, UserDto uploader, LocalAsset source) async { final result = await _albumApiRepository.addAssets(albumId, [remoteId]); if (result.added.isEmpty) { return 0; diff --git a/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart index ecfe4a60fe..1ab3f2039d 100644 --- a/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart @@ -6,6 +6,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_bu import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -142,13 +143,18 @@ class _AddActionButtonState extends ConsumerState { return; } - final addedCount = await ref.read(remoteAlbumProvider.notifier).addAssets(album.id, [latest.remoteId!]); + final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.viewer, album); if (!context.mounted) { return; } - if (addedCount == 0) { + if (!result.success) { + ImmichToast.show(context: context, msg: 'scaffold_body_error_occurred'.tr(), toastType: ToastType.error); + return; + } + + if (result.count == 0) { ImmichToast.show( context: context, msg: 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {'album': album.name}), @@ -159,7 +165,7 @@ class _AddActionButtonState extends ConsumerState { msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}), ); - // Invalidate using the asset's remote ID to refresh the "Appears in" list + // Refresh the "Appears in" list on the asset's info panel. ref.invalidate(albumsContainingAssetProvider(latest.remoteId!)); } diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index 981fe2b1f8..5a174bfc5e 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -7,7 +7,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; @@ -746,12 +745,10 @@ class AddToAlbumHeader extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { Future onCreateAlbum() async { + final selectedAssets = ref.read(multiSelectProvider).selectedAssets; final newAlbum = await ref .read(remoteAlbumProvider.notifier) - .createAlbum( - title: "Untitled Album", - assetIds: ref.read(multiSelectProvider).selectedAssets.map((e) => (e as RemoteAsset).id).toList(), - ); + .createAlbumWithAssets(title: "Untitled Album", assets: selectedAssets); if (newAlbum == null) { ImmichToast.show(context: context, toastType: ToastType.error, msg: 'errors.failed_to_create_album'.tr()); diff --git a/mobile/lib/presentation/widgets/album/pending_uploads_banner.widget.dart b/mobile/lib/presentation/widgets/album/pending_uploads_banner.widget.dart index 397170ec54..2701316e75 100644 --- a/mobile/lib/presentation/widgets/album/pending_uploads_banner.widget.dart +++ b/mobile/lib/presentation/widgets/album/pending_uploads_banner.widget.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/album/pending_album_uploads.provider.dart'; +import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart'; /// Pinned banner sliver that surfaces in-flight album uploads directly under /// the album app bar. Renders nothing while the queue is empty. Tapping the @@ -165,6 +166,8 @@ class _PendingUploadsSheet extends ConsumerWidget { } final failedCount = pending.where((p) => p.failed).length; + final inFlightCount = pending.length - failedCount; + final canAbort = inFlightCount > 0 && ref.watch(manualUploadCancelTokenProvider) != null; return SafeArea( child: Padding( @@ -183,7 +186,21 @@ class _PendingUploadsSheet extends ConsumerWidget { style: context.textTheme.titleMedium, ), ), - if (failedCount > 0) + if (canAbort) + TextButton.icon( + onPressed: () { + final cancelToken = ref.read(manualUploadCancelTokenProvider); + if (cancelToken != null && !cancelToken.isCompleted) { + cancelToken.complete(); + } + ref.read(manualUploadCancelTokenProvider.notifier).state = null; + ref.read(pendingAlbumUploadsProvider(albumId).notifier).clear(); + }, + icon: const Icon(Icons.stop_circle_outlined, size: 18), + label: Text('cancel'.t(context: context)), + style: TextButton.styleFrom(foregroundColor: context.colorScheme.error), + ) + else if (failedCount > 0) TextButton.icon( onPressed: () => ref.read(pendingAlbumUploadsProvider(albumId).notifier).clearFailed(), icon: const Icon(Icons.clear_rounded, size: 18), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart index 0bafacfe54..c3a569407a 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart @@ -3,9 +3,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/setting.model.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/bulk_tag_assets_action_button.widget.dart'; @@ -25,7 +23,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/infrastructure/setting.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; @@ -63,37 +61,23 @@ class _GeneralBottomSheetState extends ConsumerState { userMetadataPreferencesProvider.select((value) => value.valueOrNull?.tagsEnabled ?? false), ); - Future addAssetsToAlbum(RemoteAlbum album) async { - final selectedAssets = multiselect.selectedAssets; - if (selectedAssets.isEmpty) { + Future addToAlbum(RemoteAlbum album) async { + final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.timeline, album); + + if (!context.mounted) { return; } - final remoteAssets = selectedAssets.whereType(); - final addedCount = await ref - .read(remoteAlbumProvider.notifier) - .addAssets(album.id, remoteAssets.map((e) => e.id).toList()); - - if (selectedAssets.length != remoteAssets.length) { - ImmichToast.show( - context: context, - msg: 'add_to_album_bottom_sheet_some_local_assets'.t(context: context), - ); + if (!result.success) { + ImmichToast.show(context: context, msg: 'scaffold_body_error_occurred'.tr(), toastType: ToastType.error); + return; } - - if (addedCount != remoteAssets.length) { - ImmichToast.show( - context: context, - msg: 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {"album": album.name}), - ); - } else { - ImmichToast.show( - context: context, - msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {"album": album.name}), - ); - } - - ref.read(multiSelectProvider.notifier).reset(); + ImmichToast.show( + context: context, + msg: result.count == 0 + ? 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {'album': album.name}) + : 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}), + ); } Future onKeyboardExpand() { @@ -131,12 +115,10 @@ class _GeneralBottomSheetState extends ConsumerState { const DeleteLocalActionButton(source: ActionSource.timeline), if (multiselect.onlyLocal) const UploadActionButton(source: ActionSource.timeline), ], - slivers: multiselect.hasRemote - ? [ - const AddToAlbumHeader(), - AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand), - ] - : [], + slivers: [ + const AddToAlbumHeader(), + AlbumSelector(onAlbumSelected: addToAlbum, onKeyboardExpanded: onKeyboardExpand), + ], ); } } diff --git a/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart index b1e87dfaea..ac8c77af03 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart @@ -1,25 +1,78 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; -class LocalAlbumBottomSheet extends ConsumerWidget { +class LocalAlbumBottomSheet extends ConsumerStatefulWidget { const LocalAlbumBottomSheet({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { - return const BaseBottomSheet( + ConsumerState createState() => _LocalAlbumBottomSheetState(); +} + +class _LocalAlbumBottomSheetState extends ConsumerState { + late final DraggableScrollableController sheetController; + + @override + void initState() { + super.initState(); + sheetController = DraggableScrollableController(); + } + + @override + void dispose() { + sheetController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + Future addToAlbum(RemoteAlbum album) async { + final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.timeline, album); + + if (!context.mounted) { + return; + } + + if (!result.success) { + ImmichToast.show(context: context, msg: 'scaffold_body_error_occurred'.tr(), toastType: ToastType.error); + return; + } + + ImmichToast.show( + context: context, + msg: result.count == 0 + ? 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {'album': album.name}) + : 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}), + ); + } + + Future onKeyboardExpand() { + return sheetController.animateTo(0.85, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut); + } + + return BaseBottomSheet( + controller: sheetController, initialChildSize: 0.25, - maxChildSize: 0.4, + maxChildSize: 0.85, shouldCloseOnMinExtent: false, - actions: [ + actions: const [ ShareActionButton(source: ActionSource.timeline), DeleteLocalActionButton(source: ActionSource.timeline), UploadActionButton(source: ActionSource.timeline), ], + slivers: [ + const AddToAlbumHeader(), + AlbumSelector(onAlbumSelected: addToAlbum, onKeyboardExpanded: onKeyboardExpand), + ], ); } } diff --git a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart index 6848a07bb8..6b914ed077 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; @@ -21,7 +20,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_b import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; @@ -56,29 +55,28 @@ class _RemoteAlbumBottomSheetState extends ConsumerState final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash)); final ownsAlbum = ref.watch(currentUserProvider)?.id == widget.album.ownerId; - Future addAssetsToAlbum(RemoteAlbum album) async { - final selectedAssets = multiselect.selectedAssets; - if (selectedAssets.isEmpty) { + Future addToAlbum(RemoteAlbum album) async { + final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.timeline, album); + + if (!context.mounted) { return; } - final addedCount = await ref - .read(remoteAlbumProvider.notifier) - .addAssets(album.id, selectedAssets.map((e) => (e as RemoteAsset).id).toList()); - - if (addedCount != selectedAssets.length) { + if (!result.success) { ImmichToast.show( context: context, - msg: 'add_to_album_bottom_sheet_already_exists'.t(context: context, args: {"album": album.name}), - ); - } else { - ImmichToast.show( - context: context, - msg: 'add_to_album_bottom_sheet_added'.t(context: context, args: {"album": album.name}), + msg: 'scaffold_body_error_occurred'.t(context: context), + toastType: ToastType.error, ); + return; } - ref.read(multiSelectProvider.notifier).reset(); + ImmichToast.show( + context: context, + msg: result.count == 0 + ? 'add_to_album_bottom_sheet_already_exists'.t(context: context, args: {"album": album.name}) + : 'add_to_album_bottom_sheet_added'.t(context: context, args: {"album": album.name}), + ); } Future onKeyboardExpand() { @@ -118,10 +116,7 @@ class _RemoteAlbumBottomSheetState extends ConsumerState SetAlbumCoverActionButton(source: ActionSource.timeline, albumId: widget.album.id), ], slivers: ownsAlbum - ? [ - const AddToAlbumHeader(), - AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand), - ] + ? [const AddToAlbumHeader(), AlbumSelector(onAlbumSelected: addToAlbum, onKeyboardExpanded: onKeyboardExpand)] : null, ); } diff --git a/mobile/lib/providers/album/pending_album_uploads.provider.dart b/mobile/lib/providers/album/pending_album_uploads.provider.dart index db857ba3c0..7b1188891b 100644 --- a/mobile/lib/providers/album/pending_album_uploads.provider.dart +++ b/mobile/lib/providers/album/pending_album_uploads.provider.dart @@ -67,6 +67,11 @@ class AlbumPendingUploadsNotifier extends AutoDisposeFamilyNotifier { } } + Future addToAlbum(ActionSource source, RemoteAlbum album) async { + final selected = _getAssets(source).toList(growable: false); + if (selected.isEmpty) { + return const ActionResult(count: 0, success: true); + } + + final candidates = RemoteAlbumService.categorizeCandidates(selected); + final remoteIds = candidates.remoteAssetIds; + final localAssets = candidates.localAssetsToUpload; + final albumNotifier = ref.read(remoteAlbumProvider.notifier); + + int addedRemote = 0; + if (remoteIds.isNotEmpty) { + try { + addedRemote = await albumNotifier.addAssets(album.id, remoteIds); + } catch (error, stack) { + _logger.severe('Failed to add assets to album ${album.id}', error, stack); + return ActionResult(count: 0, success: false, error: error.toString()); + } + } + + // Keep the selection available for retry if the remote add fails. Once the + // album mutation succeeds, clear timeline selection so upload overlays can render. + if (source == ActionSource.timeline) { + ref.read(multiSelectProvider.notifier).reset(); + } + + if (localAssets.isEmpty) { + return ActionResult(count: addedRemote, success: true); + } + + final uploadResult = await upload( + source, + assets: localAssets, + onAssetUploaded: (asset, remoteId) async { + await albumNotifier.linkUploadedAssetToAlbum(album.id, asset, remoteId); + }, + ); + + return ActionResult( + count: addedRemote + uploadResult.count, + success: uploadResult.success, + error: uploadResult.error, + ); + } + Future removeFromAlbum(ActionSource source, String albumId) async { final ids = _getRemoteIdsForSource(source); try { @@ -495,8 +544,16 @@ class ActionNotifier extends Notifier { } } - Future upload(ActionSource source, {List? assets}) async { + Future upload( + ActionSource source, { + List? assets, + FutureOr Function(LocalAsset asset, String remoteId)? onAssetUploaded, + }) async { final assetsToUpload = assets ?? _getAssets(source).whereType().toList(); + final assetById = {for (final a in assetsToUpload) a.id: a}; + final uploadedAssetIds = {}; + final failedAssetIds = {}; + final postUploadTasks = >[]; final progressNotifier = ref.read(assetUploadProgressProvider.notifier); final cancelToken = Completer(); @@ -518,16 +575,43 @@ class ActionNotifier extends Notifier { }, onSuccess: (localAssetId, remoteAssetId) { progressNotifier.remove(localAssetId); + uploadedAssetIds.add(localAssetId); + final asset = assetById[localAssetId]; + final callback = onAssetUploaded; + if (asset != null && callback != null) { + postUploadTasks.add( + Future.sync(() => callback(asset, remoteAssetId)).catchError((Object error, StackTrace stack) { + failedAssetIds.add(localAssetId); + progressNotifier.setError(localAssetId); + _logger.warning('Post-upload callback failed for $localAssetId', error, stack); + }), + ); + } }, onError: (localAssetId, errorMessage) { + failedAssetIds.add(localAssetId); progressNotifier.setError(localAssetId); }, ), ); - return ActionResult(count: assetsToUpload.length, success: true); + + await Future.wait(postUploadTasks); + final successCount = uploadedAssetIds.difference(failedAssetIds).length; + final isSuccess = successCount == assetsToUpload.length && failedAssetIds.isEmpty; + + return ActionResult( + count: successCount, + success: isSuccess, + error: isSuccess ? null : 'Failed to upload ${assetsToUpload.length - successCount} assets', + ); } catch (error, stack) { _logger.severe('Failed manually upload assets', error, stack); - return ActionResult(count: assetsToUpload.length, success: false, error: error.toString()); + + return ActionResult( + count: uploadedAssetIds.difference(failedAssetIds).length, + success: false, + error: error.toString(), + ); } finally { ref.read(manualUploadCancelTokenProvider.notifier).state = null; Future.delayed(const Duration(seconds: 2), () { diff --git a/mobile/lib/providers/infrastructure/remote_album.provider.dart b/mobile/lib/providers/infrastructure/remote_album.provider.dart index 73a796bd31..a4bbbae818 100644 --- a/mobile/lib/providers/infrastructure/remote_album.provider.dart +++ b/mobile/lib/providers/infrastructure/remote_album.provider.dart @@ -9,6 +9,7 @@ import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/models/albums/album_search.model.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/providers/album/pending_album_uploads.provider.dart'; +import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/services/foreground_upload.service.dart'; @@ -207,6 +208,22 @@ class RemoteAlbumNotifier extends Notifier { return added; } + /// Links a freshly-uploaded local asset to an album using its new remote ID, + /// upserting a placeholder remote asset row so the local DB join survives + /// until the next sync catches up. + Future linkUploadedAssetToAlbum(String albumId, LocalAsset source, String remoteId) async { + final currentUser = ref.read(currentUserProvider); + if (currentUser == null) { + throw Exception('User not logged in'); + } + + final added = await _remoteAlbumService.linkUploadedAssetToAlbum(albumId, remoteId, currentUser, source); + if (added > 0) { + await _refreshAlbumInState(albumId); + } + return added; + } + /// Adds a heterogeneous asset selection to an album. Already-remote assets /// are linked immediately; local-only assets are queued in /// [pendingAlbumUploadsProvider] (so the album page can show them with @@ -221,11 +238,18 @@ class RemoteAlbumNotifier extends Notifier { final pendingNotifier = ref.read(pendingAlbumUploadsProvider(albumId).notifier); pendingNotifier.enqueue(candidates.localAssetsToUpload); + Completer? cancelToken; + if (candidates.localAssetsToUpload.isNotEmpty) { + cancelToken = Completer(); + ref.read(manualUploadCancelTokenProvider.notifier).state = cancelToken; + } + try { final added = await _remoteAlbumService.addAssetsToAlbum( albumId: albumId, uploader: currentUser, candidates: candidates, + cancelToken: cancelToken, uploadCallbacks: UploadCallbacks( onProgress: (localAssetId, _, bytes, totalBytes) { final progress = totalBytes > 0 ? bytes / totalBytes : 0.0; @@ -245,6 +269,15 @@ class RemoteAlbumNotifier extends Notifier { } _logger.severe('Failed to add assets to album $albumId', error, stack); rethrow; + } finally { + if (cancelToken != null) { + if (cancelToken.isCompleted) { + pendingNotifier.clear(); + } + if (ref.read(manualUploadCancelTokenProvider) == cancelToken) { + ref.read(manualUploadCancelTokenProvider.notifier).state = null; + } + } } } From a52e7dc11a270dc9eed1a75756148ccdfee69b7a Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 30 May 2026 23:10:03 -0500 Subject: [PATCH 14/50] chore: feat iOS run debug build (#28712) # Conflicts: # mobile/ios/Runner.xcodeproj/project.pbxproj --- mobile/ios/Runner.xcodeproj/project.pbxproj | 172 ++++++++++-------- .../xcshareddata/swiftpm/Package.resolved | 4 +- 2 files changed, 95 insertions(+), 81 deletions(-) diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index e3325525eb..42926ef45e 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -9,7 +9,7 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B6A31FED0FC846D6BD69BBC /* Pods_ShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */; }; + 467DA6EAF83F3481F8BD94AB /* Pods_ShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8AB817AA297EDEC88B23F3F6 /* Pods_ShareExtension.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -22,7 +22,7 @@ B2EE00022E72CA15008B6CA7 /* PermissionApi.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2EE00012E72CA15008B6CA7 /* PermissionApi.g.swift */; }; B2EE00042E72CA15008B6CA7 /* PermissionApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2EE00032E72CA15008B6CA7 /* PermissionApiImpl.swift */; }; B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */; }; - D218389C4A4C4693F141F7D1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */; }; + D3BED739C0BC29BB32E18EB2 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CC499FBCE6B29B2DAFED7130 /* Pods_Runner.framework */; }; F02538E92DFBCBDD008C3FA3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; F0B57D3A2DF764BD00DC5BCC /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F0B57D392DF764BD00DC5BCC /* WidgetKit.framework */; }; F0B57D3C2DF764BD00DC5BCC /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F0B57D3B2DF764BD00DC5BCC /* SwiftUI.framework */; }; @@ -85,16 +85,18 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 10B378D23F917891A0F23E33 /* Pods-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.release.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.release.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 2E3441B73560D0F6FD25E04F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 571EAA93D77181C7C98C2EA6 /* Pods-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.release.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.release.xcconfig"; sourceTree = ""; }; + 614A7F5DC5DB09E89E4FCBE8 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 681FBA560D5D2ADDE4F0B59E /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 6D160F04A389B9FFBC557803 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8AB817AA297EDEC88B23F3F6 /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 937632897A02DE9C249F20A6 /* Pods-ShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Immich-Debug.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Immich-Debug.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -103,7 +105,6 @@ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; A01DD6982F7F43B40049AB63 /* ImageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRequest.swift; sourceTree = ""; }; - B1FBA9EE014DE20271B0FE77 /* Pods-ShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.profile.xcconfig"; sourceTree = ""; }; B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorkerApiImpl.swift; sourceTree = ""; }; B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.swift; sourceTree = ""; }; B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connectivity.g.swift; sourceTree = ""; }; @@ -111,12 +112,11 @@ B2EE00012E72CA15008B6CA7 /* PermissionApi.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionApi.g.swift; sourceTree = ""; }; B2EE00032E72CA15008B6CA7 /* PermissionApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionApiImpl.swift; sourceTree = ""; }; B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.g.swift; sourceTree = ""; }; - E0E99CDC17B3EB7FA8BA2332 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + C4A6A71F33CE37B3C913115C /* Pods-ShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.profile.xcconfig"; sourceTree = ""; }; + CC499FBCE6B29B2DAFED7130 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F0B57D382DF764BD00DC5BCC /* WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; F0B57D392DF764BD00DC5BCC /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; F0B57D3B2DF764BD00DC5BCC /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; - F7101BB0391A314774615E89 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - F8A35EA3C3E01BD66AFDE0E5 /* Pods-ShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.debug.xcconfig"; sourceTree = ""; }; FA9973382CF6DF4B000EF859 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; FAC6F8902D287C890078CB2F /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; FAC6F8B12D287F120078CB2F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -199,7 +199,7 @@ FEE084F82EC172460045228E /* SQLiteData in Frameworks */, FEE084FB2EC1725A0045228E /* RawStructuredFieldValues in Frameworks */, FEE084FD2EC1725A0045228E /* StructuredFieldValues in Frameworks */, - D218389C4A4C4693F141F7D1 /* Pods_Runner.framework in Frameworks */, + D3BED739C0BC29BB32E18EB2 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -216,7 +216,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 3B6A31FED0FC846D6BD69BBC /* Pods_ShareExtension.framework in Frameworks */, + 467DA6EAF83F3481F8BD94AB /* Pods_ShareExtension.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -226,12 +226,12 @@ 0FB772A5B9601143383626CA /* Pods */ = { isa = PBXGroup; children = ( - 2E3441B73560D0F6FD25E04F /* Pods-Runner.debug.xcconfig */, - E0E99CDC17B3EB7FA8BA2332 /* Pods-Runner.release.xcconfig */, - F7101BB0391A314774615E89 /* Pods-Runner.profile.xcconfig */, - F8A35EA3C3E01BD66AFDE0E5 /* Pods-ShareExtension.debug.xcconfig */, - 571EAA93D77181C7C98C2EA6 /* Pods-ShareExtension.release.xcconfig */, - B1FBA9EE014DE20271B0FE77 /* Pods-ShareExtension.profile.xcconfig */, + 614A7F5DC5DB09E89E4FCBE8 /* Pods-Runner.debug.xcconfig */, + 6D160F04A389B9FFBC557803 /* Pods-Runner.release.xcconfig */, + 681FBA560D5D2ADDE4F0B59E /* Pods-Runner.profile.xcconfig */, + 937632897A02DE9C249F20A6 /* Pods-ShareExtension.debug.xcconfig */, + 10B378D23F917891A0F23E33 /* Pods-ShareExtension.release.xcconfig */, + C4A6A71F33CE37B3C913115C /* Pods-ShareExtension.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -239,10 +239,10 @@ 1754452DD81DA6620E279E51 /* Frameworks */ = { isa = PBXGroup; children = ( - 886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */, - 357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */, F0B57D392DF764BD00DC5BCC /* WidgetKit.framework */, F0B57D3B2DF764BD00DC5BCC /* SwiftUI.framework */, + CC499FBCE6B29B2DAFED7130 /* Pods_Runner.framework */, + 8AB817AA297EDEC88B23F3F6 /* Pods_ShareExtension.framework */, ); name = Frameworks; sourceTree = ""; @@ -370,7 +370,7 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 4044AF030EF7D8721844FFBA /* [CP] Check Pods Manifest.lock */, + BAEA01ACA3F5C9CD3D732370 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, @@ -378,8 +378,8 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, FAC6F89A2D287C890078CB2F /* Embed Foundation Extensions */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - D218A34AEE62BC1EF119F5B0 /* [CP] Embed Pods Frameworks */, - 6724EEB7D74949FA08581154 /* [CP] Copy Pods Resources */, + 513DA7292DED6106813332F4 /* [CP] Embed Pods Frameworks */, + 2FA39DEC809D6D7C4A01EFCB /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -393,6 +393,9 @@ FEE084F22EC172080045228E /* Schemas */, ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Immich-Debug.app */; productType = "com.apple.product-type.application"; @@ -421,7 +424,7 @@ isa = PBXNativeTarget; buildConfigurationList = FAC6F8A02D287C890078CB2F /* Build configuration list for PBXNativeTarget "ShareExtension" */; buildPhases = ( - 3BEF3D71D97E337D921C0EB5 /* [CP] Check Pods Manifest.lock */, + 8EC9CF3E20AF32BF24D4F3E1 /* [CP] Check Pods Manifest.lock */, FAC6F88C2D287C890078CB2F /* Sources */, FAC6F88D2D287C890078CB2F /* Frameworks */, FAC6F88E2D287C890078CB2F /* Resources */, @@ -470,7 +473,7 @@ ); mainGroup = 97C146E51CF9000F007C117D; packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */, FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */, ); @@ -517,6 +520,23 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 2FA39DEC809D6D7C4A01EFCB /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -533,7 +553,24 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 3BEF3D71D97E337D921C0EB5 /* [CP] Check Pods Manifest.lock */ = { + 513DA7292DED6106813332F4 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 8EC9CF3E20AF32BF24D4F3E1 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -555,7 +592,22 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 4044AF030EF7D8721844FFBA /* [CP] Check Pods Manifest.lock */ = { + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; + }; + BAEA01ACA3F5C9CD3D732370 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -577,55 +629,6 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 6724EEB7D74949FA08581154 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; - }; - D218A34AEE62BC1EF119F5B0 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -1092,7 +1095,7 @@ }; FAC6F89C2D287C890078CB2F /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = F8A35EA3C3E01BD66AFDE0E5 /* Pods-ShareExtension.debug.xcconfig */; + baseConfigurationReference = 937632897A02DE9C249F20A6 /* Pods-ShareExtension.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -1135,7 +1138,7 @@ }; FAC6F89D2D287C890078CB2F /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 571EAA93D77181C7C98C2EA6 /* Pods-ShareExtension.release.xcconfig */; + baseConfigurationReference = 10B378D23F917891A0F23E33 /* Pods-ShareExtension.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -1175,7 +1178,7 @@ }; FAC6F89E2D287C890078CB2F /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = B1FBA9EE014DE20271B0FE77 /* Pods-ShareExtension.profile.xcconfig */; + baseConfigurationReference = C4A6A71F33CE37B3C913115C /* Pods-ShareExtension.profile.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -1258,6 +1261,13 @@ }; /* End XCConfigurationList section */ +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + /* Begin XCRemoteSwiftPackageReference section */ FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */ = { isa = XCRemoteSwiftPackageReference; @@ -1278,6 +1288,10 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; FEE084F72EC172460045228E /* SQLiteData */ = { isa = XCSwiftPackageProductDependency; package = FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */; diff --git a/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index ed1e2d084d..0e2d8e8a3d 100644 --- a/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/combine-schedulers", "state" : { - "revision" : "5928286acce13def418ec36d05a001a9641086f2", - "version" : "1.0.3" + "revision" : "fd16d76fd8b9a976d88bfb6cacc05ca8d19c91b6", + "version" : "1.1.0" } }, { From 2382894fa2d70c56693e5c51d5d5d7ce7752e277 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Sun, 31 May 2026 19:30:37 +0530 Subject: [PATCH 15/50] fix: auto route rebuild on settings change (#28717) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- mobile/lib/repositories/auth.repository.dart | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/mobile/lib/repositories/auth.repository.dart b/mobile/lib/repositories/auth.repository.dart index 866d587328..dd9aed3a03 100644 --- a/mobile/lib/repositories/auth.repository.dart +++ b/mobile/lib/repositories/auth.repository.dart @@ -1,38 +1,40 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/config/app_config.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; final authRepositoryProvider = Provider( - (ref) => AuthRepository(ref.watch(driftProvider), ref.watch(appConfigProvider)), + (ref) => AuthRepository(ref.watch(driftProvider), ref.watch(settingsProvider)), ); class AuthRepository { final Drift _drift; - final AppConfig _config; + final SettingsRepository _settings; - const AuthRepository(this._drift, this._config); + const AuthRepository(this._drift, this._settings); Future clearLocalData() async { await SyncStreamRepository(_drift).reset(); } bool getEndpointSwitchingFeature() { - return _config.network.autoEndpointSwitching; + return _settings.appConfig.network.autoEndpointSwitching; } String? getPreferredWifiName() { - return _config.network.preferredWifiName; + return _settings.appConfig.network.preferredWifiName; } String? getLocalEndpoint() { - return _config.network.localEndpoint; + return _settings.appConfig.network.localEndpoint; } List getExternalEndpointList() { - return _config.network.externalEndpointList.map((url) => AuxilaryEndpoint(url: url, status: .valid)).toList(); + return _settings.appConfig.network.externalEndpointList + .map((url) => AuxilaryEndpoint(url: url, status: .valid)) + .toList(); } } From d120444a878bee655ed09de9f675d05ec36df297 Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Mon, 1 Jun 2026 13:41:53 +0100 Subject: [PATCH 16/50] fix(devcontainer): update build cache volume (#28736) --- .devcontainer/server/container-compose-overrides.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/server/container-compose-overrides.yml b/.devcontainer/server/container-compose-overrides.yml index db20390255..c5db2a6b32 100644 --- a/.devcontainer/server/container-compose-overrides.yml +++ b/.devcontainer/server/container-compose-overrides.yml @@ -15,7 +15,7 @@ services: volumes: - ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data - /etc/localtime:/etc/localtime:ro - - pnpm_store_server:/buildcache/pnpm-store + - build_cache:/buildcache - ../packages/plugin-core:/build/plugins/immich-plugin-core immich-web: env_file: !reset [] From c8a1d0e400ca9ecd5ba3b309c1c8918f20290aa5 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:10:07 +0200 Subject: [PATCH 17/50] feat: release candidate support (#28665) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- e2e/src/specs/server/api/server.e2e-spec.ts | 1 + .../server/api/system-config.e2e-spec.ts | 8 +- i18n/en.json | 4 + .../server_info/server_version.model.dart | 14 +- mobile/lib/utils/semver.dart | 72 +++++++--- .../app_bar_dialog/app_bar_server_info.dart | 8 +- mobile/openapi/README.md | 3 + mobile/openapi/lib/api.dart | 3 + mobile/openapi/lib/api_client.dart | 6 + mobile/openapi/lib/api_helper.dart | 6 + mobile/openapi/lib/model/release_channel.dart | 85 +++++++++++ .../openapi/lib/model/release_event_v1.dart | 133 ++++++++++++++++++ mobile/openapi/lib/model/release_type.dart | 100 +++++++++++++ .../model/server_version_response_dto.dart | 28 +++- .../system_config_new_version_check_dto.dart | 10 +- .../services/sync_stream_service_test.dart | 8 +- mobile/test/unit/utils/semver_test.dart | 66 +++++++++ open-api/immich-openapi-specs.json | 77 +++++++++- packages/sdk/src/fetch-client.ts | 26 ++++ pnpm-lock.yaml | 47 ++++--- server/package.json | 2 +- server/src/config.ts | 3 + server/src/decorators.ts | 10 ++ server/src/dtos/server.dto.ts | 49 +++++-- server/src/dtos/sync.dto.ts | 11 +- server/src/dtos/system-config.dto.ts | 9 +- .../repositories/server-info.repository.ts | 7 +- .../src/repositories/websocket.repository.ts | 4 +- .../services/system-config.service.spec.ts | 2 + server/src/services/version.service.spec.ts | 52 +++++-- server/src/services/version.service.ts | 35 +++-- server/src/utils/misc.ts | 4 +- .../side-bar/ServerStatus.svelte | 8 +- web/src/lib/managers/event-manager.svelte.ts | 4 +- .../lib/managers/release-manager.svelte.ts | 4 +- web/src/lib/stores/websocket.ts | 4 +- web/src/lib/types.ts | 10 +- web/src/lib/utils.spec.ts | 24 +--- web/src/lib/utils.ts | 22 +-- web/src/routes/VersionAnnouncement.svelte | 14 +- .../NewVersionCheckSettings.svelte | 17 +++ 41 files changed, 809 insertions(+), 191 deletions(-) create mode 100644 mobile/openapi/lib/model/release_channel.dart create mode 100644 mobile/openapi/lib/model/release_event_v1.dart create mode 100644 mobile/openapi/lib/model/release_type.dart diff --git a/e2e/src/specs/server/api/server.e2e-spec.ts b/e2e/src/specs/server/api/server.e2e-spec.ts index 1220e6cab5..f15d450213 100644 --- a/e2e/src/specs/server/api/server.e2e-spec.ts +++ b/e2e/src/specs/server/api/server.e2e-spec.ts @@ -95,6 +95,7 @@ describe('/server', () => { major: expect.any(Number), minor: expect.any(Number), patch: expect.any(Number), + prerelease: null, }); }); }); diff --git a/e2e/src/specs/server/api/system-config.e2e-spec.ts b/e2e/src/specs/server/api/system-config.e2e-spec.ts index 1bd7bdc489..91b747cf28 100644 --- a/e2e/src/specs/server/api/system-config.e2e-spec.ts +++ b/e2e/src/specs/server/api/system-config.e2e-spec.ts @@ -21,18 +21,18 @@ describe('/system-config', () => { const response1 = await request(app) .put('/system-config') .set('Authorization', `Bearer ${admin.accessToken}`) - .send({ ...config, newVersionCheck: { enabled: false } }); + .send({ ...config, newVersionCheck: { enabled: false, channel: 'stable' } }); expect(response1.status).toBe(200); - expect(response1.body).toEqual({ ...config, newVersionCheck: { enabled: false } }); + expect(response1.body).toEqual({ ...config, newVersionCheck: { enabled: false, channel: 'stable' } }); const response2 = await request(app) .put('/system-config') .set('Authorization', `Bearer ${admin.accessToken}`) - .send({ ...config, newVersionCheck: { enabled: true } }); + .send({ ...config, newVersionCheck: { enabled: true, channel: 'stable' } }); expect(response2.status).toBe(200); - expect(response2.body).toEqual({ ...config, newVersionCheck: { enabled: true } }); + expect(response2.body).toEqual({ ...config, newVersionCheck: { enabled: true, channel: 'stable' } }); }); it('should reject an invalid config entry', async () => { diff --git a/i18n/en.json b/i18n/en.json index dba0caf393..0c3c1aa5e0 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -305,6 +305,8 @@ "refreshing_all_libraries": "Refreshing all libraries", "registration": "Admin Registration", "registration_description": "Since you are the first user on the system, you will be assigned as the Admin and are responsible for administrative tasks, and additional users will be created by you.", + "release_channel_release_candidate": "Release candidate", + "release_channel_stable": "Stable", "remove_failed_jobs": "Remove failed jobs", "require_password_change_on_login": "Require user to change password on first login", "reset_settings_to_default": "Reset settings to default", @@ -442,6 +444,8 @@ "user_settings_description": "Manage user settings", "user_successfully_removed": "User {email} has been successfully removed.", "users_page_description": "Admin users page", + "version_check_channel": "Release channel", + "version_check_channel_description": "Pick the release channel you want to get version announcements for", "version_check_enabled_description": "Enable version check", "version_check_implications": "The version check feature relies on periodic communication with {server}", "version_check_settings": "Version Check", diff --git a/mobile/lib/models/server_info/server_version.model.dart b/mobile/lib/models/server_info/server_version.model.dart index c8bf73db81..40f35a3cd0 100644 --- a/mobile/lib/models/server_info/server_version.model.dart +++ b/mobile/lib/models/server_info/server_version.model.dart @@ -2,16 +2,12 @@ import 'package:immich_mobile/utils/semver.dart'; import 'package:openapi/api.dart'; class ServerVersion extends SemVer { - const ServerVersion({required super.major, required super.minor, required super.patch}); + const ServerVersion({required super.major, required super.minor, required super.patch, super.prerelease}); - @override - String toString() { - return 'ServerVersion(major: $major, minor: $minor, patch: $patch)'; - } + ServerVersion.fromDto(ServerVersionResponseDto dto) + : super(major: dto.major, minor: dto.minor, patch: dto.patch_, prerelease: dto.prerelease); - ServerVersion.fromDto(ServerVersionResponseDto dto) : super(major: dto.major, minor: dto.minor, patch: dto.patch_); - - bool isAtLeast({int major = 0, int minor = 0, int patch = 0}) { - return this >= SemVer(major: major, minor: minor, patch: patch); + bool isAtLeast({int major = 0, int minor = 0, int patch = 0, int? prerelease}) { + return this >= SemVer(major: major, minor: minor, patch: patch, prerelease: prerelease); } } diff --git a/mobile/lib/utils/semver.dart b/mobile/lib/utils/semver.dart index 06b186daa3..8080af00b0 100644 --- a/mobile/lib/utils/semver.dart +++ b/mobile/lib/utils/semver.dart @@ -1,36 +1,42 @@ -enum SemVerType { major, minor, patch } +enum SemVerType { major, minor, patch, prerelease } class SemVer { final int major; final int minor; final int patch; + final int? prerelease; - const SemVer({required this.major, required this.minor, required this.patch}); + const SemVer({required this.major, required this.minor, required this.patch, this.prerelease}); @override String toString() { - return '$major.$minor.$patch'; + return '$major.$minor.$patch${prerelease == null ? '' : '-rc.$prerelease'}'; } - SemVer copyWith({int? major, int? minor, int? patch}) { - return SemVer(major: major ?? this.major, minor: minor ?? this.minor, patch: patch ?? this.patch); + SemVer copyWith({int? major, int? minor, int? patch, int? prerelease}) { + return SemVer( + major: major ?? this.major, + minor: minor ?? this.minor, + patch: patch ?? this.patch, + prerelease: prerelease ?? this.prerelease, + ); } + static final _pattern = RegExp(r'^v?(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?(?:[-+].*)?$', caseSensitive: false); + factory SemVer.fromString(String version) { - if (version.toLowerCase().startsWith("v")) { - version = version.substring(1); - } - - final parts = version.split("-")[0].split('.'); - if (parts.length != 3) { + final match = _pattern.firstMatch(version); + if (match == null) { throw FormatException('Invalid semantic version string: $version'); } - try { - return SemVer(major: int.parse(parts[0]), minor: int.parse(parts[1]), patch: int.parse(parts[2])); - } catch (e) { - throw FormatException('Invalid semantic version string: $version'); - } + final prerelease = match.group(4); + return SemVer( + major: int.parse(match.group(1)!), + minor: int.parse(match.group(2)!), + patch: int.parse(match.group(3)!), + prerelease: prerelease == null ? null : int.parse(prerelease), + ); } bool operator >(SemVer other) { @@ -40,7 +46,10 @@ class SemVer { if (minor != other.minor) { return minor > other.minor; } - return patch > other.patch; + if (patch != other.patch) { + return patch > other.patch; + } + return _comparePrerelease(other) > 0; } bool operator <(SemVer other) { @@ -50,7 +59,23 @@ class SemVer { if (minor != other.minor) { return minor < other.minor; } - return patch < other.patch; + if (patch != other.patch) { + return patch < other.patch; + } + return _comparePrerelease(other) < 0; + } + + int _comparePrerelease(SemVer other) { + if (prerelease == other.prerelease) { + return 0; + } + if (prerelease == null) { + return 1; + } + if (other.prerelease == null) { + return -1; + } + return prerelease!.compareTo(other.prerelease!); } bool operator >=(SemVer other) { @@ -67,7 +92,11 @@ class SemVer { return true; } - return other is SemVer && other.major == major && other.minor == minor && other.patch == patch; + return other is SemVer && + other.major == major && + other.minor == minor && + other.patch == patch && + other.prerelease == prerelease; } SemVerType? differenceType(SemVer other) { @@ -80,10 +109,13 @@ class SemVer { if (patch != other.patch) { return SemVerType.patch; } + if (prerelease != other.prerelease) { + return SemVerType.prerelease; + } return null; } @override - int get hashCode => major.hashCode ^ minor.hashCode ^ patch.hashCode; + int get hashCode => major.hashCode ^ minor.hashCode ^ patch.hashCode ^ prerelease.hashCode; } diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart index 2809505c58..a209d280c3 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart @@ -50,9 +50,7 @@ class AppBarServerInfo extends HookConsumerWidget { divider, _ServerInfoItem( label: "server_version".tr(), - text: serverInfoState.serverVersion.major > 0 - ? "${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch}" - : "--", + text: serverInfoState.serverVersion.major > 0 ? "${serverInfoState.serverVersion}" : "--", ), divider, _ServerInfoItem(label: "server_info_box_server_url".tr(), text: getServerUrl() ?? '--', tooltip: true), @@ -60,9 +58,7 @@ class AppBarServerInfo extends HookConsumerWidget { divider, _ServerInfoItem( label: "latest_version".tr(), - text: serverInfoState.latestVersion!.major > 0 - ? "${serverInfoState.latestVersion!.major}.${serverInfoState.latestVersion!.minor}.${serverInfoState.latestVersion!.patch}" - : "--", + text: serverInfoState.latestVersion!.major > 0 ? "${serverInfoState.latestVersion!}" : "--", tooltip: true, icon: serverInfoState.versionStatus == VersionStatus.serverOutOfDate ? const Icon(Icons.info, color: Color.fromARGB(255, 243, 188, 106), size: 12) diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 23987073dd..2727825df7 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -513,6 +513,9 @@ Class | Method | HTTP request | Description - [RatingsUpdate](doc//RatingsUpdate.md) - [ReactionLevel](doc//ReactionLevel.md) - [ReactionType](doc//ReactionType.md) + - [ReleaseChannel](doc//ReleaseChannel.md) + - [ReleaseEventV1](doc//ReleaseEventV1.md) + - [ReleaseType](doc//ReleaseType.md) - [ReverseGeocodingStateResponseDto](doc//ReverseGeocodingStateResponseDto.md) - [RotateParameters](doc//RotateParameters.md) - [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index d5a6f483dc..cdba19bcff 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -258,6 +258,9 @@ part 'model/ratings_response.dart'; part 'model/ratings_update.dart'; part 'model/reaction_level.dart'; part 'model/reaction_type.dart'; +part 'model/release_channel.dart'; +part 'model/release_event_v1.dart'; +part 'model/release_type.dart'; part 'model/reverse_geocoding_state_response_dto.dart'; part 'model/rotate_parameters.dart'; part 'model/search_album_response_dto.dart'; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 3145b7faf4..14a29e9681 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -563,6 +563,12 @@ class ApiClient { return ReactionLevelTypeTransformer().decode(value); case 'ReactionType': return ReactionTypeTypeTransformer().decode(value); + case 'ReleaseChannel': + return ReleaseChannelTypeTransformer().decode(value); + case 'ReleaseEventV1': + return ReleaseEventV1.fromJson(value); + case 'ReleaseType': + return ReleaseTypeTypeTransformer().decode(value); case 'ReverseGeocodingStateResponseDto': return ReverseGeocodingStateResponseDto.fromJson(value); case 'RotateParameters': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index b5d348edd6..6cf11022c3 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -157,6 +157,12 @@ String parameterToString(dynamic value) { if (value is ReactionType) { return ReactionTypeTypeTransformer().encode(value).toString(); } + if (value is ReleaseChannel) { + return ReleaseChannelTypeTransformer().encode(value).toString(); + } + if (value is ReleaseType) { + return ReleaseTypeTypeTransformer().encode(value).toString(); + } if (value is SearchSuggestionType) { return SearchSuggestionTypeTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/release_channel.dart b/mobile/openapi/lib/model/release_channel.dart new file mode 100644 index 0000000000..48b082af07 --- /dev/null +++ b/mobile/openapi/lib/model/release_channel.dart @@ -0,0 +1,85 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +/// Release channel +class ReleaseChannel { + /// Instantiate a new enum with the provided [value]. + const ReleaseChannel._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const stable = ReleaseChannel._(r'stable'); + static const releaseCandidate = ReleaseChannel._(r'releaseCandidate'); + + /// List of all possible values in this [enum][ReleaseChannel]. + static const values = [ + stable, + releaseCandidate, + ]; + + static ReleaseChannel? fromJson(dynamic value) => ReleaseChannelTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = ReleaseChannel.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [ReleaseChannel] to String, +/// and [decode] dynamic data back to [ReleaseChannel]. +class ReleaseChannelTypeTransformer { + factory ReleaseChannelTypeTransformer() => _instance ??= const ReleaseChannelTypeTransformer._(); + + const ReleaseChannelTypeTransformer._(); + + String encode(ReleaseChannel data) => data.value; + + /// Decodes a [dynamic value][data] to a ReleaseChannel. + /// + /// 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. + ReleaseChannel? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'stable': return ReleaseChannel.stable; + case r'releaseCandidate': return ReleaseChannel.releaseCandidate; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [ReleaseChannelTypeTransformer] instance. + static ReleaseChannelTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/release_event_v1.dart b/mobile/openapi/lib/model/release_event_v1.dart new file mode 100644 index 0000000000..f26ae3e96e --- /dev/null +++ b/mobile/openapi/lib/model/release_event_v1.dart @@ -0,0 +1,133 @@ +// +// 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 ReleaseEventV1 { + /// Returns a new [ReleaseEventV1] instance. + ReleaseEventV1({ + required this.checkedAt, + required this.isAvailable, + required this.releaseVersion, + required this.serverVersion, + required this.type, + }); + + /// When the server last checked for a latest version. As an ISO timestamp + String checkedAt; + + /// Whether a new version is available + bool isAvailable; + + ServerVersionResponseDto releaseVersion; + + ServerVersionResponseDto serverVersion; + + ReleaseType type; + + @override + bool operator ==(Object other) => identical(this, other) || other is ReleaseEventV1 && + other.checkedAt == checkedAt && + other.isAvailable == isAvailable && + other.releaseVersion == releaseVersion && + other.serverVersion == serverVersion && + other.type == type; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (checkedAt.hashCode) + + (isAvailable.hashCode) + + (releaseVersion.hashCode) + + (serverVersion.hashCode) + + (type.hashCode); + + @override + String toString() => 'ReleaseEventV1[checkedAt=$checkedAt, isAvailable=$isAvailable, releaseVersion=$releaseVersion, serverVersion=$serverVersion, type=$type]'; + + Map toJson() { + final json = {}; + json[r'checkedAt'] = this.checkedAt; + json[r'isAvailable'] = this.isAvailable; + json[r'releaseVersion'] = this.releaseVersion; + json[r'serverVersion'] = this.serverVersion; + json[r'type'] = this.type; + return json; + } + + /// Returns a new [ReleaseEventV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static ReleaseEventV1? fromJson(dynamic value) { + upgradeDto(value, "ReleaseEventV1"); + if (value is Map) { + final json = value.cast(); + + return ReleaseEventV1( + checkedAt: mapValueOfType(json, r'checkedAt')!, + isAvailable: mapValueOfType(json, r'isAvailable')!, + releaseVersion: ServerVersionResponseDto.fromJson(json[r'releaseVersion'])!, + serverVersion: ServerVersionResponseDto.fromJson(json[r'serverVersion'])!, + type: ReleaseType.fromJson(json[r'type'])!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = ReleaseEventV1.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = ReleaseEventV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of ReleaseEventV1-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = ReleaseEventV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'checkedAt', + 'isAvailable', + 'releaseVersion', + 'serverVersion', + 'type', + }; +} + diff --git a/mobile/openapi/lib/model/release_type.dart b/mobile/openapi/lib/model/release_type.dart new file mode 100644 index 0000000000..2d61072286 --- /dev/null +++ b/mobile/openapi/lib/model/release_type.dart @@ -0,0 +1,100 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class ReleaseType { + /// Instantiate a new enum with the provided [value]. + const ReleaseType._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const major = ReleaseType._(r'major'); + static const premajor = ReleaseType._(r'premajor'); + static const minor = ReleaseType._(r'minor'); + static const preminor = ReleaseType._(r'preminor'); + static const patch_ = ReleaseType._(r'patch'); + static const prepatch = ReleaseType._(r'prepatch'); + static const prerelease = ReleaseType._(r'prerelease'); + + /// List of all possible values in this [enum][ReleaseType]. + static const values = [ + major, + premajor, + minor, + preminor, + patch_, + prepatch, + prerelease, + ]; + + static ReleaseType? fromJson(dynamic value) => ReleaseTypeTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = ReleaseType.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [ReleaseType] to String, +/// and [decode] dynamic data back to [ReleaseType]. +class ReleaseTypeTypeTransformer { + factory ReleaseTypeTypeTransformer() => _instance ??= const ReleaseTypeTypeTransformer._(); + + const ReleaseTypeTypeTransformer._(); + + String encode(ReleaseType data) => data.value; + + /// Decodes a [dynamic value][data] to a ReleaseType. + /// + /// 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. + ReleaseType? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'major': return ReleaseType.major; + case r'premajor': return ReleaseType.premajor; + case r'minor': return ReleaseType.minor; + case r'preminor': return ReleaseType.preminor; + case r'patch': return ReleaseType.patch_; + case r'prepatch': return ReleaseType.prepatch; + case r'prerelease': return ReleaseType.prerelease; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [ReleaseTypeTypeTransformer] instance. + static ReleaseTypeTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/server_version_response_dto.dart b/mobile/openapi/lib/model/server_version_response_dto.dart index 60161a7458..eae574f335 100644 --- a/mobile/openapi/lib/model/server_version_response_dto.dart +++ b/mobile/openapi/lib/model/server_version_response_dto.dart @@ -16,47 +16,61 @@ class ServerVersionResponseDto { required this.major, required this.minor, required this.patch_, + required this.prerelease, }); /// Major version number /// - /// Minimum value: -9007199254740991 + /// Minimum value: 0 /// Maximum value: 9007199254740991 int major; /// Minor version number /// - /// Minimum value: -9007199254740991 + /// Minimum value: 0 /// Maximum value: 9007199254740991 int minor; /// Patch version number /// - /// Minimum value: -9007199254740991 + /// Minimum value: 0 /// Maximum value: 9007199254740991 int patch_; + /// Pre-release version number + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 + int? prerelease; + @override bool operator ==(Object other) => identical(this, other) || other is ServerVersionResponseDto && other.major == major && other.minor == minor && - other.patch_ == patch_; + other.patch_ == patch_ && + other.prerelease == prerelease; @override int get hashCode => // ignore: unnecessary_parenthesis (major.hashCode) + (minor.hashCode) + - (patch_.hashCode); + (patch_.hashCode) + + (prerelease == null ? 0 : prerelease!.hashCode); @override - String toString() => 'ServerVersionResponseDto[major=$major, minor=$minor, patch_=$patch_]'; + String toString() => 'ServerVersionResponseDto[major=$major, minor=$minor, patch_=$patch_, prerelease=$prerelease]'; Map toJson() { final json = {}; json[r'major'] = this.major; json[r'minor'] = this.minor; json[r'patch'] = this.patch_; + if (this.prerelease != null) { + json[r'prerelease'] = this.prerelease; + } else { + // json[r'prerelease'] = null; + } return json; } @@ -72,6 +86,7 @@ class ServerVersionResponseDto { major: mapValueOfType(json, r'major')!, minor: mapValueOfType(json, r'minor')!, patch_: mapValueOfType(json, r'patch')!, + prerelease: mapValueOfType(json, r'prerelease'), ); } return null; @@ -122,6 +137,7 @@ class ServerVersionResponseDto { 'major', 'minor', 'patch', + 'prerelease', }; } diff --git a/mobile/openapi/lib/model/system_config_new_version_check_dto.dart b/mobile/openapi/lib/model/system_config_new_version_check_dto.dart index ec2b400dfd..17ae9577e8 100644 --- a/mobile/openapi/lib/model/system_config_new_version_check_dto.dart +++ b/mobile/openapi/lib/model/system_config_new_version_check_dto.dart @@ -13,26 +13,32 @@ part of openapi.api; class SystemConfigNewVersionCheckDto { /// Returns a new [SystemConfigNewVersionCheckDto] instance. SystemConfigNewVersionCheckDto({ + required this.channel, required this.enabled, }); + ReleaseChannel channel; + /// Enabled bool enabled; @override bool operator ==(Object other) => identical(this, other) || other is SystemConfigNewVersionCheckDto && + other.channel == channel && other.enabled == enabled; @override int get hashCode => // ignore: unnecessary_parenthesis + (channel.hashCode) + (enabled.hashCode); @override - String toString() => 'SystemConfigNewVersionCheckDto[enabled=$enabled]'; + String toString() => 'SystemConfigNewVersionCheckDto[channel=$channel, enabled=$enabled]'; Map toJson() { final json = {}; + json[r'channel'] = this.channel; json[r'enabled'] = this.enabled; return json; } @@ -46,6 +52,7 @@ class SystemConfigNewVersionCheckDto { final json = value.cast(); return SystemConfigNewVersionCheckDto( + channel: ReleaseChannel.fromJson(json[r'channel'])!, enabled: mapValueOfType(json, r'enabled')!, ); } @@ -94,6 +101,7 @@ class SystemConfigNewVersionCheckDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { + 'channel', 'enabled', }; } diff --git a/mobile/test/domain/services/sync_stream_service_test.dart b/mobile/test/domain/services/sync_stream_service_test.dart index ef29997e0b..80272d9310 100644 --- a/mobile/test/domain/services/sync_stream_service_test.dart +++ b/mobile/test/domain/services/sync_stream_service_test.dart @@ -116,7 +116,7 @@ void main() { when(() => mockApi.serverInfoApi).thenReturn(mockServerApi); when( () => mockServerApi.getServerVersion(), - ).thenAnswer((_) async => ServerVersionResponseDto(major: 1, minor: 132, patch_: 0)); + ).thenAnswer((_) async => ServerVersionResponseDto(major: 1, minor: 132, patch_: 0, prerelease: null)); when(() => mockSyncStreamRepo.updateUsersV1(any())).thenAnswer(successHandler); when(() => mockSyncStreamRepo.deleteUsersV1(any())).thenAnswer(successHandler); @@ -559,7 +559,7 @@ void main() { await Store.put(StoreKey.syncMigrationStatus, "[]"); when( () => mockServerApi.getServerVersion(), - ).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 4, patch_: 1)); + ).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 4, patch_: 1, prerelease: null)); await sut.sync(); @@ -587,7 +587,7 @@ void main() { await Store.put(StoreKey.syncMigrationStatus, "[]"); when( () => mockServerApi.getServerVersion(), - ).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 5, patch_: 0)); + ).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 5, patch_: 0, prerelease: null)); await sut.sync(); verifyInOrder([ @@ -617,7 +617,7 @@ void main() { when( () => mockServerApi.getServerVersion(), - ).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 4, patch_: 1)); + ).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 4, patch_: 1, prerelease: null)); await sut.sync(); diff --git a/mobile/test/unit/utils/semver_test.dart b/mobile/test/unit/utils/semver_test.dart index 8f1958a879..1e534af593 100644 --- a/mobile/test/unit/utils/semver_test.dart +++ b/mobile/test/unit/utils/semver_test.dart @@ -88,5 +88,71 @@ void main() { expect(version2.minor, 2); expect(version2.patch, 3); }); + + test('Orders later prerelease above earlier prerelease', () { + const rc1 = SemVer(major: 1, minor: 151, patch: 0, prerelease: 1); + const rc2 = SemVer(major: 1, minor: 151, patch: 0, prerelease: 2); + expect(rc2 > rc1, isTrue); + expect(rc1 < rc2, isTrue); + expect(rc1 == rc2, isFalse); + }); + + test('Final release outranks its prerelease of the same version', () { + const rc = SemVer(major: 1, minor: 151, patch: 0, prerelease: 1); + const release = SemVer(major: 1, minor: 151, patch: 0); + expect(release > rc, isTrue); + expect(rc < release, isTrue); + }); + + test('Higher major outranks a prerelease regardless of ordinal', () { + const rc = SemVer(major: 1, minor: 151, patch: 0, prerelease: 9); + const next = SemVer(major: 2, minor: 0, patch: 0); + expect(next > rc, isTrue); + }); + + test('Equal prerelease versions compare as equal', () { + const a = SemVer(major: 1, minor: 151, patch: 0, prerelease: 3); + const b = SemVer(major: 1, minor: 151, patch: 0, prerelease: 3); + expect(a == b, isTrue); + expect(a > b, isFalse); + expect(a < b, isFalse); + }); + + test('Reports prerelease difference type', () { + const rc1 = SemVer(major: 1, minor: 151, patch: 0, prerelease: 1); + const rc2 = SemVer(major: 1, minor: 151, patch: 0, prerelease: 2); + expect(rc1.differenceType(rc2), SemVerType.prerelease); + }); + + test('toString includes prerelease suffix when present', () { + const rc = SemVer(major: 1, minor: 151, patch: 0, prerelease: 2); + expect(rc.toString(), '1.151.0-rc.2'); + }); + + test('Parses prerelease ordinal from -rc strings', () { + final dotted = SemVer.fromString('1.151.0-rc.2'); + expect(dotted.major, 1); + expect(dotted.minor, 151); + expect(dotted.patch, 0); + expect(dotted.prerelease, 2); + + expect(SemVer.fromString('v1.151.0-rc.3').prerelease, 3); + expect(SemVer.fromString('1.2.3-rc.2+build.5').prerelease, 2); + }); + + test('Plain version string has null prerelease', () { + expect(SemVer.fromString('3.0.0').prerelease, isNull); + }); + + test('Invalid rc suffixes parse without error and have null prerelease', () { + final debug = SemVer.fromString('1.2.3-debug'); + expect(debug.major, 1); + expect(debug.minor, 2); + expect(debug.patch, 3); + expect(debug.prerelease, isNull); + + expect(SemVer.fromString('1.2.3+build.5').prerelease, isNull); + expect(SemVer.fromString('1.151.0-rc4').prerelease, isNull); + }); }); } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 5e9b460ada..be83f75546 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -20828,6 +20828,57 @@ ], "type": "string" }, + "ReleaseChannel": { + "description": "Release channel", + "enum": [ + "stable", + "releaseCandidate" + ], + "type": "string" + }, + "ReleaseEventV1": { + "properties": { + "checkedAt": { + "description": "When the server last checked for a latest version. As an ISO timestamp", + "type": "string" + }, + "isAvailable": { + "description": "Whether a new version is available", + "type": "boolean" + }, + "releaseVersion": { + "$ref": "#/components/schemas/ServerVersionResponseDto" + }, + "serverVersion": { + "$ref": "#/components/schemas/ServerVersionResponseDto" + }, + "type": { + "$ref": "#/components/schemas/ReleaseType", + "description": "Release type", + "nullable": true + } + }, + "required": [ + "checkedAt", + "isAvailable", + "releaseVersion", + "serverVersion", + "type" + ], + "type": "object" + }, + "ReleaseType": { + "enum": [ + "major", + "premajor", + "minor", + "preminor", + "patch", + "prepatch", + "prerelease" + ], + "type": "string" + }, "ReverseGeocodingStateResponseDto": { "properties": { "lastImportFileName": { @@ -21497,26 +21548,40 @@ "major": { "description": "Major version number", "maximum": 9007199254740991, - "minimum": -9007199254740991, + "minimum": 0, "type": "integer" }, "minor": { "description": "Minor version number", "maximum": 9007199254740991, - "minimum": -9007199254740991, + "minimum": 0, "type": "integer" }, "patch": { "description": "Patch version number", "maximum": 9007199254740991, - "minimum": -9007199254740991, + "minimum": 0, "type": "integer" + }, + "prerelease": { + "description": "Pre-release version number", + "maximum": 9007199254740991, + "minimum": 0, + "nullable": true, + "type": "integer", + "x-immich-history": [ + { + "version": "v3.0.0", + "state": "Added" + } + ] } }, "required": [ "major", "minor", - "patch" + "patch", + "prerelease" ], "type": "object" }, @@ -24537,12 +24602,16 @@ }, "SystemConfigNewVersionCheckDto": { "properties": { + "channel": { + "$ref": "#/components/schemas/ReleaseChannel" + }, "enabled": { "description": "Enabled", "type": "boolean" } }, "required": [ + "channel", "enabled" ], "type": "object" diff --git a/packages/sdk/src/fetch-client.ts b/packages/sdk/src/fetch-client.ts index e9388eb9de..9438968436 100644 --- a/packages/sdk/src/fetch-client.ts +++ b/packages/sdk/src/fetch-client.ts @@ -2076,6 +2076,8 @@ export type ServerVersionResponseDto = { minor: number; /** Patch version number */ patch: number; + /** Pre-release version number */ + prerelease: number | null; }; export type VersionCheckStateResponseDto = { /** Last check timestamp */ @@ -2423,6 +2425,7 @@ export type SystemConfigMetadataDto = { faces: SystemConfigFacesDto; }; export type SystemConfigNewVersionCheckDto = { + channel: ReleaseChannel; /** Enabled */ enabled: boolean; }; @@ -2768,6 +2771,16 @@ export type WorkflowShareResponseDto = { trigger: WorkflowTrigger; }; export type LicenseResponseDto = UserLicense; +export type ReleaseEventV1 = { + /** When the server last checked for a latest version. As an ISO timestamp */ + checkedAt: string; + /** Whether a new version is available */ + isAvailable: boolean; + releaseVersion: ServerVersionResponseDto; + serverVersion: ServerVersionResponseDto; + /** Release type */ + "type": ReleaseType; +}; export type SyncAckV1 = {}; export type SyncAlbumDeleteV1 = { /** Album ID */ @@ -7312,6 +7325,10 @@ export enum LogLevel { Error = "error", Fatal = "fatal" } +export enum ReleaseChannel { + Stable = "stable", + ReleaseCandidate = "releaseCandidate" +} export enum OAuthTokenEndpointAuthMethod { ClientSecretPost = "client_secret_post", ClientSecretBasic = "client_secret_basic" @@ -7320,6 +7337,15 @@ export enum AssetOrderBy { TakenAt = "takenAt", CreatedAt = "createdAt" } +export enum ReleaseType { + Major = "major", + Premajor = "premajor", + Minor = "minor", + Preminor = "preminor", + Patch = "patch", + Prepatch = "prepatch", + Prerelease = "prerelease" +} export enum UserMetadataKey { Preferences = "preferences", License = "license", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1a76031206..329203c36b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -577,8 +577,8 @@ importers: specifier: ^1.6.3 version: 1.6.4 semver: - specifier: ^7.6.2 - version: 7.8.0 + specifier: ^7.8.1 + version: 7.8.1 sharp: specifier: ^0.34.5 version: 0.34.5 @@ -11250,6 +11250,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} + engines: {node: '>=10'} + hasBin: true + send@0.19.2: resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} engines: {node: '>= 0.8.0'} @@ -16307,7 +16312,7 @@ snapshots: nopt: 5.0.0 npmlog: 5.0.1 rimraf: 3.0.2 - semver: 7.8.0 + semver: 7.8.1 tar: 6.2.1 transitivePeerDependencies: - encoding @@ -17765,7 +17770,7 @@ snapshots: '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.29.0 - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 '@types/aria-query': 5.0.4 aria-query: 5.3.0 dom-accessibility-api: 0.5.16 @@ -18469,7 +18474,7 @@ snapshots: '@typescript-eslint/visitor-keys': 8.59.4 debug: 4.4.3 minimatch: 10.2.5 - semver: 7.8.0 + semver: 7.8.1 tinyglobby: 0.2.16 ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 @@ -19569,7 +19574,7 @@ snapshots: dot-prop: 10.1.0 env-paths: 3.0.0 json-schema-typed: 8.0.2 - semver: 7.8.0 + semver: 7.8.1 uint8array-extras: 1.5.0 config-chain@1.1.13: @@ -19741,7 +19746,7 @@ snapshots: postcss-modules-scope: 3.2.1(postcss@8.5.15) postcss-modules-values: 4.0.0(postcss@8.5.15) postcss-value-parser: 4.2.0 - semver: 7.8.0 + semver: 7.8.1 optionalDependencies: webpack: 5.107.0(postcss@8.5.15) @@ -20609,7 +20614,7 @@ snapshots: find-up: 5.0.0 globals: 15.15.0 lodash.memoize: 4.1.2 - semver: 7.8.0 + semver: 7.8.1 eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.4.0(jiti@2.7.0)))(eslint@10.4.0(jiti@2.7.0))(prettier@3.8.3): dependencies: @@ -20632,7 +20637,7 @@ snapshots: postcss: 8.5.15 postcss-load-config: 3.1.4(postcss@8.5.15) postcss-safe-parser: 7.0.1(postcss@8.5.15) - semver: 7.8.0 + semver: 7.8.1 svelte-eslint-parser: 1.6.1(svelte@5.55.8(@typescript-eslint/types@8.59.4)) optionalDependencies: svelte: 5.55.8(@typescript-eslint/types@8.59.4) @@ -21110,7 +21115,7 @@ snapshots: minimatch: 3.1.5 node-abort-controller: 3.1.1 schema-utils: 3.3.0 - semver: 7.8.0 + semver: 7.8.1 tapable: 2.3.3 typescript: 5.9.3 webpack: 5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.22))(esbuild@0.28.0)(lightningcss@1.32.0) @@ -21546,7 +21551,7 @@ snapshots: history@4.10.1: dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 loose-envify: 1.4.0 resolve-pathname: 3.0.0 tiny-invariant: 1.3.3 @@ -22134,7 +22139,7 @@ snapshots: lodash.isstring: 4.0.1 lodash.once: 4.1.1 ms: 2.1.3 - semver: 7.8.0 + semver: 7.8.1 just-compare@2.3.0: {} @@ -22420,7 +22425,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.8.0 + semver: 7.8.1 maplibre-gl@5.24.0: dependencies: @@ -23255,7 +23260,7 @@ snapshots: node-abi@3.92.0: dependencies: - semver: 7.8.0 + semver: 7.8.1 optional: true node-abort-controller@3.1.1: {} @@ -23296,7 +23301,7 @@ snapshots: graceful-fs: 4.2.11 nopt: 9.0.0 proc-log: 6.1.0 - semver: 7.8.0 + semver: 7.8.1 tar: 7.5.15 tinyglobby: 0.2.16 undici: 6.25.0 @@ -23534,7 +23539,7 @@ snapshots: got: 12.6.1 registry-auth-token: 5.1.1 registry-url: 6.0.1 - semver: 7.8.0 + semver: 7.8.1 package-manager-detector@1.6.0: {} @@ -23922,7 +23927,7 @@ snapshots: cosmiconfig: 8.3.6(typescript@6.0.3) jiti: 1.21.7 postcss: 8.5.15 - semver: 7.8.0 + semver: 7.8.1 webpack: 5.107.0(postcss@8.5.15) transitivePeerDependencies: - typescript @@ -24977,12 +24982,14 @@ snapshots: semver-diff@4.0.0: dependencies: - semver: 7.8.0 + semver: 7.8.1 semver@6.3.1: {} semver@7.8.0: {} + semver@7.8.1: {} + send@0.19.2: dependencies: debug: 2.6.9 @@ -25517,7 +25524,7 @@ snapshots: postcss: 8.5.15 postcss-scss: 4.0.9(postcss@8.5.15) postcss-selector-parser: 7.1.1 - semver: 7.8.0 + semver: 7.8.1 optionalDependencies: svelte: 5.55.8(@typescript-eslint/types@8.59.4) @@ -26225,7 +26232,7 @@ snapshots: is-yarn-global: 0.4.1 latest-version: 7.0.0 pupa: 3.3.0 - semver: 7.8.0 + semver: 7.8.1 semver-diff: 4.0.0 xdg-basedir: 5.1.0 diff --git a/server/package.json b/server/package.json index 119a1ea603..957aa548d3 100644 --- a/server/package.json +++ b/server/package.json @@ -106,7 +106,7 @@ "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "sanitize-filename": "^1.6.3", - "semver": "^7.6.2", + "semver": "^7.8.1", "sharp": "^0.34.5", "sirv": "^3.0.0", "socket.io": "^4.8.1", diff --git a/server/src/config.ts b/server/src/config.ts index 999e1e45bc..8f5c9b2579 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -1,4 +1,5 @@ import { CronExpression } from '@nestjs/schedule'; +import { ReleaseChannel } from 'src/dtos/system-config.dto'; import { AudioCodec, Colorspace, @@ -135,6 +136,7 @@ export type SystemConfig = { }; newVersionCheck: { enabled: boolean; + channel: ReleaseChannel; }; nightlyTasks: { startTime: string; @@ -344,6 +346,7 @@ export const defaults = Object.freeze({ }, newVersionCheck: { enabled: true, + channel: ReleaseChannel.Stable, }, nightlyTasks: { startTime: '00:00', diff --git a/server/src/decorators.ts b/server/src/decorators.ts index c8cf1f9221..513ae36c9f 100644 --- a/server/src/decorators.ts +++ b/server/src/decorators.ts @@ -265,3 +265,13 @@ export class HistoryBuilder { return this; } } + +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type +export const extraModels: Function[] = []; + +export const ExtraModel = (): ClassDecorator => { + // eslint-disable-next-line unicorn/consistent-function-scoping, @typescript-eslint/no-unsafe-function-type + return (object: Function) => { + extraModels.push(object); + }; +}; diff --git a/server/src/dtos/server.dto.ts b/server/src/dtos/server.dto.ts index 57a58e1dd7..c770ec12ca 100644 --- a/server/src/dtos/server.dto.ts +++ b/server/src/dtos/server.dto.ts @@ -1,5 +1,6 @@ import { createZodDto } from 'nestjs-zod'; import type { SemVer } from 'semver'; +import { ExtraModel, HistoryBuilder } from 'src/decorators'; import { isoDatetimeToDate } from 'src/validation'; import z from 'zod'; @@ -58,9 +59,15 @@ const ServerStorageResponseSchema = z const ServerVersionResponseSchema = z .object({ - major: z.int().describe('Major version number'), - minor: z.int().describe('Minor version number'), - patch: z.int().describe('Patch version number'), + major: z.int().min(0).describe('Major version number'), + minor: z.int().min(0).describe('Minor version number'), + patch: z.int().min(0).describe('Patch version number'), + prerelease: z + .int() + .min(0) + .nullable() + .meta(HistoryBuilder.v3().getExtensions()) + .describe('Pre-release version number'), }) .meta({ id: 'ServerVersionResponseDto' }); @@ -140,6 +147,26 @@ const ServerFeaturesSchema = z }) .meta({ id: 'ServerFeaturesDto' }); +export enum ReleaseType { + Major = 'major', + Premajor = 'premajor', + Minor = 'minor', + Preminor = 'preminor', + Patch = 'patch', + Prepatch = 'prepatch', + Prerelease = 'prerelease', +} + +const ReleaseTypeSchema = z.enum(ReleaseType).meta({ id: 'ReleaseType' }).describe('Release type'); + +const ReleaseEventV1Schema = z.object({ + isAvailable: z.boolean().describe('Whether a new version is available'), + checkedAt: z.string().describe('When the server last checked for a latest version. As an ISO timestamp'), + serverVersion: ServerVersionResponseSchema, + releaseVersion: ServerVersionResponseSchema, + type: ReleaseTypeSchema.nullable(), +}); + export class ServerPingResponse extends createZodDto(ServerPingResponseSchema) {} export class ServerAboutResponseDto extends createZodDto(ServerAboutResponseSchema) {} export class ServerApkLinksDto extends createZodDto(ServerApkLinksSchema) {} @@ -147,7 +174,12 @@ export class ServerStorageResponseDto extends createZodDto(ServerStorageResponse export class ServerVersionResponseDto extends createZodDto(ServerVersionResponseSchema) { static fromSemVer(value: SemVer): z.infer { - return { major: value.major, minor: value.minor, patch: value.patch }; + return { + major: value.major, + minor: value.minor, + patch: value.patch, + prerelease: (value.prerelease[1] as number) ?? null, + }; } } @@ -158,10 +190,5 @@ export class ServerMediaTypesResponseDto extends createZodDto(ServerMediaTypesRe export class ServerConfigDto extends createZodDto(ServerConfigSchema) {} export class ServerFeaturesDto extends createZodDto(ServerFeaturesSchema) {} -export interface ReleaseNotification { - isAvailable: boolean; - /** ISO8601 */ - checkedAt: string; - serverVersion: ServerVersionResponseDto; - releaseVersion: ServerVersionResponseDto; -} +@ExtraModel() +export class ReleaseEventV1 extends createZodDto(ReleaseEventV1Schema) {} diff --git a/server/src/dtos/sync.dto.ts b/server/src/dtos/sync.dto.ts index 35ef874dfa..698939e627 100644 --- a/server/src/dtos/sync.dto.ts +++ b/server/src/dtos/sync.dto.ts @@ -1,5 +1,5 @@ -/* eslint-disable @typescript-eslint/no-unsafe-function-type */ import { createZodDto } from 'nestjs-zod'; +import { ExtraModel } from 'src/decorators'; import { AssetEditActionSchema } from 'src/dtos/editing.dto'; import { AlbumUserRole, @@ -17,15 +17,6 @@ import { import { isoDatetimeToDate } from 'src/validation'; import z from 'zod'; -export const extraSyncModels: Function[] = []; - -const ExtraModel = (): ClassDecorator => { - // eslint-disable-next-line unicorn/consistent-function-scoping - return (object: Function) => { - extraSyncModels.push(object); - }; -}; - const SyncUserV1Schema = z .object({ id: z.string().describe('User ID'), diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index 94c1aa36b0..d21a0f9dcf 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -151,8 +151,15 @@ const SystemConfigMapSchema = z }) .meta({ id: 'SystemConfigMapDto' }); +export enum ReleaseChannel { + Stable = 'stable', + ReleaseCandidate = 'releaseCandidate', +} + +const ReleaseChannelSchema = z.enum(ReleaseChannel).describe('Release channel').meta({ id: 'ReleaseChannel' }); + const SystemConfigNewVersionCheckSchema = z - .object({ enabled: configBool.describe('Enabled') }) + .object({ enabled: configBool.describe('Enabled'), channel: ReleaseChannelSchema }) .meta({ id: 'SystemConfigNewVersionCheckDto' }); const SystemConfigNightlyTasksSchema = z diff --git a/server/src/repositories/server-info.repository.ts b/server/src/repositories/server-info.repository.ts index 85d26d6cfa..c8fb896281 100644 --- a/server/src/repositories/server-info.repository.ts +++ b/server/src/repositories/server-info.repository.ts @@ -4,6 +4,7 @@ import { exec as execCallback } from 'node:child_process'; import { readFile } from 'node:fs/promises'; import { promisify } from 'node:util'; import sharp from 'sharp'; +import { ReleaseChannel } from 'src/dtos/system-config.dto'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; @@ -64,10 +65,12 @@ export class ServerInfoRepository { this.logger.setContext(ServerInfoRepository.name); } - async getLatestRelease(): Promise { + async getLatestRelease(channel: ReleaseChannel): Promise { try { const { versionCheck } = this.configRepository.getEnv(); - const response = await fetch(versionCheck.url); + const url = new URL(versionCheck.url); + url.searchParams.append('channel', channel); + const response = await fetch(url); if (!response.ok) { throw new Error(`Version check request failed with status ${response.status}: ${await response.text()}`); diff --git a/server/src/repositories/websocket.repository.ts b/server/src/repositories/websocket.repository.ts index b4a0fcc00a..d87d38c40b 100644 --- a/server/src/repositories/websocket.repository.ts +++ b/server/src/repositories/websocket.repository.ts @@ -10,7 +10,7 @@ import { Server, Socket } from 'socket.io'; import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { NotificationDto } from 'src/dtos/notification.dto'; -import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto'; +import { ReleaseEventV1, ServerVersionResponseDto } from 'src/dtos/server.dto'; import { SyncAssetEditV1, SyncAssetExifV1, SyncAssetV2 } from 'src/dtos/sync.dto'; import { AppRestartEvent, ArgsOf, EventRepository } from 'src/repositories/event.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; @@ -31,7 +31,7 @@ export interface ClientEventMap { on_person_thumbnail: [string]; on_server_version: [ServerVersionResponseDto]; on_config_update: []; - on_new_release: [ReleaseNotification]; + on_new_release: [ReleaseEventV1]; on_notification: [NotificationDto]; on_session_delete: [string]; diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index c9a8492b5d..f0d880f50b 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -1,5 +1,6 @@ import { BadRequestException } from '@nestjs/common'; import { defaults, SystemConfig } from 'src/config'; +import { ReleaseChannel } from 'src/dtos/system-config.dto'; import { AudioCodec, Colorspace, @@ -184,6 +185,7 @@ const updatedConfig = Object.freeze({ }, newVersionCheck: { enabled: true, + channel: ReleaseChannel.Stable, }, trash: { enabled: true, diff --git a/server/src/services/version.service.spec.ts b/server/src/services/version.service.spec.ts index 2fbe7292fa..d73edb9850 100644 --- a/server/src/services/version.service.spec.ts +++ b/server/src/services/version.service.spec.ts @@ -2,6 +2,7 @@ import { DateTime } from 'luxon'; import { SemVer } from 'semver'; import { defaults } from 'src/config'; import { serverVersion } from 'src/constants'; +import { ReleaseChannel } from 'src/dtos/system-config.dto'; import { CronJob, JobName, JobStatus, SystemMetadataKey } from 'src/enum'; import { VersionService } from 'src/services/version.service'; import { factory } from 'test/small.factory'; @@ -22,6 +23,17 @@ describe(VersionService.name, () => { mocks.cron.update.mockResolvedValue(); }); + beforeAll(() => { + vitest.mock(import('src/constants.js'), async () => ({ + ...(await vitest.importActual('src/constants.js')), + serverVersion: new SemVer('v3.0.0'), + })); + }); + + afterAll(() => { + vitest.unmock(import('src/constants.js')); + }); + it('should work', () => { expect(sut).toBeDefined(); }); @@ -66,9 +78,10 @@ describe(VersionService.name, () => { describe('getVersion', () => { it('should respond the server version', () => { expect(sut.getVersion()).toEqual({ - major: serverVersion.major, - minor: serverVersion.minor, - patch: serverVersion.patch, + major: 3, + minor: 0, + patch: 0, + prerelease: null, }); }); }); @@ -143,24 +156,24 @@ describe(VersionService.name, () => { describe('onConfigUpdate', () => { it('should queue a version check job when newVersionCheck is enabled', async () => { await sut.onConfigUpdate({ - oldConfig: { ...defaults, newVersionCheck: { enabled: false } }, - newConfig: { ...defaults, newVersionCheck: { enabled: true } }, + oldConfig: { ...defaults, newVersionCheck: { enabled: false, channel: ReleaseChannel.Stable } }, + newConfig: { ...defaults, newVersionCheck: { enabled: true, channel: ReleaseChannel.Stable } }, }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.VersionCheck, data: {} }); }); it('should not queue a version check job when newVersionCheck is disabled', async () => { await sut.onConfigUpdate({ - oldConfig: { ...defaults, newVersionCheck: { enabled: true } }, - newConfig: { ...defaults, newVersionCheck: { enabled: false } }, + oldConfig: { ...defaults, newVersionCheck: { enabled: true, channel: ReleaseChannel.Stable } }, + newConfig: { ...defaults, newVersionCheck: { enabled: false, channel: ReleaseChannel.Stable } }, }); expect(mocks.job.queue).not.toHaveBeenCalled(); }); it('should not queue a version check job when newVersionCheck was already enabled', async () => { await sut.onConfigUpdate({ - oldConfig: { ...defaults, newVersionCheck: { enabled: true } }, - newConfig: { ...defaults, newVersionCheck: { enabled: true } }, + oldConfig: { ...defaults, newVersionCheck: { enabled: true, channel: ReleaseChannel.Stable } }, + newConfig: { ...defaults, newVersionCheck: { enabled: true, channel: ReleaseChannel.Stable } }, }); expect(mocks.job.queue).not.toHaveBeenCalled(); }); @@ -169,21 +182,36 @@ describe(VersionService.name, () => { describe('onWebsocketConnection', () => { it('should send on_server_version client event', async () => { await sut.onWebsocketConnection({ userId: '42' }); - expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', expect.any(SemVer)); + expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', { + major: 3, + minor: 0, + patch: 0, + prerelease: null, + }); expect(mocks.websocket.clientSend).toHaveBeenCalledTimes(1); }); it('should also send a new release notification', async () => { mocks.systemMetadata.get.mockResolvedValue({ checkedAt: '2024-01-01', releaseVersion: 'v1.42.0' }); await sut.onWebsocketConnection({ userId: '42' }); - expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', expect.any(SemVer)); + expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', { + major: 3, + minor: 0, + patch: 0, + prerelease: null, + }); expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_new_release', '42', expect.any(Object)); }); it('should not send a release notification when the version check is disabled', async () => { mocks.systemMetadata.get.mockResolvedValueOnce({ newVersionCheck: { enabled: false } }); await sut.onWebsocketConnection({ userId: '42' }); - expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', expect.any(SemVer)); + expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', { + major: 3, + minor: 0, + patch: 0, + prerelease: null, + }); expect(mocks.websocket.clientSend).not.toHaveBeenCalledWith('on_new_release', '42', expect.any(Object)); }); }); diff --git a/server/src/services/version.service.ts b/server/src/services/version.service.ts index ce6d6d7a6f..37010db5e7 100644 --- a/server/src/services/version.service.ts +++ b/server/src/services/version.service.ts @@ -3,19 +3,27 @@ import { DateTime } from 'luxon'; import semver, { SemVer } from 'semver'; import { serverVersion } from 'src/constants'; import { OnEvent, OnJob } from 'src/decorators'; -import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto'; +import { ReleaseEventV1, ReleaseType, ServerVersionResponseDto } from 'src/dtos/server.dto'; +import { ReleaseChannel } from 'src/dtos/system-config.dto'; import { CronJob, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName, SystemMetadataKey } from 'src/enum'; import { ArgOf } from 'src/repositories/event.repository'; import { BaseService } from 'src/services/base.service'; import { VersionCheckMetadata } from 'src/types'; import { handlePromiseError } from 'src/utils/misc'; -const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): ReleaseNotification => { +const asNotification = ( + channel: ReleaseChannel, + { checkedAt, releaseVersion }: VersionCheckMetadata, +): ReleaseEventV1 => { return { - isAvailable: semver.gt(releaseVersion, serverVersion), + // can't use gt because it's broken for release candidates F https://github.com/npm/node-semver/issues/483 + isAvailable: semver.intersects(`>${serverVersion}`, releaseVersion.toString(), { + includePrerelease: channel === ReleaseChannel.ReleaseCandidate, + }), checkedAt, serverVersion: ServerVersionResponseDto.fromSemVer(serverVersion), releaseVersion: ServerVersionResponseDto.fromSemVer(new SemVer(releaseVersion)), + type: semver.diff(serverVersion, releaseVersion) as ReleaseType, }; }; @@ -98,14 +106,21 @@ export class VersionService extends BaseService { } } - const { version: releaseVersion, published_at: publishedAt } = await this.serverInfoRepository.getLatestRelease(); + const { version: releaseVersion, published_at: publishedAt } = await this.serverInfoRepository.getLatestRelease( + newVersionCheck.channel, + ); const metadata: VersionCheckMetadata = { checkedAt: DateTime.utc().toISO(), releaseVersion }; await this.systemMetadataRepository.set(SystemMetadataKey.VersionCheckState, metadata); - if (semver.gt(releaseVersion, serverVersion)) { + // can't use gt because it's broken for release candidates F https://github.com/npm/node-semver/issues/483 + if ( + semver.intersects(`>${serverVersion}`, releaseVersion.toString(), { + includePrerelease: newVersionCheck.channel === ReleaseChannel.ReleaseCandidate, + }) + ) { this.logger.log(`Found ${releaseVersion}, released at ${new Date(publishedAt).toLocaleString()}`); - this.websocketRepository.clientBroadcast('on_new_release', asNotification(metadata)); + this.websocketRepository.clientBroadcast('on_new_release', asNotification(newVersionCheck.channel, metadata)); } } catch (error: Error | any) { this.logger.warn(`Unable to run version check: ${error}\n${error?.stack}`); @@ -117,7 +132,11 @@ export class VersionService extends BaseService { @OnEvent({ name: 'WebsocketConnect' }) async onWebsocketConnection({ userId }: ArgOf<'WebsocketConnect'>) { - this.websocketRepository.clientSend('on_server_version', userId, serverVersion); + this.websocketRepository.clientSend( + 'on_server_version', + userId, + ServerVersionResponseDto.fromSemVer(serverVersion), + ); const { newVersionCheck } = await this.getConfig({ withCache: true }); if (!newVersionCheck.enabled) { @@ -126,7 +145,7 @@ export class VersionService extends BaseService { const metadata = await this.systemMetadataRepository.get(SystemMetadataKey.VersionCheckState); if (metadata) { - this.websocketRepository.clientSend('on_new_release', userId, asNotification(metadata)); + this.websocketRepository.clientSend('on_new_release', userId, asNotification(newVersionCheck.channel, metadata)); } } } diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts index 37fff07fd9..efcb509941 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -15,7 +15,7 @@ import picomatch from 'picomatch'; import parse from 'picomatch/lib/parse'; import { SystemConfig } from 'src/config'; import { CLIP_MODEL_INFO, endpointTags, serverVersion } from 'src/constants'; -import { extraSyncModels } from 'src/dtos/sync.dto'; +import { extraModels } from 'src/decorators'; import { ApiCustomExtension, ImmichCookie, ImmichHeader, MetadataKey } from 'src/enum'; import { LoggingRepository } from 'src/repositories/logging.repository'; @@ -289,7 +289,7 @@ export const useSwagger = (app: INestApplication, { write }: { write: boolean }) const options: SwaggerDocumentOptions = { operationIdFactory: (controllerKey: string, methodKey: string) => methodKey, - extraModels: extraSyncModels, + extraModels, ignoreGlobalPrefix: true, }; diff --git a/web/src/lib/components/shared-components/side-bar/ServerStatus.svelte b/web/src/lib/components/shared-components/side-bar/ServerStatus.svelte index 47b30e8454..3b77048af9 100644 --- a/web/src/lib/components/shared-components/side-bar/ServerStatus.svelte +++ b/web/src/lib/components/shared-components/side-bar/ServerStatus.svelte @@ -4,12 +4,12 @@ import ServerAboutModal from '$lib/modals/ServerAboutModal.svelte'; import { userInteraction } from '$lib/stores/user.svelte'; import { websocketStore } from '$lib/stores/websocket'; - import type { ReleaseEvent } from '$lib/types'; import { semverToName } from '$lib/utils'; import { requestServerInfo } from '$lib/utils/auth'; import { getAboutInfo, getVersionHistory, + type ReleaseEventV1, type ServerAboutResponseDto, type ServerVersionHistoryResponseDto, } from '@immich/sdk'; @@ -35,11 +35,9 @@ userInteraction.versions = versions; }); let isMain = $derived(info?.sourceRef === 'main' && info.repository === 'immich-app/immich'); - let version = $derived( - $serverVersion ? `v${$serverVersion.major}.${$serverVersion.minor}.${$serverVersion.patch}` : null, - ); + let version = $derived($serverVersion ? semverToName($serverVersion) : null); - const getReleaseInfo = (release?: ReleaseEvent) => { + const getReleaseInfo = (release?: ReleaseEventV1) => { if (!release || !release?.isAvailable || !authManager.user.isAdmin) { return; } diff --git a/web/src/lib/managers/event-manager.svelte.ts b/web/src/lib/managers/event-manager.svelte.ts index 8df3dcfb0e..4a3a560bf9 100644 --- a/web/src/lib/managers/event-manager.svelte.ts +++ b/web/src/lib/managers/event-manager.svelte.ts @@ -7,13 +7,13 @@ import type { LoginResponseDto, PersonResponseDto, QueueResponseDto, + ReleaseEventV1, SharedLinkResponseDto, SystemConfigDto, TagResponseDto, UserAdminResponseDto, WorkflowResponseDto, } from '@immich/sdk'; -import type { ReleaseEvent } from '$lib/types'; import { BaseEventManager } from '$lib/utils/base-event-manager.svelte'; import type { TreeNode } from '$lib/utils/tree-utils'; @@ -86,7 +86,7 @@ export type Events = { WorkflowUpdate: [WorkflowResponseDto]; WorkflowDelete: [WorkflowResponseDto]; - ReleaseEvent: [ReleaseEvent]; + ReleaseEvent: [ReleaseEventV1]; WebsocketConnect: []; }; diff --git a/web/src/lib/managers/release-manager.svelte.ts b/web/src/lib/managers/release-manager.svelte.ts index 15baa6de8f..fed57dd3d8 100644 --- a/web/src/lib/managers/release-manager.svelte.ts +++ b/web/src/lib/managers/release-manager.svelte.ts @@ -1,8 +1,8 @@ +import type { ReleaseEventV1 } from '@immich/sdk'; import { eventManager } from '$lib/managers/event-manager.svelte'; -import { type ReleaseEvent } from '$lib/types'; class ReleaseManager { - value = $state(); + value = $state(); constructor() { eventManager.on({ diff --git a/web/src/lib/stores/websocket.ts b/web/src/lib/stores/websocket.ts index 5765f85a16..fc33812973 100644 --- a/web/src/lib/stores/websocket.ts +++ b/web/src/lib/stores/websocket.ts @@ -3,6 +3,7 @@ import { type AssetResponseDto, type MaintenanceStatusResponseDto, type NotificationDto, + type ReleaseEventV1, type ServerVersionResponseDto, type SyncAssetEditV1, type SyncAssetV2, @@ -15,7 +16,6 @@ import { eventManager } from '$lib/managers/event-manager.svelte'; import { Route } from '$lib/route'; import { maintenanceStore } from '$lib/stores/maintenance.store'; import { notificationManager } from '$lib/stores/notification-manager.svelte'; -import type { ReleaseEvent } from '$lib/types'; import { createEventEmitter } from '$lib/utils/eventemitter'; interface AppRestartEvent { @@ -34,7 +34,7 @@ export interface Events { on_person_thumbnail: (personId: string) => void; on_server_version: (serverVersion: ServerVersionResponseDto) => void; on_config_update: () => void; - on_new_release: (event: ReleaseEvent) => void; + on_new_release: (event: ReleaseEventV1) => void; on_session_delete: (sessionId: string) => void; on_notification: (notification: NotificationDto) => void; diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index ef69a6b08e..41d98df097 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -1,4 +1,4 @@ -import type { QueueResponseDto, ServerVersionResponseDto } from '@immich/sdk'; +import type { QueueResponseDto } from '@immich/sdk'; import type { ActionItem } from '@immich/ui'; import type { DateTime } from 'luxon'; import type { SvelteSet } from 'svelte/reactivity'; @@ -7,14 +7,6 @@ import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; export type LatLng = { lng: number; lat: number }; -export interface ReleaseEvent { - isAvailable: boolean; - /** ISO8601 */ - checkedAt: string; - serverVersion: ServerVersionResponseDto; - releaseVersion: ServerVersionResponseDto; -} - export type QueueSnapshot = { timestamp: number; snapshot?: QueueResponseDto[] }; export type HeaderButtonActionItem = ActionItem & { data?: { title?: string } }; diff --git a/web/src/lib/utils.spec.ts b/web/src/lib/utils.spec.ts index 9ecc7f548e..9fde998dcd 100644 --- a/web/src/lib/utils.spec.ts +++ b/web/src/lib/utils.spec.ts @@ -1,5 +1,5 @@ import { AssetTypeEnum } from '@immich/sdk'; -import { getAssetUrl, getReleaseType } from '$lib/utils'; +import { getAssetUrl } from '$lib/utils'; import { assetFactory } from '@test-data/factories/asset-factory'; import { sharedLinkFactory } from '@test-data/factories/shared-link-factory'; @@ -161,26 +161,4 @@ describe('utils', () => { expect(url).toContain(asset.id); }); }); - - describe(getReleaseType.name, () => { - it('should return "major" for major version changes', () => { - expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 2, minor: 0, patch: 0 })).toBe('major'); - expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 3, minor: 2, patch: 1 })).toBe('major'); - }); - - it('should return "minor" for minor version changes', () => { - expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 1, patch: 0 })).toBe('minor'); - expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 2, patch: 1 })).toBe('minor'); - }); - - it('should return "patch" for patch version changes', () => { - expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 0, patch: 1 })).toBe('patch'); - expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 0, patch: 5 })).toBe('patch'); - }); - - it('should return "none" for matching versions', () => { - expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 0, patch: 0 })).toBe('none'); - expect(getReleaseType({ major: 1, minor: 2, patch: 3 }, { major: 1, minor: 2, patch: 3 })).toBe('none'); - }); - }); }); diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index 397e32e136..4bc3ce76b3 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -411,26 +411,8 @@ export function createDateFormatter(localeCode: string | undefined): DateFormatt }; } -export const getReleaseType = ( - current: ServerVersionResponseDto, - newVersion: ServerVersionResponseDto, -): 'major' | 'minor' | 'patch' | 'none' => { - if (current.major !== newVersion.major) { - return 'major'; - } - - if (current.minor !== newVersion.minor) { - return 'minor'; - } - - if (current.patch !== newVersion.patch) { - return 'patch'; - } - - return 'none'; -}; - -export const semverToName = ({ major, minor, patch }: ServerVersionResponseDto) => `v${major}.${minor}.${patch}`; +export const semverToName = ({ major, minor, patch, prerelease }: ServerVersionResponseDto) => + `v${major}.${minor}.${patch}${prerelease ? `-rc.${prerelease}` : ''}`; export const withoutIcons = (actions: ActionItem[]): ActionItem[] => actions.map((action) => ({ ...action, icon: undefined })); diff --git a/web/src/routes/VersionAnnouncement.svelte b/web/src/routes/VersionAnnouncement.svelte index 2123f2f523..2f793e4d25 100644 --- a/web/src/routes/VersionAnnouncement.svelte +++ b/web/src/routes/VersionAnnouncement.svelte @@ -2,8 +2,8 @@ import OnEvents from '$lib/components/OnEvents.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte'; import VersionAnnouncementModal from '$lib/modals/VersionAnnouncementModal.svelte'; - import type { ReleaseEvent } from '$lib/types'; - import { getReleaseType, semverToName } from '$lib/utils'; + import { semverToName } from '$lib/utils'; + import { ReleaseType, type ReleaseEventV1 } from '@immich/sdk'; import { modalManager } from '@immich/ui'; let modal = $state<{ @@ -11,16 +11,20 @@ close: () => Promise; }>(); - const onReleaseEvent = async (release: ReleaseEvent) => { + const onReleaseEvent = async (release: ReleaseEventV1) => { if (!release.isAvailable || !authManager.user.isAdmin) { return; } const releaseVersion = semverToName(release.releaseVersion); const serverVersion = semverToName(release.serverVersion); - const type = getReleaseType(release.serverVersion, release.releaseVersion); - if (type === 'none' || type === 'patch' || localStorage.getItem('appVersion') === releaseVersion) { + if ( + !release.type || + release.type === ReleaseType.Patch || + release.type === ReleaseType.Prepatch || + localStorage.getItem('appVersion') === releaseVersion + ) { return; } diff --git a/web/src/routes/admin/system-settings/NewVersionCheckSettings.svelte b/web/src/routes/admin/system-settings/NewVersionCheckSettings.svelte index 71d8424e2a..c646601718 100644 --- a/web/src/routes/admin/system-settings/NewVersionCheckSettings.svelte +++ b/web/src/routes/admin/system-settings/NewVersionCheckSettings.svelte @@ -5,8 +5,11 @@ import { systemConfigManager } from '$lib/managers/system-config-manager.svelte'; import { t } from 'svelte-i18n'; import { fade } from 'svelte/transition'; + import SettingSelect from './SettingSelect.svelte'; + import { ReleaseChannel } from '@immich/sdk'; const disabled = $derived(featureFlagsManager.value.configFile); + const config = $derived(systemConfigManager.value); let configToEdit = $state(systemConfigManager.cloneValue()); @@ -20,6 +23,20 @@ bind:checked={configToEdit.newVersionCheck.enabled} {disabled} /> + From 61cd69a286616aa4818adade3617bd747a1c7a07 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Mon, 1 Jun 2026 16:56:43 +0200 Subject: [PATCH 18/50] fix: strip rc suffix from iOS marketing version (#28741) --- mobile/ios/fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index f5f7f7887a..17fa91786e 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -49,7 +49,7 @@ def get_version_from_pubspec pubspec = YAML.load_file(pubspec_path) version_string = pubspec['version'] - version_string ? version_string.split('+').first : nil + version_string ? version_string.split('+').first.split('-').first : nil end # Helper method to configure code signing for all targets From 69b19464849126cab2bdd4d7959b70bda25287ee Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Mon, 1 Jun 2026 17:11:45 +0200 Subject: [PATCH 19/50] feat: handle prereleases in publish workflows (#28701) --- .github/workflows/cli.yml | 6 ++++-- .github/workflows/docker.yml | 4 ++-- .github/workflows/docs-deploy.yml | 12 ++++++++++-- .github/workflows/sdk.yml | 4 +++- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 42adf5c72a..df70e8d151 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -49,7 +49,9 @@ jobs: - name: Publish if: ${{ github.event_name == 'release' }} - run: mise run ci-publish + env: + NPM_TAG: ${{ github.event.release.prerelease && 'rc' || 'latest' }} + run: mise run ci-publish -- --tag "$NPM_TAG" docker: name: Docker @@ -102,7 +104,7 @@ jobs: name=ghcr.io/${{ github.repository_owner }}/immich-cli tags: | type=raw,value=${{ steps.package-version.outputs.version }},enable=${{ github.event_name == 'release' }} - type=raw,value=latest,enable=${{ github.event_name == 'release' }} + type=raw,value=latest,enable=${{ github.event_name == 'release' && !github.event.release.prerelease }} - name: Build and push image uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 8e16894b49..1add8b64d3 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -147,7 +147,7 @@ jobs: platforms: ${{ matrix.platforms }} runner-mapping: ${{ matrix.runner-mapping }} suffixes: ${{ matrix.suffixes }} - dockerhub-push: ${{ github.event_name == 'release' }} + dockerhub-push: ${{ github.event_name == 'release' && !github.event.release.prerelease }} build-args: | DEVICE=${{ matrix.device }} @@ -167,7 +167,7 @@ jobs: image: immich-server context: . dockerfile: server/Dockerfile - dockerhub-push: ${{ github.event_name == 'release' }} + dockerhub-push: ${{ github.event_name == 'release' && !github.event.release.prerelease }} build-args: | DEVICE=cpu diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 3b789e810d..5438242b50 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -98,9 +98,16 @@ jobs: shouldDeploy: true }; } else if (eventType == "release") { + const tag = context.payload.workflow_run.head_branch; + const { data: release } = await github.rest.repos.getReleaseByTag({ + owner: context.repo.owner, + repo: context.repo.repo, + tag, + }); parameters = { event: "release", - name: context.payload.workflow_run.head_branch, + name: tag, + prerelease: release.prerelease, shouldDeploy: !isFork }; } @@ -146,6 +153,7 @@ jobs: const parameters = JSON.parse(process.env.PARAM_JSON); core.setOutput("event", parameters.event); core.setOutput("name", parameters.name); + core.setOutput("prerelease", parameters.prerelease); core.setOutput("shouldDeploy", parameters.shouldDeploy); - name: Download artifact @@ -203,7 +211,7 @@ jobs: run: mise run //docs:deploy - name: Deploy Docs Release Domain - if: ${{ steps.parameters.outputs.event == 'release' }} + if: ${{ steps.parameters.outputs.event == 'release' && steps.parameters.outputs.prerelease != 'true' }} env: TF_VAR_prefix_name: ${{ steps.parameters.outputs.name}} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml index ce632fafb7..bb3b65646c 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -39,4 +39,6 @@ jobs: run: pnpm --filter @immich/sdk build - name: Publish - run: pnpm --filter @immich/sdk publish --provenance --no-git-checks + env: + NPM_TAG: ${{ github.event.release.prerelease && 'rc' || 'latest' }} + run: pnpm --filter @immich/sdk publish --provenance --no-git-checks --tag "$NPM_TAG" From 4eb100327ea5da2e90381b96809f1f1cc51cc7e3 Mon Sep 17 00:00:00 2001 From: Brandon Wees Date: Mon, 1 Jun 2026 12:38:26 -0500 Subject: [PATCH 20/50] fix: disallow cross origin/non http protocols for continueUrl on login (#28706) * fix: disallow cross origin/non http protocols for continueUrl on login * chore: use Route helper * fix: also use Route.continue in pin code prompt * fix: typecheck --- web/src/lib/route.ts | 9 +++++++++ web/src/routes/auth/login/+page.ts | 3 ++- web/src/routes/auth/pin-prompt/+page.svelte | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/web/src/lib/route.ts b/web/src/lib/route.ts index 734be99402..1846f29796 100644 --- a/web/src/lib/route.ts +++ b/web/src/lib/route.ts @@ -152,4 +152,13 @@ export const Route = { // queues queues: () => '/admin/queues', viewQueue: ({ name }: { name: QueueName }) => `/admin/queues/${asQueueSlug(name)}`, + + // continue helper for ensuring same-origin URLs + continue: (url: string | null, fallback: string) => { + if (!url || !url.startsWith('/') || url.startsWith('//')) { + return fallback; + } + + return url; + }, }; diff --git a/web/src/routes/auth/login/+page.ts b/web/src/routes/auth/login/+page.ts index 03a053fcd5..dceb340505 100644 --- a/web/src/routes/auth/login/+page.ts +++ b/web/src/routes/auth/login/+page.ts @@ -8,7 +8,8 @@ import type { PageLoad } from './$types'; export const load = (async ({ parent, url }) => { await parent(); - const continueUrl = url.searchParams.get('continue') || Route.photos(); + const continueUrl = Route.continue(url.searchParams.get('continue'), Route.photos()); + if (authManager.authenticated) { redirect(307, continueUrl); } diff --git a/web/src/routes/auth/pin-prompt/+page.svelte b/web/src/routes/auth/pin-prompt/+page.svelte index d6648889a8..fff02e054f 100644 --- a/web/src/routes/auth/pin-prompt/+page.svelte +++ b/web/src/routes/auth/pin-prompt/+page.svelte @@ -30,7 +30,7 @@ await new Promise((resolve) => setTimeout(resolve, 1000)); - await goto(data.continueUrl); + await goto(Route.continue(data.continueUrl, Route.photos())); } catch (error) { handleError(error, $t('wrong_pin_code')); isBadPinCode = true; From 3f7af51531b2fdd523cf348f6848172a9347de14 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Mon, 1 Jun 2026 19:41:08 +0200 Subject: [PATCH 21/50] fix: version check (#28746) --- web/src/lib/utils.spec.ts | 11 ++++++++++- web/src/lib/utils.ts | 2 +- .../system-settings/NewVersionCheckSettings.svelte | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/web/src/lib/utils.spec.ts b/web/src/lib/utils.spec.ts index 9fde998dcd..992cd803e7 100644 --- a/web/src/lib/utils.spec.ts +++ b/web/src/lib/utils.spec.ts @@ -1,5 +1,5 @@ import { AssetTypeEnum } from '@immich/sdk'; -import { getAssetUrl } from '$lib/utils'; +import { getAssetUrl, semverToName } from '$lib/utils'; import { assetFactory } from '@test-data/factories/asset-factory'; import { sharedLinkFactory } from '@test-data/factories/shared-link-factory'; @@ -161,4 +161,13 @@ describe('utils', () => { expect(url).toContain(asset.id); }); }); + describe('semverToName', () => { + it('should not append release candidate tag if prelease is not set', () => { + expect(semverToName({ major: 3, minor: 0, patch: 0, prerelease: null })).toEqual('v3.0.0'); + }); + + it('should append release candidate if set', () => { + expect(semverToName({ major: 3, minor: 0, patch: 0, prerelease: 0 })).toEqual('v3.0.0-rc.0'); + }); + }); }); diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index 4bc3ce76b3..9af417bb19 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -412,7 +412,7 @@ export function createDateFormatter(localeCode: string | undefined): DateFormatt } export const semverToName = ({ major, minor, patch, prerelease }: ServerVersionResponseDto) => - `v${major}.${minor}.${patch}${prerelease ? `-rc.${prerelease}` : ''}`; + `v${major}.${minor}.${patch}${prerelease === null ? '' : `-rc.${prerelease}`}`; export const withoutIcons = (actions: ActionItem[]): ActionItem[] => actions.map((action) => ({ ...action, icon: undefined })); diff --git a/web/src/routes/admin/system-settings/NewVersionCheckSettings.svelte b/web/src/routes/admin/system-settings/NewVersionCheckSettings.svelte index c646601718..653448e062 100644 --- a/web/src/routes/admin/system-settings/NewVersionCheckSettings.svelte +++ b/web/src/routes/admin/system-settings/NewVersionCheckSettings.svelte @@ -16,7 +16,7 @@
event.preventDefault()}> -
+
Date: Mon, 1 Jun 2026 13:45:24 -0400 Subject: [PATCH 22/50] fix: album name (#28751) --- packages/plugin-core/manifest.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/plugin-core/manifest.json b/packages/plugin-core/manifest.json index 6157005cab..48b4bee2c8 100644 --- a/packages/plugin-core/manifest.json +++ b/packages/plugin-core/manifest.json @@ -232,7 +232,6 @@ "albumName": { "type": "string", "title": "Album name", - "array": true, "description": "Use an album with this name if one exists, otherwise create a new one" } }, From 412884fce3847d862259b45c5247d9abf4f1f805 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 19:47:53 +0200 Subject: [PATCH 23/50] chore(deps): update ghcr.io/jdx/mise docker tag to v2026.5.18 (#28749) --- server/Dockerfile | 2 +- server/Dockerfile.dev | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/Dockerfile b/server/Dockerfile index 3d074ba17f..60363c77b0 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -56,7 +56,7 @@ FROM builder AS plugins ARG TARGETPLATFORM -COPY --from=ghcr.io/jdx/mise:2026.5.11@sha256:2ba959e4827f845fe0c4cfb4814089e790dc513040ef74f9e14925f446412a51 /usr/local/bin/mise /usr/local/bin/mise +COPY --from=ghcr.io/jdx/mise:2026.5.18@sha256:5bb3311994fa78cef307ca3077cdb18f9551da0886371fc26ea91ab56220ffc5 /usr/local/bin/mise /usr/local/bin/mise WORKDIR /app COPY ./mise.toml ./mise.toml diff --git a/server/Dockerfile.dev b/server/Dockerfile.dev index 7979e88dd3..e98f946c1e 100644 --- a/server/Dockerfile.dev +++ b/server/Dockerfile.dev @@ -2,7 +2,7 @@ FROM ghcr.io/immich-app/base-server-dev:202605051129@sha256:d07d8fcdb7e9f3ac22a811e87761ebf341ed0bb91956b89097540c2ed3fb9ca3 AS dev -COPY --from=ghcr.io/jdx/mise:2026.5.11@sha256:2ba959e4827f845fe0c4cfb4814089e790dc513040ef74f9e14925f446412a51 /usr/local/bin/mise /usr/local/bin/mise +COPY --from=ghcr.io/jdx/mise:2026.5.18@sha256:5bb3311994fa78cef307ca3077cdb18f9551da0886371fc26ea91ab56220ffc5 /usr/local/bin/mise /usr/local/bin/mise RUN echo "devdir=/buildcache/node-gyp" >> /usr/local/etc/npmrc && \ echo "store-dir=/buildcache/pnpm-store" >> /usr/local/etc/npmrc && \ From cf4789e0086a9bdc90f388ad9f386b5d3c1efff1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 18:35:36 +0000 Subject: [PATCH 24/50] chore(deps): update github-actions (major) (#28752) --- .github/workflows/docker.yml | 4 ++-- .github/workflows/prepare-release.yml | 2 +- .github/workflows/test.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 1add8b64d3..a76d6fbaa2 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -132,7 +132,7 @@ jobs: suffixes: '-rocm' platforms: linux/amd64 runner-mapping: '{"linux/amd64": "pokedex-large"}' - uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@5813c7c4f7016c748ae7ac5d5f684846649d4d20 # multi-runner-build-workflow-v2.4.0 + uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@db54dcf16fbb12c43479a23749ceea0ad1b4a704 # multi-runner-build-workflow-v3.0.0 permissions: contents: read actions: read @@ -155,7 +155,7 @@ jobs: name: Build and Push Server needs: pre-job if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }} - uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@5813c7c4f7016c748ae7ac5d5f684846649d4d20 # multi-runner-build-workflow-v2.4.0 + uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@db54dcf16fbb12c43479a23749ceea0ad1b4a704 # multi-runner-build-workflow-v3.0.0 permissions: contents: read actions: read diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 4f6a7dc75b..2cb15eaf62 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -137,7 +137,7 @@ jobs: github-token: ${{ steps.generate-token.outputs.token }} - name: Create draft release - uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2 + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 with: draft: true tag_name: ${{ needs.bump_version.outputs.version }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e16e6f059d..918e0771c3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -374,7 +374,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 - name: Setup Node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 @@ -451,7 +451,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 - name: Setup Node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 From 7eabac67021e74cea2724cda644ad3aa548ae44b Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Mon, 1 Jun 2026 14:52:29 -0400 Subject: [PATCH 25/50] feat(server): hls with real-time transcoding (#28230) * hls implementation * fix stale state after ffmpeg exit --- i18n/en.json | 4 + mobile/openapi/README.md | 5 + mobile/openapi/lib/api.dart | 1 + mobile/openapi/lib/api/assets_api.dart | 314 ++++++++++ mobile/openapi/lib/api_client.dart | 2 + mobile/openapi/lib/model/job_name.dart | 3 + .../lib/model/system_config_f_fmpeg_dto.dart | 10 +- .../system_config_f_fmpeg_realtime_dto.dart | 100 ++++ open-api/immich-openapi-specs.json | 362 ++++++++++++ packages/sdk/src/fetch-client.ts | 82 +++ server/src/config.ts | 6 + server/src/constants.ts | 39 +- server/src/controllers/index.ts | 2 + .../controllers/video-stream.controller.ts | 79 +++ server/src/cores/storage.core.ts | 12 + server/src/dtos/streaming.dto.ts | 26 + server/src/dtos/system-config.dto.ts | 5 + server/src/enum.ts | 9 +- .../src/queries/video.stream.repository.sql | 257 ++++++++- server/src/repositories/event.repository.ts | 8 + server/src/repositories/media.repository.ts | 33 +- server/src/repositories/process.repository.ts | 6 +- server/src/repositories/storage.repository.ts | 3 + .../repositories/video-stream.repository.ts | 61 +- .../src/repositories/websocket.repository.ts | 11 +- server/src/schema/enums.ts | 11 +- server/src/services/hls.service.spec.ts | 327 +++++++++++ server/src/services/hls.service.ts | 198 +++++++ server/src/services/index.ts | 4 + server/src/services/queue.service.spec.ts | 1 + server/src/services/queue.service.ts | 1 + .../services/system-config.service.spec.ts | 3 + .../src/services/transcoding.service.spec.ts | 539 ++++++++++++++++++ server/src/services/transcoding.service.ts | 387 +++++++++++++ server/src/types.ts | 30 +- server/src/utils/database.ts | 9 +- server/src/utils/event.ts | 50 ++ server/src/utils/media.ts | 320 ++++++----- server/test/fixtures/media.stub.ts | 2 +- .../repositories/storage.repository.mock.ts | 1 + server/test/utils.ts | 9 +- server/tsconfig.json | 4 +- .../system-settings/FFmpegSettings.svelte | 16 + 43 files changed, 3172 insertions(+), 180 deletions(-) create mode 100644 mobile/openapi/lib/model/system_config_f_fmpeg_realtime_dto.dart create mode 100644 server/src/controllers/video-stream.controller.ts create mode 100644 server/src/dtos/streaming.dto.ts create mode 100644 server/src/services/hls.service.spec.ts create mode 100644 server/src/services/hls.service.ts create mode 100644 server/src/services/transcoding.service.spec.ts create mode 100644 server/src/services/transcoding.service.ts create mode 100644 server/src/utils/event.ts diff --git a/i18n/en.json b/i18n/en.json index 0c3c1aa5e0..035a662545 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -401,6 +401,10 @@ "transcoding_preferred_hardware_device_description": "Applies only to VAAPI and QSV. Sets the dri node used for hardware transcoding.", "transcoding_preset_preset": "Preset (-preset)", "transcoding_preset_preset_description": "Compression speed. Slower presets produce smaller files, and increase quality when targeting a certain bitrate. VP9 ignores speeds above 'faster'.", + "transcoding_realtime": "Real-time Transcoding [EXPERIMENTAL]", + "transcoding_realtime_description": "Allows transcoding to be performed in real-time as the video is being streamed. Enables quality switching, but may cause higher playback latency and stuttering depending on server capabilities.", + "transcoding_realtime_enabled": "Enable real-time transcoding", + "transcoding_realtime_enabled_description": "If disabled, the server will refuse to start new real-time transcoding sessions.", "transcoding_reference_frames": "Reference frames", "transcoding_reference_frames_description": "The number of frames to reference when compressing a given frame. Higher values improve compression efficiency, but slow down encoding. 0 sets this value automatically.", "transcoding_required_description": "Only videos not in an accepted format", diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 2727825df7..e92b885904 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -103,12 +103,16 @@ Class | Method | HTTP request | Description *AssetsApi* | [**deleteBulkAssetMetadata**](doc//AssetsApi.md#deletebulkassetmetadata) | **DELETE** /assets/metadata | Delete asset metadata *AssetsApi* | [**downloadAsset**](doc//AssetsApi.md#downloadasset) | **GET** /assets/{id}/original | Download original asset *AssetsApi* | [**editAsset**](doc//AssetsApi.md#editasset) | **PUT** /assets/{id}/edits | Apply edits to an existing asset +*AssetsApi* | [**endSession**](doc//AssetsApi.md#endsession) | **DELETE** /assets/{id}/video/stream/{sessionId} | End HLS streaming session *AssetsApi* | [**getAssetEdits**](doc//AssetsApi.md#getassetedits) | **GET** /assets/{id}/edits | Retrieve edits for an existing asset *AssetsApi* | [**getAssetInfo**](doc//AssetsApi.md#getassetinfo) | **GET** /assets/{id} | Retrieve an asset *AssetsApi* | [**getAssetMetadata**](doc//AssetsApi.md#getassetmetadata) | **GET** /assets/{id}/metadata | Get asset metadata *AssetsApi* | [**getAssetMetadataByKey**](doc//AssetsApi.md#getassetmetadatabykey) | **GET** /assets/{id}/metadata/{key} | Retrieve asset metadata by key *AssetsApi* | [**getAssetOcr**](doc//AssetsApi.md#getassetocr) | **GET** /assets/{id}/ocr | Retrieve asset OCR data *AssetsApi* | [**getAssetStatistics**](doc//AssetsApi.md#getassetstatistics) | **GET** /assets/statistics | Get asset statistics +*AssetsApi* | [**getMainPlaylist**](doc//AssetsApi.md#getmainplaylist) | **GET** /assets/{id}/video/stream/main.m3u8 | Get HLS main playlist +*AssetsApi* | [**getMediaPlaylist**](doc//AssetsApi.md#getmediaplaylist) | **GET** /assets/{id}/video/stream/{sessionId}/{variantIndex}/playlist.m3u8 | Get HLS media playlist +*AssetsApi* | [**getSegment**](doc//AssetsApi.md#getsegment) | **GET** /assets/{id}/video/stream/{sessionId}/{variantIndex}/{filename} | Get HLS segment or init file *AssetsApi* | [**playAssetVideo**](doc//AssetsApi.md#playassetvideo) | **GET** /assets/{id}/video/playback | Play asset video *AssetsApi* | [**removeAssetEdits**](doc//AssetsApi.md#removeassetedits) | **DELETE** /assets/{id}/edits | Remove edits from an existing asset *AssetsApi* | [**runAssetJobs**](doc//AssetsApi.md#runassetjobs) | **POST** /assets/jobs | Run an asset job @@ -600,6 +604,7 @@ Class | Method | HTTP request | Description - [SystemConfigBackupsDto](doc//SystemConfigBackupsDto.md) - [SystemConfigDto](doc//SystemConfigDto.md) - [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md) + - [SystemConfigFFmpegRealtimeDto](doc//SystemConfigFFmpegRealtimeDto.md) - [SystemConfigFacesDto](doc//SystemConfigFacesDto.md) - [SystemConfigGeneratedFullsizeImageDto](doc//SystemConfigGeneratedFullsizeImageDto.md) - [SystemConfigGeneratedImageDto](doc//SystemConfigGeneratedImageDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index cdba19bcff..3af18e5fe8 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -345,6 +345,7 @@ part 'model/sync_user_v1.dart'; part 'model/system_config_backups_dto.dart'; part 'model/system_config_dto.dart'; part 'model/system_config_f_fmpeg_dto.dart'; +part 'model/system_config_f_fmpeg_realtime_dto.dart'; part 'model/system_config_faces_dto.dart'; part 'model/system_config_generated_fullsize_image_dto.dart'; part 'model/system_config_generated_image_dto.dart'; diff --git a/mobile/openapi/lib/api/assets_api.dart b/mobile/openapi/lib/api/assets_api.dart index bb14238e02..61d3f599cb 100644 --- a/mobile/openapi/lib/api/assets_api.dart +++ b/mobile/openapi/lib/api/assets_api.dart @@ -423,6 +423,76 @@ class AssetsApi { return null; } + /// End HLS streaming session + /// + /// Releases server resources for the streaming session. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [String] sessionId (required): + /// + /// * [String] key: + /// + /// * [String] slug: + Future endSessionWithHttpInfo(String id, String sessionId, { String? key, String? slug, Future? abortTrigger, }) async { + // ignore: prefer_const_declarations + final apiPath = r'/assets/{id}/video/stream/{sessionId}' + .replaceAll('{id}', id) + .replaceAll('{sessionId}', sessionId); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (key != null) { + queryParams.addAll(_queryParams('', 'key', key)); + } + if (slug != null) { + queryParams.addAll(_queryParams('', 'slug', slug)); + } + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, + ); + } + + /// End HLS streaming session + /// + /// Releases server resources for the streaming session. + /// + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [String] sessionId (required): + /// + /// * [String] key: + /// + /// * [String] slug: + Future endSession(String id, String sessionId, { String? key, String? slug, Future? abortTrigger, }) async { + final response = await endSessionWithHttpInfo(id, sessionId, key: key, slug: slug, abortTrigger: abortTrigger,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + /// Retrieve edits for an existing asset /// /// Retrieve a series of edit actions (crop, rotate, mirror) associated with the specified asset. @@ -822,6 +892,250 @@ class AssetsApi { return null; } + /// Get HLS main playlist + /// + /// Returns an HLS main playlist with all available variants for the asset. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [String] key: + /// + /// * [String] slug: + Future getMainPlaylistWithHttpInfo(String id, { String? key, String? slug, Future? abortTrigger, }) async { + // ignore: prefer_const_declarations + final apiPath = r'/assets/{id}/video/stream/main.m3u8' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (key != null) { + queryParams.addAll(_queryParams('', 'key', key)); + } + if (slug != null) { + queryParams.addAll(_queryParams('', 'slug', slug)); + } + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, + ); + } + + /// Get HLS main playlist + /// + /// Returns an HLS main playlist with all available variants for the asset. + /// + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [String] key: + /// + /// * [String] slug: + Future getMainPlaylist(String id, { String? key, String? slug, Future? abortTrigger, }) async { + final response = await getMainPlaylistWithHttpInfo(id, key: key, slug: slug, abortTrigger: abortTrigger,); + 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), 'String',) as String; + + } + return null; + } + + /// Get HLS media playlist + /// + /// Returns an HLS media playlist for one variant of the streaming session. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [String] sessionId (required): + /// + /// * [int] variantIndex (required): + /// + /// * [String] key: + /// + /// * [String] slug: + Future getMediaPlaylistWithHttpInfo(String id, String sessionId, int variantIndex, { String? key, String? slug, Future? abortTrigger, }) async { + // ignore: prefer_const_declarations + final apiPath = r'/assets/{id}/video/stream/{sessionId}/{variantIndex}/playlist.m3u8' + .replaceAll('{id}', id) + .replaceAll('{sessionId}', sessionId) + .replaceAll('{variantIndex}', variantIndex.toString()); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (key != null) { + queryParams.addAll(_queryParams('', 'key', key)); + } + if (slug != null) { + queryParams.addAll(_queryParams('', 'slug', slug)); + } + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, + ); + } + + /// Get HLS media playlist + /// + /// Returns an HLS media playlist for one variant of the streaming session. + /// + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [String] sessionId (required): + /// + /// * [int] variantIndex (required): + /// + /// * [String] key: + /// + /// * [String] slug: + Future getMediaPlaylist(String id, String sessionId, int variantIndex, { String? key, String? slug, Future? abortTrigger, }) async { + final response = await getMediaPlaylistWithHttpInfo(id, sessionId, variantIndex, key: key, slug: slug, abortTrigger: abortTrigger,); + 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), 'String',) as String; + + } + return null; + } + + /// Get HLS segment or init file + /// + /// Streams an HLS init segment (init.mp4) or media segment (seg_N.m4s). + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [String] filename (required): + /// + /// * [String] id (required): + /// + /// * [String] sessionId (required): + /// + /// * [int] variantIndex (required): + /// + /// * [String] key: + /// + /// * [String] slug: + Future getSegmentWithHttpInfo(String filename, String id, String sessionId, int variantIndex, { String? key, String? slug, Future? abortTrigger, }) async { + // ignore: prefer_const_declarations + final apiPath = r'/assets/{id}/video/stream/{sessionId}/{variantIndex}/{filename}' + .replaceAll('{filename}', filename) + .replaceAll('{id}', id) + .replaceAll('{sessionId}', sessionId) + .replaceAll('{variantIndex}', variantIndex.toString()); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (key != null) { + queryParams.addAll(_queryParams('', 'key', key)); + } + if (slug != null) { + queryParams.addAll(_queryParams('', 'slug', slug)); + } + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, + ); + } + + /// Get HLS segment or init file + /// + /// Streams an HLS init segment (init.mp4) or media segment (seg_N.m4s). + /// + /// Parameters: + /// + /// * [String] filename (required): + /// + /// * [String] id (required): + /// + /// * [String] sessionId (required): + /// + /// * [int] variantIndex (required): + /// + /// * [String] key: + /// + /// * [String] slug: + Future getSegment(String filename, String id, String sessionId, int variantIndex, { String? key, String? slug, Future? abortTrigger, }) async { + final response = await getSegmentWithHttpInfo(filename, id, sessionId, variantIndex, key: key, slug: slug, abortTrigger: abortTrigger,); + 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; + } + /// Play asset video /// /// Streams the video file for the specified asset. This endpoint also supports byte range requests. diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 14a29e9681..6efa46571e 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -737,6 +737,8 @@ class ApiClient { return SystemConfigDto.fromJson(value); case 'SystemConfigFFmpegDto': return SystemConfigFFmpegDto.fromJson(value); + case 'SystemConfigFFmpegRealtimeDto': + return SystemConfigFFmpegRealtimeDto.fromJson(value); case 'SystemConfigFacesDto': return SystemConfigFacesDto.fromJson(value); case 'SystemConfigGeneratedFullsizeImageDto': diff --git a/mobile/openapi/lib/model/job_name.dart b/mobile/openapi/lib/model/job_name.dart index 511e1158e9..435cffd623 100644 --- a/mobile/openapi/lib/model/job_name.dart +++ b/mobile/openapi/lib/model/job_name.dart @@ -52,6 +52,7 @@ class JobName { static const librarySyncFilesQueueAll = JobName._(r'LibrarySyncFilesQueueAll'); static const librarySyncFiles = JobName._(r'LibrarySyncFiles'); static const libraryScanQueueAll = JobName._(r'LibraryScanQueueAll'); + static const hlsSessionCleanup = JobName._(r'HlsSessionCleanup'); static const memoryCleanup = JobName._(r'MemoryCleanup'); static const memoryGenerate = JobName._(r'MemoryGenerate'); static const notificationsCleanup = JobName._(r'NotificationsCleanup'); @@ -110,6 +111,7 @@ class JobName { librarySyncFilesQueueAll, librarySyncFiles, libraryScanQueueAll, + hlsSessionCleanup, memoryCleanup, memoryGenerate, notificationsCleanup, @@ -203,6 +205,7 @@ class JobNameTypeTransformer { case r'LibrarySyncFilesQueueAll': return JobName.librarySyncFilesQueueAll; case r'LibrarySyncFiles': return JobName.librarySyncFiles; case r'LibraryScanQueueAll': return JobName.libraryScanQueueAll; + case r'HlsSessionCleanup': return JobName.hlsSessionCleanup; case r'MemoryCleanup': return JobName.memoryCleanup; case r'MemoryGenerate': return JobName.memoryGenerate; case r'NotificationsCleanup': return JobName.notificationsCleanup; diff --git a/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart b/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart index ecf2e5da4a..79da8da97f 100644 --- a/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart +++ b/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart @@ -25,6 +25,7 @@ class SystemConfigFFmpegDto { required this.maxBitrate, required this.preferredHwDevice, required this.preset, + required this.realtime, required this.refs, required this.targetAudioCodec, required this.targetResolution, @@ -79,6 +80,8 @@ class SystemConfigFFmpegDto { /// Preset String preset; + SystemConfigFFmpegRealtimeDto realtime; + /// References /// /// Minimum value: 0 @@ -122,6 +125,7 @@ class SystemConfigFFmpegDto { other.maxBitrate == maxBitrate && other.preferredHwDevice == preferredHwDevice && other.preset == preset && + other.realtime == realtime && other.refs == refs && other.targetAudioCodec == targetAudioCodec && other.targetResolution == targetResolution && @@ -147,6 +151,7 @@ class SystemConfigFFmpegDto { (maxBitrate.hashCode) + (preferredHwDevice.hashCode) + (preset.hashCode) + + (realtime.hashCode) + (refs.hashCode) + (targetAudioCodec.hashCode) + (targetResolution.hashCode) + @@ -158,7 +163,7 @@ class SystemConfigFFmpegDto { (twoPass.hashCode); @override - String toString() => 'SystemConfigFFmpegDto[accel=$accel, accelDecode=$accelDecode, acceptedAudioCodecs=$acceptedAudioCodecs, acceptedContainers=$acceptedContainers, acceptedVideoCodecs=$acceptedVideoCodecs, bframes=$bframes, cqMode=$cqMode, crf=$crf, gopSize=$gopSize, maxBitrate=$maxBitrate, preferredHwDevice=$preferredHwDevice, preset=$preset, refs=$refs, targetAudioCodec=$targetAudioCodec, targetResolution=$targetResolution, targetVideoCodec=$targetVideoCodec, temporalAQ=$temporalAQ, threads=$threads, tonemap=$tonemap, transcode=$transcode, twoPass=$twoPass]'; + String toString() => 'SystemConfigFFmpegDto[accel=$accel, accelDecode=$accelDecode, acceptedAudioCodecs=$acceptedAudioCodecs, acceptedContainers=$acceptedContainers, acceptedVideoCodecs=$acceptedVideoCodecs, bframes=$bframes, cqMode=$cqMode, crf=$crf, gopSize=$gopSize, maxBitrate=$maxBitrate, preferredHwDevice=$preferredHwDevice, preset=$preset, realtime=$realtime, refs=$refs, targetAudioCodec=$targetAudioCodec, targetResolution=$targetResolution, targetVideoCodec=$targetVideoCodec, temporalAQ=$temporalAQ, threads=$threads, tonemap=$tonemap, transcode=$transcode, twoPass=$twoPass]'; Map toJson() { final json = {}; @@ -174,6 +179,7 @@ class SystemConfigFFmpegDto { json[r'maxBitrate'] = this.maxBitrate; json[r'preferredHwDevice'] = this.preferredHwDevice; json[r'preset'] = this.preset; + json[r'realtime'] = this.realtime; json[r'refs'] = this.refs; json[r'targetAudioCodec'] = this.targetAudioCodec; json[r'targetResolution'] = this.targetResolution; @@ -207,6 +213,7 @@ class SystemConfigFFmpegDto { maxBitrate: mapValueOfType(json, r'maxBitrate')!, preferredHwDevice: mapValueOfType(json, r'preferredHwDevice')!, preset: mapValueOfType(json, r'preset')!, + realtime: SystemConfigFFmpegRealtimeDto.fromJson(json[r'realtime'])!, refs: mapValueOfType(json, r'refs')!, targetAudioCodec: AudioCodec.fromJson(json[r'targetAudioCodec'])!, targetResolution: mapValueOfType(json, r'targetResolution')!, @@ -275,6 +282,7 @@ class SystemConfigFFmpegDto { 'maxBitrate', 'preferredHwDevice', 'preset', + 'realtime', 'refs', 'targetAudioCodec', 'targetResolution', diff --git a/mobile/openapi/lib/model/system_config_f_fmpeg_realtime_dto.dart b/mobile/openapi/lib/model/system_config_f_fmpeg_realtime_dto.dart new file mode 100644 index 0000000000..1a8669912e --- /dev/null +++ b/mobile/openapi/lib/model/system_config_f_fmpeg_realtime_dto.dart @@ -0,0 +1,100 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SystemConfigFFmpegRealtimeDto { + /// Returns a new [SystemConfigFFmpegRealtimeDto] instance. + SystemConfigFFmpegRealtimeDto({ + required this.enabled, + }); + + /// Enable real-time HLS transcoding (alpha) + bool enabled; + + @override + bool operator ==(Object other) => identical(this, other) || other is SystemConfigFFmpegRealtimeDto && + other.enabled == enabled; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (enabled.hashCode); + + @override + String toString() => 'SystemConfigFFmpegRealtimeDto[enabled=$enabled]'; + + Map toJson() { + final json = {}; + json[r'enabled'] = this.enabled; + return json; + } + + /// Returns a new [SystemConfigFFmpegRealtimeDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SystemConfigFFmpegRealtimeDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigFFmpegRealtimeDto"); + if (value is Map) { + final json = value.cast(); + + return SystemConfigFFmpegRealtimeDto( + enabled: mapValueOfType(json, r'enabled')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SystemConfigFFmpegRealtimeDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = SystemConfigFFmpegRealtimeDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SystemConfigFFmpegRealtimeDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = SystemConfigFFmpegRealtimeDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'enabled', + }; +} + diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index be83f75546..68d04a665a 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -4308,6 +4308,351 @@ "x-immich-state": "Stable" } }, + "/assets/{id}/video/stream/main.m3u8": { + "get": { + "description": "Returns an HLS main playlist with all available variants for the asset.", + "operationId": "getMainPlaylist", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + }, + { + "name": "key", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "slug", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/vnd.apple.mpegurl": { + "schema": { + "type": "string" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Get HLS main playlist", + "tags": [ + "Assets" + ], + "x-immich-history": [ + { + "version": "v3", + "state": "Added" + }, + { + "version": "v3", + "state": "Alpha" + } + ], + "x-immich-permission": "asset.view", + "x-immich-state": "Alpha" + } + }, + "/assets/{id}/video/stream/{sessionId}": { + "delete": { + "description": "Releases server resources for the streaming session.", + "operationId": "endSession", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + }, + { + "name": "key", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "sessionId", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + }, + { + "name": "slug", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "End HLS streaming session", + "tags": [ + "Assets" + ], + "x-immich-history": [ + { + "version": "v3", + "state": "Added" + }, + { + "version": "v3", + "state": "Alpha" + } + ], + "x-immich-permission": "asset.view", + "x-immich-state": "Alpha" + } + }, + "/assets/{id}/video/stream/{sessionId}/{variantIndex}/playlist.m3u8": { + "get": { + "description": "Returns an HLS media playlist for one variant of the streaming session.", + "operationId": "getMediaPlaylist", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + }, + { + "name": "key", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "sessionId", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + }, + { + "name": "slug", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "variantIndex", + "required": true, + "in": "path", + "schema": { + "minimum": 0, + "maximum": 9007199254740991, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/vnd.apple.mpegurl": { + "schema": { + "type": "string" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Get HLS media playlist", + "tags": [ + "Assets" + ], + "x-immich-history": [ + { + "version": "v3", + "state": "Added" + }, + { + "version": "v3", + "state": "Alpha" + } + ], + "x-immich-permission": "asset.view", + "x-immich-state": "Alpha" + } + }, + "/assets/{id}/video/stream/{sessionId}/{variantIndex}/{filename}": { + "get": { + "description": "Streams an HLS init segment (init.mp4) or media segment (seg_N.m4s).", + "operationId": "getSegment", + "parameters": [ + { + "name": "filename", + "required": true, + "in": "path", + "schema": { + "pattern": "^(init\\.mp4|seg_\\d+\\.m4s)$", + "type": "string" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + }, + { + "name": "key", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "sessionId", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + }, + { + "name": "slug", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "variantIndex", + "required": true, + "in": "path", + "schema": { + "minimum": 0, + "maximum": 9007199254740991, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/octet-stream": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Get HLS segment or init file", + "tags": [ + "Assets" + ], + "x-immich-history": [ + { + "version": "v3", + "state": "Added" + }, + { + "version": "v3", + "state": "Alpha" + } + ], + "x-immich-permission": "asset.view", + "x-immich-state": "Alpha" + } + }, "/auth/admin-sign-up": { "post": { "description": "Create the first admin user in the system.", @@ -18146,6 +18491,7 @@ "LibrarySyncFilesQueueAll", "LibrarySyncFiles", "LibraryScanQueueAll", + "HlsSessionCleanup", "MemoryCleanup", "MemoryGenerate", "NotificationsCleanup", @@ -24242,6 +24588,9 @@ "description": "Preset", "type": "string" }, + "realtime": { + "$ref": "#/components/schemas/SystemConfigFFmpegRealtimeDto" + }, "refs": { "description": "References", "maximum": 6, @@ -24292,6 +24641,7 @@ "maxBitrate", "preferredHwDevice", "preset", + "realtime", "refs", "targetAudioCodec", "targetResolution", @@ -24304,6 +24654,18 @@ ], "type": "object" }, + "SystemConfigFFmpegRealtimeDto": { + "properties": { + "enabled": { + "description": "Enable real-time HLS transcoding (alpha)", + "type": "boolean" + } + }, + "required": [ + "enabled" + ], + "type": "object" + }, "SystemConfigFacesDto": { "properties": { "import": { diff --git a/packages/sdk/src/fetch-client.ts b/packages/sdk/src/fetch-client.ts index 9438968436..85b859ac1d 100644 --- a/packages/sdk/src/fetch-client.ts +++ b/packages/sdk/src/fetch-client.ts @@ -2253,6 +2253,10 @@ export type DatabaseBackupConfig = { export type SystemConfigBackupsDto = { database: DatabaseBackupConfig; }; +export type SystemConfigFFmpegRealtimeDto = { + /** Enable real-time HLS transcoding (alpha) */ + enabled: boolean; +}; export type SystemConfigFFmpegDto = { accel: TranscodeHWAccel; /** Accelerated decode */ @@ -2276,6 +2280,7 @@ export type SystemConfigFFmpegDto = { preferredHwDevice: string; /** Preset */ preset: string; + realtime: SystemConfigFFmpegRealtimeDto; /** References */ refs: number; targetAudioCodec: AudioCodec; @@ -4225,6 +4230,82 @@ export function playAssetVideo({ id, key, slug }: { ...opts })); } +/** + * Get HLS main playlist + */ +export function getMainPlaylist({ id, key, slug }: { + id: string; + key?: string; + slug?: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchBlob<{ + status: 200; + data: string; + }>(`/assets/${encodeURIComponent(id)}/video/stream/main.m3u8${QS.query(QS.explode({ + key, + slug + }))}`, { + ...opts + })); +} +/** + * End HLS streaming session + */ +export function endSession({ id, key, sessionId, slug }: { + id: string; + key?: string; + sessionId: string; + slug?: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/assets/${encodeURIComponent(id)}/video/stream/${encodeURIComponent(sessionId)}${QS.query(QS.explode({ + key, + slug + }))}`, { + ...opts, + method: "DELETE" + })); +} +/** + * Get HLS media playlist + */ +export function getMediaPlaylist({ id, key, sessionId, slug, variantIndex }: { + id: string; + key?: string; + sessionId: string; + slug?: string; + variantIndex: number; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchBlob<{ + status: 200; + data: string; + }>(`/assets/${encodeURIComponent(id)}/video/stream/${encodeURIComponent(sessionId)}/${encodeURIComponent(variantIndex)}/playlist.m3u8${QS.query(QS.explode({ + key, + slug + }))}`, { + ...opts + })); +} +/** + * Get HLS segment or init file + */ +export function getSegment({ filename, id, key, sessionId, slug, variantIndex }: { + filename: string; + id: string; + key?: string; + sessionId: string; + slug?: string; + variantIndex: number; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchBlob<{ + status: 200; + data: Blob; + }>(`/assets/${encodeURIComponent(id)}/video/stream/${encodeURIComponent(sessionId)}/${encodeURIComponent(variantIndex)}/${encodeURIComponent(filename)}${QS.query(QS.explode({ + key, + slug + }))}`, { + ...opts + })); +} /** * Register admin */ @@ -7135,6 +7216,7 @@ export enum JobName { LibrarySyncFilesQueueAll = "LibrarySyncFilesQueueAll", LibrarySyncFiles = "LibrarySyncFiles", LibraryScanQueueAll = "LibraryScanQueueAll", + HlsSessionCleanup = "HlsSessionCleanup", MemoryCleanup = "MemoryCleanup", MemoryGenerate = "MemoryGenerate", NotificationsCleanup = "NotificationsCleanup", diff --git a/server/src/config.ts b/server/src/config.ts index 8f5c9b2579..730663d046 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -46,6 +46,9 @@ export type SystemConfig = { accel: TranscodeHardwareAcceleration; accelDecode: boolean; tonemap: ToneMapping; + realtime: { + enabled: boolean; + }; }; job: Record; logging: { @@ -226,6 +229,9 @@ export const defaults = Object.freeze({ tonemap: ToneMapping.Hable, accel: TranscodeHardwareAcceleration.Disabled, accelDecode: true, + realtime: { + enabled: false, + }, }, job: { [QueueName.BackgroundTask]: { concurrency: 5 }, diff --git a/server/src/constants.ts b/server/src/constants.ts index 9f8cdbefdb..16b7731dce 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -1,7 +1,15 @@ import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { SemVer } from 'semver'; -import { ApiTag, AudioCodec, DatabaseExtension, ExifOrientation, VectorIndex } from 'src/enum'; +import { + ApiTag, + AudioCodec, + DatabaseExtension, + ExifOrientation, + TranscodeHardwareAcceleration, + VectorIndex, + VideoCodec, +} from 'src/enum'; export const IMMICH_SERVER_START = 'Immich Server is listening'; @@ -202,3 +210,32 @@ export const AUDIO_ENCODER: Record = { [AudioCodec.Opus]: 'libopus', [AudioCodec.PcmS16le]: 'pcm_s16le', }; + +export const SUPPORTED_HWA_CODECS: Record = { + [TranscodeHardwareAcceleration.Nvenc]: [VideoCodec.H264, VideoCodec.Hevc, VideoCodec.Av1], + [TranscodeHardwareAcceleration.Qsv]: [VideoCodec.H264, VideoCodec.Hevc, VideoCodec.Vp9, VideoCodec.Av1], + [TranscodeHardwareAcceleration.Vaapi]: [VideoCodec.H264, VideoCodec.Hevc, VideoCodec.Vp9, VideoCodec.Av1], + [TranscodeHardwareAcceleration.Rkmpp]: [VideoCodec.H264, VideoCodec.Hevc], + [TranscodeHardwareAcceleration.Disabled]: [VideoCodec.H264, VideoCodec.Hevc, VideoCodec.Vp9, VideoCodec.Av1], +}; + +export const HLS_BACKPRESSURE_PAUSE_SEGMENTS = 30; +export const HLS_BACKPRESSURE_RESUME_SEGMENTS = 15; +export const HLS_CLEANUP_INTERVAL_MS = 60 * 1000; +export const HLS_INACTIVITY_TIMEOUT_MS = 5 * 60 * 1000; +export const HLS_LEASE_DURATION_MS = 30 * 60 * 1000; +export const HLS_PLAYLIST_CONTENT_TYPE = 'application/vnd.apple.mpegurl'; +export const HLS_SEGMENT_DURATION = 2; +export const HLS_SEGMENT_FILENAME_REGEX = /^seg_(\d+)\.m4s$/; +export const HLS_VARIANTS = [ + { resolution: 480, codec: VideoCodec.Av1, bitrate: 1_000_000, codecString: 'av01.0.04M.08' }, + { resolution: 480, codec: VideoCodec.Hevc, bitrate: 1_200_000, codecString: 'hvc1.1.6.L90.B0' }, + { resolution: 480, codec: VideoCodec.H264, bitrate: 2_500_000, codecString: 'avc1.64001e' }, + { resolution: 720, codec: VideoCodec.Av1, bitrate: 2_000_000, codecString: 'av01.0.08M.08' }, + { resolution: 720, codec: VideoCodec.Hevc, bitrate: 2_500_000, codecString: 'hvc1.1.6.L93.B0' }, + { resolution: 720, codec: VideoCodec.H264, bitrate: 5_000_000, codecString: 'avc1.64001f' }, + { resolution: 1080, codec: VideoCodec.Av1, bitrate: 4_000_000, codecString: 'av01.0.09M.08' }, + { resolution: 1080, codec: VideoCodec.Hevc, bitrate: 4_500_000, codecString: 'hvc1.1.6.L120.B0' }, + { resolution: 1080, codec: VideoCodec.H264, bitrate: 8_000_000, codecString: 'avc1.640028' }, +]; +export const HLS_VERSION = 7; diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts index dc3754ce24..336ea1cf91 100644 --- a/server/src/controllers/index.ts +++ b/server/src/controllers/index.ts @@ -35,6 +35,7 @@ import { TimelineController } from 'src/controllers/timeline.controller'; import { TrashController } from 'src/controllers/trash.controller'; import { UserAdminController } from 'src/controllers/user-admin.controller'; import { UserController } from 'src/controllers/user.controller'; +import { VideoStreamController } from 'src/controllers/video-stream.controller'; import { ViewController } from 'src/controllers/view.controller'; import { WorkflowController } from 'src/controllers/workflow.controller'; @@ -76,6 +77,7 @@ export const controllers = [ TrashController, UserAdminController, UserController, + VideoStreamController, ViewController, WorkflowController, ]; diff --git a/server/src/controllers/video-stream.controller.ts b/server/src/controllers/video-stream.controller.ts new file mode 100644 index 0000000000..8707584361 --- /dev/null +++ b/server/src/controllers/video-stream.controller.ts @@ -0,0 +1,79 @@ +import { Controller, Delete, Get, Header, HttpCode, HttpStatus, Next, Param, Res } from '@nestjs/common'; +import { ApiProduces, ApiTags } from '@nestjs/swagger'; +import { NextFunction, Response } from 'express'; +import { HLS_PLAYLIST_CONTENT_TYPE } from 'src/constants'; +import { Endpoint, HistoryBuilder } from 'src/decorators'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { HlsSegmentParamDto, HlsSessionParamDto, HlsVariantParamDto } from 'src/dtos/streaming.dto'; +import { ApiTag, Permission, RouteKey } from 'src/enum'; +import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { HlsService } from 'src/services/hls.service'; +import { sendFile } from 'src/utils/file'; +import { UUIDParamDto } from 'src/validation'; + +@ApiTags(ApiTag.Assets) +@Controller(RouteKey.Asset) +export class VideoStreamController { + constructor( + private logger: LoggingRepository, + private service: HlsService, + ) {} + + @Get(':id/video/stream/main.m3u8') + @Authenticated({ permission: Permission.AssetView, sharedLink: true }) + @Header('Cache-Control', 'no-cache') + @Header('Content-Type', HLS_PLAYLIST_CONTENT_TYPE) + @ApiProduces(HLS_PLAYLIST_CONTENT_TYPE) + @Endpoint({ + summary: 'Get HLS main playlist', + description: 'Returns an HLS main playlist with all available variants for the asset.', + history: new HistoryBuilder().added('v3').alpha('v3'), + }) + getMainPlaylist(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) { + return this.service.getMainPlaylist(auth, id); + } + + @Get(':id/video/stream/:sessionId/:variantIndex/playlist.m3u8') + @Authenticated({ permission: Permission.AssetView, sharedLink: true }) + @Header('Cache-Control', 'no-cache') + @Header('Content-Type', HLS_PLAYLIST_CONTENT_TYPE) + @ApiProduces(HLS_PLAYLIST_CONTENT_TYPE) + @Endpoint({ + summary: 'Get HLS media playlist', + description: 'Returns an HLS media playlist for one variant of the streaming session.', + history: new HistoryBuilder().added('v3').alpha('v3'), + }) + getMediaPlaylist(@Auth() auth: AuthDto, @Param() { id, sessionId }: HlsVariantParamDto) { + return this.service.getMediaPlaylist(auth, id, sessionId); + } + + @Get(':id/video/stream/:sessionId/:variantIndex/:filename') + @FileResponse() + @Authenticated({ permission: Permission.AssetView, sharedLink: true }) + @Endpoint({ + summary: 'Get HLS segment or init file', + description: 'Streams an HLS init segment (init.mp4) or media segment (seg_N.m4s).', + history: new HistoryBuilder().added('v3').alpha('v3'), + }) + async getSegment( + @Auth() auth: AuthDto, + @Param() { id, sessionId, variantIndex, filename }: HlsSegmentParamDto, + @Res() res: Response, + @Next() next: NextFunction, + ) { + await sendFile(res, next, () => this.service.getSegment(auth, id, sessionId, variantIndex, filename), this.logger); + } + + @Delete(':id/video/stream/:sessionId') + @HttpCode(HttpStatus.NO_CONTENT) + @Authenticated({ permission: Permission.AssetView, sharedLink: true }) + @Endpoint({ + summary: 'End HLS streaming session', + description: 'Releases server resources for the streaming session.', + history: new HistoryBuilder().added('v3').alpha('v3'), + }) + async endSession(@Auth() auth: AuthDto, @Param() { id, sessionId }: HlsSessionParamDto) { + await this.service.endSession(auth, id, sessionId); + } +} diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts index d40518762d..8802145020 100644 --- a/server/src/cores/storage.core.ts +++ b/server/src/cores/storage.core.ts @@ -35,6 +35,10 @@ export interface MoveRequest { export type ThumbnailPathEntity = { id: string; ownerId: string }; +export type HlsSessionFolder = { ownerId: string; sessionId: string }; + +export type HlsVariantFolder = { ownerId: string; sessionId: string; variantIndex: number }; + export type ImagePathOptions = { fileType: AssetFileType; format: ImageFormat | RawExtractedFormat; isEdited: boolean }; let instance: StorageCore | null; @@ -125,6 +129,14 @@ export class StorageCore { return StorageCore.getNestedPath(StorageFolder.EncodedVideo, asset.ownerId, `${asset.id}.mp4`); } + static getHlsSessionFolder({ ownerId, sessionId }: HlsSessionFolder) { + return StorageCore.getNestedPath(StorageFolder.EncodedVideo, ownerId, sessionId); + } + + static getHlsVariantFolder({ ownerId, sessionId, variantIndex }: HlsVariantFolder) { + return join(StorageCore.getHlsSessionFolder({ ownerId, sessionId }), variantIndex.toString()); + } + static getAndroidMotionPath(asset: ThumbnailPathEntity, uuid: string) { return StorageCore.getNestedPath(StorageFolder.EncodedVideo, asset.ownerId, `${uuid}-MP.mp4`); } diff --git a/server/src/dtos/streaming.dto.ts b/server/src/dtos/streaming.dto.ts new file mode 100644 index 0000000000..5270e45fc2 --- /dev/null +++ b/server/src/dtos/streaming.dto.ts @@ -0,0 +1,26 @@ +import { createZodDto } from 'nestjs-zod'; +import z from 'zod'; + +const HlsSessionParamSchema = z.object({ + id: z.uuidv4(), + sessionId: z.uuidv4(), +}); + +export class HlsSessionParamDto extends createZodDto(HlsSessionParamSchema) {} + +const HlsVariantParamSchema = z.object({ + id: z.uuidv4(), + sessionId: z.uuidv4(), + variantIndex: z.coerce.number().int().min(0), +}); + +export class HlsVariantParamDto extends createZodDto(HlsVariantParamSchema) {} + +const HlsSegmentParamSchema = z.object({ + id: z.uuidv4(), + sessionId: z.uuidv4(), + variantIndex: z.coerce.number().int().min(0), + filename: z.string().regex(/^(init\.mp4|seg_\d+\.m4s)$/, { error: 'Invalid HLS segment filename' }), +}); + +export class HlsSegmentParamDto extends createZodDto(HlsSegmentParamSchema) {} diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index d21a0f9dcf..3b31705918 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -79,6 +79,11 @@ const SystemConfigFFmpegSchema = z accel: TranscodeHardwareAccelerationSchema, accelDecode: configBool.describe('Accelerated decode'), tonemap: ToneMappingSchema, + realtime: z + .object({ + enabled: configBool.describe('Enable real-time HLS transcoding (alpha)'), + }) + .meta({ id: 'SystemConfigFFmpegRealtimeDto' }), }) .meta({ id: 'SystemConfigFFmpegDto' }); diff --git a/server/src/enum.ts b/server/src/enum.ts index 27cab3fb5e..9dee1db313 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -453,11 +453,7 @@ export enum VideoCodec { export const VideoCodecSchema = z.enum(VideoCodec).describe('Target video codec').meta({ id: 'VideoCodec' }); -export enum VideoSegmentCodec { - Av1 = 'av1', - Hevc = 'hevc', - H264 = 'h264', -} +export type VideoSegmentCodec = VideoCodec.Av1 | VideoCodec.Hevc | VideoCodec.H264; export enum AudioCodec { Mp3 = 'mp3', @@ -827,6 +823,8 @@ export enum JobName { LibrarySyncFiles = 'LibrarySyncFiles', LibraryScanQueueAll = 'LibraryScanQueueAll', + HlsSessionCleanup = 'HlsSessionCleanup', + MemoryCleanup = 'MemoryCleanup', MemoryGenerate = 'MemoryGenerate', @@ -920,6 +918,7 @@ export enum DatabaseLock { MaintenanceOperation = 621, MemoryCreation = 777, VersionCheck = 800, + HlsSessionCleanup = 850, } export enum MaintenanceAction { diff --git a/server/src/queries/video.stream.repository.sql b/server/src/queries/video.stream.repository.sql index c77882d77d..714e138ce8 100644 --- a/server/src/queries/video.stream.repository.sql +++ b/server/src/queries/video.stream.repository.sql @@ -7,6 +7,7 @@ from "video_stream_session" where "id" = $1 + and "expiresAt" > $2 -- VideoStreamRepository.getVariant select @@ -27,11 +28,13 @@ where -- VideoStreamRepository.getExpiredSessions select - "id" + "video_stream_session"."id", + "asset"."ownerId" from "video_stream_session" + inner join "asset" on "asset"."id" = "video_stream_session"."assetId" where - "expiresAt" <= $1 + "video_stream_session"."expiresAt" <= $1 -- VideoStreamRepository.extendSession update "video_stream_session" @@ -44,3 +47,253 @@ where delete from "video_stream_session" where "id" = $1 + +-- VideoStreamRepository.getForMainPlaylist +select + ( + select + to_json(obj) + from + ( + select + "asset_video"."index", + "asset_video"."codecName", + "asset_video"."profile", + "asset_video"."level", + "asset_video"."bitrate", + "asset_exif"."exifImageWidth" as "width", + "asset_exif"."exifImageHeight" as "height", + "asset_video"."pixelFormat", + "asset_video"."frameCount", + "asset_exif"."fps" as "frameRate", + "asset_video"."timeBase", + case + when "asset_exif"."orientation" = '6' then -90 + when "asset_exif"."orientation" = '8' then 90 + when "asset_exif"."orientation" = '3' then 180 + else 0 + end as "rotation", + "asset_video"."colorPrimaries", + "asset_video"."colorMatrix", + "asset_video"."colorTransfer", + "asset_video"."dvProfile", + "asset_video"."dvLevel", + "asset_video"."dvBlSignalCompatibilityId" + from + ( + select + 1 + ) as "dummy" + where + "asset_video"."assetId" is not null + ) as obj + ) as "videoStream", + ( + select + to_json(obj) + from + ( + select + "asset_keyframe"."pts" as "keyframePts", + "asset_keyframe"."accDuration" as "keyframeAccDuration", + "asset_keyframe"."ownDuration" as "keyframeOwnDuration", + "asset_keyframe"."totalDuration", + "asset_keyframe"."packetCount", + "asset_keyframe"."outputFrames" + from + ( + select + 1 + ) as "dummy" + where + "asset_keyframe"."assetId" is not null + ) as obj + ) as "packets" +from + "asset" + inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId" + inner join "asset_video" on "asset"."id" = "asset_video"."assetId" + inner join "asset_keyframe" on "asset"."id" = "asset_keyframe"."assetId" +where + "asset"."id" = $1 + +-- VideoStreamRepository.getForMediaPlaylist +select + ( + select + to_json(obj) + from + ( + select + "asset_video"."index", + "asset_video"."codecName", + "asset_video"."profile", + "asset_video"."level", + "asset_video"."bitrate", + "asset_exif"."exifImageWidth" as "width", + "asset_exif"."exifImageHeight" as "height", + "asset_video"."pixelFormat", + "asset_video"."frameCount", + "asset_exif"."fps" as "frameRate", + "asset_video"."timeBase", + case + when "asset_exif"."orientation" = '6' then -90 + when "asset_exif"."orientation" = '8' then 90 + when "asset_exif"."orientation" = '3' then 180 + else 0 + end as "rotation", + "asset_video"."colorPrimaries", + "asset_video"."colorMatrix", + "asset_video"."colorTransfer", + "asset_video"."dvProfile", + "asset_video"."dvLevel", + "asset_video"."dvBlSignalCompatibilityId" + from + ( + select + 1 + ) as "dummy" + where + "asset_video"."assetId" is not null + ) as obj + ) as "videoStream", + ( + select + to_json(obj) + from + ( + select + "asset_keyframe"."pts" as "keyframePts", + "asset_keyframe"."accDuration" as "keyframeAccDuration", + "asset_keyframe"."ownDuration" as "keyframeOwnDuration", + "asset_keyframe"."totalDuration", + "asset_keyframe"."packetCount", + "asset_keyframe"."outputFrames" + from + ( + select + 1 + ) as "dummy" + where + "asset_keyframe"."assetId" is not null + ) as obj + ) as "packets" +from + "asset" + inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId" + inner join "video_stream_session" on "asset"."id" = "video_stream_session"."assetId" + inner join "asset_video" on "asset"."id" = "asset_video"."assetId" + inner join "asset_keyframe" on "asset"."id" = "asset_keyframe"."assetId" +where + "asset"."id" = $1 + and "video_stream_session"."id" = $2 + and "video_stream_session"."expiresAt" > $3 + +-- VideoStreamRepository.getForTranscoding +select + "asset"."originalPath", + ( + select + to_json(obj) + from + ( + select + "asset_audio"."index", + "asset_audio"."codecName", + "asset_audio"."profile", + "asset_audio"."bitrate" + from + ( + select + 1 + ) as "dummy" + where + "asset_audio"."assetId" is not null + ) as obj + ) as "audioStream", + ( + select + to_json(obj) + from + ( + select + "asset_video"."index", + "asset_video"."codecName", + "asset_video"."profile", + "asset_video"."level", + "asset_video"."bitrate", + "asset_exif"."exifImageWidth" as "width", + "asset_exif"."exifImageHeight" as "height", + "asset_video"."pixelFormat", + "asset_video"."frameCount", + "asset_exif"."fps" as "frameRate", + "asset_video"."timeBase", + case + when "asset_exif"."orientation" = '6' then -90 + when "asset_exif"."orientation" = '8' then 90 + when "asset_exif"."orientation" = '3' then 180 + else 0 + end as "rotation", + "asset_video"."colorPrimaries", + "asset_video"."colorMatrix", + "asset_video"."colorTransfer", + "asset_video"."dvProfile", + "asset_video"."dvLevel", + "asset_video"."dvBlSignalCompatibilityId" + from + ( + select + 1 + ) as "dummy" + where + "asset_video"."assetId" is not null + ) as obj + ) as "videoStream", + ( + select + to_json(obj) + from + ( + select + "asset_video"."formatName", + "asset_video"."formatLongName", + "asset"."duration", + "asset_video"."bitrate" + from + ( + select + 1 + ) as "dummy" + where + "asset_video"."assetId" is not null + ) as obj + ) as "format", + ( + select + to_json(obj) + from + ( + select + "asset_keyframe"."pts" as "keyframePts", + "asset_keyframe"."accDuration" as "keyframeAccDuration", + "asset_keyframe"."ownDuration" as "keyframeOwnDuration", + "asset_keyframe"."totalDuration", + "asset_keyframe"."packetCount", + "asset_keyframe"."outputFrames" + from + ( + select + 1 + ) as "dummy" + where + "asset_keyframe"."assetId" is not null + ) as obj + ) as "packets" +from + "asset" + inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId" + left join "asset_audio" on "asset"."id" = "asset_audio"."assetId" + inner join "asset_video" on "asset"."id" = "asset_video"."assetId" + inner join "asset_keyframe" on "asset"."id" = "asset_keyframe"."assetId" +where + "asset"."id" = $1 diff --git a/server/src/repositories/event.repository.ts b/server/src/repositories/event.repository.ts index fa92a6b0b7..b4d968599b 100644 --- a/server/src/repositories/event.repository.ts +++ b/server/src/repositories/event.repository.ts @@ -92,6 +92,14 @@ type EventMap = { AuthChangePassword: [{ userId: string; currentSessionId?: string; invalidateSessions?: boolean }]; + // hls streaming events + HlsSegmentRequest: [{ sessionId: string; assetId: string; variantIndex: number; segmentIndex: number }]; + HlsSegmentResult: [{ sessionId: string; variantIndex: number; segmentIndex: number; error?: string }]; + HlsHeartbeat: [{ sessionId: string; variantIndex?: number; segmentIndex?: number }]; + HlsSessionRequest: [{ sessionId: string; assetId: string; ownerId: string }]; + HlsSessionResult: [{ sessionId: string; error?: string }]; + HlsSessionEnd: [{ sessionId: string }]; + // websocket events WebsocketConnect: [{ userId: string }]; }; diff --git a/server/src/repositories/media.repository.ts b/server/src/repositories/media.repository.ts index fa08ba8701..c2ec95636a 100644 --- a/server/src/repositories/media.repository.ts +++ b/server/src/repositories/media.repository.ts @@ -490,18 +490,43 @@ export class MediaRepository { return this.parseInt(b.bit_rate) - this.parseInt(a.bit_rate); } + /* Ported from https://code.ffmpeg.org/FFmpeg/FFmpeg/src/commit/5c44245878e235ae64fe87fb9877644856d33d1d/fftools/ffmpeg_filter.c + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright (c) FFmpeg authors and contributors โ€” https://ffmpeg.org/ + * Modifications: TS port operating on probe-derived packet metadata rather than decoded AVFrames. */ private cfrOutputFrames(packets: { pts: number; duration: number }[], slotsPerTick: number) { - // Packets may be out of PTS order due to B-frames packets.sort((a, b) => a.pts - b.pts); const firstPts = packets[0].pts; let outputFrames = 0; let nextPts = 0; + const history = [0, 0, 0]; for (const pkt of packets) { - const delta = (pkt.pts - firstPts) * slotsPerTick - nextPts + pkt.duration * slotsPerTick; - const nb = delta < -1.1 ? 0 : delta > 1.1 ? Math.round(delta) : 1; + const syncIpts = (pkt.pts - firstPts) * slotsPerTick; + const duration = pkt.duration * slotsPerTick; + let delta0 = syncIpts - nextPts; + const delta = delta0 + duration; + + if (delta0 < 0 && delta > 0) { + delta0 = 0; + } + + let nb = 1; + let nbPrev = 0; + if (delta < -1.1) { + nb = 0; + } else if (delta > 1.1) { + nb = Math.round(delta); + if (delta0 > 1.1) { + nbPrev = Math.round(delta0 - 0.6); + } + } outputFrames += nb; nextPts += nb; + history[2] = history[1]; + history[1] = history[0]; + history[0] = nbPrev; } - return outputFrames; + const median = history.sort((a, b) => a - b)[1]; + return outputFrames + median; } } diff --git a/server/src/repositories/process.repository.ts b/server/src/repositories/process.repository.ts index 9d8cac1f40..928531408f 100644 --- a/server/src/repositories/process.repository.ts +++ b/server/src/repositories/process.repository.ts @@ -1,12 +1,10 @@ import { Injectable } from '@nestjs/common'; -import { ChildProcessWithoutNullStreams, fork, spawn, SpawnOptionsWithoutStdio } from 'node:child_process'; +import { fork, spawn, SpawnOptionsWithoutStdio } from 'node:child_process'; import { Duplex } from 'node:stream'; @Injectable() export class ProcessRepository { - spawn(command: string, args?: readonly string[], options?: SpawnOptionsWithoutStdio): ChildProcessWithoutNullStreams { - return spawn(command, args, options); - } + spawn = spawn; spawnDuplexStream(command: string, args?: readonly string[], options?: SpawnOptionsWithoutStdio): Duplex { let stdinClosed = false; diff --git a/server/src/repositories/storage.repository.ts b/server/src/repositories/storage.repository.ts index 1d3971fd28..9604372fbe 100644 --- a/server/src/repositories/storage.repository.ts +++ b/server/src/repositories/storage.repository.ts @@ -10,6 +10,7 @@ import { existsSync, mkdirSync, ReadOptionsWithBuffer, + watch, } from 'node:fs'; import fs from 'node:fs/promises'; import path from 'node:path'; @@ -277,6 +278,8 @@ export class StorageRepository { return () => watcher.close(); } + watchDir = watch; // Native fs.watch without chokidar overhead + private asGlob(pathToCrawl: string): string { const escapedPath = escapePath(pathToCrawl).replaceAll('"', '["]').replaceAll("'", "[']").replaceAll('`', '[`]'); const extensions = `*{${mimeTypes.getSupportedFileExtensions().join(',')}}`; diff --git a/server/src/repositories/video-stream.repository.ts b/server/src/repositories/video-stream.repository.ts index e23ee4ca4c..43c5ef80f0 100644 --- a/server/src/repositories/video-stream.repository.ts +++ b/server/src/repositories/video-stream.repository.ts @@ -8,6 +8,7 @@ import { VideoStreamSessionTable, VideoStreamVariantTable, } from 'src/schema/tables/video-stream.table'; +import { withAudioStream, withVideoFormat, withVideoPackets, withVideoStream } from 'src/utils/database'; @Injectable() export class VideoStreamRepository { @@ -27,7 +28,12 @@ export class VideoStreamRepository { @GenerateSql({ params: [DummyValue.UUID] }) getSession(id: string) { - return this.db.selectFrom('video_stream_session').selectAll().where('id', '=', id).executeTakeFirst(); + return this.db + .selectFrom('video_stream_session') + .selectAll() + .where('id', '=', id) + .where('expiresAt', '>', new Date()) + .executeTakeFirst(); } @GenerateSql({ params: [DummyValue.UUID] }) @@ -47,7 +53,12 @@ export class VideoStreamRepository { @GenerateSql() getExpiredSessions() { - return this.db.selectFrom('video_stream_session').select(['id']).where('expiresAt', '<=', new Date()).execute(); + return this.db + .selectFrom('video_stream_session') + .innerJoin('asset', 'asset.id', 'video_stream_session.assetId') + .select(['video_stream_session.id', 'asset.ownerId']) + .where('video_stream_session.expiresAt', '<=', new Date()) + .execute(); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.DATE] }) @@ -59,4 +70,50 @@ export class VideoStreamRepository { async deleteSession(id: string) { await this.db.deleteFrom('video_stream_session').where('id', '=', id).execute(); } + + @GenerateSql({ params: [DummyValue.UUID] }) + async getForMainPlaylist(id: string) { + return this.db + .selectFrom('asset') + .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId') + .where('asset.id', '=', id) + .innerJoin('asset_video', 'asset.id', 'asset_video.assetId') + .innerJoin('asset_keyframe', 'asset.id', 'asset_keyframe.assetId') + .select((eb) => withVideoStream(eb).$notNull().as('videoStream')) + .select((eb) => withVideoPackets(eb).$notNull().as('packets')) + .executeTakeFirst(); + } + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] }) + async getForMediaPlaylist(id: string, sessionId: string) { + return this.db + .selectFrom('asset') + .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId') + .innerJoin('video_stream_session', 'asset.id', 'video_stream_session.assetId') + .where('asset.id', '=', id) + .where('video_stream_session.id', '=', sessionId) + .where('video_stream_session.expiresAt', '>', new Date()) + .innerJoin('asset_video', 'asset.id', 'asset_video.assetId') + .innerJoin('asset_keyframe', 'asset.id', 'asset_keyframe.assetId') + .select((eb) => withVideoStream(eb).$notNull().as('videoStream')) + .select((eb) => withVideoPackets(eb).$notNull().as('packets')) + .executeTakeFirst(); + } + + @GenerateSql({ params: [DummyValue.UUID] }) + async getForTranscoding(id: string) { + return this.db + .selectFrom('asset') + .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId') + .where('asset.id', '=', id) + .leftJoin('asset_audio', 'asset.id', 'asset_audio.assetId') + .innerJoin('asset_video', 'asset.id', 'asset_video.assetId') + .innerJoin('asset_keyframe', 'asset.id', 'asset_keyframe.assetId') + .select('asset.originalPath') + .select((eb) => withAudioStream(eb).as('audioStream')) + .select((eb) => withVideoStream(eb).$notNull().as('videoStream')) + .select((eb) => withVideoFormat(eb).$notNull().as('format')) + .select((eb) => withVideoPackets(eb).$notNull().as('packets')) + .executeTakeFirst(); + } } diff --git a/server/src/repositories/websocket.repository.ts b/server/src/repositories/websocket.repository.ts index d87d38c40b..d79e1563e3 100644 --- a/server/src/repositories/websocket.repository.ts +++ b/server/src/repositories/websocket.repository.ts @@ -16,7 +16,16 @@ import { AppRestartEvent, ArgsOf, EventRepository } from 'src/repositories/event import { LoggingRepository } from 'src/repositories/logging.repository'; import { handlePromiseError } from 'src/utils/misc'; -export const serverEvents = ['ConfigUpdate', 'AppRestart'] as const; +export const serverEvents = [ + 'ConfigUpdate', + 'AppRestart', + 'HlsSegmentRequest', + 'HlsSegmentResult', + 'HlsHeartbeat', + 'HlsSessionRequest', + 'HlsSessionResult', + 'HlsSessionEnd', +] as const; export type ServerEvents = (typeof serverEvents)[number]; export interface ClientEventMap { diff --git a/server/src/schema/enums.ts b/server/src/schema/enums.ts index 73f8133441..ecf559c39d 100644 --- a/server/src/schema/enums.ts +++ b/server/src/schema/enums.ts @@ -1,12 +1,5 @@ import { registerEnum } from '@immich/sql-tools'; -import { - AlbumUserRole, - AssetStatus, - AssetVisibility, - ChecksumAlgorithm, - SourceType, - VideoSegmentCodec, -} from 'src/enum'; +import { AlbumUserRole, AssetStatus, AssetVisibility, ChecksumAlgorithm, SourceType, VideoCodec } from 'src/enum'; export const album_user_role_enum = registerEnum({ name: 'album_user_role_enum', @@ -35,5 +28,5 @@ export const asset_checksum_algorithm_enum = registerEnum({ export const video_stream_variant_codec_enum = registerEnum({ name: 'video_stream_variant_codec_enum', - values: Object.values(VideoSegmentCodec), + values: [VideoCodec.Av1, VideoCodec.Hevc, VideoCodec.H264], }); diff --git a/server/src/services/hls.service.spec.ts b/server/src/services/hls.service.spec.ts new file mode 100644 index 0000000000..ccbba48107 --- /dev/null +++ b/server/src/services/hls.service.spec.ts @@ -0,0 +1,327 @@ +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { TranscodeHardwareAcceleration } from 'src/enum'; +import { HlsService } from 'src/services/hls.service'; +import { eiffelTower, train, waterfall } from 'test/fixtures/media.stub'; +import { factory } from 'test/small.factory'; +import { newTestService, ServiceMocks } from 'test/utils'; + +// EXTINF values come from FFmpeg's playlist to enforce an exact match +const eiffelExpectedMediaPlaylist = `#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:2 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-MAP:URI="init.mp4" +#EXTINF:2.007222, +seg_0.m4s +#EXTINF:2.007222, +seg_1.m4s +#EXTINF:2.007222, +seg_2.m4s +#EXTINF:2.007222, +seg_3.m4s +#EXTINF:2.007222, +seg_4.m4s +#EXTINF:2.007222, +seg_5.m4s +#EXTINF:2.007222, +seg_6.m4s +#EXTINF:2.007222, +seg_7.m4s +#EXTINF:2.007222, +seg_8.m4s +#EXTINF:2.007222, +seg_9.m4s +#EXTINF:2.007222, +seg_10.m4s +#EXTINF:0.281011, +seg_11.m4s +#EXT-X-ENDLIST +`; + +const waterfallExpectedMediaPlaylist = `#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:2 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-MAP:URI="init.mp4" +#EXTINF:2.011405, +seg_0.m4s +#EXTINF:2.011405, +seg_1.m4s +#EXTINF:2.011405, +seg_2.m4s +#EXTINF:2.011405, +seg_3.m4s +#EXTINF:2.011405, +seg_4.m4s +#EXTINF:0.301711, +seg_5.m4s +#EXT-X-ENDLIST +`; + +const trainExpectedMediaPlaylist = `#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:2 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-MAP:URI="init.mp4" +#EXTINF:2.000000, +seg_0.m4s +#EXTINF:2.000000, +seg_1.m4s +#EXTINF:2.000000, +seg_2.m4s +#EXTINF:2.000000, +seg_3.m4s +#EXTINF:2.000000, +seg_4.m4s +#EXTINF:2.000000, +seg_5.m4s +#EXTINF:2.000000, +seg_6.m4s +#EXTINF:2.000000, +seg_7.m4s +#EXTINF:2.000000, +seg_8.m4s +#EXTINF:2.000000, +seg_9.m4s +#EXTINF:1.733333, +seg_10.m4s +#EXT-X-ENDLIST +`; + +const sessionId = '00000000-0000-0000-0000-000000000000'; + +const eiffelExpectedMasterDisabled = `#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-STREAM-INF:BANDWIDTH=1000000,RESOLUTION=480x852,CODECS="av01.0.04M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910 +${sessionId}/0/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=1200000,RESOLUTION=480x852,CODECS="hvc1.1.6.L90.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910 +${sessionId}/1/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=480x852,CODECS="avc1.64001e,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910 +${sessionId}/2/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=2000000,RESOLUTION=720x1280,CODECS="av01.0.08M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910 +${sessionId}/3/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=720x1280,CODECS="hvc1.1.6.L93.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910 +${sessionId}/4/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=720x1280,CODECS="avc1.64001f,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910 +${sessionId}/5/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=4000000,RESOLUTION=1080x1920,CODECS="av01.0.09M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910 +${sessionId}/6/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=4500000,RESOLUTION=1080x1920,CODECS="hvc1.1.6.L120.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910 +${sessionId}/7/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=8000000,RESOLUTION=1080x1920,CODECS="avc1.640028,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910 +${sessionId}/8/playlist.m3u8 +`; + +const eiffelExpectedMasterRkmpp = `#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-STREAM-INF:BANDWIDTH=1200000,RESOLUTION=480x852,CODECS="hvc1.1.6.L90.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910 +${sessionId}/1/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=480x852,CODECS="avc1.64001e,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910 +${sessionId}/2/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=720x1280,CODECS="hvc1.1.6.L93.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910 +${sessionId}/4/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=720x1280,CODECS="avc1.64001f,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910 +${sessionId}/5/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=4500000,RESOLUTION=1080x1920,CODECS="hvc1.1.6.L120.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910 +${sessionId}/7/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=8000000,RESOLUTION=1080x1920,CODECS="avc1.640028,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910 +${sessionId}/8/playlist.m3u8 +`; + +const waterfallExpectedMasterDisabled = `#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-STREAM-INF:BANDWIDTH=1000000,RESOLUTION=480x852,CODECS="av01.0.04M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830 +${sessionId}/0/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=1200000,RESOLUTION=480x852,CODECS="hvc1.1.6.L90.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830 +${sessionId}/1/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=480x852,CODECS="avc1.64001e,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830 +${sessionId}/2/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=2000000,RESOLUTION=720x1280,CODECS="av01.0.08M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830 +${sessionId}/3/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=720x1280,CODECS="hvc1.1.6.L93.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830 +${sessionId}/4/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=720x1280,CODECS="avc1.64001f,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830 +${sessionId}/5/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=4000000,RESOLUTION=1080x1920,CODECS="av01.0.09M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830 +${sessionId}/6/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=4500000,RESOLUTION=1080x1920,CODECS="hvc1.1.6.L120.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830 +${sessionId}/7/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=8000000,RESOLUTION=1080x1920,CODECS="avc1.640028,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830 +${sessionId}/8/playlist.m3u8 +`; + +describe(HlsService.name, () => { + let sut: HlsService; + let mocks: ServiceMocks; + + beforeEach(() => { + ({ sut, mocks } = newTestService(HlsService)); + }); + + describe('getMainPlaylist', () => { + const auth = factory.auth(); + const assetId = 'asset-1'; + + const setup = (asset: typeof eiffelTower | typeof waterfall, accel: TranscodeHardwareAcceleration) => { + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId])); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { realtime: { enabled: true }, accel } }); + mocks.videoStream.getForMainPlaylist.mockResolvedValue(asset); + mocks.crypto.randomUUID.mockReturnValue(sessionId); + mocks.websocket.serverSend.mockImplementation((event, ...rest) => { + if (event === 'HlsSessionRequest') { + const { sessionId: id } = rest[0] as { sessionId: string }; + queueMicrotask(() => sut.onSessionResult({ sessionId: id })); + } + }); + }; + + it('returns main playlist for eiffel-tower (1080p portrait, no acceleration)', async () => { + setup(eiffelTower, TranscodeHardwareAcceleration.Disabled); + await expect(sut.getMainPlaylist(auth, assetId)).resolves.toBe(eiffelExpectedMasterDisabled); + }); + + it('returns main playlist for eiffel-tower with RKMPP (no AV1 variants)', async () => { + setup(eiffelTower, TranscodeHardwareAcceleration.Rkmpp); + await expect(sut.getMainPlaylist(auth, assetId)).resolves.toBe(eiffelExpectedMasterRkmpp); + }); + + it('returns main playlist for waterfall (4K landscape) with no acceleration', async () => { + setup(waterfall, TranscodeHardwareAcceleration.Disabled); + await expect(sut.getMainPlaylist(auth, assetId)).resolves.toBe(waterfallExpectedMasterDisabled); + }); + + it('throws BadRequestException when realtime transcoding is disabled', async () => { + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId])); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { realtime: { enabled: false } } }); + await expect(sut.getMainPlaylist(auth, assetId)).rejects.toBeInstanceOf(BadRequestException); + }); + + it('throws NotFoundException when asset is not yet ready for streaming', async () => { + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId])); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { realtime: { enabled: true } } }); + await expect(sut.getMainPlaylist(auth, assetId)).rejects.toBeInstanceOf(NotFoundException); + }); + }); + + describe('getMediaPlaylist', () => { + const auth = factory.auth(); + const assetId = 'asset-1'; + const fixtures = [ + { data: eiffelTower, playlist: eiffelExpectedMediaPlaylist }, + { data: waterfall, playlist: waterfallExpectedMediaPlaylist }, + { data: train, playlist: trainExpectedMediaPlaylist }, + ]; + + it.each(fixtures)('matches FFmpeg for $data.originalPath', async ({ data, playlist }) => { + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId])); + mocks.videoStream.getForMediaPlaylist.mockResolvedValue(data); + await expect(sut.getMediaPlaylist(auth, assetId, sessionId)).resolves.toBe(playlist); + }); + + it('throws NotFoundException when the session/asset cannot be loaded', async () => { + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId])); + await expect(sut.getMediaPlaylist(auth, assetId, sessionId)).rejects.toBeInstanceOf(NotFoundException); + }); + }); + + describe('getSegment', () => { + const auth = factory.auth(); + const assetId = 'asset-1'; + const variantIndex = 0; + + beforeEach(() => { + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId])); + mocks.videoStream.getSession.mockResolvedValue({ id: sessionId, assetId } as never); + mocks.storage.checkFileExists.mockResolvedValue(true); + }); + + it('emits HlsHeartbeat with segmentIndex 0 for the first init.mp4 request', async () => { + await sut.getSegment(auth, assetId, sessionId, variantIndex, 'init.mp4'); + expect(mocks.websocket.serverSend).toHaveBeenCalledWith('HlsHeartbeat', { + sessionId, + variantIndex, + segmentIndex: 0, + }); + }); + + it('emits HlsHeartbeat with the parsed segment number for seg_K.m4s', async () => { + await sut.getSegment(auth, assetId, sessionId, variantIndex, 'seg_5.m4s'); + expect(mocks.websocket.serverSend).toHaveBeenCalledWith('HlsHeartbeat', { + sessionId, + variantIndex, + segmentIndex: 5, + }); + }); + + it('returns lastRequested + 1 for init.mp4 after a segment has been served', async () => { + await sut.getSegment(auth, assetId, sessionId, variantIndex, 'seg_5.m4s'); + mocks.websocket.serverSend.mockClear(); + await sut.getSegment(auth, assetId, sessionId, variantIndex, 'init.mp4'); + expect(mocks.websocket.serverSend).toHaveBeenCalledWith('HlsHeartbeat', { + sessionId, + variantIndex, + segmentIndex: 6, + }); + }); + + it('updates lastRequested on a backward-seek segment request', async () => { + await sut.getSegment(auth, assetId, sessionId, variantIndex, 'seg_5.m4s'); + await sut.getSegment(auth, assetId, sessionId, variantIndex, 'seg_3.m4s'); + mocks.websocket.serverSend.mockClear(); + await sut.getSegment(auth, assetId, sessionId, variantIndex, 'init.mp4'); + expect(mocks.websocket.serverSend).toHaveBeenCalledWith('HlsHeartbeat', { + sessionId, + variantIndex, + segmentIndex: 4, + }); + }); + + it('tracks segment state per session independently', async () => { + await sut.getSegment(auth, assetId, 'session-a', variantIndex, 'seg_5.m4s'); + await sut.getSegment(auth, assetId, 'session-b', variantIndex, 'seg_2.m4s'); + mocks.websocket.serverSend.mockClear(); + await sut.getSegment(auth, assetId, 'session-a', variantIndex, 'init.mp4'); + await sut.getSegment(auth, assetId, 'session-b', variantIndex, 'init.mp4'); + expect(mocks.websocket.serverSend).toHaveBeenCalledWith('HlsHeartbeat', { + sessionId: 'session-a', + variantIndex, + segmentIndex: 6, + }); + expect(mocks.websocket.serverSend).toHaveBeenCalledWith('HlsHeartbeat', { + sessionId: 'session-b', + variantIndex, + segmentIndex: 3, + }); + }); + + it('rejects pending waiters for the previous variant on variant change', async () => { + mocks.storage.checkFileExists.mockResolvedValueOnce(false); + + const pending = sut.getSegment(auth, assetId, sessionId, 0, 'seg_1.m4s'); + await new Promise((resolve) => setImmediate(resolve)); + await sut.getSegment(auth, assetId, sessionId, 1, 'seg_1.m4s'); + + await expect(pending).rejects.toThrow('Variant changed'); + }); + + it('throws NotFoundException when the session does not exist', async () => { + mocks.videoStream.getSession.mockReset(); + await expect(sut.getSegment(auth, assetId, sessionId, variantIndex, 'init.mp4')).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + }); + + describe('endSession', () => { + it('emits HlsSessionEnd', async () => { + const auth = factory.auth(); + const assetId = 'asset-1'; + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId])); + await sut.endSession(auth, assetId, sessionId); + expect(mocks.websocket.serverSend).toHaveBeenCalledWith('HlsSessionEnd', { sessionId }); + }); + }); +}); diff --git a/server/src/services/hls.service.ts b/server/src/services/hls.service.ts new file mode 100644 index 0000000000..fba8b8e060 --- /dev/null +++ b/server/src/services/hls.service.ts @@ -0,0 +1,198 @@ +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { constants } from 'node:fs'; +import { join } from 'node:path'; +import { + HLS_SEGMENT_DURATION, + HLS_SEGMENT_FILENAME_REGEX, + HLS_VARIANTS, + HLS_VERSION, + SUPPORTED_HWA_CODECS, +} from 'src/constants'; +import { StorageCore } from 'src/cores/storage.core'; +import { OnEvent } from 'src/decorators'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; +import { CacheControl, ImmichWorker, Permission } from 'src/enum'; +import { ArgOf } from 'src/repositories/event.repository'; +import { BaseService } from 'src/services/base.service'; +import { VideoPacketInfo, VideoStreamInfo } from 'src/types'; +import { PendingEvents } from 'src/utils/event'; +import { ImmichFileResponse } from 'src/utils/file'; +import { getOutputSize } from 'src/utils/media'; + +type AssetWithStreamInfo = { videoStream: VideoStreamInfo & { timeBase: number }; packets: VideoPacketInfo }; +type ApiSession = { lastRequestedSegment: number | null; lastVariantIndex: number | null }; + +@Injectable() +export class HlsService extends BaseService { + private pendingSegments = new PendingEvents<'HlsSegmentResult'>({ timeoutMs: 15_000 }); + private pendingSessions = new PendingEvents<'HlsSessionResult'>({ timeoutMs: 5000 }); + private sessions = new Map(); + + @OnEvent({ name: 'HlsSessionResult', server: true, workers: [ImmichWorker.Api] }) + onSessionResult(event: ArgOf<'HlsSessionResult'>) { + this.pendingSessions.complete(event.sessionId, event); + if (event.error) { + this.sessions.delete(event.sessionId); + this.pendingSegments.rejectByPrefix(`${event.sessionId}:`, event.error); + } + } + + @OnEvent({ name: 'HlsSessionEnd', server: true, workers: [ImmichWorker.Api] }) + onSessionEnd({ sessionId }: ArgOf<'HlsSessionEnd'>) { + this.sessions.delete(sessionId); + this.pendingSegments.rejectByPrefix(`${sessionId}:`, 'Session ended'); + } + + @OnEvent({ name: 'HlsSegmentResult', server: true, workers: [ImmichWorker.Api] }) + onSegmentResult(event: ArgOf<'HlsSegmentResult'>) { + this.pendingSegments.complete(this.getSegmentKey(event), event); + } + + async getMainPlaylist(auth: AuthDto, assetId: string) { + await this.requireAccess({ auth, permission: Permission.AssetView, ids: [assetId] }); + const { ffmpeg } = await this.getConfig({ withCache: true }); + if (!ffmpeg.realtime.enabled) { + throw new BadRequestException('Real-time transcoding is not enabled'); + } + + const asset = await this.videoStreamRepository.getForMainPlaylist(assetId); + if (!asset) { + throw new NotFoundException('Asset is not yet ready for streaming'); + } + + // Sharing the sessionId allows only one microservices worker to successfully insert to the session table. + // The microservices worker that creates a session owns the transcoding lifecycle for it. + const sessionId = this.cryptoRepository.randomUUID(); + this.websocketRepository.serverSend('HlsSessionRequest', { sessionId, assetId, ownerId: auth.user.id }); + await this.pendingSessions.wait(sessionId); + this.trackSession(sessionId); + + return this.generateMainPlaylist(sessionId, ffmpeg, asset); + } + + async getMediaPlaylist(auth: AuthDto, assetId: string, sessionId: string) { + await this.requireAccess({ auth, permission: Permission.AssetView, ids: [assetId] }); + + const asset = await this.videoStreamRepository.getForMediaPlaylist(assetId, sessionId); + if (!asset) { + throw new NotFoundException('Asset not found or not yet ready for streaming'); + } + + return this.generateMediaPlaylist(asset); + } + + async getSegment(auth: AuthDto, assetId: string, sessionId: string, variantIndex: number, filename: string) { + await this.requireAccess({ auth, permission: Permission.AssetView, ids: [assetId] }); + + const session = await this.videoStreamRepository.getSession(sessionId); + if (!session) { + throw new NotFoundException('Session not found'); + } + + const variantDir = StorageCore.getHlsVariantFolder({ ownerId: auth.user.id, sessionId, variantIndex }); + const path = join(variantDir, filename); + const response = new ImmichFileResponse({ + path, + contentType: 'video/mp4', + cacheControl: CacheControl.PrivateWithCache, + }); + + const apiSession = this.trackSession(sessionId, variantIndex); + const segmentIndex = this.getSegmentIndex(apiSession, filename); + this.websocketRepository.serverSend('HlsHeartbeat', { sessionId, variantIndex, segmentIndex }); + + if (await this.storageRepository.checkFileExists(path, constants.R_OK)) { + return response; + } + + this.websocketRepository.serverSend('HlsSegmentRequest', { sessionId, assetId, variantIndex, segmentIndex }); + await this.pendingSegments.wait(this.getSegmentKey({ sessionId, variantIndex, segmentIndex })); + + return response; + } + + async endSession(auth: AuthDto, assetId: string, sessionId: string): Promise { + await this.requireAccess({ auth, permission: Permission.AssetView, ids: [assetId] }); + + this.websocketRepository.serverSend('HlsSessionEnd', { sessionId }); + } + + private generateMainPlaylist(sessionId: string, ffmpeg: SystemConfigFFmpegDto, asset: AssetWithStreamInfo) { + const fps = ((asset.packets.packetCount * asset.videoStream.timeBase) / asset.packets.totalDuration).toFixed(3); + const sourceResolution = Math.min(asset.videoStream.height, asset.videoStream.width); + const targetResolution = Math.max(sourceResolution, HLS_VARIANTS[0].resolution); + const lines = ['#EXTM3U', `#EXT-X-VERSION:${HLS_VERSION}`]; + for (let i = 0; i < HLS_VARIANTS.length; i++) { + const { resolution, bitrate, codec, codecString } = HLS_VARIANTS[i]; + if (resolution > targetResolution || !SUPPORTED_HWA_CODECS[ffmpeg.accel].includes(codec)) { + continue; + } + const { width, height } = getOutputSize(asset.videoStream, resolution); + lines.push( + `#EXT-X-STREAM-INF:BANDWIDTH=${bitrate},RESOLUTION=${width}x${height},CODECS="${codecString},mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=${fps}`, + `${sessionId}/${i}/playlist.m3u8`, + ); + } + lines.push(''); + + if (lines.length === 3) { + throw new NotFoundException('No supported variants for this video'); + } + + return lines.join('\n'); + } + + private generateMediaPlaylist({ videoStream, packets }: AssetWithStreamInfo) { + const fps = (packets.packetCount * videoStream.timeBase) / packets.totalDuration; + const framesPerSegment = Math.ceil(HLS_SEGMENT_DURATION * fps); + const fullSegmentDuration = framesPerSegment / fps; + const segmentCount = Math.ceil(packets.outputFrames / framesPerSegment); + const lastSegmentFrames = packets.outputFrames - framesPerSegment * (segmentCount - 1); + const lastSegmentDuration = lastSegmentFrames / fps; + + const lines = [ + '#EXTM3U', + `#EXT-X-VERSION:${HLS_VERSION}`, + `#EXT-X-TARGETDURATION:${HLS_SEGMENT_DURATION}`, + '#EXT-X-MEDIA-SEQUENCE:0', + '#EXT-X-PLAYLIST-TYPE:VOD', + '#EXT-X-MAP:URI="init.mp4"', + ]; + + for (let i = 0; i < segmentCount - 1; i++) { + lines.push(`#EXTINF:${fullSegmentDuration.toFixed(6)},`, `seg_${i}.m4s`); + } + lines.push(`#EXTINF:${lastSegmentDuration.toFixed(6)},`, `seg_${segmentCount - 1}.m4s`, '#EXT-X-ENDLIST', ''); + + return lines.join('\n'); + } + + private getSegmentKey({ sessionId, variantIndex, segmentIndex }: ArgOf<'HlsSegmentResult'>) { + return `${sessionId}:${variantIndex}:${segmentIndex}`; + } + + private getSegmentIndex(session: ApiSession, filename: string) { + if (filename.endsWith('.mp4')) { + return (session.lastRequestedSegment ?? -1) + 1; + } + const segmentIndex = Number.parseInt(HLS_SEGMENT_FILENAME_REGEX.exec(filename)![1]); + session.lastRequestedSegment = segmentIndex; + return segmentIndex; + } + + private trackSession(id: string, variantIndex: number | null = null) { + const session = this.sessions.get(id); + if (!session) { + const newSession = { lastRequestedSegment: null, lastVariantIndex: variantIndex }; + this.sessions.set(id, newSession); + return newSession; + } + + if (session.lastVariantIndex !== null && session.lastVariantIndex !== variantIndex) { + this.pendingSegments.rejectByPrefix(`${id}:${session.lastVariantIndex}:`, 'Variant changed'); + } + session.lastVariantIndex = variantIndex; + return session; + } +} diff --git a/server/src/services/index.ts b/server/src/services/index.ts index b733483aa8..3c23e723bc 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -11,6 +11,7 @@ import { DatabaseBackupService } from 'src/services/database-backup.service'; import { DatabaseService } from 'src/services/database.service'; import { DownloadService } from 'src/services/download.service'; import { DuplicateService } from 'src/services/duplicate.service'; +import { HlsService } from 'src/services/hls.service'; import { JobService } from 'src/services/job.service'; import { LibraryService } from 'src/services/library.service'; import { MaintenanceService } from 'src/services/maintenance.service'; @@ -39,6 +40,7 @@ import { SystemMetadataService } from 'src/services/system-metadata.service'; import { TagService } from 'src/services/tag.service'; import { TelemetryService } from 'src/services/telemetry.service'; import { TimelineService } from 'src/services/timeline.service'; +import { TranscodingService } from 'src/services/transcoding.service'; import { TrashService } from 'src/services/trash.service'; import { UserAdminService } from 'src/services/user-admin.service'; import { UserService } from 'src/services/user.service'; @@ -61,6 +63,7 @@ export const services = [ DatabaseService, DownloadService, DuplicateService, + HlsService, JobService, LibraryService, MaintenanceService, @@ -89,6 +92,7 @@ export const services = [ TagService, TelemetryService, TimelineService, + TranscodingService, TrashService, UserAdminService, UserService, diff --git a/server/src/services/queue.service.spec.ts b/server/src/services/queue.service.spec.ts index d4c425e8bd..48c61c0951 100644 --- a/server/src/services/queue.service.spec.ts +++ b/server/src/services/queue.service.spec.ts @@ -41,6 +41,7 @@ describe(QueueService.name, () => { { name: JobName.PersonCleanup }, { name: JobName.MemoryCleanup }, { name: JobName.SessionCleanup }, + { name: JobName.HlsSessionCleanup }, { name: JobName.AuditTableCleanup }, { name: JobName.MemoryGenerate }, { name: JobName.UserSyncUsage }, diff --git a/server/src/services/queue.service.ts b/server/src/services/queue.service.ts index ba6f4c5f3b..d11c9180b2 100644 --- a/server/src/services/queue.service.ts +++ b/server/src/services/queue.service.ts @@ -269,6 +269,7 @@ export class QueueService extends BaseService { { name: JobName.PersonCleanup }, { name: JobName.MemoryCleanup }, { name: JobName.SessionCleanup }, + { name: JobName.HlsSessionCleanup }, { name: JobName.AuditTableCleanup }, ); } diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index f0d880f50b..2d0850ac58 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -73,6 +73,9 @@ const updatedConfig = Object.freeze({ accel: TranscodeHardwareAcceleration.Disabled, accelDecode: true, tonemap: ToneMapping.Hable, + realtime: { + enabled: false, + }, }, logging: { enabled: true, diff --git a/server/src/services/transcoding.service.spec.ts b/server/src/services/transcoding.service.spec.ts new file mode 100644 index 0000000000..349cba6a7d --- /dev/null +++ b/server/src/services/transcoding.service.spec.ts @@ -0,0 +1,539 @@ +import { + HLS_BACKPRESSURE_PAUSE_SEGMENTS, + HLS_BACKPRESSURE_RESUME_SEGMENTS, + HLS_CLEANUP_INTERVAL_MS, + HLS_INACTIVITY_TIMEOUT_MS, + HLS_LEASE_DURATION_MS, +} from 'src/constants'; +import { TranscodingService } from 'src/services/transcoding.service'; +import { VIDEO_STREAM_SESSION_PK_CONSTRAINT } from 'src/utils/database'; +import { eiffelTower, train, waterfall } from 'test/fixtures/media.stub'; +import { mockSpawn, newTestService, ServiceMocks } from 'test/utils'; +import { vi } from 'vitest'; + +describe(TranscodingService.name, () => { + let sut: TranscodingService; + let mocks: ServiceMocks; + + const sessionId = 'session-1'; + const assetId = 'asset-1'; + const ownerId = 'user-1'; + + const completeSegment = (index: number) => { + const listener = vi.mocked(mocks.storage.watchDir).mock.lastCall?.[1]; + expect(listener).toBeDefined(); + listener!('rename', `seg_${index}.m4s`); + }; + + const completeSegmentsThrough = (start: number, end: number) => { + for (let i = start; i <= end; i++) { + completeSegment(i); + } + }; + + beforeEach(() => { + ({ sut, mocks } = newTestService(TranscodingService)); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { realtime: { enabled: true } } }); + mocks.videoStream.getForTranscoding.mockResolvedValue(eiffelTower); + }); + + describe('onSessionRequest', () => { + it('creates the session row and emits HlsSessionResult on success', async () => { + await sut.onSessionRequest({ sessionId, assetId, ownerId }); + + expect(mocks.videoStream.createSession).toHaveBeenCalledWith({ + id: sessionId, + assetId, + expiresAt: expect.any(Date), + }); + expect(mocks.websocket.serverSend).toHaveBeenCalledWith('HlsSessionResult', { sessionId }); + }); + + it('treats a primary-key conflict as a no-op for replay tolerance', async () => { + mocks.videoStream.createSession.mockRejectedValue({ constraint_name: VIDEO_STREAM_SESSION_PK_CONSTRAINT }); + + await sut.onSessionRequest({ sessionId, assetId, ownerId }); + + expect(mocks.websocket.serverSend).not.toHaveBeenCalled(); + }); + + it('emits HlsSessionResult with an error on other DB failures', async () => { + mocks.videoStream.createSession.mockRejectedValue(new Error('database is down')); + + await sut.onSessionRequest({ sessionId, assetId, ownerId }); + + expect(mocks.websocket.serverSend).toHaveBeenCalledWith('HlsSessionResult', { + sessionId, + error: 'Failed to create HLS session', + }); + }); + }); + + describe('onSessionEnd', () => { + it('removes the session, kills the transcode, and deletes the dir + DB row', async () => { + await sut.onSessionRequest({ sessionId, assetId, ownerId }); + const process = mockSpawn(0, '', ''); + mocks.process.spawn.mockReturnValue(process); + await sut.onSegmentRequest({ sessionId, assetId, variantIndex: 0, segmentIndex: 0 }); + + await sut.onSessionEnd({ sessionId }); + + expect(process.kill).toHaveBeenCalled(); + expect(mocks.storage.unlinkDir).toHaveBeenCalled(); + expect(mocks.videoStream.deleteSession).toHaveBeenCalledWith(sessionId); + }); + + it('is a no-op when the session is unknown', async () => { + await sut.onSessionEnd({ sessionId: 'never-created' }); + + expect(mocks.videoStream.deleteSession).not.toHaveBeenCalled(); + expect(mocks.storage.unlinkDir).not.toHaveBeenCalled(); + }); + }); + + describe('onHeartbeat', () => { + it('extends the DB lease when remaining time falls below half', async () => { + vi.useFakeTimers(); + try { + await sut.onSessionRequest({ sessionId, assetId, ownerId }); + vi.setSystemTime(Date.now() + HLS_LEASE_DURATION_MS / 2 + 1); + + await sut.onHeartbeat({ sessionId }); + + expect(mocks.videoStream.extendSession).toHaveBeenCalledWith(sessionId, expect.any(Date)); + } finally { + vi.useRealTimers(); + } + }); + + it('does not extend the lease while it is still fresh', async () => { + await sut.onSessionRequest({ sessionId, assetId, ownerId }); + + await sut.onHeartbeat({ sessionId }); + + expect(mocks.videoStream.extendSession).not.toHaveBeenCalled(); + }); + + it('is a no-op when the session is unknown', async () => { + await sut.onHeartbeat({ sessionId: 'never-created' }); + + expect(mocks.videoStream.extendSession).not.toHaveBeenCalled(); + }); + }); + + describe('onSegmentRequest', () => { + beforeEach(async () => { + await sut.onSessionRequest({ sessionId, assetId, ownerId }); + mocks.websocket.serverSend.mockClear(); + }); + + it('spawns FFmpeg on the first request', async () => { + mocks.process.spawn.mockReturnValue(mockSpawn(0, '', '')); + + await sut.onSegmentRequest({ sessionId, assetId, variantIndex: 0, segmentIndex: 0 }); + + expect(mocks.process.spawn).toHaveBeenCalledTimes(1); + expect(mocks.process.spawn).toHaveBeenCalledWith('ffmpeg', expect.any(Array), expect.any(Object)); + }); + + it('kills and respawns when the variant changes', async () => { + const first = mockSpawn(0, '', ''); + const second = mockSpawn(0, '', ''); + mocks.process.spawn.mockReturnValueOnce(first).mockReturnValueOnce(second); + + await sut.onSegmentRequest({ sessionId, assetId, variantIndex: 0, segmentIndex: 0 }); + await sut.onSegmentRequest({ sessionId, assetId, variantIndex: 1, segmentIndex: 0 }); + + expect(first.kill).toHaveBeenCalled(); + expect(mocks.process.spawn).toHaveBeenCalledTimes(2); + }); + + it('kills and respawns when seeking before the start segment', async () => { + const first = mockSpawn(0, '', ''); + const second = mockSpawn(0, '', ''); + mocks.process.spawn.mockReturnValueOnce(first).mockReturnValueOnce(second); + + await sut.onSegmentRequest({ sessionId, assetId, variantIndex: 0, segmentIndex: 5 }); + await sut.onSegmentRequest({ sessionId, assetId, variantIndex: 0, segmentIndex: 2 }); + + expect(first.kill).toHaveBeenCalled(); + expect(mocks.process.spawn).toHaveBeenCalledTimes(2); + }); + + it('kills and respawns when the requested segment is too far ahead', async () => { + const first = mockSpawn(0, '', ''); + const second = mockSpawn(0, '', ''); + mocks.process.spawn.mockReturnValueOnce(first).mockReturnValueOnce(second); + + await sut.onSegmentRequest({ sessionId, assetId, variantIndex: 0, segmentIndex: 0 }); + await sut.onSegmentRequest({ sessionId, assetId, variantIndex: 0, segmentIndex: 5 }); + + expect(first.kill).toHaveBeenCalled(); + expect(mocks.process.spawn).toHaveBeenCalledTimes(2); + }); + + it('does not spawn when the session is unknown', async () => { + await sut.onSegmentRequest({ sessionId: 'never-created', assetId, variantIndex: 0, segmentIndex: 0 }); + + expect(mocks.process.spawn).not.toHaveBeenCalled(); + }); + + it('accepts segments from a restart after the previous ffmpeg exited on its own', async () => { + const first = mockSpawn(0, '', ''); + const second = mockSpawn(0, '', ''); + mocks.process.spawn.mockReturnValueOnce(first).mockReturnValueOnce(second); + + await sut.onSegmentRequest({ sessionId, assetId, variantIndex: 0, segmentIndex: 10 }); + completeSegment(10); + + const onCalls = vi.mocked(first.on).mock.calls as unknown as [string, (code: number) => void][]; + const exitHandler = onCalls.find(([event]) => event === 'exit')?.[1]; + exitHandler?.(0); + + mocks.websocket.serverSend.mockClear(); + await sut.onSegmentRequest({ sessionId, assetId, variantIndex: 0, segmentIndex: 2 }); + completeSegment(2); + + expect(mocks.websocket.serverSend).toHaveBeenCalledWith('HlsSegmentResult', { + sessionId, + variantIndex: 0, + segmentIndex: 2, + }); + }); + }); + + describe('backpressure', () => { + let proc: ReturnType; + + beforeEach(async () => { + proc = mockSpawn(0, '', ''); + mocks.process.spawn.mockReturnValue(proc); + + await sut.onSessionRequest({ sessionId, assetId, ownerId }); + await sut.onSegmentRequest({ sessionId, assetId, variantIndex: 0, segmentIndex: 0 }); + }); + + it('pauses the transcode once the lead exceeds HLS_BACKPRESSURE_PAUSE_SEGMENTS', async () => { + completeSegmentsThrough(0, HLS_BACKPRESSURE_PAUSE_SEGMENTS + 1); + + await sut.onHeartbeat({ sessionId, segmentIndex: 0 }); + + expect(proc.kill).toHaveBeenCalledWith('SIGSTOP'); + }); + + it('does not pause when the lead equals the pause threshold', async () => { + completeSegmentsThrough(0, HLS_BACKPRESSURE_PAUSE_SEGMENTS); + + await sut.onHeartbeat({ sessionId, segmentIndex: 0 }); + + expect(proc.kill).not.toHaveBeenCalled(); + }); + + it('resumes once the lead drops below HLS_BACKPRESSURE_RESUME_SEGMENTS', async () => { + completeSegmentsThrough(0, HLS_BACKPRESSURE_PAUSE_SEGMENTS + 1); + await sut.onHeartbeat({ sessionId, segmentIndex: 0 }); + expect(proc.kill).toHaveBeenCalledWith('SIGSTOP'); + vi.mocked(proc.kill).mockClear(); + + const requested = HLS_BACKPRESSURE_PAUSE_SEGMENTS + 1 - (HLS_BACKPRESSURE_RESUME_SEGMENTS - 1); + await sut.onHeartbeat({ sessionId, segmentIndex: requested }); + + expect(proc.kill).toHaveBeenCalledWith('SIGCONT'); + }); + + it('stays paused while the lead is in the dead-band', async () => { + completeSegmentsThrough(0, HLS_BACKPRESSURE_PAUSE_SEGMENTS + 1); + await sut.onHeartbeat({ sessionId, segmentIndex: 0 }); + vi.mocked(proc.kill).mockClear(); + + const requested = HLS_BACKPRESSURE_PAUSE_SEGMENTS + 1 - HLS_BACKPRESSURE_RESUME_SEGMENTS; + await sut.onHeartbeat({ sessionId, segmentIndex: requested }); + + expect(proc.kill).not.toHaveBeenCalled(); + }); + + it('is a no-op when no segment has completed yet', async () => { + await sut.onHeartbeat({ sessionId, segmentIndex: 0 }); + + expect(proc.kill).not.toHaveBeenCalled(); + }); + + it('is a no-op when the heartbeat omits segmentIndex', async () => { + completeSegmentsThrough(0, HLS_BACKPRESSURE_PAUSE_SEGMENTS + 1); + + await sut.onHeartbeat({ sessionId }); + + expect(proc.kill).not.toHaveBeenCalled(); + }); + + it('resumes the paused transcode when the client requests the next in-range segment', async () => { + completeSegmentsThrough(0, HLS_BACKPRESSURE_PAUSE_SEGMENTS + 1); + await sut.onHeartbeat({ sessionId, segmentIndex: 0 }); + expect(proc.kill).toHaveBeenCalledWith('SIGSTOP'); + vi.mocked(proc.kill).mockClear(); + + await sut.onSegmentRequest({ sessionId, assetId, variantIndex: 0, segmentIndex: 1 }); + + expect(proc.kill).toHaveBeenCalledWith('SIGCONT'); + expect(mocks.process.spawn).toHaveBeenCalledTimes(1); + }); + + it('does not re-pause a freshly spawned transcode after a seek-driven restart', async () => { + const newProc = mockSpawn(0, '', ''); + mocks.process.spawn.mockReturnValueOnce(newProc); + + completeSegmentsThrough(0, HLS_BACKPRESSURE_PAUSE_SEGMENTS + 1); + await sut.onHeartbeat({ sessionId, segmentIndex: 0 }); + expect(proc.kill).toHaveBeenCalledWith('SIGSTOP'); + + await sut.onSegmentRequest({ sessionId, assetId, variantIndex: 1, segmentIndex: 0 }); + vi.mocked(newProc.kill).mockClear(); + + await sut.onHeartbeat({ sessionId, segmentIndex: 0 }); + + expect(newProc.kill).not.toHaveBeenCalled(); + }); + + it('ignores stale segment events from the prior transcode after a backward seek', async () => { + const newProc = mockSpawn(0, '', ''); + mocks.process.spawn.mockReturnValueOnce(newProc); + + const completedAhead = HLS_BACKPRESSURE_PAUSE_SEGMENTS + 5; + completeSegmentsThrough(1, completedAhead); // seg_0 was emitted in beforeEach + + await sut.onSegmentRequest({ sessionId, assetId, variantIndex: 1, segmentIndex: 0 }); + + vi.mocked(newProc.kill).mockClear(); + mocks.websocket.serverSend.mockClear(); + completeSegment(completedAhead + 1); + + expect(mocks.websocket.serverSend).not.toHaveBeenCalledWith( + 'HlsSegmentResult', + expect.objectContaining({ segmentIndex: completedAhead + 1 }), + ); + expect(newProc.kill).not.toHaveBeenCalled(); + + completeSegment(0); + expect(mocks.websocket.serverSend).toHaveBeenCalledWith( + 'HlsSegmentResult', + expect.objectContaining({ segmentIndex: 0 }), + ); + }); + }); + + describe('inactivity sweeper', () => { + it('reaps a session whose last activity exceeds the inactivity timeout', async () => { + vi.useFakeTimers(); + try { + await sut.onSessionRequest({ sessionId, assetId, ownerId }); + mocks.websocket.serverSend.mockClear(); + await vi.advanceTimersByTimeAsync(HLS_INACTIVITY_TIMEOUT_MS + HLS_CLEANUP_INTERVAL_MS); + + expect(mocks.websocket.serverSend).toHaveBeenCalledWith('HlsSessionEnd', { sessionId }); + expect(mocks.videoStream.deleteSession).toHaveBeenCalledWith(sessionId); + } finally { + vi.useRealTimers(); + } + }); + }); + + describe('onShutdown', () => { + it('ends every active session', async () => { + await sut.onSessionRequest({ sessionId: 'session-a', assetId, ownerId }); + await sut.onSessionRequest({ sessionId: 'session-b', assetId, ownerId }); + + await sut.onShutdown(); + + expect(mocks.videoStream.deleteSession).toHaveBeenCalledWith('session-a'); + expect(mocks.videoStream.deleteSession).toHaveBeenCalledWith('session-b'); + }); + }); + + describe('onHlsSessionCleanup', () => { + it('reaps DB-expired sessions under a database lock', async () => { + mocks.database.withLock.mockImplementation(async (_, fn) => fn()); + mocks.videoStream.getExpiredSessions.mockResolvedValue([ + { id: 'expired-1', ownerId: 'user-a' }, + { id: 'expired-2', ownerId: 'user-b' }, + ]); + + await sut.onHlsSessionCleanup(); + + expect(mocks.videoStream.deleteSession).toHaveBeenCalledWith('expired-1'); + expect(mocks.videoStream.deleteSession).toHaveBeenCalledWith('expired-2'); + expect(mocks.storage.unlinkDir).toHaveBeenCalledTimes(2); + }); + }); + + describe('FFmpeg full command', () => { + const baseCommand = [ + '-nostdin', + '-nostats', + '-i', + 'eiffel-tower.mp4', + '-map', + '0:0', + '-map_metadata', + '-1', + '-map', + '0:1', + '-g', + '50', + '-keyint_min', + '50', + '-crf', + '23', + '-copyts', + '-r', + '50130000/2012441', + '-avoid_negative_ts', + 'disabled', + '-f', + 'hls', + '-hls_time', + '2', + '-hls_list_size', + '0', + '-hls_segment_type', + 'fmp4', + '-hls_fmp4_init_filename', + 'init.mp4', + '-hls_segment_options', + 'movflags=+frag_discont', + '-hls_flags', + 'temp_file', + '-start_number', + '0', + ]; + + it.each([ + { + variantIndex: 6, + expected: [ + ...baseCommand, + '-c:v', + 'libsvtav1', + '-c:a', + 'aac', + '-preset', + '12', + '-svtav1-params', + 'hierarchical-levels=3:lookahead=0:enable-tf=0:mbr=4000k', + '-hls_segment_filename', + '/data/encoded-video/user-1/se/ss/session-1/6/seg_%d.m4s', + '/data/encoded-video/user-1/se/ss/session-1/6/playlist.m3u8', + ].sort(), + }, + { + variantIndex: 4, + expected: [ + ...baseCommand, + '-c:v', + 'hevc', + '-c:a', + 'aac', + '-tag:v', + 'hvc1', + '-preset', + 'ultrafast', + '-maxrate', + '2500k', + '-bufsize', + '5000k', + '-x265-params', + 'no-scenecut=1:no-open-gop=1', + '-vf', + 'scale=720:-2', + '-hls_segment_filename', + '/data/encoded-video/user-1/se/ss/session-1/4/seg_%d.m4s', + '/data/encoded-video/user-1/se/ss/session-1/4/playlist.m3u8', + ].sort(), + }, + { + variantIndex: 2, + expected: [ + ...baseCommand, + '-c:v', + 'h264', + '-c:a', + 'aac', + '-preset', + 'ultrafast', + '-maxrate', + '2500k', + '-bufsize', + '5000k', + '-sc_threshold:v', + '0', + '-vf', + 'scale=480:-2', + '-hls_segment_filename', + '/data/encoded-video/user-1/se/ss/session-1/2/seg_%d.m4s', + '/data/encoded-video/user-1/se/ss/session-1/2/playlist.m3u8', + ].sort(), + }, + ])('builds the expected FFmpeg command for $codec (variant $variantIndex)', async ({ variantIndex, expected }) => { + mocks.process.spawn.mockReturnValue(mockSpawn(0, '', '')); + + await sut.onSessionRequest({ sessionId, assetId, ownerId }); + await sut.onSegmentRequest({ sessionId, assetId, variantIndex, segmentIndex: 0 }); + + expect(mocks.process.spawn.mock.calls[0][1].toSorted()).toEqual(expected); + }); + }); + + describe('FFmpeg seek per segment', () => { + const eiffelSeeks = [ + 0, 1.987_15, 3.994_372_222_222_222, 6.001_594_444_444_444, 8.008_816_666_666_666, 10.016_038_888_888_888, + 12.023_261_111_111_111, 14.030_483_333_333_333, 16.037_705_555_555_554, 18.044_927_777_777_776, + 20.052_149_999_999_997, 22.059_372_222_222_223, + ]; + const waterfallSeeks = [ + 0, 1.994_642_826_321_467, 4.006_047_357_065_803, 6.017_451_887_810_139_5, 8.028_856_418_554_476, + 10.040_260_949_298_812, + ]; + const trainSeeks = [ + 0, 1.991_666_666_666_666_7, 3.991_666_666_666_666_7, 5.991_666_666_666_666, 7.991_666_666_666_666, + 9.991_666_666_666_667, 11.991_666_666_666_667, 13.991_666_666_666_667, 15.991_666_666_666_667, + 17.991_666_666_666_667, 19.991_666_666_666_667, + ]; + const cases = [ + ...eiffelSeeks.map((expected, segmentIndex) => ({ + name: `${eiffelTower.originalPath} K=${segmentIndex}`, + fixture: eiffelTower, + segmentIndex, + expected, + })), + ...waterfallSeeks.map((expected, segmentIndex) => ({ + name: `${waterfall.originalPath} K=${segmentIndex}`, + fixture: waterfall, + segmentIndex, + expected, + })), + ...trainSeeks.map((expected, segmentIndex) => ({ + name: `${train.originalPath} K=${segmentIndex}`, + fixture: train, + segmentIndex, + expected, + })), + ]; + + it.each(cases)('$name', async ({ fixture, segmentIndex, expected }) => { + mocks.videoStream.getForTranscoding.mockResolvedValue(fixture); + mocks.process.spawn.mockReturnValue(mockSpawn(0, '', '')); + + await sut.onSessionRequest({ sessionId, assetId, ownerId }); + await sut.onSegmentRequest({ sessionId, assetId, variantIndex: 0, segmentIndex }); + + const args = mocks.process.spawn.mock.calls[0][1] as string[]; + if (expected === 0) { + expect(args).toEqual(expect.arrayContaining(['-copyts', '-avoid_negative_ts', 'disabled'])); + expect(args).not.toContain('-ss'); + } else { + expect(args).toEqual( + expect.arrayContaining(['-ss', String(expected), '-copyts', '-avoid_negative_ts', 'disabled']), + ); + } + }); + }); +}); diff --git a/server/src/services/transcoding.service.ts b/server/src/services/transcoding.service.ts new file mode 100644 index 0000000000..69e2529d63 --- /dev/null +++ b/server/src/services/transcoding.service.ts @@ -0,0 +1,387 @@ +import { Injectable } from '@nestjs/common'; +import { ChildProcess } from 'node:child_process'; +import { join } from 'node:path'; +import { + HLS_BACKPRESSURE_PAUSE_SEGMENTS, + HLS_BACKPRESSURE_RESUME_SEGMENTS, + HLS_CLEANUP_INTERVAL_MS, + HLS_INACTIVITY_TIMEOUT_MS, + HLS_LEASE_DURATION_MS, + HLS_SEGMENT_DURATION, + HLS_SEGMENT_FILENAME_REGEX, + HLS_VARIANTS, +} from 'src/constants'; +import { StorageCore } from 'src/cores/storage.core'; +import { OnEvent, OnJob } from 'src/decorators'; +import { DatabaseLock, ImmichWorker, JobName, QueueName, TranscodeTarget } from 'src/enum'; +import { ArgOf } from 'src/repositories/event.repository'; +import { BaseService } from 'src/services/base.service'; +import { VideoInterfaces } from 'src/types'; +import { isVideoStreamSessionPkConstraint } from 'src/utils/database'; +import { BaseConfig } from 'src/utils/media'; + +type Session = { + assetId: string; + expiresAt: Date; + id: string; + lastActivityTime: Date; + lastClientRequestedSegment: number | null; + lastCompletedSegment: number | null; + ownerId: string; + paused: boolean; + process: ChildProcess | null; + startSegment: number | null; + variantIndex: number | null; +}; + +@Injectable() +export class TranscodingService extends BaseService { + private sessions = new Map(); + private videoInterfaces: VideoInterfaces = { dri: [], mali: false }; + private cleanupInterval: NodeJS.Timeout | null = null; + + @OnEvent({ name: 'AppBootstrap', workers: [ImmichWorker.Microservices] }) + async onBootstrap() { + const [videoInterfaces] = await Promise.all([this.storageCore.getVideoInterfaces(), this.removeExpiredSessions()]); + this.videoInterfaces = videoInterfaces; + } + + @OnEvent({ name: 'AppShutdown', workers: [ImmichWorker.Microservices] }) + onShutdown() { + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval); + this.cleanupInterval = null; + } + return Promise.all([...this.sessions.values()].map(({ id }) => this.onSessionEnd({ sessionId: id }))); + } + + @OnJob({ name: JobName.HlsSessionCleanup, queue: QueueName.BackgroundTask }) + onHlsSessionCleanup() { + return this.removeExpiredSessions(); + } + + @OnEvent({ name: 'HlsSessionRequest', server: true, workers: [ImmichWorker.Microservices] }) + async onSessionRequest({ assetId, sessionId, ownerId }: ArgOf<'HlsSessionRequest'>) { + try { + const expiresAt = new Date(Date.now() + HLS_LEASE_DURATION_MS); + await this.videoStreamRepository.createSession({ id: sessionId, assetId, expiresAt }); + this.sessions.set(sessionId, { + assetId, + expiresAt, + id: sessionId, + lastActivityTime: new Date(), + lastClientRequestedSegment: null, + lastCompletedSegment: null, + ownerId, + paused: false, + process: null, + startSegment: null, + variantIndex: null, + }); + this.cleanupInterval ??= setInterval(() => void this.removeInactiveSessions(), HLS_CLEANUP_INTERVAL_MS); + this.websocketRepository.serverSend('HlsSessionResult', { sessionId }); + } catch (error) { + // If insertion failed due to a PK constraint, another worker has already created a session for this ID. + if (!isVideoStreamSessionPkConstraint(error)) { + this.logger.error(`Failed to create HLS session ${sessionId}: ${error}`); + this.websocketRepository.serverSend('HlsSessionResult', { sessionId, error: 'Failed to create HLS session' }); + } + } + } + + @OnEvent({ name: 'HlsSessionEnd', server: true, workers: [ImmichWorker.Microservices] }) + async onSessionEnd({ sessionId }: ArgOf<'HlsSessionEnd'>) { + const session = this.sessions.get(sessionId); + if (!session) { + return; + } + this.sessions.delete(sessionId); + if (this.cleanupInterval && this.sessions.size === 0) { + clearInterval(this.cleanupInterval); + this.cleanupInterval = null; + } + this.stopTranscode(session); + await this.removeSessionDir(session); + await this.videoStreamRepository.deleteSession(sessionId); + } + + @OnEvent({ name: 'HlsHeartbeat', server: true, workers: [ImmichWorker.Microservices] }) + async onHeartbeat({ sessionId, segmentIndex }: ArgOf<'HlsHeartbeat'>) { + const session = this.sessions.get(sessionId); + if (!session) { + return; + } + + session.lastActivityTime = new Date(); + + if (segmentIndex !== undefined) { + session.lastClientRequestedSegment = segmentIndex; + this.applyBackpressure(session); + } + + const remaining = session.expiresAt.getTime() - Date.now(); + if (remaining < HLS_LEASE_DURATION_MS / 2) { + session.expiresAt = new Date(Date.now() + HLS_LEASE_DURATION_MS); + await this.videoStreamRepository.extendSession(sessionId, session.expiresAt); + } + } + + @OnEvent({ name: 'HlsSegmentRequest', server: true, workers: [ImmichWorker.Microservices] }) + async onSegmentRequest({ sessionId, variantIndex, segmentIndex }: ArgOf<'HlsSegmentRequest'>) { + const session = this.sessions.get(sessionId); + if (!session) { + return; + } + + session.variantIndex ??= variantIndex; + session.startSegment ??= segmentIndex; + const curSegment = session.lastCompletedSegment === null ? session.startSegment : session.lastCompletedSegment + 1; + const needsRestart = + session.variantIndex !== variantIndex || segmentIndex < session.startSegment || segmentIndex > curSegment + 1; + if (needsRestart) { + this.stopTranscode(session); + session.variantIndex = variantIndex; + session.startSegment = segmentIndex; + } else if (session.process) { + this.resumeTranscode(session); + return; + } + + const process = await this.startTranscode(session, variantIndex, segmentIndex); + if (process) { + session.process = process; + } + } + + private applyBackpressure(session: Session) { + if (session.lastCompletedSegment === null || session.lastClientRequestedSegment === null) { + return; + } + const lead = session.lastCompletedSegment - session.lastClientRequestedSegment; + this.logger.debug(`Session ${session.id} lead is ${lead} segments`); + if (!session.paused && lead > HLS_BACKPRESSURE_PAUSE_SEGMENTS) { + this.pauseTranscode(session); + } else if (session.paused && lead < HLS_BACKPRESSURE_RESUME_SEGMENTS) { + this.resumeTranscode(session); + } + } + + private async startTranscode(session: Session, variantIndex: number, startSegment: number) { + const { ffmpeg } = await this.getConfig({ withCache: true }); + + const asset = await this.videoStreamRepository.getForTranscoding(session.assetId); + if (!asset) { + this.logger.error(`Asset ${session.assetId} not found for HLS transcoding`); + return; + } + + if (session.variantIndex !== variantIndex || session.startSegment !== startSegment) { + return; + } + + const variant = HLS_VARIANTS[variantIndex]; + if (!variant) { + this.logger.error(`Variant ${variantIndex} out of range for asset ${session.assetId}`); + await this.failSession(session, `Invalid variant index ${variantIndex}`); + return; + } + + const variantDir = StorageCore.getHlsVariantFolder({ + ownerId: session.ownerId, + sessionId: session.id, + variantIndex, + }); + this.storageRepository.mkdirSync(variantDir); + + // Encoder runs at fps = packetCount ร— timeBase / totalDuration with + // gop = ceil(SEGMENT_DURATION ร— fps). To start segment K's content at + // exactly cfr slot K ร— gop, seek to the midpoint between slots Kร—gopโˆ’1 and + // Kร—gop. accurate_seek's "discard < target" then keeps the source frame + // that quantizes to slot Kร—gop and discards the one quantizing to Kร—gopโˆ’1. + const fps = (asset.packets.packetCount * asset.videoStream.timeBase) / asset.packets.totalDuration; + const gop = Math.ceil(HLS_SEGMENT_DURATION * fps); + const seekSeconds = startSegment > 0 ? (startSegment * gop - 0.5) / fps : 0; + + let config; + try { + config = BaseConfig.create( + { + ...ffmpeg, + targetVideoCodec: variant.codec, + targetResolution: String(variant.resolution), + maxBitrate: `${Math.round(variant.bitrate / 1000)}k`, + gopSize: gop, + }, + this.videoInterfaces, + { strictGop: true, lowLatency: true }, + ); + } catch (error: any) { + this.logger.error( + `Failed to create transcode config for variant ${variantIndex} asset ${session.assetId}: ${error?.message ?? error}`, + ); + await this.failSession(session, `Failed to start transcode: ${error?.message ?? 'unknown error'}`); + return; + } + const args = config.getHlsCommand( + { + initFilename: 'init.mp4', + inputPath: asset.originalPath, + packetCount: asset.packets.packetCount, + playlistFilename: join(variantDir, 'playlist.m3u8'), + seekSeconds, + segmentDuration: HLS_SEGMENT_DURATION, + segmentFilename: join(variantDir, 'seg_%d.m4s'), + startSegment, + target: TranscodeTarget.All, + timeBase: asset.videoStream.timeBase, + totalDuration: asset.packets.totalDuration, + }, + asset.videoStream, + asset.audioStream ?? undefined, + ); + this.logger.log( + `Starting HLS transcode for asset ${session.assetId} variant ${variantIndex} with command: ffmpeg ${args.join(' ')}`, + ); + const process = this.processRepository.spawn('ffmpeg', args, { stdio: ['ignore', 'ignore', 'pipe'] }); + this.attachProcessHandlers(process, session, variantIndex); + return process; + } + + private failSession(session: Session, error: string) { + this.websocketRepository.serverSend('HlsSessionResult', { sessionId: session.id, error }); + return this.onSessionEnd({ sessionId: session.id }); + } + + private attachProcessHandlers(process: ChildProcess, session: Session, variantIndex: number) { + let stderr = ''; + const variantDir = StorageCore.getHlsVariantFolder({ + ownerId: session.ownerId, + sessionId: session.id, + variantIndex, + }); + + // hlsenc writes each segment as `seg_K.m4s.tmp` then renames to + // `seg_K.m4s`. The rename event fires the moment the renamed file is + // observable โ€” the only signal we need to tell the API worker the + // segment is ready to serve. + const watcher = this.storageRepository.watchDir(variantDir, (eventType, filename) => { + if (eventType !== 'rename' || !filename || session.process !== process) { + return; + } + const match = HLS_SEGMENT_FILENAME_REGEX.exec(filename); + if (!match) { + return; + } + const segmentIndex = Number.parseInt(match[1]); + const expected = session.lastCompletedSegment === null ? session.startSegment : session.lastCompletedSegment + 1; + // Ignore stale events from old process after seek + if (expected === null || segmentIndex !== expected) { + return; + } + session.lastCompletedSegment = segmentIndex; + this.websocketRepository.serverSend('HlsSegmentResult', { + sessionId: session.id, + variantIndex, + segmentIndex, + }); + this.applyBackpressure(session); + }); + watcher.on('error', (error) => { + this.logger.error(`watcher error for ${variantDir}: ${error}`); + }); + + process.stderr!.on('data', (chunk: Buffer) => { + if (session.process !== process) { + return; + } + stderr += chunk.toString(); + }); + + process.on('exit', (code) => { + watcher.close(); + if (session.process !== process || session.variantIndex !== variantIndex) { + return; + } + session.paused = false; + session.process = null; + session.lastCompletedSegment = null; + if (code) { + this.logger.error( + `FFmpeg exited with code ${code} for variant ${variantIndex} asset ${session.assetId}\n${stderr}`, + ); + void this.failSession(session, `Transcoding process exited unexpectedly with code ${code}`).catch((error) => + this.logger.error(`Failed to end session ${session.id} after ffmpeg exit: ${error}`), + ); + } + }); + } + + private stopTranscode(session: Session) { + if (!session.process) { + return; + } + // SIGTERM makes it rename .tmp segments to .m4s even if they're still incomplete + session.process.kill('SIGKILL'); + session.process = null; + session.lastCompletedSegment = null; + session.paused = false; + this.logger.debug(`Stopped transcoding for session ${session.id}`); + } + + private pauseTranscode(session: Session) { + if (session.paused || !session.process) { + return; + } + session.process.kill('SIGSTOP'); + session.paused = true; + this.logger.debug(`Paused transcoding for session ${session.id}`); + } + + private resumeTranscode(session: Session) { + if (!session.paused || !session.process) { + return; + } + session.process.kill('SIGCONT'); + session.paused = false; + this.logger.debug(`Resumed transcoding for session ${session.id}`); + } + + private async removeSessionDir(session: { ownerId: string; id: string }) { + const dir = StorageCore.getHlsSessionFolder({ ownerId: session.ownerId, sessionId: session.id }); + try { + await this.storageRepository.unlinkDir(dir, { recursive: true, force: true }); + } catch (error) { + if ((error as NodeJS.ErrnoException)?.code !== 'ENOENT') { + throw error; + } + this.logger.warn(`Session dir ${dir} does not exist.`); + } + } + + private removeInactiveSessions() { + const cutoff = Date.now() - HLS_INACTIVITY_TIMEOUT_MS; + const inactiveSessions = [...this.sessions.values()].filter((s) => s.lastActivityTime.getTime() < cutoff); + return Promise.all( + inactiveSessions.map(async (session) => { + try { + this.websocketRepository.serverSend('HlsSessionEnd', { sessionId: session.id }); + await this.onSessionEnd({ sessionId: session.id }); + } catch (error) { + this.logger.error(`Failed to sweep inactive HLS session ${session.id}: ${error}`); + } + }), + ); + } + + private removeExpiredSessions() { + return this.databaseRepository.withLock(DatabaseLock.HlsSessionCleanup, async () => { + const expiredSessions = await this.videoStreamRepository.getExpiredSessions(); + await Promise.all( + expiredSessions.map(async (session) => { + await this.removeSessionDir(session); + await this.videoStreamRepository.deleteSession(session.id); + }), + ); + }); + } +} diff --git a/server/src/types.ts b/server/src/types.ts index 6b985af662..dde279e5d8 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -29,7 +29,6 @@ import { SystemMetadataKey, TranscodeTarget, UserMetadataKey, - VideoCodec, WorkflowType, } from 'src/enum'; @@ -162,6 +161,25 @@ export interface TranscodeCommand { }; } +export interface VideoTuning { + strictGop: boolean; + lowLatency: boolean; +} + +export interface HlsCommandOptions { + initFilename: string; + inputPath: string; + packetCount: number; + playlistFilename: string; + seekSeconds?: number; + segmentDuration: number; + segmentFilename: string; + startSegment: number; + target: TranscodeTarget; + timeBase: number; + totalDuration: number; +} + export interface BitrateDistribution { max: number; target: number; @@ -177,14 +195,11 @@ export interface ImageBuffer { export interface VideoCodecSWConfig { getCommand( target: TranscodeTarget, - videoStream: VideoStreamInfo, - audioStream?: AudioStreamInfo, + video: VideoStreamInfo, + audio?: AudioStreamInfo, format?: VideoFormat, ): TranscodeCommand; -} - -export interface VideoCodecHWConfig extends VideoCodecSWConfig { - getSupportedCodecs(): Array; + getHlsCommand(options: HlsCommandOptions, video: VideoStreamInfo, audio?: AudioStreamInfo): string[]; } export interface ProbeOptions { @@ -371,6 +386,7 @@ export type JobItem = // Cleanup | { name: JobName.SessionCleanup; data?: IBaseJob } + | { name: JobName.HlsSessionCleanup; data?: IBaseJob } // Tags | { name: JobName.TagCleanup; data?: IBaseJob } diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index fbf32c0ac2..cb942b5366 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -71,10 +71,13 @@ export const removeUndefinedKeys = (update: T, template: unkno }; export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_checksum'; +export const VIDEO_STREAM_SESSION_PK_CONSTRAINT = 'video_stream_session_pkey'; -export const isAssetChecksumConstraint = (error: unknown) => { - return (error as PostgresError)?.constraint_name === 'UQ_assets_owner_checksum'; -}; +export const isAssetChecksumConstraint = (error: unknown) => + (error as PostgresError)?.constraint_name === ASSET_CHECKSUM_CONSTRAINT; + +export const isVideoStreamSessionPkConstraint = (error: unknown) => + (error as PostgresError)?.constraint_name === VIDEO_STREAM_SESSION_PK_CONSTRAINT; export function withDefaultVisibility(qb: SelectQueryBuilder) { return qb.where('asset.visibility', 'in', [sql.lit(AssetVisibility.Archive), sql.lit(AssetVisibility.Timeline)]); diff --git a/server/src/utils/event.ts b/server/src/utils/event.ts new file mode 100644 index 0000000000..fd791620de --- /dev/null +++ b/server/src/utils/event.ts @@ -0,0 +1,50 @@ +import { ArgOf, EmitEvent } from 'src/repositories/event.repository'; + +export class PendingEvents extends { error?: string } ? T : never }[EmitEvent]> { + private pending = new Map>[]; timeout: NodeJS.Timeout }>(); + private timeoutMs: number; + + constructor({ timeoutMs }: { timeoutMs: number }) { + this.timeoutMs = timeoutMs; + } + + wait(key: string): Promise> { + const completer = Promise.withResolvers>(); + const existing = this.pending.get(key); + if (existing) { + existing.completers.push(completer); + return completer.promise; + } + + const timeout = setTimeout(() => this.complete(key, { error: 'Request timed out' }), this.timeoutMs); + this.pending.set(key, { completers: [completer], timeout }); + return completer.promise; + } + + complete(key: string, value: ArgOf | { error: string }) { + const pending = this.pending.get(key); + if (!pending) { + return; + } + clearTimeout(pending.timeout); + this.pending.delete(key); + if ('error' in value) { + const error = new Error(value.error); + for (const completer of pending.completers) { + completer.reject(error); + } + } else { + for (const completer of pending.completers) { + completer.resolve(value); + } + } + } + + rejectByPrefix(prefix: string, error: string) { + for (const key of this.pending.keys()) { + if (key.startsWith(prefix)) { + this.complete(key, { error }); + } + } + } +} diff --git a/server/src/utils/media.ts b/server/src/utils/media.ts index 49e11edab7..b3a617c36e 100644 --- a/server/src/utils/media.ts +++ b/server/src/utils/media.ts @@ -1,4 +1,4 @@ -import { AUDIO_ENCODER } from 'src/constants'; +import { AUDIO_ENCODER, SUPPORTED_HWA_CODECS } from 'src/constants'; import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; import { ColorMatrix, @@ -13,38 +13,56 @@ import { import { AudioStreamInfo, BitrateDistribution, + HlsCommandOptions, TranscodeCommand, - VideoCodecHWConfig, VideoCodecSWConfig, VideoFormat, VideoInterfaces, VideoStreamInfo, + VideoTuning, } from 'src/types'; +export const isVideoRotated = (videoStream: VideoStreamInfo): boolean => Math.abs(videoStream.rotation) === 90; + +export const isVideoVertical = (videoStream: VideoStreamInfo): boolean => + videoStream.height > videoStream.width || isVideoRotated(videoStream); + +export const getOutputSize = (videoStream: VideoStreamInfo, targetRes: number) => { + const factor = Math.max(videoStream.height, videoStream.width) / Math.min(videoStream.height, videoStream.width); + let larger = Math.round(targetRes * factor); + if (larger % 2 !== 0) { + larger -= 1; + } + return isVideoVertical(videoStream) ? { width: targetRes, height: larger } : { width: larger, height: targetRes }; +}; + export class BaseConfig implements VideoCodecSWConfig { readonly presets = ['veryslow', 'slower', 'slow', 'medium', 'fast', 'faster', 'veryfast', 'superfast', 'ultrafast']; - protected constructor(protected config: SystemConfigFFmpegDto) {} + protected constructor( + protected config: SystemConfigFFmpegDto, + protected tune: VideoTuning = { strictGop: false, lowLatency: false }, + ) {} - static create(config: SystemConfigFFmpegDto, interfaces: VideoInterfaces): VideoCodecSWConfig { + static create(config: SystemConfigFFmpegDto, interfaces: VideoInterfaces, tune?: VideoTuning) { if (config.accel === TranscodeHardwareAcceleration.Disabled) { - return this.getSWCodecConfig(config); + return this.getSWCodecConfig(config, tune); } - return this.getHWCodecConfig(config, interfaces); + return this.getHWCodecConfig(config, interfaces, tune); } - private static getSWCodecConfig(config: SystemConfigFFmpegDto) { + private static getSWCodecConfig(config: SystemConfigFFmpegDto, tune?: VideoTuning): VideoCodecSWConfig { switch (config.targetVideoCodec) { case VideoCodec.H264: { - return new H264Config(config); + return new H264Config(config, tune); } case VideoCodec.Hevc: { - return new HEVCConfig(config); + return new HEVCConfig(config, tune); } case VideoCodec.Vp9: { - return new VP9Config(config); + return new VP9Config(config, tune); } case VideoCodec.Av1: { - return new AV1Config(config); + return new AV1Config(config, tune); } default: { throw new Error(`Codec '${config.targetVideoCodec}' is unsupported`); @@ -52,72 +70,122 @@ export class BaseConfig implements VideoCodecSWConfig { } } - private static getHWCodecConfig(config: SystemConfigFFmpegDto, interfaces: VideoInterfaces) { - let handler: VideoCodecHWConfig; + private static getHWCodecConfig(config: SystemConfigFFmpegDto, interfaces: VideoInterfaces, tune?: VideoTuning) { + if (!SUPPORTED_HWA_CODECS[config.accel].includes(config.targetVideoCodec)) { + throw new Error( + `${config.accel.toUpperCase()} acceleration does not support codec '${config.targetVideoCodec.toUpperCase()}'. Supported codecs: ${SUPPORTED_HWA_CODECS[config.accel]}`, + ); + } + + let handler: VideoCodecSWConfig; switch (config.accel) { case TranscodeHardwareAcceleration.Nvenc: { handler = config.accelDecode - ? new NvencHwDecodeConfig(config, interfaces) - : new NvencSwDecodeConfig(config, interfaces); + ? new NvencHwDecodeConfig(config, interfaces, tune) + : new NvencSwDecodeConfig(config, interfaces, tune); break; } case TranscodeHardwareAcceleration.Qsv: { handler = config.accelDecode - ? new QsvHwDecodeConfig(config, interfaces) - : new QsvSwDecodeConfig(config, interfaces); + ? new QsvHwDecodeConfig(config, interfaces, tune) + : new QsvSwDecodeConfig(config, interfaces, tune); break; } case TranscodeHardwareAcceleration.Vaapi: { handler = config.accelDecode - ? new VaapiHwDecodeConfig(config, interfaces) - : new VaapiSwDecodeConfig(config, interfaces); + ? new VaapiHwDecodeConfig(config, interfaces, tune) + : new VaapiSwDecodeConfig(config, interfaces, tune); break; } case TranscodeHardwareAcceleration.Rkmpp: { handler = config.accelDecode - ? new RkmppHwDecodeConfig(config, interfaces) - : new RkmppSwDecodeConfig(config, interfaces); + ? new RkmppHwDecodeConfig(config, interfaces, tune) + : new RkmppSwDecodeConfig(config, interfaces, tune); break; } default: { throw new Error(`${config.accel.toUpperCase()} acceleration is unsupported`); } } - if (!handler.getSupportedCodecs().includes(config.targetVideoCodec)) { - throw new Error( - `${config.accel.toUpperCase()} acceleration does not support codec '${config.targetVideoCodec.toUpperCase()}'. Supported codecs: ${handler.getSupportedCodecs()}`, - ); - } return handler; } - getCommand( - target: TranscodeTarget, - videoStream: VideoStreamInfo, - audioStream?: AudioStreamInfo, - format?: VideoFormat, - ) { + getCommand(target: TranscodeTarget, video: VideoStreamInfo, audio?: AudioStreamInfo, format?: VideoFormat) { const options = { - inputOptions: this.getBaseInputOptions(videoStream, format), - outputOptions: [...this.getBaseOutputOptions(target, videoStream, audioStream), '-v', 'verbose'], + inputOptions: this.getBaseInputOptions(video, format), + outputOptions: [ + ...this.getBaseOutputOptions(target, video, audio), + ...this.getPresetOptions(), + ...this.getBitrateOptions(), + ...this.getEncoderOptions(), + '-movflags', + 'faststart', + '-fps_mode', + 'passthrough', + '-v', + 'verbose', + ], twoPass: this.eligibleForTwoPass(), - progress: { frameCount: videoStream.frameCount, percentInterval: 5 }, + progress: { frameCount: video.frameCount, percentInterval: 5 }, } as TranscodeCommand; if ([TranscodeTarget.All, TranscodeTarget.Video].includes(target)) { - const filters = this.getFilterOptions(videoStream); + const filters = this.getFilterOptions(video); if (filters.length > 0) { options.outputOptions.push('-vf', filters.join(',')); } } - options.outputOptions.push( + return options; + } + + getHlsCommand(options: HlsCommandOptions, video: VideoStreamInfo, audio?: AudioStreamInfo) { + const args: string[] = this.getBaseInputOptions(video); + if (options.seekSeconds) { + args.push('-ss', String(options.seekSeconds)); + } + args.push( + '-nostdin', + '-nostats', + '-i', + options.inputPath, + ...this.getBaseOutputOptions(options.target, video, audio), ...this.getPresetOptions(), - ...this.getOutputThreadOptions(), ...this.getBitrateOptions(), + ...this.getEncoderOptions(), + '-copyts', + '-r', + `${options.packetCount * options.timeBase}/${options.totalDuration}`, + '-avoid_negative_ts', + 'disabled', + '-f', + 'hls', + '-hls_time', + String(options.segmentDuration), + '-hls_list_size', + '0', + '-hls_segment_type', + 'fmp4', + '-hls_fmp4_init_filename', + options.initFilename, + '-hls_segment_options', + 'movflags=+frag_discont', + '-hls_flags', + 'temp_file', + '-hls_segment_filename', + options.segmentFilename, + '-start_number', + String(options.startSegment), ); - return options; + if ([TranscodeTarget.All, TranscodeTarget.Video].includes(options.target)) { + const filters = this.getFilterOptions(video); + if (filters.length > 0) { + args.push('-vf', filters.join(',')); + } + } + args.push(options.playlistFilename); + return args; } // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -129,23 +197,7 @@ export class BaseConfig implements VideoCodecSWConfig { const videoCodec = [TranscodeTarget.All, TranscodeTarget.Video].includes(target) ? this.getVideoCodec() : 'copy'; const audioCodec = [TranscodeTarget.All, TranscodeTarget.Audio].includes(target) ? this.getAudioEncoder() : 'copy'; - const options = [ - '-c:v', - videoCodec, - '-c:a', - audioCodec, - // Makes a second pass moving the moov atom to the - // beginning of the file for improved playback speed. - '-movflags', - 'faststart', - '-fps_mode', - 'passthrough', - '-map', - `0:${videoStream.index}`, - '-map_metadata', - '-1', - ]; - + const options = ['-c:v', videoCodec, '-c:a', audioCodec, '-map', `0:${videoStream.index}`, '-map_metadata', '-1']; if (audioStream) { options.push('-map', `0:${audioStream.index}`); } @@ -157,18 +209,22 @@ export class BaseConfig implements VideoCodecSWConfig { } if (this.getGopSize() > 0) { options.push('-g', `${this.getGopSize()}`); + if (this.tune.strictGop) { + options.push('-keyint_min', `${this.getGopSize()}`); + } } - - if ( - this.config.targetVideoCodec === VideoCodec.Hevc && - (videoCodec !== 'copy' || videoStream.codecName === 'hevc') - ) { + const isHvc = (videoCodec === 'copy' ? videoStream.codecName : videoCodec) === VideoCodec.Hevc; + if (isHvc) { options.push('-tag:v', 'hvc1'); } return options; } + getEncoderOptions(): string[] { + return []; + } + getFilterOptions(videoStream: VideoStreamInfo) { const options = []; if (this.shouldScale(videoStream)) { @@ -272,25 +328,7 @@ export class BaseConfig implements VideoCodecSWConfig { getScaling(videoStream: VideoStreamInfo, mult = 2) { const targetResolution = this.getTargetResolution(videoStream); - return this.isVideoVertical(videoStream) ? `${targetResolution}:-${mult}` : `-${mult}:${targetResolution}`; - } - - getSize(videoStream: VideoStreamInfo) { - const smaller = this.getTargetResolution(videoStream); - const factor = Math.max(videoStream.height, videoStream.width) / Math.min(videoStream.height, videoStream.width); - let larger = Math.round(smaller * factor); - if (larger % 2 !== 0) { - larger -= 1; - } - return this.isVideoVertical(videoStream) ? { width: smaller, height: larger } : { width: larger, height: smaller }; - } - - isVideoRotated(videoStream: VideoStreamInfo) { - return Math.abs(videoStream.rotation) === 90; - } - - isVideoVertical(videoStream: VideoStreamInfo) { - return videoStream.height > videoStream.width || this.isVideoRotated(videoStream); + return isVideoVertical(videoStream) ? `${targetResolution}:-${mult}` : `-${mult}:${targetResolution}`; } isBitrateConstrained() { @@ -353,23 +391,18 @@ export class BaseConfig implements VideoCodecSWConfig { } } -export class BaseHWConfig extends BaseConfig implements VideoCodecHWConfig { +export class BaseHWConfig extends BaseConfig { protected device: string; - protected interfaces: VideoInterfaces; constructor( protected config: SystemConfigFFmpegDto, - interfaces: VideoInterfaces, + protected interfaces: VideoInterfaces, + tune?: VideoTuning, ) { - super(config); - this.interfaces = interfaces; + super(config, tune); this.device = this.getDevice(interfaces); } - getSupportedCodecs() { - return [VideoCodec.H264, VideoCodec.Hevc]; - } - validateDevices(devices: string[]) { if (devices.length === 0) { throw new Error('No /dev/dri devices found. If using Docker, make sure at least one /dev/dri device is mounted'); @@ -474,24 +507,32 @@ export class ThumbnailConfig extends BaseConfig { } export class H264Config extends BaseConfig { - getOutputThreadOptions() { - const options = super.getOutputThreadOptions(); - if (this.config.threads === 1) { - options.push('-x264-params', 'frame-threads=1:pools=none'); + getEncoderOptions(): string[] { + const out = this.getOutputThreadOptions(); + if (this.tune.strictGop) { + out.push('-sc_threshold:v', '0'); } - - return options; + if (this.config.threads === 1) { + out.push('-x264-params', 'frame-threads=1:pools=none'); + } + return out; } } export class HEVCConfig extends BaseConfig { - getOutputThreadOptions() { - const options = super.getOutputThreadOptions(); - if (this.config.threads === 1) { - options.push('-x265-params', 'frame-threads=1:pools=none'); + getEncoderOptions(): string[] { + const out: string[] = this.getOutputThreadOptions(); + const params: string[] = []; + if (this.tune.strictGop) { + params.push('no-scenecut=1', 'no-open-gop=1'); } - - return options; + if (this.config.threads === 1) { + params.push('frame-threads=1', 'pools=none'); + } + if (params.length > 0) { + out.push('-x265-params', params.join(':')); + } + return out; } } @@ -520,8 +561,8 @@ export class VP9Config extends BaseConfig { return [`-${this.useCQP() ? 'q:v' : 'crf'}`, `${this.config.crf}`, '-b:v', `${bitrates.max}${bitrates.unit}`]; } - getOutputThreadOptions() { - return ['-row-mt', '1', ...super.getOutputThreadOptions()]; + getEncoderOptions(): string[] { + return ['-row-mt', '1', ...this.getOutputThreadOptions()]; } eligibleForTwoPass() { @@ -543,23 +584,22 @@ export class AV1Config extends BaseConfig { } getBitrateOptions() { - const options = ['-crf', `${this.config.crf}`]; - const bitrates = this.getBitrateDistribution(); - const svtparams = []; - if (this.config.threads > 0) { - svtparams.push(`lp=${this.config.threads}`); - } - if (bitrates.max > 0) { - svtparams.push(`mbr=${bitrates.max}${bitrates.unit}`); - } - if (svtparams.length > 0) { - options.push('-svtav1-params', svtparams.join(':')); - } - return options; + return ['-crf', `${this.config.crf}`]; } - getOutputThreadOptions() { - return []; // Already set above with svtav1-params + getEncoderOptions(): string[] { + const params: string[] = []; + if (this.tune.lowLatency) { + params.push('hierarchical-levels=3', 'lookahead=0', 'enable-tf=0'); + } + if (this.config.threads > 0) { + params.push(`lp=${this.config.threads}`); + } + const bitrates = this.getBitrateDistribution(); + if (bitrates.max > 0) { + params.push(`mbr=${bitrates.max}${bitrates.unit}`); + } + return params.length > 0 ? ['-svtav1-params', params.join(':')] : []; } eligibleForTwoPass() { @@ -572,10 +612,6 @@ export class NvencSwDecodeConfig extends BaseHWConfig { return '0'; } - getSupportedCodecs() { - return [VideoCodec.H264, VideoCodec.Hevc, VideoCodec.Av1]; - } - getBaseInputOptions() { return ['-init_hw_device', `cuda=cuda:${this.device}`, '-filter_hw_device', 'cuda']; } @@ -652,6 +688,14 @@ export class NvencSwDecodeConfig extends BaseHWConfig { return []; } + getEncoderOptions(): string[] { + const out = this.getOutputThreadOptions(); + if (this.tune.strictGop) { + out.push('-forced-idr', '1'); + } + return out; + } + getRefs() { const bframes = this.getBFrames(); if (bframes > 0 && bframes < 3 && this.config.refs < 3) { @@ -703,8 +747,8 @@ export class NvencHwDecodeConfig extends NvencSwDecodeConfig { return ['-threads', '1']; } - getOutputThreadOptions() { - return []; + getEncoderOptions(): string[] { + return this.tune.strictGop ? ['-forced-idr', '1'] : []; } } @@ -749,10 +793,6 @@ export class QsvSwDecodeConfig extends BaseHWConfig { return options; } - getSupportedCodecs() { - return [VideoCodec.H264, VideoCodec.Hevc, VideoCodec.Vp9, VideoCodec.Av1]; - } - // recommended from https://github.com/intel/media-delivery/blob/master/doc/benchmarks/intel-iris-xe-max-graphics/intel-iris-xe-max-graphics.md getBFrames() { if (this.config.bframes < 0) { @@ -775,6 +815,14 @@ export class QsvSwDecodeConfig extends BaseHWConfig { getScaling(videoStream: VideoStreamInfo): string { return super.getScaling(videoStream, 1); } + + getEncoderOptions(): string[] { + const out = this.getOutputThreadOptions(); + if (this.tune.strictGop) { + out.push('-idr_interval', '0'); + } + return out; + } } export class QsvHwDecodeConfig extends QsvSwDecodeConfig { @@ -888,13 +936,17 @@ export class VaapiSwDecodeConfig extends BaseHWConfig { return options; } - getSupportedCodecs() { - return [VideoCodec.H264, VideoCodec.Hevc, VideoCodec.Vp9, VideoCodec.Av1]; - } - useCQP() { return this.config.cqMode !== CQMode.Icq || this.config.targetVideoCodec === VideoCodec.Vp9; } + + getEncoderOptions(): string[] { + const out = this.getOutputThreadOptions(); + if (this.tune.strictGop) { + out.push('-idr_interval', '0'); + } + return out; + } } export class VaapiHwDecodeConfig extends VaapiSwDecodeConfig { @@ -988,10 +1040,6 @@ export class RkmppSwDecodeConfig extends BaseHWConfig { return ['-rc_mode', 'CQP', '-qp_init', `${this.config.crf}`]; } - getSupportedCodecs() { - return [VideoCodec.H264, VideoCodec.Hevc]; - } - getVideoCodec(): string { return `${this.config.targetVideoCodec}_rkmpp`; } diff --git a/server/test/fixtures/media.stub.ts b/server/test/fixtures/media.stub.ts index f034ab873d..7edd1c2dee 100644 --- a/server/test/fixtures/media.stub.ts +++ b/server/test/fixtures/media.stub.ts @@ -597,7 +597,7 @@ export const train = { packets: { totalDuration: 12_290, packetCount: 1229, - outputFrames: 1303, + outputFrames: 1304, keyframePts: [ 0, 601, 1201, 1802, 2402, 3003, 3604, 4204, 4805, 5405, 6006, 6607, 7207, 7808, 8408, 9009, 9609, 10_210, 10_811, 11_411, 12_062, 12_703, diff --git a/server/test/repositories/storage.repository.mock.ts b/server/test/repositories/storage.repository.mock.ts index 334d7d0d53..c1fb7ceaa6 100644 --- a/server/test/repositories/storage.repository.mock.ts +++ b/server/test/repositories/storage.repository.mock.ts @@ -75,5 +75,6 @@ export const newStorageRepositoryMock = (): Mocked ({ close: vitest.fn(), on: vitest.fn() })), }; }; diff --git a/server/test/utils.ts b/server/test/utils.ts index 75ada7b551..e707971aad 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -181,7 +181,11 @@ export const automock = ( const mocks: Mock[] = []; const instance = new Dependency(...args); - for (const property of Object.getOwnPropertyNames(Dependency.prototype)) { + const propertyNames = new Set([ + ...Object.getOwnPropertyNames(Dependency.prototype), + ...Object.getOwnPropertyNames(instance), + ]); + for (const property of propertyNames) { if (property === 'constructor') { continue; } @@ -346,7 +350,7 @@ export const getMocks = () => { trash: automock(TrashRepository), user: automock(UserRepository, { strict: false }), versionHistory: automock(VersionHistoryRepository), - videoStream: automock(VideoStreamRepository), + videoStream: automock(VideoStreamRepository, { strict: false }), view: automock(ViewRepository), // eslint-disable-next-line no-sparse-arrays websocket: automock(WebsocketRepository, { args: [, loggerMock], strict: false }), @@ -500,6 +504,7 @@ export const mockSpawn = vitest.fn((exitCode: number, stdout: string, stderr: st callback(exitCode); } }), + kill: vitest.fn(), } as unknown as ChildProcessWithoutNullStreams; }); diff --git a/server/tsconfig.json b/server/tsconfig.json index e087544f6b..c3aede3e5b 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -8,9 +8,9 @@ "experimentalDecorators": true, "allowSyntheticDefaultImports": true, "resolveJsonModule": true, - "target": "es2022", + "target": "es2024", "moduleResolution": "node16", - "lib": ["dom", "es2023"], + "lib": ["dom", "es2024"], "sourceMap": true, "outDir": "./dist", "incremental": true, diff --git a/web/src/routes/admin/system-settings/FFmpegSettings.svelte b/web/src/routes/admin/system-settings/FFmpegSettings.svelte index 2fbc45ce2a..33d6ba84f6 100644 --- a/web/src/routes/admin/system-settings/FFmpegSettings.svelte +++ b/web/src/routes/admin/system-settings/FFmpegSettings.svelte @@ -388,6 +388,22 @@ />
+ + +
+ +
+
From 138e2d915869d14365bb69c3d65366ba314b8f03 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Mon, 1 Jun 2026 15:49:57 -0400 Subject: [PATCH 26/50] feat(web): hls player (#28312) * update e2e * hls player * fix transcoding restart on explicit quality selection * move level filtering to manager * move init to manager declaration * refactor commit on release * these lints... * fix seek sometimes being ignored * fix panic downswitch --- e2e/src/specs/server/api/server.e2e-spec.ts | 1 + i18n/en.json | 1 + .../lib/model/server_features_dto.dart | 11 +- open-api/immich-openapi-specs.json | 5 + packages/sdk/src/fetch-client.ts | 2 + pnpm-lock.yaml | 30 ++ server/src/dtos/server.dto.ts | 1 + server/src/services/server.service.spec.ts | 1 + server/src/services/server.service.ts | 3 +- web/package.json | 2 + .../asset-viewer/VideoNativeViewer.svelte | 274 +++++++++++++++--- .../asset-viewer/immich-time-range.ts | 54 ++++ .../media-capabilities-manager.svelte.ts | 92 ++++++ web/src/lib/utils.ts | 8 + 14 files changed, 449 insertions(+), 36 deletions(-) create mode 100644 web/src/lib/components/asset-viewer/immich-time-range.ts create mode 100644 web/src/lib/managers/media-capabilities-manager.svelte.ts diff --git a/e2e/src/specs/server/api/server.e2e-spec.ts b/e2e/src/specs/server/api/server.e2e-spec.ts index f15d450213..9ab2f5d823 100644 --- a/e2e/src/specs/server/api/server.e2e-spec.ts +++ b/e2e/src/specs/server/api/server.e2e-spec.ts @@ -116,6 +116,7 @@ describe('/server', () => { oauthAutoLaunch: false, ocr: false, passwordLogin: true, + realtimeTranscoding: false, search: true, sidecar: true, trash: true, diff --git a/i18n/en.json b/i18n/en.json index 035a662545..90beb7077e 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -2460,6 +2460,7 @@ "video": "Video", "video_hover_setting": "Play video thumbnail on hover", "video_hover_setting_description": "Play video thumbnail when mouse is hovering over item. Even when disabled, playback can be started by hovering over the play icon.", + "video_quality": "Video quality", "videos": "Videos", "videos_count": "{count, plural, one {# Video} other {# Videos}}", "videos_only": "Videos only", diff --git a/mobile/openapi/lib/model/server_features_dto.dart b/mobile/openapi/lib/model/server_features_dto.dart index 79494b74eb..9b75ef2b32 100644 --- a/mobile/openapi/lib/model/server_features_dto.dart +++ b/mobile/openapi/lib/model/server_features_dto.dart @@ -23,6 +23,7 @@ class ServerFeaturesDto { required this.oauthAutoLaunch, required this.ocr, required this.passwordLogin, + required this.realtimeTranscoding, required this.reverseGeocoding, required this.search, required this.sidecar, @@ -60,6 +61,9 @@ class ServerFeaturesDto { /// Whether password login is enabled bool passwordLogin; + /// Whether real-time transcoding is enabled + bool realtimeTranscoding; + /// Whether reverse geocoding is enabled bool reverseGeocoding; @@ -87,6 +91,7 @@ class ServerFeaturesDto { other.oauthAutoLaunch == oauthAutoLaunch && other.ocr == ocr && other.passwordLogin == passwordLogin && + other.realtimeTranscoding == realtimeTranscoding && other.reverseGeocoding == reverseGeocoding && other.search == search && other.sidecar == sidecar && @@ -106,6 +111,7 @@ class ServerFeaturesDto { (oauthAutoLaunch.hashCode) + (ocr.hashCode) + (passwordLogin.hashCode) + + (realtimeTranscoding.hashCode) + (reverseGeocoding.hashCode) + (search.hashCode) + (sidecar.hashCode) + @@ -113,7 +119,7 @@ class ServerFeaturesDto { (trash.hashCode); @override - String toString() => 'ServerFeaturesDto[configFile=$configFile, duplicateDetection=$duplicateDetection, email=$email, facialRecognition=$facialRecognition, importFaces=$importFaces, map=$map, oauth=$oauth, oauthAutoLaunch=$oauthAutoLaunch, ocr=$ocr, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, trash=$trash]'; + String toString() => 'ServerFeaturesDto[configFile=$configFile, duplicateDetection=$duplicateDetection, email=$email, facialRecognition=$facialRecognition, importFaces=$importFaces, map=$map, oauth=$oauth, oauthAutoLaunch=$oauthAutoLaunch, ocr=$ocr, passwordLogin=$passwordLogin, realtimeTranscoding=$realtimeTranscoding, reverseGeocoding=$reverseGeocoding, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, trash=$trash]'; Map toJson() { final json = {}; @@ -127,6 +133,7 @@ class ServerFeaturesDto { json[r'oauthAutoLaunch'] = this.oauthAutoLaunch; json[r'ocr'] = this.ocr; json[r'passwordLogin'] = this.passwordLogin; + json[r'realtimeTranscoding'] = this.realtimeTranscoding; json[r'reverseGeocoding'] = this.reverseGeocoding; json[r'search'] = this.search; json[r'sidecar'] = this.sidecar; @@ -154,6 +161,7 @@ class ServerFeaturesDto { oauthAutoLaunch: mapValueOfType(json, r'oauthAutoLaunch')!, ocr: mapValueOfType(json, r'ocr')!, passwordLogin: mapValueOfType(json, r'passwordLogin')!, + realtimeTranscoding: mapValueOfType(json, r'realtimeTranscoding')!, reverseGeocoding: mapValueOfType(json, r'reverseGeocoding')!, search: mapValueOfType(json, r'search')!, sidecar: mapValueOfType(json, r'sidecar')!, @@ -216,6 +224,7 @@ class ServerFeaturesDto { 'oauthAutoLaunch', 'ocr', 'passwordLogin', + 'realtimeTranscoding', 'reverseGeocoding', 'search', 'sidecar', diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 68d04a665a..d9087c375d 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -21682,6 +21682,10 @@ "description": "Whether password login is enabled", "type": "boolean" }, + "realtimeTranscoding": { + "description": "Whether real-time transcoding is enabled", + "type": "boolean" + }, "reverseGeocoding": { "description": "Whether reverse geocoding is enabled", "type": "boolean" @@ -21714,6 +21718,7 @@ "oauthAutoLaunch", "ocr", "passwordLogin", + "realtimeTranscoding", "reverseGeocoding", "search", "sidecar", diff --git a/packages/sdk/src/fetch-client.ts b/packages/sdk/src/fetch-client.ts index 85b859ac1d..163558e6a6 100644 --- a/packages/sdk/src/fetch-client.ts +++ b/packages/sdk/src/fetch-client.ts @@ -1993,6 +1993,8 @@ export type ServerFeaturesDto = { ocr: boolean; /** Whether password login is enabled */ passwordLogin: boolean; + /** Whether real-time transcoding is enabled */ + realtimeTranscoding: boolean; /** Whether reverse geocoding is enabled */ reverseGeocoding: boolean; /** Whether search is enabled */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 329203c36b..debdb79b93 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -820,6 +820,12 @@ importers: happy-dom: specifier: ^20.0.0 version: 20.9.0 + hls-video-element: + specifier: ^1.5.11 + version: 1.5.11 + hls.js: + specifier: ^1.6.16 + version: 1.6.16 intl-messageformat: specifier: ^11.0.0 version: 11.2.6 @@ -6947,6 +6953,9 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + custom-media-element@1.4.6: + resolution: {integrity: sha512-/HRYqJOa1ob5ik4q7FIJVYxTJCFs/FL3+cQPAJjUf2uiqrDEzbTgB315gQ2rG8oK3w094W9m5tcB8S5Qah+caA==} + cytoscape-cose-bilkent@4.1.0: resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} peerDependencies: @@ -8271,6 +8280,12 @@ packages: history@4.10.1: resolution: {integrity: sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==} + hls-video-element@1.5.11: + resolution: {integrity: sha512-tJJ65/52CDxj8XFyIve6zT9nVVdUIc6mqvKR25X0ycPKHk07rpjp4xxVteeCefDUBSf/tFLhlICFmn3KWj37xA==} + + hls.js@1.6.16: + resolution: {integrity: sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==} + hogan.js@3.0.2: resolution: {integrity: sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg==} hasBin: true @@ -9275,6 +9290,9 @@ packages: media-chrome@4.19.0: resolution: {integrity: sha512-HWhDTwts+BSbdPkkB1VsJXp5kvL0IxY7xFT5tBwliM2+89kTPVTnHnev+9it2f9PweANjT/C8/C/S0PW9oyZbA==} + media-tracks@0.3.5: + resolution: {integrity: sha512-l54rkKXlLBt3ob3zOLWHcnjvwUmX5bNEZ70igyapOZZC9imzqBmq1oz8p2roiV04KhjblFIi2hetLPF1oYVLRA==} + media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -19869,6 +19887,8 @@ snapshots: csstype@3.2.3: {} + custom-media-element@1.4.6: {} + cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.4): dependencies: cose-base: 1.0.3 @@ -21558,6 +21578,14 @@ snapshots: tiny-warning: 1.0.3 value-equal: 1.0.1 + hls-video-element@1.5.11: + dependencies: + custom-media-element: 1.4.6 + hls.js: 1.6.16 + media-tracks: 0.3.5 + + hls.js@1.6.16: {} + hogan.js@3.0.2: dependencies: mkdirp: 0.3.0 @@ -22661,6 +22689,8 @@ snapshots: transitivePeerDependencies: - react + media-tracks@0.3.5: {} + media-typer@0.3.0: {} media-typer@1.1.0: {} diff --git a/server/src/dtos/server.dto.ts b/server/src/dtos/server.dto.ts index c770ec12ca..6370557785 100644 --- a/server/src/dtos/server.dto.ts +++ b/server/src/dtos/server.dto.ts @@ -144,6 +144,7 @@ const ServerFeaturesSchema = z search: z.boolean().describe('Whether search is enabled'), email: z.boolean().describe('Whether email notifications are enabled'), ocr: z.boolean().describe('Whether OCR is enabled'), + realtimeTranscoding: z.boolean().describe('Whether real-time transcoding is enabled'), }) .meta({ id: 'ServerFeaturesDto' }); diff --git a/server/src/services/server.service.spec.ts b/server/src/services/server.service.spec.ts index 6e1187a900..e02945d015 100644 --- a/server/src/services/server.service.spec.ts +++ b/server/src/services/server.service.spec.ts @@ -148,6 +148,7 @@ describe(ServerService.name, () => { configFile: false, trash: true, email: false, + realtimeTranscoding: false, }); expect(mocks.systemMetadata.get).toHaveBeenCalled(); }); diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index ff9e90f7e0..aeeb41fcb0 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -86,7 +86,7 @@ export class ServerService extends BaseService { } async getFeatures(): Promise { - const { reverseGeocoding, metadata, map, machineLearning, trash, oauth, passwordLogin, notifications } = + const { reverseGeocoding, metadata, map, machineLearning, trash, oauth, passwordLogin, notifications, ffmpeg } = await this.getConfig({ withCache: false }); const { configFile } = this.configRepository.getEnv(); @@ -106,6 +106,7 @@ export class ServerService extends BaseService { passwordLogin: passwordLogin.enabled, configFile: !!configFile, email: notifications.smtp.enabled, + realtimeTranscoding: ffmpeg.realtime.enabled, }; } diff --git a/web/package.json b/web/package.json index 2fb37daaed..36f5cf9647 100644 --- a/web/package.json +++ b/web/package.json @@ -46,6 +46,8 @@ "geojson": "^0.5.0", "handlebars": "^4.7.8", "happy-dom": "^20.0.0", + "hls-video-element": "^1.5.11", + "hls.js": "^1.6.16", "intl-messageformat": "^11.0.0", "justified-layout": "^4.1.0", "lodash-es": "^4.17.21", diff --git a/web/src/lib/components/asset-viewer/VideoNativeViewer.svelte b/web/src/lib/components/asset-viewer/VideoNativeViewer.svelte index 295c5842a0..8f84466295 100644 --- a/web/src/lib/components/asset-viewer/VideoNativeViewer.svelte +++ b/web/src/lib/components/asset-viewer/VideoNativeViewer.svelte @@ -5,7 +5,7 @@ import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte'; import { castManager } from '$lib/managers/cast-manager.svelte'; import { autoPlayVideo, lang, loopVideo as loopVideoPreference } from '$lib/stores/preferences.store'; - import { getAssetMediaUrl, getAssetPlaybackUrl } from '$lib/utils'; + import { getAssetHlsSessionUrl, getAssetHlsUrl, getAssetMediaUrl, getAssetPlaybackUrl } from '$lib/utils'; import { AssetMediaSize, type AssetResponseDto } from '@immich/sdk'; import { Icon, LoadingSpinner } from '@immich/ui'; import { @@ -21,6 +21,9 @@ mdiVolumeMedium, mdiVolumeMute, } from '@mdi/js'; + import Hls, { AbrController, Events, type FragLoadedData, type FragLoadingData, type HlsConfig } from 'hls.js'; + import 'hls-video-element'; + import type HlsVideoElement from 'hls-video-element'; import 'media-chrome/media-control-bar'; import 'media-chrome/media-controller'; import 'media-chrome/media-fullscreen-button'; @@ -28,9 +31,10 @@ import 'media-chrome/media-play-button'; import 'media-chrome/media-playback-rate-button'; import 'media-chrome/media-time-display'; - import 'media-chrome/media-time-range'; + import './immich-time-range'; import 'media-chrome/media-volume-range'; import 'media-chrome/menu/media-playback-rate-menu'; + import 'media-chrome/menu/media-rendition-menu'; import 'media-chrome/menu/media-settings-menu'; import 'media-chrome/menu/media-settings-menu-button'; import 'media-chrome/menu/media-settings-menu-item'; @@ -38,6 +42,8 @@ import { useSwipe, type SwipeCustomEvent } from 'svelte-gestures'; import { t } from 'svelte-i18n'; import { fade } from 'svelte/transition'; + import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte'; + import { mediaCapabilitiesManager } from '$lib/managers/media-capabilities-manager.svelte'; interface Props { asset: AssetResponseDto; @@ -69,14 +75,155 @@ let videoPlayer: HTMLVideoElement | undefined = $state(); let isLoading = $state(true); - let assetFileUrl = $derived( - playOriginalVideo - ? getAssetMediaUrl({ id: assetId, size: AssetMediaSize.Original, cacheKey }) - : getAssetPlaybackUrl({ id: assetId, cacheKey }), - ); + let assetFileUrl = $derived.by(() => { + if (featureFlagsManager.value.realtimeTranscoding) { + return getAssetHlsUrl(assetId); + } + + if (playOriginalVideo) { + return getAssetMediaUrl({ id: assetId, size: AssetMediaSize.Original, cacheKey }); + } + + return getAssetPlaybackUrl({ id: assetId, cacheKey }); + }); const aspectRatio = $derived(asset.width && asset.height ? `${asset.width} / ${asset.height}` : undefined); let showVideo = $state(false); let hasFocused = $state(false); + let activeSession: { assetId: string; id: string } | undefined; + let rebuildCount = 0; + + const MAX_REBUILDS = 1; + const SESSION_ID_REGEX = /\/video\/stream\/([0-9a-f-]{36})\//; + + // hls.js can abandon fetching an in-flight fragment if it thinks it'll take too long, in which case + // it emergency switches to a different variant. This extends the delay even further due to + // cold starting another transcode, so let the fragment finish and have steady ABR decide the next level. + // + // It can also emergency switch between fragments: while a switch's first segment is still loading, + // it can run out of buffer and drop to a lower level for just one segment before continuing at the switched quality. + // This can cause multiple redundant transcoding restarts when it occurs. + // Hold the committed level until its first fragment lands, then resume normal ABR. + class NoAbandonAbrController extends AbrController { + private switchTarget = -1; + + protected override onFragLoading(_event: Events.FRAG_LOADING, data: FragLoadingData) { + if (data.frag.sn === 'initSegment') { + this.switchTarget = data.frag.level; + } + } + + protected override onFragLoaded(event: Events.FRAG_LOADED, data: FragLoadedData) { + if (data.frag.sn !== 'initSegment') { + this.switchTarget = -1; + } + super.onFragLoaded(event, data); + } + + override get nextAutoLevel(): number { + const level = super.nextAutoLevel; + const target = this.hls.levels[this.switchTarget]; + // Hold the committed level, but only while hls.js still considers it healthy. + if (target && level < this.switchTarget && target.loadError === 0 && target.fragmentError === 0) { + return this.switchTarget; + } + return level; + } + + override set nextAutoLevel(level: number) { + super.nextAutoLevel = level; + } + } + + const hlsConfig: Partial = { + abrController: NoAbandonAbrController, + highBufferWatchdogPeriod: 10, + detectStallWithCurrentTimeMs: 10_000, + maxBufferHole: 0.5, + maxBufferLength: 30, + maxMaxBufferLength: 60, + fragLoadPolicy: { + default: { + maxTimeToFirstByteMs: 30_000, + maxLoadTimeMs: 60_000, + timeoutRetry: { maxNumRetry: 5, retryDelayMs: 100, maxRetryDelayMs: 0 }, + errorRetry: { maxNumRetry: 3, retryDelayMs: 1000, maxRetryDelayMs: 8000 }, + }, + }, + useMediaCapabilities: false, + }; + + const releaseSession = () => { + const session = activeSession; + if (!session) { + return; + } + activeSession = undefined; + const url = getAssetHlsSessionUrl(session.assetId, session.id); + void fetch(url, { method: 'DELETE' }).catch(() => console.warn('Failed to release HLS session', session)); + }; + + const isHlsElement = (el: HTMLVideoElement | undefined): el is HlsVideoElement => { + return el?.tagName === 'HLS-VIDEO'; + }; + + const wireHlsListeners = (el: HlsVideoElement, assetId: string, resumeTime?: number) => { + const api = el.api; + if (!api) { + return; + } + + // This is a hack to make the rendition menu use `api.currentLevel` instead of `api.nextLevel`. + // `api.nextLevel` makes the player request the next segment followed by the current segment. + // That backward request causes the server to restart transcoding for no reason. + Object.defineProperty(api, 'nextLevel', { + configurable: true, + get: () => api.currentLevel, + set: (level: number) => { + api.currentLevel = level; + }, + }); + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + api.on(Hls.Events.MANIFEST_PARSED, async () => { + // Defer hls.js's first fragment load until we filter out suboptimal variants + api.stopLoad(); + const id = api.levels[0]?.url[0]?.match(SESSION_ID_REGEX)?.[1]; + if (id) { + activeSession = { assetId, id }; + } + + const keep = await mediaCapabilitiesManager.efficientLevels(api.levels); + for (let i = api.levels.length - 1; i >= 0; i--) { + if (!keep.has(i)) { + api.removeLevel(i); + } + } + + api.startLoad(resumeTime); + }); + + api.on(Hls.Events.FRAG_LOADED, () => (rebuildCount = 0)); + + api.on(Hls.Events.ERROR, (_, data) => { + // 404 on a fragment can mean the server-side session has expired. Refetch + // master for a new session, but give up if it still 404s. + if ( + !data.fatal || + data.details !== Hls.ErrorDetails.FRAG_LOAD_ERROR || + data.response?.code !== 404 || + rebuildCount++ >= MAX_REBUILDS + ) { + console.error('HLS error', JSON.stringify(data)); + return; + } + console.warn('Error loading segment, starting new session'); + activeSession = undefined; + resumeTime = el.currentTime; + el.load(); + // wireHlsListeners must run after el.api is repopulated. + queueMicrotask(() => wireHlsListeners(el, assetId, resumeTime)); + }); + }; onMount(() => { showVideo = true; @@ -84,10 +231,31 @@ $effect(() => { // reactive on `assetFileUrl` changes - if (assetFileUrl) { + if (videoPlayer && assetFileUrl) { hasFocused = false; - videoPlayer?.load(); + rebuildCount = 0; + releaseSession(); + if (isHlsElement(videoPlayer)) { + videoPlayer.config = hlsConfig; + videoPlayer.src = assetFileUrl; + const el = videoPlayer; + queueMicrotask(() => wireHlsListeners(el, assetId)); + } else { + videoPlayer.load(); + } } + return releaseSession; + }); + + const onPagehide = (event: PageTransitionEvent) => { + if (!event.persisted) { + releaseSession(); + } + }; + + $effect(() => { + window.addEventListener('pagehide', onPagehide); + return () => window.removeEventListener('pagehide', onPagehide); }); onDestroy(() => { @@ -144,6 +312,10 @@ videoPlayer?.pause(); } }); + + // The time is only refreshed on HLS fragment decode by default, + // so manually emit events on seek to update it immediately. + const onSeeking = (event: Event) => event.currentTarget?.dispatchEvent(new Event('timeupdate')); {#if showVideo} @@ -172,27 +344,51 @@ style:aspect-ratio={aspectRatio} defaultduration={asset.duration! / 1000} > - + {#if featureFlagsManager.value.realtimeTranscoding} + handleCanPlay(e.currentTarget as HTMLVideoElement)} + onended={onVideoEnded} + onseeking={onSeeking} + onplaying={(e: Event) => { + if (!hasFocused) { + (e.currentTarget as HTMLElement).focus(); + hasFocused = true; + } + }} + onclose={onClose} + poster={getAssetMediaUrl({ id: asset.id, size: AssetMediaSize.Preview, cacheKey })} + > + {:else} + + {/if} {#if extendedControls} {/if} @@ -238,7 +444,7 @@ {/if} - +
@@ -248,7 +454,7 @@
{/if} - {#if assetViewerManager.isFaceEditMode} + {#if assetViewerManager.isFaceEditMode && videoPlayer} {/if} {/if} @@ -291,12 +497,12 @@ font-variant-numeric: tabular-nums; } - media-time-range, + immich-time-range, media-volume-range { --media-control-hover-background: none; } - media-time-range:hover, + immich-time-range:hover, media-volume-range:hover { --media-range-thumb-opacity: 1; } diff --git a/web/src/lib/components/asset-viewer/immich-time-range.ts b/web/src/lib/components/asset-viewer/immich-time-range.ts new file mode 100644 index 0000000000..a3de131e73 --- /dev/null +++ b/web/src/lib/components/asset-viewer/immich-time-range.ts @@ -0,0 +1,54 @@ +import { MediaUIEvents } from 'media-chrome/constants'; +import MediaTimeRange from 'media-chrome/media-time-range'; + +const COMMIT_DELAY_MS = 750; + +/** Custom MediaTimeRange that only seeks after pointer release to avoid hammering the server. + * Keyboard input uses timed debouncing instead since there's no release event. */ +class ImmichTimeRange extends MediaTimeRange { + private seeking = false; + private pending: number | undefined; + private idleTimer: ReturnType | undefined; + + override connectedCallback() { + super.connectedCallback(); + this.addEventListener('pointerdown', this.hold); + this.addEventListener('keydown', this.hold); + this.addEventListener('pointerup', this.release); + this.addEventListener('pointercancel', this.release); + this.addEventListener(MediaUIEvents.MEDIA_SEEK_REQUEST, this.intercept, { capture: true }); + } + + private hold(event: Event) { + if (event instanceof KeyboardEvent) { + if (!this.keysUsed.includes(event.key)) { + return; + } + clearTimeout(this.idleTimer); + this.idleTimer = setTimeout(this.release, COMMIT_DELAY_MS); + } + this.seeking = true; + } + + private intercept(event: Event) { + if (!this.seeking) { + return; // not mid-scrub, or this is the request we replay in release() + } + this.pending = (event as CustomEvent).detail; + event.stopImmediatePropagation(); + } + + private release() { + clearTimeout(this.idleTimer); + this.seeking = false; + if (this.pending !== undefined) { + const detail = this.pending; + this.pending = undefined; + this.dispatchEvent(new CustomEvent(MediaUIEvents.MEDIA_SEEK_REQUEST, { bubbles: true, composed: true, detail })); + } + } +} + +if (!globalThis.customElements.get('immich-time-range')) { + globalThis.customElements.define('immich-time-range', ImmichTimeRange); +} diff --git a/web/src/lib/managers/media-capabilities-manager.svelte.ts b/web/src/lib/managers/media-capabilities-manager.svelte.ts new file mode 100644 index 0000000000..ccabc6680d --- /dev/null +++ b/web/src/lib/managers/media-capabilities-manager.svelte.ts @@ -0,0 +1,92 @@ +export type Level = { videoCodec?: string; width: number; height: number; bitrate: number; frameRate: number }; + +export const DEFAULT_DECODING_INFO: MediaCapabilitiesDecodingInfo = { + powerEfficient: true, + smooth: true, + supported: true, + keySystemAccess: null, +}; + +class MediaCapabilitiesManager { + private cache = new Map>(); + + init() { + for (const level of [ + { videoCodec: 'av01.0.04M.08', width: 854, height: 480, bitrate: 1_000_000, frameRate: 60 }, + { videoCodec: 'hvc1.1.6.L90.B0', width: 854, height: 480, bitrate: 1_200_000, frameRate: 60 }, + { videoCodec: 'av01.0.08M.08', width: 1280, height: 720, bitrate: 2_000_000, frameRate: 60 }, + { videoCodec: 'hvc1.1.6.L93.B0', width: 1280, height: 720, bitrate: 2_500_000, frameRate: 60 }, + { videoCodec: 'av01.0.09M.08', width: 1920, height: 1080, bitrate: 4_000_000, frameRate: 60 }, + { videoCodec: 'hvc1.1.6.L120.B0', width: 1920, height: 1080, bitrate: 4_500_000, frameRate: 60 }, + { videoCodec: 'av01.0.12M.08', width: 2560, height: 1440, bitrate: 7_000_000, frameRate: 60 }, + { videoCodec: 'hvc1.2.4.L150.B0', width: 2560, height: 1440, bitrate: 8_000_000, frameRate: 60 }, + ]) { + this.cache.set(this.cacheKey(level), this.queryDecodingInfo(level)); + } + + for (const level of [ + { videoCodec: 'avc1.64001e', width: 854, height: 480, bitrate: 2_500_000, frameRate: 60 }, + { videoCodec: 'avc1.64001f', width: 1280, height: 720, bitrate: 5_000_000, frameRate: 60 }, + { videoCodec: 'avc1.640028', width: 1920, height: 1080, bitrate: 8_000_000, frameRate: 60 }, + { videoCodec: 'avc1.640032', width: 2560, height: 1440, bitrate: 16_000_000, frameRate: 60 }, + ]) { + this.cache.set(this.cacheKey(level), Promise.resolve(DEFAULT_DECODING_INFO)); + } + } + + async efficientLevels(levels: Level[]) { + const decodingInfo = await Promise.all(levels.map((level) => this.decodingInfo(level))); + // eslint-disable-next-line svelte/prefer-svelte-reactivity + const lowestBitrateByHeight = new Map(); + for (let i = 0; i < levels.length; i++) { + if (!decodingInfo[i].powerEfficient) { + continue; + } + + const { bitrate, height } = levels[i]; + const cur = lowestBitrateByHeight.get(height); + if (cur === undefined || bitrate < levels[cur].bitrate) { + lowestBitrateByHeight.set(height, i); + } + } + + return new Set(lowestBitrateByHeight.values()); + } + + decodingInfo(level: Level) { + const key = this.cacheKey(level); + const existing = this.cache.get(key); + if (existing) { + return existing; + } + const promise = this.queryDecodingInfo(level); + this.cache.set(key, promise); + return promise; + } + + private async queryDecodingInfo(level: Level) { + try { + return await navigator.mediaCapabilities.decodingInfo({ + type: 'media-source', + video: { + contentType: `video/mp4; codecs="${level.videoCodec}"`, + width: level.width, + height: level.height, + bitrate: level.bitrate, + framerate: level.frameRate, + }, + }); + } catch { + return DEFAULT_DECODING_INFO; + } + } + + private cacheKey({ videoCodec, width, height, frameRate }: Level) { + const resolution = Math.min(width, height); + const fpsBucket = Math.trunc(frameRate / 61) * 60; + return `${videoCodec}|${resolution}|${fpsBucket}`; + } +} + +export const mediaCapabilitiesManager = new MediaCapabilitiesManager(); +mediaCapabilitiesManager.init(); diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index 9af417bb19..4ff3564ffa 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -244,6 +244,14 @@ export const getAssetPlaybackUrl = (options: AssetUrlOptions) => { return createUrl(getAssetPlaybackPath(id), { ...authManager.params, c }); }; +export const getAssetHlsUrl = (id: string) => { + return createUrl(`/assets/${id}/video/stream/main.m3u8`, authManager.params); +}; + +export const getAssetHlsSessionUrl = (id: string, sessionId: string) => { + return createUrl(`/assets/${id}/video/stream/${sessionId}`, authManager.params); +}; + export const getProfileImageUrl = (user: UserResponseDto) => createUrl(getUserProfileImagePath(user.id), { updatedAt: user.profileChangedAt }); From 728e92ea334d79eed7779d1e34798b4e1d84ce44 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 00:37:10 -0400 Subject: [PATCH 27/50] chore(deps): update dependency @immich/ui to v0.79.3 (#28758) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 90 +++++++++++++++++++++++--------------------------- 1 file changed, 42 insertions(+), 48 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index debdb79b93..326c2762f6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -615,7 +615,7 @@ importers: version: 10.0.1(eslint@10.4.0(jiti@2.7.0)) '@nestjs/cli': specifier: ^11.0.2 - version: 11.0.21(@swc/core@1.15.33(@swc/helpers@0.5.22))(@types/node@24.12.4)(esbuild@0.28.0)(lightningcss@1.32.0)(prettier@3.8.3) + version: 11.0.21(@swc/core@1.15.33(@swc/helpers@0.5.23))(@types/node@24.12.4)(esbuild@0.28.0)(lightningcss@1.32.0)(prettier@3.8.3) '@nestjs/schematics': specifier: ^11.0.0 version: 11.1.0(chokidar@4.0.3)(prettier@3.8.3)(typescript@6.0.3) @@ -624,7 +624,7 @@ importers: version: 11.1.21(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(@nestjs/platform-express@11.1.21) '@swc/core': specifier: ^1.4.14 - version: 1.15.33(@swc/helpers@0.5.22) + version: 1.15.33(@swc/helpers@0.5.23) '@types/archiver': specifier: ^7.0.0 version: 7.0.0 @@ -744,7 +744,7 @@ importers: version: 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) unplugin-swc: specifier: ^1.4.5 - version: 1.5.9(@swc/core@1.15.33(@swc/helpers@0.5.22))(rollup@4.60.4) + version: 1.5.9(@swc/core@1.15.33(@swc/helpers@0.5.23))(rollup@4.60.4) vite-tsconfig-paths: specifier: ^6.0.0 version: 6.1.1(typescript@6.0.3)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)) @@ -765,7 +765,7 @@ importers: version: link:../packages/sdk '@immich/ui': specifier: ^0.79.2 - version: 0.79.2(@sveltejs/kit@2.60.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.55.8(@typescript-eslint/types@8.59.4))(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4))(typescript@6.0.3)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4)) + version: 0.79.3(@sveltejs/kit@2.60.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.55.8(@typescript-eslint/types@8.59.4))(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4))(typescript@6.0.3)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4)) '@mapbox/mapbox-gl-rtl-text': specifier: 0.4.0 version: 0.4.0 @@ -1699,10 +1699,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.29.2': - resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} - engines: {node: '>=6.9.0'} - '@babel/runtime@7.29.7': resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} @@ -3220,8 +3216,8 @@ packages: resolution: {integrity: sha512-O1SJ+BbeFVsUTF4af1MfagJZM+lPgLjI8lQ3SZNjpo8SGJReSbUl2ii03OKuGni/G0yp2GnRLpOTNSHYGtVrcg==} hasBin: true - '@immich/ui@0.79.2': - resolution: {integrity: sha512-tnpYhYHrjrFJK18QglRMzPUtHv6q5V6tW38HiAraQJBv7MCg+yaJDrdF8omM2L5F311FGlv1PZLJYvmR4e49PA==} + '@immich/ui@0.79.3': + resolution: {integrity: sha512-QO2gLcmAIVLvcv3eb6RZ5kDahmVMDZlfE2RfbpyRKNI1wTjvTLP5ibIsofY0fXxLf44cN3vcjmz16CXS8bvVWQ==} peerDependencies: '@sveltejs/kit': ^2.13.0 svelte: ^5.0.0 @@ -3369,8 +3365,8 @@ packages: '@types/node': optional: true - '@internationalized/date@3.12.1': - resolution: {integrity: sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ==} + '@internationalized/date@3.12.2': + resolution: {integrity: sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==} '@ioredis/commands@1.5.1': resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==} @@ -4999,8 +4995,8 @@ packages: '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - '@swc/helpers@0.5.22': - resolution: {integrity: sha512-/e2Ly3Docn9kYByap6TV4oquJ3wQuz3c+kC74riqtkwU9CwTMeuj6t2rW+bRr4pyOx/CYQM4wr0RgaKQwGEz0A==} + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} '@swc/types@0.1.26': resolution: {integrity: sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==} @@ -13819,8 +13815,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/runtime@7.29.2': {} - '@babel/runtime@7.29.7': {} '@babel/template@7.28.6': @@ -14264,7 +14258,7 @@ snapshots: '@babel/preset-env': 7.29.5(@babel/core@7.29.0) '@babel/preset-react': 7.28.5(@babel/core@7.29.0) '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 '@babel/traverse': 7.29.0 '@docusaurus/logger': 3.10.1 '@docusaurus/utils': 3.10.1(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -14298,7 +14292,7 @@ snapshots: '@babel/preset-env': 7.29.5(@babel/core@7.29.0) '@babel/preset-react': 7.28.5(@babel/core@7.29.0) '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 '@babel/traverse': 7.29.0 '@docusaurus/logger': 3.10.1 '@docusaurus/utils': 3.10.1(postcss@8.5.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -14404,7 +14398,7 @@ snapshots: react-router: 5.3.4(react@19.2.6) react-router-config: 5.1.1(react-router@5.3.4(react@19.2.6))(react@19.2.6) react-router-dom: 5.3.4(react@19.2.6) - semver: 7.8.0 + semver: 7.8.1 serve-handler: 6.1.7 tinypool: 1.1.1 tslib: 2.8.1 @@ -15915,12 +15909,12 @@ snapshots: pg-connection-string: 2.13.0 postgres: 3.4.9 - '@immich/ui@0.79.2(@sveltejs/kit@2.60.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.55.8(@typescript-eslint/types@8.59.4))(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4))(typescript@6.0.3)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4))': + '@immich/ui@0.79.3(@sveltejs/kit@2.60.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.55.8(@typescript-eslint/types@8.59.4))(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4))(typescript@6.0.3)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4))': dependencies: - '@internationalized/date': 3.12.1 + '@internationalized/date': 3.12.2 '@mdi/js': 7.4.47 '@sveltejs/kit': 2.60.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.55.8(@typescript-eslint/types@8.59.4))(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4))(typescript@6.0.3)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)) - bits-ui: 2.18.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.60.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.55.8(@typescript-eslint/types@8.59.4))(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4))(typescript@6.0.3)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4)) + bits-ui: 2.18.1(@internationalized/date@3.12.2)(@sveltejs/kit@2.60.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.55.8(@typescript-eslint/types@8.59.4))(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4))(typescript@6.0.3)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4)) luxon: 3.7.2 simple-icons: 16.20.0 svelte: 5.55.8(@typescript-eslint/types@8.59.4) @@ -16069,9 +16063,9 @@ snapshots: optionalDependencies: '@types/node': 24.12.4 - '@internationalized/date@3.12.1': + '@internationalized/date@3.12.2': dependencies: - '@swc/helpers': 0.5.22 + '@swc/helpers': 0.5.23 '@ioredis/commands@1.5.1': {} @@ -16474,7 +16468,7 @@ snapshots: bullmq: 5.76.10 tslib: 2.8.1 - '@nestjs/cli@11.0.21(@swc/core@1.15.33(@swc/helpers@0.5.22))(@types/node@24.12.4)(esbuild@0.28.0)(lightningcss@1.32.0)(prettier@3.8.3)': + '@nestjs/cli@11.0.21(@swc/core@1.15.33(@swc/helpers@0.5.23))(@types/node@24.12.4)(esbuild@0.28.0)(lightningcss@1.32.0)(prettier@3.8.3)': dependencies: '@angular-devkit/core': 19.2.24(chokidar@4.0.3) '@angular-devkit/schematics': 19.2.24(chokidar@4.0.3) @@ -16485,17 +16479,17 @@ snapshots: chokidar: 4.0.3 cli-table3: 0.6.5 commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.22))(esbuild@0.28.0)(lightningcss@1.32.0)) + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.23))(esbuild@0.28.0)(lightningcss@1.32.0)) glob: 13.0.6 node-emoji: 1.11.0 ora: 5.4.1 tsconfig-paths: 4.2.0 tsconfig-paths-webpack-plugin: 4.2.0 typescript: 5.9.3 - webpack: 5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.22))(esbuild@0.28.0)(lightningcss@1.32.0) + webpack: 5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.23))(esbuild@0.28.0)(lightningcss@1.32.0) webpack-node-externals: 3.0.0 optionalDependencies: - '@swc/core': 1.15.33(@swc/helpers@0.5.22) + '@swc/core': 1.15.33(@swc/helpers@0.5.23) transitivePeerDependencies: - '@minify-html/node' - '@swc/css' @@ -17684,7 +17678,7 @@ snapshots: '@swc/core-win32-x64-msvc@1.15.33': optional: true - '@swc/core@1.15.33(@swc/helpers@0.5.22)': + '@swc/core@1.15.33(@swc/helpers@0.5.23)': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.26 @@ -17701,11 +17695,11 @@ snapshots: '@swc/core-win32-arm64-msvc': 1.15.33 '@swc/core-win32-ia32-msvc': 1.15.33 '@swc/core-win32-x64-msvc': 1.15.33 - '@swc/helpers': 0.5.22 + '@swc/helpers': 0.5.23 '@swc/counter@0.1.3': {} - '@swc/helpers@0.5.22': + '@swc/helpers@0.5.23': dependencies: tslib: 2.8.1 @@ -19110,11 +19104,11 @@ snapshots: binary-extensions@2.3.0: {} - bits-ui@2.18.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.60.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.55.8(@typescript-eslint/types@8.59.4))(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4))(typescript@6.0.3)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4)): + bits-ui@2.18.1(@internationalized/date@3.12.2)(@sveltejs/kit@2.60.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.55.8(@typescript-eslint/types@8.59.4))(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4))(typescript@6.0.3)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4)): dependencies: '@floating-ui/core': 1.7.5 '@floating-ui/dom': 1.7.6 - '@internationalized/date': 3.12.1 + '@internationalized/date': 3.12.2 esm-env: 1.2.2 runed: 0.35.1(@sveltejs/kit@2.60.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.55.8(@typescript-eslint/types@8.59.4))(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4))(typescript@6.0.3)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4)) svelte: 5.55.8(@typescript-eslint/types@8.59.4) @@ -20681,7 +20675,7 @@ snapshots: pluralize: 8.0.0 regexp-tree: 0.1.27 regjsparser: 0.13.1 - semver: 7.8.0 + semver: 7.8.1 strip-indent: 4.1.1 eslint-scope@5.1.1: @@ -21123,7 +21117,7 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.22))(esbuild@0.28.0)(lightningcss@1.32.0)): + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.23))(esbuild@0.28.0)(lightningcss@1.32.0)): dependencies: '@babel/code-frame': 7.29.0 chalk: 4.1.2 @@ -21138,7 +21132,7 @@ snapshots: semver: 7.8.1 tapable: 2.3.3 typescript: 5.9.3 - webpack: 5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.22))(esbuild@0.28.0)(lightningcss@1.32.0) + webpack: 5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.23))(esbuild@0.28.0)(lightningcss@1.32.0) form-data-encoder@2.1.4: {} @@ -24521,19 +24515,19 @@ snapshots: react-loadable-ssr-addon-v5-slorber@1.0.3(@docusaurus/react-loadable@6.0.0(react@19.2.6))(webpack@5.107.0(postcss@8.5.15)): dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.6)' webpack: 5.107.0(postcss@8.5.15) react-router-config@5.1.1(react-router@5.3.4(react@19.2.6))(react@19.2.6): dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 react: 19.2.6 react-router: 5.3.4(react@19.2.6) react-router-dom@5.3.4(react@19.2.6): dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 history: 4.10.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -24544,7 +24538,7 @@ snapshots: react-router@5.3.4(react@19.2.6): dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 history: 4.10.1 hoist-non-react-statics: 3.3.2 loose-envify: 1.4.0 @@ -25134,7 +25128,7 @@ snapshots: detect-libc: 2.1.2 node-addon-api: 8.7.0 node-gyp: 12.3.0 - semver: 7.8.0 + semver: 7.8.1 optionalDependencies: '@img/sharp-darwin-arm64': 0.34.5 '@img/sharp-darwin-x64': 0.34.5 @@ -25812,15 +25806,15 @@ snapshots: - bare-abort-controller - react-native-b4a - terser-webpack-plugin@5.6.0(@swc/core@1.15.33(@swc/helpers@0.5.22))(esbuild@0.28.0)(lightningcss@1.32.0)(webpack@5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.22))(esbuild@0.28.0)(lightningcss@1.32.0)): + terser-webpack-plugin@5.6.0(@swc/core@1.15.33(@swc/helpers@0.5.23))(esbuild@0.28.0)(lightningcss@1.32.0)(webpack@5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.23))(esbuild@0.28.0)(lightningcss@1.32.0)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.47.1 - webpack: 5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.22))(esbuild@0.28.0)(lightningcss@1.32.0) + webpack: 5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.23))(esbuild@0.28.0)(lightningcss@1.32.0) optionalDependencies: - '@swc/core': 1.15.33(@swc/helpers@0.5.22) + '@swc/core': 1.15.33(@swc/helpers@0.5.23) esbuild: 0.28.0 lightningcss: 1.32.0 @@ -26227,10 +26221,10 @@ snapshots: unpipe@1.0.0: {} - unplugin-swc@1.5.9(@swc/core@1.15.33(@swc/helpers@0.5.22))(rollup@4.60.4): + unplugin-swc@1.5.9(@swc/core@1.15.33(@swc/helpers@0.5.23))(rollup@4.60.4): dependencies: '@rollup/pluginutils': 5.3.0(rollup@4.60.4) - '@swc/core': 1.15.33(@swc/helpers@0.5.22) + '@swc/core': 1.15.33(@swc/helpers@0.5.23) load-tsconfig: 0.2.5 unplugin: 2.3.11 transitivePeerDependencies: @@ -26629,7 +26623,7 @@ snapshots: webpack-virtual-modules@0.6.2: {} - webpack@5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.22))(esbuild@0.28.0)(lightningcss@1.32.0): + webpack@5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.23))(esbuild@0.28.0)(lightningcss@1.32.0): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.9 @@ -26653,7 +26647,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.3 - terser-webpack-plugin: 5.6.0(@swc/core@1.15.33(@swc/helpers@0.5.22))(esbuild@0.28.0)(lightningcss@1.32.0)(webpack@5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.22))(esbuild@0.28.0)(lightningcss@1.32.0)) + terser-webpack-plugin: 5.6.0(@swc/core@1.15.33(@swc/helpers@0.5.23))(esbuild@0.28.0)(lightningcss@1.32.0)(webpack@5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.23))(esbuild@0.28.0)(lightningcss@1.32.0)) watchpack: 2.5.1 webpack-sources: 3.4.1 transitivePeerDependencies: From d57a152040e0e23cbbcd9f403b1c4ab4e9e3058e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 00:37:42 -0400 Subject: [PATCH 28/50] chore(deps): update prom/prometheus docker digest to 69f5241 (#28757) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docker/docker-compose.prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 24ecb02624..33d17887cf 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -85,7 +85,7 @@ services: container_name: immich_prometheus ports: - 9090:9090 - image: prom/prometheus@sha256:e4254400b85610324913f0dc4acf92603d9984e7519414c5a12811aa6146acc3 + image: prom/prometheus@sha256:69f5241418838263316593f7274a304b095c40bcf22e57272865da91bd60a8ac volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml - prometheus-data:/prometheus From 2afde23a5d1140b04a1614511143f21c8b075937 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 00:39:19 -0400 Subject: [PATCH 29/50] chore(deps): update github-actions (#28750) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-mobile.yml | 6 ++-- .github/workflows/cache-cleanup.yml | 2 +- .github/workflows/check-openapi.yml | 2 +- .github/workflows/cli.yml | 14 +++++----- .github/workflows/close-duplicates.yml | 2 +- .github/workflows/codeql-analysis.yml | 8 +++--- .github/workflows/docker.yml | 6 ++-- .github/workflows/docs-build.yml | 4 +-- .github/workflows/docs-deploy.yml | 4 +-- .github/workflows/docs-destroy.yml | 2 +- .github/workflows/fix-format.yml | 2 +- .github/workflows/pr-label-validation.yml | 2 +- .github/workflows/pr-labeler.yml | 2 +- .github/workflows/prepare-release.yml | 2 +- .github/workflows/preview-label.yaml | 4 +-- .github/workflows/sdk.yml | 2 +- .github/workflows/static_analysis.yml | 4 +-- .github/workflows/test.yml | 34 +++++++++++------------ .github/workflows/weblate-lock.yml | 4 +-- 19 files changed, 53 insertions(+), 53 deletions(-) diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index 1f3b99ef38..2f1446c6e5 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -51,7 +51,7 @@ jobs: should_run: ${{ steps.check.outputs.should_run }} steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -79,7 +79,7 @@ jobs: steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -201,7 +201,7 @@ jobs: steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} diff --git a/.github/workflows/cache-cleanup.yml b/.github/workflows/cache-cleanup.yml index 4ecd758f6d..aa801d851a 100644 --- a/.github/workflows/cache-cleanup.yml +++ b/.github/workflows/cache-cleanup.yml @@ -19,7 +19,7 @@ jobs: actions: write steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} diff --git a/.github/workflows/check-openapi.yml b/.github/workflows/check-openapi.yml index 0ce156a0bc..f2b3e3c248 100644 --- a/.github/workflows/check-openapi.yml +++ b/.github/workflows/check-openapi.yml @@ -24,7 +24,7 @@ jobs: persist-credentials: false - name: Check for breaking API changes - uses: oasdiff/oasdiff-action/breaking@6147a58e5d1249a12f42fc864ab791d571a30015 # v0.0.47 + uses: oasdiff/oasdiff-action/breaking@50e6a3413e5aa9c3ae4d8393c34745be44288b46 # v0.0.48 with: base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json revision: open-api/immich-openapi-specs.json diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index df70e8d151..3a17dbb579 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -31,7 +31,7 @@ jobs: working-directory: ./packages/cli steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -63,7 +63,7 @@ jobs: steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -75,13 +75,13 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Set up QEMU - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - name: Login to GitHub Container Registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 if: ${{ !github.event.pull_request.head.repo.fork }} with: registry: ghcr.io @@ -96,7 +96,7 @@ jobs: - name: Generate docker image tags id: metadata - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 + uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 with: flavor: | latest=false @@ -107,7 +107,7 @@ jobs: type=raw,value=latest,enable=${{ github.event_name == 'release' && !github.event.release.prerelease }} - name: Build and push image - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: file: packages/cli/Dockerfile platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/close-duplicates.yml b/.github/workflows/close-duplicates.yml index b0b5258048..590256058d 100644 --- a/.github/workflows/close-duplicates.yml +++ b/.github/workflows/close-duplicates.yml @@ -35,7 +35,7 @@ jobs: needs: [get_body, should_run] if: ${{ needs.should_run.outputs.should_run == 'true' }} container: - image: ghcr.io/immich-app/mdq:main@sha256:0a8b8867773a0f8368061f47578603f438349f8f1f28b0e16105f481e5c794e0 + image: ghcr.io/immich-app/mdq:main@sha256:e73f60195b39748c4876f23e3e6cd22a68a9754acec8aef1fd6979fd52cd2c9f outputs: checked: ${{ steps.get_checkbox.outputs.checked }} steps: diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index cebd9b1747..d10fd72335 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -44,7 +44,7 @@ jobs: steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -57,7 +57,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 + uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -70,7 +70,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 + uses: github/codeql-action/autobuild@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 # โ„น๏ธ Command-line programs to run using the OS shell. # ๐Ÿ“š See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -83,6 +83,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 + uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 with: category: '/language:${{matrix.language}}' diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a76d6fbaa2..133950fb0a 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -23,7 +23,7 @@ jobs: should_run: ${{ steps.check.outputs.should_run }} steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -60,7 +60,7 @@ jobs: suffix: ['', '-cuda', '-rocm', '-openvino', '-armnn', '-rknn'] steps: - name: Login to GitHub Container Registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -90,7 +90,7 @@ jobs: suffix: [''] steps: - name: Login to GitHub Container Registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: ghcr.io username: ${{ github.repository_owner }} diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml index 23f16c4c47..70ea4cb9e4 100644 --- a/.github/workflows/docs-build.yml +++ b/.github/workflows/docs-build.yml @@ -21,7 +21,7 @@ jobs: should_run: ${{ steps.check.outputs.should_run }} steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -54,7 +54,7 @@ jobs: steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 5438242b50..dd6ba4256e 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -20,7 +20,7 @@ jobs: artifact: ${{ steps.get-artifact.outputs.result }} steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -126,7 +126,7 @@ jobs: if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }} steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} diff --git a/.github/workflows/docs-destroy.yml b/.github/workflows/docs-destroy.yml index b1d75f241c..19554b5d20 100644 --- a/.github/workflows/docs-destroy.yml +++ b/.github/workflows/docs-destroy.yml @@ -17,7 +17,7 @@ jobs: pull-requests: write steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} diff --git a/.github/workflows/fix-format.yml b/.github/workflows/fix-format.yml index 23f23c1f4c..6e48e89fd7 100644 --- a/.github/workflows/fix-format.yml +++ b/.github/workflows/fix-format.yml @@ -15,7 +15,7 @@ jobs: pull-requests: write steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} diff --git a/.github/workflows/pr-label-validation.yml b/.github/workflows/pr-label-validation.yml index f5c1802bbc..d99ce46d29 100644 --- a/.github/workflows/pr-label-validation.yml +++ b/.github/workflows/pr-label-validation.yml @@ -14,7 +14,7 @@ jobs: pull-requests: write steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index 4df27e581e..3d1646a88d 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 2cb15eaf62..6a7c2c9cf4 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -49,7 +49,7 @@ jobs: permissions: {} # No job-level permissions are needed because it uses the app-token steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} diff --git a/.github/workflows/preview-label.yaml b/.github/workflows/preview-label.yaml index 94fb9f797c..736507fee9 100644 --- a/.github/workflows/preview-label.yaml +++ b/.github/workflows/preview-label.yaml @@ -14,7 +14,7 @@ jobs: pull-requests: write steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -32,7 +32,7 @@ jobs: pull-requests: write steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml index bb3b65646c..4bff026205 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -16,7 +16,7 @@ jobs: packages: write steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 6fdd7af18b..872d0c08c7 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -20,7 +20,7 @@ jobs: should_run: ${{ steps.check.outputs.should_run }} steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -49,7 +49,7 @@ jobs: working-directory: ./mobile steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 918e0771c3..fdaee15a59 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ jobs: should_run: ${{ steps.check.outputs.should_run }} steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -71,7 +71,7 @@ jobs: contents: read steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -102,7 +102,7 @@ jobs: working-directory: ./packages/cli steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -133,7 +133,7 @@ jobs: working-directory: ./packages/cli steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -177,7 +177,7 @@ jobs: working-directory: ./web steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -215,7 +215,7 @@ jobs: working-directory: ./web steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -243,7 +243,7 @@ jobs: contents: read steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -293,7 +293,7 @@ jobs: working-directory: ./e2e steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -325,7 +325,7 @@ jobs: working-directory: ./server steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -361,7 +361,7 @@ jobs: runner: [ubuntu-latest, ubuntu-24.04-arm] steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -438,7 +438,7 @@ jobs: runner: [ubuntu-latest, ubuntu-24.04-arm] steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -546,7 +546,7 @@ jobs: contents: read steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -583,7 +583,7 @@ jobs: working-directory: ./machine-learning steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -613,7 +613,7 @@ jobs: working-directory: ./.github steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -643,7 +643,7 @@ jobs: contents: read steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -664,7 +664,7 @@ jobs: contents: read steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -722,7 +722,7 @@ jobs: - 5432:5432 steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} diff --git a/.github/workflows/weblate-lock.yml b/.github/workflows/weblate-lock.yml index 7063820839..af321ce534 100644 --- a/.github/workflows/weblate-lock.yml +++ b/.github/workflows/weblate-lock.yml @@ -24,7 +24,7 @@ jobs: should_run: ${{ steps.check.outputs.should_run }} steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -47,7 +47,7 @@ jobs: if: ${{ fromJSON(needs.pre-job.outputs.should_run).i18n == true }} steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 + uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} From da8ed3eceb3acf8086d345f3c2b1b911eab16ee2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 08:09:27 +0000 Subject: [PATCH 30/50] chore(deps): update docker.io/valkey/valkey:9 docker digest to 4963247 (#28622) --- docker/docker-compose.dev.yml | 2 +- docker/docker-compose.prod.yml | 2 +- docker/docker-compose.rootless.yml | 2 +- docker/docker-compose.yml | 2 +- e2e/docker-compose.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index adcb6b628f..2398b971dd 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -154,7 +154,7 @@ services: redis: container_name: immich_redis - image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193 + image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9 healthcheck: test: redis-cli ping || exit 1 diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 33d17887cf..bad7527ca8 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -56,7 +56,7 @@ services: redis: container_name: immich_redis - image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193 + image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9 healthcheck: test: redis-cli ping || exit 1 restart: always diff --git a/docker/docker-compose.rootless.yml b/docker/docker-compose.rootless.yml index 3f3e53424b..33cbc4016a 100644 --- a/docker/docker-compose.rootless.yml +++ b/docker/docker-compose.rootless.yml @@ -61,7 +61,7 @@ services: redis: container_name: immich_redis - image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193 + image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9 user: '1000:1000' security_opt: - no-new-privileges:true diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 5f3ad35245..98444d8793 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -49,7 +49,7 @@ services: redis: container_name: immich_redis - image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193 + image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9 healthcheck: test: redis-cli ping || exit 1 restart: always diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index 0ccd54cf3f..8a48cbbdd6 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -44,7 +44,7 @@ services: redis: container_name: immich-e2e-redis - image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193 + image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9 healthcheck: test: redis-cli ping || exit 1 From 5dcdbf04eab646e5e7adf8bc51590b912c685a54 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 11:47:20 +0200 Subject: [PATCH 31/50] chore(deps): update base-image to v202605121138 (#28760) --- server/Dockerfile | 4 ++-- server/Dockerfile.dev | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/Dockerfile b/server/Dockerfile index 60363c77b0..90c34355b8 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/immich-app/base-server-dev:202605051129@sha256:d07d8fcdb7e9f3ac22a811e87761ebf341ed0bb91956b89097540c2ed3fb9ca3 AS builder +FROM ghcr.io/immich-app/base-server-dev:202605121138@sha256:127cc323590e1d64d765016492e1de1e9355da8f658f078aef7be6bede0fdd0f AS builder ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ CI=1 \ COREPACK_HOME=/tmp \ @@ -80,7 +80,7 @@ RUN --mount=type=cache,id=pnpm-packages,target=/buildcache/pnpm-store \ --mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \ mise //:plugins -FROM ghcr.io/immich-app/base-server-prod:202605051129@sha256:50f7ffe4ed31e360c90c4905bd5f6658f2a121297544e3fe9368e338b3f76bcd +FROM ghcr.io/immich-app/base-server-prod:202605121138@sha256:b346d42d86f42799fede6b6c9f6feed0018c5ee46e4ac31d1165f50737cee842 WORKDIR /usr/src/app ENV NODE_ENV=production \ diff --git a/server/Dockerfile.dev b/server/Dockerfile.dev index e98f946c1e..81a1cedd7c 100644 --- a/server/Dockerfile.dev +++ b/server/Dockerfile.dev @@ -1,5 +1,5 @@ # dev build -FROM ghcr.io/immich-app/base-server-dev:202605051129@sha256:d07d8fcdb7e9f3ac22a811e87761ebf341ed0bb91956b89097540c2ed3fb9ca3 AS dev +FROM ghcr.io/immich-app/base-server-dev:202605121138@sha256:127cc323590e1d64d765016492e1de1e9355da8f658f078aef7be6bede0fdd0f AS dev COPY --from=ghcr.io/jdx/mise:2026.5.18@sha256:5bb3311994fa78cef307ca3077cdb18f9551da0886371fc26ea91ab56220ffc5 /usr/local/bin/mise /usr/local/bin/mise From 40983b46c886ddf0fb9e8f059e887647de0f4d5e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 13:37:34 +0200 Subject: [PATCH 32/50] chore(deps): update dependency @vitest/coverage-v8 to v4 (#28761) --- pnpm-lock.yaml | 87 +++++---------------------------------------- server/package.json | 2 +- 2 files changed, 10 insertions(+), 79 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 326c2762f6..e515bf340b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -695,8 +695,8 @@ importers: specifier: ^13.15.2 version: 13.15.10 '@vitest/coverage-v8': - specifier: ^3.0.0 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.13)(@types/node@24.12.4)(happy-dom@20.9.0)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3))(lightningcss@1.32.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)) + specifier: ^4.0.0 + version: 4.1.7(vitest@3.2.4(@types/debug@4.1.13)(@types/node@24.12.4)(happy-dom@20.9.0)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3))(lightningcss@1.32.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)) eslint: specifier: ^10.0.0 version: 10.4.0(jiti@2.7.0) @@ -1110,10 +1110,6 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} - '@angular-devkit/core@19.2.24': resolution: {integrity: sha512-Kd49warf6U/EyWe5BszF/eebN3zQ3bk7tgfEljAw8q/rX95UUtriJubWvp6pgzHfzBA4jwq8f+QiNZB8eBEXPA==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} @@ -3379,10 +3375,6 @@ packages: resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} - '@istanbuljs/schema@0.1.6': - resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} - engines: {node: '>=8'} - '@jest/schemas@29.6.3': resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -5692,15 +5684,6 @@ packages: peerDependencies: valibot: ^1.4.0 - '@vitest/coverage-v8@3.2.4': - resolution: {integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==} - peerDependencies: - '@vitest/browser': 3.2.4 - vitest: 3.2.4 - peerDependenciesMeta: - '@vitest/browser': - optional: true - '@vitest/coverage-v8@4.1.7': resolution: {integrity: sha512-qsYPeXc5Q9dFLd1i8Ap+Bx8sQgcp+rFVQo4R0dDsWNBzl26ldVF1qOO+RL24K7FDrR6pA+50XedRLSoSG24bVQ==} peerDependencies: @@ -6051,9 +6034,6 @@ packages: ast-metadata-inferer@0.8.1: resolution: {integrity: sha512-ht3Dm6Zr7SXv6t1Ra6gFo0+kLDglHGrEbYihTkcycrbHw7WCcuhBzPlJYHEsIpycaUwzsJHje+vUcxXUX4ztTA==} - ast-v8-to-istanbul@0.3.12: - resolution: {integrity: sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==} - ast-v8-to-istanbul@1.0.0: resolution: {integrity: sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==} @@ -8724,10 +8704,6 @@ packages: resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} engines: {node: '>=10'} - istanbul-lib-source-maps@5.0.6: - resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} - engines: {node: '>=10'} - istanbul-reports@3.2.0: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} @@ -9178,9 +9154,6 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - magicast@0.3.5: - resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} - magicast@0.5.3: resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==} @@ -11882,10 +11855,6 @@ packages: engines: {node: '>=10'} hasBin: true - test-exclude@7.0.2: - resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==} - engines: {node: '>=18'} - testcontainers@11.14.0: resolution: {integrity: sha512-r9pniwv/iwzyHaI7gwAvAm4Y+IvjJg3vBWdjrUCaDMc2AXIr4jKbq7jJO18Mw2ybs73pZy1Aj7p/4RVBGMRWjg==} @@ -13026,11 +12995,6 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@ampproject/remapping@2.3.0': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - '@angular-devkit/core@19.2.24(chokidar@4.0.3)': dependencies: ajv: 8.18.0 @@ -16082,8 +16046,6 @@ snapshots: dependencies: minipass: 7.1.3 - '@istanbuljs/schema@0.1.6': {} - '@jest/schemas@29.6.3': dependencies: '@sinclair/typebox': 0.27.10 @@ -18520,24 +18482,19 @@ snapshots: dependencies: valibot: 1.4.0(typescript@6.0.3) - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.13)(@types/node@24.12.4)(happy-dom@20.9.0)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3))(lightningcss@1.32.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0))': + '@vitest/coverage-v8@4.1.7(vitest@3.2.4(@types/debug@4.1.13)(@types/node@24.12.4)(happy-dom@20.9.0)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3))(lightningcss@1.32.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0))': dependencies: - '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 - ast-v8-to-istanbul: 0.3.12 - debug: 4.4.3 + '@vitest/utils': 4.1.7 + ast-v8-to-istanbul: 1.0.0 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.6 istanbul-reports: 3.2.0 - magic-string: 0.30.21 - magicast: 0.3.5 - std-env: 3.10.0 - test-exclude: 7.0.2 - tinyrainbow: 2.0.0 + magicast: 0.5.3 + obug: 2.1.1 + std-env: 4.1.0 + tinyrainbow: 3.1.0 vitest: 3.2.4(@types/debug@4.1.13)(@types/node@24.12.4)(happy-dom@20.9.0)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3))(lightningcss@1.32.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0) - transitivePeerDependencies: - - supports-color '@vitest/coverage-v8@4.1.7(vitest@4.1.7)': dependencies: @@ -18951,12 +18908,6 @@ snapshots: dependencies: '@mdn/browser-compat-data': 5.7.6 - ast-v8-to-istanbul@0.3.12: - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - estree-walker: 3.0.3 - js-tokens: 10.0.0 - ast-v8-to-istanbul@1.0.0: dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -22004,14 +21955,6 @@ snapshots: make-dir: 4.0.0 supports-color: 7.2.0 - istanbul-lib-source-maps@5.0.6: - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - debug: 4.4.3 - istanbul-lib-coverage: 3.2.2 - transitivePeerDependencies: - - supports-color - istanbul-reports@3.2.0: dependencies: html-escaper: 2.0.2 @@ -22429,12 +22372,6 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - magicast@0.3.5: - dependencies: - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 - source-map-js: 1.2.1 - magicast@0.5.3: dependencies: '@babel/parser': 7.29.3 @@ -25848,12 +25785,6 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 - test-exclude@7.0.2: - dependencies: - '@istanbuljs/schema': 0.1.6 - glob: 10.5.0 - minimatch: 10.2.5 - testcontainers@11.14.0: dependencies: '@balena/dockerignore': 1.0.2 diff --git a/server/package.json b/server/package.json index 957aa548d3..42e24c4433 100644 --- a/server/package.json +++ b/server/package.json @@ -147,7 +147,7 @@ "@types/supertest": "^7.0.0", "@types/ua-parser-js": "^0.7.36", "@types/validator": "^13.15.2", - "@vitest/coverage-v8": "^3.0.0", + "@vitest/coverage-v8": "^4.0.0", "eslint": "^10.0.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.1.3", From c5fb67c0040526403b12bb165a541e7c01afe49f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 13:38:57 +0200 Subject: [PATCH 33/50] chore(deps): update dependency prettier-plugin-svelte to v4 (#28762) --- pnpm-lock.yaml | 19 ++++++++++--------- web/package.json | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e515bf340b..06c0c7e73d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -928,7 +928,7 @@ importers: version: 14.6.1(@testing-library/dom@10.4.1) '@trivago/prettier-plugin-sort-imports': specifier: ^6.0.2 - version: 6.0.2(prettier-plugin-svelte@3.5.2(prettier@3.8.3)(svelte@5.55.8(@typescript-eslint/types@8.59.4)))(prettier@3.8.3)(svelte@5.55.8(@typescript-eslint/types@8.59.4)) + version: 6.0.2(prettier-plugin-svelte@4.1.0(prettier@3.8.3)(svelte@5.55.8(@typescript-eslint/types@8.59.4)))(prettier@3.8.3)(svelte@5.55.8(@typescript-eslint/types@8.59.4)) '@types/chromecast-caf-sender': specifier: ^1.0.11 version: 1.0.11 @@ -984,8 +984,8 @@ importers: specifier: ^4.1.1 version: 4.2.0(prettier@3.8.3) prettier-plugin-svelte: - specifier: ^3.3.3 - version: 3.5.2(prettier@3.8.3)(svelte@5.55.8(@typescript-eslint/types@8.59.4)) + specifier: ^4.0.0 + version: 4.1.0(prettier@3.8.3)(svelte@5.55.8(@typescript-eslint/types@8.59.4)) rollup-plugin-visualizer: specifier: ^7.0.0 version: 7.0.1(rolldown@1.0.1)(rollup@4.60.4) @@ -10678,11 +10678,12 @@ packages: peerDependencies: prettier: ^3.0.0 - prettier-plugin-svelte@3.5.2: - resolution: {integrity: sha512-ItFouLvzSFE3ulNl4DKoWM3BGcbDCNVpIyy/Y3F2gC3aNiGLxtFUdffVqO5Z5hhYG+DFT5KULWaxmeFFpdbvaQ==} + prettier-plugin-svelte@4.1.0: + resolution: {integrity: sha512-YZkhA2Q9oOerFFG9tq+2f98WYT7Z2JgrybJrAyrB78jpsH9i/DdgplXemehuFPgsldetFNCcR/yCcYlDjPy94Q==} + engines: {node: '>=20'} peerDependencies: prettier: ^3.0.0 - svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 + svelte: ^5.0.0 prettier@3.8.3: resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} @@ -17787,7 +17788,7 @@ snapshots: '@tokenizer/token@0.3.0': {} - '@trivago/prettier-plugin-sort-imports@6.0.2(prettier-plugin-svelte@3.5.2(prettier@3.8.3)(svelte@5.55.8(@typescript-eslint/types@8.59.4)))(prettier@3.8.3)(svelte@5.55.8(@typescript-eslint/types@8.59.4))': + '@trivago/prettier-plugin-sort-imports@6.0.2(prettier-plugin-svelte@4.1.0(prettier@3.8.3)(svelte@5.55.8(@typescript-eslint/types@8.59.4)))(prettier@3.8.3)(svelte@5.55.8(@typescript-eslint/types@8.59.4))': dependencies: '@babel/generator': 7.29.1 '@babel/parser': 7.29.3 @@ -17799,7 +17800,7 @@ snapshots: parse-imports-exports: 0.2.4 prettier: 3.8.3 optionalDependencies: - prettier-plugin-svelte: 3.5.2(prettier@3.8.3)(svelte@5.55.8(@typescript-eslint/types@8.59.4)) + prettier-plugin-svelte: 4.1.0(prettier@3.8.3)(svelte@5.55.8(@typescript-eslint/types@8.59.4)) svelte: 5.55.8(@typescript-eslint/types@8.59.4) transitivePeerDependencies: - supports-color @@ -24242,7 +24243,7 @@ snapshots: dependencies: prettier: 3.8.3 - prettier-plugin-svelte@3.5.2(prettier@3.8.3)(svelte@5.55.8(@typescript-eslint/types@8.59.4)): + prettier-plugin-svelte@4.1.0(prettier@3.8.3)(svelte@5.55.8(@typescript-eslint/types@8.59.4)): dependencies: prettier: 3.8.3 svelte: 5.55.8(@typescript-eslint/types@8.59.4) diff --git a/web/package.json b/web/package.json index 36f5cf9647..62c5a43bd0 100644 --- a/web/package.json +++ b/web/package.json @@ -103,7 +103,7 @@ "happy-dom": "^20.0.0", "prettier": "^3.7.4", "prettier-plugin-sort-json": "^4.1.1", - "prettier-plugin-svelte": "^3.3.3", + "prettier-plugin-svelte": "^4.0.0", "rollup-plugin-visualizer": "^7.0.0", "svelte": "5.55.8", "svelte-check": "^4.4.6", From 03554b24ad9603a95cace514156c0235374712f4 Mon Sep 17 00:00:00 2001 From: Min Idzelis Date: Tue, 2 Jun 2026 07:42:33 -0400 Subject: [PATCH 34/50] fix(web): skip thumbhash fade for offscreen thumbnails (#27335) --- .../components/assets/thumbnail/Thumbnail.svelte | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/web/src/lib/components/assets/thumbnail/Thumbnail.svelte b/web/src/lib/components/assets/thumbnail/Thumbnail.svelte index aef52d0cfd..347ff3e75f 100644 --- a/web/src/lib/components/assets/thumbnail/Thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/Thumbnail.svelte @@ -78,6 +78,7 @@ let mouseOver = $state(false); let loaded = $state(false); let thumbError = $state(false); + let skipFade = $state(false); let width = $derived(thumbnailSize || thumbnailWidth || 235); let height = $derived(thumbnailSize || thumbnailHeight || 235); @@ -252,7 +253,12 @@ widthStyle="{width}px" heightStyle="{height}px" curve={selected} - onComplete={(errored) => ((loaded = true), (thumbError = errored))} + onComplete={(errored) => { + const rect = element?.getBoundingClientRect(); + skipFade = !rect || rect.bottom < 0 || rect.top > window.innerHeight; + loaded = true; + thumbError = errored; + }} /> {#if asset.isVideo}
@@ -297,7 +303,10 @@ Date: Tue, 2 Jun 2026 12:05:42 +0000 Subject: [PATCH 35/50] chore(deps): update dependency testcontainers to v12 (#28763) --- pnpm-lock.yaml | 36 ++++++++++++++---------------------- server/package.json | 2 +- 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 06c0c7e73d..6aa01f8ace 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -734,8 +734,8 @@ importers: specifier: ^3.4.0 version: 3.4.19(tsx@4.22.3)(yaml@2.9.0) testcontainers: - specifier: ^11.0.0 - version: 11.14.0 + specifier: ^12.0.0 + version: 12.0.1 typescript: specifier: ^6.0.0 version: 6.0.3 @@ -7292,9 +7292,9 @@ packages: resolution: {integrity: sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==} engines: {node: '>= 8.0'} - dockerode@4.0.12: - resolution: {integrity: sha512-/bCZd6KlGcjZO8Buqmi/vXuqEGVEZ0PNjx/biBNqJD3MhK9DmdiAuKxqfNhflgDESDIiBz3qF+0e55+CpnrUcw==} - engines: {node: '>= 8.0'} + dockerode@5.0.0: + resolution: {integrity: sha512-C52mvJ+7lcyhWNfrzVfFsbTrBfy/ezE9FGEYLpu17FUeBcCkxERk9nN7uDl/478ynDiQ4U+5DbQC2vENHkVEtQ==} + engines: {node: '>= 14.17'} docusaurus-lunr-search@3.6.0: resolution: {integrity: sha512-CCEAnj5e67sUZmIb2hOl4xb4nDN07fb0fvRDDmdWlYpUvyS1CSKbw4lsGInLyUFEEEBzxQmT6zaVQdF/8Zretg==} @@ -11856,8 +11856,8 @@ packages: engines: {node: '>=10'} hasBin: true - testcontainers@11.14.0: - resolution: {integrity: sha512-r9pniwv/iwzyHaI7gwAvAm4Y+IvjJg3vBWdjrUCaDMc2AXIr4jKbq7jJO18Mw2ybs73pZy1Aj7p/4RVBGMRWjg==} + testcontainers@12.0.1: + resolution: {integrity: sha512-EMjjfMNJf3HlL7V3elkxqKUO1r3CtqNBTdmKGwwma/lOtUGfoWvFJ0WQ/KQf1DHEMnRjLWzW4cXbv/Tndsbcbw==} text-decoder@1.2.7: resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} @@ -11947,8 +11947,8 @@ packages: resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} hasBin: true - tmp@0.2.5: - resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} + tmp@0.2.7: + resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} engines: {node: '>=14.14'} to-regex-range@5.0.1: @@ -12289,11 +12289,6 @@ packages: resolution: {integrity: sha512-6S5mCapmzcxetOD/2UEjL0GF5e4+gB07Dh8qs63xylw5ay4XuyW6iQs70FOJo/puf10LCkvhp4jYMQSDUBYEFg==} engines: {node: '>=10.0.0'} - uuid@10.0.0: - resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} - deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). - hasBin: true - uuid@14.0.0: resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} hasBin: true @@ -20180,7 +20175,7 @@ snapshots: transitivePeerDependencies: - supports-color - dockerode@4.0.12: + dockerode@5.0.0: dependencies: '@balena/dockerignore': 1.0.2 '@grpc/grpc-js': 1.14.3 @@ -20188,7 +20183,6 @@ snapshots: docker-modem: 5.0.7 protobufjs: 7.6.0 tar-fs: 2.1.4 - uuid: 10.0.0 transitivePeerDependencies: - supports-color @@ -25786,7 +25780,7 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 - testcontainers@11.14.0: + testcontainers@12.0.1: dependencies: '@balena/dockerignore': 1.0.2 '@types/dockerode': 4.0.1 @@ -25795,13 +25789,13 @@ snapshots: byline: 5.0.0 debug: 4.4.3 docker-compose: 1.4.2 - dockerode: 4.0.12 + dockerode: 5.0.0 get-port: 7.2.0 proper-lockfile: 4.1.2 properties-reader: 3.0.1 ssh-remote-port-forward: 1.0.4 tar-fs: 3.1.2 - tmp: 0.2.5 + tmp: 0.2.7 undici: 7.25.0 transitivePeerDependencies: - bare-abort-controller @@ -25882,7 +25876,7 @@ snapshots: tldts-core: 6.1.86 optional: true - tmp@0.2.5: {} + tmp@0.2.7: {} to-regex-range@5.0.1: dependencies: @@ -26232,8 +26226,6 @@ snapshots: - encoding - supports-color - uuid@10.0.0: {} - uuid@14.0.0: {} uuid@8.3.2: {} diff --git a/server/package.json b/server/package.json index 42e24c4433..2aca79a0dc 100644 --- a/server/package.json +++ b/server/package.json @@ -160,7 +160,7 @@ "sql-formatter": "^15.0.0", "supertest": "^7.1.0", "tailwindcss": "^3.4.0", - "testcontainers": "^11.0.0", + "testcontainers": "^12.0.0", "typescript": "^6.0.0", "typescript-eslint": "^8.28.0", "unplugin-swc": "^1.4.5", From 65d8b35f8b21deba054b28876d51fd66d6625a04 Mon Sep 17 00:00:00 2001 From: Min Idzelis Date: Tue, 2 Jun 2026 08:54:44 -0400 Subject: [PATCH 37/50] refactor(web): align gallery-viewer viewport naming and tunables (#28743) --- .../gallery-viewer/GalleryViewer.svelte | 50 ++++++++++--------- .../[[assetId=id]]/+page.svelte | 2 +- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/web/src/lib/components/shared-components/gallery-viewer/GalleryViewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/GalleryViewer.svelte index fc2131310a..ef1c2bda76 100644 --- a/web/src/lib/components/shared-components/gallery-viewer/GalleryViewer.svelte +++ b/web/src/lib/components/shared-components/gallery-viewer/GalleryViewer.svelte @@ -22,11 +22,16 @@ import { getJustifiedLayoutFromAssets } from '$lib/utils/layout-utils'; import { navigate } from '$lib/utils/navigation'; import { isTimelineAsset, toTimelineAsset } from '$lib/utils/timeline-util'; + import { TUNABLES } from '$lib/utils/tunables'; import { AssetVisibility, type AssetResponseDto } from '@immich/sdk'; import { modalManager } from '@immich/ui'; import { debounce } from 'lodash-es'; import { t } from 'svelte-i18n'; + const { + TIMELINE: { INTERSECTION_EXPAND_TOP, INTERSECTION_EXPAND_BOTTOM }, + } = TUNABLES; + type Props = { assets: AssetResponseDto[]; viewerAssets?: AssetResponseDto[]; @@ -34,7 +39,7 @@ disableAssetSelect?: boolean; showArchiveIcon?: boolean; viewport: Viewport; - onIntersected?: (() => void) | undefined; + onEndReached?: (() => void) | undefined; showAssetName?: boolean; onReload?: (() => void) | undefined; pageHeaderOffset?: number; @@ -50,7 +55,7 @@ disableAssetSelect = false, showArchiveIcon = false, viewport, - onIntersected = undefined, + onEndReached = undefined, showAssetName = false, onReload = undefined, slidingWindowOffset = 0, @@ -70,24 +75,23 @@ }), ); - const getStyle = (i: number) => { - const geo = geometry; - return `top: ${geo.getTop(i)}px; left: ${geo.getLeft(i)}px; width: ${geo.getWidth(i)}px; height: ${geo.getHeight(i)}px;`; + const getStyle = (index: number) => { + return `top: ${geometry.getTop(index)}px; left: ${geometry.getLeft(index)}px; width: ${geometry.getWidth(index)}px; height: ${geometry.getHeight(index)}px;`; }; - const isIntersecting = (i: number) => { - const geo = geometry; + const isInOrNearViewport = (index: number) => { const window = slidingWindow; - const top = geo.getTop(i); - return top + pageHeaderOffset < window.bottom && top + geo.getHeight(i) > window.top; + const top = geometry.getTop(index); + return top + pageHeaderOffset < window.bottom && top + geometry.getHeight(index) > window.top; }; let shiftKeyIsDown = $state(false); let lastAssetMouseEvent: TimelineAsset | null = $state(null); let scrollTop = $state(0); + let slidingWindow = $derived.by(() => { - const top = (scrollTop || 0) - slidingWindowOffset; - const bottom = top + viewport.height + slidingWindowOffset; + const top = (scrollTop || 0) - slidingWindowOffset - INTERSECTION_EXPAND_TOP; + const bottom = top + viewport.height + slidingWindowOffset + INTERSECTION_EXPAND_BOTTOM; return { top, bottom, @@ -101,17 +105,15 @@ const updateSlidingWindow = () => (scrollTop = document.scrollingElement?.scrollTop ?? 0); - const debouncedOnIntersected = debounce(() => onIntersected?.(), 750, { maxWait: 100, leading: true }); + const debouncedOnEndReached = debounce(() => onEndReached?.(), 750, { maxWait: 100, leading: true }); - let lastIntersectedHeight = 0; + let lastEndReachedHeight = 0; $effect(() => { - // Intersect if there's only one viewport worth of assets left to scroll. if (geometry.containerHeight - slidingWindow.bottom <= viewport.height) { - // Notify we got to (near) the end of scroll. - const intersectedHeight = geometry.containerHeight; - if (lastIntersectedHeight !== intersectedHeight) { - debouncedOnIntersected(); - lastIntersectedHeight = intersectedHeight; + const contentHeight = geometry.containerHeight; + if (lastEndReachedHeight !== contentHeight) { + debouncedOnEndReached(); + lastEndReachedHeight = contentHeight; } } }); @@ -362,10 +364,10 @@ style:height={geometry.containerHeight + 'px'} style:width={geometry.containerWidth + 'px'} > - {#each assets as asset, i (asset.id + '-' + i)} - {#if isIntersecting(i)} + {#each assets as asset, index (asset.id + '-' + index)} + {#if isInOrNearViewport(index)} {@const currentAsset = toTimelineAsset(asset)} -
+
{ @@ -382,8 +384,8 @@ asset={currentAsset} selected={assetInteraction.hasSelectedAsset(currentAsset.id)} selectionCandidate={assetInteraction.hasSelectionCandidate(currentAsset.id)} - thumbnailWidth={geometry.getWidth(i)} - thumbnailHeight={geometry.getHeight(i)} + thumbnailWidth={geometry.getWidth(index)} + thumbnailHeight={geometry.getHeight(index)} /> {#if showAssetName && !isTimelineAsset(asset)}
Date: Tue, 2 Jun 2026 20:35:15 +0530 Subject: [PATCH 38/50] fix(web): prevent partner assets from being selected in geolocation utility (#28737) Co-authored-by: Daniel Dietzler --- web/src/routes/(user)/utilities/geolocation/+page.svelte | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/web/src/routes/(user)/utilities/geolocation/+page.svelte b/web/src/routes/(user)/utilities/geolocation/+page.svelte index 18287a7b13..061872e42c 100644 --- a/web/src/routes/(user)/utilities/geolocation/+page.svelte +++ b/web/src/routes/(user)/utilities/geolocation/+page.svelte @@ -38,6 +38,8 @@ withCoordinates: true, }; + const isOwnAsset = (asset: TimelineAsset) => asset.ownerId === authManager.user.id; + const handleUpdate = async () => { if (!point) { return; @@ -54,7 +56,7 @@ await updateAssets({ assetBulkUpdateDto: { - ids: assetMultiSelectManager.assets.map((asset) => asset.id), + ids: assetMultiSelectManager.assets.filter((asset) => isOwnAsset(asset)).map((asset) => asset.id), latitude: point.lat, longitude: point.lng, }, @@ -124,7 +126,7 @@ }, 1500); point = { lat: asset.latitude, lng: asset.longitude }; void setQueryValue('at', asset.id); - } else { + } else if (isOwnAsset(asset)) { onClick(timelineManager, timelineDay.getAssets(), timelineDay.groupTitle, asset); } }; @@ -199,6 +201,9 @@ onThumbnailClick={handleThumbnailClick} > {#snippet customThumbnailLayout(asset: TimelineAsset)} + {#if !isOwnAsset(asset)} +
+ {/if} {#if hasGps(asset)}
{asset.city || $t('gps')} From 59750dad7d56fc9d5e1c7103a7395934741469f7 Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 2 Jun 2026 17:19:59 +0200 Subject: [PATCH 39/50] feat: places in context search (#28768) --- web/src/lib/commands.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/web/src/lib/commands.ts b/web/src/lib/commands.ts index ddc90a4a42..7498756ac0 100644 --- a/web/src/lib/commands.ts +++ b/web/src/lib/commands.ts @@ -16,6 +16,7 @@ import { mdiLink, mdiLockOutline, mdiMagnify, + mdiMapMarkerOutline, mdiMapOutline, mdiServer, mdiStateMachine, @@ -93,6 +94,11 @@ export const getPagesProvider = ($t: MessageFormatter) => { onAction: () => goto(Route.people()), $if: () => authManager.authenticated && authManager.preferences.people.enabled, }, + { + title: $t('places'), + icon: mdiMapMarkerOutline, + onAction: () => goto(Route.places()), + }, { title: $t('shared_links'), icon: mdiLink, From 109e0a7ad014fbd405170b1a95e1ba5047db68e1 Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 2 Jun 2026 17:37:20 +0200 Subject: [PATCH 40/50] fix(mobile): invisible ink splashes in asset sheet (#28756) --- .../asset_viewer/sheet_tile.widget.dart | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/mobile/lib/presentation/widgets/asset_viewer/sheet_tile.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/sheet_tile.widget.dart index 2af68e1ff0..69e84ee03d 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/sheet_tile.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/sheet_tile.widget.dart @@ -63,16 +63,19 @@ class SheetTile extends ConsumerWidget { subtitleWidget = null; } - return ListTile( - dense: true, - visualDensity: VisualDensity.compact, - title: GestureDetector(onLongPress: () => copyTitle(context, ref), child: titleWidget), - titleAlignment: ListTileTitleAlignment.center, - leading: leading, - trailing: trailing, - contentPadding: leading == null ? null : const EdgeInsets.only(left: 25), - subtitle: subtitleWidget, - onTap: onTap, + return Material( + type: MaterialType.transparency, + child: ListTile( + dense: true, + visualDensity: VisualDensity.compact, + title: GestureDetector(onLongPress: () => copyTitle(context, ref), child: titleWidget), + titleAlignment: ListTileTitleAlignment.center, + leading: leading, + trailing: trailing, + contentPadding: leading == null ? null : const EdgeInsets.only(left: 25), + subtitle: subtitleWidget, + onTap: onTap, + ), ); } } From 368cb7a4ad90f8cc9abd8c0975bf9092bd36c06b Mon Sep 17 00:00:00 2001 From: Tim Jones <57257388+timjonez@users.noreply.github.com> Date: Tue, 2 Jun 2026 09:05:55 -0700 Subject: [PATCH 41/50] feat: minimum face count per user (#27452) * add user metadata table and use to filter persons in person.getAllForUser query * update PersonRepository.getAllForUser query * remove minFaces from PersonSearchOptions interface * fix person.getAllForUser query * update types and openapi specs * add minFaces field to user settings page * remove old arg from tests * add e2e test to verify minimumFace user preference * add i18n label and description for english * update default min faces * fetch minFaces ML default and use as per-user default in frontend * update e2e tests * fix bugs in people getAllForUser query * update person getNumberOfPeople query to reflect correct number of people according to minFaces threshold * updated mobile openapi specs? * use subquery in coalesce instead of join * remove out of scope query update --- e2e/src/specs/server/api/server.e2e-spec.ts | 1 + e2e/src/specs/server/api/user.e2e-spec.ts | 15 ++++++++++++ i18n/en.json | 2 ++ mobile/openapi/lib/model/people_response.dart | 23 ++++++++++++++++++- mobile/openapi/lib/model/people_update.dart | 23 ++++++++++++++++++- .../openapi/lib/model/server_config_dto.dart | 14 ++++++++++- open-api/immich-openapi-specs.json | 19 +++++++++++++++ packages/sdk/src/fetch-client.ts | 6 +++++ server/src/dtos/server.dto.ts | 1 + server/src/dtos/user-preferences.dto.ts | 2 ++ server/src/queries/person.repository.sql | 13 ++++++++++- server/src/repositories/person.repository.ts | 15 +++++++++--- server/src/services/person.service.spec.ts | 2 -- server/src/services/person.service.ts | 2 -- server/src/services/server.service.spec.ts | 1 + server/src/services/server.service.ts | 1 + server/src/types.ts | 1 + server/src/utils/preferences.ts | 1 + .../user-settings/FeatureSettings.svelte | 7 +++++- 19 files changed, 137 insertions(+), 12 deletions(-) diff --git a/e2e/src/specs/server/api/server.e2e-spec.ts b/e2e/src/specs/server/api/server.e2e-spec.ts index 9ab2f5d823..902c5302e5 100644 --- a/e2e/src/specs/server/api/server.e2e-spec.ts +++ b/e2e/src/specs/server/api/server.e2e-spec.ts @@ -141,6 +141,7 @@ describe('/server', () => { maintenanceMode: false, mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json', mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json', + minFaces: 3, }); }); }); diff --git a/e2e/src/specs/server/api/user.e2e-spec.ts b/e2e/src/specs/server/api/user.e2e-spec.ts index 8a2197efde..2dc789a91b 100644 --- a/e2e/src/specs/server/api/user.e2e-spec.ts +++ b/e2e/src/specs/server/api/user.e2e-spec.ts @@ -230,6 +230,21 @@ describe('/users', () => { const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); expect(after).toMatchObject({ download: { includeEmbeddedVideos: true } }); }); + + it('should update minimum face count to display people', async () => { + const before = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); + expect(before).toMatchObject({ people: { minimumFaces: 3 } }); + + const { status, body } = await request(app) + .put('/users/me/preferences') + .send({ people: { minimumFaces: 2 } }) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(200); + expect(body).toMatchObject({ people: { minimumFaces: 2 } }); + + const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); + expect(after).toMatchObject({ people: { minimumFaces: 2 } }); + }); }); describe('GET /users/:id', () => { diff --git a/i18n/en.json b/i18n/en.json index 90beb7077e..f4ad3001c2 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1592,6 +1592,8 @@ "merge_people_prompt": "Do you want to merge these people? This action is irreversible.", "merge_people_successfully": "Merge people successfully", "merged_people_count": "Merged {count, plural, one {# person} other {# people}}", + "minFaces": "Minimum faces", + "minFaces_description": "The minimum number of recognized faces for a person to be displayed", "minimize": "Minimize", "minute": "Minute", "minutes": "Minutes", diff --git a/mobile/openapi/lib/model/people_response.dart b/mobile/openapi/lib/model/people_response.dart index 9d5d8ec18a..ba7128d932 100644 --- a/mobile/openapi/lib/model/people_response.dart +++ b/mobile/openapi/lib/model/people_response.dart @@ -14,32 +14,52 @@ class PeopleResponse { /// Returns a new [PeopleResponse] instance. PeopleResponse({ required this.enabled, + this.minimumFaces, required this.sidebarWeb, }); /// Whether people are enabled bool enabled; + /// People face threshold + /// + /// Minimum value: 1 + /// Maximum value: 9007199254740991 + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + int? minimumFaces; + /// Whether people appear in web sidebar bool sidebarWeb; @override bool operator ==(Object other) => identical(this, other) || other is PeopleResponse && other.enabled == enabled && + other.minimumFaces == minimumFaces && other.sidebarWeb == sidebarWeb; @override int get hashCode => // ignore: unnecessary_parenthesis (enabled.hashCode) + + (minimumFaces == null ? 0 : minimumFaces!.hashCode) + (sidebarWeb.hashCode); @override - String toString() => 'PeopleResponse[enabled=$enabled, sidebarWeb=$sidebarWeb]'; + String toString() => 'PeopleResponse[enabled=$enabled, minimumFaces=$minimumFaces, sidebarWeb=$sidebarWeb]'; Map toJson() { final json = {}; json[r'enabled'] = this.enabled; + if (this.minimumFaces != null) { + json[r'minimumFaces'] = this.minimumFaces; + } else { + // json[r'minimumFaces'] = null; + } json[r'sidebarWeb'] = this.sidebarWeb; return json; } @@ -54,6 +74,7 @@ class PeopleResponse { return PeopleResponse( enabled: mapValueOfType(json, r'enabled')!, + minimumFaces: mapValueOfType(json, r'minimumFaces'), sidebarWeb: mapValueOfType(json, r'sidebarWeb')!, ); } diff --git a/mobile/openapi/lib/model/people_update.dart b/mobile/openapi/lib/model/people_update.dart index fe16479bac..05459f19f3 100644 --- a/mobile/openapi/lib/model/people_update.dart +++ b/mobile/openapi/lib/model/people_update.dart @@ -14,6 +14,7 @@ class PeopleUpdate { /// Returns a new [PeopleUpdate] instance. PeopleUpdate({ this.enabled, + this.minimumFaces, this.sidebarWeb, }); @@ -26,6 +27,18 @@ class PeopleUpdate { /// bool? enabled; + /// People face threshold + /// + /// Minimum value: 1 + /// Maximum value: 9007199254740991 + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + int? minimumFaces; + /// Whether people appear in web sidebar /// /// Please note: This property should have been non-nullable! Since the specification file @@ -38,16 +51,18 @@ class PeopleUpdate { @override bool operator ==(Object other) => identical(this, other) || other is PeopleUpdate && other.enabled == enabled && + other.minimumFaces == minimumFaces && other.sidebarWeb == sidebarWeb; @override int get hashCode => // ignore: unnecessary_parenthesis (enabled == null ? 0 : enabled!.hashCode) + + (minimumFaces == null ? 0 : minimumFaces!.hashCode) + (sidebarWeb == null ? 0 : sidebarWeb!.hashCode); @override - String toString() => 'PeopleUpdate[enabled=$enabled, sidebarWeb=$sidebarWeb]'; + String toString() => 'PeopleUpdate[enabled=$enabled, minimumFaces=$minimumFaces, sidebarWeb=$sidebarWeb]'; Map toJson() { final json = {}; @@ -56,6 +71,11 @@ class PeopleUpdate { } else { // json[r'enabled'] = null; } + if (this.minimumFaces != null) { + json[r'minimumFaces'] = this.minimumFaces; + } else { + // json[r'minimumFaces'] = null; + } if (this.sidebarWeb != null) { json[r'sidebarWeb'] = this.sidebarWeb; } else { @@ -74,6 +94,7 @@ class PeopleUpdate { return PeopleUpdate( enabled: mapValueOfType(json, r'enabled'), + minimumFaces: mapValueOfType(json, r'minimumFaces'), sidebarWeb: mapValueOfType(json, r'sidebarWeb'), ); } diff --git a/mobile/openapi/lib/model/server_config_dto.dart b/mobile/openapi/lib/model/server_config_dto.dart index 316edb609f..0eaaec7c7f 100644 --- a/mobile/openapi/lib/model/server_config_dto.dart +++ b/mobile/openapi/lib/model/server_config_dto.dart @@ -20,6 +20,7 @@ class ServerConfigDto { required this.maintenanceMode, required this.mapDarkStyleUrl, required this.mapLightStyleUrl, + required this.minFaces, required this.oauthButtonText, required this.publicUsers, required this.trashDays, @@ -47,6 +48,12 @@ class ServerConfigDto { /// Map light style URL String mapLightStyleUrl; + /// People min faces server default + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 + int minFaces; + /// OAuth button text String oauthButtonText; @@ -74,6 +81,7 @@ class ServerConfigDto { other.maintenanceMode == maintenanceMode && other.mapDarkStyleUrl == mapDarkStyleUrl && other.mapLightStyleUrl == mapLightStyleUrl && + other.minFaces == minFaces && other.oauthButtonText == oauthButtonText && other.publicUsers == publicUsers && other.trashDays == trashDays && @@ -89,13 +97,14 @@ class ServerConfigDto { (maintenanceMode.hashCode) + (mapDarkStyleUrl.hashCode) + (mapLightStyleUrl.hashCode) + + (minFaces.hashCode) + (oauthButtonText.hashCode) + (publicUsers.hashCode) + (trashDays.hashCode) + (userDeleteDelay.hashCode); @override - String toString() => 'ServerConfigDto[externalDomain=$externalDomain, isInitialized=$isInitialized, isOnboarded=$isOnboarded, loginPageMessage=$loginPageMessage, maintenanceMode=$maintenanceMode, mapDarkStyleUrl=$mapDarkStyleUrl, mapLightStyleUrl=$mapLightStyleUrl, oauthButtonText=$oauthButtonText, publicUsers=$publicUsers, trashDays=$trashDays, userDeleteDelay=$userDeleteDelay]'; + String toString() => 'ServerConfigDto[externalDomain=$externalDomain, isInitialized=$isInitialized, isOnboarded=$isOnboarded, loginPageMessage=$loginPageMessage, maintenanceMode=$maintenanceMode, mapDarkStyleUrl=$mapDarkStyleUrl, mapLightStyleUrl=$mapLightStyleUrl, minFaces=$minFaces, oauthButtonText=$oauthButtonText, publicUsers=$publicUsers, trashDays=$trashDays, userDeleteDelay=$userDeleteDelay]'; Map toJson() { final json = {}; @@ -106,6 +115,7 @@ class ServerConfigDto { json[r'maintenanceMode'] = this.maintenanceMode; json[r'mapDarkStyleUrl'] = this.mapDarkStyleUrl; json[r'mapLightStyleUrl'] = this.mapLightStyleUrl; + json[r'minFaces'] = this.minFaces; json[r'oauthButtonText'] = this.oauthButtonText; json[r'publicUsers'] = this.publicUsers; json[r'trashDays'] = this.trashDays; @@ -129,6 +139,7 @@ class ServerConfigDto { maintenanceMode: mapValueOfType(json, r'maintenanceMode')!, mapDarkStyleUrl: mapValueOfType(json, r'mapDarkStyleUrl')!, mapLightStyleUrl: mapValueOfType(json, r'mapLightStyleUrl')!, + minFaces: mapValueOfType(json, r'minFaces')!, oauthButtonText: mapValueOfType(json, r'oauthButtonText')!, publicUsers: mapValueOfType(json, r'publicUsers')!, trashDays: mapValueOfType(json, r'trashDays')!, @@ -187,6 +198,7 @@ class ServerConfigDto { 'maintenanceMode', 'mapDarkStyleUrl', 'mapLightStyleUrl', + 'minFaces', 'oauthButtonText', 'publicUsers', 'trashDays', diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index d9087c375d..33eaf13fc2 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -19907,6 +19907,12 @@ "description": "Whether people are enabled", "type": "boolean" }, + "minimumFaces": { + "description": "People face threshold", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + }, "sidebarWeb": { "description": "Whether people appear in web sidebar", "type": "boolean" @@ -19968,6 +19974,12 @@ "description": "Whether people are enabled", "type": "boolean" }, + "minimumFaces": { + "description": "People face threshold", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + }, "sidebarWeb": { "description": "Whether people appear in web sidebar", "type": "boolean" @@ -21604,6 +21616,12 @@ "description": "Map light style URL", "type": "string" }, + "minFaces": { + "description": "People min faces server default", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, "oauthButtonText": { "description": "OAuth button text", "type": "string" @@ -21633,6 +21651,7 @@ "maintenanceMode", "mapDarkStyleUrl", "mapLightStyleUrl", + "minFaces", "oauthButtonText", "publicUsers", "trashDays", diff --git a/packages/sdk/src/fetch-client.ts b/packages/sdk/src/fetch-client.ts index 163558e6a6..89d0e513d8 100644 --- a/packages/sdk/src/fetch-client.ts +++ b/packages/sdk/src/fetch-client.ts @@ -298,6 +298,8 @@ export type MemoriesResponse = { export type PeopleResponse = { /** Whether people are enabled */ enabled: boolean; + /** People face threshold */ + minimumFaces?: number; /** Whether people appear in web sidebar */ sidebarWeb: boolean; }; @@ -375,6 +377,8 @@ export type MemoriesUpdate = { export type PeopleUpdate = { /** Whether people are enabled */ enabled?: boolean; + /** People face threshold */ + minimumFaces?: number; /** Whether people appear in web sidebar */ sidebarWeb?: boolean; }; @@ -1963,6 +1967,8 @@ export type ServerConfigDto = { mapDarkStyleUrl: string; /** Map light style URL */ mapLightStyleUrl: string; + /** People min faces server default */ + minFaces: number; /** OAuth button text */ oauthButtonText: string; /** Whether public user registration is enabled */ diff --git a/server/src/dtos/server.dto.ts b/server/src/dtos/server.dto.ts index 6370557785..03d45fab1c 100644 --- a/server/src/dtos/server.dto.ts +++ b/server/src/dtos/server.dto.ts @@ -124,6 +124,7 @@ const ServerConfigSchema = z mapDarkStyleUrl: z.string().describe('Map dark style URL'), mapLightStyleUrl: z.string().describe('Map light style URL'), maintenanceMode: z.boolean().describe('Whether maintenance mode is active'), + minFaces: z.int().describe('People min faces server default'), }) .meta({ id: 'ServerConfigDto' }); diff --git a/server/src/dtos/user-preferences.dto.ts b/server/src/dtos/user-preferences.dto.ts index 7a7c1d2558..b8894e4d51 100644 --- a/server/src/dtos/user-preferences.dto.ts +++ b/server/src/dtos/user-preferences.dto.ts @@ -45,6 +45,7 @@ const PeopleUpdateSchema = z .object({ enabled: z.boolean().optional().describe('Whether people are enabled'), sidebarWeb: z.boolean().optional().describe('Whether people appear in web sidebar'), + minimumFaces: z.int().min(1).optional().describe('People face threshold'), }) .optional() .meta({ id: 'PeopleUpdate' }); @@ -138,6 +139,7 @@ const PeopleResponseSchema = z .object({ enabled: z.boolean().describe('Whether people are enabled'), sidebarWeb: z.boolean().describe('Whether people appear in web sidebar'), + minimumFaces: z.int().min(1).optional().describe('People face threshold'), }) .meta({ id: 'PeopleResponse' }); diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql index 318c151cca..a2f3f64442 100644 --- a/server/src/queries/person.repository.sql +++ b/server/src/queries/person.repository.sql @@ -42,7 +42,18 @@ group by having ( "person"."name" != $3 - or count("asset_face"."assetId") >= $4 + or count("asset_face"."assetId") >= COALESCE( + ( + SELECT + value -> 'people' ->> 'minimumFaces' + FROM + user_metadata + WHERE + "userId" = $4 + AND key = 'preferences' + ), + '3' + )::int ) order by "person"."isHidden" asc, diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index 2a9f822e94..0db03a18c7 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -4,7 +4,7 @@ import { jsonObjectFrom } from 'kysely/helpers/postgres'; import { InjectKysely } from 'nestjs-kysely'; import { AssetFace } from 'src/database'; import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; -import { AssetFileType, AssetVisibility, SourceType } from 'src/enum'; +import { AssetFileType, AssetVisibility, SourceType, UserMetadataKey } from 'src/enum'; import { DB } from 'src/schema'; import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; import { FaceSearchTable } from 'src/schema/tables/face-search.table'; @@ -13,7 +13,6 @@ import { dummy, removeUndefinedKeys, withFilePath } from 'src/utils/database'; import { paginationHelper, PaginationOptions } from 'src/utils/pagination'; export interface PersonSearchOptions { - minimumFaceCount: number; withHidden: boolean; closestFaceAssetId?: string; } @@ -168,7 +167,17 @@ export class PersonRepository { .having((eb) => eb.or([ eb('person.name', '!=', ''), - eb((innerEb) => innerEb.fn.count('asset_face.assetId'), '>=', options?.minimumFaceCount || 1), + eb( + (innerEb) => innerEb.fn.count('asset_face.assetId'), + '>=', + sql`COALESCE( + (SELECT value -> 'people' ->> 'minimumFaces' + FROM user_metadata + WHERE "userId" = ${userId} + AND key = ${sql.lit(UserMetadataKey.Preferences)}), + '3' + )::int `, + ), ]), ) .groupBy('person.id') diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts index 8b303d04f6..5fa7cb87bd 100644 --- a/server/src/services/person.service.spec.ts +++ b/server/src/services/person.service.spec.ts @@ -57,7 +57,6 @@ describe(PersonService.name, () => { ], }); expect(mocks.person.getAllForUser).toHaveBeenCalledWith({ skip: 0, take: 10 }, auth.user.id, { - minimumFaceCount: 3, withHidden: true, }); }); @@ -84,7 +83,6 @@ describe(PersonService.name, () => { ], }); expect(mocks.person.getAllForUser).toHaveBeenCalledWith({ skip: 0, take: 10 }, auth.user.id, { - minimumFaceCount: 3, withHidden: false, }); }); diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index fde5313f4d..de5767ef87 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -63,9 +63,7 @@ export class PersonService extends BaseService { } closestFaceAssetId = person.faceAssetId; } - const { machineLearning } = await this.getConfig({ withCache: false }); const { items, hasNextPage } = await this.personRepository.getAllForUser(pagination, auth.user.id, { - minimumFaceCount: machineLearning.facialRecognition.minFaces, withHidden, closestFaceAssetId, }); diff --git a/server/src/services/server.service.spec.ts b/server/src/services/server.service.spec.ts index e02945d015..e1575a496a 100644 --- a/server/src/services/server.service.spec.ts +++ b/server/src/services/server.service.spec.ts @@ -168,6 +168,7 @@ describe(ServerService.name, () => { mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json', mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json', maintenanceMode: false, + minFaces: 3, }); expect(mocks.systemMetadata.get).toHaveBeenCalled(); }); diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index aeeb41fcb0..3b66b677a5 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -128,6 +128,7 @@ export class ServerService extends BaseService { mapDarkStyleUrl: config.map.darkStyle, mapLightStyleUrl: config.map.lightStyle, maintenanceMode: false, + minFaces: config.machineLearning.facialRecognition.minFaces, }; } diff --git a/server/src/types.ts b/server/src/types.ts index dde279e5d8..4e5a383cca 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -539,6 +539,7 @@ export type UserPreferences = { people: { enabled: boolean; sidebarWeb: boolean; + minimumFaces: number; }; ratings: { enabled: boolean; diff --git a/server/src/utils/preferences.ts b/server/src/utils/preferences.ts index b25369670a..6b67398d23 100644 --- a/server/src/utils/preferences.ts +++ b/server/src/utils/preferences.ts @@ -21,6 +21,7 @@ const getDefaultPreferences = (): UserPreferences => { people: { enabled: true, sidebarWeb: false, + minimumFaces: 3, }, sharedLinks: { enabled: true, diff --git a/web/src/routes/(user)/user-settings/FeatureSettings.svelte b/web/src/routes/(user)/user-settings/FeatureSettings.svelte index 219552cbd2..8077ba190b 100644 --- a/web/src/routes/(user)/user-settings/FeatureSettings.svelte +++ b/web/src/routes/(user)/user-settings/FeatureSettings.svelte @@ -1,4 +1,5 @@