Compare commits

..

1 Commits

Author SHA1 Message Date
Alex Tran e04d25d8f5 chore(mobile): better second to first assets shown 2024-06-12 16:58:58 -05:00
242 changed files with 7079 additions and 13712 deletions
+2 -2
View File
@@ -1,11 +1,11 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: I have a question or need support - 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. about: We use GitHub for tracking bugs, please check out our Discord channel for freaky fast support.
- name: Feature Request - name: Feature Request
url: https://github.com/immich-app/immich/discussions/new?category=feature-request url: https://github.com/immich-app/immich/discussions/new?category=feature-request
about: Please use our GitHub Discussion for making feature requests. about: Please use our GitHub Discussion for making feature requests.
- name: I'm unsure where to go - 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! about: If you are unsure where to go, then joining our Discord is recommended; Just ask!
+1 -1
View File
@@ -88,7 +88,7 @@ jobs:
type=raw,value=latest,enable=${{ github.event_name == 'release' }} type=raw,value=latest,enable=${{ github.event_name == 'release' }}
- name: Build and push image - name: Build and push image
uses: docker/build-push-action@v6.0.0 uses: docker/build-push-action@v5.4.0
with: with:
file: cli/Dockerfile file: cli/Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
+1 -1
View File
@@ -115,7 +115,7 @@ jobs:
fi fi
- name: Build and push image - name: Build and push image
uses: docker/build-push-action@v6.0.0 uses: docker/build-push-action@v5.4.0
with: with:
context: ${{ matrix.context }} context: ${{ matrix.context }}
file: ${{ matrix.file }} file: ${{ matrix.file }}
-45
View File
@@ -35,48 +35,3 @@ sql:
attach-server: attach-server:
docker exec -it docker_immich-server_1 sh 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"> <p align="center">
<br/> <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://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"/> <img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" alt="Discord"/>
</a> </a>
<br/> <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 WORKDIR /usr/src/open-api/typescript-sdk
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
+58 -60
View File
@@ -1,12 +1,12 @@
{ {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.4", "version": "2.2.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.4", "version": "2.2.3",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
@@ -47,14 +47,14 @@
}, },
"../open-api/typescript-sdk": { "../open-api/typescript-sdk": {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.106.4", "version": "1.106.3",
"dev": true, "dev": true,
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
"@oazapfts/runtime": "^1.0.2" "@oazapfts/runtime": "^1.0.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.14.2", "@types/node": "^20.11.0",
"typescript": "^5.3.3" "typescript": "^5.3.3"
} }
}, },
@@ -1138,9 +1138,9 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.14.2", "version": "20.12.13",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.13.tgz",
"integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", "integrity": "sha512-gBGeanV41c1L171rR7wjbMiEpEI/l5XFQdLLfhr/REwpgDy/4U8y89+i8kRiLzDyZdOkXh+cRaTetUnCYutoXA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1154,17 +1154,17 @@
"dev": true "dev": true
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.12.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.12.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.11.0.tgz",
"integrity": "sha512-7F91fcbuDf/d3S8o21+r3ZncGIke/+eWk0EpO21LXhDfLahriZF9CGj4fbAetEjlaBdjdSm9a6VeXbpbT6Z40Q==", "integrity": "sha512-P+qEahbgeHW4JQ/87FuItjBj8O3MYv5gELDzr8QaQ7fsll1gSMTYb6j87MYyxwf3DtD7uGFB9ShwgmCJB5KmaQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.10.0", "@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.12.0", "@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/type-utils": "7.12.0", "@typescript-eslint/type-utils": "7.11.0",
"@typescript-eslint/utils": "7.12.0", "@typescript-eslint/utils": "7.11.0",
"@typescript-eslint/visitor-keys": "7.12.0", "@typescript-eslint/visitor-keys": "7.11.0",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^5.3.1", "ignore": "^5.3.1",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
@@ -1188,16 +1188,16 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "7.12.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.12.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.11.0.tgz",
"integrity": "sha512-dm/J2UDY3oV3TKius2OUZIFHsomQmpHtsV0FTh1WO8EKgHLQ1QCADUqscPgTpU+ih1e21FQSRjXckHn3txn6kQ==", "integrity": "sha512-yimw99teuaXVWsBcPO1Ais02kwJ1jmNA1KxE7ng0aT7ndr1pT1wqj0OJnsYVGKKlc4QJai86l/025L6z8CljOg==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "7.12.0", "@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/types": "7.12.0", "@typescript-eslint/types": "7.11.0",
"@typescript-eslint/typescript-estree": "7.12.0", "@typescript-eslint/typescript-estree": "7.11.0",
"@typescript-eslint/visitor-keys": "7.12.0", "@typescript-eslint/visitor-keys": "7.11.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -1217,14 +1217,14 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "7.12.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.12.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz",
"integrity": "sha512-itF1pTnN6F3unPak+kutH9raIkL3lhH1YRPGgt7QQOh43DQKVJXmWkpb+vpc/TiDHs6RSd9CTbDsc/Y+Ygq7kg==", "integrity": "sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.12.0", "@typescript-eslint/types": "7.11.0",
"@typescript-eslint/visitor-keys": "7.12.0" "@typescript-eslint/visitor-keys": "7.11.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || >=20.0.0" "node": "^18.18.0 || >=20.0.0"
@@ -1235,14 +1235,14 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "7.12.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.12.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.11.0.tgz",
"integrity": "sha512-lib96tyRtMhLxwauDWUp/uW3FMhLA6D0rJ8T7HmH7x23Gk1Gwwu8UZ94NMXBvOELn6flSPiBrCKlehkiXyaqwA==", "integrity": "sha512-WmppUEgYy+y1NTseNMJ6mCFxt03/7jTOy08bcg7bxJJdsM4nuhnchyBbE8vryveaJUf62noH7LodPSo5Z0WUCg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/typescript-estree": "7.12.0", "@typescript-eslint/typescript-estree": "7.11.0",
"@typescript-eslint/utils": "7.12.0", "@typescript-eslint/utils": "7.11.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^1.3.0" "ts-api-utils": "^1.3.0"
}, },
@@ -1263,9 +1263,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "7.12.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.12.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz",
"integrity": "sha512-o+0Te6eWp2ppKY3mLCU+YA9pVJxhUJE15FV7kxuD9jgwIAa+w/ycGJBMrYDTpVGUM/tgpa9SeMOugSabWFq7bg==", "integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -1277,14 +1277,14 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "7.12.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.12.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz",
"integrity": "sha512-5bwqLsWBULv1h6pn7cMW5dXX/Y2amRqLaKqsASVwbBHMZSnHqE/HN4vT4fE0aFsiwxYvr98kqOWh1a8ZKXalCQ==", "integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.12.0", "@typescript-eslint/types": "7.11.0",
"@typescript-eslint/visitor-keys": "7.12.0", "@typescript-eslint/visitor-keys": "7.11.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"globby": "^11.1.0", "globby": "^11.1.0",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@@ -1306,16 +1306,16 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "7.12.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.12.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.11.0.tgz",
"integrity": "sha512-Y6hhwxwDx41HNpjuYswYp6gDbkiZ8Hin9Bf5aJQn1bpTs3afYY4GX+MPYxma8jtoIV2GRwTM/UJm/2uGCVv+DQ==", "integrity": "sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.4.0", "@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "7.12.0", "@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/types": "7.12.0", "@typescript-eslint/types": "7.11.0",
"@typescript-eslint/typescript-estree": "7.12.0" "@typescript-eslint/typescript-estree": "7.11.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || >=20.0.0" "node": "^18.18.0 || >=20.0.0"
@@ -1329,13 +1329,13 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "7.12.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.12.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz",
"integrity": "sha512-uZk7DevrQLL3vSnfFl5bj4sL75qC9D6EdjemIdbtkuUmIheWpuiiylSY01JxJE7+zGrOWDZrp1WxOuDntvKrHQ==", "integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.12.0", "@typescript-eslint/types": "7.11.0",
"eslint-visitor-keys": "^3.4.3" "eslint-visitor-keys": "^3.4.3"
}, },
"engines": { "engines": {
@@ -3411,11 +3411,10 @@
} }
}, },
"node_modules/prettier": { "node_modules/prettier": {
"version": "3.3.1", "version": "3.2.5",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.1.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
"integrity": "sha512-7CAwy5dRsxs8PHXT3twixW9/OEll8MLE0VRPCJyl7CkS6VHGPSlsVaWTiASPTyGyYRyApxlaWTzwUxVNrhcwDg==", "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
"dev": true, "dev": true,
"license": "MIT",
"bin": { "bin": {
"prettier": "bin/prettier.cjs" "prettier": "bin/prettier.cjs"
}, },
@@ -4282,9 +4281,9 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "5.2.13", "version": "5.2.12",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.13.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.12.tgz",
"integrity": "sha512-SSq1noJfY9pR3I1TUENL3rQYDQCFqgD+lM6fTRAM8Nv6Lsg5hDLaXkjETVeBt+7vZBCMoibD+6IWnT2mJ+Zb/A==", "integrity": "sha512-/gC8GxzxMK5ntBwb48pR32GGhENnjtY30G4A0jemunsBkiEZFw60s8InGpN8gkhHEkjnRK1aSAxeQgwvFhUHAA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -4481,11 +4480,10 @@
"dev": true "dev": true
}, },
"node_modules/yaml": { "node_modules/yaml": {
"version": "2.4.5", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz",
"integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==", "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==",
"dev": true, "dev": true,
"license": "ISC",
"bin": { "bin": {
"yaml": "bin.mjs" "yaml": "bin.mjs"
}, },
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.4", "version": "2.2.3",
"description": "Command Line Interface (CLI) for Immich", "description": "Command Line Interface (CLI) for Immich",
"type": "module", "type": "module",
"exports": "./dist/index.js", "exports": "./dist/index.js",
+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 - Bitcoin: 3QVAb9dCHutquVejeNXitPqZX26Yg5kxb7
- Give a project a star - the contributors love gazing at the stars and seeing their creations shining in the sky. - 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! 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 - Bitcoin: 3QVAb9dCHutquVejeNXitPqZX26Yg5kxb7
- Give a project a star - the contributors love gazing at the stars and seeing their creations shining in the sky. - 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! 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. 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 ## Albums
@@ -408,11 +442,4 @@ docker exec -it immich_postgres psql --dbname=immich --username=<DB_USERNAME> --
</details> </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 [huggingface]: https://huggingface.co/immich-app
+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 + 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 ```diff
services: services:
+1 -1
View File
@@ -1,7 +1,7 @@
# Troubleshooting # Troubleshooting
:::tip :::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 ## Known Issues
+2 -4
View File
@@ -27,7 +27,7 @@ For more information about setting up the community image see [here](https://git
:::info :::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/) - 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 - 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)_ - 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**" 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 > <details >
<summary>Using an existing Postgres container? Click me! Otherwise proceed to step 5.</summary> <summary>Using an existing Postgres container? Click me! Otherwise proceed to step 5.</summary>
<ul> <ul>
@@ -71,7 +70,6 @@ alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich"
/> />
</ul> </ul>
</details> </details>
5. Click "**Save Changes**", you will be promoted to edit stack UI labels, just leave this blank and click "**Ok**" 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**" 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: 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-issues]: https://github.com/immich-app/immich/issues
[github-releases]: https://github.com/immich-app/immich/releases [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 # Quick Start
Here is a quick, no-choices path to install Immich and take it for a test drive. 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. to install and use it.
## Requirements ## Requirements
Check the [requirements page](/docs/install/requirements) to get started. 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 Follow the [Docker Compose (Recommended)](/docs/install/docker-compose) instructions
to install the server. to install the server.
- Where random passwords are required, `pwgen` is a handy utility. - Where random passwords are required, `pwgen` is a handy utility.
- `UPLOAD_LOCATION` should be set to some new directory on the server - `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". - You may ignore "Step 4 - Upgrading".
## Try the Web UI ## 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. 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'; import MobileAppBackup from '/docs/partials/_mobile-app-backup.md';
<MobileAppBackup /> <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. 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" /> <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 You may want to back up the content of your Immich instance
along with other parts of your server; be sure to read about along with other parts of your server; be sure to read about
[database backup](/docs/administration/backup-and-restore). [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; You may decide you'd like to install the server a different way;
the Install category on the left menu provides many options. the Install category on the left menu provides many options.
+2 -2
View File
@@ -124,7 +124,7 @@ const config = {
position: 'right', position: 'right',
}, },
{ {
href: 'https://discord.immich.app', href: 'https://discord.gg/D8JsnBEuKb',
label: 'Discord', label: 'Discord',
position: 'right', position: 'right',
}, },
@@ -151,7 +151,7 @@ const config = {
items: [ items: [
{ {
label: 'Discord', label: 'Discord',
href: 'https://discord.immich.app', href: 'https://discord.com/invite/D8JsnBEuKb',
}, },
{ {
label: 'Reddit', label: 'Reddit',
+164 -192
View File
@@ -2155,10 +2155,9 @@
} }
}, },
"node_modules/@docusaurus/core": { "node_modules/@docusaurus/core": {
"version": "3.4.0", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.4.0.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.3.2.tgz",
"integrity": "sha512-g+0wwmN2UJsBqy2fQRQ6fhXruoEa62JDeEa5d8IdTJlMoaDaEDfHh7WjwGRn4opuTQWpjAwP/fbcgyHKlE+64w==", "integrity": "sha512-PzKMydKI3IU1LmeZQDi+ut5RSuilbXnA8QdowGeJEgU8EJjmx3rBHNT1LxQxOVqNEwpWi/csLwd9bn7rUjggPA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/core": "^7.23.3", "@babel/core": "^7.23.3",
"@babel/generator": "^7.23.3", "@babel/generator": "^7.23.3",
@@ -2170,12 +2169,12 @@
"@babel/runtime": "^7.22.6", "@babel/runtime": "^7.22.6",
"@babel/runtime-corejs3": "^7.22.6", "@babel/runtime-corejs3": "^7.22.6",
"@babel/traverse": "^7.22.8", "@babel/traverse": "^7.22.8",
"@docusaurus/cssnano-preset": "3.4.0", "@docusaurus/cssnano-preset": "3.3.2",
"@docusaurus/logger": "3.4.0", "@docusaurus/logger": "3.3.2",
"@docusaurus/mdx-loader": "3.4.0", "@docusaurus/mdx-loader": "3.3.2",
"@docusaurus/utils": "3.4.0", "@docusaurus/utils": "3.3.2",
"@docusaurus/utils-common": "3.4.0", "@docusaurus/utils-common": "3.3.2",
"@docusaurus/utils-validation": "3.4.0", "@docusaurus/utils-validation": "3.3.2",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"babel-loader": "^9.1.3", "babel-loader": "^9.1.3",
"babel-plugin-dynamic-import-node": "^2.3.3", "babel-plugin-dynamic-import-node": "^2.3.3",
@@ -2241,10 +2240,9 @@
} }
}, },
"node_modules/@docusaurus/cssnano-preset": { "node_modules/@docusaurus/cssnano-preset": {
"version": "3.4.0", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.4.0.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.3.2.tgz",
"integrity": "sha512-qwLFSz6v/pZHy/UP32IrprmH5ORce86BGtN0eBtG75PpzQJAzp9gefspox+s8IEOr0oZKuQ/nhzZ3xwyc3jYJQ==", "integrity": "sha512-+5+epLk/Rp4vFML4zmyTATNc3Is+buMAL6dNjrMWahdJCJlMWMPd/8YfU+2PA57t8mlSbhLJ7vAZVy54cd1vRQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"cssnano-preset-advanced": "^6.1.2", "cssnano-preset-advanced": "^6.1.2",
"postcss": "^8.4.38", "postcss": "^8.4.38",
@@ -2256,10 +2254,9 @@
} }
}, },
"node_modules/@docusaurus/logger": { "node_modules/@docusaurus/logger": {
"version": "3.4.0", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.4.0.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.3.2.tgz",
"integrity": "sha512-bZwkX+9SJ8lB9kVRkXw+xvHYSMGG4bpYHKGXeXFvyVc79NMeeBSGgzd4TQLHH+DYeOJoCdl8flrFJVxlZ0wo/Q==", "integrity": "sha512-Ldu38GJ4P8g4guN7d7pyCOJ7qQugG7RVyaxrK8OnxuTlaImvQw33aDRwaX2eNmX8YK6v+//Z502F4sOZbHHCHQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"chalk": "^4.1.2", "chalk": "^4.1.2",
"tslib": "^2.6.0" "tslib": "^2.6.0"
@@ -2269,14 +2266,13 @@
} }
}, },
"node_modules/@docusaurus/mdx-loader": { "node_modules/@docusaurus/mdx-loader": {
"version": "3.4.0", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.4.0.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.3.2.tgz",
"integrity": "sha512-kSSbrrk4nTjf4d+wtBA9H+FGauf2gCax89kV8SUSJu3qaTdSIKdWERlngsiHaCFgZ7laTJ8a67UFf+xlFPtuTw==", "integrity": "sha512-AFRxj/aOk3/mfYDPxE3wTbrjeayVRvNSZP7mgMuUlrb2UlPRbSVAFX1k2RbgAJrnTSwMgb92m2BhJgYRfptN3g==",
"license": "MIT",
"dependencies": { "dependencies": {
"@docusaurus/logger": "3.4.0", "@docusaurus/logger": "3.3.2",
"@docusaurus/utils": "3.4.0", "@docusaurus/utils": "3.3.2",
"@docusaurus/utils-validation": "3.4.0", "@docusaurus/utils-validation": "3.3.2",
"@mdx-js/mdx": "^3.0.0", "@mdx-js/mdx": "^3.0.0",
"@slorber/remark-comment": "^1.0.0", "@slorber/remark-comment": "^1.0.0",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
@@ -2308,12 +2304,11 @@
} }
}, },
"node_modules/@docusaurus/module-type-aliases": { "node_modules/@docusaurus/module-type-aliases": {
"version": "3.4.0", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.4.0.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.3.2.tgz",
"integrity": "sha512-A1AyS8WF5Bkjnb8s+guTDuYmUiwJzNrtchebBHpc0gz0PyHJNMaybUlSrmJjHVcGrya0LKI4YcR3lBDQfXRYLw==", "integrity": "sha512-b/XB0TBJah5yKb4LYuJT4buFvL0MGAb0+vJDrJtlYMguRtsEBkf2nWl5xP7h4Dlw6ol0hsHrCYzJ50kNIOEclw==",
"license": "MIT",
"dependencies": { "dependencies": {
"@docusaurus/types": "3.4.0", "@docusaurus/types": "3.3.2",
"@types/history": "^4.7.11", "@types/history": "^4.7.11",
"@types/react": "*", "@types/react": "*",
"@types/react-router-config": "*", "@types/react-router-config": "*",
@@ -2327,18 +2322,17 @@
} }
}, },
"node_modules/@docusaurus/plugin-content-blog": { "node_modules/@docusaurus/plugin-content-blog": {
"version": "3.4.0", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.4.0.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.3.2.tgz",
"integrity": "sha512-vv6ZAj78ibR5Jh7XBUT4ndIjmlAxkijM3Sx5MAAzC1gyv0vupDQNhzuFg1USQmQVj3P5I6bquk12etPV3LJ+Xw==", "integrity": "sha512-fJU+dmqp231LnwDJv+BHVWft8pcUS2xVPZdeYH6/ibH1s2wQ/sLcmUrGWyIv/Gq9Ptj8XWjRPMghlxghuPPoxg==",
"license": "MIT",
"dependencies": { "dependencies": {
"@docusaurus/core": "3.4.0", "@docusaurus/core": "3.3.2",
"@docusaurus/logger": "3.4.0", "@docusaurus/logger": "3.3.2",
"@docusaurus/mdx-loader": "3.4.0", "@docusaurus/mdx-loader": "3.3.2",
"@docusaurus/types": "3.4.0", "@docusaurus/types": "3.3.2",
"@docusaurus/utils": "3.4.0", "@docusaurus/utils": "3.3.2",
"@docusaurus/utils-common": "3.4.0", "@docusaurus/utils-common": "3.3.2",
"@docusaurus/utils-validation": "3.4.0", "@docusaurus/utils-validation": "3.3.2",
"cheerio": "^1.0.0-rc.12", "cheerio": "^1.0.0-rc.12",
"feed": "^4.2.2", "feed": "^4.2.2",
"fs-extra": "^11.1.1", "fs-extra": "^11.1.1",
@@ -2359,19 +2353,18 @@
} }
}, },
"node_modules/@docusaurus/plugin-content-docs": { "node_modules/@docusaurus/plugin-content-docs": {
"version": "3.4.0", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.4.0.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.3.2.tgz",
"integrity": "sha512-HkUCZffhBo7ocYheD9oZvMcDloRnGhBMOZRyVcAQRFmZPmNqSyISlXA1tQCIxW+r478fty97XXAGjNYzBjpCsg==", "integrity": "sha512-Dm1ri2VlGATTN3VGk1ZRqdRXWa1UlFubjaEL6JaxaK7IIFqN/Esjpl+Xw10R33loHcRww/H76VdEeYayaL76eg==",
"license": "MIT",
"dependencies": { "dependencies": {
"@docusaurus/core": "3.4.0", "@docusaurus/core": "3.3.2",
"@docusaurus/logger": "3.4.0", "@docusaurus/logger": "3.3.2",
"@docusaurus/mdx-loader": "3.4.0", "@docusaurus/mdx-loader": "3.3.2",
"@docusaurus/module-type-aliases": "3.4.0", "@docusaurus/module-type-aliases": "3.3.2",
"@docusaurus/types": "3.4.0", "@docusaurus/types": "3.3.2",
"@docusaurus/utils": "3.4.0", "@docusaurus/utils": "3.3.2",
"@docusaurus/utils-common": "3.4.0", "@docusaurus/utils-common": "3.3.2",
"@docusaurus/utils-validation": "3.4.0", "@docusaurus/utils-validation": "3.3.2",
"@types/react-router-config": "^5.0.7", "@types/react-router-config": "^5.0.7",
"combine-promises": "^1.1.0", "combine-promises": "^1.1.0",
"fs-extra": "^11.1.1", "fs-extra": "^11.1.1",
@@ -2390,16 +2383,15 @@
} }
}, },
"node_modules/@docusaurus/plugin-content-pages": { "node_modules/@docusaurus/plugin-content-pages": {
"version": "3.4.0", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.4.0.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.3.2.tgz",
"integrity": "sha512-h2+VN/0JjpR8fIkDEAoadNjfR3oLzB+v1qSXbIAKjQ46JAHx3X22n9nqS+BWSQnTnp1AjkjSvZyJMekmcwxzxg==", "integrity": "sha512-EKc9fQn5H2+OcGER8x1aR+7URtAGWySUgULfqE/M14+rIisdrBstuEZ4lUPDRrSIexOVClML82h2fDS+GSb8Ew==",
"license": "MIT",
"dependencies": { "dependencies": {
"@docusaurus/core": "3.4.0", "@docusaurus/core": "3.3.2",
"@docusaurus/mdx-loader": "3.4.0", "@docusaurus/mdx-loader": "3.3.2",
"@docusaurus/types": "3.4.0", "@docusaurus/types": "3.3.2",
"@docusaurus/utils": "3.4.0", "@docusaurus/utils": "3.3.2",
"@docusaurus/utils-validation": "3.4.0", "@docusaurus/utils-validation": "3.3.2",
"fs-extra": "^11.1.1", "fs-extra": "^11.1.1",
"tslib": "^2.6.0", "tslib": "^2.6.0",
"webpack": "^5.88.1" "webpack": "^5.88.1"
@@ -2413,14 +2405,13 @@
} }
}, },
"node_modules/@docusaurus/plugin-debug": { "node_modules/@docusaurus/plugin-debug": {
"version": "3.4.0", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.4.0.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.3.2.tgz",
"integrity": "sha512-uV7FDUNXGyDSD3PwUaf5YijX91T5/H9SX4ErEcshzwgzWwBtK37nUWPU3ZLJfeTavX3fycTOqk9TglpOLaWkCg==", "integrity": "sha512-oBIBmwtaB+YS0XlmZ3gCO+cMbsGvIYuAKkAopoCh0arVjtlyPbejzPrHuCoRHB9G7abjNZw7zoONOR8+8LM5+Q==",
"license": "MIT",
"dependencies": { "dependencies": {
"@docusaurus/core": "3.4.0", "@docusaurus/core": "3.3.2",
"@docusaurus/types": "3.4.0", "@docusaurus/types": "3.3.2",
"@docusaurus/utils": "3.4.0", "@docusaurus/utils": "3.3.2",
"fs-extra": "^11.1.1", "fs-extra": "^11.1.1",
"react-json-view-lite": "^1.2.0", "react-json-view-lite": "^1.2.0",
"tslib": "^2.6.0" "tslib": "^2.6.0"
@@ -2434,14 +2425,13 @@
} }
}, },
"node_modules/@docusaurus/plugin-google-analytics": { "node_modules/@docusaurus/plugin-google-analytics": {
"version": "3.4.0", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.4.0.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.3.2.tgz",
"integrity": "sha512-mCArluxEGi3cmYHqsgpGGt3IyLCrFBxPsxNZ56Mpur0xSlInnIHoeLDH7FvVVcPJRPSQ9/MfRqLsainRw+BojA==", "integrity": "sha512-jXhrEIhYPSClMBK6/IA8qf1/FBoxqGXZvg7EuBax9HaK9+kL3L0TJIlatd8jQJOMtds8mKw806TOCc3rtEad1A==",
"license": "MIT",
"dependencies": { "dependencies": {
"@docusaurus/core": "3.4.0", "@docusaurus/core": "3.3.2",
"@docusaurus/types": "3.4.0", "@docusaurus/types": "3.3.2",
"@docusaurus/utils-validation": "3.4.0", "@docusaurus/utils-validation": "3.3.2",
"tslib": "^2.6.0" "tslib": "^2.6.0"
}, },
"engines": { "engines": {
@@ -2453,14 +2443,13 @@
} }
}, },
"node_modules/@docusaurus/plugin-google-gtag": { "node_modules/@docusaurus/plugin-google-gtag": {
"version": "3.4.0", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.4.0.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.3.2.tgz",
"integrity": "sha512-Dsgg6PLAqzZw5wZ4QjUYc8Z2KqJqXxHxq3vIoyoBWiLEEfigIs7wHR+oiWUQy3Zk9MIk6JTYj7tMoQU0Jm3nqA==", "integrity": "sha512-vcrKOHGbIDjVnNMrfbNpRQR1x6Jvcrb48kVzpBAOsKbj9rXZm/idjVAXRaewwobHdOrJkfWS/UJoxzK8wyLRBQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@docusaurus/core": "3.4.0", "@docusaurus/core": "3.3.2",
"@docusaurus/types": "3.4.0", "@docusaurus/types": "3.3.2",
"@docusaurus/utils-validation": "3.4.0", "@docusaurus/utils-validation": "3.3.2",
"@types/gtag.js": "^0.0.12", "@types/gtag.js": "^0.0.12",
"tslib": "^2.6.0" "tslib": "^2.6.0"
}, },
@@ -2473,14 +2462,13 @@
} }
}, },
"node_modules/@docusaurus/plugin-google-tag-manager": { "node_modules/@docusaurus/plugin-google-tag-manager": {
"version": "3.4.0", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.4.0.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.3.2.tgz",
"integrity": "sha512-O9tX1BTwxIhgXpOLpFDueYA9DWk69WCbDRrjYoMQtFHSkTyE7RhNgyjSPREUWJb9i+YUg3OrsvrBYRl64FCPCQ==", "integrity": "sha512-ldkR58Fdeks0vC+HQ+L+bGFSJsotQsipXD+iKXQFvkOfmPIV6QbHRd7IIcm5b6UtwOiK33PylNS++gjyLUmaGw==",
"license": "MIT",
"dependencies": { "dependencies": {
"@docusaurus/core": "3.4.0", "@docusaurus/core": "3.3.2",
"@docusaurus/types": "3.4.0", "@docusaurus/types": "3.3.2",
"@docusaurus/utils-validation": "3.4.0", "@docusaurus/utils-validation": "3.3.2",
"tslib": "^2.6.0" "tslib": "^2.6.0"
}, },
"engines": { "engines": {
@@ -2492,17 +2480,16 @@
} }
}, },
"node_modules/@docusaurus/plugin-sitemap": { "node_modules/@docusaurus/plugin-sitemap": {
"version": "3.4.0", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.4.0.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.3.2.tgz",
"integrity": "sha512-+0VDvx9SmNrFNgwPoeoCha+tRoAjopwT0+pYO1xAbyLcewXSemq+eLxEa46Q1/aoOaJQ0qqHELuQM7iS2gp33Q==", "integrity": "sha512-/ZI1+bwZBhAgC30inBsHe3qY9LOZS+79fRGkNdTcGHRMcdAp6Vw2pCd1gzlxd/xU+HXsNP6cLmTOrggmRp3Ujg==",
"license": "MIT",
"dependencies": { "dependencies": {
"@docusaurus/core": "3.4.0", "@docusaurus/core": "3.3.2",
"@docusaurus/logger": "3.4.0", "@docusaurus/logger": "3.3.2",
"@docusaurus/types": "3.4.0", "@docusaurus/types": "3.3.2",
"@docusaurus/utils": "3.4.0", "@docusaurus/utils": "3.3.2",
"@docusaurus/utils-common": "3.4.0", "@docusaurus/utils-common": "3.3.2",
"@docusaurus/utils-validation": "3.4.0", "@docusaurus/utils-validation": "3.3.2",
"fs-extra": "^11.1.1", "fs-extra": "^11.1.1",
"sitemap": "^7.1.1", "sitemap": "^7.1.1",
"tslib": "^2.6.0" "tslib": "^2.6.0"
@@ -2516,24 +2503,23 @@
} }
}, },
"node_modules/@docusaurus/preset-classic": { "node_modules/@docusaurus/preset-classic": {
"version": "3.4.0", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.4.0.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.3.2.tgz",
"integrity": "sha512-Ohj6KB7siKqZaQhNJVMBBUzT3Nnp6eTKqO+FXO3qu/n1hJl3YLwVKTWBg28LF7MWrKu46UuYavwMRxud0VyqHg==", "integrity": "sha512-1SDS7YIUN1Pg3BmD6TOTjhB7RSBHJRpgIRKx9TpxqyDrJ92sqtZhomDc6UYoMMLQNF2wHFZZVGFjxJhw2VpL+Q==",
"license": "MIT",
"dependencies": { "dependencies": {
"@docusaurus/core": "3.4.0", "@docusaurus/core": "3.3.2",
"@docusaurus/plugin-content-blog": "3.4.0", "@docusaurus/plugin-content-blog": "3.3.2",
"@docusaurus/plugin-content-docs": "3.4.0", "@docusaurus/plugin-content-docs": "3.3.2",
"@docusaurus/plugin-content-pages": "3.4.0", "@docusaurus/plugin-content-pages": "3.3.2",
"@docusaurus/plugin-debug": "3.4.0", "@docusaurus/plugin-debug": "3.3.2",
"@docusaurus/plugin-google-analytics": "3.4.0", "@docusaurus/plugin-google-analytics": "3.3.2",
"@docusaurus/plugin-google-gtag": "3.4.0", "@docusaurus/plugin-google-gtag": "3.3.2",
"@docusaurus/plugin-google-tag-manager": "3.4.0", "@docusaurus/plugin-google-tag-manager": "3.3.2",
"@docusaurus/plugin-sitemap": "3.4.0", "@docusaurus/plugin-sitemap": "3.3.2",
"@docusaurus/theme-classic": "3.4.0", "@docusaurus/theme-classic": "3.3.2",
"@docusaurus/theme-common": "3.4.0", "@docusaurus/theme-common": "3.3.2",
"@docusaurus/theme-search-algolia": "3.4.0", "@docusaurus/theme-search-algolia": "3.3.2",
"@docusaurus/types": "3.4.0" "@docusaurus/types": "3.3.2"
}, },
"engines": { "engines": {
"node": ">=18.0" "node": ">=18.0"
@@ -2544,23 +2530,22 @@
} }
}, },
"node_modules/@docusaurus/theme-classic": { "node_modules/@docusaurus/theme-classic": {
"version": "3.4.0", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.4.0.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.3.2.tgz",
"integrity": "sha512-0IPtmxsBYv2adr1GnZRdMkEQt1YW6tpzrUPj02YxNpvJ5+ju4E13J5tB4nfdaen/tfR1hmpSPlTFPvTf4kwy8Q==", "integrity": "sha512-gepHFcsluIkPb4Im9ukkiO4lXrai671wzS3cKQkY9BXQgdVwsdPf/KS0Vs4Xlb0F10fTz+T3gNjkxNEgSN9M0A==",
"license": "MIT",
"dependencies": { "dependencies": {
"@docusaurus/core": "3.4.0", "@docusaurus/core": "3.3.2",
"@docusaurus/mdx-loader": "3.4.0", "@docusaurus/mdx-loader": "3.3.2",
"@docusaurus/module-type-aliases": "3.4.0", "@docusaurus/module-type-aliases": "3.3.2",
"@docusaurus/plugin-content-blog": "3.4.0", "@docusaurus/plugin-content-blog": "3.3.2",
"@docusaurus/plugin-content-docs": "3.4.0", "@docusaurus/plugin-content-docs": "3.3.2",
"@docusaurus/plugin-content-pages": "3.4.0", "@docusaurus/plugin-content-pages": "3.3.2",
"@docusaurus/theme-common": "3.4.0", "@docusaurus/theme-common": "3.3.2",
"@docusaurus/theme-translations": "3.4.0", "@docusaurus/theme-translations": "3.3.2",
"@docusaurus/types": "3.4.0", "@docusaurus/types": "3.3.2",
"@docusaurus/utils": "3.4.0", "@docusaurus/utils": "3.3.2",
"@docusaurus/utils-common": "3.4.0", "@docusaurus/utils-common": "3.3.2",
"@docusaurus/utils-validation": "3.4.0", "@docusaurus/utils-validation": "3.3.2",
"@mdx-js/react": "^3.0.0", "@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"copy-text-to-clipboard": "^3.2.0", "copy-text-to-clipboard": "^3.2.0",
@@ -2584,18 +2569,17 @@
} }
}, },
"node_modules/@docusaurus/theme-common": { "node_modules/@docusaurus/theme-common": {
"version": "3.4.0", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.4.0.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.3.2.tgz",
"integrity": "sha512-0A27alXuv7ZdCg28oPE8nH/Iz73/IUejVaCazqu9elS4ypjiLhK3KfzdSQBnL/g7YfHSlymZKdiOHEo8fJ0qMA==", "integrity": "sha512-kXqSaL/sQqo4uAMQ4fHnvRZrH45Xz2OdJ3ABXDS7YVGPSDTBC8cLebFrRR4YF9EowUHto1UC/EIklJZQMG/usA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@docusaurus/mdx-loader": "3.4.0", "@docusaurus/mdx-loader": "3.3.2",
"@docusaurus/module-type-aliases": "3.4.0", "@docusaurus/module-type-aliases": "3.3.2",
"@docusaurus/plugin-content-blog": "3.4.0", "@docusaurus/plugin-content-blog": "3.3.2",
"@docusaurus/plugin-content-docs": "3.4.0", "@docusaurus/plugin-content-docs": "3.3.2",
"@docusaurus/plugin-content-pages": "3.4.0", "@docusaurus/plugin-content-pages": "3.3.2",
"@docusaurus/utils": "3.4.0", "@docusaurus/utils": "3.3.2",
"@docusaurus/utils-common": "3.4.0", "@docusaurus/utils-common": "3.3.2",
"@types/history": "^4.7.11", "@types/history": "^4.7.11",
"@types/react": "*", "@types/react": "*",
"@types/react-router-config": "*", "@types/react-router-config": "*",
@@ -2614,19 +2598,18 @@
} }
}, },
"node_modules/@docusaurus/theme-search-algolia": { "node_modules/@docusaurus/theme-search-algolia": {
"version": "3.4.0", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.4.0.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.3.2.tgz",
"integrity": "sha512-aiHFx7OCw4Wck1z6IoShVdUWIjntC8FHCw9c5dR8r3q4Ynh+zkS8y2eFFunN/DL6RXPzpnvKCg3vhLQYJDmT9Q==", "integrity": "sha512-qLkfCl29VNBnF1MWiL9IyOQaHxUvicZp69hISyq/xMsNvFKHFOaOfk9xezYod2Q9xx3xxUh9t/QPigIei2tX4w==",
"license": "MIT",
"dependencies": { "dependencies": {
"@docsearch/react": "^3.5.2", "@docsearch/react": "^3.5.2",
"@docusaurus/core": "3.4.0", "@docusaurus/core": "3.3.2",
"@docusaurus/logger": "3.4.0", "@docusaurus/logger": "3.3.2",
"@docusaurus/plugin-content-docs": "3.4.0", "@docusaurus/plugin-content-docs": "3.3.2",
"@docusaurus/theme-common": "3.4.0", "@docusaurus/theme-common": "3.3.2",
"@docusaurus/theme-translations": "3.4.0", "@docusaurus/theme-translations": "3.3.2",
"@docusaurus/utils": "3.4.0", "@docusaurus/utils": "3.3.2",
"@docusaurus/utils-validation": "3.4.0", "@docusaurus/utils-validation": "3.3.2",
"algoliasearch": "^4.18.0", "algoliasearch": "^4.18.0",
"algoliasearch-helper": "^3.13.3", "algoliasearch-helper": "^3.13.3",
"clsx": "^2.0.0", "clsx": "^2.0.0",
@@ -2645,10 +2628,9 @@
} }
}, },
"node_modules/@docusaurus/theme-translations": { "node_modules/@docusaurus/theme-translations": {
"version": "3.4.0", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.4.0.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.3.2.tgz",
"integrity": "sha512-zSxCSpmQCCdQU5Q4CnX/ID8CSUUI3fvmq4hU/GNP/XoAWtXo9SAVnM3TzpU8Gb//H3WCsT8mJcTfyOk3d9ftNg==", "integrity": "sha512-bPuiUG7Z8sNpGuTdGnmKl/oIPeTwKr0AXLGu9KaP6+UFfRZiyWbWE87ti97RrevB2ffojEdvchNujparR3jEZQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"fs-extra": "^11.1.1", "fs-extra": "^11.1.1",
"tslib": "^2.6.0" "tslib": "^2.6.0"
@@ -2658,10 +2640,9 @@
} }
}, },
"node_modules/@docusaurus/types": { "node_modules/@docusaurus/types": {
"version": "3.4.0", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.4.0.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.3.2.tgz",
"integrity": "sha512-4jcDO8kXi5Cf9TcyikB/yKmz14f2RZ2qTRerbHAsS+5InE9ZgSLBNLsewtFTcTOXSVcbU3FoGOzcNWAmU1TR0A==", "integrity": "sha512-5p201S7AZhliRxTU7uMKtSsoC8mgPA9bs9b5NQg1IRdRxJfflursXNVsgc3PcMqiUTul/v1s3k3rXXFlRE890w==",
"license": "MIT",
"dependencies": { "dependencies": {
"@mdx-js/mdx": "^3.0.0", "@mdx-js/mdx": "^3.0.0",
"@types/history": "^4.7.11", "@types/history": "^4.7.11",
@@ -2679,13 +2660,12 @@
} }
}, },
"node_modules/@docusaurus/utils": { "node_modules/@docusaurus/utils": {
"version": "3.4.0", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.4.0.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.3.2.tgz",
"integrity": "sha512-fRwnu3L3nnWaXOgs88BVBmG1yGjcQqZNHG+vInhEa2Sz2oQB+ZjbEMO5Rh9ePFpZ0YDiDUhpaVjwmS+AU2F14g==", "integrity": "sha512-f4YMnBVymtkSxONv4Y8js3Gez9IgHX+Lcg6YRMOjVbq8sgCcdYK1lf6SObAuz5qB/mxiSK7tW0M9aaiIaUSUJg==",
"license": "MIT",
"dependencies": { "dependencies": {
"@docusaurus/logger": "3.4.0", "@docusaurus/logger": "3.3.2",
"@docusaurus/utils-common": "3.4.0", "@docusaurus/utils-common": "3.3.2",
"@svgr/webpack": "^8.1.0", "@svgr/webpack": "^8.1.0",
"escape-string-regexp": "^4.0.0", "escape-string-regexp": "^4.0.0",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
@@ -2702,7 +2682,6 @@
"shelljs": "^0.8.5", "shelljs": "^0.8.5",
"tslib": "^2.6.0", "tslib": "^2.6.0",
"url-loader": "^4.1.1", "url-loader": "^4.1.1",
"utility-types": "^3.10.0",
"webpack": "^5.88.1" "webpack": "^5.88.1"
}, },
"engines": { "engines": {
@@ -2718,10 +2697,9 @@
} }
}, },
"node_modules/@docusaurus/utils-common": { "node_modules/@docusaurus/utils-common": {
"version": "3.4.0", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.4.0.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.3.2.tgz",
"integrity": "sha512-NVx54Wr4rCEKsjOH5QEVvxIqVvm+9kh7q8aYTU5WzUU9/Hctd6aTrcZ3G0Id4zYJ+AeaG5K5qHA4CY5Kcm2iyQ==", "integrity": "sha512-QWFTLEkPYsejJsLStgtmetMFIA3pM8EPexcZ4WZ7b++gO5jGVH7zsipREnCHzk6+eDgeaXfkR6UPaTt86bp8Og==",
"license": "MIT",
"dependencies": { "dependencies": {
"tslib": "^2.6.0" "tslib": "^2.6.0"
}, },
@@ -2738,18 +2716,15 @@
} }
}, },
"node_modules/@docusaurus/utils-validation": { "node_modules/@docusaurus/utils-validation": {
"version": "3.4.0", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.4.0.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.3.2.tgz",
"integrity": "sha512-hYQ9fM+AXYVTWxJOT1EuNaRnrR2WGpRdLDQG07O8UOpsvCPWUVOeo26Rbm0JWY2sGLfzAb+tvJ62yF+8F+TV0g==", "integrity": "sha512-itDgFs5+cbW9REuC7NdXals4V6++KifgVMzoGOOOSIifBQw+8ULhy86u5e1lnptVL0sv8oAjq2alO7I40GR7pA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@docusaurus/logger": "3.4.0", "@docusaurus/logger": "3.3.2",
"@docusaurus/utils": "3.4.0", "@docusaurus/utils": "3.3.2",
"@docusaurus/utils-common": "3.4.0", "@docusaurus/utils-common": "3.3.2",
"fs-extra": "^11.2.0",
"joi": "^17.9.2", "joi": "^17.9.2",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"tslib": "^2.6.0" "tslib": "^2.6.0"
}, },
"engines": { "engines": {
@@ -13598,11 +13573,10 @@
} }
}, },
"node_modules/prettier": { "node_modules/prettier": {
"version": "3.3.1", "version": "3.2.5",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.1.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
"integrity": "sha512-7CAwy5dRsxs8PHXT3twixW9/OEll8MLE0VRPCJyl7CkS6VHGPSlsVaWTiASPTyGyYRyApxlaWTzwUxVNrhcwDg==", "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
"dev": true, "dev": true,
"license": "MIT",
"bin": { "bin": {
"prettier": "bin/prettier.cjs" "prettier": "bin/prettier.cjs"
}, },
@@ -16012,10 +15986,9 @@
} }
}, },
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "3.4.4", "version": "3.4.3",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz",
"integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==", "integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==",
"license": "MIT",
"dependencies": { "dependencies": {
"@alloc/quick-lru": "^5.2.0", "@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2", "arg": "^5.0.2",
@@ -16052,7 +16025,6 @@
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"license": "ISC",
"dependencies": { "dependencies": {
"is-glob": "^4.0.3" "is-glob": "^4.0.3"
}, },
+1 -1
View File
@@ -36,7 +36,7 @@ function HomepageHeader() {
<Link <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" 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 Discord
</Link> </Link>
-4
View File
@@ -1,8 +1,4 @@
[ [
{
"label": "v1.106.4",
"url": "https://v1.106.4.archive.immich.app"
},
{ {
"label": "v1.106.3", "label": "v1.106.3",
"url": "https://v1.106.3.archive.immich.app" "url": "https://v1.106.3.archive.immich.app"
+59 -61
View File
@@ -1,12 +1,12 @@
{ {
"name": "immich-e2e", "name": "immich-e2e",
"version": "1.106.4", "version": "1.106.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "immich-e2e", "name": "immich-e2e",
"version": "1.106.4", "version": "1.106.3",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"devDependencies": { "devDependencies": {
"@immich/cli": "file:../cli", "@immich/cli": "file:../cli",
@@ -39,7 +39,7 @@
}, },
"../cli": { "../cli": {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.4", "version": "2.2.3",
"dev": true, "dev": true,
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
@@ -81,14 +81,14 @@
}, },
"../open-api/typescript-sdk": { "../open-api/typescript-sdk": {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.106.4", "version": "1.106.3",
"dev": true, "dev": true,
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
"@oazapfts/runtime": "^1.0.2" "@oazapfts/runtime": "^1.0.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.14.2", "@types/node": "^20.11.0",
"typescript": "^5.3.3" "typescript": "^5.3.3"
} }
}, },
@@ -1230,9 +1230,9 @@
"dev": true "dev": true
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.14.2", "version": "20.12.13",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.13.tgz",
"integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", "integrity": "sha512-gBGeanV41c1L171rR7wjbMiEpEI/l5XFQdLLfhr/REwpgDy/4U8y89+i8kRiLzDyZdOkXh+cRaTetUnCYutoXA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1344,17 +1344,17 @@
} }
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.12.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.12.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.11.0.tgz",
"integrity": "sha512-7F91fcbuDf/d3S8o21+r3ZncGIke/+eWk0EpO21LXhDfLahriZF9CGj4fbAetEjlaBdjdSm9a6VeXbpbT6Z40Q==", "integrity": "sha512-P+qEahbgeHW4JQ/87FuItjBj8O3MYv5gELDzr8QaQ7fsll1gSMTYb6j87MYyxwf3DtD7uGFB9ShwgmCJB5KmaQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.10.0", "@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.12.0", "@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/type-utils": "7.12.0", "@typescript-eslint/type-utils": "7.11.0",
"@typescript-eslint/utils": "7.12.0", "@typescript-eslint/utils": "7.11.0",
"@typescript-eslint/visitor-keys": "7.12.0", "@typescript-eslint/visitor-keys": "7.11.0",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^5.3.1", "ignore": "^5.3.1",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
@@ -1378,16 +1378,16 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "7.12.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.12.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.11.0.tgz",
"integrity": "sha512-dm/J2UDY3oV3TKius2OUZIFHsomQmpHtsV0FTh1WO8EKgHLQ1QCADUqscPgTpU+ih1e21FQSRjXckHn3txn6kQ==", "integrity": "sha512-yimw99teuaXVWsBcPO1Ais02kwJ1jmNA1KxE7ng0aT7ndr1pT1wqj0OJnsYVGKKlc4QJai86l/025L6z8CljOg==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "7.12.0", "@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/types": "7.12.0", "@typescript-eslint/types": "7.11.0",
"@typescript-eslint/typescript-estree": "7.12.0", "@typescript-eslint/typescript-estree": "7.11.0",
"@typescript-eslint/visitor-keys": "7.12.0", "@typescript-eslint/visitor-keys": "7.11.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -1407,14 +1407,14 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "7.12.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.12.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz",
"integrity": "sha512-itF1pTnN6F3unPak+kutH9raIkL3lhH1YRPGgt7QQOh43DQKVJXmWkpb+vpc/TiDHs6RSd9CTbDsc/Y+Ygq7kg==", "integrity": "sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.12.0", "@typescript-eslint/types": "7.11.0",
"@typescript-eslint/visitor-keys": "7.12.0" "@typescript-eslint/visitor-keys": "7.11.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || >=20.0.0" "node": "^18.18.0 || >=20.0.0"
@@ -1425,14 +1425,14 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "7.12.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.12.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.11.0.tgz",
"integrity": "sha512-lib96tyRtMhLxwauDWUp/uW3FMhLA6D0rJ8T7HmH7x23Gk1Gwwu8UZ94NMXBvOELn6flSPiBrCKlehkiXyaqwA==", "integrity": "sha512-WmppUEgYy+y1NTseNMJ6mCFxt03/7jTOy08bcg7bxJJdsM4nuhnchyBbE8vryveaJUf62noH7LodPSo5Z0WUCg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/typescript-estree": "7.12.0", "@typescript-eslint/typescript-estree": "7.11.0",
"@typescript-eslint/utils": "7.12.0", "@typescript-eslint/utils": "7.11.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^1.3.0" "ts-api-utils": "^1.3.0"
}, },
@@ -1453,9 +1453,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "7.12.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.12.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz",
"integrity": "sha512-o+0Te6eWp2ppKY3mLCU+YA9pVJxhUJE15FV7kxuD9jgwIAa+w/ycGJBMrYDTpVGUM/tgpa9SeMOugSabWFq7bg==", "integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -1467,14 +1467,14 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "7.12.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.12.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz",
"integrity": "sha512-5bwqLsWBULv1h6pn7cMW5dXX/Y2amRqLaKqsASVwbBHMZSnHqE/HN4vT4fE0aFsiwxYvr98kqOWh1a8ZKXalCQ==", "integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.12.0", "@typescript-eslint/types": "7.11.0",
"@typescript-eslint/visitor-keys": "7.12.0", "@typescript-eslint/visitor-keys": "7.11.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"globby": "^11.1.0", "globby": "^11.1.0",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@@ -1522,16 +1522,16 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "7.12.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.12.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.11.0.tgz",
"integrity": "sha512-Y6hhwxwDx41HNpjuYswYp6gDbkiZ8Hin9Bf5aJQn1bpTs3afYY4GX+MPYxma8jtoIV2GRwTM/UJm/2uGCVv+DQ==", "integrity": "sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.4.0", "@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "7.12.0", "@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/types": "7.12.0", "@typescript-eslint/types": "7.11.0",
"@typescript-eslint/typescript-estree": "7.12.0" "@typescript-eslint/typescript-estree": "7.11.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || >=20.0.0" "node": "^18.18.0 || >=20.0.0"
@@ -1545,13 +1545,13 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "7.12.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.12.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz",
"integrity": "sha512-uZk7DevrQLL3vSnfFl5bj4sL75qC9D6EdjemIdbtkuUmIheWpuiiylSY01JxJE7+zGrOWDZrp1WxOuDntvKrHQ==", "integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.12.0", "@typescript-eslint/types": "7.11.0",
"eslint-visitor-keys": "^3.4.3" "eslint-visitor-keys": "^3.4.3"
}, },
"engines": { "engines": {
@@ -2700,9 +2700,9 @@
} }
}, },
"node_modules/exiftool-vendored": { "node_modules/exiftool-vendored": {
"version": "26.2.0", "version": "26.1.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-26.2.0.tgz", "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-26.1.0.tgz",
"integrity": "sha512-7P6jQ944or7ic2SJzW+uaWK4TLDXlaCppHrBayl4MpIrVcEeQjiQTez4/oOH0wULIRu4j4H6Xruz4SLrDaafUg==", "integrity": "sha512-Bhy2Ia86Agt3+PbJJhWeVMqJNXl74XJ0Oygef5F5uCL13fTxlmF8dECHiChyx8bBc3sxIw+2Q3ehWunJh3bs6w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -4127,11 +4127,10 @@
} }
}, },
"node_modules/pg": { "node_modules/pg": {
"version": "8.12.0", "version": "8.11.5",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz", "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.5.tgz",
"integrity": "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==", "integrity": "sha512-jqgNHSKL5cbDjFlHyYsCXmQDrfIX/3RsNwYqpd4N0Kt8niLuNoRNH+aazv6cOd43gPh9Y4DjQCtb+X0MH0Hvnw==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"pg-connection-string": "^2.6.4", "pg-connection-string": "^2.6.4",
"pg-pool": "^3.6.2", "pg-pool": "^3.6.2",
@@ -4386,11 +4385,10 @@
} }
}, },
"node_modules/prettier": { "node_modules/prettier": {
"version": "3.3.1", "version": "3.2.5",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.1.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
"integrity": "sha512-7CAwy5dRsxs8PHXT3twixW9/OEll8MLE0VRPCJyl7CkS6VHGPSlsVaWTiASPTyGyYRyApxlaWTzwUxVNrhcwDg==", "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
"dev": true, "dev": true,
"license": "MIT",
"bin": { "bin": {
"prettier": "bin/prettier.cjs" "prettier": "bin/prettier.cjs"
}, },
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "immich-e2e", "name": "immich-e2e",
"version": "1.106.4", "version": "1.106.3",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
+10 -15
View File
@@ -250,23 +250,18 @@ describe('/admin/users', () => {
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); 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) }); const after = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ avatar: { color: 'orange' } }); expect(after).toEqual({
}); avatar: { color: 'orange' },
memories: { enabled: false },
it('should update download archive size', async () => { emailNotifications: { enabled: true, albumInvite: true, albumUpdate: true },
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 } });
}); });
}); });
-39
View File
@@ -173,45 +173,6 @@ describe('/users', () => {
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ memories: { enabled: false } }); 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', () => { describe('GET /users/:id', () => {
+8 -1
View File
@@ -398,7 +398,14 @@ export const utils = {
return; 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) => { setPersonThumbnail: async (personId: string) => {
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "machine-learning" name = "machine-learning"
version = "1.106.4" version = "1.106.3"
description = "" description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"] authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md" readme = "README.md"
+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', task: 'bundle',
build_type: 'Release', build_type: 'Release',
properties: { properties: {
"android.injected.version.code" => 144, "android.injected.version.code" => 143,
"android.injected.version.name" => "1.106.4", "android.injected.version.name" => "1.106.3",
} }
) )
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') 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')
-2
View File
@@ -295,8 +295,6 @@
"memories_check_back_tomorrow": "Check back tomorrow for more memories", "memories_check_back_tomorrow": "Check back tomorrow for more memories",
"memories_start_over": "Start Over", "memories_start_over": "Start Over",
"memories_swipe_to_close": "Swipe up to close", "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", "monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "Motion Photos", "motion_photos_page_title": "Motion Photos",
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", "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_check_back_tomorrow": "明日もう一度確認してください",
"memories_start_over": "始める", "memories_start_over": "始める",
"memories_swipe_to_close": "上にスワイプして閉じる", "memories_swipe_to_close": "上にスワイプして閉じる",
"memories_year_ago": "過去1年間",
"memories_years_ago": "過去{}年間",
"monthly_title_text_date_format": "yyyy年 MM月", "monthly_title_text_date_format": "yyyy年 MM月",
"motion_photos_page_title": "モーションフォト", "motion_photos_page_title": "モーションフォト",
"multiselect_grid_edit_date_time_err_read_only": "読み取り専用の項目の日付を変更できません", "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_check_back_tomorrow": "Kom morgen terug voor meer herinneringen",
"memories_start_over": "Opnieuw beginnen", "memories_start_over": "Opnieuw beginnen",
"memories_swipe_to_close": "Swipe omhoog om te sluiten", "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", "monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "Bewegende foto's", "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", "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_check_back_tomorrow": "明天再看",
"memories_start_over": "再看一次", "memories_start_over": "再看一次",
"memories_swipe_to_close": "上划关闭", "memories_swipe_to_close": "上划关闭",
"memories_year_ago": "1年前",
"memories_years_ago": "{}年前",
"monthly_title_text_date_format": "MMMM y", "monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "动图", "motion_photos_page_title": "动图",
"multiselect_grid_edit_date_time_err_read_only": "无法编辑只读项目的日期,跳过", "multiselect_grid_edit_date_time_err_read_only": "无法编辑只读项目的日期,跳过",
-2
View File
@@ -295,8 +295,6 @@
"memories_check_back_tomorrow": "明天再看", "memories_check_back_tomorrow": "明天再看",
"memories_start_over": "再看一次", "memories_start_over": "再看一次",
"memories_swipe_to_close": "上划关闭", "memories_swipe_to_close": "上划关闭",
"memories_year_ago": "1年前",
"memories_years_ago": "{}年前",
"monthly_title_text_date_format": "MMMM y", "monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "动图", "motion_photos_page_title": "动图",
"multiselect_grid_edit_date_time_err_read_only": "无法编辑只读项目的日期,跳过", "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_check_back_tomorrow": "Check back tomorrow for more memories",
"memories_start_over": "Start Over", "memories_start_over": "Start Over",
"memories_swipe_to_close": "Swipe up to close", "memories_swipe_to_close": "Swipe up to close",
"memories_year_ago": "1年前",
"memories_years_ago": "{}年前",
"monthly_title_text_date_format": "MMMM y", "monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "Motion Photos", "motion_photos_page_title": "Motion Photos",
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
+1 -1
View File
@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta" desc "iOS Beta"
lane :beta do lane :beta do
increment_version_number( increment_version_number(
version_number: "1.106.4" version_number: "1.106.3"
) )
increment_build_number( increment_build_number(
build_number: latest_testflight_build_number + 1, build_number: latest_testflight_build_number + 1,
@@ -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:cancellation_token_http/http.dart';
import 'package:collection/collection.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/available_album.model.dart';
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
@@ -41,7 +41,7 @@ class BackUpState {
final Set<AvailableAlbum> excludedBackupAlbums; final Set<AvailableAlbum> excludedBackupAlbums;
/// Assets that are not overlapping in selected backup albums and excluded backup albums /// 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 /// All assets from the selected albums that have been backup
final Set<String> selectedAlbumsBackupAssetsIds; final Set<String> selectedAlbumsBackupAssetsIds;
@@ -94,7 +94,7 @@ class BackUpState {
List<AvailableAlbum>? availableAlbums, List<AvailableAlbum>? availableAlbums,
Set<AvailableAlbum>? selectedBackupAlbums, Set<AvailableAlbum>? selectedBackupAlbums,
Set<AvailableAlbum>? excludedBackupAlbums, Set<AvailableAlbum>? excludedBackupAlbums,
Set<BackupCandidate>? allUniqueAssets, Set<AssetEntity>? allUniqueAssets,
Set<String>? selectedAlbumsBackupAssetsIds, Set<String>? selectedAlbumsBackupAssetsIds,
CurrentUploadAsset? currentUploadAsset, CurrentUploadAsset? currentUploadAsset,
}) { }) {
+3 -1
View File
@@ -119,7 +119,9 @@ class PhotosPage extends HookConsumerWidget {
child: Container( child: Container(
height: kToolbarHeight + MediaQuery.of(context).padding.top, height: kToolbarHeight + MediaQuery.of(context).padding.top,
color: context.themeData.appBarTheme.backgroundColor, 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( Positioned(
right: 0, right: 0,
bottom: MediaQuery.of(context).padding.bottom + 16, bottom: 30,
child: ElevatedButton( child: ElevatedButton(
onPressed: onZoomToLocation, onPressed: onZoomToLocation,
style: ElevatedButton.styleFrom( 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_people_row.dart';
import 'package:immich_mobile/widgets/search/curated_places_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/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/routing/router.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
@@ -31,7 +31,7 @@ class SearchPage extends HookConsumerWidget {
final curatedPeople = ref.watch(getAllPeopleProvider); final curatedPeople = ref.watch(getAllPeopleProvider);
final isMapEnabled = final isMapEnabled =
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.map)); 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( TextStyle categoryTitleStyle = const TextStyle(
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@@ -53,15 +53,16 @@ class SearchPage extends HookConsumerWidget {
} }
buildPeople() { buildPeople() {
return curatedPeople.widgetWhen( return SizedBox(
onError: (error, stack) => const ScaffoldErrorBody(withIcon: false), height: imageSize,
onData: (people) { child: curatedPeople.widgetWhen(
return SearchRowSection( onError: (error, stack) => const ScaffoldErrorBody(withIcon: false),
onViewAllPressed: () => context.pushRoute(const AllPeopleRoute()), onData: (people) => Padding(
title: "search_page_people".tr(), padding: const EdgeInsets.only(
isEmpty: people.isEmpty, left: 16,
top: 8,
),
child: CuratedPeopleRow( child: CuratedPeopleRow(
padding: const EdgeInsets.symmetric(horizontal: 16),
content: people content: people
.map((e) => SearchCuratedContent(label: e.name, id: e.id)) .map((e) => SearchCuratedContent(label: e.name, id: e.id))
.take(12) .take(12)
@@ -78,46 +79,42 @@ class SearchPage extends HookConsumerWidget {
showNameEditModel(person.id, person.label), showNameEditModel(person.id, person.label),
}, },
), ),
); ),
}, ),
); );
} }
buildPlaces() { buildPlaces() {
return places.widgetWhen( return SizedBox(
onError: (error, stack) => const ScaffoldErrorBody(withIcon: false), height: imageSize,
onData: (data) { child: places.widgetWhen(
return SearchRowSection( onError: (error, stack) => const ScaffoldErrorBody(withIcon: false),
onViewAllPressed: () => context.pushRoute(const AllPlacesRoute()), onData: (data) => CuratedPlacesRow(
title: "search_page_places".tr(), isMapEnabled: isMapEnabled,
isEmpty: !isMapEnabled && data.isEmpty, content: data,
child: CuratedPlacesRow( imageSize: imageSize,
isMapEnabled: isMapEnabled, onTap: (content, index) {
content: data, context.pushRoute(
imageSize: imageSize, SearchInputRoute(
onTap: (content, index) { prefilter: SearchFilter(
context.pushRoute( people: {},
SearchInputRoute( location: SearchLocationFilter(
prefilter: SearchFilter( city: content.label,
people: {},
location: SearchLocationFilter(
city: content.label,
),
camera: SearchCameraFilter(),
date: SearchDateFilter(),
display: SearchDisplayFilters(
isNotInAlbum: false,
isArchive: false,
isFavorite: false,
),
mediaType: AssetType.other,
), ),
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( return Scaffold(
appBar: const ImmichAppBar(), appBar: const ImmichAppBar(),
body: ListView( body: Stack(
children: [ children: [
buildSearchButton(), ListView(
const SizedBox(height: 8.0), children: [
buildPeople(), buildSearchButton(),
const SizedBox(height: 8.0), SearchRowTitle(
buildPlaces(), title: "search_page_people".tr(),
const SizedBox(height: 24.0), onViewAllPressed: () =>
Padding( context.pushRoute(const AllPeopleRoute()),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'search_page_your_activity',
style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
), ),
).tr(), buildPeople(),
), SearchRowTitle(
ListTile( title: "search_page_places".tr(),
leading: Icon( onViewAllPressed: () =>
Icons.favorite_border_rounded, context.pushRoute(const AllPlacesRoute()),
color: categoryIconColor, top: 0,
),
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(), const SizedBox(height: 10.0),
), buildPlaces(),
ListTile( const SizedBox(height: 24.0),
title: Text('search_page_videos', style: categoryTitleStyle).tr(), Padding(
leading: Icon( padding: const EdgeInsets.symmetric(horizontal: 16),
Icons.play_circle_outline, child: Text(
color: categoryIconColor, 'search_page_your_activity',
), style: context.textTheme.bodyLarge?.copyWith(
onTap: () => context.pushRoute(const AllVideosRoute()), fontWeight: FontWeight.w500,
), ),
const CategoryDivider(), ).tr(),
ListTile( ),
title: Text( ListTile(
'search_page_motion_photos', leading: Icon(
style: categoryTitleStyle, Icons.favorite_border_rounded,
).tr(), color: categoryIconColor,
leading: Icon( ),
Icons.motion_photos_on_outlined, title: Text('search_page_favorites', style: categoryTitleStyle)
color: categoryIconColor, .tr(),
), onTap: () => context.pushRoute(const FavoritesRoute()),
onTap: () => context.pushRoute(const AllMotionPhotosRoute()), ),
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()),
),
],
), ),
], ],
), ),
+11
View File
@@ -50,8 +50,19 @@ class AssetNotifier extends StateNotifier<bool> {
await clearAssetsAndAlbums(_db); await clearAssetsAndAlbums(_db);
log.info("Manual refresh requested, cleared assets and albums from db"); log.info("Manual refresh requested, cleared assets and albums from db");
} }
final bool changedUsers = await _userService.refreshUsers(); final bool changedUsers = await _userService.refreshUsers();
final assetCount = await _db.assets.count();
if (assetCount == 0) {
debugPrint("First sync, refreshing all assets");
await _assetService.refreshRemoteAssets(firstSync: true);
debugPrint("First sync, DONE refreshing all assets");
}
debugPrint("First sync, CONTINUE refreshing all assets");
final bool newRemote = await _assetService.refreshRemoteAssets(); final bool newRemote = await _assetService.refreshRemoteAssets();
final bool newLocal = await _albumService.refreshDeviceAlbums(); final bool newLocal = await _albumService.refreshDeviceAlbums();
debugPrint( debugPrint(
"changedUsers: $changedUsers, newRemote: $newRemote, newLocal: $newLocal", "changedUsers: $changedUsers, newRemote: $newRemote, newLocal: $newLocal",
@@ -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,
),
);
@@ -6,7 +6,6 @@ import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/models/backup/available_album.model.dart'; import 'package:immich_mobile/models/backup/available_album.model.dart';
import 'package:immich_mobile/entities/backup_album.entity.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/backup_state.model.dart';
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
import 'package:immich_mobile/models/backup/error_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 /// Those assets are unique and are used as the total assets
/// ///
Future<void> _updateBackupAssetCount() async { Future<void> _updateBackupAssetCount() async {
debugPrint("UPDATE BACKUP ASSET COUNTTT");
final duplicatedAssetIds = await _backupService.getDuplicatedAssetIds(); final duplicatedAssetIds = await _backupService.getDuplicatedAssetIds();
final Set<BackupCandidate> assetsFromSelectedAlbums = {}; final Set<AssetEntity> assetsFromSelectedAlbums = {};
final Set<BackupCandidate> assetsFromExcludedAlbums = {}; final Set<AssetEntity> assetsFromExcludedAlbums = {};
/// Extracing assets from selected albums
for (final album in state.selectedBackupAlbums) { for (final album in state.selectedBackupAlbums) {
final assetCount = await album.albumEntity.assetCountAsync; final assetCount = await album.albumEntity.assetCountAsync;
@@ -307,19 +304,9 @@ class BackupNotifier extends StateNotifier<BackUpState> {
start: 0, start: 0,
end: assetCount, end: assetCount,
); );
assetsFromSelectedAlbums.addAll(assets);
final candidate = assets.map(
(e) => BackupCandidate(
id: e.id,
albumName: [album.albumEntity.name],
asset: e,
),
);
assetsFromSelectedAlbums.addAll(candidate.toSet());
} }
/// Extracing assets from excluded albums
for (final album in state.excludedBackupAlbums) { for (final album in state.excludedBackupAlbums) {
final assetCount = await album.albumEntity.assetCountAsync; final assetCount = await album.albumEntity.assetCountAsync;
@@ -331,48 +318,29 @@ class BackupNotifier extends StateNotifier<BackUpState> {
start: 0, start: 0,
end: assetCount, end: assetCount,
); );
assetsFromExcludedAlbums.addAll(assets);
final candidate = assets.map(
(e) => BackupCandidate(
id: e.id,
albumName: [album.albumEntity.name],
asset: e,
),
);
assetsFromExcludedAlbums.addAll(candidate);
} }
Set<BackupCandidate> allUniqueAssets = final Set<AssetEntity> allUniqueAssets =
assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums); assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums);
final allAssetsInDatabase = await _backupService.getDeviceBackupAsset(); final allAssetsInDatabase = await _backupService.getDeviceBackupAsset();
if (allAssetsInDatabase == null) { if (allAssetsInDatabase == null) {
return; return;
} }
// Find asset that were backup from selected albums // Find asset that were backup from selected albums
final Set<String> selectedAlbumsBackupAssets = final Set<String> selectedAlbumsBackupAssets =
Set.from(allUniqueAssets.map((e) => e.asset.id)); Set.from(allUniqueAssets.map((e) => e.id));
selectedAlbumsBackupAssets selectedAlbumsBackupAssets
.removeWhere((assetId) => !allAssetsInDatabase.contains(assetId)); .removeWhere((assetId) => !allAssetsInDatabase.contains(assetId));
// Remove duplicated asset from all unique assets // Remove duplicated asset from all unique assets
allUniqueAssets.removeWhere( 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) { if (allUniqueAssets.isEmpty) {
log.info("No assets are selected for back up"); log.info("No assets are selected for back up");
state = state.copyWith( state = state.copyWith(
@@ -391,8 +359,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
// Save to persistent storage // Save to persistent storage
await _updatePersistentAlbumsSelection(); await _updatePersistentAlbumsSelection();
debugPrint("backup asset ${allUniqueAssets.length}", wrapWidth: 80);
} }
/// Get all necessary information for calculating the available albums, /// Get all necessary information for calculating the available albums,
@@ -539,7 +505,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
if (isDuplicated) { if (isDuplicated) {
state = state.copyWith( state = state.copyWith(
allUniqueAssets: state.allUniqueAssets allUniqueAssets: state.allUniqueAssets
.where((e) => e.asset.id != deviceAssetId) .where((asset) => asset.id != deviceAssetId)
.toSet(), .toSet(),
); );
} else { } else {
@@ -556,7 +522,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
state.selectedAlbumsBackupAssetsIds.length == state.selectedAlbumsBackupAssetsIds.length ==
0) { 0) {
final latestAssetBackup = final latestAssetBackup =
state.allUniqueAssets.map((e) => e.asset.modifiedDateTime).reduce( state.allUniqueAssets.map((e) => e.modifiedDateTime).reduce(
(v, e) => e.isAfter(v) ? e : v, (v, e) => e.isAfter(v) ? e : v,
); );
state = state.copyWith( state = state.copyWith(
+13 -3
View File
@@ -43,7 +43,7 @@ class AssetService {
/// Checks the server for updated assets and updates the local database if /// Checks the server for updated assets and updates the local database if
/// required. Returns `true` if there were any changes. /// required. Returns `true` if there were any changes.
Future<bool> refreshRemoteAssets() async { Future<bool> refreshRemoteAssets({bool firstSync = false}) async {
final syncedUserIds = await _db.eTags.where().idProperty().findAll(); final syncedUserIds = await _db.eTags.where().idProperty().findAll();
final List<User> syncedUsers = syncedUserIds.isEmpty final List<User> syncedUsers = syncedUserIds.isEmpty
? [] ? []
@@ -57,6 +57,7 @@ class AssetService {
getChangedAssets: _getRemoteAssetChanges, getChangedAssets: _getRemoteAssetChanges,
loadAssets: _getRemoteAssets, loadAssets: _getRemoteAssets,
refreshUsers: _userService.getUsersFromServer, refreshUsers: _userService.getUsersFromServer,
firstSync: firstSync,
); );
debugPrint("refreshRemoteAssets full took ${sw.elapsedMilliseconds}ms"); debugPrint("refreshRemoteAssets full took ${sw.elapsedMilliseconds}ms");
return changes; return changes;
@@ -97,8 +98,12 @@ class AssetService {
} }
/// Returns `null` if the server state did not change, else list of assets /// Returns `null` if the server state did not change, else list of assets
Future<List<Asset>?> _getRemoteAssets(User user, DateTime until) async { Future<List<Asset>?> _getRemoteAssets(
const int chunkSize = 10000; User user,
DateTime until,
bool firstSync,
) async {
int chunkSize = firstSync ? 1000 : 10000;
try { try {
final List<Asset> allAssets = []; final List<Asset> allAssets = [];
String? lastId; String? lastId;
@@ -120,6 +125,11 @@ class AssetService {
allAssets.addAll(assets.map(Asset.remote)); allAssets.addAll(assets.map(Asset.remote));
if (assets.length != chunkSize) break; if (assets.length != chunkSize) break;
lastId = assets.last.id; lastId = assets.last.id;
if (firstSync) {
// first sync only loads the first chunk
break;
}
} }
return allAssets; return allAssets;
} catch (error, stack) { } catch (error, stack) {
@@ -1,5 +1,4 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; 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/entities/exif_info.entity.dart';
import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart';
@@ -13,36 +12,46 @@ class AssetDescriptionService {
final Isar _db; final Isar _db;
final ApiService _api; final ApiService _api;
Future<void> setDescription( setDescription(
Asset asset, String description,
String newDescription, String remoteAssetId,
int localExifId,
) async { ) 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( final result = await _api.assetsApi.updateAsset(
remoteAssetId, remoteAssetId,
UpdateAssetDto(description: newDescription), UpdateAssetDto(description: description),
); );
final description = result?.exifInfo?.description; if (result?.exifInfo?.description != null) {
if (description != null) {
var exifInfo = await _db.exifInfos.get(localExifId); var exifInfo = await _db.exifInfos.get(localExifId);
if (exifInfo != null) { if (exifInfo != null) {
exifInfo.description = description; exifInfo.description = result!.exifInfo!.description;
await _db.writeTxn( await _db.writeTxn(
() => _db.exifInfos.put(exifInfo), () => _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( 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:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/models/memories/memory.model.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:logging/logging.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
import '../utils/string_helper.dart';
final memoryServiceProvider = StateProvider<MemoryService>((ref) { final memoryServiceProvider = StateProvider<MemoryService>((ref) {
return MemoryService( return MemoryService(
ref.watch(apiServiceProvider), ref.watch(apiServiceProvider),
@@ -41,12 +42,9 @@ class MemoryService {
final dbAssets = final dbAssets =
await _db.assets.getAllByRemoteId(assets.map((e) => e.id)); await _db.assets.getAllByRemoteId(assets.map((e) => e.id));
if (dbAssets.isNotEmpty) { if (dbAssets.isNotEmpty) {
final String title = yearsAgo <= 1
? 'memories_year_ago'.tr()
: 'memories_years_ago'.tr(args: [yearsAgo.toString()]);
memories.add( memories.add(
Memory( Memory(
title: title, title: '$yearsAgo year${s(yearsAgo)} ago',
assets: dbAssets, assets: dbAssets,
), ),
); );
+15 -7
View File
@@ -46,14 +46,18 @@ class SyncService {
List<User> users, List<User> users,
DateTime since, DateTime since,
) getChangedAssets, ) getChangedAssets,
required FutureOr<List<Asset>?> Function(User user, DateTime until) required FutureOr<List<Asset>?> Function(
loadAssets, User user,
DateTime until,
bool firstSync,
) loadAssets,
required FutureOr<List<User>?> Function() refreshUsers, required FutureOr<List<User>?> Function() refreshUsers,
required bool firstSync,
}) => }) =>
_lock.run( _lock.run(
() async => () async =>
await _syncRemoteAssetChanges(users, getChangedAssets) ?? await _syncRemoteAssetChanges(users, getChangedAssets) ??
await _syncRemoteAssetsFull(refreshUsers, loadAssets), await _syncRemoteAssetsFull(refreshUsers, loadAssets, firstSync),
); );
/// Syncs remote albums to the database /// Syncs remote albums to the database
@@ -212,7 +216,9 @@ class SyncService {
/// Syncs assets by loading and comparing all assets from the server. /// Syncs assets by loading and comparing all assets from the server.
Future<bool> _syncRemoteAssetsFull( Future<bool> _syncRemoteAssetsFull(
FutureOr<List<User>?> Function() refreshUsers, FutureOr<List<User>?> Function() refreshUsers,
FutureOr<List<Asset>?> Function(User user, DateTime until) loadAssets, FutureOr<List<Asset>?> Function(User user, DateTime until, bool firstSync)
loadAssets,
bool firstSync,
) async { ) async {
final serverUsers = await refreshUsers(); final serverUsers = await refreshUsers();
if (serverUsers == null) { if (serverUsers == null) {
@@ -228,17 +234,19 @@ class SyncService {
.findAll(); .findAll();
bool changes = false; bool changes = false;
for (User u in users) { for (User u in users) {
changes |= await _syncRemoteAssetsForUser(u, loadAssets); changes |= await _syncRemoteAssetsForUser(u, loadAssets, firstSync);
} }
return changes; return changes;
} }
Future<bool> _syncRemoteAssetsForUser( Future<bool> _syncRemoteAssetsForUser(
User user, User user,
FutureOr<List<Asset>?> Function(User user, DateTime until) loadAssets, FutureOr<List<Asset>?> Function(User user, DateTime until, bool firstSync)
loadAssets,
bool firstSync,
) async { ) async {
final DateTime now = DateTime.now().toUtc(); final DateTime now = DateTime.now().toUtc();
final List<Asset>? remote = await loadAssets(user, now); final List<Asset>? remote = await loadAssets(user, now, firstSync);
if (remote == null) { if (remote == null) {
return false; return false;
} }
@@ -2,11 +2,10 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.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/entities/asset.entity.dart';
import 'package:immich_mobile/providers/user.provider.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:immich_mobile/widgets/common/immich_toast.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@@ -14,11 +13,9 @@ class DescriptionInput extends HookConsumerWidget {
DescriptionInput({ DescriptionInput({
super.key, super.key,
required this.asset, required this.asset,
this.exifInfo,
}); });
final Asset asset; final Asset asset;
final ExifInfo? exifInfo;
final Logger _log = Logger('DescriptionInput'); final Logger _log = Logger('DescriptionInput');
@override @override
@@ -28,25 +25,25 @@ class DescriptionInput extends HookConsumerWidget {
final focusNode = useFocusNode(); final focusNode = useFocusNode();
final isFocus = useState(false); final isFocus = useState(false);
final isTextEmpty = useState(controller.text.isEmpty); 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 owner = ref.watch(currentUserProvider);
final hasError = useState(false); final hasError = useState(false);
useEffect( useEffect(
() { () {
controller.text = exifInfo?.description ?? ''; controller.text = description;
isTextEmpty.value = exifInfo?.description?.isEmpty ?? true; isTextEmpty.value = description.isEmpty;
return null; return null;
}, },
[exifInfo?.description], [description],
); );
submitDescription(String description) async { submitDescription(String description) async {
hasError.value = false; hasError.value = false;
try { try {
await descriptionProvider.setDescription( await descriptionProvider.setDescription(
asset,
description, description,
); );
} catch (error, stack) { } catch (error, stack) {
@@ -88,7 +85,7 @@ class DescriptionInput extends HookConsumerWidget {
isFocus.value = false; isFocus.value = false;
focusNode.unfocus(); focusNode.unfocus();
if (exifInfo?.description != controller.text) { if (description != controller.text) {
await submitDescription(controller.text); await submitDescription(controller.text);
} }
}, },
@@ -73,8 +73,7 @@ class ExifBottomSheet extends HookConsumerWidget {
child: Column( child: Column(
children: [ children: [
dateWidget, dateWidget,
if (asset.isRemote) if (asset.isRemote) DescriptionInput(asset: asset),
DescriptionInput(asset: asset, exifInfo: exifInfo),
], ],
), ),
), ),
@@ -133,8 +132,7 @@ class ExifBottomSheet extends HookConsumerWidget {
child: Column( child: Column(
children: [ children: [
dateWidget, dateWidget,
if (asset.isRemote) if (asset.isRemote) DescriptionInput(asset: asset),
DescriptionInput(asset: asset, exifInfo: exifInfo),
Padding( Padding(
padding: EdgeInsets.only(top: asset.isRemote ? 0 : 16.0), padding: EdgeInsets.only(top: asset.isRemote ? 0 : 16.0),
child: ExifLocation( child: ExifLocation(
@@ -2,12 +2,11 @@ import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/search/search_curated_content.model.dart'; import 'package:immich_mobile/models/search/search_curated_content.model.dart';
import 'package:immich_mobile/widgets/search/thumbnail_with_info.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:immich_mobile/utils/image_url_builder.dart';
class CuratedPeopleRow extends StatelessWidget { class CuratedPeopleRow extends StatelessWidget {
static const double imageSize = 60.0;
final List<SearchCuratedContent> content; final List<SearchCuratedContent> content;
final EdgeInsets? padding; final EdgeInsets? padding;
@@ -25,68 +24,88 @@ class CuratedPeopleRow extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( const imageSize = 60.0;
height: imageSize + 30,
child: ListView.separated( // Guard empty [content]
padding: padding, if (content.isEmpty) {
scrollDirection: Axis.horizontal, // Return empty thumbnail
separatorBuilder: (context, index) => const SizedBox(width: 16), return Align(
itemBuilder: (context, index) { alignment: Alignment.centerLeft,
final person = content[index]; child: Padding(
final headers = { padding: const EdgeInsets.symmetric(horizontal: 16.0),
"x-immich-user-token": Store.get(StoreKey.accessToken), child: SizedBox(
}; width: imageSize,
return Column( height: imageSize,
crossAxisAlignment: CrossAxisAlignment.center, child: ThumbnailWithInfo(
children: [ textInfo: '',
GestureDetector( onTap: () {},
onTap: () => onTap?.call(person, index), ),
child: SizedBox( ),
height: imageSize, ),
child: Material( );
shape: const CircleBorder(side: BorderSide.none), }
elevation: 3,
child: CircleAvatar( return ListView.builder(
maxRadius: imageSize / 2, padding: padding,
backgroundImage: NetworkImage( scrollDirection: Axis.horizontal,
getFaceThumbnailUrl(person.id), itemBuilder: (context, index) {
headers: headers, final person = content[index];
final headers = {
"x-immich-user-token": Store.get(StoreKey.accessToken),
};
return Padding(
padding: const EdgeInsets.only(right: 18.0),
child: SizedBox(
width: imageSize,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
GestureDetector(
onTap: () => onTap?.call(person, index),
child: SizedBox(
height: imageSize,
child: Material(
shape: const CircleBorder(side: BorderSide.none),
elevation: 3,
child: CircleAvatar(
maxRadius: imageSize / 2,
backgroundImage: NetworkImage(
getFaceThumbnailUrl(person.id),
headers: headers,
),
), ),
), ),
), ),
), ),
), if (person.label == "")
const SizedBox(height: 8), GestureDetector(
_buildPersonLabel(context, person, index), onTap: () => onNameTap?.call(person, index),
], child: Padding(
); padding: const EdgeInsets.only(top: 8.0),
}, child: Text(
itemCount: content.length, "exif_bottom_sheet_person_add_person",
), style: context.textTheme.labelLarge?.copyWith(
); color: context.primaryColor,
} ),
).tr(),
Widget _buildPersonLabel( ),
BuildContext context, )
SearchCuratedContent person, else
int index, Padding(
) { padding: const EdgeInsets.only(top: 8.0),
if (person.label.isEmpty) { child: Text(
return GestureDetector( person.label,
onTap: () => onNameTap?.call(person, index), textAlign: TextAlign.center,
child: Text( overflow: TextOverflow.ellipsis,
"exif_bottom_sheet_person_add_person", style: context.textTheme.labelLarge,
style: context.textTheme.labelLarge?.copyWith( ),
color: context.primaryColor, ),
],
),
), ),
).tr(), );
); },
} itemCount: content.length,
return Text(
person.label,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
style: context.textTheme.labelLarge,
); );
} }
} }
+113 -42
View File
@@ -1,64 +1,135 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/models/search/search_curated_content.model.dart'; import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
import 'package:immich_mobile/widgets/search/search_map_thumbnail.dart'; import 'package:immich_mobile/widgets/search/curated_row.dart';
import 'package:immich_mobile/widgets/search/thumbnail_with_info.dart'; import 'package:immich_mobile/widgets/search/thumbnail_with_info.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
class CuratedPlacesRow extends CuratedRow {
final bool isMapEnabled;
class CuratedPlacesRow extends StatelessWidget {
const CuratedPlacesRow({ const CuratedPlacesRow({
super.key, super.key,
required this.content, required super.content,
required this.imageSize,
this.isMapEnabled = true, this.isMapEnabled = true,
this.onTap, super.imageSize,
super.onTap,
}); });
final bool isMapEnabled;
final List<SearchCuratedContent> content;
final double imageSize;
/// Callback with the content and the index when tapped
final Function(SearchCuratedContent, int)? onTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Calculating the actual index of the content based on the whether map is enabled or not. // Calculating the actual index of the content based on the whether map is enabled or not.
// If enabled, inject map as the first item in the list (index 0) and so the actual content will start from index 1 // If enabled, inject map as the first item in the list (index 0) and so the actual content will start from index 1
final int actualContentIndex = isMapEnabled ? 1 : 0; final int actualContentIndex = isMapEnabled ? 1 : 0;
Widget buildMapThumbnail() {
return SizedBox( return GestureDetector(
height: imageSize, onTap: () => context.pushRoute(
child: ListView.builder( const MapRoute(),
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(
horizontal: 16,
), ),
itemBuilder: (context, index) { child: SizedBox.square(
// Injecting Map thumbnail as the first element dimension: imageSize,
if (isMapEnabled && index == 0) { child: Stack(
return SearchMapThumbnail( children: [
size: imageSize, Padding(
); padding: const EdgeInsets.only(right: 10.0),
} child: MapThumbnail(
final actualIndex = index - actualContentIndex; zoom: 2,
final object = content[actualIndex]; centre: const LatLng(
final thumbnailRequestUrl = 47,
'${Store.get(StoreKey.serverEndpoint)}/assets/${object.id}/thumbnail'; 5,
return SizedBox( ),
height: imageSize,
width: imageSize,
showAttribution: false,
),
),
Padding(
padding: const EdgeInsets.only(right: 10.0),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.black,
gradient: LinearGradient(
begin: FractionalOffset.topCenter,
end: FractionalOffset.bottomCenter,
colors: [
Colors.blueGrey.withOpacity(0.0),
Colors.black.withOpacity(0.4),
],
stops: const [0.0, 0.4],
),
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.only(bottom: 10),
child: const Text(
"search_page_your_map",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14,
),
).tr(),
),
),
],
),
),
);
}
// Return empty thumbnail
if (!isMapEnabled && content.isEmpty) {
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: SizedBox(
width: imageSize, width: imageSize,
height: imageSize, height: imageSize,
child: Padding( child: ThumbnailWithInfo(
padding: const EdgeInsets.only(right: 10.0), textInfo: '',
child: ThumbnailWithInfo( onTap: () {},
imageUrl: thumbnailRequestUrl,
textInfo: object.label,
onTap: () => onTap?.call(object, actualIndex),
),
), ),
); ),
}, ),
itemCount: content.length + actualContentIndex, );
}
return ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(
horizontal: 16,
), ),
itemBuilder: (context, index) {
// Injecting Map thumbnail as the first element
if (isMapEnabled && index == 0) {
return buildMapThumbnail();
}
final actualIndex = index - actualContentIndex;
final object = content[actualIndex];
final thumbnailRequestUrl =
'${Store.get(StoreKey.serverEndpoint)}/assets/${object.id}/thumbnail';
return SizedBox(
width: imageSize,
height: imageSize,
child: Padding(
padding: const EdgeInsets.only(right: 10.0),
child: ThumbnailWithInfo(
imageUrl: thumbnailRequestUrl,
textInfo: object.label,
onTap: () => onTap?.call(object, actualIndex),
),
),
);
},
itemCount: content.length + actualContentIndex,
); );
} }
} }
@@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/models/search/search_curated_content.model.dart';
import 'package:immich_mobile/widgets/search/thumbnail_with_info.dart';
import 'package:immich_mobile/entities/store.entity.dart';
class CuratedRow extends StatelessWidget {
final List<SearchCuratedContent> content;
final double imageSize;
/// Callback with the content and the index when tapped
final Function(SearchCuratedContent, int)? onTap;
const CuratedRow({
super.key,
required this.content,
this.imageSize = 200,
this.onTap,
});
@override
Widget build(BuildContext context) {
// Guard empty [content]
if (content.isEmpty) {
// Return empty thumbnail
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: SizedBox(
width: imageSize,
height: imageSize,
child: ThumbnailWithInfo(
textInfo: '',
onTap: () {},
),
),
),
);
}
return ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(
horizontal: 16,
),
itemBuilder: (context, index) {
final object = content[index];
final thumbnailRequestUrl =
'${Store.get(StoreKey.serverEndpoint)}/assets/${object.id}/thumbnail';
return SizedBox(
width: imageSize,
height: imageSize,
child: Padding(
padding: const EdgeInsets.only(right: 4.0),
child: ThumbnailWithInfo(
imageUrl: thumbnailRequestUrl,
textInfo: object.label,
onTap: () => onTap?.call(object, index),
),
),
);
},
itemCount: content.length,
);
}
}
@@ -5,7 +5,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart';
import 'package:immich_mobile/providers/search/search_filter.provider.dart'; import 'package:immich_mobile/providers/search/search_filter.provider.dart';
import 'package:immich_mobile/widgets/search/search_filter/common/dropdown.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
class CameraPicker extends HookConsumerWidget { class CameraPicker extends HookConsumerWidget {
@@ -13,7 +12,6 @@ class CameraPicker extends HookConsumerWidget {
final Function(Map<String, String?>) onSelect; final Function(Map<String, String?>) onSelect;
final SearchCameraFilter? filter; final SearchCameraFilter? filter;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final makeTextController = useTextEditingController(text: filter?.make); final makeTextController = useTextEditingController(text: filter?.make);
@@ -34,73 +32,90 @@ class CameraPicker extends HookConsumerWidget {
), ),
); );
final makeWidget = SearchDropdown( final inputDecorationTheme = InputDecorationTheme(
dropdownMenuEntries: switch (make) { border: OutlineInputBorder(
AsyncError() => [], borderRadius: BorderRadius.circular(20),
AsyncData(:final value) => value ),
.map( contentPadding: const EdgeInsets.only(left: 16),
(e) => DropdownMenuEntry(
value: e,
label: e,
),
)
.toList(),
_ => [],
},
label: const Text('search_filter_camera_make').tr(),
controller: makeTextController,
leadingIcon: const Icon(Icons.photo_camera_rounded),
onSelected: (value) {
selectedMake.value = value.toString();
onSelect({
'make': selectedMake.value,
'model': selectedModel.value,
});
},
); );
final modelWidget = SearchDropdown( final menuStyle = MenuStyle(
dropdownMenuEntries: switch (models) { shape: WidgetStatePropertyAll<OutlinedBorder>(
AsyncError() => [], RoundedRectangleBorder(
AsyncData(:final value) => value borderRadius: BorderRadius.circular(15),
.map( ),
(e) => DropdownMenuEntry( ),
value: e,
label: e,
),
)
.toList(),
_ => [],
},
label: const Text('search_filter_camera_model').tr(),
controller: modelTextController,
leadingIcon: const Icon(Icons.camera),
onSelected: (value) {
selectedModel.value = value.toString();
onSelect({
'make': selectedMake.value,
'model': selectedModel.value,
});
},
); );
if (context.isMobile) { return Container(
return Column( padding: const EdgeInsets.only(
// bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
makeWidget, DropdownMenu(
const SizedBox(height: 8), dropdownMenuEntries: switch (make) {
modelWidget, AsyncError() => [],
AsyncData(:final value) => value
.map(
(e) => DropdownMenuEntry(
value: e,
label: e,
),
)
.toList(),
_ => [],
},
width: context.width * 0.45,
menuHeight: 400,
label: const Text('search_filter_camera_make').tr(),
inputDecorationTheme: inputDecorationTheme,
controller: makeTextController,
menuStyle: menuStyle,
leadingIcon: const Icon(Icons.photo_camera_rounded),
trailingIcon: const Icon(Icons.arrow_drop_down_rounded),
selectedTrailingIcon: const Icon(Icons.arrow_drop_up_rounded),
onSelected: (value) {
selectedMake.value = value.toString();
onSelect({
'make': selectedMake.value,
'model': selectedModel.value,
});
},
),
DropdownMenu(
dropdownMenuEntries: switch (models) {
AsyncError() => [],
AsyncData(:final value) => value
.map(
(e) => DropdownMenuEntry(
value: e,
label: e,
),
)
.toList(),
_ => [],
},
width: context.width * 0.45,
menuHeight: 400,
label: const Text('search_filter_camera_model').tr(),
inputDecorationTheme: inputDecorationTheme,
controller: modelTextController,
menuStyle: menuStyle,
leadingIcon: const Icon(Icons.camera),
trailingIcon: const Icon(Icons.arrow_drop_down_rounded),
selectedTrailingIcon: const Icon(Icons.arrow_drop_up_rounded),
onSelected: (value) {
selectedModel.value = value.toString();
onSelect({
'make': selectedMake.value,
'model': selectedModel.value,
});
},
),
], ],
); ),
}
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(child: makeWidget),
const SizedBox(width: 16),
Expanded(child: modelWidget),
],
); );
} }
} }
@@ -1,52 +0,0 @@
import 'package:flutter/material.dart';
class SearchDropdown<T> extends StatelessWidget {
const SearchDropdown({
super.key,
required this.dropdownMenuEntries,
required this.controller,
this.onSelected,
this.label,
this.leadingIcon,
});
final List<DropdownMenuEntry<T>> dropdownMenuEntries;
final TextEditingController controller;
final void Function(T?)? onSelected;
final Widget? label;
final Widget? leadingIcon;
@override
Widget build(BuildContext context) {
final inputDecorationTheme = InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
),
contentPadding: const EdgeInsets.only(left: 16),
);
final menuStyle = MenuStyle(
shape: WidgetStatePropertyAll<OutlinedBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
);
return LayoutBuilder(
builder: (context, constraints) {
return DropdownMenu(
leadingIcon: leadingIcon,
width: constraints.maxWidth,
dropdownMenuEntries: dropdownMenuEntries,
label: label,
inputDecorationTheme: inputDecorationTheme,
menuStyle: menuStyle,
trailingIcon: const Icon(Icons.arrow_drop_down_rounded),
selectedTrailingIcon: const Icon(Icons.arrow_drop_up_rounded),
onSelected: onSelected,
);
},
);
}
}
@@ -38,10 +38,7 @@ class FilterBottomSheetScaffold extends StatelessWidget {
style: context.textTheme.headlineSmall, style: context.textTheme.headlineSmall,
), ),
), ),
Padding( buildChildWidget(),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: buildChildWidget(),
),
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Row( child: Row(
@@ -2,9 +2,9 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart';
import 'package:immich_mobile/providers/search/search_filter.provider.dart'; import 'package:immich_mobile/providers/search/search_filter.provider.dart';
import 'package:immich_mobile/widgets/search/search_filter/common/dropdown.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
class LocationPicker extends HookConsumerWidget { class LocationPicker extends HookConsumerWidget {
@@ -48,9 +48,24 @@ class LocationPicker extends HookConsumerWidget {
), ),
); );
final inputDecorationTheme = InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
),
contentPadding: const EdgeInsets.only(left: 16),
);
final menuStyle = MenuStyle(
shape: WidgetStatePropertyAll<OutlinedBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
);
return Column( return Column(
children: [ children: [
SearchDropdown( DropdownMenu(
dropdownMenuEntries: switch (countries) { dropdownMenuEntries: switch (countries) {
AsyncError() => [], AsyncError() => [],
AsyncData(:final value) => value AsyncData(:final value) => value
@@ -63,8 +78,14 @@ class LocationPicker extends HookConsumerWidget {
.toList(), .toList(),
_ => [], _ => [],
}, },
menuHeight: 400,
width: context.width * 0.9,
label: const Text('search_filter_location_country').tr(), label: const Text('search_filter_location_country').tr(),
inputDecorationTheme: inputDecorationTheme,
menuStyle: menuStyle,
controller: countryTextController, controller: countryTextController,
trailingIcon: const Icon(Icons.arrow_drop_down_rounded),
selectedTrailingIcon: const Icon(Icons.arrow_drop_up_rounded),
onSelected: (value) { onSelected: (value) {
if (value.toString() == selectedCountry.value) { if (value.toString() == selectedCountry.value) {
return; return;
@@ -82,7 +103,7 @@ class LocationPicker extends HookConsumerWidget {
const SizedBox( const SizedBox(
height: 16, height: 16,
), ),
SearchDropdown( DropdownMenu(
dropdownMenuEntries: switch (states) { dropdownMenuEntries: switch (states) {
AsyncError() => [], AsyncError() => [],
AsyncData(:final value) => value AsyncData(:final value) => value
@@ -95,8 +116,14 @@ class LocationPicker extends HookConsumerWidget {
.toList(), .toList(),
_ => [], _ => [],
}, },
menuHeight: 400,
width: context.width * 0.9,
label: const Text('search_filter_location_state').tr(), label: const Text('search_filter_location_state').tr(),
inputDecorationTheme: inputDecorationTheme,
menuStyle: menuStyle,
controller: stateTextController, controller: stateTextController,
trailingIcon: const Icon(Icons.arrow_drop_down_rounded),
selectedTrailingIcon: const Icon(Icons.arrow_drop_up_rounded),
onSelected: (value) { onSelected: (value) {
if (value.toString() == selectedState.value) { if (value.toString() == selectedState.value) {
return; return;
@@ -113,7 +140,7 @@ class LocationPicker extends HookConsumerWidget {
const SizedBox( const SizedBox(
height: 16, height: 16,
), ),
SearchDropdown( DropdownMenu(
dropdownMenuEntries: switch (cities) { dropdownMenuEntries: switch (cities) {
AsyncError() => [], AsyncError() => [],
AsyncData(:final value) => value AsyncData(:final value) => value
@@ -126,8 +153,14 @@ class LocationPicker extends HookConsumerWidget {
.toList(), .toList(),
_ => [], _ => [],
}, },
menuHeight: 400,
width: context.width * 0.9,
label: const Text('search_filter_location_city').tr(), label: const Text('search_filter_location_city').tr(),
inputDecorationTheme: inputDecorationTheme,
menuStyle: menuStyle,
controller: cityTextController, controller: cityTextController,
trailingIcon: const Icon(Icons.arrow_drop_down_rounded),
selectedTrailingIcon: const Icon(Icons.arrow_drop_up_rounded),
onSelected: (value) { onSelected: (value) {
selectedCity.value = value.toString(); selectedCity.value = value.toString();
onSelected({ onSelected({
@@ -1,76 +0,0 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
class SearchMapThumbnail extends StatelessWidget {
const SearchMapThumbnail({
super.key,
this.size = 60.0,
});
final double size;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => context.pushRoute(
const MapRoute(),
),
child: SizedBox.square(
dimension: size,
child: Stack(
children: [
Padding(
padding: const EdgeInsets.only(right: 10.0),
child: MapThumbnail(
zoom: 2,
centre: const LatLng(
47,
5,
),
height: size,
width: size,
showAttribution: false,
),
),
Padding(
padding: const EdgeInsets.only(right: 10.0),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.black,
gradient: LinearGradient(
begin: FractionalOffset.topCenter,
end: FractionalOffset.bottomCenter,
colors: [
Colors.blueGrey.withOpacity(0.0),
Colors.black.withOpacity(0.4),
],
stops: const [0.0, 0.4],
),
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.only(bottom: 10),
child: const Text(
"search_page_your_map",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14,
),
).tr(),
),
),
],
),
),
);
}
}
@@ -1,37 +0,0 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/widgets/search/search_row_title.dart';
class SearchRowSection extends StatelessWidget {
const SearchRowSection({
super.key,
required this.onViewAllPressed,
required this.title,
this.isEmpty = false,
required this.child,
});
final Function() onViewAllPressed;
final String title;
final bool isEmpty;
final Widget child;
@override
Widget build(BuildContext context) {
if (isEmpty) {
return const SizedBox.shrink();
}
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SearchRowTitle(
onViewAllPressed: onViewAllPressed,
title: title,
),
),
child,
],
);
}
}
+30 -21
View File
@@ -3,36 +3,45 @@ import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
class SearchRowTitle extends StatelessWidget { class SearchRowTitle extends StatelessWidget {
final Function() onViewAllPressed;
final String title;
final double top;
const SearchRowTitle({ const SearchRowTitle({
super.key, super.key,
required this.onViewAllPressed, required this.onViewAllPressed,
required this.title, required this.title,
this.top = 12,
}); });
final Function() onViewAllPressed;
final String title;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Padding(
mainAxisAlignment: MainAxisAlignment.spaceBetween, padding: EdgeInsets.only(
children: [ left: 16.0,
Text( right: 16.0,
title, top: top,
style: context.textTheme.bodyLarge?.copyWith( ),
fontWeight: FontWeight.w500, child: Row(
), mainAxisAlignment: MainAxisAlignment.spaceBetween,
), children: [
TextButton( Text(
onPressed: onViewAllPressed, title,
child: Text( style: context.textTheme.bodyLarge?.copyWith(
'search_page_view_all_button', fontWeight: FontWeight.w500,
style: context.textTheme.labelLarge?.copyWith(
color: context.primaryColor,
), ),
).tr(), ),
), TextButton(
], onPressed: onViewAllPressed,
child: Text(
'search_page_view_all_button',
style: context.textTheme.labelLarge?.copyWith(
color: context.primaryColor,
),
).tr(),
),
],
),
); );
} }
} }
+1 -3
View File
@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.106.4 - API version: 1.106.3
- Generator version: 7.5.0 - Generator version: 7.5.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen - Build package: org.openapitools.codegen.languages.DartClientCodegen
@@ -291,9 +291,7 @@ Class | Method | HTTP request | Description
- [CreateTagDto](doc//CreateTagDto.md) - [CreateTagDto](doc//CreateTagDto.md)
- [DownloadArchiveInfo](doc//DownloadArchiveInfo.md) - [DownloadArchiveInfo](doc//DownloadArchiveInfo.md)
- [DownloadInfoDto](doc//DownloadInfoDto.md) - [DownloadInfoDto](doc//DownloadInfoDto.md)
- [DownloadResponse](doc//DownloadResponse.md)
- [DownloadResponseDto](doc//DownloadResponseDto.md) - [DownloadResponseDto](doc//DownloadResponseDto.md)
- [DownloadUpdate](doc//DownloadUpdate.md)
- [DuplicateDetectionConfig](doc//DuplicateDetectionConfig.md) - [DuplicateDetectionConfig](doc//DuplicateDetectionConfig.md)
- [DuplicateResponseDto](doc//DuplicateResponseDto.md) - [DuplicateResponseDto](doc//DuplicateResponseDto.md)
- [EmailNotificationsResponse](doc//EmailNotificationsResponse.md) - [EmailNotificationsResponse](doc//EmailNotificationsResponse.md)
-2
View File
@@ -118,9 +118,7 @@ part 'model/create_profile_image_response_dto.dart';
part 'model/create_tag_dto.dart'; part 'model/create_tag_dto.dart';
part 'model/download_archive_info.dart'; part 'model/download_archive_info.dart';
part 'model/download_info_dto.dart'; part 'model/download_info_dto.dart';
part 'model/download_response.dart';
part 'model/download_response_dto.dart'; part 'model/download_response_dto.dart';
part 'model/download_update.dart';
part 'model/duplicate_detection_config.dart'; part 'model/duplicate_detection_config.dart';
part 'model/duplicate_response_dto.dart'; part 'model/duplicate_response_dto.dart';
part 'model/email_notifications_response.dart'; part 'model/email_notifications_response.dart';
-4
View File
@@ -298,12 +298,8 @@ class ApiClient {
return DownloadArchiveInfo.fromJson(value); return DownloadArchiveInfo.fromJson(value);
case 'DownloadInfoDto': case 'DownloadInfoDto':
return DownloadInfoDto.fromJson(value); return DownloadInfoDto.fromJson(value);
case 'DownloadResponse':
return DownloadResponse.fromJson(value);
case 'DownloadResponseDto': case 'DownloadResponseDto':
return DownloadResponseDto.fromJson(value); return DownloadResponseDto.fromJson(value);
case 'DownloadUpdate':
return DownloadUpdate.fromJson(value);
case 'DuplicateDetectionConfig': case 'DuplicateDetectionConfig':
return DuplicateDetectionConfig.fromJson(value); return DuplicateDetectionConfig.fromJson(value);
case 'DuplicateResponseDto': case 'DuplicateResponseDto':
+1 -18
View File
@@ -31,7 +31,6 @@ class AssetResponseDto {
this.livePhotoVideoId, this.livePhotoVideoId,
required this.localDateTime, required this.localDateTime,
required this.originalFileName, required this.originalFileName,
this.originalMimeType,
required this.originalPath, required this.originalPath,
this.owner, this.owner,
required this.ownerId, required this.ownerId,
@@ -92,14 +91,6 @@ class AssetResponseDto {
String originalFileName; String originalFileName;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? originalMimeType;
String originalPath; String originalPath;
/// ///
@@ -160,7 +151,6 @@ class AssetResponseDto {
other.livePhotoVideoId == livePhotoVideoId && other.livePhotoVideoId == livePhotoVideoId &&
other.localDateTime == localDateTime && other.localDateTime == localDateTime &&
other.originalFileName == originalFileName && other.originalFileName == originalFileName &&
other.originalMimeType == originalMimeType &&
other.originalPath == originalPath && other.originalPath == originalPath &&
other.owner == owner && other.owner == owner &&
other.ownerId == ownerId && other.ownerId == ownerId &&
@@ -197,7 +187,6 @@ class AssetResponseDto {
(livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) + (livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
(localDateTime.hashCode) + (localDateTime.hashCode) +
(originalFileName.hashCode) + (originalFileName.hashCode) +
(originalMimeType == null ? 0 : originalMimeType!.hashCode) +
(originalPath.hashCode) + (originalPath.hashCode) +
(owner == null ? 0 : owner!.hashCode) + (owner == null ? 0 : owner!.hashCode) +
(ownerId.hashCode) + (ownerId.hashCode) +
@@ -214,7 +203,7 @@ class AssetResponseDto {
(updatedAt.hashCode); (updatedAt.hashCode);
@override @override
String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, stack=$stack, stackCount=$stackCount, stackParentId=$stackParentId, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt]'; String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, stack=$stack, stackCount=$stackCount, stackParentId=$stackParentId, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@@ -252,11 +241,6 @@ class AssetResponseDto {
} }
json[r'localDateTime'] = this.localDateTime.toUtc().toIso8601String(); json[r'localDateTime'] = this.localDateTime.toUtc().toIso8601String();
json[r'originalFileName'] = this.originalFileName; json[r'originalFileName'] = this.originalFileName;
if (this.originalMimeType != null) {
json[r'originalMimeType'] = this.originalMimeType;
} else {
// json[r'originalMimeType'] = null;
}
json[r'originalPath'] = this.originalPath; json[r'originalPath'] = this.originalPath;
if (this.owner != null) { if (this.owner != null) {
json[r'owner'] = this.owner; json[r'owner'] = this.owner;
@@ -320,7 +304,6 @@ class AssetResponseDto {
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'), livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
localDateTime: mapDateTime(json, r'localDateTime', r'')!, localDateTime: mapDateTime(json, r'localDateTime', r'')!,
originalFileName: mapValueOfType<String>(json, r'originalFileName')!, originalFileName: mapValueOfType<String>(json, r'originalFileName')!,
originalMimeType: mapValueOfType<String>(json, r'originalMimeType'),
originalPath: mapValueOfType<String>(json, r'originalPath')!, originalPath: mapValueOfType<String>(json, r'originalPath')!,
owner: UserResponseDto.fromJson(json[r'owner']), owner: UserResponseDto.fromJson(json[r'owner']),
ownerId: mapValueOfType<String>(json, r'ownerId')!, ownerId: mapValueOfType<String>(json, r'ownerId')!,
-98
View File
@@ -1,98 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class DownloadResponse {
/// Returns a new [DownloadResponse] instance.
DownloadResponse({
required this.archiveSize,
});
int archiveSize;
@override
bool operator ==(Object other) => identical(this, other) || other is DownloadResponse &&
other.archiveSize == archiveSize;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(archiveSize.hashCode);
@override
String toString() => 'DownloadResponse[archiveSize=$archiveSize]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'archiveSize'] = this.archiveSize;
return json;
}
/// Returns a new [DownloadResponse] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static DownloadResponse? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return DownloadResponse(
archiveSize: mapValueOfType<int>(json, r'archiveSize')!,
);
}
return null;
}
static List<DownloadResponse> listFromJson(dynamic json, {bool growable = false,}) {
final result = <DownloadResponse>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = DownloadResponse.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, DownloadResponse> mapFromJson(dynamic json) {
final map = <String, DownloadResponse>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = DownloadResponse.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of DownloadResponse-objects as value to a dart map
static Map<String, List<DownloadResponse>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<DownloadResponse>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = DownloadResponse.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'archiveSize',
};
}
-108
View File
@@ -1,108 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class DownloadUpdate {
/// Returns a new [DownloadUpdate] instance.
DownloadUpdate({
this.archiveSize,
});
/// Minimum value: 1
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
int? archiveSize;
@override
bool operator ==(Object other) => identical(this, other) || other is DownloadUpdate &&
other.archiveSize == archiveSize;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(archiveSize == null ? 0 : archiveSize!.hashCode);
@override
String toString() => 'DownloadUpdate[archiveSize=$archiveSize]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.archiveSize != null) {
json[r'archiveSize'] = this.archiveSize;
} else {
// json[r'archiveSize'] = null;
}
return json;
}
/// Returns a new [DownloadUpdate] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static DownloadUpdate? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return DownloadUpdate(
archiveSize: mapValueOfType<int>(json, r'archiveSize'),
);
}
return null;
}
static List<DownloadUpdate> listFromJson(dynamic json, {bool growable = false,}) {
final result = <DownloadUpdate>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = DownloadUpdate.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, DownloadUpdate> mapFromJson(dynamic json) {
final map = <String, DownloadUpdate>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = DownloadUpdate.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of DownloadUpdate-objects as value to a dart map
static Map<String, List<DownloadUpdate>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<DownloadUpdate>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = DownloadUpdate.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}
+1 -9
View File
@@ -14,15 +14,12 @@ class UserPreferencesResponseDto {
/// Returns a new [UserPreferencesResponseDto] instance. /// Returns a new [UserPreferencesResponseDto] instance.
UserPreferencesResponseDto({ UserPreferencesResponseDto({
required this.avatar, required this.avatar,
required this.download,
required this.emailNotifications, required this.emailNotifications,
required this.memories, required this.memories,
}); });
AvatarResponse avatar; AvatarResponse avatar;
DownloadResponse download;
EmailNotificationsResponse emailNotifications; EmailNotificationsResponse emailNotifications;
MemoryResponse memories; MemoryResponse memories;
@@ -30,7 +27,6 @@ class UserPreferencesResponseDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is UserPreferencesResponseDto && bool operator ==(Object other) => identical(this, other) || other is UserPreferencesResponseDto &&
other.avatar == avatar && other.avatar == avatar &&
other.download == download &&
other.emailNotifications == emailNotifications && other.emailNotifications == emailNotifications &&
other.memories == memories; other.memories == memories;
@@ -38,17 +34,15 @@ class UserPreferencesResponseDto {
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(avatar.hashCode) + (avatar.hashCode) +
(download.hashCode) +
(emailNotifications.hashCode) + (emailNotifications.hashCode) +
(memories.hashCode); (memories.hashCode);
@override @override
String toString() => 'UserPreferencesResponseDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, memories=$memories]'; String toString() => 'UserPreferencesResponseDto[avatar=$avatar, emailNotifications=$emailNotifications, memories=$memories]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'avatar'] = this.avatar; json[r'avatar'] = this.avatar;
json[r'download'] = this.download;
json[r'emailNotifications'] = this.emailNotifications; json[r'emailNotifications'] = this.emailNotifications;
json[r'memories'] = this.memories; json[r'memories'] = this.memories;
return json; return json;
@@ -63,7 +57,6 @@ class UserPreferencesResponseDto {
return UserPreferencesResponseDto( return UserPreferencesResponseDto(
avatar: AvatarResponse.fromJson(json[r'avatar'])!, avatar: AvatarResponse.fromJson(json[r'avatar'])!,
download: DownloadResponse.fromJson(json[r'download'])!,
emailNotifications: EmailNotificationsResponse.fromJson(json[r'emailNotifications'])!, emailNotifications: EmailNotificationsResponse.fromJson(json[r'emailNotifications'])!,
memories: MemoryResponse.fromJson(json[r'memories'])!, memories: MemoryResponse.fromJson(json[r'memories'])!,
); );
@@ -114,7 +107,6 @@ class UserPreferencesResponseDto {
/// The list of required keys that must be present in a JSON. /// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'avatar', 'avatar',
'download',
'emailNotifications', 'emailNotifications',
'memories', 'memories',
}; };
+1 -18
View File
@@ -14,7 +14,6 @@ class UserPreferencesUpdateDto {
/// Returns a new [UserPreferencesUpdateDto] instance. /// Returns a new [UserPreferencesUpdateDto] instance.
UserPreferencesUpdateDto({ UserPreferencesUpdateDto({
this.avatar, this.avatar,
this.download,
this.emailNotifications, this.emailNotifications,
this.memories, this.memories,
}); });
@@ -27,14 +26,6 @@ class UserPreferencesUpdateDto {
/// ///
AvatarUpdate? avatar; AvatarUpdate? avatar;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
DownloadUpdate? download;
/// ///
/// Please note: This property should have been non-nullable! Since the specification file /// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated /// does not include a default value (using the "default:" property), however, the generated
@@ -54,7 +45,6 @@ class UserPreferencesUpdateDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is UserPreferencesUpdateDto && bool operator ==(Object other) => identical(this, other) || other is UserPreferencesUpdateDto &&
other.avatar == avatar && other.avatar == avatar &&
other.download == download &&
other.emailNotifications == emailNotifications && other.emailNotifications == emailNotifications &&
other.memories == memories; other.memories == memories;
@@ -62,12 +52,11 @@ class UserPreferencesUpdateDto {
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(avatar == null ? 0 : avatar!.hashCode) + (avatar == null ? 0 : avatar!.hashCode) +
(download == null ? 0 : download!.hashCode) +
(emailNotifications == null ? 0 : emailNotifications!.hashCode) + (emailNotifications == null ? 0 : emailNotifications!.hashCode) +
(memories == null ? 0 : memories!.hashCode); (memories == null ? 0 : memories!.hashCode);
@override @override
String toString() => 'UserPreferencesUpdateDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, memories=$memories]'; String toString() => 'UserPreferencesUpdateDto[avatar=$avatar, emailNotifications=$emailNotifications, memories=$memories]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@@ -76,11 +65,6 @@ class UserPreferencesUpdateDto {
} else { } else {
// json[r'avatar'] = null; // json[r'avatar'] = null;
} }
if (this.download != null) {
json[r'download'] = this.download;
} else {
// json[r'download'] = null;
}
if (this.emailNotifications != null) { if (this.emailNotifications != null) {
json[r'emailNotifications'] = this.emailNotifications; json[r'emailNotifications'] = this.emailNotifications;
} else { } else {
@@ -103,7 +87,6 @@ class UserPreferencesUpdateDto {
return UserPreferencesUpdateDto( return UserPreferencesUpdateDto(
avatar: AvatarUpdate.fromJson(json[r'avatar']), avatar: AvatarUpdate.fromJson(json[r'avatar']),
download: DownloadUpdate.fromJson(json[r'download']),
emailNotifications: EmailNotificationsUpdate.fromJson(json[r'emailNotifications']), emailNotifications: EmailNotificationsUpdate.fromJson(json[r'emailNotifications']),
memories: MemoryUpdate.fromJson(json[r'memories']), memories: MemoryUpdate.fromJson(json[r'memories']),
); );
+1 -1
View File
@@ -1805,4 +1805,4 @@ packages:
version: "3.1.2" version: "3.1.2"
sdks: sdks:
dart: ">=3.3.0 <4.0.0" dart: ">=3.3.0 <4.0.0"
flutter: ">=3.22.2" flutter: ">=3.22.1"
+2 -2
View File
@@ -2,11 +2,11 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none' publish_to: 'none'
version: 1.106.4+144 version: 1.106.3+143
environment: environment:
sdk: '>=3.3.0 <4.0.0' sdk: '>=3.3.0 <4.0.0'
flutter: 3.22.2 flutter: 3.22.1
dependencies: dependencies:
flutter: flutter:
@@ -78,8 +78,9 @@ void main() {
final bool c1 = await s.syncRemoteAssetsToDb( final bool c1 = await s.syncRemoteAssetsToDb(
users: [owner], users: [owner],
getChangedAssets: _failDiff, getChangedAssets: _failDiff,
loadAssets: (u, d) => remoteAssets, loadAssets: (u, d, firstSync) => remoteAssets,
refreshUsers: () => [owner], refreshUsers: () => [owner],
firstSync: false,
); );
expect(c1, isFalse); expect(c1, isFalse);
expect(db.assets.countSync(), 5); expect(db.assets.countSync(), 5);
@@ -99,8 +100,9 @@ void main() {
final bool c1 = await s.syncRemoteAssetsToDb( final bool c1 = await s.syncRemoteAssetsToDb(
users: [owner], users: [owner],
getChangedAssets: _failDiff, getChangedAssets: _failDiff,
loadAssets: (u, d) => remoteAssets, loadAssets: (u, d, firstSync) => remoteAssets,
refreshUsers: () => [owner], refreshUsers: () => [owner],
firstSync: false,
); );
expect(c1, isTrue); expect(c1, isTrue);
expect(db.assets.countSync(), 7); expect(db.assets.countSync(), 7);
@@ -120,16 +122,18 @@ void main() {
final bool c1 = await s.syncRemoteAssetsToDb( final bool c1 = await s.syncRemoteAssetsToDb(
users: [owner], users: [owner],
getChangedAssets: _failDiff, getChangedAssets: _failDiff,
loadAssets: (u, d) => remoteAssets, loadAssets: (u, d, firstSync) => remoteAssets,
refreshUsers: () => [owner], refreshUsers: () => [owner],
firstSync: false,
); );
expect(c1, isTrue); expect(c1, isTrue);
expect(db.assets.countSync(), 8); expect(db.assets.countSync(), 8);
final bool c2 = await s.syncRemoteAssetsToDb( final bool c2 = await s.syncRemoteAssetsToDb(
users: [owner], users: [owner],
getChangedAssets: _failDiff, getChangedAssets: _failDiff,
loadAssets: (u, d) => remoteAssets, loadAssets: (u, d, firstSync) => remoteAssets,
refreshUsers: () => [owner], refreshUsers: () => [owner],
firstSync: false,
); );
expect(c2, isFalse); expect(c2, isFalse);
expect(db.assets.countSync(), 8); expect(db.assets.countSync(), 8);
@@ -137,8 +141,9 @@ void main() {
final bool c3 = await s.syncRemoteAssetsToDb( final bool c3 = await s.syncRemoteAssetsToDb(
users: [owner], users: [owner],
getChangedAssets: _failDiff, getChangedAssets: _failDiff,
loadAssets: (u, d) => remoteAssets, loadAssets: (u, d, firstSync) => remoteAssets,
refreshUsers: () => [owner], refreshUsers: () => [owner],
firstSync: false,
); );
expect(c3, isTrue); expect(c3, isTrue);
expect(db.assets.countSync(), 7); expect(db.assets.countSync(), 7);
@@ -147,8 +152,9 @@ void main() {
final bool c4 = await s.syncRemoteAssetsToDb( final bool c4 = await s.syncRemoteAssetsToDb(
users: [owner], users: [owner],
getChangedAssets: _failDiff, getChangedAssets: _failDiff,
loadAssets: (u, d) => remoteAssets, loadAssets: (u, d, firstSync) => remoteAssets,
refreshUsers: () => [owner], refreshUsers: () => [owner],
firstSync: false,
); );
expect(c4, isTrue); expect(c4, isTrue);
expect(db.assets.countSync(), 9); expect(db.assets.countSync(), 9);
@@ -166,8 +172,9 @@ void main() {
final bool c = await s.syncRemoteAssetsToDb( final bool c = await s.syncRemoteAssetsToDb(
users: [owner], users: [owner],
getChangedAssets: (user, since) async => (toUpsert, toDelete), getChangedAssets: (user, since) async => (toUpsert, toDelete),
loadAssets: (user, date) => throw Exception(), loadAssets: (user, date, firstSync) => throw Exception(),
refreshUsers: () => throw Exception(), refreshUsers: () => throw Exception(),
firstSync: false,
); );
expect(c, isTrue); expect(c, isTrue);
expect(db.assets.countSync(), 6); expect(db.assets.countSync(), 6);
+1 -31
View File
@@ -6735,7 +6735,7 @@
"info": { "info": {
"title": "Immich", "title": "Immich",
"description": "Immich API", "description": "Immich API",
"version": "1.106.4", "version": "1.106.3",
"contact": {} "contact": {}
}, },
"tags": [], "tags": [],
@@ -7708,9 +7708,6 @@
"originalFileName": { "originalFileName": {
"type": "string" "type": "string"
}, },
"originalMimeType": {
"type": "string"
},
"originalPath": { "originalPath": {
"type": "string" "type": "string"
}, },
@@ -8125,17 +8122,6 @@
}, },
"type": "object" "type": "object"
}, },
"DownloadResponse": {
"properties": {
"archiveSize": {
"type": "integer"
}
},
"required": [
"archiveSize"
],
"type": "object"
},
"DownloadResponseDto": { "DownloadResponseDto": {
"properties": { "properties": {
"archives": { "archives": {
@@ -8154,15 +8140,6 @@
], ],
"type": "object" "type": "object"
}, },
"DownloadUpdate": {
"properties": {
"archiveSize": {
"minimum": 1,
"type": "integer"
}
},
"type": "object"
},
"DuplicateDetectionConfig": { "DuplicateDetectionConfig": {
"properties": { "properties": {
"enabled": { "enabled": {
@@ -11275,9 +11252,6 @@
"avatar": { "avatar": {
"$ref": "#/components/schemas/AvatarResponse" "$ref": "#/components/schemas/AvatarResponse"
}, },
"download": {
"$ref": "#/components/schemas/DownloadResponse"
},
"emailNotifications": { "emailNotifications": {
"$ref": "#/components/schemas/EmailNotificationsResponse" "$ref": "#/components/schemas/EmailNotificationsResponse"
}, },
@@ -11287,7 +11261,6 @@
}, },
"required": [ "required": [
"avatar", "avatar",
"download",
"emailNotifications", "emailNotifications",
"memories" "memories"
], ],
@@ -11298,9 +11271,6 @@
"avatar": { "avatar": {
"$ref": "#/components/schemas/AvatarUpdate" "$ref": "#/components/schemas/AvatarUpdate"
}, },
"download": {
"$ref": "#/components/schemas/DownloadUpdate"
},
"emailNotifications": { "emailNotifications": {
"$ref": "#/components/schemas/EmailNotificationsUpdate" "$ref": "#/components/schemas/EmailNotificationsUpdate"
}, },
+5 -5
View File
@@ -1,12 +1,12 @@
{ {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.106.4", "version": "1.106.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.106.4", "version": "1.106.3",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
"@oazapfts/runtime": "^1.0.2" "@oazapfts/runtime": "^1.0.2"
@@ -22,9 +22,9 @@
"integrity": "sha512-8tKiYffhwTGHSHYGnZ3oneLGCjX0po/XAXQ5Ng9fqKkvIdl/xz8+Vh8i+6xjzZqvZ2pLVpUcuSfnvNI/x67L0g==" "integrity": "sha512-8tKiYffhwTGHSHYGnZ3oneLGCjX0po/XAXQ5Ng9fqKkvIdl/xz8+Vh8i+6xjzZqvZ2pLVpUcuSfnvNI/x67L0g=="
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.14.2", "version": "20.12.13",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.13.tgz",
"integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", "integrity": "sha512-gBGeanV41c1L171rR7wjbMiEpEI/l5XFQdLLfhr/REwpgDy/4U8y89+i8kRiLzDyZdOkXh+cRaTetUnCYutoXA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.106.4", "version": "1.106.3",
"description": "Auto-generated TypeScript SDK for the Immich API", "description": "Auto-generated TypeScript SDK for the Immich API",
"type": "module", "type": "module",
"main": "./build/index.js", "main": "./build/index.js",
+1 -10
View File
@@ -1,6 +1,6 @@
/** /**
* Immich * Immich
* 1.106.4 * 1.106.3
* DO NOT MODIFY - This file has been generated using oazapfts. * DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts * See https://www.npmjs.com/package/oazapfts
*/ */
@@ -78,9 +78,6 @@ export type UserAdminUpdateDto = {
export type AvatarResponse = { export type AvatarResponse = {
color: UserAvatarColor; color: UserAvatarColor;
}; };
export type DownloadResponse = {
archiveSize: number;
};
export type EmailNotificationsResponse = { export type EmailNotificationsResponse = {
albumInvite: boolean; albumInvite: boolean;
albumUpdate: boolean; albumUpdate: boolean;
@@ -91,16 +88,12 @@ export type MemoryResponse = {
}; };
export type UserPreferencesResponseDto = { export type UserPreferencesResponseDto = {
avatar: AvatarResponse; avatar: AvatarResponse;
download: DownloadResponse;
emailNotifications: EmailNotificationsResponse; emailNotifications: EmailNotificationsResponse;
memories: MemoryResponse; memories: MemoryResponse;
}; };
export type AvatarUpdate = { export type AvatarUpdate = {
color?: UserAvatarColor; color?: UserAvatarColor;
}; };
export type DownloadUpdate = {
archiveSize?: number;
};
export type EmailNotificationsUpdate = { export type EmailNotificationsUpdate = {
albumInvite?: boolean; albumInvite?: boolean;
albumUpdate?: boolean; albumUpdate?: boolean;
@@ -111,7 +104,6 @@ export type MemoryUpdate = {
}; };
export type UserPreferencesUpdateDto = { export type UserPreferencesUpdateDto = {
avatar?: AvatarUpdate; avatar?: AvatarUpdate;
download?: DownloadUpdate;
emailNotifications?: EmailNotificationsUpdate; emailNotifications?: EmailNotificationsUpdate;
memories?: MemoryUpdate; memories?: MemoryUpdate;
}; };
@@ -190,7 +182,6 @@ export type AssetResponseDto = {
livePhotoVideoId?: string | null; livePhotoVideoId?: string | null;
localDateTime: string; localDateTime: string;
originalFileName: string; originalFileName: string;
originalMimeType?: string;
originalPath: string; originalPath: string;
owner?: UserResponseDto; owner?: UserResponseDto;
ownerId: string; ownerId: string;
+1 -1
View File
@@ -1,7 +1,7 @@
<p align="center"> <p align="center">
<br/> <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://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"/> <img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" alt="Discord"/>
</a> </a>
<br/> <br/>
+1 -1
View File
@@ -1,7 +1,7 @@
<p align="center"> <p align="center">
<br/> <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=Llicència&logoColor=000000&labelColor=ececec" alt="Llicència: MIT"></a> <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=Llicència&logoColor=000000&labelColor=ececec" alt="Llicència: MIT"></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" atl="Discord"/> <img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" atl="Discord"/>
</a> </a>
<br/> <br/>
+1 -1
View File
@@ -1,7 +1,7 @@
<p align="center"> <p align="center">
<br/> <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="Lizenz: MIT"></a> <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="Lizenz: MIT"></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"/> <img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" alt="Discord"/>
</a> </a>
<br/> <br/>
+14 -31
View File
@@ -1,7 +1,7 @@
<p align="center"> <p align="center">
<br/> <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="Licencia: MIT"></a> <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="Licencia: MIT"></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" atl="Discord"/> <img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" atl="Discord"/>
</a> </a>
<br/> <br/>
@@ -11,7 +11,7 @@
<p align="center"> <p align="center">
<img src="../design/immich-logo-stacked-light.svg" width="300" title="Iniciar sesión con URL personalizada"> <img src="../design/immich-logo-stacked-light.svg" width="300" title="Iniciar sesión con URL personalizada">
</p> </p>
<h3 align="center">Immich: Una solución Self-Hosted de alto rendimiento para la copia de seguridad de fotos y videos</h3> <h3 align="center">Immich: Una solución Self-Hosted de copia de seguridad de fotos y videos de alto rendimiento</h3>
<br/> <br/>
<a href="https://immich.app"> <a href="https://immich.app">
<img src="../design/immich-screenshots.png" title="Captura de pantalla principal"> <img src="../design/immich-screenshots.png" title="Captura de pantalla principal">
@@ -34,44 +34,43 @@
<a href="README_ar_JO.md">العربية</a> <a href="README_ar_JO.md">العربية</a>
</p> </p>
## Advertencia ## Descargo de responsabilidad
- ⚠️ El proyecto está en **activo desarrollo**. - ⚠️ El proyecto está en **desarrollo muy activo**.
- ⚠️ Es probable que haya errores y cambios disruptivos. - ⚠️ Es probable que haya errores y cambios disruptivos.
- ⚠️ **¡No utilices la aplicación como única forma de almacenar tus fotos y videos!** - ⚠️ **¡No utilices la aplicación como única forma de almacenar tus fotos y videos!**
- ⚠️ Siempre sigue el plan de backups [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) para tus fotos y videos.
## Contenido ## Contenido
- [Documentación oficial](https://immich.app/docs) - [Documentación oficial](https://immich.app/docs)
- [Hoja de ruta](https://github.com/orgs/immich-app/projects/1) - [Hoja de ruta](https://github.com/orgs/immich-app/projects/1)
- [Demo](#demo) - [Demostración](#demo)
- [Funciones](#funciones) - [Funciones](#features)
- [Introducción](https://immich.app/docs/overview/introduction) - [Introducción](https://immich.app/docs/overview/introduction)
- [Instalación](https://immich.app/docs/install/requirements) - [Instalación](https://immich.app/docs/install/requirements)
- [Directrices para contribuir](https://immich.app/docs/overview/support-the-project) - [Directrices para contribuir](https://immich.app/docs/overview/support-the-project)
## Documentación ## Documentación
Puedes encontrar la documentación oficial, incluidas las guías de instalación, en <https://immich.app/>. Puedes encontrar la documentación principal, incluidas las guías de instalación, en <https://immich.app/>.
## Demo ## Demostración
Puedes acceder a la demostración web en <https://demo.immich.app> Puedes acceder a la demostración web en <https://demo.immich.app>
Para la aplicación móvil, puedes usar `https://demo.immich.app/api` en la `URL de la terminal del servidor`. Para la aplicación móvil, puedes usar `https://demo.immich.app/api` como `URL de la terminal del servidor`.
```bash title="Credenciales de la demo" ```bash title="Credenciales de la demostración"
Las Credenciales Las credenciales son
correo: demo@immich.app correo electrónico: demo@immich.app
contraseña: demo contraseña: demo
``` ```
```bash ```bash
Especificaciones: Una VM de nivel gratuito de Oracle - Ámsterdam - CPU ARM64 de cuatro núcleos a 2.4 GHz, 24 GB de RAM Especificaciones: VM de nivel gratuito de Oracle - Ámsterdam - CPU ARM64 de cuatro núcleos a 2.4 GHz, 24 GB de RAM
``` ```
## Funciones ## Funcionalidades
| Funcionalidades | Móvil | Web | | Funcionalidades | Móvil | Web |
| ----------------------------------------------------- | ------ | --- | | ----------------------------------------------------- | ------ | --- |
@@ -100,19 +99,3 @@ Especificaciones: Una VM de nivel gratuito de Oracle - Ámsterdam - CPU ARM64 de
| Recuerdos (hace x años) | Sí | Sí | | Recuerdos (hace x años) | Sí | Sí |
| Soporte sin conexión | Sí | No | | Soporte sin conexión | Sí | No |
| Galería de solo lectura | Sí | Sí | | Galería de solo lectura | Sí | Sí |
## Contribuidores
<a href="https://github.com/alextran1502/immich/graphs/contributors">
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
</a>
## Historial de Estrellas
<a href="https://star-history.com/#immich-app/immich&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=immich-app/immich&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" width="100%" />
</picture>
</a>
+1 -1
View File
@@ -1,7 +1,7 @@
<p align="center"> <p align="center">
<br/> <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://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" atl="Discord"/> <img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" atl="Discord"/>
</a> </a>
<br/> <br/>
+1 -1
View File
@@ -1,7 +1,7 @@
<p align="center"> <p align="center">
<br/> <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://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" atl="Discord"/> <img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" atl="Discord"/>
</a> </a>
<br/> <br/>
+1 -1
View File
@@ -1,7 +1,7 @@
<p align="center"> <p align="center">
<br/> <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://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" atl="Discord"/> <img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" atl="Discord"/>
</a> </a>
<br/> <br/>
+1 -1
View File
@@ -1,7 +1,7 @@
<p align="center"> <p align="center">
<br/> <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://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"/> <img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" alt="Discord"/>
</a> </a>
<br/> <br/>
+1 -1
View File
@@ -1,7 +1,7 @@
<p align="center"> <p align="center">
<br/> <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://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" atl="Discord"/> <img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" atl="Discord"/>
</a> </a>
<br/> <br/>
+1 -1
View File
@@ -1,7 +1,7 @@
<p align="center"> <p align="center">
<br/> <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="Licença: AGPLv3"></a> <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="Licença: 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"/> <img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" alt="Discord"/>
</a> </a>
<br/> <br/>
+1 -1
View File
@@ -1,7 +1,7 @@
<p align="center"> <p align="center">
<br/> <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://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"/> <img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" alt="Discord"/>
</a> </a>
<br/> <br/>
+1 -1
View File
@@ -1,7 +1,7 @@
<p align="center"> <p align="center">
<br/> <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://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"/> <img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" alt="Discord"/>
</a> </a>
<br/> <br/>
+1 -1
View File
@@ -1,7 +1,7 @@
<p align="center"> <p align="center">
<br/> <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://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" atl="Discord"/> <img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" atl="Discord"/>
</a> </a>
<br/> <br/>
+1 -1
View File
@@ -1,7 +1,7 @@
<p align="center"> <p align="center">
<br/> <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://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" atl="Discord"/> <img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" atl="Discord"/>
</a> </a>
<br/> <br/>
+1177 -1022
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -1,6 +1,6 @@
{ {
"name": "immich", "name": "immich",
"version": "1.106.4", "version": "1.106.3",
"description": "", "description": "",
"author": "", "author": "",
"private": true, "private": true,
@@ -46,10 +46,10 @@
"@nestjs/swagger": "^7.1.8", "@nestjs/swagger": "^7.1.8",
"@nestjs/typeorm": "^10.0.0", "@nestjs/typeorm": "^10.0.0",
"@nestjs/websockets": "^10.2.2", "@nestjs/websockets": "^10.2.2",
"@opentelemetry/auto-instrumentations-node": "^0.47.0", "@opentelemetry/auto-instrumentations-node": "^0.46.0",
"@opentelemetry/context-async-hooks": "^1.24.0", "@opentelemetry/context-async-hooks": "^1.24.0",
"@opentelemetry/exporter-prometheus": "^0.52.0", "@opentelemetry/exporter-prometheus": "^0.51.0",
"@opentelemetry/sdk-node": "^0.52.0", "@opentelemetry/sdk-node": "^0.51.0",
"@react-email/components": "^0.0.19", "@react-email/components": "^0.0.19",
"@socket.io/postgres-adapter": "^0.3.1", "@socket.io/postgres-adapter": "^0.3.1",
"archiver": "^7.0.0", "archiver": "^7.0.0",
@@ -60,7 +60,7 @@
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.0", "class-validator": "^0.14.0",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"exiftool-vendored": "~27.0.0", "exiftool-vendored": "~26.1.0",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"fluent-ffmpeg": "^2.1.2", "fluent-ffmpeg": "^2.1.2",
"geo-tz": "^8.0.0", "geo-tz": "^8.0.0",
-4
View File
@@ -13,14 +13,12 @@ import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { AssetEntity, AssetType } from 'src/entities/asset.entity'; import { AssetEntity, AssetType } from 'src/entities/asset.entity';
import { SmartInfoEntity } from 'src/entities/smart-info.entity'; import { SmartInfoEntity } from 'src/entities/smart-info.entity';
import { mimeTypes } from 'src/utils/mime-types';
export class SanitizedAssetResponseDto { export class SanitizedAssetResponseDto {
id!: string; id!: string;
@ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType }) @ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType })
type!: AssetType; type!: AssetType;
thumbhash!: string | null; thumbhash!: string | null;
originalMimeType?: string;
resized!: boolean; resized!: boolean;
localDateTime!: Date; localDateTime!: Date;
duration!: string; duration!: string;
@@ -89,7 +87,6 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
const sanitizedAssetResponse: SanitizedAssetResponseDto = { const sanitizedAssetResponse: SanitizedAssetResponseDto = {
id: entity.id, id: entity.id,
type: entity.type, type: entity.type,
originalMimeType: mimeTypes.lookup(entity.originalFileName),
thumbhash: entity.thumbhash?.toString('base64') ?? null, thumbhash: entity.thumbhash?.toString('base64') ?? null,
localDateTime: entity.localDateTime, localDateTime: entity.localDateTime,
resized: !!entity.previewPath, resized: !!entity.previewPath,
@@ -110,7 +107,6 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
type: entity.type, type: entity.type,
originalPath: entity.originalPath, originalPath: entity.originalPath,
originalFileName: entity.originalFileName, originalFileName: entity.originalFileName,
originalMimeType: mimeTypes.lookup(entity.originalFileName),
resized: !!entity.previewPath, resized: !!entity.previewPath,
thumbhash: entity.thumbhash?.toString('base64') ?? null, thumbhash: entity.thumbhash?.toString('base64') ?? null,
fileCreatedAt: entity.fileCreatedAt, fileCreatedAt: entity.fileCreatedAt,
+1 -20
View File
@@ -1,6 +1,6 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { IsEnum, IsInt, IsPositive, ValidateNested } from 'class-validator'; import { IsEnum, ValidateNested } from 'class-validator';
import { UserAvatarColor, UserPreferences } from 'src/entities/user-metadata.entity'; import { UserAvatarColor, UserPreferences } from 'src/entities/user-metadata.entity';
import { Optional, ValidateBoolean } from 'src/validation'; import { Optional, ValidateBoolean } from 'src/validation';
@@ -27,14 +27,6 @@ class EmailNotificationsUpdate {
albumUpdate?: boolean; albumUpdate?: boolean;
} }
class DownloadUpdate {
@Optional()
@IsInt()
@IsPositive()
@ApiProperty({ type: 'integer' })
archiveSize?: number;
}
export class UserPreferencesUpdateDto { export class UserPreferencesUpdateDto {
@Optional() @Optional()
@ValidateNested() @ValidateNested()
@@ -50,11 +42,6 @@ export class UserPreferencesUpdateDto {
@ValidateNested() @ValidateNested()
@Type(() => EmailNotificationsUpdate) @Type(() => EmailNotificationsUpdate)
emailNotifications?: EmailNotificationsUpdate; emailNotifications?: EmailNotificationsUpdate;
@Optional()
@ValidateNested()
@Type(() => DownloadUpdate)
download?: DownloadUpdate;
} }
class AvatarResponse { class AvatarResponse {
@@ -72,16 +59,10 @@ class EmailNotificationsResponse {
albumUpdate!: boolean; albumUpdate!: boolean;
} }
class DownloadResponse {
@ApiProperty({ type: 'integer' })
archiveSize!: number;
}
export class UserPreferencesResponseDto implements UserPreferences { export class UserPreferencesResponseDto implements UserPreferences {
memories!: MemoryResponse; memories!: MemoryResponse;
avatar!: AvatarResponse; avatar!: AvatarResponse;
emailNotifications!: EmailNotificationsResponse; emailNotifications!: EmailNotificationsResponse;
download!: DownloadResponse;
} }
export const mapPreferences = (preferences: UserPreferences): UserPreferencesResponseDto => { export const mapPreferences = (preferences: UserPreferences): UserPreferencesResponseDto => {
+4 -4
View File
@@ -1,7 +1,6 @@
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { FaceSearchEntity } from 'src/entities/face-search.entity';
import { PersonEntity } from 'src/entities/person.entity'; import { PersonEntity } from 'src/entities/person.entity';
import { Column, Entity, Index, ManyToOne, OneToOne, PrimaryGeneratedColumn } from 'typeorm'; import { Column, Entity, Index, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
@Entity('asset_faces', { synchronize: false }) @Entity('asset_faces', { synchronize: false })
@Index('IDX_asset_faces_assetId_personId', ['assetId', 'personId']) @Index('IDX_asset_faces_assetId_personId', ['assetId', 'personId'])
@@ -16,8 +15,9 @@ export class AssetFaceEntity {
@Column({ nullable: true, type: 'uuid' }) @Column({ nullable: true, type: 'uuid' })
personId!: string | null; personId!: string | null;
@OneToOne(() => FaceSearchEntity, (faceSearchEntity) => faceSearchEntity.face, { cascade: ['insert'] }) @Index('face_index', { synchronize: false })
faceSearch?: FaceSearchEntity; @Column({ type: 'float4', array: true, select: false, transformer: { from: (v) => JSON.parse(v), to: (v) => v } })
embedding!: number[];
@Column({ default: 0, type: 'int' }) @Column({ default: 0, type: 'int' })
imageWidth!: number; imageWidth!: number;
-21
View File
@@ -1,21 +0,0 @@
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { asVector } from 'src/utils/database';
import { Column, Entity, Index, JoinColumn, OneToOne, PrimaryColumn } from 'typeorm';
@Entity('face_search', { synchronize: false })
export class FaceSearchEntity {
@OneToOne(() => AssetFaceEntity, { onDelete: 'CASCADE', nullable: true })
@JoinColumn({ name: 'faceId', referencedColumnName: 'id' })
face?: AssetFaceEntity;
@PrimaryColumn()
faceId!: string;
@Index('face_index', { synchronize: false })
@Column({
type: 'float4',
array: true,
transformer: { from: (v) => JSON.parse(v), to: (v) => asVector(v) },
})
embedding!: number[];
}
-2
View File
@@ -8,7 +8,6 @@ import { AssetStackEntity } from 'src/entities/asset-stack.entity';
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { AuditEntity } from 'src/entities/audit.entity'; import { AuditEntity } from 'src/entities/audit.entity';
import { ExifEntity } from 'src/entities/exif.entity'; import { ExifEntity } from 'src/entities/exif.entity';
import { FaceSearchEntity } from 'src/entities/face-search.entity';
import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity'; import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
import { LibraryEntity } from 'src/entities/library.entity'; import { LibraryEntity } from 'src/entities/library.entity';
import { MemoryEntity } from 'src/entities/memory.entity'; import { MemoryEntity } from 'src/entities/memory.entity';
@@ -35,7 +34,6 @@ export const entities = [
AssetJobStatusEntity, AssetJobStatusEntity,
AuditEntity, AuditEntity,
ExifEntity, ExifEntity,
FaceSearchEntity,
GeodataPlacesEntity, GeodataPlacesEntity,
MemoryEntity, MemoryEntity,
MoveEntity, MoveEntity,
@@ -1,5 +1,4 @@
import { UserEntity } from 'src/entities/user.entity'; import { UserEntity } from 'src/entities/user.entity';
import { HumanReadableSize } from 'src/utils/bytes';
import { Column, DeepPartial, Entity, ManyToOne, PrimaryColumn } from 'typeorm'; import { Column, DeepPartial, Entity, ManyToOne, PrimaryColumn } from 'typeorm';
@Entity('user_metadata') @Entity('user_metadata')
@@ -42,9 +41,6 @@ export interface UserPreferences {
albumInvite: boolean; albumInvite: boolean;
albumUpdate: boolean; albumUpdate: boolean;
}; };
download: {
archiveSize: number;
};
} }
export const getDefaultPreferences = (user: { email: string }): UserPreferences => { export const getDefaultPreferences = (user: { email: string }): UserPreferences => {
@@ -65,9 +61,6 @@ export const getDefaultPreferences = (user: { email: string }): UserPreferences
albumInvite: true, albumInvite: true,
albumUpdate: true, albumUpdate: true,
}, },
download: {
archiveSize: HumanReadableSize.GiB * 4,
},
}; };
}; };
-4
View File
@@ -47,10 +47,6 @@ export interface ImageDimensions {
height: number; height: number;
} }
export interface InputDimensions extends ImageDimensions {
inputPath: string;
}
export interface VideoInfo { export interface VideoInfo {
format: VideoFormat; format: VideoFormat;
videoStreams: VideoStreamInfo[]; videoStreams: VideoStreamInfo[];
@@ -1,54 +0,0 @@
import { getVectorExtension } from 'src/database.config';
import { DatabaseExtension } from 'src/interfaces/database.interface';
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddFaceSearchRelation1718486162779 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
if (getVectorExtension() === DatabaseExtension.VECTORS) {
await queryRunner.query(`SET search_path TO "$user", public, vectors`);
await queryRunner.query(`SET vectors.pgvector_compatibility=on`);
}
await queryRunner.query(`
CREATE TABLE face_search (
"faceId" uuid PRIMARY KEY REFERENCES asset_faces(id) ON DELETE CASCADE,
embedding vector(512) NOT NULL )`);
await queryRunner.query(`ALTER TABLE face_search ALTER COLUMN embedding SET STORAGE EXTERNAL`);
await queryRunner.query(`ALTER TABLE smart_search ALTER COLUMN embedding SET STORAGE EXTERNAL`);
await queryRunner.query(`
INSERT INTO face_search("faceId", embedding)
SELECT id, embedding
FROM asset_faces faces`);
await queryRunner.query(`ALTER TABLE asset_faces DROP COLUMN "embedding"`);
await queryRunner.query(`
CREATE INDEX face_index ON face_search
USING hnsw (embedding vector_cosine_ops)
WITH (ef_construction = 300, m = 16)`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
if (getVectorExtension() === DatabaseExtension.VECTORS) {
await queryRunner.query(`SET search_path TO "$user", public, vectors`);
await queryRunner.query(`SET vectors.pgvector_compatibility=on`);
}
await queryRunner.query(`ALTER TABLE asset_faces ADD COLUMN "embedding" vector(512)`);
await queryRunner.query(`ALTER TABLE face_search ALTER COLUMN embedding SET STORAGE DEFAULT`);
await queryRunner.query(`ALTER TABLE smart_search ALTER COLUMN embedding SET STORAGE DEFAULT`);
await queryRunner.query(`
UPDATE asset_faces
SET embedding = fs.embedding
FROM face_search fs
WHERE id = fs."faceId"`);
await queryRunner.query(`DROP TABLE face_search`);
await queryRunner.query(`
CREATE INDEX face_index ON asset_faces
USING hnsw (embedding vector_cosine_ops)
WITH (ef_construction = 300, m = 16)`);
}
}
+2 -3
View File
@@ -241,16 +241,15 @@ WITH
"faces"."boundingBoxY1" AS "boundingBoxY1", "faces"."boundingBoxY1" AS "boundingBoxY1",
"faces"."boundingBoxX2" AS "boundingBoxX2", "faces"."boundingBoxX2" AS "boundingBoxX2",
"faces"."boundingBoxY2" AS "boundingBoxY2", "faces"."boundingBoxY2" AS "boundingBoxY2",
"search"."embedding" <= > $1 AS "distance" "faces"."embedding" <= > $1 AS "distance"
FROM FROM
"asset_faces" "faces" "asset_faces" "faces"
INNER JOIN "assets" "asset" ON "asset"."id" = "faces"."assetId" INNER JOIN "assets" "asset" ON "asset"."id" = "faces"."assetId"
AND ("asset"."deletedAt" IS NULL) AND ("asset"."deletedAt" IS NULL)
INNER JOIN "face_search" "search" ON "search"."faceId" = "faces"."id"
WHERE WHERE
"asset"."ownerId" IN ($2) "asset"."ownerId" IN ($2)
ORDER BY ORDER BY
"search"."embedding" <= > $1 ASC "faces"."embedding" <= > $1 ASC
LIMIT LIMIT
100 100
) )
@@ -98,7 +98,7 @@ export class DatabaseRepository implements IDatabaseRepository {
} catch (error) { } catch (error) {
if (getVectorExtension() === DatabaseExtension.VECTORS) { if (getVectorExtension() === DatabaseExtension.VECTORS) {
this.logger.warn(`Could not reindex index ${index}. Attempting to auto-fix.`); this.logger.warn(`Could not reindex index ${index}. Attempting to auto-fix.`);
const table = index === VectorIndex.CLIP ? 'smart_search' : 'face_search'; const table = index === VectorIndex.CLIP ? 'smart_search' : 'asset_faces';
const dimSize = await this.getDimSize(table); const dimSize = await this.getDimSize(table);
await this.dataSource.manager.transaction(async (manager) => { await this.dataSource.manager.transaction(async (manager) => {
await this.setSearchPath(manager); await this.setSearchPath(manager);

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