Compare commits

..

5 Commits

Author SHA1 Message Date
mertalev 8d2a849edc optimized scrfd code 2024-06-09 23:03:34 -04:00
mertalev fb4fe5d40b change log message 2024-06-08 21:24:23 -04:00
mertalev 717961ce7b add tests 2024-06-08 21:24:23 -04:00
mertalev 259386cf13 refactor 2024-06-08 21:24:23 -04:00
mertalev 7e587c2703 squeeze output dims 2024-06-08 21:24:23 -04:00
394 changed files with 14575 additions and 23174 deletions
+2 -2
View File
@@ -1,11 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: I have a question or need support
url: https://discord.immich.app
url: https://discord.gg/D8JsnBEuKb
about: We use GitHub for tracking bugs, please check out our Discord channel for freaky fast support.
- name: Feature Request
url: https://github.com/immich-app/immich/discussions/new?category=feature-request
about: Please use our GitHub Discussion for making feature requests.
- name: I'm unsure where to go
url: https://discord.immich.app
url: https://discord.gg/D8JsnBEuKb
about: If you are unsure where to go, then joining our Discord is recommended; Just ask!
-23
View File
@@ -1,23 +0,0 @@
cli:
- changed-files:
- any-glob-to-any-file: cli/**
documentation:
- changed-files:
- any-glob-to-any-file: docs/**
🖥️web:
- changed-files:
- any-glob-to-any-file: web/**
📱mobile:
- changed-files:
- any-glob-to-any-file: mobile/**
🗄️server:
- changed-files:
- any-glob-to-any-file: server/**
🧠machine-learning:
- changed-files:
- any-glob-to-any-file: machine-learning/**
+12 -13
View File
@@ -1,17 +1,16 @@
name: CLI Build
on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'cli/**'
- '.github/workflows/cli.yml'
- "cli/**"
- ".github/workflows/cli.yml"
pull_request:
branches: [main]
paths:
- 'cli/**'
- '.github/workflows/cli.yml'
release:
types: [published]
- "cli/**"
- ".github/workflows/cli.yml"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -33,8 +32,8 @@ jobs:
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@v4
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
node-version: "20.x"
registry-url: "https://registry.npmjs.org"
- name: Prepare SDK
run: npm ci --prefix ../open-api/typescript-sdk/
- name: Build SDK
@@ -42,7 +41,7 @@ jobs:
- run: npm ci
- run: npm run build
- run: npm publish
if: ${{ github.event_name == 'release' }}
if: ${{ github.event_name == 'workflow_dispatch' }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -84,15 +83,15 @@ jobs:
images: |
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=${{ steps.package-version.outputs.version }},enable=${{ github.event_name == 'workflow_dispatch' }}
type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' }}
- name: Build and push image
uses: docker/build-push-action@v6.0.0
uses: docker/build-push-action@v5.3.0
with:
file: cli/Dockerfile
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name == 'release' }}
push: ${{ github.event_name == 'workflow_dispatch' }}
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ${{ steps.metadata.outputs.tags }}
+1 -1
View File
@@ -115,7 +115,7 @@ jobs:
fi
- name: Build and push image
uses: docker/build-push-action@v6.0.0
uses: docker/build-push-action@v5.3.0
with:
context: ${{ matrix.context }}
file: ${{ matrix.file }}
-12
View File
@@ -1,12 +0,0 @@
name: "Pull Request Labeler"
on:
- pull_request_target
jobs:
labeler:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v5
+13
View File
@@ -0,0 +1,13 @@
name: Enforce PR labels
on:
pull_request:
types: [labeled, unlabeled, opened, edited, synchronize]
jobs:
enforce-label:
name: Enforce label
runs-on: ubuntu-latest
steps:
- if: toJson(github.event.pull_request.labels) == '[]'
run: exit 1
+6 -45
View File
@@ -10,6 +10,12 @@ dev-update:
dev-scale:
docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
stage:
docker compose -f ./docker/docker-compose.staging.yml up --build -V --remove-orphans
pull-stage:
docker compose -f ./docker/docker-compose.staging.yml pull
.PHONY: e2e
e2e:
docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
@@ -35,48 +41,3 @@ sql:
attach-server:
docker exec -it docker_immich-server_1 sh
MODULES = e2e server web cli sdk
audit-%:
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) audit fix
install-%:
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) i
build-cli: build-sdk
build-web: build-sdk
build-%: install-%
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) run | grep 'build' >/dev/null \
&& npm --prefix $(subst sdk,open-api/typescript-sdk,$*) run build || true
format-%:
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) run | grep 'format:fix' >/dev/null \
&& npm --prefix $(subst sdk,open-api/typescript-sdk,$*) run format:fix || true
lint-%:
npm --prefix $* run lint:fix
check-%:
npm --prefix $* run check
check-web:
npm --prefix web run check:typescript
npm --prefix web run check:svelte
test-%:
npm --prefix $* run test
test-e2e:
docker compose -f ./e2e/docker-compose.yml build
npm --prefix e2e run test
npm --prefix e2e run test:web
build-all: $(foreach M,$(MODULES),build-$M) ;
install-all: $(foreach M,$(MODULES),install-$M) ;
check-all: $(foreach M,$(MODULES),check-$M) ;
lint-all: $(foreach M,$(MODULES),lint-$M) ;
format-all: $(foreach M,$(MODULES),format-$M) ;
audit-all: $(foreach M,$(MODULES),audit-$M) ;
hygiene-all: lint-all format-all check-all sql audit-all;
test-all: $(foreach M,$(MODULES),test-$M) ;
clean:
find . -name "node_modules" -type d -prune -exec rm -rf '{}' +
find . -name "dist" -type d -prune -exec rm -rf '{}' +
find . -name "build" -type d -prune -exec rm -rf '{}' +
find . -name "svelte-kit" -type d -prune -exec rm -rf '{}' +
docker compose -f ./docker/docker-compose.dev.yml rm -v -f || true
docker compose -f ./e2e/docker-compose.yml rm -v -f || true
+1 -1
View File
@@ -1,7 +1,7 @@
<p align="center">
<br/>
<a href="https://opensource.org/license/agpl-v3"><img src="https://img.shields.io/badge/License-AGPL_v3-blue.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="License: AGPLv3"></a>
<a href="https://discord.immich.app">
<a href="https://discord.gg/D8JsnBEuKb">
<img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" alt="Discord"/>
</a>
<br/>
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:20-alpine3.19@sha256:eb17a0816c6475000def8bf0dd0a85bc59340235eb9fbb0aff158b4c9a3c7d6f as core
FROM node:20-alpine3.19@sha256:696ae41fb5880949a15ade7879a2deae93b3f0723f757bdb5b8a9e4a744ce27f as core
WORKDIR /usr/src/open-api/typescript-sdk
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
+58 -60
View File
@@ -1,12 +1,12 @@
{
"name": "@immich/cli",
"version": "2.2.4",
"version": "2.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/cli",
"version": "2.2.4",
"version": "2.2.0",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"fast-glob": "^3.3.2",
@@ -47,14 +47,14 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.106.4",
"version": "1.105.1",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^20.14.2",
"@types/node": "^20.12.13",
"typescript": "^5.3.3"
}
},
@@ -1138,9 +1138,9 @@
}
},
"node_modules/@types/node": {
"version": "20.14.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz",
"integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==",
"version": "20.12.13",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.13.tgz",
"integrity": "sha512-gBGeanV41c1L171rR7wjbMiEpEI/l5XFQdLLfhr/REwpgDy/4U8y89+i8kRiLzDyZdOkXh+cRaTetUnCYutoXA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1154,17 +1154,17 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.12.0.tgz",
"integrity": "sha512-7F91fcbuDf/d3S8o21+r3ZncGIke/+eWk0EpO21LXhDfLahriZF9CGj4fbAetEjlaBdjdSm9a6VeXbpbT6Z40Q==",
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.11.0.tgz",
"integrity": "sha512-P+qEahbgeHW4JQ/87FuItjBj8O3MYv5gELDzr8QaQ7fsll1gSMTYb6j87MYyxwf3DtD7uGFB9ShwgmCJB5KmaQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.12.0",
"@typescript-eslint/type-utils": "7.12.0",
"@typescript-eslint/utils": "7.12.0",
"@typescript-eslint/visitor-keys": "7.12.0",
"@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/type-utils": "7.11.0",
"@typescript-eslint/utils": "7.11.0",
"@typescript-eslint/visitor-keys": "7.11.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@@ -1188,16 +1188,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "7.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.12.0.tgz",
"integrity": "sha512-dm/J2UDY3oV3TKius2OUZIFHsomQmpHtsV0FTh1WO8EKgHLQ1QCADUqscPgTpU+ih1e21FQSRjXckHn3txn6kQ==",
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.11.0.tgz",
"integrity": "sha512-yimw99teuaXVWsBcPO1Ais02kwJ1jmNA1KxE7ng0aT7ndr1pT1wqj0OJnsYVGKKlc4QJai86l/025L6z8CljOg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/scope-manager": "7.12.0",
"@typescript-eslint/types": "7.12.0",
"@typescript-eslint/typescript-estree": "7.12.0",
"@typescript-eslint/visitor-keys": "7.12.0",
"@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/typescript-estree": "7.11.0",
"@typescript-eslint/visitor-keys": "7.11.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1217,14 +1217,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "7.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.12.0.tgz",
"integrity": "sha512-itF1pTnN6F3unPak+kutH9raIkL3lhH1YRPGgt7QQOh43DQKVJXmWkpb+vpc/TiDHs6RSd9CTbDsc/Y+Ygq7kg==",
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz",
"integrity": "sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.12.0",
"@typescript-eslint/visitor-keys": "7.12.0"
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/visitor-keys": "7.11.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1235,14 +1235,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "7.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.12.0.tgz",
"integrity": "sha512-lib96tyRtMhLxwauDWUp/uW3FMhLA6D0rJ8T7HmH7x23Gk1Gwwu8UZ94NMXBvOELn6flSPiBrCKlehkiXyaqwA==",
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.11.0.tgz",
"integrity": "sha512-WmppUEgYy+y1NTseNMJ6mCFxt03/7jTOy08bcg7bxJJdsM4nuhnchyBbE8vryveaJUf62noH7LodPSo5Z0WUCg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "7.12.0",
"@typescript-eslint/utils": "7.12.0",
"@typescript-eslint/typescript-estree": "7.11.0",
"@typescript-eslint/utils": "7.11.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@@ -1263,9 +1263,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "7.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.12.0.tgz",
"integrity": "sha512-o+0Te6eWp2ppKY3mLCU+YA9pVJxhUJE15FV7kxuD9jgwIAa+w/ycGJBMrYDTpVGUM/tgpa9SeMOugSabWFq7bg==",
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz",
"integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1277,14 +1277,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "7.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.12.0.tgz",
"integrity": "sha512-5bwqLsWBULv1h6pn7cMW5dXX/Y2amRqLaKqsASVwbBHMZSnHqE/HN4vT4fE0aFsiwxYvr98kqOWh1a8ZKXalCQ==",
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz",
"integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "7.12.0",
"@typescript-eslint/visitor-keys": "7.12.0",
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/visitor-keys": "7.11.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -1306,16 +1306,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "7.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.12.0.tgz",
"integrity": "sha512-Y6hhwxwDx41HNpjuYswYp6gDbkiZ8Hin9Bf5aJQn1bpTs3afYY4GX+MPYxma8jtoIV2GRwTM/UJm/2uGCVv+DQ==",
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.11.0.tgz",
"integrity": "sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "7.12.0",
"@typescript-eslint/types": "7.12.0",
"@typescript-eslint/typescript-estree": "7.12.0"
"@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/typescript-estree": "7.11.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1329,13 +1329,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "7.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.12.0.tgz",
"integrity": "sha512-uZk7DevrQLL3vSnfFl5bj4sL75qC9D6EdjemIdbtkuUmIheWpuiiylSY01JxJE7+zGrOWDZrp1WxOuDntvKrHQ==",
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz",
"integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.12.0",
"@typescript-eslint/types": "7.11.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@@ -3411,11 +3411,10 @@
}
},
"node_modules/prettier": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.1.tgz",
"integrity": "sha512-7CAwy5dRsxs8PHXT3twixW9/OEll8MLE0VRPCJyl7CkS6VHGPSlsVaWTiASPTyGyYRyApxlaWTzwUxVNrhcwDg==",
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -4282,9 +4281,9 @@
}
},
"node_modules/vite": {
"version": "5.2.13",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.13.tgz",
"integrity": "sha512-SSq1noJfY9pR3I1TUENL3rQYDQCFqgD+lM6fTRAM8Nv6Lsg5hDLaXkjETVeBt+7vZBCMoibD+6IWnT2mJ+Zb/A==",
"version": "5.2.12",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.12.tgz",
"integrity": "sha512-/gC8GxzxMK5ntBwb48pR32GGhENnjtY30G4A0jemunsBkiEZFw60s8InGpN8gkhHEkjnRK1aSAxeQgwvFhUHAA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4481,11 +4480,10 @@
"dev": true
},
"node_modules/yaml": {
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz",
"integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==",
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz",
"integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==",
"dev": true,
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@immich/cli",
"version": "2.2.4",
"version": "2.2.0",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",
+1 -1
View File
@@ -103,7 +103,7 @@ services:
ports:
- 5432:5432
healthcheck:
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT SUM(checksum_failures) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
interval: 5m
start_interval: 30s
start_period: 5m
+1 -1
View File
@@ -61,7 +61,7 @@ services:
ports:
- 5432:5432
healthcheck:
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT SUM(checksum_failures) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
interval: 5m
start_interval: 30s
start_period: 5m
+1 -1
View File
@@ -59,7 +59,7 @@ services:
volumes:
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
healthcheck:
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT SUM(checksum_failures) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
interval: 5m
start_interval: 30s
start_period: 5m
+3 -3
View File
@@ -3,10 +3,10 @@ global:
evaluation_interval: 15s
scrape_configs:
- job_name: immich_api
- job_name: immich_server
static_configs:
- targets: ['immich-server:8081']
- job_name: immich_microservices
static_configs:
- targets: ['immich-server:8082']
- targets: ['immich-microservices:8081']
+1 -1
View File
@@ -43,7 +43,7 @@ if [ -n "${quota:-}" ] && [ -n "${period:-}" ]; then
cpus=1
fi
else
cpus=$(grep -c ^processor /proc/cpuinfo)
cpus=$(grep -c processor /proc/cpuinfo)
fi
echo "$cpus"
+1 -1
View File
@@ -94,7 +94,7 @@ Thank you, and I am asking for your support for the project. I hope to be a full
- Bitcoin: 3QVAb9dCHutquVejeNXitPqZX26Yg5kxb7
- Give a project a star - the contributors love gazing at the stars and seeing their creations shining in the sky.
Join our friendly [Discord](https://discord.immich.app) to talk and discuss Immich, tech, or anything
Join our friendly [Discord](https://discord.gg/D8JsnBEuKb) to talk and discuss Immich, tech, or anything
Cheer!
+1 -1
View File
@@ -142,7 +142,7 @@ Thank you, and I am asking for your support for the project. I hope to be a full
- Bitcoin: 3QVAb9dCHutquVejeNXitPqZX26Yg5kxb7
- Give a project a star - the contributors love gazing at the stars and seeing their creations shining in the sky.
Join our friendly [Discord](https://discord.immich.app) to talk and discuss Immich, tech, or anything
Join our friendly [Discord](https://discord.gg/D8JsnBEuKb) to talk and discuss Immich, tech, or anything
Cheer!
+34 -7
View File
@@ -133,6 +133,40 @@ For example, say you have existing transcodes with the policy "Videos higher tha
No. Our design principle is that the original assets should always be untouched.
### How can I move all data (photos, persons, albums, libraries) from one user to another?
This is not officially supported but can be accomplished with some database updates. You can do this on the command line (in the PostgreSQL container using the `psql` command), or you can add, for example, an [Adminer](https://www.adminer.org/) container to the `docker-compose.yml` file so that you can use a web interface.
<details>
<summary>Steps</summary>
1. **MAKE A BACKUP** - See [backup and restore](/docs/administration/backup-and-restore.md).
2. Find the ID of both the 'source' and the 'destination' user (it's the id column in the `users` table)
3. Four tables need to be updated:
```sql
BEGIN;
-- reassign albums
UPDATE albums SET "ownerId" = '<destinationId>' WHERE "ownerId" = '<sourceId>';
-- reassign people
UPDATE person SET "ownerId" = '<destinationId>' WHERE "ownerId" = '<sourceId>';
-- reassign assets
UPDATE assets SET "ownerId" = '<destinationId>' WHERE "ownerId" = '<sourceId>'
AND CHECKSUM NOT IN (SELECT CHECKSUM FROM assets WHERE "ownerId" = '<destinationId>');
-- reassign external libraries
UPDATE libraries SET "ownerId" = '<destinationId>' WHERE "ownerId" = '<sourceId>';
COMMIT;
```
4. There might be left-over assets in the 'source' user's library if they are skipped by the last query because of duplicate checksums. These are probably duplicates anyway, and can probably be removed.
</details>
---
## Albums
@@ -408,11 +442,4 @@ docker exec -it immich_postgres psql --dbname=immich --username=<DB_USERNAME> --
</details>
If corruption is detected, you should immediately make a backup before performing any other work in the database.
To do so, you may need to set the `zero_damaged_pages=on` flag for the database server to allow `pg_dumpall` to succeed.
After taking a backup, the recommended next step is to restore the database from a healthy backup before corruption was detected.
The damaged database dump can be used to manually recover any changes made since the last backup, if needed.
The causes of possible corruption are many, but can include unexpected poweroffs or unmounts, use of a network share for Postgres data, or a poor storage medium such an SD card or failing HDD/SSD.
[huggingface]: https://huggingface.co/immich-app
@@ -1,23 +0,0 @@
# Email Notifications
Immich supports the option to send notifications via Email for the following events:
- Creating a new user
- Notifying a user when they get added to a shared album
- Informing other users about the addition of new assets to a shared album
## SMTP settings
You can access the settings panel from the web at `Administration -> Settings -> Notification settings`
Under Email, enter the following details to connect with SMTP servers.
You can use the following [guide](/docs/guides/smtp-gmail) to use Gmail's SMTP server.
<img src={require('./img/email-settings.png').default} width="80%" title="SMTP settings" />
## User's notifications settings
Users can manage their email notification settings from their account settings page on the web. They can choose to turn email notifications on or off for the following events:
<img src={require('./img/user-notifications-settings.png').default} width="80%" title="User notification settings" />
Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

+1 -1
View File
@@ -27,7 +27,7 @@ Copy the entire `immich-server` block as a new service and make the following ch
+ container_name: immich_microservices
```
Once you have two copies of the immich-server service, make the following changes to each one. This will allow one container to only serve the web UI and API, and the other one to handle all other tasks.
Once you have two copies of the immich-server service, make the following chnages to each one. This will allow one container to only serve the web UI and API, and the other one to handle all other tasks.
```diff
services:
+31 -55
View File
@@ -10,59 +10,6 @@ Viewing and modifying the system settings is restricted to the Administrator.
You can always return to the default settings by clicking the `Reset to default` button.
:::
## Authentication Settings
Manage password, OAuth, and other authentication settings
### OAuth Authentication
Immich supports OAuth Authentication. Read more about this feature and its configuration [here](/docs/administration/oauth).
### Password Authentication
The administrator can choose to disable login with username and password for the entire instance. This means that **no one**, including the system administrator, will be able to log using this method. If [OAuth Authentication](/docs/administration/oauth) is also disabled, no users will be able to login using **any** method. Changing this setting does not affect existing sessions, just new login attempts.
:::tip
You can always use the [Server CLI](/docs/administration/server-commands) to re-enable password login.
:::
## Image Settings (thumbnails and previews)
- Thumbnails - Used in the main timeline.
- Previews - Used in the asset viewer.
By default Immich creates 3 thumbnails for each asset,
Blurred (thumbhash) , Small - thumbnails (webp) , and Large - previews (jpeg/webp), using these settings you can change the quality for the thumbnails and previews files that are created.
**Thumbnail format**
Allows you to choose the type of format you want for the Thumbnail images, Webp produces smaller files than jpeg, but is slower to encode.
:::tip
You can read in detail about the advantages and disadvantages of using webp over jpeg on [Adobe's website](https://www.adobe.com/creativecloud/file-types/image/raster/webp-file.html)
:::
**Thumbnail resolution**
Used when viewing groups of photos (main timeline, album view, etc.). Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness.
**Preview format**
Allows you to choose the type of format you want for the Preview images, Webp produces smaller files than jpeg, but is slower to encode.
**Preview resolution**
Used when viewing a single photo and for machine learning. Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness.
**Quality**
Image quality from 1-100. Higher is better for quality but produces larger files, this option affects the Preview and Thumbnail images.
**Prefer wide gamut**
Use Display P3 for thumbnails. This better preserves the vibrance of images with wide colorspaces, but images may appear differently on old devices with an old browser version. sRGB images are kept as sRGB to avoid color shifts.
**Prefer embedded preview**
Use embedded previews in RAW photos as the input to image processing when available. This can produce more accurate colors for some images, but the quality of the preview is camera-dependent and the image may have more compression artifacts.
:::tip
The default resolution for Large thumbnails can be lowered from 1440p (default) to 1080p or 720p to save storage space.
:::
## Job Settings
Using these settings, you can determine the amount of work that will run concurrently for each task in microservices. Some tasks can be set to higher values on computers with powerful hardware and storage with good I/O capabilities.
@@ -145,9 +92,17 @@ The map can be adjusted via [OpenMapTiles](https://openmaptiles.org/styles/) for
Immich supports [Reverse Geocoding](/docs/features/reverse-geocoding) using data from the [GeoNames](https://www.geonames.org/) geographical database.
## Notification Settings
## OAuth Authentication
SMTP server setup, for user creation notifications, new albums, etc. More information can be found [here](/docs/administration/email-notification)
Immich supports OAuth Authentication. Read more about this feature and its configuration [here](/docs/administration/oauth).
## Password Authentication
The administrator can choose to disable login with username and password for the entire instance. This means that **no one**, including the system administrator, will be able to log using this method. If [OAuth Authentication](/docs/administration/oauth) is also disabled, no users will be able to login using **any** method. Changing this setting does not affect existing sessions, just new login attempts.
:::tip
You can always use the [Server CLI](/docs/administration/server-commands) to re-enable password login.
:::
## Server Settings
@@ -175,6 +130,27 @@ p {
}
```
## Thumbnail Settings
By default Immich creates 3 thumbnails for each asset,
Blurred (thumbhash) , Small (webp) , and Large (jpeg), using these settings you can change the quality for the thumbnail files that are created.
**Small thumbnail resolution**
Used when viewing groups of photos (main timeline, album view, etc.). Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness.
**Large thumbnail resolution**
Used when viewing a single photo and for machine learning. Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness.
**Quality**
Thumbnail quality from 1-100. Higher is better for quality but produces larger files.
**Prefer wide gamut**
Use display p3 for thumbnails. This better preserves the vibrance of images with wide color spaces, but images may appear differently on old devices with an old browser version. Srgb images are kept as srgb to avoid color shifts.
:::tip
The default resolution for Large thumbnails can be lowered from 1440p (default) to 1080p or 720p to save storage space.
:::
## Trash Settings
In the system administrator's option to set a trash for deleted files, these files will remain in the trash until the deletion date 30 days (default) or as defined by the system administrator.
@@ -13,20 +13,6 @@ Immich supports multiple users, each with their own library.
<UserCreate />
## Send new user email notification
:::note
This option is only available if an SMTP server has been configured in the administrator settings.
:::
Admin can send a welcome email if the Email option is set, you can learn here how to set up the SMTP server in Immich.
<img
src={require('./img/send-user-email-notification.webp').default}
width="40%"
title="Send user email notification"
/>
## Set Storage Quota For User
Admin can specify the storage quota for the user as the instance's admin; once the limit is reached, the user won't be able to upload to the instance anymore.
+1 -1
View File
@@ -1,7 +1,7 @@
# Troubleshooting
:::tip
A great option to get assistance with troubleshooting is to join our [Discord](https://discord.immich.app) server, where we have a dedicated channel for `#contributing`.
A great option to get assistance with troubleshooting is to join our [Discord](https://discord.gg/D8JsnBEuKb) server, where we have a dedicated channel for `#contributing`.
:::
## Known Issues
+7 -8
View File
@@ -60,17 +60,17 @@ For RKMPP to work:
#### Basic Setup
1. If you do not already have it, download the latest [`hwaccel.transcoding.yml`][hw-file] file and ensure it's in the same folder as the `docker-compose.yml`.
2. In the `docker-compose.yml` under `immich-server`, uncomment the `extends` section and change `cpu` to the appropriate backend.
2. In the `docker-compose.yml` under `immich-microservices`, uncomment the `extends` section and change `cpu` to the appropriate backend.
- For VAAPI on WSL2, be sure to use `vaapi-wsl` rather than `vaapi`
3. Redeploy the `immich-server` container with these updated settings.
3. Redeploy the `immich-microservices` container with these updated settings.
4. In the Admin page under `Video transcoding settings`, change the hardware acceleration setting to the appropriate option and save.
5. (Optional) If using a compatible backend, you may enable hardware decoding for optimal performance.
#### Single Compose File
Some platforms, including Unraid and Portainer, do not support multiple Compose files as of writing. As an alternative, you can "inline" the relevant contents of the [`hwaccel.transcoding.yml`][hw-file] file into the `immich-server` service directly.
Some platforms, including Unraid and Portainer, do not support multiple Compose files as of writing. As an alternative, you can "inline" the relevant contents of the [`hwaccel.transcoding.yml`][hw-file] file into the `immich-microservices` service directly.
For example, the `qsv` section in this file is:
@@ -79,22 +79,21 @@ devices:
- /dev/dri:/dev/dri
```
You can add this to the `immich-server` service instead of extending from `hwaccel.transcoding.yml`:
You can add this to the `immich-microservices` service instead of extending from `hwaccel.transcoding.yml`:
```yaml
immich-server:
container_name: immich_server
immich-microservices:
container_name: immich_microservices
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
# Note the lack of an `extends` section
devices:
- /dev/dri:/dev/dri
command: ['start.sh', 'microservices']
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
ports:
- 2283:3001
depends_on:
- redis
- database
Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

-20
View File
@@ -1,20 +0,0 @@
# SMTP settings using Gmail
This guide walks you through how to get the information you need to set up your Immich instance to send emails using Gmail's SMTP server.
## Create an app password
From your Google account settings
- Add [2-Step Verification](https://support.google.com/accounts/answer/185839) to your Google account (Required)
- [Create an app password](https://myaccount.google.com/apppasswords).
At the end of creating your app passwords, a password will be displayed; save it, it will be used for the password field when setting up the SMTP server in Immich.
<img src={require('./img/google-app-password.webp').default} title="Authorised redirect URIs" />
## Entering the SMTP credential in Immich
Entering your credential in Immich's email notification settings at `Administration -> Settings -> Notification Settings`
<img src={require('./img/email-settings.png').default} width="80%" title="SMTP settings" />
+11 -13
View File
@@ -38,19 +38,17 @@ Regardless of filesystem, it is not recommended to use a network share for your
## General
| Variable | Description | Default | Containers | Workers |
| :---------------------------------- | :---------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
| `TZ` | Timezone | | server | microservices |
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
| `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload`<sup>\*1</sup> | server | api, microservices |
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
| `IMMICH_WEB_ROOT` | Path of root index.html | `/usr/src/app/www` | server | api |
| `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | server | microservices |
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
| `CPU_CORES` | Amount of cores available to the immich server | auto-detected cpu core count | server | |
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
| Variable | Description | Default | Containers | Workers |
| :------------------------------ | :---------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
| `TZ` | Timezone | | server | microservices |
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
| `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload`<sup>\*1</sup> | server | api, microservices |
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
| `IMMICH_WEB_ROOT` | Path of root index.html | `/usr/src/app/www` | server | api |
| `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | server | microservices |
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
| `CPU_CORES` | Amount of cores available to the immich server | auto-detected cpu core count | server | |
\*1: With the default `WORKDIR` of `/usr/src/app`, this path will resolve to `/usr/src/app/upload`.
It only need to be set if the Immich deployment method is changing.
+2 -4
View File
@@ -27,7 +27,7 @@ For more information about setting up the community image see [here](https://git
:::info
- Guide was written using Unraid v6.12.10.
- Guide was written using Unraid v6.12.10
- Requires you to have installed the plugin: [Docker Compose Manager](https://forums.unraid.net/topic/114415-plugin-docker-compose-manager/)
- An Unraid share created for your images
- There has been a [report](https://forums.unraid.net/topic/130006-errortraps-traps-node27707-trap-invalid-opcode-ip14fcfc8d03c0-sp7fff32889dd8-more/#comment-1189395) of this not working if your Unraid server doesn't support AVX _(e.g. using a T610)_
@@ -46,8 +46,7 @@ alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich"
/>
3. Select the cog ⚙️ next to Immich then click "**Edit Stack**"
4. Click "**Compose File**" and then paste the entire contents of the [Immich Docker Compose](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml) file into the Unraid editor. Remove any text that may be in the text area by default. Note that Unraid v6.12.10 uses version 24.0.9 of the Docker Engine, which does not support healthcheck `start_interval` as defined in the `database` service of the Docker compose file (version 25 or higher is needed). This parameter defines an initial waiting period before starting health checks, to give the container time to start up. Commenting out the `start_interval` and `start_period` parameters will allow the containers to start up normally. The only downside to this is that the database container will not receive an initial health check until `interval` time has passed.
4. Click "**Compose File**" and then paste the entire contents of the [Immich Docker Compose](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml) file into the Unraid editor. Remove any text that may be in the text area by default.
<details >
<summary>Using an existing Postgres container? Click me! Otherwise proceed to step 5.</summary>
<ul>
@@ -71,7 +70,6 @@ alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich"
/>
</ul>
</details>
5. Click "**Save Changes**", you will be promoted to edit stack UI labels, just leave this blank and click "**Ok**"
6. Select the cog ⚙️ next to Immich, click "**Edit Stack**", then click "**Env File**"
7. Paste the entire contents of the [Immich example.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file into the Unraid editor, then **before saving** edit the following:
+1 -1
View File
@@ -13,4 +13,4 @@ Running into an issue or have a question? Try the following:
[github-issues]: https://github.com/immich-app/immich/issues
[github-releases]: https://github.com/immich-app/immich/releases
[discord-link]: https://discord.immich.app
[discord-link]: https://discord.com/invite/D8JsnBEuKb
+8 -8
View File
@@ -5,21 +5,21 @@ sidebar_position: 3
# Quick Start
Here is a quick, no-choices path to install Immich and take it for a test drive.
Once you've tried it, you might use one of the many other ways
Once you've tried it, perhaps you'll use one of the many other ways
to install and use it.
## Requirements
Check the [requirements page](/docs/install/requirements) to get started.
## Install and Launch via Docker Compose
## Install and launch via Docker Compose
Follow the [Docker Compose (Recommended)](/docs/install/docker-compose) instructions
to install the server.
- Where random passwords are required, `pwgen` is a handy utility.
- `UPLOAD_LOCATION` should be set to some new directory on the server
with enough free space.
with free space.
- You may ignore "Step 4 - Upgrading".
## Try the Web UI
@@ -48,26 +48,26 @@ import MobileAppLogin from '/docs/partials/_mobile-app-login.md';
In the mobile app, you should see the photo you uploaded from the web UI.
### Transfer Photos from Your Mobile Device
### Transfer Photos from your Mobile Device
import MobileAppBackup from '/docs/partials/_mobile-app-backup.md';
<MobileAppBackup />
The backup time differs depending on how many photos are on your mobile device. Large uploads may
Depending on how many photos are on your mobile device, this backup may
take quite a while.
You can select the **Jobs** tab to see Immich processing your photos.
You can select the Jobs tab to see Immich processing your photos.
<img src={require('/docs/guides/img/jobs-tab.png').default} title="Jobs tab" />
## Set up Your Backups
## Set up your backups
You may want to back up the content of your Immich instance
along with other parts of your server; be sure to read about
[database backup](/docs/administration/backup-and-restore).
## Where to Go From Here
## Where to go from here?
You may decide you'd like to install the server a different way;
the Install category on the left menu provides many options.
+2 -6
View File
@@ -94,10 +94,6 @@ const config = {
srcDark: 'img/immich-logo-inline-dark.png',
},
items: [
{
type: 'custom-versionSwitcher',
position: 'right',
},
{
to: '/docs/overview/introduction',
position: 'right',
@@ -124,7 +120,7 @@ const config = {
position: 'right',
},
{
href: 'https://discord.immich.app',
href: 'https://discord.gg/D8JsnBEuKb',
label: 'Discord',
position: 'right',
},
@@ -151,7 +147,7 @@ const config = {
items: [
{
label: 'Discord',
href: 'https://discord.immich.app',
href: 'https://discord.com/invite/D8JsnBEuKb',
},
{
label: 'Reddit',
+164 -192
View File
@@ -2155,10 +2155,9 @@
}
},
"node_modules/@docusaurus/core": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.4.0.tgz",
"integrity": "sha512-g+0wwmN2UJsBqy2fQRQ6fhXruoEa62JDeEa5d8IdTJlMoaDaEDfHh7WjwGRn4opuTQWpjAwP/fbcgyHKlE+64w==",
"license": "MIT",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.3.2.tgz",
"integrity": "sha512-PzKMydKI3IU1LmeZQDi+ut5RSuilbXnA8QdowGeJEgU8EJjmx3rBHNT1LxQxOVqNEwpWi/csLwd9bn7rUjggPA==",
"dependencies": {
"@babel/core": "^7.23.3",
"@babel/generator": "^7.23.3",
@@ -2170,12 +2169,12 @@
"@babel/runtime": "^7.22.6",
"@babel/runtime-corejs3": "^7.22.6",
"@babel/traverse": "^7.22.8",
"@docusaurus/cssnano-preset": "3.4.0",
"@docusaurus/logger": "3.4.0",
"@docusaurus/mdx-loader": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"@docusaurus/cssnano-preset": "3.3.2",
"@docusaurus/logger": "3.3.2",
"@docusaurus/mdx-loader": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/utils-common": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"autoprefixer": "^10.4.14",
"babel-loader": "^9.1.3",
"babel-plugin-dynamic-import-node": "^2.3.3",
@@ -2241,10 +2240,9 @@
}
},
"node_modules/@docusaurus/cssnano-preset": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.4.0.tgz",
"integrity": "sha512-qwLFSz6v/pZHy/UP32IrprmH5ORce86BGtN0eBtG75PpzQJAzp9gefspox+s8IEOr0oZKuQ/nhzZ3xwyc3jYJQ==",
"license": "MIT",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.3.2.tgz",
"integrity": "sha512-+5+epLk/Rp4vFML4zmyTATNc3Is+buMAL6dNjrMWahdJCJlMWMPd/8YfU+2PA57t8mlSbhLJ7vAZVy54cd1vRQ==",
"dependencies": {
"cssnano-preset-advanced": "^6.1.2",
"postcss": "^8.4.38",
@@ -2256,10 +2254,9 @@
}
},
"node_modules/@docusaurus/logger": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.4.0.tgz",
"integrity": "sha512-bZwkX+9SJ8lB9kVRkXw+xvHYSMGG4bpYHKGXeXFvyVc79NMeeBSGgzd4TQLHH+DYeOJoCdl8flrFJVxlZ0wo/Q==",
"license": "MIT",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.3.2.tgz",
"integrity": "sha512-Ldu38GJ4P8g4guN7d7pyCOJ7qQugG7RVyaxrK8OnxuTlaImvQw33aDRwaX2eNmX8YK6v+//Z502F4sOZbHHCHQ==",
"dependencies": {
"chalk": "^4.1.2",
"tslib": "^2.6.0"
@@ -2269,14 +2266,13 @@
}
},
"node_modules/@docusaurus/mdx-loader": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.4.0.tgz",
"integrity": "sha512-kSSbrrk4nTjf4d+wtBA9H+FGauf2gCax89kV8SUSJu3qaTdSIKdWERlngsiHaCFgZ7laTJ8a67UFf+xlFPtuTw==",
"license": "MIT",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.3.2.tgz",
"integrity": "sha512-AFRxj/aOk3/mfYDPxE3wTbrjeayVRvNSZP7mgMuUlrb2UlPRbSVAFX1k2RbgAJrnTSwMgb92m2BhJgYRfptN3g==",
"dependencies": {
"@docusaurus/logger": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"@docusaurus/logger": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"@mdx-js/mdx": "^3.0.0",
"@slorber/remark-comment": "^1.0.0",
"escape-html": "^1.0.3",
@@ -2308,12 +2304,11 @@
}
},
"node_modules/@docusaurus/module-type-aliases": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.4.0.tgz",
"integrity": "sha512-A1AyS8WF5Bkjnb8s+guTDuYmUiwJzNrtchebBHpc0gz0PyHJNMaybUlSrmJjHVcGrya0LKI4YcR3lBDQfXRYLw==",
"license": "MIT",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.3.2.tgz",
"integrity": "sha512-b/XB0TBJah5yKb4LYuJT4buFvL0MGAb0+vJDrJtlYMguRtsEBkf2nWl5xP7h4Dlw6ol0hsHrCYzJ50kNIOEclw==",
"dependencies": {
"@docusaurus/types": "3.4.0",
"@docusaurus/types": "3.3.2",
"@types/history": "^4.7.11",
"@types/react": "*",
"@types/react-router-config": "*",
@@ -2327,18 +2322,17 @@
}
},
"node_modules/@docusaurus/plugin-content-blog": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.4.0.tgz",
"integrity": "sha512-vv6ZAj78ibR5Jh7XBUT4ndIjmlAxkijM3Sx5MAAzC1gyv0vupDQNhzuFg1USQmQVj3P5I6bquk12etPV3LJ+Xw==",
"license": "MIT",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.3.2.tgz",
"integrity": "sha512-fJU+dmqp231LnwDJv+BHVWft8pcUS2xVPZdeYH6/ibH1s2wQ/sLcmUrGWyIv/Gq9Ptj8XWjRPMghlxghuPPoxg==",
"dependencies": {
"@docusaurus/core": "3.4.0",
"@docusaurus/logger": "3.4.0",
"@docusaurus/mdx-loader": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"@docusaurus/core": "3.3.2",
"@docusaurus/logger": "3.3.2",
"@docusaurus/mdx-loader": "3.3.2",
"@docusaurus/types": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/utils-common": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"cheerio": "^1.0.0-rc.12",
"feed": "^4.2.2",
"fs-extra": "^11.1.1",
@@ -2359,19 +2353,18 @@
}
},
"node_modules/@docusaurus/plugin-content-docs": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.4.0.tgz",
"integrity": "sha512-HkUCZffhBo7ocYheD9oZvMcDloRnGhBMOZRyVcAQRFmZPmNqSyISlXA1tQCIxW+r478fty97XXAGjNYzBjpCsg==",
"license": "MIT",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.3.2.tgz",
"integrity": "sha512-Dm1ri2VlGATTN3VGk1ZRqdRXWa1UlFubjaEL6JaxaK7IIFqN/Esjpl+Xw10R33loHcRww/H76VdEeYayaL76eg==",
"dependencies": {
"@docusaurus/core": "3.4.0",
"@docusaurus/logger": "3.4.0",
"@docusaurus/mdx-loader": "3.4.0",
"@docusaurus/module-type-aliases": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"@docusaurus/core": "3.3.2",
"@docusaurus/logger": "3.3.2",
"@docusaurus/mdx-loader": "3.3.2",
"@docusaurus/module-type-aliases": "3.3.2",
"@docusaurus/types": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/utils-common": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"@types/react-router-config": "^5.0.7",
"combine-promises": "^1.1.0",
"fs-extra": "^11.1.1",
@@ -2390,16 +2383,15 @@
}
},
"node_modules/@docusaurus/plugin-content-pages": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.4.0.tgz",
"integrity": "sha512-h2+VN/0JjpR8fIkDEAoadNjfR3oLzB+v1qSXbIAKjQ46JAHx3X22n9nqS+BWSQnTnp1AjkjSvZyJMekmcwxzxg==",
"license": "MIT",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.3.2.tgz",
"integrity": "sha512-EKc9fQn5H2+OcGER8x1aR+7URtAGWySUgULfqE/M14+rIisdrBstuEZ4lUPDRrSIexOVClML82h2fDS+GSb8Ew==",
"dependencies": {
"@docusaurus/core": "3.4.0",
"@docusaurus/mdx-loader": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"@docusaurus/core": "3.3.2",
"@docusaurus/mdx-loader": "3.3.2",
"@docusaurus/types": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"fs-extra": "^11.1.1",
"tslib": "^2.6.0",
"webpack": "^5.88.1"
@@ -2413,14 +2405,13 @@
}
},
"node_modules/@docusaurus/plugin-debug": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.4.0.tgz",
"integrity": "sha512-uV7FDUNXGyDSD3PwUaf5YijX91T5/H9SX4ErEcshzwgzWwBtK37nUWPU3ZLJfeTavX3fycTOqk9TglpOLaWkCg==",
"license": "MIT",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.3.2.tgz",
"integrity": "sha512-oBIBmwtaB+YS0XlmZ3gCO+cMbsGvIYuAKkAopoCh0arVjtlyPbejzPrHuCoRHB9G7abjNZw7zoONOR8+8LM5+Q==",
"dependencies": {
"@docusaurus/core": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/core": "3.3.2",
"@docusaurus/types": "3.3.2",
"@docusaurus/utils": "3.3.2",
"fs-extra": "^11.1.1",
"react-json-view-lite": "^1.2.0",
"tslib": "^2.6.0"
@@ -2434,14 +2425,13 @@
}
},
"node_modules/@docusaurus/plugin-google-analytics": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.4.0.tgz",
"integrity": "sha512-mCArluxEGi3cmYHqsgpGGt3IyLCrFBxPsxNZ56Mpur0xSlInnIHoeLDH7FvVVcPJRPSQ9/MfRqLsainRw+BojA==",
"license": "MIT",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.3.2.tgz",
"integrity": "sha512-jXhrEIhYPSClMBK6/IA8qf1/FBoxqGXZvg7EuBax9HaK9+kL3L0TJIlatd8jQJOMtds8mKw806TOCc3rtEad1A==",
"dependencies": {
"@docusaurus/core": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"@docusaurus/core": "3.3.2",
"@docusaurus/types": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"tslib": "^2.6.0"
},
"engines": {
@@ -2453,14 +2443,13 @@
}
},
"node_modules/@docusaurus/plugin-google-gtag": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.4.0.tgz",
"integrity": "sha512-Dsgg6PLAqzZw5wZ4QjUYc8Z2KqJqXxHxq3vIoyoBWiLEEfigIs7wHR+oiWUQy3Zk9MIk6JTYj7tMoQU0Jm3nqA==",
"license": "MIT",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.3.2.tgz",
"integrity": "sha512-vcrKOHGbIDjVnNMrfbNpRQR1x6Jvcrb48kVzpBAOsKbj9rXZm/idjVAXRaewwobHdOrJkfWS/UJoxzK8wyLRBQ==",
"dependencies": {
"@docusaurus/core": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"@docusaurus/core": "3.3.2",
"@docusaurus/types": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"@types/gtag.js": "^0.0.12",
"tslib": "^2.6.0"
},
@@ -2473,14 +2462,13 @@
}
},
"node_modules/@docusaurus/plugin-google-tag-manager": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.4.0.tgz",
"integrity": "sha512-O9tX1BTwxIhgXpOLpFDueYA9DWk69WCbDRrjYoMQtFHSkTyE7RhNgyjSPREUWJb9i+YUg3OrsvrBYRl64FCPCQ==",
"license": "MIT",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.3.2.tgz",
"integrity": "sha512-ldkR58Fdeks0vC+HQ+L+bGFSJsotQsipXD+iKXQFvkOfmPIV6QbHRd7IIcm5b6UtwOiK33PylNS++gjyLUmaGw==",
"dependencies": {
"@docusaurus/core": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"@docusaurus/core": "3.3.2",
"@docusaurus/types": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"tslib": "^2.6.0"
},
"engines": {
@@ -2492,17 +2480,16 @@
}
},
"node_modules/@docusaurus/plugin-sitemap": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.4.0.tgz",
"integrity": "sha512-+0VDvx9SmNrFNgwPoeoCha+tRoAjopwT0+pYO1xAbyLcewXSemq+eLxEa46Q1/aoOaJQ0qqHELuQM7iS2gp33Q==",
"license": "MIT",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.3.2.tgz",
"integrity": "sha512-/ZI1+bwZBhAgC30inBsHe3qY9LOZS+79fRGkNdTcGHRMcdAp6Vw2pCd1gzlxd/xU+HXsNP6cLmTOrggmRp3Ujg==",
"dependencies": {
"@docusaurus/core": "3.4.0",
"@docusaurus/logger": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"@docusaurus/core": "3.3.2",
"@docusaurus/logger": "3.3.2",
"@docusaurus/types": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/utils-common": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"fs-extra": "^11.1.1",
"sitemap": "^7.1.1",
"tslib": "^2.6.0"
@@ -2516,24 +2503,23 @@
}
},
"node_modules/@docusaurus/preset-classic": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.4.0.tgz",
"integrity": "sha512-Ohj6KB7siKqZaQhNJVMBBUzT3Nnp6eTKqO+FXO3qu/n1hJl3YLwVKTWBg28LF7MWrKu46UuYavwMRxud0VyqHg==",
"license": "MIT",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.3.2.tgz",
"integrity": "sha512-1SDS7YIUN1Pg3BmD6TOTjhB7RSBHJRpgIRKx9TpxqyDrJ92sqtZhomDc6UYoMMLQNF2wHFZZVGFjxJhw2VpL+Q==",
"dependencies": {
"@docusaurus/core": "3.4.0",
"@docusaurus/plugin-content-blog": "3.4.0",
"@docusaurus/plugin-content-docs": "3.4.0",
"@docusaurus/plugin-content-pages": "3.4.0",
"@docusaurus/plugin-debug": "3.4.0",
"@docusaurus/plugin-google-analytics": "3.4.0",
"@docusaurus/plugin-google-gtag": "3.4.0",
"@docusaurus/plugin-google-tag-manager": "3.4.0",
"@docusaurus/plugin-sitemap": "3.4.0",
"@docusaurus/theme-classic": "3.4.0",
"@docusaurus/theme-common": "3.4.0",
"@docusaurus/theme-search-algolia": "3.4.0",
"@docusaurus/types": "3.4.0"
"@docusaurus/core": "3.3.2",
"@docusaurus/plugin-content-blog": "3.3.2",
"@docusaurus/plugin-content-docs": "3.3.2",
"@docusaurus/plugin-content-pages": "3.3.2",
"@docusaurus/plugin-debug": "3.3.2",
"@docusaurus/plugin-google-analytics": "3.3.2",
"@docusaurus/plugin-google-gtag": "3.3.2",
"@docusaurus/plugin-google-tag-manager": "3.3.2",
"@docusaurus/plugin-sitemap": "3.3.2",
"@docusaurus/theme-classic": "3.3.2",
"@docusaurus/theme-common": "3.3.2",
"@docusaurus/theme-search-algolia": "3.3.2",
"@docusaurus/types": "3.3.2"
},
"engines": {
"node": ">=18.0"
@@ -2544,23 +2530,22 @@
}
},
"node_modules/@docusaurus/theme-classic": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.4.0.tgz",
"integrity": "sha512-0IPtmxsBYv2adr1GnZRdMkEQt1YW6tpzrUPj02YxNpvJ5+ju4E13J5tB4nfdaen/tfR1hmpSPlTFPvTf4kwy8Q==",
"license": "MIT",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.3.2.tgz",
"integrity": "sha512-gepHFcsluIkPb4Im9ukkiO4lXrai671wzS3cKQkY9BXQgdVwsdPf/KS0Vs4Xlb0F10fTz+T3gNjkxNEgSN9M0A==",
"dependencies": {
"@docusaurus/core": "3.4.0",
"@docusaurus/mdx-loader": "3.4.0",
"@docusaurus/module-type-aliases": "3.4.0",
"@docusaurus/plugin-content-blog": "3.4.0",
"@docusaurus/plugin-content-docs": "3.4.0",
"@docusaurus/plugin-content-pages": "3.4.0",
"@docusaurus/theme-common": "3.4.0",
"@docusaurus/theme-translations": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"@docusaurus/core": "3.3.2",
"@docusaurus/mdx-loader": "3.3.2",
"@docusaurus/module-type-aliases": "3.3.2",
"@docusaurus/plugin-content-blog": "3.3.2",
"@docusaurus/plugin-content-docs": "3.3.2",
"@docusaurus/plugin-content-pages": "3.3.2",
"@docusaurus/theme-common": "3.3.2",
"@docusaurus/theme-translations": "3.3.2",
"@docusaurus/types": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/utils-common": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"copy-text-to-clipboard": "^3.2.0",
@@ -2584,18 +2569,17 @@
}
},
"node_modules/@docusaurus/theme-common": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.4.0.tgz",
"integrity": "sha512-0A27alXuv7ZdCg28oPE8nH/Iz73/IUejVaCazqu9elS4ypjiLhK3KfzdSQBnL/g7YfHSlymZKdiOHEo8fJ0qMA==",
"license": "MIT",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.3.2.tgz",
"integrity": "sha512-kXqSaL/sQqo4uAMQ4fHnvRZrH45Xz2OdJ3ABXDS7YVGPSDTBC8cLebFrRR4YF9EowUHto1UC/EIklJZQMG/usA==",
"dependencies": {
"@docusaurus/mdx-loader": "3.4.0",
"@docusaurus/module-type-aliases": "3.4.0",
"@docusaurus/plugin-content-blog": "3.4.0",
"@docusaurus/plugin-content-docs": "3.4.0",
"@docusaurus/plugin-content-pages": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"@docusaurus/mdx-loader": "3.3.2",
"@docusaurus/module-type-aliases": "3.3.2",
"@docusaurus/plugin-content-blog": "3.3.2",
"@docusaurus/plugin-content-docs": "3.3.2",
"@docusaurus/plugin-content-pages": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/utils-common": "3.3.2",
"@types/history": "^4.7.11",
"@types/react": "*",
"@types/react-router-config": "*",
@@ -2614,19 +2598,18 @@
}
},
"node_modules/@docusaurus/theme-search-algolia": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.4.0.tgz",
"integrity": "sha512-aiHFx7OCw4Wck1z6IoShVdUWIjntC8FHCw9c5dR8r3q4Ynh+zkS8y2eFFunN/DL6RXPzpnvKCg3vhLQYJDmT9Q==",
"license": "MIT",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.3.2.tgz",
"integrity": "sha512-qLkfCl29VNBnF1MWiL9IyOQaHxUvicZp69hISyq/xMsNvFKHFOaOfk9xezYod2Q9xx3xxUh9t/QPigIei2tX4w==",
"dependencies": {
"@docsearch/react": "^3.5.2",
"@docusaurus/core": "3.4.0",
"@docusaurus/logger": "3.4.0",
"@docusaurus/plugin-content-docs": "3.4.0",
"@docusaurus/theme-common": "3.4.0",
"@docusaurus/theme-translations": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"@docusaurus/core": "3.3.2",
"@docusaurus/logger": "3.3.2",
"@docusaurus/plugin-content-docs": "3.3.2",
"@docusaurus/theme-common": "3.3.2",
"@docusaurus/theme-translations": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"algoliasearch": "^4.18.0",
"algoliasearch-helper": "^3.13.3",
"clsx": "^2.0.0",
@@ -2645,10 +2628,9 @@
}
},
"node_modules/@docusaurus/theme-translations": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.4.0.tgz",
"integrity": "sha512-zSxCSpmQCCdQU5Q4CnX/ID8CSUUI3fvmq4hU/GNP/XoAWtXo9SAVnM3TzpU8Gb//H3WCsT8mJcTfyOk3d9ftNg==",
"license": "MIT",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.3.2.tgz",
"integrity": "sha512-bPuiUG7Z8sNpGuTdGnmKl/oIPeTwKr0AXLGu9KaP6+UFfRZiyWbWE87ti97RrevB2ffojEdvchNujparR3jEZQ==",
"dependencies": {
"fs-extra": "^11.1.1",
"tslib": "^2.6.0"
@@ -2658,10 +2640,9 @@
}
},
"node_modules/@docusaurus/types": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.4.0.tgz",
"integrity": "sha512-4jcDO8kXi5Cf9TcyikB/yKmz14f2RZ2qTRerbHAsS+5InE9ZgSLBNLsewtFTcTOXSVcbU3FoGOzcNWAmU1TR0A==",
"license": "MIT",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.3.2.tgz",
"integrity": "sha512-5p201S7AZhliRxTU7uMKtSsoC8mgPA9bs9b5NQg1IRdRxJfflursXNVsgc3PcMqiUTul/v1s3k3rXXFlRE890w==",
"dependencies": {
"@mdx-js/mdx": "^3.0.0",
"@types/history": "^4.7.11",
@@ -2679,13 +2660,12 @@
}
},
"node_modules/@docusaurus/utils": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.4.0.tgz",
"integrity": "sha512-fRwnu3L3nnWaXOgs88BVBmG1yGjcQqZNHG+vInhEa2Sz2oQB+ZjbEMO5Rh9ePFpZ0YDiDUhpaVjwmS+AU2F14g==",
"license": "MIT",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.3.2.tgz",
"integrity": "sha512-f4YMnBVymtkSxONv4Y8js3Gez9IgHX+Lcg6YRMOjVbq8sgCcdYK1lf6SObAuz5qB/mxiSK7tW0M9aaiIaUSUJg==",
"dependencies": {
"@docusaurus/logger": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"@docusaurus/logger": "3.3.2",
"@docusaurus/utils-common": "3.3.2",
"@svgr/webpack": "^8.1.0",
"escape-string-regexp": "^4.0.0",
"file-loader": "^6.2.0",
@@ -2702,7 +2682,6 @@
"shelljs": "^0.8.5",
"tslib": "^2.6.0",
"url-loader": "^4.1.1",
"utility-types": "^3.10.0",
"webpack": "^5.88.1"
},
"engines": {
@@ -2718,10 +2697,9 @@
}
},
"node_modules/@docusaurus/utils-common": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.4.0.tgz",
"integrity": "sha512-NVx54Wr4rCEKsjOH5QEVvxIqVvm+9kh7q8aYTU5WzUU9/Hctd6aTrcZ3G0Id4zYJ+AeaG5K5qHA4CY5Kcm2iyQ==",
"license": "MIT",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.3.2.tgz",
"integrity": "sha512-QWFTLEkPYsejJsLStgtmetMFIA3pM8EPexcZ4WZ7b++gO5jGVH7zsipREnCHzk6+eDgeaXfkR6UPaTt86bp8Og==",
"dependencies": {
"tslib": "^2.6.0"
},
@@ -2738,18 +2716,15 @@
}
},
"node_modules/@docusaurus/utils-validation": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.4.0.tgz",
"integrity": "sha512-hYQ9fM+AXYVTWxJOT1EuNaRnrR2WGpRdLDQG07O8UOpsvCPWUVOeo26Rbm0JWY2sGLfzAb+tvJ62yF+8F+TV0g==",
"license": "MIT",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.3.2.tgz",
"integrity": "sha512-itDgFs5+cbW9REuC7NdXals4V6++KifgVMzoGOOOSIifBQw+8ULhy86u5e1lnptVL0sv8oAjq2alO7I40GR7pA==",
"dependencies": {
"@docusaurus/logger": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"fs-extra": "^11.2.0",
"@docusaurus/logger": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/utils-common": "3.3.2",
"joi": "^17.9.2",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"tslib": "^2.6.0"
},
"engines": {
@@ -13598,11 +13573,10 @@
}
},
"node_modules/prettier": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.1.tgz",
"integrity": "sha512-7CAwy5dRsxs8PHXT3twixW9/OEll8MLE0VRPCJyl7CkS6VHGPSlsVaWTiASPTyGyYRyApxlaWTzwUxVNrhcwDg==",
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -16012,10 +15986,9 @@
}
},
"node_modules/tailwindcss": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz",
"integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==",
"license": "MIT",
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz",
"integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@@ -16052,7 +16025,6 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.3"
},
-59
View File
@@ -1,59 +0,0 @@
import '@docusaurus/theme-classic/lib/theme/Unlisted/index';
import { useWindowSize } from '@docusaurus/theme-common';
import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem';
import React, { useEffect, useState } from 'react';
export default function VersionSwitcher(): JSX.Element {
const [versions, setVersions] = useState([]);
const [label, setLabel] = useState('Versions');
const windowSize = useWindowSize();
useEffect(() => {
async function getVersions() {
try {
let baseUrl = 'https://immich.app';
if (window.location.origin === 'http://localhost:3005') {
baseUrl = window.location.origin;
}
const response = await fetch(`${baseUrl}/archived-versions.json`);
const archiveVersions = await response.json();
const allVersions = [
{ label: 'Next', url: 'https://main.preview.immich.app' },
{ label: 'Latest', url: 'https://immich.app' },
...archiveVersions,
];
setVersions(allVersions);
const activeVersion = allVersions.find((version) => new URL(version.url).origin === window.location.origin);
if (activeVersion) {
setLabel(activeVersion.label);
}
} catch (error) {
console.error('Failed to fetch versions', error);
}
}
if (versions.length === 0) {
getVersions();
}
}, []);
return (
versions.length > 0 && (
<DropdownNavbarItem
className="navbar__item"
label={label}
mobile={windowSize === 'mobile'}
items={versions.map(({ label, url }) => ({
label,
to: url,
target: '_self',
}))}
/>
)
);
}
+1 -1
View File
@@ -36,7 +36,7 @@ function HomepageHeader() {
<Link
className="flex place-items-center place-content-center py-3 px-8 border bg-immich-dark-primary dark:bg-immich-primary rounded-full hover:no-underline text-immich-primary dark:text-immich-dark-bg font-bold uppercase"
to="https://discord.immich.app"
to="https://discord.gg/D8JsnBEuKb"
>
Discord
</Link>
+7 -15
View File
@@ -63,14 +63,12 @@ import {
mdiVectorCombine,
mdiVideo,
mdiWeb,
mdiContentDuplicate,
} from '@mdi/js';
import Layout from '@theme/Layout';
import React from 'react';
import { Item, Timeline } from '../components/timeline';
const releases = {
'v1.106.0': new Date(2024, 5, 11),
'v1.104.0': new Date(2024, 4, 13),
'v1.103.0': new Date(2024, 3, 29),
'v1.102.0': new Date(2024, 3, 15),
@@ -218,19 +216,13 @@ const roadmap: Item[] = [
];
const milestones: Item[] = [
withRelease({
icon: mdiContentDuplicate,
title: 'Similar image detection',
description: 'Detect duplicate assets that arent exactly identical',
release: 'v1.106.0',
}),
withRelease({
icon: mdiVectorCombine,
title: 'Container consolidation',
description:
'The microservices container can be run as a worker within the server image, allowing us to remove it from the default stack.',
release: 'v1.106.0',
}),
// withRelease({
// icon: mdiVectorCombine,
// title: 'Container consolidation',
// description:
// 'The microservices container can be run as a worker within the server image, allowing us to remove it from the default stack.',
// release: 'v1.106.0',
// }),
withRelease({
icon: mdiPencil,
iconColor: 'saddlebrown',
@@ -1,7 +0,0 @@
import ComponentTypes from '@theme-original/NavbarItem/ComponentTypes';
import VersionSwitcher from '@site/src/components/version-switcher';
export default {
...ComponentTypes,
'custom-versionSwitcher': VersionSwitcher,
};
-62
View File
@@ -1,62 +0,0 @@
[
{
"label": "v1.106.4",
"url": "https://v1.106.4.archive.immich.app"
},
{
"label": "v1.106.3",
"url": "https://v1.106.3.archive.immich.app"
},
{
"label": "v1.106.2",
"url": "https://v1.106.2.archive.immich.app"
},
{
"label": "v1.106.1",
"url": "https://v1.106.1.archive.immich.app"
},
{
"label": "v1.105.1",
"url": "https://v1.105.1.archive.immich.app/"
},
{
"label": "v1.105.0",
"url": "https://v1.105.0.archive.immich.app/"
},
{
"label": "v1.104.0",
"url": "https://v1.104.0.archive.immich.app/"
},
{
"label": "v1.103.1",
"url": "https://v1.103.1.archive.immich.app/"
},
{
"label": "v1.103.0",
"url": "https://v1.103.0.archive.immich.app/"
},
{
"label": "v1.102.3",
"url": "https://v1.102.3.archive.immich.app/"
},
{
"label": "v1.102.2",
"url": "https://v1.102.2.archive.immich.app/"
},
{
"label": "v1.102.1",
"url": "https://v1.102.1.archive.immich.app/"
},
{
"label": "v1.102.0",
"url": "https://v1.102.0.archive.immich.app/"
},
{
"label": "v1.101.0",
"url": "https://v1.101.0.archive.immich.app/"
},
{
"label": "v1.100.0",
"url": "https://v1.100.0.archive.immich.app/"
}
]
+59 -61
View File
@@ -1,12 +1,12 @@
{
"name": "immich-e2e",
"version": "1.106.4",
"version": "1.105.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-e2e",
"version": "1.106.4",
"version": "1.105.1",
"license": "GNU Affero General Public License version 3",
"devDependencies": {
"@immich/cli": "file:../cli",
@@ -39,7 +39,7 @@
},
"../cli": {
"name": "@immich/cli",
"version": "2.2.4",
"version": "2.2.0",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
@@ -81,14 +81,14 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.106.4",
"version": "1.105.1",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^20.14.2",
"@types/node": "^20.12.13",
"typescript": "^5.3.3"
}
},
@@ -1230,9 +1230,9 @@
"dev": true
},
"node_modules/@types/node": {
"version": "20.14.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz",
"integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==",
"version": "20.12.13",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.13.tgz",
"integrity": "sha512-gBGeanV41c1L171rR7wjbMiEpEI/l5XFQdLLfhr/REwpgDy/4U8y89+i8kRiLzDyZdOkXh+cRaTetUnCYutoXA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1344,17 +1344,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.12.0.tgz",
"integrity": "sha512-7F91fcbuDf/d3S8o21+r3ZncGIke/+eWk0EpO21LXhDfLahriZF9CGj4fbAetEjlaBdjdSm9a6VeXbpbT6Z40Q==",
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.11.0.tgz",
"integrity": "sha512-P+qEahbgeHW4JQ/87FuItjBj8O3MYv5gELDzr8QaQ7fsll1gSMTYb6j87MYyxwf3DtD7uGFB9ShwgmCJB5KmaQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.12.0",
"@typescript-eslint/type-utils": "7.12.0",
"@typescript-eslint/utils": "7.12.0",
"@typescript-eslint/visitor-keys": "7.12.0",
"@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/type-utils": "7.11.0",
"@typescript-eslint/utils": "7.11.0",
"@typescript-eslint/visitor-keys": "7.11.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@@ -1378,16 +1378,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "7.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.12.0.tgz",
"integrity": "sha512-dm/J2UDY3oV3TKius2OUZIFHsomQmpHtsV0FTh1WO8EKgHLQ1QCADUqscPgTpU+ih1e21FQSRjXckHn3txn6kQ==",
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.11.0.tgz",
"integrity": "sha512-yimw99teuaXVWsBcPO1Ais02kwJ1jmNA1KxE7ng0aT7ndr1pT1wqj0OJnsYVGKKlc4QJai86l/025L6z8CljOg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/scope-manager": "7.12.0",
"@typescript-eslint/types": "7.12.0",
"@typescript-eslint/typescript-estree": "7.12.0",
"@typescript-eslint/visitor-keys": "7.12.0",
"@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/typescript-estree": "7.11.0",
"@typescript-eslint/visitor-keys": "7.11.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1407,14 +1407,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "7.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.12.0.tgz",
"integrity": "sha512-itF1pTnN6F3unPak+kutH9raIkL3lhH1YRPGgt7QQOh43DQKVJXmWkpb+vpc/TiDHs6RSd9CTbDsc/Y+Ygq7kg==",
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz",
"integrity": "sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.12.0",
"@typescript-eslint/visitor-keys": "7.12.0"
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/visitor-keys": "7.11.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1425,14 +1425,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "7.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.12.0.tgz",
"integrity": "sha512-lib96tyRtMhLxwauDWUp/uW3FMhLA6D0rJ8T7HmH7x23Gk1Gwwu8UZ94NMXBvOELn6flSPiBrCKlehkiXyaqwA==",
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.11.0.tgz",
"integrity": "sha512-WmppUEgYy+y1NTseNMJ6mCFxt03/7jTOy08bcg7bxJJdsM4nuhnchyBbE8vryveaJUf62noH7LodPSo5Z0WUCg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "7.12.0",
"@typescript-eslint/utils": "7.12.0",
"@typescript-eslint/typescript-estree": "7.11.0",
"@typescript-eslint/utils": "7.11.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@@ -1453,9 +1453,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "7.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.12.0.tgz",
"integrity": "sha512-o+0Te6eWp2ppKY3mLCU+YA9pVJxhUJE15FV7kxuD9jgwIAa+w/ycGJBMrYDTpVGUM/tgpa9SeMOugSabWFq7bg==",
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz",
"integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1467,14 +1467,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "7.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.12.0.tgz",
"integrity": "sha512-5bwqLsWBULv1h6pn7cMW5dXX/Y2amRqLaKqsASVwbBHMZSnHqE/HN4vT4fE0aFsiwxYvr98kqOWh1a8ZKXalCQ==",
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz",
"integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "7.12.0",
"@typescript-eslint/visitor-keys": "7.12.0",
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/visitor-keys": "7.11.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -1522,16 +1522,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "7.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.12.0.tgz",
"integrity": "sha512-Y6hhwxwDx41HNpjuYswYp6gDbkiZ8Hin9Bf5aJQn1bpTs3afYY4GX+MPYxma8jtoIV2GRwTM/UJm/2uGCVv+DQ==",
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.11.0.tgz",
"integrity": "sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "7.12.0",
"@typescript-eslint/types": "7.12.0",
"@typescript-eslint/typescript-estree": "7.12.0"
"@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/typescript-estree": "7.11.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1545,13 +1545,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "7.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.12.0.tgz",
"integrity": "sha512-uZk7DevrQLL3vSnfFl5bj4sL75qC9D6EdjemIdbtkuUmIheWpuiiylSY01JxJE7+zGrOWDZrp1WxOuDntvKrHQ==",
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz",
"integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.12.0",
"@typescript-eslint/types": "7.11.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@@ -2700,9 +2700,9 @@
}
},
"node_modules/exiftool-vendored": {
"version": "26.2.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-26.2.0.tgz",
"integrity": "sha512-7P6jQ944or7ic2SJzW+uaWK4TLDXlaCppHrBayl4MpIrVcEeQjiQTez4/oOH0wULIRu4j4H6Xruz4SLrDaafUg==",
"version": "26.1.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-26.1.0.tgz",
"integrity": "sha512-Bhy2Ia86Agt3+PbJJhWeVMqJNXl74XJ0Oygef5F5uCL13fTxlmF8dECHiChyx8bBc3sxIw+2Q3ehWunJh3bs6w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4127,11 +4127,10 @@
}
},
"node_modules/pg": {
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz",
"integrity": "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==",
"version": "8.11.5",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.11.5.tgz",
"integrity": "sha512-jqgNHSKL5cbDjFlHyYsCXmQDrfIX/3RsNwYqpd4N0Kt8niLuNoRNH+aazv6cOd43gPh9Y4DjQCtb+X0MH0Hvnw==",
"dev": true,
"license": "MIT",
"dependencies": {
"pg-connection-string": "^2.6.4",
"pg-pool": "^3.6.2",
@@ -4386,11 +4385,10 @@
}
},
"node_modules/prettier": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.1.tgz",
"integrity": "sha512-7CAwy5dRsxs8PHXT3twixW9/OEll8MLE0VRPCJyl7CkS6VHGPSlsVaWTiASPTyGyYRyApxlaWTzwUxVNrhcwDg==",
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "1.106.4",
"version": "1.105.1",
"description": "",
"main": "index.js",
"type": "module",
-25
View File
@@ -1148,29 +1148,4 @@ describe('/asset', () => {
expect(video.checksum).toStrictEqual(checksum);
});
});
describe('POST /assets/exist', () => {
it('ignores invalid deviceAssetIds', async () => {
const response = await utils.checkExistingAssets(user1.accessToken, {
deviceId: 'test-assets-exist',
deviceAssetIds: ['invalid', 'INVALID'],
});
expect(response.existingIds).toHaveLength(0);
});
it('returns the IDs of existing assets', async () => {
await utils.createAsset(user1.accessToken, {
deviceId: 'test-assets-exist',
deviceAssetId: 'test-asset-0',
});
const response = await utils.checkExistingAssets(user1.accessToken, {
deviceId: 'test-assets-exist',
deviceAssetIds: ['test-asset-0'],
});
expect(response.existingIds).toEqual(['test-asset-0']);
});
});
});
+6 -58
View File
@@ -1,11 +1,4 @@
import {
LibraryResponseDto,
LoginResponseDto,
ScanLibraryDto,
getAllLibraries,
removeOfflineFiles,
scanLibrary,
} from '@immich/sdk';
import { LibraryResponseDto, LoginResponseDto, ScanLibraryDto, getAllLibraries, scanLibrary } from '@immich/sdk';
import { cpSync, existsSync } from 'node:fs';
import { Socket } from 'socket.io-client';
import { userDto, uuidDto } from 'src/fixtures';
@@ -391,51 +384,6 @@ describe('/libraries', () => {
);
});
it('should not try to delete offline files', async () => {
utils.createImageFile(`${testAssetDir}/temp/offline1/assetA.png`);
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline1`],
});
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(initialAssets).toEqual({
count: 1,
total: 1,
facets: [],
items: [expect.objectContaining({ originalFileName: 'assetA.png' })],
nextPage: null,
});
utils.removeImageFile(`${testAssetDir}/temp/offline1/assetA.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets: offlineAssets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
isOffline: true,
});
expect(offlineAssets).toEqual({
count: 1,
total: 1,
facets: [],
items: [expect.objectContaining({ originalFileName: 'assetA.png' })],
nextPage: null,
});
utils.createImageFile(`${testAssetDir}/temp/offline1/assetA.png`);
await removeOfflineFiles({ id: library.id }, { headers: asBearerAuth(admin.accessToken) });
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForWebsocketEvent({ event: 'assetDelete', total: 1 });
expect(existsSync(`${testAssetDir}/temp/offline1/assetA.png`)).toBe(true);
});
it('should scan new files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
@@ -559,10 +507,10 @@ describe('/libraries', () => {
it('should remove offline files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline2`],
importPaths: [`${testAssetDirInternal}/temp`],
});
utils.createImageFile(`${testAssetDir}/temp/offline2/assetA.png`);
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
@@ -570,9 +518,9 @@ describe('/libraries', () => {
const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
});
expect(initialAssets.count).toBe(1);
expect(initialAssets.count).toBe(3);
utils.removeImageFile(`${testAssetDir}/temp/offline2/assetA.png`);
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
@@ -593,7 +541,7 @@ describe('/libraries', () => {
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(0);
expect(assets.count).toBe(2);
});
it('should not remove online files', async () => {
+10 -15
View File
@@ -250,23 +250,18 @@ describe('/admin/users', () => {
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ avatar: { color: 'orange' } });
expect(body).toEqual({
avatar: { color: 'orange' },
memories: { enabled: false },
emailNotifications: { enabled: true, albumInvite: true, albumUpdate: true },
});
const after = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ avatar: { color: 'orange' } });
});
it('should update download archive size', async () => {
const { status, body } = await request(app)
.put(`/admin/users/${admin.userId}/preferences`)
.send({ download: { archiveSize: 1_234_567 } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ download: { archiveSize: 1_234_567 } });
const after = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ download: { archiveSize: 1_234_567 } });
expect(after).toEqual({
avatar: { color: 'orange' },
memories: { enabled: false },
emailNotifications: { enabled: true, albumInvite: true, albumUpdate: true },
});
});
});
-39
View File
@@ -173,45 +173,6 @@ describe('/users', () => {
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ memories: { enabled: false } });
});
it('should update avatar color', async () => {
const { status, body } = await request(app)
.put(`/users/me/preferences`)
.send({ avatar: { color: 'blue' } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ avatar: { color: 'blue' } });
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ avatar: { color: 'blue' } });
});
it('should require an integer for download archive size', async () => {
const { status, body } = await request(app)
.put(`/users/me/preferences`)
.send({ download: { archiveSize: 1_234_567.89 } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['download.archiveSize must be an integer number']));
});
it('should update download archive size', async () => {
const before = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(before).toMatchObject({ download: { archiveSize: 4 * 2 ** 30 } });
const { status, body } = await request(app)
.put(`/users/me/preferences`)
.send({ download: { archiveSize: 1_234_567 } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ download: { archiveSize: 1_234_567 } });
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ download: { archiveSize: 1_234_567 } });
});
});
describe('GET /users/:id', () => {
+8 -6
View File
@@ -3,7 +3,6 @@ import {
AssetMediaCreateDto,
AssetMediaResponseDto,
AssetResponseDto,
CheckExistingAssetsDto,
CreateAlbumDto,
CreateLibraryDto,
MetadataSearchDto,
@@ -11,7 +10,6 @@ import {
SharedLinkCreateDto,
UserAdminCreateDto,
ValidateLibraryDto,
checkExistingAssets,
createAlbum,
createApiKey,
createLibrary,
@@ -376,9 +374,6 @@ export const utils = {
getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
checkExistingAssets: (accessToken: string, checkExistingAssetsDto: CheckExistingAssetsDto) =>
checkExistingAssets({ checkExistingAssetsDto }, { headers: asBearerAuth(accessToken) }),
metadataSearch: async (accessToken: string, dto: MetadataSearchDto) => {
return searchMetadata({ metadataSearchDto: dto }, { headers: asBearerAuth(accessToken) });
},
@@ -398,7 +393,14 @@ export const utils = {
return;
}
await client.query('INSERT INTO asset_faces ("assetId", "personId") VALUES ($1, $2)', [assetId, personId]);
const vector = Array.from({ length: 512 }, Math.random);
const embedding = `[${vector.join(',')}]`;
await client.query('INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)', [
assetId,
personId,
embedding,
]);
},
setPersonThumbnail: async (personId: string) => {
@@ -1,40 +1,33 @@
from pathlib import Path
from typing import Any
import numpy as np
from insightface.model_zoo import RetinaFace
import onnxruntime as ort
from numpy.typing import NDArray
from app.models.base import InferenceModel
from app.models.transforms import decode_cv2
from app.models.session import ort_has_batch_dim, ort_expand_outputs
from app.models.transforms import decode_pil
from app.schemas import FaceDetectionOutput, ModelSession, ModelTask, ModelType
from .scrfd import SCRFD
from PIL import Image
from PIL.ImageOps import pad
class FaceDetector(InferenceModel):
depends = []
identity = (ModelType.DETECTION, ModelTask.FACIAL_RECOGNITION)
def __init__(
self,
model_name: str,
min_score: float = 0.7,
cache_dir: Path | str | None = None,
**model_kwargs: Any,
) -> None:
self.min_score = model_kwargs.pop("minScore", min_score)
super().__init__(model_name, cache_dir, **model_kwargs)
def _load(self) -> ModelSession:
session = self._make_session(self.model_path)
self.model = RetinaFace(session=session)
self.model.prepare(ctx_id=0, det_thresh=self.min_score, input_size=(640, 640))
if isinstance(session, ort.InferenceSession) and not ort_has_batch_dim(session):
ort_expand_outputs(session)
self.model = SCRFD(session=session)
return session
def _predict(self, inputs: NDArray[np.uint8] | bytes, **kwargs: Any) -> FaceDetectionOutput:
inputs = decode_cv2(inputs)
def _predict(self, inputs: NDArray[np.uint8] | bytes | Image.Image, **kwargs: Any) -> FaceDetectionOutput:
inputs = self._transform(inputs)
bboxes, landmarks = self._detect(inputs)
[bboxes], [landmarks] = self.model.detect(inputs, threshold=kwargs.pop("minScore", 0.7))
return {
"boxes": bboxes[:, :4].round(),
"scores": bboxes[:, 4],
@@ -44,5 +37,7 @@ class FaceDetector(InferenceModel):
def _detect(self, inputs: NDArray[np.uint8] | bytes) -> tuple[NDArray[np.float32], NDArray[np.float32]]:
return self.model.detect(inputs) # type: ignore
def configure(self, **kwargs: Any) -> None:
self.model.det_thresh = kwargs.pop("minScore", self.model.det_thresh)
def _transform(self, inputs: NDArray[np.uint8] | bytes | Image.Image) -> NDArray[np.uint8]:
image = decode_pil(inputs)
padded = pad(image, (640, 640), method=Image.Resampling.BICUBIC)
return np.array(padded, dtype=np.uint8)[None, ...]
@@ -2,16 +2,15 @@ from pathlib import Path
from typing import Any
import numpy as np
import onnx
import onnxruntime as ort
from insightface.model_zoo import ArcFaceONNX
from insightface.utils.face_align import norm_crop
from numpy.typing import NDArray
from onnx.tools.update_model_dims import update_inputs_outputs_dims
from PIL import Image
from app.config import clean_name, log
from app.models.base import InferenceModel
from app.models.session import ort_add_batch_dim, ort_has_batch_dim
from app.models.transforms import decode_cv2
from app.schemas import FaceDetectionOutput, FacialRecognitionOutput, ModelSession, ModelTask, ModelType
@@ -32,8 +31,9 @@ class FaceRecognizer(InferenceModel):
def _load(self) -> ModelSession:
session = self._make_session(self.model_path)
if not self._has_batch_dim(session):
self._add_batch_dim(self.model_path)
if isinstance(session, ort.InferenceSession) and not ort_has_batch_dim(session):
log.info(f"Adding batch dimension to recognition model {self.model_name}")
ort_add_batch_dim(self.model_path, self.model_path)
session = self._make_session(self.model_path)
self.model = ArcFaceONNX(
self.model_path.with_suffix(".onnx").as_posix(),
@@ -62,16 +62,3 @@ class FaceRecognizer(InferenceModel):
def _crop(self, image: NDArray[np.uint8], faces: FaceDetectionOutput) -> list[NDArray[np.uint8]]:
return [norm_crop(image, landmark) for landmark in faces["landmarks"]]
def _has_batch_dim(self, session: ort.InferenceSession) -> bool:
return not isinstance(session, ort.InferenceSession) or session.get_inputs()[0].shape[0] == "batch"
def _add_batch_dim(self, model_path: Path) -> None:
log.debug(f"Adding batch dimension to model {model_path}")
proto = onnx.load(model_path)
static_input_dims = [shape.dim_value for shape in proto.graph.input[0].type.tensor_type.shape.dim[1:]]
static_output_dims = [shape.dim_value for shape in proto.graph.output[0].type.tensor_type.shape.dim[1:]]
input_dims = {proto.graph.input[0].name: ["batch"] + static_input_dims}
output_dims = {proto.graph.output[0].name: ["batch"] + static_output_dims}
updated_proto = update_inputs_outputs_dims(proto, input_dims, output_dims)
onnx.save(updated_proto, model_path)
@@ -0,0 +1,325 @@
# Based on InsightFace-REST by SthPhoenix https://github.com/SthPhoenix/InsightFace-REST/blob/master/src/api_trt/modules/model_zoo/detectors/scrfd.py
# Primary changes made:
# 1. Removed CuPy-related code
# 2. Adapted proposal generation to be thread-safe
# 3. Added typing
# 4. Assume RGB input
# 5. Removed unused variables
# Copyright 2021 SthPhoenix
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# -*- coding: utf-8 -*-
# Based on Jia Guo reference implementation at
# https://github.com/deepinsight/insightface/blob/master/detection/scrfd/tools/scrfd.py
from __future__ import division
import cv2
import numpy as np
from numba import njit
from app.schemas import ModelSession
from numpy.typing import NDArray
@njit(cache=True, nogil=True)
def nms(dets, threshold: float = 0.4) -> NDArray[np.float32]:
x1 = dets[:, 0]
y1 = dets[:, 1]
x2 = dets[:, 2]
y2 = dets[:, 3]
scores = dets[:, 4]
areas = (x2 - x1 + 1) * (y2 - y1 + 1)
order = scores.argsort()[::-1]
keep = []
while order.size > 0:
i = order[0]
keep.append(i)
xx1 = np.maximum(x1[i], x1[order[1:]])
yy1 = np.maximum(y1[i], y1[order[1:]])
xx2 = np.minimum(x2[i], x2[order[1:]])
yy2 = np.minimum(y2[i], y2[order[1:]])
w = np.maximum(0.0, xx2 - xx1 + 1)
h = np.maximum(0.0, yy2 - yy1 + 1)
inter = w * h
ovr = inter / (areas[i] + areas[order[1:]] - inter)
inds = np.where(ovr <= threshold)[0]
order = order[inds + 1]
return np.asarray(keep)
@njit(fastmath=True, cache=True, nogil=True)
def single_distance2bbox(point: NDArray[np.float32], distance: NDArray[np.float32], stride: int) -> NDArray[np.float32]:
"""
Fast conversion of single bbox distances to coordinates
:param point: Anchor point
:param distance: Bbox distances from anchor point
:param stride: Current stride scale
:return: bbox
"""
distance[0] = point[0] - distance[0] * stride
distance[1] = point[1] - distance[1] * stride
distance[2] = point[0] + distance[2] * stride
distance[3] = point[1] + distance[3] * stride
return distance
@njit(fastmath=True, cache=True, nogil=True)
def single_distance2kps(point: NDArray[np.float32], distance: NDArray[np.float32], stride: int) -> NDArray[np.float32]:
"""
Fast conversion of single keypoint distances to coordinates
:param point: Anchor point
:param distance: Keypoint distances from anchor point
:param stride: Current stride scale
:return: keypoint
"""
for ix in range(0, distance.shape[0], 2):
distance[ix] = distance[ix] * stride + point[0]
distance[ix + 1] = distance[ix + 1] * stride + point[1]
return distance
@njit(fastmath=True, cache=True, nogil=True)
def generate_proposals(
score_blob: NDArray[np.float32],
bbox_blob: NDArray[np.float32],
kpss_blob: NDArray[np.float32],
stride: int,
anchors: NDArray[np.float32],
threshold: float,
) -> tuple[NDArray[np.float32], NDArray[np.float32], NDArray[np.float32]]:
"""
Convert distances from anchors to actual coordinates on source image
and filter proposals by confidence threshold.
:param score_blob: Raw scores for stride
:param bbox_blob: Raw bbox distances for stride
:param kpss_blob: Raw keypoints distances for stride
:param stride: Stride scale
:param anchors: Precomputed anchors for stride
:param threshold: Confidence threshold
:return: Filtered scores, bboxes and keypoints
"""
idxs = []
for ix in range(score_blob.shape[0]):
if score_blob[ix][0] > threshold:
idxs.append(ix)
score_out = np.empty((len(idxs), 1), dtype="float32")
bbox_out = np.empty((len(idxs), 4), dtype="float32")
kpss_out = np.empty((len(idxs), 10), dtype="float32")
for i in range(len(idxs)):
ix = idxs[i]
score_out[i] = score_blob[ix]
bbox_out[i] = single_distance2bbox(anchors[ix], bbox_blob[ix], stride)
kpss_out[i] = single_distance2kps(anchors[ix], kpss_blob[ix], stride)
return score_out, bbox_out, kpss_out
@njit(fastmath=True, cache=True, nogil=True)
def filter(
bboxes_list: NDArray[np.float32],
kpss_list: NDArray[np.float32],
scores_list: NDArray[np.float32],
nms_threshold: float = 0.4,
) -> tuple[NDArray[np.float32], NDArray[np.float32]]:
"""
Filter postprocessed network outputs with NMS
:param bboxes_list: List of bboxes (np.ndarray)
:param kpss_list: List of keypoints (np.ndarray)
:param scores_list: List of scores (np.ndarray)
:return: Face bboxes with scores [t,l,b,r,score], and key points
"""
pre_det = np.hstack((bboxes_list, scores_list))
keep = nms(pre_det, threshold=nms_threshold)
det = pre_det[keep, :]
kpss = kpss_list[keep, :]
kpss = kpss.reshape((kpss.shape[0], -1, 2))
return det, kpss
class SCRFD:
def __init__(self, session: ModelSession):
self.session = session
self.center_cache: dict[tuple[int, int], NDArray[np.float32]] = {}
self.nms_threshold = 0.4
self.fmc = 3
self._feat_stride_fpn = [8, 16, 32]
self._num_anchors = 2
def prepare(self, nms_threshold: float = 0.4) -> None:
"""
Populate class parameters
:param nms_threshold: Threshold for NMS IoU
"""
self.nms_threshold = nms_threshold
def detect(
self, imgs: NDArray[np.uint8], threshold: float = 0.5
) -> tuple[list[NDArray[np.float32]], list[NDArray[np.float32]]]:
"""
Run detection pipeline for provided images
:param img: Raw image as nd.ndarray with HWC shape
:param threshold: Confidence threshold
:return: Face bboxes with scores [t,l,b,r,score], and key points
"""
height, width = imgs.shape[1:3]
blob = self._preprocess(imgs)
net_outs = self._forward(blob)
batch_bboxes, batch_kpss, batch_scores = self._postprocess(net_outs, height, width, threshold)
dets_list = []
kpss_list = []
for e in range(imgs.shape[0]):
if len(batch_bboxes[e]) == 0:
det, kpss = np.zeros((0, 5), dtype="float32"), np.zeros((0, 10), dtype="float32")
else:
det, kpss = filter(batch_bboxes[e], batch_kpss[e], batch_scores[e], self.nms_threshold)
dets_list.append(det)
kpss_list.append(kpss)
return dets_list, kpss_list
@staticmethod
def _build_anchors(
input_height: int, input_width: int, strides: list[int], num_anchors: int
) -> NDArray[np.float32]:
"""
Precompute anchor points for provided image size
:param input_height: Input image height
:param input_width: Input image width
:param strides: Model strides
:param num_anchors: Model num anchors
:return: box centers
"""
centers = []
for stride in strides:
height = input_height // stride
width = input_width // stride
anchor_centers = np.stack(np.mgrid[:height, :width][::-1], axis=-1).astype(np.float32)
anchor_centers = (anchor_centers * stride).reshape((-1, 2))
if num_anchors > 1:
anchor_centers = np.stack([anchor_centers] * num_anchors, axis=1).reshape((-1, 2))
centers.append(anchor_centers)
return centers
def _preprocess(self, images: NDArray[np.uint8]):
"""
Normalize image on CPU if backend can't provide CUDA stream,
otherwise preprocess image on GPU using CuPy
:param img: Raw image as np.ndarray with HWC shape
:return: Preprocessed image or None if image was processed on device
"""
input_size = tuple(images[0].shape[0:2][::-1])
return cv2.dnn.blobFromImages(images, 1.0 / 128, input_size, (127.5, 127.5, 127.5), swapRB=False)
def _forward(self, blob: NDArray[np.float32]) -> list[NDArray[np.float32]]:
"""
Send input data to inference backend.
:param blob: Preprocessed image of shape NCHW or None
:return: network outputs
"""
return self.session.run(None, {"input.1": blob})
def _postprocess(
self, net_outs: list[NDArray[np.float32]], height: int, width: int, threshold: float
) -> tuple[list[NDArray[np.float32]], list[NDArray[np.float32]], list[NDArray[np.float32]]]:
"""
Precompute anchor points for provided image size and process network outputs
:param net_outs: Network outputs
:param input_height: Input image height
:param input_width: Input image width
:param threshold: Confidence threshold
:return: filtered bboxes, keypoints and scores
"""
key = (height, width)
if not self.center_cache.get(key):
self.center_cache[key] = self._build_anchors(height, width, self._feat_stride_fpn, self._num_anchors)
anchor_centers = self.center_cache[key]
bboxes, kpss, scores = self._process_strides(net_outs, threshold, anchor_centers)
return bboxes, kpss, scores
def _process_strides(
self, net_outs: list[NDArray[np.float32]], threshold: float, anchors: NDArray[np.float32]
) -> tuple[list[NDArray[np.float32]], list[NDArray[np.float32]], list[NDArray[np.float32]]]:
"""
Process network outputs by strides and return results proposals filtered by threshold
:param net_outs: Network outputs
:param threshold: Confidence threshold
:param anchor_centers: Precomputed anchor centers for all strides
:return: filtered bboxes, keypoints and scores
"""
batch_size = net_outs[0].shape[0]
bboxes_by_img = []
kpss_by_img = []
scores_by_img = []
for batch in range(batch_size):
scores_strided = []
bboxes_strided = []
kpss_strided = []
for idx, stride in enumerate(self._feat_stride_fpn):
score_blob = net_outs[idx][batch]
bbox_blob = net_outs[idx + self.fmc][batch]
kpss_blob = net_outs[idx + self.fmc * 2][batch]
stride_anchors = anchors[idx]
score_list, bbox_list, kpss_list = generate_proposals(
score_blob,
bbox_blob,
kpss_blob,
stride,
stride_anchors,
threshold,
)
scores_strided.append(score_list)
bboxes_strided.append(bbox_list)
kpss_strided.append(kpss_list)
bboxes_by_img.append(np.concatenate(bboxes_strided, axis=0))
kpss_by_img.append(np.concatenate(kpss_strided, axis=0))
scores_by_img.append(np.concatenate(scores_strided, axis=0))
return bboxes_by_img, kpss_by_img, scores_by_img
+34
View File
@@ -0,0 +1,34 @@
from pathlib import Path
import numpy as np
import onnx
import onnxruntime as ort
from numpy.typing import NDArray
from onnx.shape_inference import infer_shapes
from onnx.tools.update_model_dims import update_inputs_outputs_dims
def ort_has_batch_dim(session: ort.InferenceSession) -> bool:
return session.get_inputs()[0].shape[0] == "batch"
def ort_expand_outputs(session: ort.InferenceSession) -> None:
original_run = session.run
def run(output_names: list[str], input_feed: dict[str, NDArray[np.float32]]) -> list[NDArray[np.float32]]:
out: list[NDArray[np.float32]] = original_run(output_names, input_feed)
out = [np.expand_dims(o, axis=0) for o in out]
return out
session.run = run
def ort_add_batch_dim(input_path: Path, output_path: Path) -> None:
proto = onnx.load(input_path)
static_input_dims = [shape.dim_value for shape in proto.graph.input[0].type.tensor_type.shape.dim[1:]]
static_output_dims = [shape.dim_value for shape in proto.graph.output[0].type.tensor_type.shape.dim[1:]]
input_dims = {proto.graph.input[0].name: ["batch"] + static_input_dims}
output_dims = {proto.graph.output[0].name: ["batch"] + static_output_dims}
updated_proto = update_inputs_outputs_dims(proto, input_dims, output_dims)
inferred = infer_shapes(updated_proto)
onnx.save(inferred, output_path)
+3 -1
View File
@@ -3,6 +3,7 @@ from typing import IO
import cv2
import numpy as np
from numba import njit
from numpy.typing import NDArray
from PIL import Image
@@ -30,10 +31,11 @@ def to_numpy(img: Image.Image) -> NDArray[np.float32]:
return np.asarray(img if img.mode == "RGB" else img.convert("RGB"), dtype=np.float32) / 255.0
@njit(cache=True, fastmath=True, nogil=True)
def normalize(
img: NDArray[np.float32], mean: float | NDArray[np.float32], std: float | NDArray[np.float32]
) -> NDArray[np.float32]:
return np.divide(img - mean, std, dtype=np.float32)
return (img - mean) / std
def get_pil_resampling(resample: str) -> Image.Resampling:
+67
View File
@@ -7,6 +7,7 @@ from types import SimpleNamespace
from typing import Any, Callable
from unittest import mock
from app.models.session import ort_add_batch_dim, ort_has_batch_dim, ort_squeeze_outputs
import cv2
import numpy as np
import onnxruntime as ort
@@ -734,3 +735,69 @@ class TestEndpoints:
assert expected_face["boundingBox"] == actual_face["boundingBox"]
assert np.allclose(expected_face["embedding"], actual_face["embedding"])
assert np.allclose(expected_face["score"], actual_face["score"])
class TestSessionUtils:
def test_ort_has_batch_dim(self, mocker: MockerFixture) -> None:
mock_session = mocker.Mock(spec=ort.InferenceSession)
mock_session.get_inputs.return_value = [SimpleNamespace(shape=["batch", 3, 224, 224], name="input.1")]
assert ort_has_batch_dim(mock_session) is True
def test_ort_has_no_batch_dim(self, mocker: MockerFixture) -> None:
mock_session = mocker.Mock(spec=ort.InferenceSession)
mock_session.get_inputs.return_value = [SimpleNamespace(shape=[1, 3, 224, 224], name="input.1")]
assert ort_has_batch_dim(mock_session) is False
def test_ort_squeeze_outputs(self, mocker: MockerFixture) -> None:
mock_session = mocker.Mock(spec=ort.InferenceSession)
mock_session.run.return_value = [np.random.rand(1, 3, 224, 224).astype(np.float32)]
ort_squeeze_outputs(mock_session)
out = mock_session.run(["output"], {"input": np.random.rand(1, 3, 224, 224).astype(np.float32)})
assert len(out) == 1
assert out[0].shape == (3, 224, 224)
def test_ort_add_batch_dim(self, mocker: MockerFixture) -> None:
mock_proto = mocker.Mock()
mock_proto.graph.input = [
SimpleNamespace(name="input", type=mock.Mock(tensor_type=SimpleNamespace(shape=SimpleNamespace())))
]
mock_proto.graph.output = [
SimpleNamespace(name="output", type=SimpleNamespace(tensor_type=SimpleNamespace(shape=SimpleNamespace())))
]
mock_proto.graph.input[0].type.tensor_type.shape.dim = [
SimpleNamespace(dim_value=3),
SimpleNamespace(dim_value=224),
SimpleNamespace(dim_value=224),
]
mock_proto.graph.output[0].type.tensor_type.shape.dim = [
SimpleNamespace(dim_value=3),
SimpleNamespace(dim_value=224),
SimpleNamespace(dim_value=224),
]
mock_load = mocker.patch("app.models.session.onnx.load")
mock_load.return_value = mock_proto
mock_update_dims = mocker.patch("app.models.session.update_inputs_outputs_dims")
mock_updated = mocker.Mock()
mock_update_dims.return_value = mock_updated
mock_infer_shapes = mocker.patch("app.models.session.infer_shapes")
mock_inferred = mocker.Mock()
mock_infer_shapes.return_value = mock_inferred
mock_save = mocker.patch("app.models.session.onnx.save")
input_path, output_path = Path("input.onnx"), Path("output.onnx")
ort_add_batch_dim(input_path, output_path)
mock_load.assert_called_once_with(input_path)
mock_save.assert_called_once_with(mock_inferred, output_path)
mock_update_dims.assert_called_once_with(mock_proto, mock.ANY, mock.ANY)
assert mock_update_dims.call_args_list == [
mock.call(mock_proto, {"input": ["batch", 224, 224]}, {"output": ["batch", 224, 224]})
]
+97 -37
View File
@@ -1236,13 +1236,13 @@ socks = ["socksio (==1.*)"]
[[package]]
name = "huggingface-hub"
version = "0.23.3"
version = "0.23.2"
description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
optional = false
python-versions = ">=3.8.0"
files = [
{file = "huggingface_hub-0.23.3-py3-none-any.whl", hash = "sha256:22222c41223f1b7c209ae5511d2d82907325a0e3cdbce5f66949d43c598ff3bc"},
{file = "huggingface_hub-0.23.3.tar.gz", hash = "sha256:1a1118a0b3dea3bab6c325d71be16f5ffe441d32f3ac7c348d6875911b694b5b"},
{file = "huggingface_hub-0.23.2-py3-none-any.whl", hash = "sha256:48727a16e704d409c4bb5913613308499664f22a99743435dc3a13b23c485827"},
{file = "huggingface_hub-0.23.2.tar.gz", hash = "sha256:f6829b62d5fdecb452a76fdbec620cba4c1573655a8d710c1df71735fd9edbd2"},
]
[package.dependencies]
@@ -1528,6 +1528,36 @@ files = [
lint = ["pre-commit (>=3.3)"]
test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
[[package]]
name = "llvmlite"
version = "0.42.0"
description = "lightweight wrapper around basic LLVM functionality"
optional = false
python-versions = ">=3.9"
files = [
{file = "llvmlite-0.42.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3366938e1bf63d26c34fbfb4c8e8d2ded57d11e0567d5bb243d89aab1eb56098"},
{file = "llvmlite-0.42.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c35da49666a21185d21b551fc3caf46a935d54d66969d32d72af109b5e7d2b6f"},
{file = "llvmlite-0.42.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70f44ccc3c6220bd23e0ba698a63ec2a7d3205da0d848804807f37fc243e3f77"},
{file = "llvmlite-0.42.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:763f8d8717a9073b9e0246998de89929071d15b47f254c10eef2310b9aac033d"},
{file = "llvmlite-0.42.0-cp310-cp310-win_amd64.whl", hash = "sha256:8d90edf400b4ceb3a0e776b6c6e4656d05c7187c439587e06f86afceb66d2be5"},
{file = "llvmlite-0.42.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ae511caed28beaf1252dbaf5f40e663f533b79ceb408c874c01754cafabb9cbf"},
{file = "llvmlite-0.42.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81e674c2fe85576e6c4474e8c7e7aba7901ac0196e864fe7985492b737dbab65"},
{file = "llvmlite-0.42.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb3975787f13eb97629052edb5017f6c170eebc1c14a0433e8089e5db43bcce6"},
{file = "llvmlite-0.42.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5bece0cdf77f22379f19b1959ccd7aee518afa4afbd3656c6365865f84903f9"},
{file = "llvmlite-0.42.0-cp311-cp311-win_amd64.whl", hash = "sha256:7e0c4c11c8c2aa9b0701f91b799cb9134a6a6de51444eff5a9087fc7c1384275"},
{file = "llvmlite-0.42.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:08fa9ab02b0d0179c688a4216b8939138266519aaa0aa94f1195a8542faedb56"},
{file = "llvmlite-0.42.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b2fce7d355068494d1e42202c7aff25d50c462584233013eb4470c33b995e3ee"},
{file = "llvmlite-0.42.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebe66a86dc44634b59a3bc860c7b20d26d9aaffcd30364ebe8ba79161a9121f4"},
{file = "llvmlite-0.42.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d47494552559e00d81bfb836cf1c4d5a5062e54102cc5767d5aa1e77ccd2505c"},
{file = "llvmlite-0.42.0-cp312-cp312-win_amd64.whl", hash = "sha256:05cb7e9b6ce69165ce4d1b994fbdedca0c62492e537b0cc86141b6e2c78d5888"},
{file = "llvmlite-0.42.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bdd3888544538a94d7ec99e7c62a0cdd8833609c85f0c23fcb6c5c591aec60ad"},
{file = "llvmlite-0.42.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d0936c2067a67fb8816c908d5457d63eba3e2b17e515c5fe00e5ee2bace06040"},
{file = "llvmlite-0.42.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a78ab89f1924fc11482209f6799a7a3fc74ddc80425a7a3e0e8174af0e9e2301"},
{file = "llvmlite-0.42.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7599b65c7af7abbc978dbf345712c60fd596aa5670496561cc10e8a71cebfb2"},
{file = "llvmlite-0.42.0-cp39-cp39-win_amd64.whl", hash = "sha256:43d65cc4e206c2e902c1004dd5418417c4efa6c1d04df05c6c5675a27e8ca90e"},
{file = "llvmlite-0.42.0.tar.gz", hash = "sha256:f92b09243c0cc3f457da8b983f67bd8e1295d0f5b3746c7a1861d7a99403854a"},
]
[[package]]
name = "locust"
version = "2.28.0"
@@ -1864,6 +1894,40 @@ doc = ["nb2plots (>=0.7)", "nbconvert (<7.9)", "numpydoc (>=1.6)", "pillow (>=9.
extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.11)", "sympy (>=1.10)"]
test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"]
[[package]]
name = "numba"
version = "0.59.1"
description = "compiling Python code using LLVM"
optional = false
python-versions = ">=3.9"
files = [
{file = "numba-0.59.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97385a7f12212c4f4bc28f648720a92514bee79d7063e40ef66c2d30600fd18e"},
{file = "numba-0.59.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0b77aecf52040de2a1eb1d7e314497b9e56fba17466c80b457b971a25bb1576d"},
{file = "numba-0.59.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3476a4f641bfd58f35ead42f4dcaf5f132569c4647c6f1360ccf18ee4cda3990"},
{file = "numba-0.59.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:525ef3f820931bdae95ee5379c670d5c97289c6520726bc6937a4a7d4230ba24"},
{file = "numba-0.59.1-cp310-cp310-win_amd64.whl", hash = "sha256:990e395e44d192a12105eca3083b61307db7da10e093972ca285c85bef0963d6"},
{file = "numba-0.59.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:43727e7ad20b3ec23ee4fc642f5b61845c71f75dd2825b3c234390c6d8d64051"},
{file = "numba-0.59.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:411df625372c77959570050e861981e9d196cc1da9aa62c3d6a836b5cc338966"},
{file = "numba-0.59.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2801003caa263d1e8497fb84829a7ecfb61738a95f62bc05693fcf1733e978e4"},
{file = "numba-0.59.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dd2842fac03be4e5324ebbbd4d2d0c8c0fc6e0df75c09477dd45b288a0777389"},
{file = "numba-0.59.1-cp311-cp311-win_amd64.whl", hash = "sha256:0594b3dfb369fada1f8bb2e3045cd6c61a564c62e50cf1f86b4666bc721b3450"},
{file = "numba-0.59.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1cce206a3b92836cdf26ef39d3a3242fec25e07f020cc4feec4c4a865e340569"},
{file = "numba-0.59.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8c8b4477763cb1fbd86a3be7050500229417bf60867c93e131fd2626edb02238"},
{file = "numba-0.59.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d80bce4ef7e65bf895c29e3889ca75a29ee01da80266a01d34815918e365835"},
{file = "numba-0.59.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7ad1d217773e89a9845886401eaaab0a156a90aa2f179fdc125261fd1105096"},
{file = "numba-0.59.1-cp312-cp312-win_amd64.whl", hash = "sha256:5bf68f4d69dd3a9f26a9b23548fa23e3bcb9042e2935257b471d2a8d3c424b7f"},
{file = "numba-0.59.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4e0318ae729de6e5dbe64c75ead1a95eb01fabfe0e2ebed81ebf0344d32db0ae"},
{file = "numba-0.59.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0f68589740a8c38bb7dc1b938b55d1145244c8353078eea23895d4f82c8b9ec1"},
{file = "numba-0.59.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:649913a3758891c77c32e2d2a3bcbedf4a69f5fea276d11f9119677c45a422e8"},
{file = "numba-0.59.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9712808e4545270291d76b9a264839ac878c5eb7d8b6e02c970dc0ac29bc8187"},
{file = "numba-0.59.1-cp39-cp39-win_amd64.whl", hash = "sha256:8d51ccd7008a83105ad6a0082b6a2b70f1142dc7cfd76deb8c5a862367eb8c86"},
{file = "numba-0.59.1.tar.gz", hash = "sha256:76f69132b96028d2774ed20415e8c528a34e3299a40581bae178f0994a2f370b"},
]
[package.dependencies]
llvmlite = "==0.42.*"
numpy = ">=1.22,<1.27"
[[package]]
name = "numpy"
version = "1.26.3"
@@ -2037,11 +2101,8 @@ description = "ONNX Runtime is a runtime accelerator for Machine Learning models
optional = false
python-versions = "*"
files = [
{file = "onnxruntime_openvino-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ed693011b472f9a617b2d5c4785d5fa1e1b77f7cb2b02e47b899534ec6c6396"},
{file = "onnxruntime_openvino-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:5152b5e56e83e022ced2986700d68dd8ba7b1466761725ce774f679c5710ab87"},
{file = "onnxruntime_openvino-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ce3b1aa06d6b8b732d314d217028ec4735de5806215c44d3bdbcad03b9260d5"},
{file = "onnxruntime_openvino-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:21133a701bb07ea19e01f48b8c23beee575f2e879f49173843f275d7c91a625a"},
{file = "onnxruntime_openvino-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76824dac3c392ad4b812f29c18be2055ab3bba2e3c111e44baae847b33d5b081"},
]
[package.dependencies]
@@ -2054,18 +2115,18 @@ sympy = "*"
[[package]]
name = "opencv-python-headless"
version = "4.10.0.82"
version = "4.9.0.80"
description = "Wrapper package for OpenCV python bindings."
optional = false
python-versions = ">=3.6"
files = [
{file = "opencv-python-headless-4.10.0.82.tar.gz", hash = "sha256:de9e742c1b9540816fbd115b0b03841d41ed0c65566b0d7a5371f98b131b7e6d"},
{file = "opencv_python_headless-4.10.0.82-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:a09ed50ba21cc5bf5d436cb0e784ad09c692d6b1d1454252772f6c8f2c7b4088"},
{file = "opencv_python_headless-4.10.0.82-cp37-abi3-macosx_12_0_x86_64.whl", hash = "sha256:977a5fd21e1fe0d3d2134887db4441f8725abeae95150126302f31fcd9f548fa"},
{file = "opencv_python_headless-4.10.0.82-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db4ec6755838b0be12510bfc9ffb014779c612418f11f4f7e6f505c36124a3aa"},
{file = "opencv_python_headless-4.10.0.82-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10a37fa5276967ecf6eb297295b16b28b7a2eb3b568ca0ee469fb1a5954de298"},
{file = "opencv_python_headless-4.10.0.82-cp37-abi3-win32.whl", hash = "sha256:94736e9b322d13db4768fd35588ad5e8995e78e207263076bfbee18aac835ad5"},
{file = "opencv_python_headless-4.10.0.82-cp37-abi3-win_amd64.whl", hash = "sha256:c1822fa23d1641c0249ed5eb906f4c385f7959ff1bd601a776d56b0c18914af4"},
{file = "opencv-python-headless-4.9.0.80.tar.gz", hash = "sha256:71a4cd8cf7c37122901d8e81295db7fb188730e33a0e40039a4e59c1030b0958"},
{file = "opencv_python_headless-4.9.0.80-cp37-abi3-macosx_10_16_x86_64.whl", hash = "sha256:2ea8a2edc4db87841991b2fbab55fc07b97ecb602e0f47d5d485bd75cee17c1a"},
{file = "opencv_python_headless-4.9.0.80-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0ee54e27be493e8f7850847edae3128e18b540dac1d7b2e4001b8944e11e1c6"},
{file = "opencv_python_headless-4.9.0.80-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57ce2865e8fec431c6f97a81e9faaf23fa5be61011d0a75ccf47a3c0d65fa73d"},
{file = "opencv_python_headless-4.9.0.80-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:976656362d68d9f40a5c66f83901430538002465f7db59142784f3893918f3df"},
{file = "opencv_python_headless-4.9.0.80-cp37-abi3-win32.whl", hash = "sha256:11e3849d83e6651d4e7699aadda9ec7ed7c38957cbbcb99db074f2a2d2de9670"},
{file = "opencv_python_headless-4.9.0.80-cp37-abi3-win_amd64.whl", hash = "sha256:a8056c2cb37cd65dfcdf4153ca16f7362afcf3a50d600d6bb69c660fc61ee29c"},
]
[package.dependencies]
@@ -2438,13 +2499,13 @@ files = [
[[package]]
name = "pytest"
version = "8.2.2"
version = "8.2.1"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"},
{file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"},
{file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"},
{file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"},
]
[package.dependencies]
@@ -2601,7 +2662,6 @@ files = [
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
@@ -2799,28 +2859,28 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "ruff"
version = "0.4.8"
version = "0.4.7"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.4.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7663a6d78f6adb0eab270fa9cf1ff2d28618ca3a652b60f2a234d92b9ec89066"},
{file = "ruff-0.4.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eeceb78da8afb6de0ddada93112869852d04f1cd0f6b80fe464fd4e35c330913"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aad360893e92486662ef3be0a339c5ca3c1b109e0134fcd37d534d4be9fb8de3"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:284c2e3f3396fb05f5f803c9fffb53ebbe09a3ebe7dda2929ed8d73ded736deb"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7354f921e3fbe04d2a62d46707e569f9315e1a613307f7311a935743c51a764"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:72584676164e15a68a15778fd1b17c28a519e7a0622161eb2debdcdabdc71883"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9678d5c9b43315f323af2233a04d747409d1e3aa6789620083a82d1066a35199"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704977a658131651a22b5ebeb28b717ef42ac6ee3b11e91dc87b633b5d83142b"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05f8d6f0c3cce5026cecd83b7a143dcad503045857bc49662f736437380ad45"},
{file = "ruff-0.4.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ea874950daca5697309d976c9afba830d3bf0ed66887481d6bca1673fc5b66a"},
{file = "ruff-0.4.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fc95aac2943ddf360376be9aa3107c8cf9640083940a8c5bd824be692d2216dc"},
{file = "ruff-0.4.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:384154a1c3f4bf537bac69f33720957ee49ac8d484bfc91720cc94172026ceed"},
{file = "ruff-0.4.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e9d5ce97cacc99878aa0d084c626a15cd21e6b3d53fd6f9112b7fc485918e1fa"},
{file = "ruff-0.4.8-py3-none-win32.whl", hash = "sha256:6d795d7639212c2dfd01991259460101c22aabf420d9b943f153ab9d9706e6a9"},
{file = "ruff-0.4.8-py3-none-win_amd64.whl", hash = "sha256:e14a3a095d07560a9d6769a72f781d73259655919d9b396c650fc98a8157555d"},
{file = "ruff-0.4.8-py3-none-win_arm64.whl", hash = "sha256:14019a06dbe29b608f6b7cbcec300e3170a8d86efaddb7b23405cb7f7dcaf780"},
{file = "ruff-0.4.8.tar.gz", hash = "sha256:16d717b1d57b2e2fd68bd0bf80fb43931b79d05a7131aa477d66fc40fbd86268"},
{file = "ruff-0.4.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e089371c67892a73b6bb1525608e89a2aca1b77b5440acf7a71dda5dac958f9e"},
{file = "ruff-0.4.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:10f973d521d910e5f9c72ab27e409e839089f955be8a4c8826601a6323a89753"},
{file = "ruff-0.4.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59c3d110970001dfa494bcd95478e62286c751126dfb15c3c46e7915fc49694f"},
{file = "ruff-0.4.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa9773c6c00f4958f73b317bc0fd125295110c3776089f6ef318f4b775f0abe4"},
{file = "ruff-0.4.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07fc80bbb61e42b3b23b10fda6a2a0f5a067f810180a3760c5ef1b456c21b9db"},
{file = "ruff-0.4.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:fa4dafe3fe66d90e2e2b63fa1591dd6e3f090ca2128daa0be33db894e6c18648"},
{file = "ruff-0.4.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7c0083febdec17571455903b184a10026603a1de078428ba155e7ce9358c5f6"},
{file = "ruff-0.4.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad1b20e66a44057c326168437d680a2166c177c939346b19c0d6b08a62a37589"},
{file = "ruff-0.4.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbf5d818553add7511c38b05532d94a407f499d1a76ebb0cad0374e32bc67202"},
{file = "ruff-0.4.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:50e9651578b629baec3d1513b2534de0ac7ed7753e1382272b8d609997e27e83"},
{file = "ruff-0.4.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8874a9df7766cb956b218a0a239e0a5d23d9e843e4da1e113ae1d27ee420877a"},
{file = "ruff-0.4.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b9de9a6e49f7d529decd09381c0860c3f82fa0b0ea00ea78409b785d2308a567"},
{file = "ruff-0.4.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:13a1768b0691619822ae6d446132dbdfd568b700ecd3652b20d4e8bc1e498f78"},
{file = "ruff-0.4.7-py3-none-win32.whl", hash = "sha256:769e5a51df61e07e887b81e6f039e7ed3573316ab7dd9f635c5afaa310e4030e"},
{file = "ruff-0.4.7-py3-none-win_amd64.whl", hash = "sha256:9e3ab684ad403a9ed1226894c32c3ab9c2e0718440f6f50c7c5829932bc9e054"},
{file = "ruff-0.4.7-py3-none-win_arm64.whl", hash = "sha256:10f2204b9a613988e3484194c2c9e96a22079206b22b787605c255f130db5ed7"},
{file = "ruff-0.4.7.tar.gz", hash = "sha256:2331d2b051dc77a289a653fcc6a42cce357087c5975738157cd966590b18b5e1"},
]
[[package]]
@@ -3572,4 +3632,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.10,<3.12"
content-hash = "db51ad1e631b569e106927683a13124252bd80974def1f2edbe23ac87d89c461"
content-hash = "a44e079d565fc1166458690ca2dc5826e198cc07ccab0ebaf71b5ab5e0eed150"
+2 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "machine-learning"
version = "1.106.4"
version = "1.105.1"
description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md"
@@ -23,6 +23,7 @@ orjson = ">=3.9.5"
gunicorn = ">=21.1.0"
huggingface-hub = ">=0.20.1,<1.0"
tokenizers = ">=0.15.0,<1.0"
numba = "^0.59.1"
[tool.poetry.group.dev.dependencies]
mypy = ">=1.3.0"
-17
View File
@@ -1,17 +0,0 @@
#! /usr/bin/env node
const { readFileSync, writeFileSync } = require('node:fs');
const nextVersion = process.argv[2];
if (!nextVersion) {
console.log('Usage: archive-version.js <version>');
process.exit(1);
}
const filename = './docs/static/archived-versions.json';
const oldVersions = JSON.parse(readFileSync(filename));
const newVersions = [
{ label: `v${nextVersion}`, url: `https://v${nextVersion}.archive.immich.app` },
...oldVersions,
];
writeFileSync(filename, JSON.stringify(newVersions, null, 2) + '\n');
+2 -6
View File
@@ -66,12 +66,10 @@ if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then
npm --prefix server run build
make open-api
npm --prefix open-api/typescript-sdk version "$SERVER_PUMP"
# TODO use $SERVER_PUMP once we pass 2.2.x
npm --prefix cli version patch
npm --prefix cli i --package-lock-only
npm --prefix web version "$SERVER_PUMP"
npm --prefix web i --package-lock-only
npm --prefix e2e version "$SERVER_PUMP"
npm --prefix web i --package-lock-only
npm --prefix cli i --package-lock-only
npm --prefix e2e i --package-lock-only
poetry --directory machine-learning version "$SERVER_PUMP"
fi
@@ -85,6 +83,4 @@ sed -i "s/version_number: \"$CURRENT_SERVER\"$/version_number: \"$NEXT_SERVER\"/
sed -i "s/\"android\.injected\.version\.code\" => $CURRENT_MOBILE,/\"android\.injected\.version\.code\" => $NEXT_MOBILE,/" mobile/android/fastlane/Fastfile
sed -i "s/^version: $CURRENT_SERVER+$CURRENT_MOBILE$/version: $NEXT_SERVER+$NEXT_MOBILE/" mobile/pubspec.yaml
./misc/release/archive-version.js "$NEXT_SERVER"
echo "IMMICH_VERSION=v$NEXT_SERVER" >>"$GITHUB_ENV"
+1 -1
View File
@@ -1,3 +1,3 @@
{
"flutter": "3.22.2"
"flutter": "3.22.1"
}
+2 -2
View File
@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 144,
"android.injected.version.name" => "1.106.4",
"android.injected.version.code" => 140,
"android.injected.version.name" => "1.105.1",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
+3 -3
View File
@@ -5,17 +5,17 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000381">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000374">
</testcase>
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="52.832426">
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="84.292464">
</testcase>
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="27.616558">
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="33.336934">
</testcase>
-2
View File
@@ -295,8 +295,6 @@
"memories_check_back_tomorrow": "Check back tomorrow for more memories",
"memories_start_over": "Start Over",
"memories_swipe_to_close": "Swipe up to close",
"memories_year_ago": "A year ago",
"memories_years_ago": "{} years ago",
"monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "Motion Photos",
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
-2
View File
@@ -295,8 +295,6 @@
"memories_check_back_tomorrow": "明日もう一度確認してください",
"memories_start_over": "始める",
"memories_swipe_to_close": "上にスワイプして閉じる",
"memories_year_ago": "過去1年間",
"memories_years_ago": "過去{}年間",
"monthly_title_text_date_format": "yyyy年 MM月",
"motion_photos_page_title": "モーションフォト",
"multiselect_grid_edit_date_time_err_read_only": "読み取り専用の項目の日付を変更できません",
-2
View File
@@ -295,8 +295,6 @@
"memories_check_back_tomorrow": "Kom morgen terug voor meer herinneringen",
"memories_start_over": "Opnieuw beginnen",
"memories_swipe_to_close": "Swipe omhoog om te sluiten",
"memories_year_ago": "1 jaar geleden",
"memories_years_ago": "{} jaar geleden",
"monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "Bewegende foto's",
"multiselect_grid_edit_date_time_err_read_only": "Kan datum van alleen-lezen asset(s) niet wijzigen, overslaan",
-2
View File
@@ -295,8 +295,6 @@
"memories_check_back_tomorrow": "明天再看",
"memories_start_over": "再看一次",
"memories_swipe_to_close": "上划关闭",
"memories_year_ago": "1年前",
"memories_years_ago": "{}年前",
"monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "动图",
"multiselect_grid_edit_date_time_err_read_only": "无法编辑只读项目的日期,跳过",
-2
View File
@@ -295,8 +295,6 @@
"memories_check_back_tomorrow": "明天再看",
"memories_start_over": "再看一次",
"memories_swipe_to_close": "上划关闭",
"memories_year_ago": "1年前",
"memories_years_ago": "{}年前",
"monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "动图",
"multiselect_grid_edit_date_time_err_read_only": "无法编辑只读项目的日期,跳过",
-2
View File
@@ -295,8 +295,6 @@
"memories_check_back_tomorrow": "Check back tomorrow for more memories",
"memories_start_over": "Start Over",
"memories_swipe_to_close": "Swipe up to close",
"memories_year_ago": "1年前",
"memories_years_ago": "{}年前",
"monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "Motion Photos",
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
+3 -3
View File
@@ -383,7 +383,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 160;
CURRENT_PROJECT_VERSION = 157;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -525,7 +525,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 160;
CURRENT_PROJECT_VERSION = 157;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -553,7 +553,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 160;
CURRENT_PROJECT_VERSION = 157;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
+2 -2
View File
@@ -58,11 +58,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.106.3</string>
<string>1.105.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>160</string>
<string>157</string>
<key>FLTEnableImpeller</key>
<true />
<key>ITSAppUsesNonExemptEncryption</key>
+1 -1
View File
@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta"
lane :beta do
increment_version_number(
version_number: "1.106.4"
version_number: "1.105.1"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,
+6 -6
View File
@@ -5,32 +5,32 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000491">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.020864">
</testcase>
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="39.414297">
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.917777">
</testcase>
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="32.521647">
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="5.283943">
</testcase>
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.511733">
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.944748">
</testcase>
<testcase classname="fastlane.lanes" name="4: build_app" time="202.628277">
<testcase classname="fastlane.lanes" name="4: build_app" time="215.971639">
</testcase>
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="73.861852">
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="76.674601">
</testcase>
@@ -1,42 +0,0 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:photo_manager/photo_manager.dart';
class BackupCandidate {
final String id;
final List<String> albumName;
final AssetEntity asset;
BackupCandidate({
required this.id,
required this.albumName,
required this.asset,
});
BackupCandidate copyWith({
String? id,
List<String>? albumName,
AssetEntity? asset,
}) {
return BackupCandidate(
id: id ?? this.id,
albumName: albumName ?? this.albumName,
asset: asset ?? this.asset,
);
}
@override
String toString() => 'BackupCandidate(albumName: $albumName, asset: $asset)';
@override
bool operator ==(covariant BackupCandidate other) {
if (identical(this, other)) return true;
return other.id == id &&
other.albumName == albumName &&
other.asset == asset;
}
@override
int get hashCode => id.hashCode ^ albumName.hashCode ^ asset.hashCode;
}
@@ -2,7 +2,7 @@
import 'package:cancellation_token_http/http.dart';
import 'package:collection/collection.dart';
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:immich_mobile/models/backup/available_album.model.dart';
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
@@ -41,7 +41,7 @@ class BackUpState {
final Set<AvailableAlbum> excludedBackupAlbums;
/// Assets that are not overlapping in selected backup albums and excluded backup albums
final Set<BackupCandidate> allUniqueAssets;
final Set<AssetEntity> allUniqueAssets;
/// All assets from the selected albums that have been backup
final Set<String> selectedAlbumsBackupAssetsIds;
@@ -94,7 +94,7 @@ class BackUpState {
List<AvailableAlbum>? availableAlbums,
Set<AvailableAlbum>? selectedBackupAlbums,
Set<AvailableAlbum>? excludedBackupAlbums,
Set<BackupCandidate>? allUniqueAssets,
Set<AssetEntity>? allUniqueAssets,
Set<String>? selectedAlbumsBackupAssetsIds,
CurrentUploadAsset? currentUploadAsset,
}) {
@@ -75,7 +75,9 @@ class VideoViewerPage extends HookConsumerWidget {
// Also sets the error if there is an error in the playback
void updateVideoPlayback() {
final videoPlayback = VideoPlaybackValue.fromController(controller);
ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback;
if (!loopVideo) {
ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback;
}
final state = videoPlayback.state;
// Enable the WakeLock while the video is playing
@@ -108,9 +110,7 @@ class VideoViewerPage extends HookConsumerWidget {
}
// Subscribes to listener
Future.microtask(() {
controller.addListener(updateVideoPlayback);
});
controller.addListener(updateVideoPlayback);
return () {
// Removes listener when we dispose
controller.removeListener(updateVideoPlayback);
+1 -1
View File
@@ -3,7 +3,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/build_context_extensions.dart';
import 'package:immich_mobile/widgets/forms/login/login_form.dart';
import 'package:immich_mobile/widgets/forms/login_form.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:package_info_plus/package_info_plus.dart';
+3 -1
View File
@@ -119,7 +119,9 @@ class PhotosPage extends HookConsumerWidget {
child: Container(
height: kToolbarHeight + MediaQuery.of(context).padding.top,
color: context.themeData.appBarTheme.backgroundColor,
child: const ImmichAppBar(),
child: const SafeArea(
child: ImmichAppBar(),
),
),
),
],
+1 -1
View File
@@ -298,7 +298,7 @@ class MapPage extends HookConsumerWidget {
),
Positioned(
right: 0,
bottom: MediaQuery.of(context).padding.bottom + 16,
bottom: 30,
child: ElevatedButton(
onPressed: onZoomToLocation,
style: ElevatedButton.styleFrom(
+120 -108
View File
@@ -13,7 +13,7 @@ import 'package:immich_mobile/providers/search/search_page_state.provider.dart';
import 'package:immich_mobile/widgets/search/curated_people_row.dart';
import 'package:immich_mobile/widgets/search/curated_places_row.dart';
import 'package:immich_mobile/widgets/search/person_name_edit_form.dart';
import 'package:immich_mobile/widgets/search/search_row_section.dart';
import 'package:immich_mobile/widgets/search/search_row_title.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
@@ -31,7 +31,7 @@ class SearchPage extends HookConsumerWidget {
final curatedPeople = ref.watch(getAllPeopleProvider);
final isMapEnabled =
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.map));
final double imageSize = math.min(context.width / 3, 150);
double imageSize = math.min(context.width / 3, 150);
TextStyle categoryTitleStyle = const TextStyle(
fontWeight: FontWeight.w500,
@@ -53,15 +53,16 @@ class SearchPage extends HookConsumerWidget {
}
buildPeople() {
return curatedPeople.widgetWhen(
onError: (error, stack) => const ScaffoldErrorBody(withIcon: false),
onData: (people) {
return SearchRowSection(
onViewAllPressed: () => context.pushRoute(const AllPeopleRoute()),
title: "search_page_people".tr(),
isEmpty: people.isEmpty,
return SizedBox(
height: imageSize,
child: curatedPeople.widgetWhen(
onError: (error, stack) => const ScaffoldErrorBody(withIcon: false),
onData: (people) => Padding(
padding: const EdgeInsets.only(
left: 16,
top: 8,
),
child: CuratedPeopleRow(
padding: const EdgeInsets.symmetric(horizontal: 16),
content: people
.map((e) => SearchCuratedContent(label: e.name, id: e.id))
.take(12)
@@ -78,46 +79,42 @@ class SearchPage extends HookConsumerWidget {
showNameEditModel(person.id, person.label),
},
),
);
},
),
),
);
}
buildPlaces() {
return places.widgetWhen(
onError: (error, stack) => const ScaffoldErrorBody(withIcon: false),
onData: (data) {
return SearchRowSection(
onViewAllPressed: () => context.pushRoute(const AllPlacesRoute()),
title: "search_page_places".tr(),
isEmpty: !isMapEnabled && data.isEmpty,
child: CuratedPlacesRow(
isMapEnabled: isMapEnabled,
content: data,
imageSize: imageSize,
onTap: (content, index) {
context.pushRoute(
SearchInputRoute(
prefilter: SearchFilter(
people: {},
location: SearchLocationFilter(
city: content.label,
),
camera: SearchCameraFilter(),
date: SearchDateFilter(),
display: SearchDisplayFilters(
isNotInAlbum: false,
isArchive: false,
isFavorite: false,
),
mediaType: AssetType.other,
return SizedBox(
height: imageSize,
child: places.widgetWhen(
onError: (error, stack) => const ScaffoldErrorBody(withIcon: false),
onData: (data) => CuratedPlacesRow(
isMapEnabled: isMapEnabled,
content: data,
imageSize: imageSize,
onTap: (content, index) {
context.pushRoute(
SearchInputRoute(
prefilter: SearchFilter(
people: {},
location: SearchLocationFilter(
city: content.label,
),
camera: SearchCameraFilter(),
date: SearchDateFilter(),
display: SearchDisplayFilters(
isNotInAlbum: false,
isArchive: false,
isFavorite: false,
),
mediaType: AssetType.other,
),
);
},
),
);
},
),
);
},
),
),
);
}
@@ -163,73 +160,88 @@ class SearchPage extends HookConsumerWidget {
return Scaffold(
appBar: const ImmichAppBar(),
body: ListView(
body: Stack(
children: [
buildSearchButton(),
const SizedBox(height: 8.0),
buildPeople(),
const SizedBox(height: 8.0),
buildPlaces(),
const SizedBox(height: 24.0),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'search_page_your_activity',
style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
ListView(
children: [
buildSearchButton(),
SearchRowTitle(
title: "search_page_people".tr(),
onViewAllPressed: () =>
context.pushRoute(const AllPeopleRoute()),
),
).tr(),
),
ListTile(
leading: Icon(
Icons.favorite_border_rounded,
color: categoryIconColor,
),
title:
Text('search_page_favorites', style: categoryTitleStyle).tr(),
onTap: () => context.pushRoute(const FavoritesRoute()),
),
const CategoryDivider(),
ListTile(
leading: Icon(
Icons.schedule_outlined,
color: categoryIconColor,
),
title: Text(
'search_page_recently_added',
style: categoryTitleStyle,
).tr(),
onTap: () => context.pushRoute(const RecentlyAddedRoute()),
),
const SizedBox(height: 24.0),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Text(
'search_page_categories',
style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
buildPeople(),
SearchRowTitle(
title: "search_page_places".tr(),
onViewAllPressed: () =>
context.pushRoute(const AllPlacesRoute()),
top: 0,
),
).tr(),
),
ListTile(
title: Text('search_page_videos', style: categoryTitleStyle).tr(),
leading: Icon(
Icons.play_circle_outline,
color: categoryIconColor,
),
onTap: () => context.pushRoute(const AllVideosRoute()),
),
const CategoryDivider(),
ListTile(
title: Text(
'search_page_motion_photos',
style: categoryTitleStyle,
).tr(),
leading: Icon(
Icons.motion_photos_on_outlined,
color: categoryIconColor,
),
onTap: () => context.pushRoute(const AllMotionPhotosRoute()),
const SizedBox(height: 10.0),
buildPlaces(),
const SizedBox(height: 24.0),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'search_page_your_activity',
style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
),
).tr(),
),
ListTile(
leading: Icon(
Icons.favorite_border_rounded,
color: categoryIconColor,
),
title: Text('search_page_favorites', style: categoryTitleStyle)
.tr(),
onTap: () => context.pushRoute(const FavoritesRoute()),
),
const CategoryDivider(),
ListTile(
leading: Icon(
Icons.schedule_outlined,
color: categoryIconColor,
),
title: Text(
'search_page_recently_added',
style: categoryTitleStyle,
).tr(),
onTap: () => context.pushRoute(const RecentlyAddedRoute()),
),
const SizedBox(height: 24.0),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Text(
'search_page_categories',
style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
),
).tr(),
),
ListTile(
title:
Text('search_page_videos', style: categoryTitleStyle).tr(),
leading: Icon(
Icons.play_circle_outline,
color: categoryIconColor,
),
onTap: () => context.pushRoute(const AllVideosRoute()),
),
const CategoryDivider(),
ListTile(
title: Text(
'search_page_motion_photos',
style: categoryTitleStyle,
).tr(),
leading: Icon(
Icons.motion_photos_on_outlined,
color: categoryIconColor,
),
onTap: () => context.pushRoute(const AllMotionPhotosRoute()),
),
],
),
],
),
@@ -0,0 +1,87 @@
import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/services/asset_description.service.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:isar/isar.dart';
class AssetDescriptionNotifier extends StateNotifier<String> {
final Isar _db;
final AssetDescriptionService _service;
final Asset _asset;
AssetDescriptionNotifier(
this._db,
this._service,
this._asset,
) : super('') {
_fetchLocalDescription();
_fetchRemoteDescription();
}
String get description => state;
/// Fetches the local database value for description
/// and writes it to [state]
void _fetchLocalDescription() async {
final localExifId = _asset.exifInfo?.id;
// Guard [localExifId] null
if (localExifId == null) {
return;
}
// Subscribe to local changes
final exifInfo = await _db.exifInfos.get(localExifId);
// Guard
if (exifInfo?.description == null) {
return;
}
state = exifInfo!.description!;
}
/// Fetches the remote value and sets the state
void _fetchRemoteDescription() async {
final remoteAssetId = _asset.remoteId;
final localExifId = _asset.exifInfo?.id;
// Guard [remoteAssetId] and [localExifId] null
if (remoteAssetId == null || localExifId == null) {
return;
}
// Reads the latest from the remote and writes it to DB in the service
final latest = await _service.readLatest(remoteAssetId, localExifId);
state = latest;
}
/// Sets the description to [description]
/// Uses the service to set the asset value
Future<void> setDescription(String description) async {
state = description;
final remoteAssetId = _asset.remoteId;
final localExifId = _asset.exifInfo?.id;
// Guard [remoteAssetId] and [localExifId] null
if (remoteAssetId == null || localExifId == null) {
return;
}
return _service.setDescription(description, remoteAssetId, localExifId);
}
}
final assetDescriptionProvider = StateNotifierProvider.autoDispose
.family<AssetDescriptionNotifier, String, Asset>(
(ref, asset) => AssetDescriptionNotifier(
ref.watch(dbProvider),
ref.watch(assetDescriptionServiceProvider),
asset,
),
);
@@ -31,9 +31,6 @@ Future<VideoPlayerController> videoPlayerController(
controller = VideoPlayerController.networkUrl(
url,
httpHeaders: {"x-immich-user-token": accessToken},
videoPlayerOptions: asset.livePhotoVideoId != null
? VideoPlayerOptions(mixWithOthers: true)
: VideoPlayerOptions(mixWithOthers: false),
);
}
@@ -7,7 +7,7 @@ part of 'video_player_controller_provider.dart';
// **************************************************************************
String _$videoPlayerControllerHash() =>
r'642469a44287188a7c301f5cad3df3e23c84d85c';
r'40b31f7b1a73fab84c311b0f06bedf5322143cd9';
/// Copied from Dart SDK
class _SystemHash {
@@ -93,18 +93,4 @@ class VideoPlayerControls extends StateNotifier<VideoPlaybackControls> {
pause: !state.pause,
);
}
void restart() {
state = VideoPlaybackControls(
position: 0,
mute: state.mute,
pause: true,
);
state = VideoPlaybackControls(
position: 0,
mute: state.mute,
pause: false,
);
}
}
@@ -6,7 +6,6 @@ import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/models/backup/available_album.model.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
import 'package:immich_mobile/models/backup/backup_state.model.dart';
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
@@ -290,12 +289,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
/// Those assets are unique and are used as the total assets
///
Future<void> _updateBackupAssetCount() async {
debugPrint("UPDATE BACKUP ASSET COUNTTT");
final duplicatedAssetIds = await _backupService.getDuplicatedAssetIds();
final Set<BackupCandidate> assetsFromSelectedAlbums = {};
final Set<BackupCandidate> assetsFromExcludedAlbums = {};
final Set<AssetEntity> assetsFromSelectedAlbums = {};
final Set<AssetEntity> assetsFromExcludedAlbums = {};
/// Extracing assets from selected albums
for (final album in state.selectedBackupAlbums) {
final assetCount = await album.albumEntity.assetCountAsync;
@@ -307,19 +304,9 @@ class BackupNotifier extends StateNotifier<BackUpState> {
start: 0,
end: assetCount,
);
final candidate = assets.map(
(e) => BackupCandidate(
id: e.id,
albumName: [album.albumEntity.name],
asset: e,
),
);
assetsFromSelectedAlbums.addAll(candidate.toSet());
assetsFromSelectedAlbums.addAll(assets);
}
/// Extracing assets from excluded albums
for (final album in state.excludedBackupAlbums) {
final assetCount = await album.albumEntity.assetCountAsync;
@@ -331,48 +318,29 @@ class BackupNotifier extends StateNotifier<BackUpState> {
start: 0,
end: assetCount,
);
final candidate = assets.map(
(e) => BackupCandidate(
id: e.id,
albumName: [album.albumEntity.name],
asset: e,
),
);
assetsFromExcludedAlbums.addAll(candidate);
assetsFromExcludedAlbums.addAll(assets);
}
Set<BackupCandidate> allUniqueAssets =
final Set<AssetEntity> allUniqueAssets =
assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums);
final allAssetsInDatabase = await _backupService.getDeviceBackupAsset();
if (allAssetsInDatabase == null) {
return;
}
// Find asset that were backup from selected albums
final Set<String> selectedAlbumsBackupAssets =
Set.from(allUniqueAssets.map((e) => e.asset.id));
Set.from(allUniqueAssets.map((e) => e.id));
selectedAlbumsBackupAssets
.removeWhere((assetId) => !allAssetsInDatabase.contains(assetId));
// Remove duplicated asset from all unique assets
allUniqueAssets.removeWhere(
(e) => duplicatedAssetIds.contains(e.asset.id),
(asset) => duplicatedAssetIds.contains(asset.id),
);
/// Merge different album name of the same id
allUniqueAssets = allUniqueAssets.map((e) {
final List<String> albumNames = allUniqueAssets
.where((a) => a.id == e.id)
.map((a) => a.albumName)
.expand((e) => e)
.toList();
return e.copyWith(albumName: albumNames);
}).toSet();
if (allUniqueAssets.isEmpty) {
log.info("No assets are selected for back up");
state = state.copyWith(
@@ -391,8 +359,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
// Save to persistent storage
await _updatePersistentAlbumsSelection();
debugPrint("backup asset ${allUniqueAssets.length}", wrapWidth: 80);
}
/// Get all necessary information for calculating the available albums,
@@ -539,7 +505,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
if (isDuplicated) {
state = state.copyWith(
allUniqueAssets: state.allUniqueAssets
.where((e) => e.asset.id != deviceAssetId)
.where((asset) => asset.id != deviceAssetId)
.toSet(),
);
} else {
@@ -556,7 +522,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
state.selectedAlbumsBackupAssetsIds.length ==
0) {
final latestAssetBackup =
state.allUniqueAssets.map((e) => e.asset.modifiedDateTime).reduce(
state.allUniqueAssets.map((e) => e.modifiedDateTime).reduce(
(v, e) => e.isAfter(v) ? e : v,
);
state = state.copyWith(
+1 -1
View File
@@ -6,7 +6,7 @@ part of 'map_state.provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$mapStateNotifierHash() => r'31fafe17aa85c48379a22ed3db3cc94af59ce5b8';
String _$mapStateNotifierHash() => r'87a8623f726d438d115d5a15609c71372726ee2f';
/// See also [MapStateNotifier].
@ProviderFor(MapStateNotifier)
+4 -5
View File
@@ -101,6 +101,7 @@ class AssetService {
const int chunkSize = 10000;
try {
final List<Asset> allAssets = [];
DateTime? lastCreationDate;
String? lastId;
// will break on error or once all assets are loaded
while (true) {
@@ -108,17 +109,15 @@ class AssetService {
limit: chunkSize,
updatedUntil: until,
lastId: lastId,
lastCreationDate: lastCreationDate,
userId: user.id,
);
log.fine("Requesting $chunkSize assets from $lastId");
final List<AssetResponseDto>? assets =
await _apiService.syncApi.getFullSyncForUser(dto);
if (assets == null) return null;
log.fine(
"Received ${assets.length} assets from ${assets.firstOrNull?.id} to ${assets.lastOrNull?.id}",
);
allAssets.addAll(assets.map(Asset.remote));
if (assets.length != chunkSize) break;
if (assets.isEmpty) break;
lastCreationDate = assets.last.fileCreatedAt;
lastId = assets.last.id;
}
return allAssets;
@@ -1,5 +1,4 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/db.provider.dart';
@@ -13,36 +12,46 @@ class AssetDescriptionService {
final Isar _db;
final ApiService _api;
Future<void> setDescription(
Asset asset,
String newDescription,
setDescription(
String description,
String remoteAssetId,
int localExifId,
) async {
final remoteAssetId = asset.remoteId;
final localExifId = asset.exifInfo?.id;
// Guard [remoteAssetId] and [localExifId] null
if (remoteAssetId == null || localExifId == null) {
return;
}
final result = await _api.assetsApi.updateAsset(
remoteAssetId,
UpdateAssetDto(description: newDescription),
UpdateAssetDto(description: description),
);
final description = result?.exifInfo?.description;
if (description != null) {
if (result?.exifInfo?.description != null) {
var exifInfo = await _db.exifInfos.get(localExifId);
if (exifInfo != null) {
exifInfo.description = description;
exifInfo.description = result!.exifInfo!.description;
await _db.writeTxn(
() => _db.exifInfos.put(exifInfo),
);
}
}
}
Future<String> readLatest(String assetRemoteId, int localExifId) async {
final latestAssetFromServer =
await _api.assetsApi.getAssetInfo(assetRemoteId);
final localExifInfo = await _db.exifInfos.get(localExifId);
if (latestAssetFromServer != null && localExifInfo != null) {
localExifInfo.description =
latestAssetFromServer.exifInfo?.description ?? '';
await _db.writeTxn(
() => _db.exifInfos.put(localExifInfo),
);
return localExifInfo.description!;
}
return "";
}
}
final assetDescriptionServiceProvider = Provider(
+3 -5
View File
@@ -1,4 +1,3 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/models/memories/memory.model.dart';
@@ -9,6 +8,8 @@ import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
import '../utils/string_helper.dart';
final memoryServiceProvider = StateProvider<MemoryService>((ref) {
return MemoryService(
ref.watch(apiServiceProvider),
@@ -41,12 +42,9 @@ class MemoryService {
final dbAssets =
await _db.assets.getAllByRemoteId(assets.map((e) => e.id));
if (dbAssets.isNotEmpty) {
final String title = yearsAgo <= 1
? 'memories_year_ago'.tr()
: 'memories_years_ago'.tr(args: [yearsAgo.toString()]);
memories.add(
Memory(
title: title,
title: '$yearsAgo year${s(yearsAgo)} ago',
assets: dbAssets,
),
);
@@ -1,17 +0,0 @@
String? getVersionCompatibilityMessage(
int appMajor,
int appMinor,
int serverMajor,
int serverMinor,
) {
if (serverMajor != appMajor) {
return 'Your app major version is not compatible with the server!';
}
// Add latest compat info up top
if (serverMinor < 106 && appMinor >= 106) {
return 'Your app minor version is not compatible with the server! Please update your server to version v1.106.0 or newer to login';
}
return null;
}
@@ -64,8 +64,6 @@ class CustomVideoPlayerControls extends HookConsumerWidget {
final state = ref.read(videoPlaybackValueProvider).state;
if (state == VideoPlaybackState.playing) {
ref.read(videoPlayerControlsProvider.notifier).pause();
} else if (state == VideoPlaybackState.completed) {
ref.read(videoPlayerControlsProvider.notifier).restart();
} else {
ref.read(videoPlayerControlsProvider.notifier).play();
}
@@ -2,11 +2,10 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_description.provider.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/services/asset_description.service.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:logging/logging.dart';
@@ -14,11 +13,9 @@ class DescriptionInput extends HookConsumerWidget {
DescriptionInput({
super.key,
required this.asset,
this.exifInfo,
});
final Asset asset;
final ExifInfo? exifInfo;
final Logger _log = Logger('DescriptionInput');
@override
@@ -28,25 +25,25 @@ class DescriptionInput extends HookConsumerWidget {
final focusNode = useFocusNode();
final isFocus = useState(false);
final isTextEmpty = useState(controller.text.isEmpty);
final descriptionProvider = ref.watch(assetDescriptionServiceProvider);
final descriptionProvider =
ref.watch(assetDescriptionProvider(asset).notifier);
final description = ref.watch(assetDescriptionProvider(asset));
final owner = ref.watch(currentUserProvider);
final hasError = useState(false);
useEffect(
() {
controller.text = exifInfo?.description ?? '';
isTextEmpty.value = exifInfo?.description?.isEmpty ?? true;
controller.text = description;
isTextEmpty.value = description.isEmpty;
return null;
},
[exifInfo?.description],
[description],
);
submitDescription(String description) async {
hasError.value = false;
try {
await descriptionProvider.setDescription(
asset,
description,
);
} catch (error, stack) {
@@ -88,7 +85,7 @@ class DescriptionInput extends HookConsumerWidget {
isFocus.value = false;
focusNode.unfocus();
if (exifInfo?.description != controller.text) {
if (description != controller.text) {
await submitDescription(controller.text);
}
},
@@ -73,8 +73,7 @@ class ExifBottomSheet extends HookConsumerWidget {
child: Column(
children: [
dateWidget,
if (asset.isRemote)
DescriptionInput(asset: asset, exifInfo: exifInfo),
if (asset.isRemote) DescriptionInput(asset: asset),
],
),
),
@@ -133,8 +132,7 @@ class ExifBottomSheet extends HookConsumerWidget {
child: Column(
children: [
dateWidget,
if (asset.isRemote)
DescriptionInput(asset: asset, exifInfo: exifInfo),
if (asset.isRemote) DescriptionInput(asset: asset),
Padding(
padding: EdgeInsets.only(top: asset.isRemote ? 0 : 16.0),
child: ExifLocation(
@@ -1,49 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
class EmailInput extends StatelessWidget {
final TextEditingController controller;
final FocusNode? focusNode;
final Function()? onSubmit;
const EmailInput({
super.key,
required this.controller,
this.focusNode,
this.onSubmit,
});
String? _validateInput(String? email) {
if (email == null || email == '') return null;
if (email.endsWith(' ')) return 'login_form_err_trailing_whitespace'.tr();
if (email.startsWith(' ')) return 'login_form_err_leading_whitespace'.tr();
if (email.contains(' ') || !email.contains('@')) {
return 'login_form_err_invalid_email'.tr();
}
return null;
}
@override
Widget build(BuildContext context) {
return TextFormField(
autofocus: true,
controller: controller,
decoration: InputDecoration(
labelText: 'login_form_label_email'.tr(),
border: const OutlineInputBorder(),
hintText: 'login_form_email_hint'.tr(),
hintStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 14,
),
),
validator: _validateInput,
autovalidateMode: AutovalidateMode.always,
autofillHints: const [AutofillHints.email],
keyboardType: TextInputType.emailAddress,
onFieldSubmitted: (_) => onSubmit?.call(),
focusNode: focusNode,
textInputAction: TextInputAction.next,
);
}
}
@@ -1,21 +0,0 @@
import 'package:flutter/material.dart';
class LoadingIcon extends StatelessWidget {
const LoadingIcon({super.key});
@override
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.only(top: 18.0),
child: SizedBox(
width: 24,
height: 24,
child: FittedBox(
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
),
);
}
}
@@ -1,27 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class LoginButton extends ConsumerWidget {
final Function() onPressed;
const LoginButton({
super.key,
required this.onPressed,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton.icon(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
),
onPressed: onPressed,
icon: const Icon(Icons.login_rounded),
label: const Text(
"login_form_button_text",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
).tr(),
);
}
}
@@ -1,34 +0,0 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
class OAuthLoginButton extends ConsumerWidget {
final TextEditingController serverEndpointController;
final ValueNotifier<bool> isLoading;
final String buttonLabel;
final Function() onPressed;
const OAuthLoginButton({
super.key,
required this.serverEndpointController,
required this.isLoading,
required this.buttonLabel,
required this.onPressed,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: context.primaryColor.withAlpha(230),
padding: const EdgeInsets.symmetric(vertical: 12),
),
onPressed: onPressed,
icon: const Icon(Icons.pin_rounded),
label: Text(
buttonLabel,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
);
}
}
@@ -1,49 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class PasswordInput extends HookConsumerWidget {
final TextEditingController controller;
final FocusNode? focusNode;
final Function()? onSubmit;
const PasswordInput({
super.key,
required this.controller,
this.focusNode,
this.onSubmit,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isPasswordVisible = useState<bool>(false);
return TextFormField(
obscureText: !isPasswordVisible.value,
controller: controller,
decoration: InputDecoration(
labelText: 'login_form_label_password'.tr(),
border: const OutlineInputBorder(),
hintText: 'login_form_password_hint'.tr(),
hintStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 14,
),
suffixIcon: IconButton(
onPressed: () => isPasswordVisible.value = !isPasswordVisible.value,
icon: Icon(
isPasswordVisible.value
? Icons.visibility_off_sharp
: Icons.visibility_sharp,
),
),
),
autofillHints: const [AutofillHints.password],
keyboardType: TextInputType.text,
onFieldSubmitted: (_) => onSubmit?.call(),
focusNode: focusNode,
textInputAction: TextInputAction.go,
);
}
}
@@ -1,54 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/utils/url_helper.dart';
class ServerEndpointInput extends StatelessWidget {
final TextEditingController controller;
final FocusNode focusNode;
final Function()? onSubmit;
const ServerEndpointInput({
super.key,
required this.controller,
required this.focusNode,
this.onSubmit,
});
String? _validateInput(String? url) {
if (url == null || url.isEmpty) return null;
final parsedUrl = Uri.tryParse(sanitizeUrl(url));
if (parsedUrl == null ||
!parsedUrl.isAbsolute ||
!parsedUrl.scheme.startsWith("http") ||
parsedUrl.host.isEmpty) {
return 'login_form_err_invalid_url'.tr();
}
return null;
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 16.0),
child: TextFormField(
controller: controller,
decoration: InputDecoration(
labelText: 'login_form_endpoint_url'.tr(),
border: const OutlineInputBorder(),
hintText: 'login_form_endpoint_hint'.tr(),
errorMaxLines: 4,
),
validator: _validateInput,
autovalidateMode: AutovalidateMode.always,
focusNode: focusNode,
autofillHints: const [AutofillHints.url],
keyboardType: TextInputType.url,
autocorrect: false,
onFieldSubmitted: (_) => onSubmit?.call(),
textInputAction: TextInputAction.go,
),
);
}
}
@@ -15,19 +15,11 @@ import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/providers/authentication.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/utils/version_compatibility.dart';
import 'package:immich_mobile/widgets/common/immich_logo.dart';
import 'package:immich_mobile/widgets/common/immich_title_text.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/utils/url_helper.dart';
import 'package:immich_mobile/widgets/forms/login/email_input.dart';
import 'package:immich_mobile/widgets/forms/login/loading_icon.dart';
import 'package:immich_mobile/widgets/forms/login/login_button.dart';
import 'package:immich_mobile/widgets/forms/login/o_auth_login_button.dart';
import 'package:immich_mobile/widgets/forms/login/password_input.dart';
import 'package:immich_mobile/widgets/forms/login/server_endpoint_input.dart';
import 'package:openapi/api.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:permission_handler/permission_handler.dart';
class LoginForm extends HookConsumerWidget {
@@ -53,31 +45,9 @@ class LoginForm extends HookConsumerWidget {
final logoAnimationController = useAnimationController(
duration: const Duration(seconds: 60),
)..repeat();
final serverInfo = ref.watch(serverInfoProvider);
final warningMessage = useState<String?>(null);
final ValueNotifier<String?> serverEndpoint = useState<String?>(null);
checkVersionMismatch() async {
try {
final packageInfo = await PackageInfo.fromPlatform();
final appVersion = packageInfo.version;
final appMajorVersion = int.parse(appVersion.split('.')[0]);
final appMinorVersion = int.parse(appVersion.split('.')[1]);
final serverMajorVersion = serverInfo.serverVersion.major;
final serverMinorVersion = serverInfo.serverVersion.minor;
warningMessage.value = getVersionCompatibilityMessage(
appMajorVersion,
appMinorVersion,
serverMajorVersion,
serverMinorVersion,
);
} catch (error) {
warningMessage.value = 'Error checking version compatibility';
}
}
/// Fetch the server login credential and enables oAuth login if necessary
/// Returns true if successful, false otherwise
Future<bool> getServerLoginCredential() async {
@@ -338,40 +308,11 @@ class LoginForm extends HookConsumerWidget {
);
}
buildVersionCompatWarning() {
checkVersionMismatch();
if (warningMessage.value == null) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color:
context.isDarkTheme ? Colors.red.shade700 : Colors.red.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color:
context.isDarkTheme ? Colors.red.shade900 : Colors.red[200]!,
),
),
child: Text(
warningMessage.value!,
textAlign: TextAlign.center,
),
),
);
}
buildLogin() {
return AutofillGroup(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
buildVersionCompatWarning(),
Text(
sanitizeUrl(serverEndpointController.text),
style: context.textTheme.displaySmall,
@@ -475,6 +416,7 @@ class LoginForm extends HookConsumerWidget {
),
],
),
const SizedBox(height: 18),
// Note: This used to have an AnimatedSwitcher, but was removed
// because of https://github.com/flutter/flutter/issues/120874
@@ -488,3 +430,218 @@ class LoginForm extends HookConsumerWidget {
);
}
}
class ServerEndpointInput extends StatelessWidget {
final TextEditingController controller;
final FocusNode focusNode;
final Function()? onSubmit;
const ServerEndpointInput({
super.key,
required this.controller,
required this.focusNode,
this.onSubmit,
});
String? _validateInput(String? url) {
if (url == null || url.isEmpty) return null;
final parsedUrl = Uri.tryParse(sanitizeUrl(url));
if (parsedUrl == null ||
!parsedUrl.isAbsolute ||
!parsedUrl.scheme.startsWith("http") ||
parsedUrl.host.isEmpty) {
return 'login_form_err_invalid_url'.tr();
}
return null;
}
@override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
decoration: InputDecoration(
labelText: 'login_form_endpoint_url'.tr(),
border: const OutlineInputBorder(),
hintText: 'login_form_endpoint_hint'.tr(),
errorMaxLines: 4,
),
validator: _validateInput,
autovalidateMode: AutovalidateMode.always,
focusNode: focusNode,
autofillHints: const [AutofillHints.url],
keyboardType: TextInputType.url,
autocorrect: false,
onFieldSubmitted: (_) => onSubmit?.call(),
textInputAction: TextInputAction.go,
);
}
}
class EmailInput extends StatelessWidget {
final TextEditingController controller;
final FocusNode? focusNode;
final Function()? onSubmit;
const EmailInput({
super.key,
required this.controller,
this.focusNode,
this.onSubmit,
});
String? _validateInput(String? email) {
if (email == null || email == '') return null;
if (email.endsWith(' ')) return 'login_form_err_trailing_whitespace'.tr();
if (email.startsWith(' ')) return 'login_form_err_leading_whitespace'.tr();
if (email.contains(' ') || !email.contains('@')) {
return 'login_form_err_invalid_email'.tr();
}
return null;
}
@override
Widget build(BuildContext context) {
return TextFormField(
autofocus: true,
controller: controller,
decoration: InputDecoration(
labelText: 'login_form_label_email'.tr(),
border: const OutlineInputBorder(),
hintText: 'login_form_email_hint'.tr(),
hintStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 14,
),
),
validator: _validateInput,
autovalidateMode: AutovalidateMode.always,
autofillHints: const [AutofillHints.email],
keyboardType: TextInputType.emailAddress,
onFieldSubmitted: (_) => onSubmit?.call(),
focusNode: focusNode,
textInputAction: TextInputAction.next,
);
}
}
class PasswordInput extends HookConsumerWidget {
final TextEditingController controller;
final FocusNode? focusNode;
final Function()? onSubmit;
const PasswordInput({
super.key,
required this.controller,
this.focusNode,
this.onSubmit,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isPasswordVisible = useState<bool>(false);
return TextFormField(
obscureText: !isPasswordVisible.value,
controller: controller,
decoration: InputDecoration(
labelText: 'login_form_label_password'.tr(),
border: const OutlineInputBorder(),
hintText: 'login_form_password_hint'.tr(),
hintStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 14,
),
suffixIcon: IconButton(
onPressed: () => isPasswordVisible.value = !isPasswordVisible.value,
icon: Icon(
isPasswordVisible.value
? Icons.visibility_off_sharp
: Icons.visibility_sharp,
),
),
),
autofillHints: const [AutofillHints.password],
keyboardType: TextInputType.text,
onFieldSubmitted: (_) => onSubmit?.call(),
focusNode: focusNode,
textInputAction: TextInputAction.go,
);
}
}
class LoginButton extends ConsumerWidget {
final Function() onPressed;
const LoginButton({
super.key,
required this.onPressed,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton.icon(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
),
onPressed: onPressed,
icon: const Icon(Icons.login_rounded),
label: const Text(
"login_form_button_text",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
).tr(),
);
}
}
class OAuthLoginButton extends ConsumerWidget {
final TextEditingController serverEndpointController;
final ValueNotifier<bool> isLoading;
final String buttonLabel;
final Function() onPressed;
const OAuthLoginButton({
super.key,
required this.serverEndpointController,
required this.isLoading,
required this.buttonLabel,
required this.onPressed,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: context.primaryColor.withAlpha(230),
padding: const EdgeInsets.symmetric(vertical: 12),
),
onPressed: onPressed,
icon: const Icon(Icons.pin_rounded),
label: Text(
buttonLabel,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
);
}
}
class LoadingIcon extends StatelessWidget {
const LoadingIcon({super.key});
@override
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.only(top: 18.0),
child: SizedBox(
width: 24,
height: 24,
child: FittedBox(
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
),
);
}
}

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