Compare commits

..

10 Commits

Author SHA1 Message Date
Adam Gastineau
c27cffb731 Update lock file 2026-07-24 11:34:53 -07:00
Adam Gastineau
803ddb97d0 Relocate tool definition 2026-07-24 11:32:14 -07:00
Adam Gastineau
c008f8e1cb Version openapi-generator-cli with mise 2026-07-24 11:24:07 -07:00
Adam Gastineau
fdf2bba853 Merge branch 'main' of https://github.com/immich-app/immich into chore/harden-mobile-openapi-gen 2026-07-24 10:10:18 -07:00
Adam Gastineau
3694d5ef06 Cleaner mise cascade 2026-07-24 10:08:38 -07:00
Adam Gastineau
1f4df8d236 chore(mobile): harden mobile OpenAPI codegen and dependency install 2026-07-24 09:37:24 -07:00
bo0tzz
4a5f13d0e5 fix: don't skip person thumbnail generation if ML is disabled (#30194) 2026-07-24 10:06:32 -04:00
NOBOIKE
5fa920a5f0 fix(web): use RTL transform origin in AdaptiveImage (#30182) 2026-07-24 10:44:33 +02:00
Jason Rasmussen
acc7e6b299 fix: min faces user preference (#30177) 2026-07-23 17:06:09 -04:00
Adam Gastineau
3af94a4805 fix(mobile): properly align iOS widget loading state (#30173) 2026-07-23 12:31:33 -07:00
17 changed files with 109 additions and 782 deletions

View File

@@ -127,9 +127,8 @@ jobs:
with:
packages: ''
- name: Get Packages
working-directory: ./mobile
run: flutter pub get
- name: Install dependencies
run: mise //mobile:install:ci
- name: Generate translation file
run: mise //mobile:codegen:translation
@@ -226,9 +225,8 @@ jobs:
github_token: ${{ steps.token.outputs.token }}
working_directory: ./mobile
- name: Install Flutter dependencies
working-directory: ./mobile
run: flutter pub get
- name: Install dependencies
run: mise //mobile:install:ci
- name: Generate translation files
run: mise //mobile:codegen:translation

View File

@@ -48,9 +48,8 @@ jobs:
github_token: ${{ github.token }}
working_directory: ./mobile
- name: Get packages
working-directory: ./mobile
run: flutter pub get
- name: Install dependencies
run: mise //mobile:install:ci
- name: Fetch base spec from main
run: |

View File

@@ -70,10 +70,10 @@ jobs:
working_directory: ./mobile
- name: Install dependencies
run: flutter pub get
run: mise //mobile:install:ci
- name: Install dependencies for UI package
run: flutter pub get
run: flutter pub get --enforce-lockfile
working-directory: ./mobile/packages/ui
- name: Generate translation files

View File

@@ -610,8 +610,7 @@ jobs:
working_directory: ./mobile
- name: Install dependencies
run: flutter pub get
working-directory: ./mobile
run: mise //mobile:install:ci
- name: Generate translation files
run: mise //mobile:codegen:translation
@@ -920,6 +919,6 @@ jobs:
# emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
# disable-animations: true
# script: |
# flutter pub get
# flutter pub get --enforce-lockfile
# flutter test integration_test
# attempt_limit: 3

View File

@@ -243,6 +243,10 @@ url = "https://nodejs.org/dist/v24.15.0/node-v24.15.0-darwin-x64.tar.gz"
checksum = "sha256:cc5149eabd53779ce1e7bdc5401643622d0c7e6800ade18928a767e940bb0e62"
url = "https://nodejs.org/dist/v24.15.0/node-v24.15.0-win-x64.zip"
[[tools."npm:@openapitools/openapi-generator-cli"]]
version = "2.40.1"
backend = "npm:@openapitools/openapi-generator-cli"
[[tools."npm:oazapfts"]]
version = "7.5.0"
backend = "npm:oazapfts"

View File

@@ -19,6 +19,7 @@ node = "24.15.0"
pnpm = "11.13.1"
terragrunt = "1.1.1"
opentofu = "1.12.4"
"npm:@openapitools/openapi-generator-cli" = "2.40.1"
"npm:oazapfts" = "7.5.0"
"github:extism/cli" = "1.6.3"
"github:webassembly/binaryen" = "version_124"
@@ -61,6 +62,14 @@ run = [
[tasks.open-api-dart]
dir = "open-api"
sources = [
"immich-openapi-specs.json",
"openapitools.json",
"templates/**/*",
"patch/*",
"bin/generate-dart-sdk.sh",
]
outputs = ["../mobile/openapi/lib/"]
run = "bash ./bin/generate-dart-sdk.sh"
[tasks.open-api]

View File

@@ -18,15 +18,21 @@ struct ImmichWidgetView: View {
var body: some View {
if entry.image == nil {
VStack {
Image("LaunchImage")
.tintedWidgetImageModifier()
Text(entry.metadata.error?.errorDescription ?? "")
.minimumScaleFactor(0.25)
.multilineTextAlignment(.center)
.foregroundStyle(.secondary)
}
.padding(16)
Image("LaunchImage")
.tintedWidgetImageModifier()
.overlay(alignment: .bottom) {
if let error = entry.metadata.error?.errorDescription {
Text(error)
.minimumScaleFactor(0.25)
.multilineTextAlignment(.center)
.foregroundStyle(.secondary)
.fixedSize()
.alignmentGuide(.bottom) { dimensions in
// Place the text below the bottom of the image
dimensions[.top] - 8
}
}
}
} else {
ZStack(alignment: .leading) {
Color.clear.overlay(

View File

@@ -83,13 +83,7 @@ struct ImmichMemoryProvider: TimelineProvider {
return
}
let memories: [MemoryResult]
do {
memories = try await api.fetchMemory(for: Date.now)
} catch {
completion(ImageEntry.handleError(for: cacheKey))
return
}
let memories = try await api.fetchMemory(for: Date.now)
await withTaskGroup(of: ImageEntry?.self) { group in
var totalAssets = 0

View File

@@ -29,45 +29,6 @@ url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/wind
version = "1.37.0"
backend = "github:CQLabs/homebrew-dcm"
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-arm64"]
checksum = "sha256:253da2512b149913dfe345bf9a62a79acb2d730f66e71162ba4a92dfc4224b82"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-arm-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543838"
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-arm64-musl"]
checksum = "sha256:253da2512b149913dfe345bf9a62a79acb2d730f66e71162ba4a92dfc4224b82"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-arm-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543838"
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-x64"]
checksum = "sha256:477e086d4099c12f21e5ccd83b005d5fb945dd4cac4fd127fd9a08d7649af1cf"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-x64-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543797"
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-x64-musl"]
checksum = "sha256:477e086d4099c12f21e5ccd83b005d5fb945dd4cac4fd127fd9a08d7649af1cf"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-x64-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543797"
[tools."github:CQLabs/homebrew-dcm"."platforms.macos-arm64"]
checksum = "sha256:30bede64367d09067093cc57af6ec9496d7717898138ded5cb98a16ac8dd9d93"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-macos-arm-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543757"
[tools."github:CQLabs/homebrew-dcm"."platforms.macos-x64"]
checksum = "sha256:e56cb99872be7445a4de1d37e5438ca70e3bcd83be7a2b9b385e3538881f8068"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-macos-x64-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543727"
[tools."github:CQLabs/homebrew-dcm"."platforms.windows-x64"]
checksum = "sha256:f133470daa3fb0427f039b424392af7e917d7e7db6b556aa2a968ab0e31587da"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-windows-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543660"
[[tools."github:CQLabs/homebrew-dcm"]]
version = "1.37.0"
backend = "github:CQLabs/homebrew-dcm"
[tools."github:CQLabs/homebrew-dcm".options]
asset_pattern = "dcm-linux-arm-release.zip"
@@ -102,6 +63,18 @@ url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/4045
version = "1.37.0"
backend = "github:CQLabs/homebrew-dcm"
[tools."github:CQLabs/homebrew-dcm".options]
asset_pattern = "dcm-macos-arm-release.zip"
[tools."github:CQLabs/homebrew-dcm"."platforms.macos-arm64"]
checksum = "sha256:30bede64367d09067093cc57af6ec9496d7717898138ded5cb98a16ac8dd9d93"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-macos-arm-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543757"
[[tools."github:CQLabs/homebrew-dcm"]]
version = "1.37.0"
backend = "github:CQLabs/homebrew-dcm"
[tools."github:CQLabs/homebrew-dcm".options]
asset_pattern = "dcm-macos-x64-release.zip"
@@ -122,42 +95,6 @@ checksum = "sha256:f133470daa3fb0427f039b424392af7e917d7e7db6b556aa2a968ab0e3158
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-windows-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543660"
[[tools."github:CQLabs/homebrew-dcm"]]
version = "1.37.0"
backend = "github:CQLabs/homebrew-dcm"
[tools."github:CQLabs/homebrew-dcm".options]
asset_pattern = "dcm-macos-arm-release.zip"
[tools."github:CQLabs/homebrew-dcm"."platforms.macos-arm64"]
checksum = "sha256:30bede64367d09067093cc57af6ec9496d7717898138ded5cb98a16ac8dd9d93"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-macos-arm-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543757"
[[tools.java]]
version = "21.0.2"
backend = "core:java"
[tools.java."platforms.linux-arm64"]
checksum = "sha256:08db1392a48d4eb5ea5315cf8f18b89dbaf36cda663ba882cf03c704c9257ec2"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-aarch64_bin.tar.gz"
[tools.java."platforms.linux-x64"]
checksum = "sha256:a2def047a73941e01a73739f92755f86b895811afb1f91243db214cff5bdac3f"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-x64_bin.tar.gz"
[tools.java."platforms.macos-arm64"]
checksum = "sha256:b3d588e16ec1e0ef9805d8a696591bd518a5cea62567da8f53b5ce32d11d22e4"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_macos-aarch64_bin.tar.gz"
[tools.java."platforms.macos-x64"]
checksum = "sha256:8fd09e15dc406387a0aba70bf5d99692874e999bf9cd9208b452b5d76ac922d3"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_macos-x64_bin.tar.gz"
[tools.java."platforms.windows-x64"]
checksum = "sha256:b6c17e747ae78cdd6de4d7532b3164b277daee97c007d3eaa2b39cca99882664"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_windows-x64_bin.zip"
[[tools.java]]
version = "21.0.2"
backend = "core:java"
@@ -184,3 +121,7 @@ url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c55847602
[tools.java."platforms.windows-x64"]
checksum = "sha256:b6c17e747ae78cdd6de4d7532b3164b277daee97c007d3eaa2b39cca99882664"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_windows-x64_bin.zip"
[[tools."npm:@openapitools/openapi-generator-cli"]]
version = "2.40.1"
backend = "npm:@openapitools/openapi-generator-cli"

View File

@@ -91,8 +91,15 @@ run = "dart run drift_dev make-migrations"
[tasks.install]
alias = "install"
description = "Install flutter dependencies"
depends = ["//:open-api-dart"]
run = "flutter pub get"
[tasks."install:ci"]
description = "Install flutter dependencies in CI"
run = [
{ task = "install", args = ["--enforce-lockfile"] },
]
[tasks.start]
alias = "start"
description = "Start flutter app"

View File

@@ -1,22 +1,19 @@
#!/usr/bin/env bash
OPENAPI_GENERATOR_VERSION=v7.24.0
set -euo pipefail
# usage: ./bin/generate-dart-sdk.sh
TEMPLATE_DIR=$(mktemp -d)
trap 'rm -rf "$TEMPLATE_DIR"' EXIT
# Installed via mise
openapi-generator-cli author template -g dart -o "$TEMPLATE_DIR"
patch --no-backup-if-mismatch -u "$TEMPLATE_DIR/api.mustache" <./templates/mobile/api.mustache.patch
patch --no-backup-if-mismatch -u "$TEMPLATE_DIR/serialization/native/native_class.mustache" <./templates/mobile/serialization/native/native_class.mustache.patch
rm -rf ../mobile/openapi
cd ./templates/mobile/serialization/native
wget -O native_class.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/$OPENAPI_GENERATOR_VERSION/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache
patch --no-backup-if-mismatch -u native_class.mustache <native_class.mustache.patch
cd ../../
wget -O api.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/$OPENAPI_GENERATOR_VERSION/modules/openapi-generator/src/main/resources/dart2/api.mustache
patch --no-backup-if-mismatch -u api.mustache <api.mustache.patch
cd ../../
pnpm dlx --allow-build="" @openapitools/openapi-generator-cli generate -g dart -i ./immich-openapi-specs.json -o ../mobile/openapi -t ./templates/mobile --additional-properties=useOptional=true
openapi-generator-cli generate -g dart -i ./immich-openapi-specs.json -o ../mobile/openapi -t "$TEMPLATE_DIR" --additional-properties=useOptional=true
# Post generate patches
patch --no-backup-if-mismatch -u ../mobile/openapi/lib/api_client.dart <./patch/api_client.dart.patch

View File

@@ -1,196 +0,0 @@
{{>header}}
{{>part_of}}
{{#operations}}
class {{{classname}}} {
{{{classname}}}([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
final ApiClient apiClient;
{{#operation}}
{{#summary}}
/// {{{.}}}
{{/summary}}
{{#notes}}
{{#summary}}
///
{{/summary}}
/// {{{notes}}}
///
/// Note: This method returns the HTTP [Response].
{{/notes}}
{{^notes}}
{{#summary}}
///
/// Note: This method returns the HTTP [Response].
{{/summary}}
{{^summary}}
/// Performs an HTTP '{{{httpMethod}}} {{{path}}}' operation and returns the [Response].
{{/summary}}
{{/notes}}
{{#hasParams}}
{{#summary}}
///
{{/summary}}
{{^summary}}
{{#notes}}
///
{{/notes}}
{{/summary}}
/// Parameters:
///
{{/hasParams}}
{{#allParams}}
/// * [{{{dataType}}}] {{{paramName}}}{{#required}} (required){{/required}}{{#optional}} (optional){{/optional}}:
{{#description}}
/// {{{.}}}
{{/description}}
{{^-last}}
///
{{/-last}}
{{/allParams}}
Future<Response> {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}}, {{/required}}{{/allParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}}, {{/required}}{{/allParams}}Future<void>? abortTrigger, }) async {
// ignore: prefer_const_declarations
final apiPath = r'{{{path}}}'{{#pathParams}}
.replaceAll({{=<% %>=}}'{<% baseName %>}'<%={{ }}=%>, {{{paramName}}}{{^isString}}.toString(){{/isString}}){{/pathParams}};
// ignore: prefer_final_locals
Object? postBody{{#bodyParam}} = {{{paramName}}}{{/bodyParam}};
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
{{#hasQueryParams}}
{{#queryParams}}
{{^required}}
if ({{{paramName}}} != null) {
{{/required}}
queryParams.addAll(_queryParams('{{{collectionFormat}}}', '{{{baseName}}}', {{{paramName}}}));
{{^required}}
}
{{/required}}
{{/queryParams}}
{{/hasQueryParams}}
{{#hasHeaderParams}}
{{#headerParams}}
{{#required}}
headerParams[r'{{{baseName}}}'] = parameterToString({{{paramName}}});
{{/required}}
{{^required}}
if ({{{paramName}}} != null) {
headerParams[r'{{{baseName}}}'] = parameterToString({{{paramName}}});
}
{{/required}}
{{/headerParams}}
{{/hasHeaderParams}}
const contentTypes = <String>[{{#prioritizedContentTypes}}'{{{mediaType}}}'{{^-last}}, {{/-last}}{{/prioritizedContentTypes}}];
{{#isMultipart}}
bool hasFields = false;
final mp = MultipartRequest('{{{httpMethod}}}', Uri.parse(apiPath));
{{#formParams}}
{{^isFile}}
if ({{{paramName}}} != null) {
hasFields = true;
mp.fields[r'{{{baseName}}}'] = parameterToString({{{paramName}}});
}
{{/isFile}}
{{#isFile}}
if ({{{paramName}}} != null) {
hasFields = true;
mp.fields[r'{{{baseName}}}'] = {{{paramName}}}.field;
mp.files.add({{{paramName}}});
}
{{/isFile}}
{{/formParams}}
if (hasFields) {
postBody = mp;
}
{{/isMultipart}}
{{^isMultipart}}
{{#formParams}}
{{^isFile}}
if ({{{paramName}}} != null) {
formParams[r'{{{baseName}}}'] = parameterToString({{{paramName}}});
}
{{/isFile}}
{{/formParams}}
{{/isMultipart}}
return apiClient.invokeAPI(
apiPath,
'{{{httpMethod}}}',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
abortTrigger: abortTrigger,
);
}
{{#summary}}
/// {{{.}}}
{{/summary}}
{{#notes}}
{{#summary}}
///
{{/summary}}
/// {{{notes}}}
{{/notes}}
{{#hasParams}}
{{#summary}}
///
{{/summary}}
{{^summary}}
{{#notes}}
///
{{/notes}}
{{/summary}}
/// Parameters:
///
{{/hasParams}}
{{#allParams}}
/// * [{{{dataType}}}] {{{paramName}}}{{#required}} (required){{/required}}{{#optional}} (optional){{/optional}}:
{{#description}}
/// {{{.}}}
{{/description}}
{{^-last}}
///
{{/-last}}
{{/allParams}}
Future<{{#returnType}}{{{.}}}?{{/returnType}}{{^returnType}}void{{/returnType}}> {{{nickname}}}({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}}, {{/required}}{{/allParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}}, {{/required}}{{/allParams}}Future<void>? 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));
}
{{#returnType}}
// 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) {
{{#native_serialization}}
{{#isArray}}
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, '{{{returnType}}}') as List)
.cast<{{{returnBaseType}}}>()
.{{#uniqueItems}}toSet(){{/uniqueItems}}{{^uniqueItems}}toList(growable: false){{/uniqueItems}};
{{/isArray}}
{{^isArray}}
{{#isMap}}
return {{{returnType}}}.from(await apiClient.deserializeAsync(await _decodeBodyBytes(response), '{{{returnType}}}'),);
{{/isMap}}
{{^isMap}}
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), '{{{returnType}}}',) as {{{returnType}}};
{{/isMap}}{{/isArray}}{{/native_serialization}}
}
return null;
{{/returnType}}
}
{{/operation}}
}
{{/operations}}

View File

@@ -1,458 +0,0 @@
class {{{classname}}} {
{{>dart_constructor}}
{{#vars}}
{{#description}}
/// {{{.}}}
{{/description}}
{{^isEnum}}
{{#minimum}}
{{#description}}
///
{{/description}}
/// Minimum value: {{{.}}}
{{/minimum}}
{{#maximum}}
{{#description}}
{{^minimum}}
///
{{/minimum}}
{{/description}}
/// Maximum value: {{{.}}}
{{/maximum}}
{{^isNullable}}
{{^required}}
{{^defaultValue}}
///
/// 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.
///
{{/defaultValue}}
{{/required}}
{{/isNullable}}
{{/isEnum}}
{{#required}}
{{#useFinalProperties}}final {{/useFinalProperties}}{{{datatypeWithEnum}}}{{#isNullable}}?{{/isNullable}} {{{name}}};
{{/required}}
{{^required}}
{{#vendorExtensions.x-is-optional}}
{{#useFinalProperties}}final {{/useFinalProperties}}{{{datatypeWithEnum}}} {{{name}}};
{{/vendorExtensions.x-is-optional}}
{{^vendorExtensions.x-is-optional}}
{{#useFinalProperties}}final {{/useFinalProperties}}{{{datatypeWithEnum}}}{{#isNullable}}?{{/isNullable}}{{^isNullable}}{{^defaultValue}}?{{/defaultValue}}{{/isNullable}} {{{name}}};
{{/vendorExtensions.x-is-optional}}
{{/required}}
{{/vars}}
@override
bool operator ==(Object other) => identical(this, other) || other is {{{classname}}} &&
{{#vars}}
{{#isMap}}_deepEquality.equals(other.{{{name}}}, {{{name}}}){{/isMap}}{{^isMap}}{{#isArray}}_deepEquality.equals(other.{{{name}}}, {{{name}}}){{/isArray}}{{^isArray}}other.{{{name}}} == {{{name}}}{{/isArray}}{{/isMap}}{{^-last}} &&{{/-last}}{{#-last}};{{/-last}}
{{/vars}}
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
{{#vars}}
({{#isNullable}}{{{name}}} == null ? 0 : {{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}{{{name}}} == null ? 0 : {{/defaultValue}}{{/required}}{{/isNullable}}{{{name}}}{{#isNullable}}!{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}!{{/defaultValue}}{{/required}}{{/isNullable}}.hashCode){{^-last}} +{{/-last}}{{#-last}};{{/-last}}
{{/vars}}
@override
String toString() => '{{{classname}}}[{{#vars}}{{{name}}}=${{{name}}}{{^-last}}, {{/-last}}{{/vars}}]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
{{#vars}}
{{#vendorExtensions.x-is-optional}}
if (this.{{{name}}}.isPresent) {
final value = this.{{{name}}}.value;
{{#isDateTime}}
{{#pattern}}
json[r'{{{baseName}}}'] = value == null ? null : (_isEpochMarker(r'{{{pattern}}}')
? value.millisecondsSinceEpoch
: value.toUtc().toIso8601String());
{{/pattern}}
{{^pattern}}
json[r'{{{baseName}}}'] = value == null ? null : value.toUtc().toIso8601String();
{{/pattern}}
{{/isDateTime}}
{{#isDate}}
{{#pattern}}
json[r'{{{baseName}}}'] = value == null ? null : (_isEpochMarker(r'{{{pattern}}}')
? value.millisecondsSinceEpoch
: _dateFormatter.format(value.toUtc()));
{{/pattern}}
{{^pattern}}
json[r'{{{baseName}}}'] = value == null ? null : _dateFormatter.format(value.toUtc());
{{/pattern}}
{{/isDate}}
{{^isDateTime}}
{{^isDate}}
json[r'{{{baseName}}}'] = value{{#isArray}}{{#uniqueItems}} == null ? null : value.toList(growable: false){{/uniqueItems}}{{/isArray}};
{{/isDate}}
{{/isDateTime}}
}
{{/vendorExtensions.x-is-optional}}
{{^vendorExtensions.x-is-optional}}
{{#isNullable}}
if (this.{{{name}}} != null) {
{{/isNullable}}
{{^isNullable}}
{{^required}}
{{^defaultValue}}
if (this.{{{name}}} != null) {
{{/defaultValue}}
{{/required}}
{{/isNullable}}
{{#isDateTime}}
{{#pattern}}
json[r'{{{baseName}}}'] = _isEpochMarker(r'{{{pattern}}}')
? this.{{{name}}}{{#isNullable}}!{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}!{{/defaultValue}}{{/required}}{{/isNullable}}.millisecondsSinceEpoch
: this.{{{name}}}{{#isNullable}}!{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}!{{/defaultValue}}{{/required}}{{/isNullable}}.toUtc().toIso8601String();
{{/pattern}}
{{^pattern}}
json[r'{{{baseName}}}'] = this.{{{name}}}{{#isNullable}}!{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}!{{/defaultValue}}{{/required}}{{/isNullable}}.toUtc().toIso8601String();
{{/pattern}}
{{/isDateTime}}
{{#isDate}}
{{#pattern}}
json[r'{{{baseName}}}'] = _isEpochMarker(r'{{{pattern}}}')
? this.{{{name}}}{{#isNullable}}!{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}!{{/defaultValue}}{{/required}}{{/isNullable}}.millisecondsSinceEpoch
: _dateFormatter.format(this.{{{name}}}{{#isNullable}}!{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}!{{/defaultValue}}{{/required}}{{/isNullable}}.toUtc());
{{/pattern}}
{{^pattern}}
json[r'{{{baseName}}}'] = _dateFormatter.format(this.{{{name}}}{{#isNullable}}!{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}!{{/defaultValue}}{{/required}}{{/isNullable}}.toUtc());
{{/pattern}}
{{/isDate}}
{{^isDateTime}}
{{^isDate}}
json[r'{{{baseName}}}'] = this.{{{name}}}{{#isArray}}{{#uniqueItems}}{{#isNullable}}!{{/isNullable}}.toList(growable: false){{/uniqueItems}}{{/isArray}};
{{/isDate}}
{{/isDateTime}}
{{#isNullable}}
} else {
json[r'{{{baseName}}}'] = null;
}
{{/isNullable}}
{{^isNullable}}
{{^required}}
{{^defaultValue}}
} else {
json[r'{{{baseName}}}'] = null;
}
{{/defaultValue}}
{{/required}}
{{/isNullable}}
{{/vendorExtensions.x-is-optional}}
{{/vars}}
return json;
}
{{#useFinalProperties}}
/// Clones this instance of [{{{classname}}}] and returns a new one where some of the
/// properties have changed.
{{{classname}}} copyWith({
{{#vars}}
{{{datatypeWithEnum}}}? {{{name}}},{{#isNullable}}
bool {{{name}}}SetToNull = false,{{/isNullable}}
{{/vars}}
}) => {{{classname}}}(
{{#vars}}
{{{name}}}: {{#isNullable}}{{{name}}}SetToNull ? null : {{/isNullable}}{{{name}}} ?? this.{{{name}}},
{{/vars}}
);
{{/useFinalProperties}}
/// Returns a new [{{{classname}}}] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static {{{classname}}}? fromJson(dynamic value) {
upgradeDto(value, "{{{classname}}}");
if (value is Map) {
final json = value.cast<String, dynamic>();
return {{{classname}}}(
{{#vars}}
{{#isDateTime}}
{{#vendorExtensions.x-is-optional}}
{{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(mapDateTime(json, r'{{{baseName}}}', r'{{{pattern}}}')) : const Optional.absent(),
{{/vendorExtensions.x-is-optional}}
{{^vendorExtensions.x-is-optional}}
{{{name}}}: mapDateTime(json, r'{{{baseName}}}', r'{{{pattern}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
{{/vendorExtensions.x-is-optional}}
{{/isDateTime}}
{{#isDate}}
{{#vendorExtensions.x-is-optional}}
{{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(mapDateTime(json, r'{{{baseName}}}', r'{{{pattern}}}')) : const Optional.absent(),
{{/vendorExtensions.x-is-optional}}
{{^vendorExtensions.x-is-optional}}
{{{name}}}: mapDateTime(json, r'{{{baseName}}}', r'{{{pattern}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
{{/vendorExtensions.x-is-optional}}
{{/isDate}}
{{^isDateTime}}
{{^isDate}}
{{#complexType}}
{{#isArray}}
{{#items.isArray}}
{{#vendorExtensions.x-is-optional}}
{{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(json[r'{{{baseName}}}'] is List
? (json[r'{{{baseName}}}'] as List).map((e) =>
{{#items.complexType}}
e == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.complexType}}>[]{{/items.isNullable}} : {{items.complexType}}.listFromJson(e){{#uniqueItems}}.toSet(){{/uniqueItems}}
{{/items.complexType}}
{{^items.complexType}}
e == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}>[]{{/items.isNullable}} : (e as List).map((value) => value as {{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}).toList(growable: false)
{{/items.complexType}}
).toList()
: {{#isNullable}}null{{/isNullable}}{{^isNullable}}const []{{/isNullable}}) : const Optional.absent(),
{{/vendorExtensions.x-is-optional}}
{{^vendorExtensions.x-is-optional}}
{{{name}}}: json[r'{{{baseName}}}'] is List
? (json[r'{{{baseName}}}'] as List).map((e) =>
{{#items.complexType}}
e == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.complexType}}>[]{{/items.isNullable}} : {{items.complexType}}.listFromJson(e){{#uniqueItems}}.toSet(){{/uniqueItems}}
{{/items.complexType}}
{{^items.complexType}}
e == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}>[]{{/items.isNullable}} : (e as List).map((value) => value as {{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}).toList(growable: false)
{{/items.complexType}}
).toList()
: {{#isNullable}}null{{/isNullable}}{{^isNullable}}const []{{/isNullable}},
{{/vendorExtensions.x-is-optional}}
{{/items.isArray}}
{{^items.isArray}}
{{#vendorExtensions.x-is-optional}}
{{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present({{{complexType}}}.listFromJson(json[r'{{{baseName}}}']){{#uniqueItems}}.toSet(){{/uniqueItems}}) : const Optional.absent(),
{{/vendorExtensions.x-is-optional}}
{{^vendorExtensions.x-is-optional}}
{{{name}}}: {{{complexType}}}.listFromJson(json[r'{{{baseName}}}']){{#uniqueItems}}.toSet(){{/uniqueItems}},
{{/vendorExtensions.x-is-optional}}
{{/items.isArray}}
{{/isArray}}
{{^isArray}}
{{#isMap}}
{{#items.isArray}}
{{#vendorExtensions.x-is-optional}}
{{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(json[r'{{{baseName}}}'] == null ? null
{{#items.complexType}}
: {{items.complexType}}.mapListFromJson(json[r'{{{baseName}}}'])) : const Optional.absent(),
{{/items.complexType}}
{{^items.complexType}}
: (json[r'{{{baseName}}}'] as Map<String, dynamic>).map((k, v) => MapEntry(k, v == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}>[]{{/items.isNullable}} : (v as List).map((value) => value as {{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}).toList(growable: false)))) : const Optional.absent(),
{{/items.complexType}}
{{/vendorExtensions.x-is-optional}}
{{^vendorExtensions.x-is-optional}}
{{{name}}}: json[r'{{{baseName}}}'] == null
? {{#defaultValue}}{{{.}}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}}
{{#items.complexType}}
: {{items.complexType}}.mapListFromJson(json[r'{{{baseName}}}']),
{{/items.complexType}}
{{^items.complexType}}
: (json[r'{{{baseName}}}'] as Map<String, dynamic>).map((k, v) => MapEntry(k, v == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}>[]{{/items.isNullable}} : (v as List).map((value) => value as {{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}).toList(growable: false))),
{{/items.complexType}}
{{/vendorExtensions.x-is-optional}}
{{/items.isArray}}
{{^items.isArray}}
{{#items.isMap}}
{{#items.complexType}}
{{#vendorExtensions.x-is-optional}}
{{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present({{items.complexType}}.mapFromJson(json[r'{{{baseName}}}'])) : const Optional.absent(),
{{/vendorExtensions.x-is-optional}}
{{^vendorExtensions.x-is-optional}}
{{{name}}}: {{items.complexType}}.mapFromJson(json[r'{{{baseName}}}']),
{{/vendorExtensions.x-is-optional}}
{{/items.complexType}}
{{^items.complexType}}
{{#vendorExtensions.x-is-optional}}
{{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(mapCastOfType<String, dynamic>(json, r'{{{baseName}}}')) : const Optional.absent(),
{{/vendorExtensions.x-is-optional}}
{{^vendorExtensions.x-is-optional}}
{{{name}}}: mapCastOfType<String, dynamic>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
{{/vendorExtensions.x-is-optional}}
{{/items.complexType}}
{{/items.isMap}}
{{^items.isMap}}
{{#items.complexType}}
{{#vendorExtensions.x-is-optional}}
{{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present({{{items.complexType}}}.mapFromJson(json[r'{{{baseName}}}'])) : const Optional.absent(),
{{/vendorExtensions.x-is-optional}}
{{^vendorExtensions.x-is-optional}}
{{{name}}}: {{{items.complexType}}}.mapFromJson(json[r'{{{baseName}}}']),
{{/vendorExtensions.x-is-optional}}
{{/items.complexType}}
{{^items.complexType}}
{{#vendorExtensions.x-is-optional}}
{{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(mapCastOfType<String, {{items.dataType}}>(json, r'{{{baseName}}}')) : const Optional.absent(),
{{/vendorExtensions.x-is-optional}}
{{^vendorExtensions.x-is-optional}}
{{{name}}}: mapCastOfType<String, {{items.dataType}}>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
{{/vendorExtensions.x-is-optional}}
{{/items.complexType}}
{{/items.isMap}}
{{/items.isArray}}
{{/isMap}}
{{^isMap}}
{{#isBinary}}
{{{name}}}: null, // No support for decoding binary content from JSON
{{/isBinary}}
{{^isBinary}}
{{#vendorExtensions.x-is-optional}}
{{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present({{{complexType}}}.fromJson(json[r'{{{baseName}}}'])) : const Optional.absent(),
{{/vendorExtensions.x-is-optional}}
{{^vendorExtensions.x-is-optional}}
{{{name}}}: {{{complexType}}}.fromJson(json[r'{{{baseName}}}']){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
{{/vendorExtensions.x-is-optional}}
{{/isBinary}}
{{/isMap}}
{{/isArray}}
{{/complexType}}
{{^complexType}}
{{#isArray}}
{{#isEnum}}
{{#vendorExtensions.x-is-optional}}
{{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present({{{items.datatypeWithEnum}}}.listFromJson(json[r'{{{baseName}}}']){{#uniqueItems}}.toSet(){{/uniqueItems}}) : const Optional.absent(),
{{/vendorExtensions.x-is-optional}}
{{^vendorExtensions.x-is-optional}}
{{{name}}}: {{{items.datatypeWithEnum}}}.listFromJson(json[r'{{{baseName}}}']){{#uniqueItems}}.toSet(){{/uniqueItems}},
{{/vendorExtensions.x-is-optional}}
{{/isEnum}}
{{^isEnum}}
{{#vendorExtensions.x-is-optional}}
{{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(json[r'{{{baseName}}}'] is Iterable
? (json[r'{{{baseName}}}'] as Iterable).cast<{{{items.datatype}}}>().{{#uniqueItems}}toSet(){{/uniqueItems}}{{^uniqueItems}}toList(growable: false){{/uniqueItems}}
: {{#defaultValue}}{{{.}}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}}) : const Optional.absent(),
{{/vendorExtensions.x-is-optional}}
{{^vendorExtensions.x-is-optional}}
{{{name}}}: json[r'{{{baseName}}}'] is Iterable
? (json[r'{{{baseName}}}'] as Iterable).cast<{{{items.datatype}}}>().{{#uniqueItems}}toSet(){{/uniqueItems}}{{^uniqueItems}}toList(growable: false){{/uniqueItems}}
: {{#defaultValue}}{{{.}}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}},
{{/vendorExtensions.x-is-optional}}
{{/isEnum}}
{{/isArray}}
{{^isArray}}
{{#isMap}}
{{#vendorExtensions.x-is-optional}}
{{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(mapCastOfType<String, {{{items.datatype}}}>(json, r'{{{baseName}}}')) : const Optional.absent(),
{{/vendorExtensions.x-is-optional}}
{{^vendorExtensions.x-is-optional}}
{{{name}}}: mapCastOfType<String, {{{items.datatype}}}>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
{{/vendorExtensions.x-is-optional}}
{{/isMap}}
{{^isMap}}
{{#isNumber}}
{{#vendorExtensions.x-is-optional}}
{{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(json[r'{{{baseName}}}'] == null ? null : num.parse('${json[r'{{{baseName}}}']}')) : const Optional.absent(),
{{/vendorExtensions.x-is-optional}}
{{^vendorExtensions.x-is-optional}}
{{{name}}}: {{#isNullable}}json[r'{{{baseName}}}'] == null
? {{#defaultValue}}{{{.}}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}}
: {{/isNullable}}{{{datatypeWithEnum}}}.parse('${json[r'{{{baseName}}}']}'),
{{/vendorExtensions.x-is-optional}}
{{/isNumber}}
{{^isNumber}}
{{#vendorExtensions.x-original-is-integer}}
{{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(json[r'{{{baseName}}}'] == null ? null : int.parse('${json[r'{{{baseName}}}']}')) : const Optional.absent(),
{{/vendorExtensions.x-original-is-integer}}
{{^vendorExtensions.x-original-is-integer}}
{{#vendorExtensions.x-original-is-number}}
{{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(json[r'{{{baseName}}}'] == null ? null : num.parse('${json[r'{{{baseName}}}']}')) : const Optional.absent(),
{{/vendorExtensions.x-original-is-number}}
{{^vendorExtensions.x-original-is-number}}
{{^isEnum}}
{{#vendorExtensions.x-is-optional}}
{{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(mapValueOfType<{{#vendorExtensions.x-unwrapped-datatype}}{{{vendorExtensions.x-unwrapped-datatype}}}{{/vendorExtensions.x-unwrapped-datatype}}{{^vendorExtensions.x-unwrapped-datatype}}{{{datatypeWithEnum}}}{{/vendorExtensions.x-unwrapped-datatype}}>(json, r'{{{baseName}}}')) : const Optional.absent(),
{{/vendorExtensions.x-is-optional}}
{{^vendorExtensions.x-is-optional}}
{{{name}}}: mapValueOfType<{{{datatypeWithEnum}}}>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
{{/vendorExtensions.x-is-optional}}
{{/isEnum}}
{{#isEnum}}
{{#vendorExtensions.x-is-optional}}
{{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present({{{enumName}}}.fromJson(json[r'{{{baseName}}}'])) : const Optional.absent(),
{{/vendorExtensions.x-is-optional}}
{{^vendorExtensions.x-is-optional}}
{{{name}}}: {{{enumName}}}.fromJson(json[r'{{{baseName}}}']){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? const {{{enumName}}}._({{{.}}}){{/defaultValue}}{{/required}},
{{/vendorExtensions.x-is-optional}}
{{/isEnum}}
{{/vendorExtensions.x-original-is-number}}
{{/vendorExtensions.x-original-is-integer}}
{{/isNumber}}
{{/isMap}}
{{/isArray}}
{{/complexType}}
{{/isDate}}
{{/isDateTime}}
{{/vars}}
);
}
return null;
}
static List<{{{classname}}}> listFromJson(dynamic json, {bool growable = false,}) {
final result = <{{{classname}}}>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = {{{classname}}}.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, {{{classname}}}> mapFromJson(dynamic json) {
final map = <String, {{{classname}}}>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = {{{classname}}}.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of {{{classname}}}-objects as value to a dart map
static Map<String, List<{{{classname}}}>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<{{{classname}}}>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = {{{classname}}}.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
{{#vars}}
{{#required}}
'{{{baseName}}}',
{{/required}}
{{/vars}}
};
}
{{#vars}}
{{^isModel}}
{{#isEnum}}
{{^isContainer}}
{{>serialization/native/native_enum_inline}}
{{/isContainer}}
{{#isContainer}}
{{#mostInnerItems}}
{{>serialization/native/native_enum_inline}}
{{/mostInnerItems}}
{{/isContainer}}
{{/isEnum}}
{{/isModel}}
{{/vars}}

View File

@@ -0,0 +1,25 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await sql`
INSERT INTO "user_metadata" ("userId", "key", "value")
SELECT "user"."id", 'preferences', jsonb_build_object('people', jsonb_build_object('minimumFaces', "config"."minFaces"))
FROM "user"
CROSS JOIN (
SELECT "value"->'machineLearning'->'facialRecognition'->'minFaces' AS "minFaces"
FROM "system_metadata"
WHERE "key" = 'system-config'
AND "value"->'machineLearning'->'facialRecognition'->'minFaces' IS NOT NULL
) AS "config"
ON CONFLICT ("userId", "key") DO UPDATE
SET "value" = "user_metadata"."value" || jsonb_build_object(
'people',
COALESCE("user_metadata"."value"->'people', '{}'::jsonb) || (EXCLUDED."value"->'people')
)
WHERE "user_metadata"."value"->'people'->'minimumFaces' IS NULL
`.execute(db);
}
export async function down(): Promise<void> {
// not supported
}

View File

@@ -1507,12 +1507,17 @@ describe(MediaService.name, () => {
});
describe('handleGeneratePersonThumbnail', () => {
it('should skip if machine learning is disabled', async () => {
it('should generate a thumbnail even if machine learning is disabled', async () => {
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.machineLearningDisabled);
mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.newThumbnailMiddle);
mocks.media.generateThumbnail.mockResolvedValue();
mocks.media.decodeImage.mockResolvedValue({
data: Buffer.from(''),
info: { width: 1000, height: 1000 } as OutputInfo,
});
await expect(sut.handleGeneratePersonThumbnail({ id: 'person-1' })).resolves.toBe(JobStatus.Skipped);
expect(mocks.asset.getByIds).not.toHaveBeenCalled();
expect(mocks.systemMetadata.get).toHaveBeenCalled();
await expect(sut.handleGeneratePersonThumbnail({ id: 'person-1' })).resolves.toBe(JobStatus.Success);
expect(mocks.media.generateThumbnail).toHaveBeenCalled();
});
it('should skip a person not found', async () => {

View File

@@ -43,7 +43,7 @@ import { getAssetFile, getDimensions } from 'src/utils/asset.util';
import { checkFaceVisibility, checkOcrVisibility } from 'src/utils/editor';
import { BaseConfig, ThumbnailConfig } from 'src/utils/media';
import { mimeTypes } from 'src/utils/mime-types';
import { clamp, isFaceImportEnabled, isFacialRecognitionEnabled } from 'src/utils/misc';
import { clamp } from 'src/utils/misc';
import { getOutputDimensions } from 'src/utils/transform';
interface UpsertFileOptions {
@@ -410,11 +410,7 @@ export class MediaService extends BaseService {
@OnJob({ name: JobName.PersonGenerateThumbnail, queue: QueueName.ThumbnailGeneration })
async handleGeneratePersonThumbnail({ id }: JobOf<JobName.PersonGenerateThumbnail>): Promise<JobStatus> {
const { machineLearning, metadata, image } = await this.getConfig({ withCache: true });
if (!isFacialRecognitionEnabled(machineLearning) && !isFaceImportEnabled(metadata)) {
return JobStatus.Skipped;
}
const { image } = await this.getConfig({ withCache: true });
const data = await this.personRepository.getDataForThumbnailGenerationJob(id);
if (!data) {
this.logger.error(`Could not generate person thumbnail for ${id}: missing data`);

View File

@@ -63,6 +63,7 @@
import { toTimelineAsset } from '$lib/utils/timeline-util';
import type { AssetResponseDto, SharedLinkResponseDto } from '@immich/sdk';
import { untrack, type Snippet } from 'svelte';
import { languageManager } from '$lib/managers/language-manager.svelte';
type Props = {
asset: AssetResponseDto;
@@ -232,7 +233,7 @@
style:width={rasterWidth}
style:height={rasterHeight}
style:transform="scale({rasterScale})"
style:transform-origin="0 0"
style:transform-origin={languageManager.rtl ? 'right top' : 'left top'}
style:will-change={maxRasterPixels > 0 ? 'transform' : undefined}
>
{#if show.alphaBackground}