mirror of
https://github.com/immich-app/immich.git
synced 2026-01-20 16:43:16 -08:00
Compare commits
101 Commits
feat/mobil
...
web/automa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
736b968eab | ||
|
|
d5d8426bda | ||
|
|
be4a783845 | ||
|
|
c30cd3b378 | ||
|
|
0d3cc28f45 | ||
|
|
f004487be0 | ||
|
|
21231d53a5 | ||
|
|
a99862120d | ||
|
|
776023b149 | ||
|
|
7d4187962a | ||
|
|
a93534fc3c | ||
|
|
cef84f6ced | ||
|
|
a2180a467d | ||
|
|
1e3dceea4d | ||
|
|
fd4514711f | ||
|
|
2dd7c13b88 | ||
|
|
40931b5668 | ||
|
|
25549b87c9 | ||
|
|
7ec62f12b5 | ||
|
|
caf76f0713 | ||
|
|
6778653825 | ||
|
|
c858b43717 | ||
|
|
6eb1b82541 | ||
|
|
71b6d8b569 | ||
|
|
3abfe3c99e | ||
|
|
171b6bb0a6 | ||
|
|
78c7ff855d | ||
|
|
57be9182d4 | ||
|
|
886e07604e | ||
|
|
431ffebddd | ||
|
|
74c921148b | ||
|
|
eaf9e5e477 | ||
|
|
4478e524f8 | ||
|
|
e72e41a7aa | ||
|
|
efd8f0d648 | ||
|
|
d2b5cc6a4a | ||
|
|
596c35dc00 | ||
|
|
112d6d60ec | ||
|
|
c50241369a | ||
|
|
b74f8273c2 | ||
|
|
8573c84605 | ||
|
|
a4f805e99b | ||
|
|
7db07bbe61 | ||
|
|
3a9df6dae8 | ||
|
|
c227f9893e | ||
|
|
a3feca2580 | ||
|
|
b21566c2fc | ||
|
|
1071396a4a | ||
|
|
7e92ef9428 | ||
|
|
f58886514d | ||
|
|
17dc12cf7d | ||
|
|
6d4d0f86cf | ||
|
|
14b1425e98 | ||
|
|
c70d9f9055 | ||
|
|
18fa6018c0 | ||
|
|
47fb9bd213 | ||
|
|
6e6deec40c | ||
|
|
877207a2e6 | ||
|
|
64cfd017b4 | ||
|
|
4c4ebf769f | ||
|
|
28d081338b | ||
|
|
50c9bc0336 | ||
|
|
ed2e4e5217 | ||
|
|
1aa8707b8a | ||
|
|
103cb60a57 | ||
|
|
58e516c766 | ||
|
|
bcdec25843 | ||
|
|
28f591d01b | ||
|
|
dba365634a | ||
|
|
1c1e461936 | ||
|
|
2db76034b1 | ||
|
|
95e67a7b1d | ||
|
|
3deaaf14c0 | ||
|
|
084a97a77a | ||
|
|
ed74213c63 | ||
|
|
7ce1662b05 | ||
|
|
f959f2de85 | ||
|
|
07716bbff7 | ||
|
|
0f74b17000 | ||
|
|
3c7f70ec30 | ||
|
|
85df3f1e99 | ||
|
|
a903898781 | ||
|
|
25e1887939 | ||
|
|
9c696e4c28 | ||
|
|
87a36846f4 | ||
|
|
ded01401f8 | ||
|
|
8aff392275 | ||
|
|
14b798fcc4 | ||
|
|
97c099e26d | ||
|
|
3eb61a9d53 | ||
|
|
e65b3a8ea0 | ||
|
|
1fdbc949d6 | ||
|
|
605da89425 | ||
|
|
0d062b32a8 | ||
|
|
a4267ed60f | ||
|
|
58346465aa | ||
|
|
ec76e5ef23 | ||
|
|
372fae20d9 | ||
|
|
16543a233b | ||
|
|
73b961f5fa | ||
|
|
e0d15c96f1 |
10
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
10
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -87,6 +87,16 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant logs below. (code formatting is
|
||||
enabled, no need for backticks)
|
||||
render: shell
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
|
||||
10
.github/workflows/build-mobile.yml
vendored
10
.github/workflows/build-mobile.yml
vendored
@@ -37,15 +37,15 @@ jobs:
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: "zulu"
|
||||
java-version: "11.0.21+9"
|
||||
cache: "gradle"
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
cache: 'gradle'
|
||||
|
||||
- name: Setup Flutter SDK
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: "3.19.3"
|
||||
channel: 'stable'
|
||||
flutter-version: '3.19.3'
|
||||
cache: true
|
||||
|
||||
- name: Create the Keystore
|
||||
|
||||
4
.github/workflows/docker-cleanup.yml
vendored
4
.github/workflows/docker-cleanup.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
steps:
|
||||
- name: Clean temporary images
|
||||
if: "${{ env.TOKEN != '' }}"
|
||||
uses: stumpylog/image-cleaner-action/ephemeral@v0.5.0
|
||||
uses: stumpylog/image-cleaner-action/ephemeral@v0.6.0
|
||||
with:
|
||||
token: "${{ env.TOKEN }}"
|
||||
owner: "immich-app"
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
steps:
|
||||
- name: Clean untagged images
|
||||
if: "${{ env.TOKEN != '' }}"
|
||||
uses: stumpylog/image-cleaner-action/untagged@v0.5.0
|
||||
uses: stumpylog/image-cleaner-action/untagged@v0.6.0
|
||||
with:
|
||||
token: "${{ env.TOKEN }}"
|
||||
owner: "immich-app"
|
||||
|
||||
15
.github/workflows/test.yml
vendored
15
.github/workflows/test.yml
vendored
@@ -10,19 +10,6 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
server-e2e-jobs:
|
||||
name: Server (e2e-jobs)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Run e2e tests
|
||||
run: make server-e2e-jobs
|
||||
|
||||
doc-tests:
|
||||
name: Docs
|
||||
runs-on: ubuntu-latest
|
||||
@@ -221,7 +208,7 @@ jobs:
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version: '3.16.9'
|
||||
flutter-version: '3.19.3'
|
||||
- name: Run tests
|
||||
working-directory: ./mobile
|
||||
run: flutter test -j 1
|
||||
|
||||
3
Makefile
3
Makefile
@@ -16,9 +16,6 @@ stage:
|
||||
pull-stage:
|
||||
docker compose -f ./docker/docker-compose.staging.yml pull
|
||||
|
||||
server-e2e-jobs:
|
||||
docker compose -f ./server/e2e/docker-compose.server-e2e.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build
|
||||
|
||||
.PHONY: e2e
|
||||
e2e:
|
||||
docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:20-alpine3.19@sha256:7e227295e96f5b00aa79555ae166f50610940d888fc2e321cf36304cbd17d7d6 as core
|
||||
FROM node:20-alpine3.19@sha256:ec0c413b1d84f3f7f67ec986ba885930c57b5318d2eb3abc6960ee05d4f2eb28 as core
|
||||
|
||||
WORKDIR /usr/src/open-api/typescript-sdk
|
||||
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
||||
|
||||
241
cli/package-lock.json
generated
241
cli/package-lock.json
generated
@@ -47,15 +47,15 @@
|
||||
},
|
||||
"../open-api/typescript-sdk": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.101.0",
|
||||
"version": "1.102.3",
|
||||
"dev": true,
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.12.4",
|
||||
"typescript": "^5.4.4"
|
||||
"@types/node": "^20.11.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
@@ -1193,12 +1193,6 @@
|
||||
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/istanbul-lib-coverage": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
|
||||
"integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
@@ -1230,9 +1224,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.12.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.4.tgz",
|
||||
"integrity": "sha512-E+Fa9z3wSQpzgYQdYmme5X3OTuejnnTx88A6p6vkkJosR3KBz+HpE3kqNm98VE6cfLFcISx7zW7MsJkH6KwbTw==",
|
||||
"version": "20.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz",
|
||||
"integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
@@ -1251,22 +1245,22 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.5.0.tgz",
|
||||
"integrity": "sha512-HpqNTH8Du34nLxbKgVMGljZMG0rJd2O9ecvr2QLYp+7512ty1j42KnsFwspPXg1Vh8an9YImf6CokUBltisZFQ==",
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.6.0.tgz",
|
||||
"integrity": "sha512-gKmTNwZnblUdnTIJu3e9kmeRRzV2j1a/LUO27KNNAnIC5zjy1aSvXSRp4rVNlmAoHlQ7HzX42NbKpcSr4jF80A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.5.1",
|
||||
"@typescript-eslint/scope-manager": "7.5.0",
|
||||
"@typescript-eslint/type-utils": "7.5.0",
|
||||
"@typescript-eslint/utils": "7.5.0",
|
||||
"@typescript-eslint/visitor-keys": "7.5.0",
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "7.6.0",
|
||||
"@typescript-eslint/type-utils": "7.6.0",
|
||||
"@typescript-eslint/utils": "7.6.0",
|
||||
"@typescript-eslint/visitor-keys": "7.6.0",
|
||||
"debug": "^4.3.4",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.2.4",
|
||||
"ignore": "^5.3.1",
|
||||
"natural-compare": "^1.4.0",
|
||||
"semver": "^7.5.4",
|
||||
"ts-api-utils": "^1.0.1"
|
||||
"semver": "^7.6.0",
|
||||
"ts-api-utils": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
@@ -1286,15 +1280,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.5.0.tgz",
|
||||
"integrity": "sha512-cj+XGhNujfD2/wzR1tabNsidnYRaFfEkcULdcIyVBYcXjBvBKOes+mpMBP7hMpOyk+gBcfXsrg4NBGAStQyxjQ==",
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.6.0.tgz",
|
||||
"integrity": "sha512-usPMPHcwX3ZoPWnBnhhorc14NJw9J4HpSXQX4urF2TPKG0au0XhJoZyX62fmvdHONUkmyUe74Hzm1//XA+BoYg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "7.5.0",
|
||||
"@typescript-eslint/types": "7.5.0",
|
||||
"@typescript-eslint/typescript-estree": "7.5.0",
|
||||
"@typescript-eslint/visitor-keys": "7.5.0",
|
||||
"@typescript-eslint/scope-manager": "7.6.0",
|
||||
"@typescript-eslint/types": "7.6.0",
|
||||
"@typescript-eslint/typescript-estree": "7.6.0",
|
||||
"@typescript-eslint/visitor-keys": "7.6.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1314,13 +1308,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.5.0.tgz",
|
||||
"integrity": "sha512-Z1r7uJY0MDeUlql9XJ6kRVgk/sP11sr3HKXn268HZyqL7i4cEfrdFuSSY/0tUqT37l5zT0tJOsuDP16kio85iA==",
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.6.0.tgz",
|
||||
"integrity": "sha512-ngttyfExA5PsHSx0rdFgnADMYQi+Zkeiv4/ZxGYUWd0nLs63Ha0ksmp8VMxAIC0wtCFxMos7Lt3PszJssG/E6w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.5.0",
|
||||
"@typescript-eslint/visitor-keys": "7.5.0"
|
||||
"@typescript-eslint/types": "7.6.0",
|
||||
"@typescript-eslint/visitor-keys": "7.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
@@ -1331,15 +1325,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.5.0.tgz",
|
||||
"integrity": "sha512-A021Rj33+G8mx2Dqh0nMO9GyjjIBK3MqgVgZ2qlKf6CJy51wY/lkkFqq3TqqnH34XyAHUkq27IjlUkWlQRpLHw==",
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.6.0.tgz",
|
||||
"integrity": "sha512-NxAfqAPNLG6LTmy7uZgpK8KcuiS2NZD/HlThPXQRGwz6u7MDBWRVliEEl1Gj6U7++kVJTpehkhZzCJLMK66Scw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "7.5.0",
|
||||
"@typescript-eslint/utils": "7.5.0",
|
||||
"@typescript-eslint/typescript-estree": "7.6.0",
|
||||
"@typescript-eslint/utils": "7.6.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^1.0.1"
|
||||
"ts-api-utils": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
@@ -1358,9 +1352,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.5.0.tgz",
|
||||
"integrity": "sha512-tv5B4IHeAdhR7uS4+bf8Ov3k793VEVHd45viRRkehIUZxm0WF82VPiLgHzA/Xl4TGPg1ZD49vfxBKFPecD5/mg==",
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.6.0.tgz",
|
||||
"integrity": "sha512-h02rYQn8J+MureCvHVVzhl69/GAfQGPQZmOMjG1KfCl7o3HtMSlPaPUAPu6lLctXI5ySRGIYk94clD/AUMCUgQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
@@ -1371,19 +1365,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.5.0.tgz",
|
||||
"integrity": "sha512-YklQQfe0Rv2PZEueLTUffiQGKQneiIEKKnfIqPIOxgM9lKSZFCjT5Ad4VqRKj/U4+kQE3fa8YQpskViL7WjdPQ==",
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.6.0.tgz",
|
||||
"integrity": "sha512-+7Y/GP9VuYibecrCQWSKgl3GvUM5cILRttpWtnAu8GNL9j11e4tbuGZmZjJ8ejnKYyBRb2ddGQ3rEFCq3QjMJw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.5.0",
|
||||
"@typescript-eslint/visitor-keys": "7.5.0",
|
||||
"@typescript-eslint/types": "7.6.0",
|
||||
"@typescript-eslint/visitor-keys": "7.6.0",
|
||||
"debug": "^4.3.4",
|
||||
"globby": "^11.1.0",
|
||||
"is-glob": "^4.0.3",
|
||||
"minimatch": "9.0.3",
|
||||
"semver": "^7.5.4",
|
||||
"ts-api-utils": "^1.0.1"
|
||||
"minimatch": "^9.0.4",
|
||||
"semver": "^7.6.0",
|
||||
"ts-api-utils": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
@@ -1399,18 +1393,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.5.0.tgz",
|
||||
"integrity": "sha512-3vZl9u0R+/FLQcpy2EHyRGNqAS/ofJ3Ji8aebilfJe+fobK8+LbIFmrHciLVDxjDoONmufDcnVSF38KwMEOjzw==",
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.6.0.tgz",
|
||||
"integrity": "sha512-x54gaSsRRI+Nwz59TXpCsr6harB98qjXYzsRxGqvA5Ue3kQH+FxS7FYU81g/omn22ML2pZJkisy6Q+ElK8pBCA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@types/json-schema": "^7.0.12",
|
||||
"@types/semver": "^7.5.0",
|
||||
"@typescript-eslint/scope-manager": "7.5.0",
|
||||
"@typescript-eslint/types": "7.5.0",
|
||||
"@typescript-eslint/typescript-estree": "7.5.0",
|
||||
"semver": "^7.5.4"
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@typescript-eslint/scope-manager": "7.6.0",
|
||||
"@typescript-eslint/types": "7.6.0",
|
||||
"@typescript-eslint/typescript-estree": "7.6.0",
|
||||
"semver": "^7.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
@@ -1424,13 +1418,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.5.0.tgz",
|
||||
"integrity": "sha512-mcuHM/QircmA6O7fy6nn2w/3ditQkj+SgtOc8DW3uQ10Yfj42amm2i+6F2K4YAOPNNTmE6iM1ynM6lrSwdendA==",
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.6.0.tgz",
|
||||
"integrity": "sha512-4eLB7t+LlNUmXzfOu1VAIAdkjbu5xNSerURS9X/S5TUKWFRpXRQZbmtPqgKmYx8bj3J0irtQXSiWAOY82v+cgw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.5.0",
|
||||
"eslint-visitor-keys": "^3.4.1"
|
||||
"@typescript-eslint/types": "7.6.0",
|
||||
"eslint-visitor-keys": "^3.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
@@ -1447,9 +1441,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@vitest/coverage-v8": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.4.0.tgz",
|
||||
"integrity": "sha512-4hDGyH1SvKpgZnIByr9LhGgCEuF9DKM34IBLCC/fVfy24Z3+PZ+Ii9hsVBsHvY1umM1aGPEjceRkzxCfcQ10wg==",
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.5.0.tgz",
|
||||
"integrity": "sha512-1igVwlcqw1QUMdfcMlzzY4coikSIBN944pkueGi0pawrX5I5Z+9hxdTR+w3Sg6Q3eZhvdMAs8ZaF9JuTG1uYOQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.1",
|
||||
@@ -1464,24 +1458,23 @@
|
||||
"picocolors": "^1.0.0",
|
||||
"std-env": "^3.5.0",
|
||||
"strip-literal": "^2.0.0",
|
||||
"test-exclude": "^6.0.0",
|
||||
"v8-to-istanbul": "^9.2.0"
|
||||
"test-exclude": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vitest": "1.4.0"
|
||||
"vitest": "1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.4.0.tgz",
|
||||
"integrity": "sha512-Jths0sWCJZ8BxjKe+p+eKsoqev1/T8lYcrjavEaz8auEJ4jAVY0GwW3JKmdVU4mmNPLPHixh4GNXP7GFtAiDHA==",
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.5.0.tgz",
|
||||
"integrity": "sha512-0pzuCI6KYi2SIC3LQezmxujU9RK/vwC1U9R0rLuGlNGcOuDWxqWKu6nUdFsX9tH1WU0SXtAxToOsEjeUn1s3hA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@vitest/spy": "1.4.0",
|
||||
"@vitest/utils": "1.4.0",
|
||||
"@vitest/spy": "1.5.0",
|
||||
"@vitest/utils": "1.5.0",
|
||||
"chai": "^4.3.10"
|
||||
},
|
||||
"funding": {
|
||||
@@ -1489,12 +1482,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.4.0.tgz",
|
||||
"integrity": "sha512-EDYVSmesqlQ4RD2VvWo3hQgTJ7ZrFQ2VSJdfiJiArkCerDAGeyF1i6dHkmySqk573jLp6d/cfqCN+7wUB5tLgg==",
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.5.0.tgz",
|
||||
"integrity": "sha512-7HWwdxXP5yDoe7DTpbif9l6ZmDwCzcSIK38kTSIt6CFEpMjX4EpCgT6wUmS0xTXqMI6E/ONmfgRKmaujpabjZQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@vitest/utils": "1.4.0",
|
||||
"@vitest/utils": "1.5.0",
|
||||
"p-limit": "^5.0.0",
|
||||
"pathe": "^1.1.1"
|
||||
},
|
||||
@@ -1530,9 +1523,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.4.0.tgz",
|
||||
"integrity": "sha512-saAFnt5pPIA5qDGxOHxJ/XxhMFKkUSBJmVt5VgDsAqPTX6JP326r5C/c9UuCMPoXNzuudTPsYDZCoJ5ilpqG2A==",
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.5.0.tgz",
|
||||
"integrity": "sha512-qpv3fSEuNrhAO3FpH6YYRdaECnnRjg9VxbhdtPwPRnzSfHVXnNzzrpX4cJxqiwgRMo7uRMWDFBlsBq4Cr+rO3A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"magic-string": "^0.30.5",
|
||||
@@ -1544,9 +1537,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.4.0.tgz",
|
||||
"integrity": "sha512-Ywau/Qs1DzM/8Uc+yA77CwSegizMlcgTJuYGAi0jujOteJOUf1ujunHThYo243KG9nAyWT3L9ifPYZ5+As/+6Q==",
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.5.0.tgz",
|
||||
"integrity": "sha512-vu6vi6ew5N5MMHJjD5PoakMRKYdmIrNJmyfkhRpQt5d9Ewhw9nZ5Aqynbi3N61bvk9UvZ5UysMT6ayIrZ8GA9w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tinyspy": "^2.2.0"
|
||||
@@ -1556,9 +1549,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.4.0.tgz",
|
||||
"integrity": "sha512-mx3Yd1/6e2Vt/PUC98DcqTirtfxUyAZ32uK82r8rZzbtBeBo+nqgnjx/LvqQdWsrvNtm14VmurNgcf4nqY5gJg==",
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.5.0.tgz",
|
||||
"integrity": "sha512-BDU0GNL8MWkRkSRdNFvCUCAVOeHaUlVJ9Tx0TYBZyXaaOTmGtUFObzchCivIBrIwKzvZA7A9sCejVhXM2aY98A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"diff-sequences": "^29.6.3",
|
||||
@@ -1909,12 +1902,6 @@
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/core-js-compat": {
|
||||
"version": "3.36.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.0.tgz",
|
||||
@@ -3139,9 +3126,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
|
||||
"integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
|
||||
"version": "9.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
|
||||
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
@@ -4258,9 +4245,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tinypool": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.2.tgz",
|
||||
"integrity": "sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==",
|
||||
"version": "0.8.4",
|
||||
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz",
|
||||
"integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -4368,9 +4355,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.4.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz",
|
||||
"integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==",
|
||||
"version": "5.4.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
|
||||
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -4431,20 +4418,6 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-to-istanbul": {
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz",
|
||||
"integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.12",
|
||||
"@types/istanbul-lib-coverage": "^2.0.1",
|
||||
"convert-source-map": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/validate-npm-package-license": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
||||
@@ -4511,9 +4484,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite-node": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.4.0.tgz",
|
||||
"integrity": "sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==",
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.5.0.tgz",
|
||||
"integrity": "sha512-tV8h6gMj6vPzVCa7l+VGq9lwoJjW8Y79vst8QZZGiuRAfijU+EEWuc0kFpmndQrWhMMhet1jdSF+40KSZUqIIw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cac": "^6.7.14",
|
||||
@@ -4552,16 +4525,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.4.0.tgz",
|
||||
"integrity": "sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==",
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.5.0.tgz",
|
||||
"integrity": "sha512-d8UKgR0m2kjdxDWX6911uwxout6GHS0XaGH1cksSIVVG8kRlE7G7aBw7myKQCvDI5dT4j7ZMa+l706BIORMDLw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@vitest/expect": "1.4.0",
|
||||
"@vitest/runner": "1.4.0",
|
||||
"@vitest/snapshot": "1.4.0",
|
||||
"@vitest/spy": "1.4.0",
|
||||
"@vitest/utils": "1.4.0",
|
||||
"@vitest/expect": "1.5.0",
|
||||
"@vitest/runner": "1.5.0",
|
||||
"@vitest/snapshot": "1.5.0",
|
||||
"@vitest/spy": "1.5.0",
|
||||
"@vitest/utils": "1.5.0",
|
||||
"acorn-walk": "^8.3.2",
|
||||
"chai": "^4.3.10",
|
||||
"debug": "^4.3.4",
|
||||
@@ -4573,9 +4546,9 @@
|
||||
"std-env": "^3.5.0",
|
||||
"strip-literal": "^2.0.0",
|
||||
"tinybench": "^2.5.1",
|
||||
"tinypool": "^0.8.2",
|
||||
"tinypool": "^0.8.3",
|
||||
"vite": "^5.0.0",
|
||||
"vite-node": "1.4.0",
|
||||
"vite-node": "1.5.0",
|
||||
"why-is-node-running": "^2.2.2"
|
||||
},
|
||||
"bin": {
|
||||
@@ -4590,8 +4563,8 @@
|
||||
"peerDependencies": {
|
||||
"@edge-runtime/vm": "*",
|
||||
"@types/node": "^18.0.0 || >=20.0.0",
|
||||
"@vitest/browser": "1.4.0",
|
||||
"@vitest/ui": "1.4.0",
|
||||
"@vitest/browser": "1.5.0",
|
||||
"@vitest/ui": "1.5.0",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*"
|
||||
},
|
||||
|
||||
@@ -97,7 +97,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70
|
||||
image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
|
||||
|
||||
database:
|
||||
container_name: immich_postgres
|
||||
|
||||
@@ -54,7 +54,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70
|
||||
image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
|
||||
restart: always
|
||||
|
||||
database:
|
||||
@@ -88,7 +88,7 @@ services:
|
||||
command: ['./run.sh', '-disable-reporting']
|
||||
ports:
|
||||
- 3000:3000
|
||||
image: grafana/grafana:10.4.1-ubuntu@sha256:65e0e7d0f0b001cb0478bce5093bff917677dc308dd27a0aa4b3ac38e4fd877c
|
||||
image: grafana/grafana:10.4.2-ubuntu@sha256:4f55071b556fb03f12b41423c98a185ed6695ed9ff2558e35805f0dd765fd958
|
||||
volumes:
|
||||
- grafana-data:/var/lib/grafana
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: registry.hub.docker.com/library/redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
|
||||
image: registry.hub.docker.com/library/redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
|
||||
restart: always
|
||||
|
||||
database:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Website
|
||||
|
||||
This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.
|
||||
This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
|
||||
|
||||
### Installation
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
The admin password can be reset by running the [reset-admin-password](/docs/administration/server-commands.md) command on the immich-server.
|
||||
|
||||
### How can I see list of all users in Immich?
|
||||
### How can I see a list of all users in Immich?
|
||||
|
||||
You can see the list of all users by running [list-users](/docs/administration/server-commands.md) Command on the Immich-server.
|
||||
|
||||
@@ -24,14 +24,14 @@ You can see the list of all users by running [list-users](/docs/administration/s
|
||||
|
||||
### I cannot log into the application after an update. What can I do?
|
||||
|
||||
First, verify that the mobile app and server are both running the same version (major and minor).
|
||||
Verify that the mobile app and server are both running the same version (major and minor).
|
||||
|
||||
:::note
|
||||
App store updates sometimes take longer because the stores (Google play store and Apple app store)
|
||||
need to approve the update first which may take some time.
|
||||
App store updates sometimes take longer because the stores (Google Play Store and Apple App Store)
|
||||
need to approve the update first, and it can take some time.
|
||||
:::
|
||||
|
||||
If you still cannot login to the app, try the following:
|
||||
If you still cannot log in to the app, try the following:
|
||||
|
||||
- Check the mobile logs
|
||||
- Make sure login credentials are correct by logging in on the web app
|
||||
@@ -40,6 +40,11 @@ If you still cannot login to the app, try the following:
|
||||
|
||||
## Assets
|
||||
|
||||
### Does Immich change the file?
|
||||
|
||||
No, Immich does not touch the original file under any circumstances,
|
||||
all edited metadata are saved in the companion sidecar file and the database.
|
||||
|
||||
### Can I add my existing photo library?
|
||||
|
||||
Yes, with an [External Library](/docs/features/libraries.md).
|
||||
@@ -50,11 +55,11 @@ Template changes will only apply to _new_ assets. To retroactively apply the tem
|
||||
|
||||
### Why are only photos and not videos being uploaded to Immich?
|
||||
|
||||
This often happens when using a reverse proxy (such as nginx or Cloudflare tunnel) in front of Immich. Make sure to set your reverse proxy to allow large `POST` requests. In `nginx`, set `client_max_body_size 50000M;` or similar. Also check the disk space of your reverse proxy, in some cases proxies cache requests to disk before passing them on, and if disk space runs out the request fails.
|
||||
This often happens when using a reverse proxy (such as Nginx or Cloudflare tunnel) in front of Immich. Make sure to set your reverse proxy to allow large `POST` requests. In `nginx`, set `client_max_body_size 50000M;` or similar. Also, check the disk space of your reverse proxy. In some cases, proxies cache requests to disk before passing them on, and if disk space runs out, the request fails.
|
||||
|
||||
### Why are some photos stored in the file system with the wrong date?
|
||||
|
||||
There are a few different scenarios that can lead to this situation. The solution is to run the storage migration job again. The job is only _automatically_ run once per asset, after upload. If metadata extraction originally failed, the jobs were cleared/cancelled, etc. the job may not have run automatically the first time.
|
||||
There are a few different scenarios that can lead to this situation. The solution is to rerun the storage migration job. The job is only automatically run once per asset after upload. If metadata extraction originally failed, the jobs were cleared/canceled, etc., the job may not have run automatically the first time.
|
||||
|
||||
### How can I hide photos from the timeline?
|
||||
|
||||
@@ -68,23 +73,27 @@ See [Backup and Restore](/docs/administration/backup-and-restore.md).
|
||||
|
||||
No, it currently does not. There is an [open feature request on GitHub](https://github.com/immich-app/immich/discussions/4348).
|
||||
|
||||
### Does Immich support filtering of NSFW images?
|
||||
### Does Immich support the filtering of NSFW images?
|
||||
|
||||
No, it currently does not. There is an [open feature request on Github](https://github.com/immich-app/immich/discussions/2451).
|
||||
|
||||
### Why are there so many thumbnail generation jobs?
|
||||
|
||||
There are three thubmanil jobs for each asset:
|
||||
There are three thumbnail jobs for each asset:
|
||||
|
||||
- Blurred (thumbhash)
|
||||
- Small (webp)
|
||||
- Large (jpeg)
|
||||
- Preview (Webp)
|
||||
- Thumbnail (Jpeg)
|
||||
|
||||
Also, there are additional jobs for person (face) thumbnails.
|
||||
|
||||
### Why do files from WhatsApp not appear with the correct date?
|
||||
|
||||
Files sent on WhatsApp are saved without metadata on the file. Therefore, Immich has no way of knowing the original date of the file when files are uploaded from WhatsApp, not the order of arrival on the device. [See #3527](https://github.com/immich-app/immich/issues/3527).
|
||||
|
||||
### What happens if an asset exists in more than one account?
|
||||
|
||||
There are no requirements for assets to be unique across users. If multiple users upload the same image they are processed as if they were distinct assets and jobs run and thumbnails are generated accordingly.
|
||||
There are no requirements for assets to be unique across users. If multiple users upload the same image, it is processed as if it were a distinct asset, and jobs run and thumbnails are generated accordingly.
|
||||
|
||||
### Why do HDR videos appear pale in Immich player but look normal after download?
|
||||
|
||||
@@ -96,40 +105,36 @@ Immich always keeps your original files. Alongside that, it generates a transcod
|
||||
|
||||
### How can I delete transcoded videos without deleting the original?
|
||||
|
||||
The transcoded version of an asset can be deleted by setting a transcode policy that makes it unnecessary, then running a transcoding job for that asset. This can be done on a per-asset basis by starting a transcoding job for a single asset with the _Refresh encoded videos_ button in the asset viewer options, or for all assets by running transcoding jobs for all assets from the administration page.
|
||||
The transcoded version of an asset can be deleted by setting a transcode policy that makes it unnecessary and then running a transcoding job for that asset. This can be done on a per-asset basis by starting a transcoding job for a single asset with the _Refresh encoded videos_ button in the asset viewer options or for all assets by running transcoding jobs for all assets from the administration page.
|
||||
|
||||
To update the transcode policy, navigate to Administration > Video Transcoding Settings > Transcoding Policy and select a policy from the drop-down. This policy will determine whether an existing transcode will be deleted or overwritten in the transcoding job. If a video should be transcoded according to this policy, an existing transcode is overwritten. If not, then it is deleted.
|
||||
|
||||
:::note
|
||||
For example, say you have existing transcodes with the policy "Videos higher than normal resolution or not in the desired format" and switch to a narrower policy: "Videos not in the desired format". If an asset was only transcoded due to its resolution, then running a transcoding job for it will now delete the existing transcode. This is because resolution is no longer part of the transcode policy and the transcode is unnecessary as a result. Likewise, if you set the policy to "Don't transcode any videos" and run transcoding jobs for all assets, this will delete all existing transcodes as they are all unnecessary.
|
||||
For example, say you have existing transcodes with the policy "Videos higher than normal resolution or not in the desired format" and switch to a narrower policy: "Videos not in the desired format." If an asset was only transcoded due to its resolution, running a transcoding job for it will delete the existing transcode. This is because resolution is no longer part of the transcode policy and the transcode is unnecessary. Likewise, if you set the policy to "Don't transcode any videos" and run transcoding jobs for all assets, this will delete all existing transcodes as they are unnecessary.
|
||||
:::
|
||||
|
||||
### Is it possible to compress images during backup?
|
||||
|
||||
No. Our golden rule is that the original assets should always be untouched, so we don't think this feature is a good fit for Immich.
|
||||
No. Our design principle is that the original assets should always be untouched.
|
||||
|
||||
### How can I move all data (photos, persons, albums) 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.
|
||||
|
||||
:::warning
|
||||
This is an advanced operation. If you can't do it with the steps described here, this is not for you.
|
||||
:::
|
||||
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)
|
||||
2. Find the ID of both the 'source' and the 'destination' user (it's the id column in the `users` table)
|
||||
|
||||
3. Three tables need to be updated:
|
||||
|
||||
```sql
|
||||
// reassign albums
|
||||
// Reassign albums
|
||||
UPDATE albums SET "ownerId" = '<destinationId>' WHERE "ownerId" = '<sourceId>';
|
||||
|
||||
// reassign people
|
||||
// Reassign people
|
||||
UPDATE person SET "ownerId" = '<destinationId>' WHERE "ownerId" = '<sourceId>';
|
||||
|
||||
// reassign assets
|
||||
@@ -159,7 +164,7 @@ No, not yet. For updates on this planned feature, follow the [GitHub discussion]
|
||||
|
||||
### Can I add an external library while keeping the existing album structure?
|
||||
|
||||
We haven't put in an official mechanism to create albums from external libraries at the moment, but there are some [workarounds from the community](https://github.com/immich-app/immich/discussions/4279) to help you achieve that.
|
||||
We haven't implemented an official mechanism for creating albums from external libraries, but there are some [workarounds from the community](https://github.com/immich-app/immich/discussions/4279) to help you achieve that.
|
||||
|
||||
### What happens to duplicates in external libraries?
|
||||
|
||||
@@ -171,7 +176,7 @@ Duplicate checking only exists for upload libraries, using the file hash. Furthe
|
||||
|
||||
### How does smart search work?
|
||||
|
||||
Immich uses CLIP models, for more information about CLIP and its capabilities read about it [here](https://openai.com/research/clip).
|
||||
Immich uses CLIP models. For more information about CLIP and its capabilities, read about it [here](https://openai.com/research/clip).
|
||||
|
||||
### How does facial recognition work?
|
||||
|
||||
@@ -189,33 +194,31 @@ However, disabling all jobs will not disable the machine learning service itself
|
||||
|
||||
### I'm getting errors about models being corrupt or failing to download. What do I do?
|
||||
|
||||
You can delete the model cache volume, which is where models are downloaded to. This will give the service a clean environment to download the model again. If models are failing to download entirely, you can manually download them from [Huggingface](https://huggingface.co/immich-app) and place them in the cache folder.
|
||||
|
||||
### Why did Immich decide to remove object detection?
|
||||
|
||||
The feature added keywords to images for metadata search, but wasn't used for smart search. Smart search made it unnecessary as it isn't limited to exact keywords. Combined with it causing crashes on some devices, using many dependencies and causing user confusion as to how search worked, it was better to remove the job altogether.
|
||||
For more info see [here](https://github.com/immich-app/immich/pull/5903)
|
||||
You can delete the model cache volume, where models are downloaded. This will give the service a clean environment to download the model again. If models are failing to download entirely, you can manually download them from [Huggingface][huggingface] and place them in the cache folder.
|
||||
|
||||
### Can I use a custom CLIP model?
|
||||
|
||||
No, this is not supported. Only models listed in the [Huggingface](https://huggingface.co/immich-app) page are compatible. Feel free to make a feature request if there's a model not listed here that you think should be added.
|
||||
No, this is not supported. Only models listed in the [Huggingface][huggingface] page are compatible. Feel free to make a feature request if there's a model not listed here that you think should be added.
|
||||
|
||||
### I want to be able to search in other languages besides English. How can I do that?
|
||||
|
||||
You can change to a multilingual model listed [here](https://huggingface.co/collections/immich-app/multilingual-clip-654eb08c2382f591eeb8c2a7) by going to Administration > Machine Learning Settings > Smart Search and replacing the name of the model. Be sure to re-run Smart Search on all assets after this change. You can then search in over 100 languages.
|
||||
|
||||
:::note
|
||||
Feel free to make a feature request if there's a model you want to use that isn't in [Immich Huggingface list](https://huggingface.co/immich-app).
|
||||
Feel free to make a feature request if there's a model you want to use that isn't in [Immich Huggingface list][huggingface].
|
||||
:::
|
||||
|
||||
### Does Immich support Facial Recognition for videos ?
|
||||
### Does Immich support Facial Recognition for videos?
|
||||
|
||||
Immich's machine learning feature operate on the generated thumbnail. If a face is visible in the video's thumbnail it will be picked up by facial recognition.
|
||||
Immich's machine learning feature operates on the generated thumbnail. If a face is visible in the video's thumbnail it will be picked up by facial recognition.
|
||||
Scanning the entire video for faces may be implemented in the future.
|
||||
|
||||
### Does Immich have animal recognition?
|
||||
|
||||
No.
|
||||
:::tip
|
||||
You can use [Smart Search](/docs/features/smart-search.md) for this to some extent. For example, if you have a Golden Retriever and a Chihuahua, type these words in the smart search and watch the results.
|
||||
:::
|
||||
|
||||
### I'm getting a lot of "faces" that aren't faces, what can I do?
|
||||
|
||||
@@ -248,8 +251,8 @@ The initial backup is the most intensive due to the number of jobs running. The
|
||||
- Lower the job concurrency for these jobs to 1.
|
||||
- Under Settings > Transcoding Settings > Threads, set the number of threads to a low number like 1 or 2.
|
||||
- Under Settings > Machine Learning Settings > Facial Recognition > Model Name, you can change the facial recognition model to `buffalo_s` instead of `buffalo_l`. The former is a smaller and faster model, albeit not as good.
|
||||
- You _must_ re-run the Face Detection job for all images after this for facial recognition on new images to work properly.
|
||||
- If these changes are not enough, see [below](/docs/FAQ#how-can-i-disable-machine-learning) for how you can disable machine learning.
|
||||
- For facial recognition on new images to work properly, You must re-run the Face Detection job for all images after this.
|
||||
- If these changes are not enough, see [below](/docs/FAQ#how-can-i-disable-machine-learning) for instructions on how to disable machine learning.
|
||||
|
||||
### Can I limit the amount of CPU and RAM usage?
|
||||
|
||||
@@ -280,13 +283,17 @@ On a normal machine, 2 or 3 concurrent jobs can probably max the CPU. Beyond thi
|
||||
|
||||
Do not exaggerate with the amount of jobs because you're probably thoroughly overloading the server.
|
||||
|
||||
More detail can be found [here](https://discord.com/channels/979116623879368755/994044917355663450/1174711719994605708)
|
||||
More details can be found [here](https://discord.com/channels/979116623879368755/994044917355663450/1174711719994605708)
|
||||
:::
|
||||
|
||||
### Why is Immich using so much of my CPU?
|
||||
|
||||
When a large amount of assets are uploaded to Immich it makes sense that the CPU and RAM will be heavily used due to machine learning work and creating image thumbnails.
|
||||
Once this process completes, the percentage of CPU usage will drop to around 3-5% usage
|
||||
When a large number of assets are uploaded to Immich, it makes sense that the CPU and RAM will be heavily used for machine learning work and creating image thumbnails.
|
||||
Once this process is completed, the percentage of CPU usage will drop to around 3-5% usage
|
||||
|
||||
### My server shows Server Status Offline | Version Unknown what can I do?
|
||||
|
||||
You need to enable Websocket on your reverse proxy.
|
||||
|
||||
---
|
||||
|
||||
@@ -311,7 +318,7 @@ For a further hardened system, you can add the following block to every containe
|
||||
|
||||
```yaml
|
||||
security_opt:
|
||||
# Prevent escalation of privileges after container is started
|
||||
# Prevent escalation of privileges after the container is started
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
# Prevent access to raw network traffic
|
||||
@@ -322,7 +329,7 @@ cap_drop:
|
||||
|
||||
Data for Immich comes in two forms:
|
||||
|
||||
1. **Metadata** stored in a postgres database, persisted via the `pg_data` volume
|
||||
1. **Metadata** stored in a Postgres database, persisted via the `pg_data` volume
|
||||
2. **Files** (originals, thumbs, profile, etc.), stored in the `UPLOAD_LOCATION` folder, more [info](/docs/administration/backup-and-restore#asset-types-and-storage-locations).
|
||||
|
||||
To remove the **Metadata** you can stop Immich and delete the volume.
|
||||
@@ -359,3 +366,5 @@ If your version of Immich is below 1.92.0 and the crash occurs after logs about
|
||||
### Why does Immich log migration errors on startup?
|
||||
|
||||
Sometimes Immich logs errors such as "duplicate key value violates unique constraint" or "column (...) of relation (...) already exists". Because of Immich's container structure, this error can be seen when both immich and immich-microservices start at the same time and attempt to migrate or create the database structure. Since the database migration is run sequentially and inside of transactions, this error message does not cause harm to your installation of Immich and can safely be ignored. If needed, you can manually restart Immich by running `docker restart immich immich-microservices`.
|
||||
|
||||
[huggingface]: https://huggingface.co/immich-app
|
||||
|
||||
@@ -52,8 +52,8 @@ Before enabling OAuth in Immich, a new client application needs to be configured
|
||||
|
||||
Hostname
|
||||
|
||||
- `https://immich.example.com/auth/login`)
|
||||
- `https://immich.example.com/user-settings`)
|
||||
- `https://immich.example.com/auth/login`
|
||||
- `https://immich.example.com/user-settings`
|
||||
|
||||
## Enable OAuth
|
||||
|
||||
|
||||
@@ -10,9 +10,11 @@ Running with a pre-existing Postgres server can unlock powerful administrative f
|
||||
|
||||
## Prerequisites
|
||||
|
||||
You must install pgvecto.rs using their [instructions](https://docs.pgvecto.rs/getting-started/installation.html). After installation, add `shared_preload_libraries = 'vectors.so'` to your `postgresql.conf`. If you already have some `shared_preload_libraries` set, you can separate each extension with a comma. For example, `shared_preload_libraries = 'pg_stat_statements, vectors.so'`.
|
||||
You must install pgvecto.rs into your instance of Postgres using their [instructions][vectors-install]. After installation, add `shared_preload_libraries = 'vectors.so'` to your `postgresql.conf`. If you already have some `shared_preload_libraries` set, you can separate each extension with a comma. For example, `shared_preload_libraries = 'pg_stat_statements, vectors.so'`.
|
||||
|
||||
:::note
|
||||
Immich is known to work with Postgres versions 14, 15, and 16. Earlier versions are unsupported.
|
||||
|
||||
Make sure the installed version of pgvecto.rs is compatible with your version of Immich. For example, if your Immich version uses the dedicated database image `tensorchord/pgvecto-rs:pg14-v0.2.1`, you must install pgvecto.rs `>= 0.2.1, < 0.3.0`.
|
||||
:::
|
||||
|
||||
@@ -30,6 +32,10 @@ DB_URL='postgresql://immichdbusername:immichdbpassword@postgreshost:postgresport
|
||||
# DB_URL='postgresql://immichdbusername:immichdbpassword@postgreshost:postgresport/immichdatabasename?sslmode=require&sslmode=no-verify'
|
||||
```
|
||||
|
||||
:::info
|
||||
When `DB_URL` is defined, the other database (`DB_*`) variables are ignored, with the exception of `DB_VECTOR_EXTENSION`.
|
||||
:::
|
||||
|
||||
## With superuser permission
|
||||
|
||||
Typically Immich expects superuser permission in the database, which you can grant by running `ALTER USER <immichdbusername> WITH SUPERUSER;` at the `psql` console. If you prefer not to grant superuser permissions, follow the instructions in the next section.
|
||||
@@ -50,8 +56,7 @@ ALTER DATABASE <immichdatabasename> OWNER TO <immichdbusername>;
|
||||
CREATE EXTENSION vectors;
|
||||
CREATE EXTENSION earthdistance CASCADE;
|
||||
ALTER DATABASE <immichdatabasename> SET search_path TO "$user", public, vectors;
|
||||
GRANT USAGE ON SCHEMA vectors TO <immichdbusername>;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA vectors GRANT SELECT ON TABLES TO <immichdbusername>;
|
||||
ALTER SCHEMA vectors OWNER TO <immichdbusername>;
|
||||
COMMIT;
|
||||
```
|
||||
|
||||
@@ -63,4 +68,6 @@ When installing a new version of pgvecto.rs, you will need to manually update th
|
||||
|
||||
#### Permission denied for view
|
||||
|
||||
If you get the error `driverError: error: permission denied for view pg_vector_index_stat`, you can fix this by connecting to the Immich database and running `GRANT SELECT ON TABLE pg_vector_index_stat to <immichdbusername>;`.
|
||||
If you get the error `driverError: error: permission denied for view pg_vector_index_stat`, you can fix this by connecting to the Immich database and running `GRANT SELECT ON TABLE pg_vector_index_stat TO <immichdbusername>;`.
|
||||
|
||||
[vectors-install]: https://docs.pgvecto.rs/getting-started/installation.html
|
||||
|
||||
@@ -37,9 +37,7 @@ You can set the scanning interval using the preset or cron format. For more info
|
||||
|
||||
## Logging
|
||||
|
||||
By default logs are set to record at the log level, the network administrator can choose a deeper or lower level of logs according to his decision or according to the needs required by the Immich support team.
|
||||
|
||||
Here you can [learn about the different error levels](https://sematext.com/blog/logging-levels/).
|
||||
The default Immich log level is `Log` (commonly known as `Info`). The Immich administrator can choose a higher or lower log level according to personal preference or as requested by the Immich support team.
|
||||
|
||||
## Machine Learning Settings
|
||||
|
||||
|
||||
12
docs/docs/community-guides.mdx
Normal file
12
docs/docs/community-guides.mdx
Normal file
@@ -0,0 +1,12 @@
|
||||
# Community Guides
|
||||
|
||||
This page lists community guides that are written around Immich, but not officially supported by the development team.
|
||||
|
||||
:::warning
|
||||
This list comes with no guarantees about security, performance, reliability, or accuracy. Use at your own risk.
|
||||
:::
|
||||
|
||||
import CommunityGuides from '../src/components/community-guides.tsx';
|
||||
import React from 'react';
|
||||
|
||||
<CommunityGuides />
|
||||
@@ -9,7 +9,7 @@ When contributing code through a pull request, please check the following:
|
||||
- [ ] `npm run check:svelte` (Type checking via SvelteKit)
|
||||
- [ ] `npm test` (unit tests)
|
||||
|
||||
:::tip
|
||||
:::tip AIO
|
||||
Run all web checks with `npm run check:all`
|
||||
:::
|
||||
|
||||
@@ -20,10 +20,14 @@ Run all web checks with `npm run check:all`
|
||||
- [ ] `npm run check` (Type checking via `tsc`)
|
||||
- [ ] `npm test` (unit tests)
|
||||
|
||||
:::tip
|
||||
:::tip AIO
|
||||
Run all server checks with `npm run check:all`
|
||||
:::
|
||||
|
||||
:::info Auto Fix
|
||||
You can use `npm run __:fix` to potentially correct some issues automatically for `npm run format` and `lint`.
|
||||
:::
|
||||
|
||||
## OpenAPI
|
||||
|
||||
The OpenAPI client libraries need to be regenerated whenever there are changes to the `immich-openapi-specs.json` file. Note that you should not modify this file directly as it is auto-generated. See [OpenAPI](/docs/developer/open-api.md) for more details.
|
||||
|
||||
@@ -16,7 +16,7 @@ Thanks for being interested in contributing 😊
|
||||
|
||||
## Environment
|
||||
|
||||
### Server and web app
|
||||
### Services
|
||||
|
||||
This environment includes the services below. Additional details are available in each service's README.
|
||||
|
||||
@@ -28,7 +28,7 @@ This environment includes the services below. Additional details are available i
|
||||
|
||||
All the services are packaged to run as with single Docker Compose command.
|
||||
|
||||
### Instructions
|
||||
### Server and web apps
|
||||
|
||||
1. Clone the project repo.
|
||||
2. Run `cp docker/example.env docker/.env`.
|
||||
@@ -47,13 +47,7 @@ You can access the web from `http://your-machine-ip:2283` or `http://localhost:2
|
||||
|
||||
**Note:** the "web" development container runs with uid 1000. If that uid does not have read/write permissions on the mounted volumes, you may encounter errors
|
||||
|
||||
### Mobile app
|
||||
|
||||
The mobile app `(/mobile)` will required Flutter toolchain 3.13.x to be installed on your system.
|
||||
|
||||
Please refer to the [Flutter's official documentation](https://flutter.dev/docs/get-started/install) for more information on setting up the toolchain on your machine.
|
||||
|
||||
### Connect to a remote backend
|
||||
#### Connect web to a remote backend
|
||||
|
||||
If you only want to do web development connected to an existing, remote backend, follow these steps:
|
||||
|
||||
@@ -66,6 +60,14 @@ If you only want to do web development connected to an existing, remote backend,
|
||||
IMMICH_SERVER_URL=https://demo.immich.app/ npm run dev
|
||||
```
|
||||
|
||||
### Mobile app
|
||||
|
||||
The mobile app `(/mobile)` will required Flutter toolchain 3.13.x to be installed on your system.
|
||||
|
||||
Please refer to the [Flutter's official documentation](https://flutter.dev/docs/get-started/install) for more information on setting up the toolchain on your machine.
|
||||
|
||||
The mobile app asks you what backend to connect to. You can utilize the demo backend (https://demo.immich.app/) if you don't need to change server code or upload photos. Alternatively, you can run the server yourself per the instructions above.
|
||||
|
||||
## IDE setup
|
||||
|
||||
### Lint / format extensions
|
||||
|
||||
@@ -8,15 +8,24 @@ Unit are run by calling `npm run test` from the `server` directory.
|
||||
|
||||
### End to end tests
|
||||
|
||||
The backend has two end-to-end test suites that can be called with the following two commands from the project root directory:
|
||||
The e2e tests can be run by first starting up a test production environment via:
|
||||
|
||||
- `make server-e2e-api`
|
||||
- `make server-e2e-jobs`
|
||||
```bash
|
||||
make e2e
|
||||
```
|
||||
|
||||
#### API (e2e)
|
||||
Once the test environment is running, the e2e tests can be run via:
|
||||
|
||||
The API e2e tests spin up a test database and execute http requests against the server, validating the expected response codes and functionality for API endpoints.
|
||||
```bash
|
||||
cd e2e/
|
||||
npm test
|
||||
```
|
||||
|
||||
#### Jobs (e2e)
|
||||
The tests check various things including:
|
||||
|
||||
The Jobs e2e tests spin up a docker test environment where thumbnail generation, library scanning, and other _job_ workflows are validated.
|
||||
- Authentication and authorization
|
||||
- Query param, body, and url validation
|
||||
- Response codes
|
||||
- Thumbnail generation
|
||||
- Metadata extraction
|
||||
- Library scanning
|
||||
|
||||
BIN
docs/docs/features/img/library-custom-scan-interval.png
Normal file
BIN
docs/docs/features/img/library-custom-scan-interval.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
@@ -161,7 +161,7 @@ The christmas trip library will now be scanned in the background. In the meantim
|
||||
|
||||
- Click on Create External Library.
|
||||
|
||||
:::info Note
|
||||
:::note
|
||||
If you get an error here, please rename the other external library to something else. This is a bug that will be fixed in a future release.
|
||||
:::
|
||||
|
||||
@@ -175,3 +175,14 @@ If you get an error here, please rename the other external library to something
|
||||
- Click on Scan Library Files
|
||||
|
||||
Within seconds, the assets from the old-pics and videos folders should show up in the main timeline.
|
||||
|
||||
### Set Custom Scan Interval
|
||||
|
||||
:::note
|
||||
Only an admin can do this.
|
||||
:::
|
||||
|
||||
You can define a custom interval for the trigger external library rescan under Administration -> Settings -> Library.
|
||||
You can set the scanning interval using the preset or cron format. For more information you can refer to [Crontab Guru](https://crontab.guru/).
|
||||
|
||||
<img src={require('./img/library-custom-scan-interval.png').default} width="75%" title='Set custom scan interval for external library' />
|
||||
|
||||
@@ -10,7 +10,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele
|
||||
## Supported Backends
|
||||
|
||||
- ARM NN (Mali)
|
||||
- CUDA (NVIDIA)
|
||||
- CUDA (NVIDIA) Note: It is supported with [compute capability](https://developer.nvidia.com/cuda-gpus) 5.2 or higher
|
||||
- OpenVINO (Intel)
|
||||
|
||||
## Limitations
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Immich supports a number of image and video formats, the most common of which are outlined here.
|
||||
|
||||
:::note
|
||||
For the full list, you can refer to the [Immich source code](https://github.com/immich-app/immich/blob/main/server/src/utils/mime-types.ts).
|
||||
For the full list, refer to the [Immich source code](https://github.com/immich-app/immich/blob/main/server/src/utils/mime-types.ts).
|
||||
:::
|
||||
|
||||
## Image formats
|
||||
|
||||
50
docs/docs/guides/custom-locations.md
Normal file
50
docs/docs/guides/custom-locations.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Files Custom Locations
|
||||
|
||||
This guide explains storing generated and raw files with docker's volume mount in different locations.
|
||||
|
||||
:::note Backup
|
||||
It is important to remember to update the backup settings after following the guide to back up the new backup paths if using automatic backup tools.
|
||||
:::
|
||||
|
||||
In our `.env` file, we will define variables that will help us in the future when we want to move to a more advanced server in the future
|
||||
|
||||
```diff title=".env"
|
||||
# You can find documentation for all the supported env variables at https://immich.app/docs/install/environment-variables
|
||||
|
||||
# Custom location where your uploaded, thumbnails, and transcoded video files are stored
|
||||
- {UPLOAD_LOCATION}=./library
|
||||
+ {UPLOAD_LOCATION}=/custom/location/on/your/system/
|
||||
+ {THUMB_LOCATION}=/custom/location/on/your/system/
|
||||
+ {ENCODED_VIDEO_LOCATION}=/custom/location/on/your/system/
|
||||
...
|
||||
```
|
||||
|
||||
After defining the locations for these files, we will edit the `docker-compose.yml` file accordingly and add the new variables to the `immich-server` and `immich-microservices` containers.
|
||||
|
||||
```diff title="docker-compose.yml"
|
||||
services:
|
||||
immich-server:
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
+ - ${THUMB_LOCATION}:/usr/src/app/upload/thumbs
|
||||
+ - ${ENCODED_VIDEO_LOCATION}:/usr/src/app/upload/encoded-video
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
|
||||
...
|
||||
|
||||
immich-microservices:
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
+ - ${THUMB_LOCATION}:/usr/src/app/upload/thumbs
|
||||
+ - ${ENCODED_VIDEO_LOCATION}:/usr/src/app/upload/encoded-video
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
```
|
||||
|
||||
Restart Immich to register the changes.
|
||||
|
||||
```
|
||||
docker compose down
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Thanks to [Jrasm91](https://github.com/immich-app/immich/discussions/2110#discussioncomment-5477767) for writing the guide.
|
||||
@@ -45,5 +45,5 @@ Open pgAdmin and click "Add New Server".
|
||||
Click on "Save" to connect to the Immich database.
|
||||
|
||||
:::tip
|
||||
View [Database Queries](https://immich.app/docs/guides/database-queries/) for common database queries.
|
||||
View [Database Queries](/docs/guides/database-queries/) for common database queries.
|
||||
:::
|
||||
|
||||
@@ -17,7 +17,7 @@ The `"originalFileName"` column is the name of the file at time of upload, inclu
|
||||
:::
|
||||
|
||||
```sql title="Find by original filename"
|
||||
SELECT * FROM "assets" WHERE "originalFileName" = 'PXL_20230903_232542848';
|
||||
SELECT * FROM "assets" WHERE "originalFileName" = 'PXL_20230903_232542848.jpg';
|
||||
SELECT * FROM "assets" WHERE "originalFileName" LIKE 'PXL_%'; -- all files starting with PXL_
|
||||
SELECT * FROM "assets" WHERE "originalFileName" LIKE '%_2023_%'; -- all files with _2023_ in the middle
|
||||
```
|
||||
@@ -27,21 +27,30 @@ SELECT * FROM "assets" WHERE "originalPath" = 'upload/library/admin/2023/2023-09
|
||||
SELECT * FROM "assets" WHERE "originalPath" LIKE 'upload/library/admin/2023/%';
|
||||
```
|
||||
|
||||
```sql title="Find by checksum" (sha1)
|
||||
:::note
|
||||
You can calculate the checksum for a particular file by using the command `sha1sum <filename>`.
|
||||
:::
|
||||
|
||||
```sql title="Find by checksum (SHA-1)"
|
||||
SELECT encode("checksum", 'hex') FROM "assets";
|
||||
SELECT * FROM "assets" WHERE "checksum" = decode('69de19c87658c4c15d9cacb9967b8e033bf74dd1', 'hex');
|
||||
```
|
||||
|
||||
```sql title="Live photos"
|
||||
SELECT * FROM "assets" where "livePhotoVideoId" IS NOT NULL;
|
||||
SELECT * FROM "assets" WHERE "livePhotoVideoId" IS NOT NULL;
|
||||
```
|
||||
|
||||
```sql title="Without metadata"
|
||||
SELECT "assets".* FROM "exif" LEFT JOIN "assets" ON "assets"."id" = "exif"."assetId" WHERE "exif"."assetId" IS NULL;
|
||||
SELECT "assets".* FROM "exif"
|
||||
LEFT JOIN "assets" ON "assets"."id" = "exif"."assetId"
|
||||
WHERE "exif"."assetId" IS NULL;
|
||||
```
|
||||
|
||||
```sql title="size < 100,000 bytes, smallest to largest"
|
||||
SELECT * FROM "assets" JOIN "exif" ON "assets"."id" = "exif"."assetId" WHERE "exif"."fileSizeInByte" < 100000 ORDER BY "exif"."fileSizeInByte" ASC;
|
||||
SELECT * FROM "assets"
|
||||
JOIN "exif" ON "assets"."id" = "exif"."assetId"
|
||||
WHERE "exif"."fileSizeInByte" < 100000
|
||||
ORDER BY "exif"."fileSizeInByte" ASC;
|
||||
```
|
||||
|
||||
```sql title="Without thumbnails"
|
||||
@@ -54,20 +63,14 @@ SELECT * FROM "assets" WHERE "assets"."type" = 'IMAGE';
|
||||
```
|
||||
|
||||
```sql title="Count by type"
|
||||
SELECT "assets"."type", count(*) FROM "assets" GROUP BY "assets"."type";
|
||||
SELECT "assets"."type", COUNT(*) FROM "assets" GROUP BY "assets"."type";
|
||||
```
|
||||
|
||||
```sql title="Count by type (per user)"
|
||||
SELECT
|
||||
"users"."email", "assets"."type", COUNT(*)
|
||||
FROM
|
||||
"assets"
|
||||
JOIN
|
||||
"users" ON "assets"."ownerId" = "users"."id"
|
||||
GROUP BY
|
||||
"assets"."type", "users"."email"
|
||||
ORDER BY
|
||||
"users"."email";
|
||||
SELECT "users"."email", "assets"."type", COUNT(*) FROM "assets"
|
||||
JOIN "users" ON "assets"."ownerId" = "users"."id"
|
||||
GROUP BY "assets"."type", "users"."email"
|
||||
ORDER BY "users"."email";
|
||||
```
|
||||
|
||||
```sql title="Failed file movements"
|
||||
@@ -76,7 +79,7 @@ SELECT * FROM "move_history";
|
||||
|
||||
## Users
|
||||
|
||||
```sql title="List"
|
||||
```sql title="List all users"
|
||||
SELECT * FROM "users";
|
||||
```
|
||||
|
||||
@@ -87,3 +90,9 @@ SELECT "key", "value" FROM "system_config";
|
||||
```
|
||||
|
||||
(Only used when not using the [config file](/docs/install/config-file))
|
||||
|
||||
## Persons
|
||||
|
||||
```sql title="Delete person and unset it for the faces it was associated with"
|
||||
DELETE FROM person WHERE name = 'PersonNameHere';
|
||||
```
|
||||
|
||||
@@ -14,12 +14,6 @@ Edit `docker-compose.yml` to add two new mount points under `volumes:`
|
||||
- ${EXTERNAL_PATH}:/usr/src/app/external
|
||||
```
|
||||
|
||||
```
|
||||
immich-microservices:
|
||||
volumes:
|
||||
- ${EXTERNAL_PATH}:/usr/src/app/external
|
||||
```
|
||||
|
||||
Be sure to add exactly the same line to both `immich-server:` and `immich-microservices:`.
|
||||
|
||||
Edit `.env` to define `EXTERNAL_PATH`, substituting in the correct path for your computer:
|
||||
|
||||
@@ -6,10 +6,14 @@ To alleviate [performance issues on low-memory systems](/docs/FAQ.mdx#why-is-imm
|
||||
- Copy the following `docker-compose.yml` to your ML system.
|
||||
- Start the container by running `docker compose up -d`.
|
||||
|
||||
:::note Info
|
||||
:::info
|
||||
Starting with version v1.93.0 face detection work and face recognize were split. From now on face detection is done in the immich_machine_learning service, but facial recognition is done in the immich_microservices service.
|
||||
:::
|
||||
|
||||
:::note
|
||||
The [hwaccel.ml.yml](https://github.com/immich-app/immich/releases/latest/download/hwaccel.ml.yml) file also needs to be in the same folder if trying to use [hardware acceleration](/docs/features/ml-hardware-acceleration).
|
||||
:::
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
|
||||
@@ -120,7 +120,8 @@ The default configuration looks like this:
|
||||
"previewFormat": "jpeg",
|
||||
"previewSize": 1440,
|
||||
"quality": 80,
|
||||
"colorspace": "p3"
|
||||
"colorspace": "p3",
|
||||
"extractEmbedded": false
|
||||
},
|
||||
"newVersionCheck": {
|
||||
"enabled": true
|
||||
|
||||
@@ -7,7 +7,8 @@ import ExampleEnv from '!!raw-loader!../../../docker/example.env';
|
||||
|
||||
# Docker Compose [Recommended]
|
||||
|
||||
Docker Compose is the recommended method to run Immich in production. Below are the steps to deploy Immich with Docker Compose.
|
||||
Docker Compose is the recommended method to run Immich in production. Below are the steps to deploy Immich with Docker Compose.
|
||||
Immich requires Docker Compose version 2.x.
|
||||
|
||||
### Step 1 - Download the required files
|
||||
|
||||
@@ -66,7 +67,7 @@ docker compose up -d
|
||||
```
|
||||
|
||||
:::info Docker version
|
||||
If you get an error `unknown shorthand flag: 'd' in -d`, you are probably running the wrong Docker version. (This happens, for example, with the docker.io package in Ubuntu 22.04.3 LTS.) You can correct the problem by `apt remove`ing Ubuntu's docker.io package and installing docker and docker-compose via [Docker's official repository](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository).
|
||||
If you get an error `unknown shorthand flag: 'd' in -d`, you are probably running the wrong Docker version. (This happens, for example, with the docker.io package in Ubuntu 22.04.3 LTS.) You can correct the problem by `apt remove`ing Ubuntu's docker.io package and installing docker and docker-compose via [Docker's official repository][docker-repo].
|
||||
|
||||
Note that the correct command really is `docker compose`, not `docker-compose`. If you try the latter on vanilla Ubuntu 22.04 it will fail in a different way:
|
||||
|
||||
@@ -83,27 +84,31 @@ For more information on how to use the application, please refer to the [Post In
|
||||
:::
|
||||
|
||||
:::note GitHub Authentication
|
||||
Downloading container images might require you to authenticate to the GitHub Container Registry ([steps here](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-to-the-container-registry)).
|
||||
Downloading container images might require you to authenticate to the GitHub Container Registry ([steps here][container-auth]).
|
||||
:::
|
||||
|
||||
### Step 4 - Upgrading
|
||||
|
||||
:::danger Breaking Changes
|
||||
It is important to follow breaking updates to avoid problems. You can see versions that had breaking changes [here](https://github.com/immich-app/immich/discussions?discussions_q=label%3Abreaking-change+sort%3Adate_created).
|
||||
It is important to follow breaking updates to avoid problems. You can see versions that had breaking changes [here][breaking].
|
||||
:::
|
||||
|
||||
If `IMMICH_VERSION` is set, it will need to be updated to the latest or desired version.
|
||||
|
||||
When a new version of Immich is [released](https://github.com/immich-app/immich/releases), the application can be upgraded with the following commands, run in the directory with the `docker-compose.yml` file:
|
||||
When a new version of Immich is [released][releases], the application can be upgraded with the following commands, run in the directory with the `docker-compose.yml` file:
|
||||
|
||||
```bash title="Upgrade Immich"
|
||||
docker compose pull && docker compose up -d
|
||||
```
|
||||
|
||||
:::caution Automatic Updates
|
||||
Immich is currently under heavy development, which means you can expect [breaking changes](https://github.com/immich-app/immich/discussions?discussions_q=label%3Abreaking-change+sort%3Adate_created) and bugs. Therefore, we recommend reading the release notes prior to updating and to take special care when using automated tools like [Watchtower][watchtower].
|
||||
Immich is currently under heavy development, which means you can expect [breaking changes][breaking] and bugs. Therefore, we recommend reading the release notes prior to updating and to take special care when using automated tools like [Watchtower][watchtower].
|
||||
:::
|
||||
|
||||
[compose-file]: https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
|
||||
[env-file]: https://github.com/immich-app/immich/releases/latest/download/example.env
|
||||
[watchtower]: https://containrrr.dev/watchtower/
|
||||
[breaking]: https://github.com/immich-app/immich/discussions?discussions_q=label%3Abreaking-change+sort%3Adate_created
|
||||
[container-auth]: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-to-the-container-registry
|
||||
[releases]: https://github.com/immich-app/immich/releases
|
||||
[docker-repo]: https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository
|
||||
|
||||
@@ -41,19 +41,20 @@ These environment variables are used by the `docker-compose.yml` file and do **N
|
||||
| `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | microservices |
|
||||
|
||||
:::tip
|
||||
`TZ` should be set to a `TZ identifier` from [this list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List). For example, `TZ="Etc/UTC"`.
|
||||
`TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`.
|
||||
|
||||
`TZ` is only used by `exiftool`, which is present in the microservices container, as a fallback in case the timezone cannot be determined from the image metadata.
|
||||
:::
|
||||
|
||||
## Ports
|
||||
|
||||
| Variable | Description | Default | Services |
|
||||
| :---------------------- | :-------------------- | :-------: | :--------------- |
|
||||
| `SERVER_PORT` | Server Port | `3001` | server |
|
||||
| `MICROSERVICES_PORT` | Microservices Port | `3002` | microservices |
|
||||
| `MACHINE_LEARNING_HOST` | Machine Learning Host | `0.0.0.0` | machine learning |
|
||||
| `MACHINE_LEARNING_PORT` | Machine Learning Port | `3003` | machine learning |
|
||||
| Variable | Description | Default | Services |
|
||||
| :---------------------- | :-------------------- | :-------: | :-------------------- |
|
||||
| `HOST` | Host | `0.0.0.0` | server, microservices |
|
||||
| `SERVER_PORT` | Server Port | `3001` | server |
|
||||
| `MICROSERVICES_PORT` | Microservices Port | `3002` | microservices |
|
||||
| `MACHINE_LEARNING_HOST` | Machine Learning Host | `0.0.0.0` | machine learning |
|
||||
| `MACHINE_LEARNING_PORT` | Machine Learning Port | `3003` | machine learning |
|
||||
|
||||
## Database
|
||||
|
||||
@@ -71,7 +72,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N
|
||||
|
||||
:::info
|
||||
|
||||
When `DB_URL` is defined, the other database (`DB_*`) variables are ignored.
|
||||
When `DB_URL` is defined, the other database (`DB_*`) variables are ignored, with the exception of `DB_VECTOR_EXTENSION`.
|
||||
|
||||
:::
|
||||
|
||||
@@ -90,7 +91,7 @@ When `DB_URL` is defined, the other database (`DB_*`) variables are ignored.
|
||||
:::info
|
||||
|
||||
`REDIS_URL` must start with `ioredis://` and then include a `base64` encoded JSON string for the configuration.
|
||||
More info can be found in the upstream [ioredis](https://ioredis.readthedocs.io/en/latest/API/) documentation.
|
||||
More info can be found in the upstream [ioredis][redis-api] documentation.
|
||||
|
||||
- When `REDIS_URL` is defined, the other redis (`REDIS_*`) variables are ignored.
|
||||
- When `REDIS_SOCKET` is defined, the other redis (`REDIS_*`) variables are ignored.
|
||||
@@ -158,7 +159,7 @@ Other machine learning parameters can be tuned from the admin UI.
|
||||
|
||||
## Docker Secrets
|
||||
|
||||
The following variables support the use of [Docker secrets](https://docs.docker.com/engine/swarm/secrets/) for additional security.
|
||||
The following variables support the use of [Docker secrets][docker-secrets] for additional security.
|
||||
|
||||
To use any of these, replace the regular environment variable with the equivalent `_FILE` environment variable. The value of
|
||||
the `_FILE` variable should be set to the path of a file containing the variable value.
|
||||
@@ -172,8 +173,14 @@ the `_FILE` variable should be set to the path of a file containing the variable
|
||||
| `DB_URL` | `DB_URL_FILE`<sup>\*1</sup> |
|
||||
| `REDIS_PASSWORD` | `REDIS_PASSWORD_FILE`<sup>\*2</sup> |
|
||||
|
||||
\*1: See the [official documentation](https://github.com/docker-library/docs/tree/master/postgres#docker-secrets) for
|
||||
\*1: See the [official documentation][docker-secrets-docs] for
|
||||
details on how to use Docker Secrets in the Postgres image.
|
||||
|
||||
\*2: See [this comment](https://github.com/docker-library/redis/issues/46#issuecomment-335326234) for an example of how
|
||||
\*2: See [this comment][docker-secrets-example] for an example of how
|
||||
to use use a Docker secret for the password in the Redis container.
|
||||
|
||||
[tz-list]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List
|
||||
[docker-secrets-example]: https://github.com/docker-library/redis/issues/46#issuecomment-335326234
|
||||
[docker-secrets-docs]: https://github.com/docker-library/docs/tree/master/postgres#docker-secrets
|
||||
[docker-secrets]: https://docs.docker.com/engine/swarm/secrets/
|
||||
[redis-api]: https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository
|
||||
|
||||
@@ -6,7 +6,7 @@ sidebar_position: 40
|
||||
|
||||
You can deploy Immich on Kubernetes using [the official Helm chart](https://github.com/immich-app/immich-charts/tree/main/charts/immich).
|
||||
|
||||
If you want examples of how other people run Immich on Kubernetes, using the official chart or otherwise, you can find them at https://nanne.dev/k8s-at-home-search/#/immich.
|
||||
You can view some [examples](https://kubesearch.dev/#/immich) of how other people run Immich on Kubernetes, using the official chart or otherwise.
|
||||
|
||||
:::caution DNS in Alpine containers
|
||||
Immich makes use of Alpine container images. These can encounter [a DNS resolution bug](https://stackoverflow.com/a/65593511) on Kubernetes clusters if the host
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Immich allows the admin user to set the uploaded filename pattern. Both at the directory and filename level.
|
||||
Immich allows the admin user to set the uploaded filename pattern at the directory and filename level as well as the [storage label for a user](/docs/administration/user-management/#set-storage-label-for-user).
|
||||
|
||||
:::note new version
|
||||
On new machines running version 1.92.0 storage template engine is off by default, for [more info](https://github.com/immich-app/immich/releases/tag/v1.92.0#:~:text=the%20partner%E2%80%99s%20assets.-,Hardening%20storage%20template,-We%20have%20further).
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
If you have friends or family members who want to use the application as well, you can create addition accounts. The default password is `password`, and the user can change their password after logging in to the application for the first time.
|
||||
If you have friends or family members who want to use the application as well, you can create addition accounts. The default password is `password`, and the user has to change their password after logging in to the application for the first time. The system administrator can disable this option by unchecking the option "Require user to change password on first login" in the user registration interface.
|
||||
|
||||
<img src={require('./img/create-new-user.png').default} title='Admin Registration' />
|
||||
<img src={require('./img/create-new-user.png').default} width="90%" title='New User Registration' />
|
||||
|
||||
@@ -144,6 +144,10 @@ const config = {
|
||||
label: 'Discord',
|
||||
href: 'https://discord.com/invite/D8JsnBEuKb',
|
||||
},
|
||||
{
|
||||
label: 'Reddit',
|
||||
href: 'https://www.reddit.com/r/immich/',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -157,6 +161,10 @@ const config = {
|
||||
label: 'GitHub',
|
||||
href: 'https://github.com/immich-app/immich',
|
||||
},
|
||||
{
|
||||
label: 'YouTube',
|
||||
href: 'https://www.youtube.com/@immich-app',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
6
docs/package-lock.json
generated
6
docs/package-lock.json
generated
@@ -16125,9 +16125,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.4.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz",
|
||||
"integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==",
|
||||
"version": "5.4.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
|
||||
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
||||
71
docs/src/components/community-guides.tsx
Normal file
71
docs/src/components/community-guides.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import Link from '@docusaurus/Link';
|
||||
import React from 'react';
|
||||
|
||||
interface CommunityGuidesProps {
|
||||
title: string;
|
||||
description: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
const guides: CommunityGuidesProps[] = [
|
||||
{
|
||||
title: 'Cloudflare Tunnels with SSO/OAuth',
|
||||
description: `Setting up Cloudflare Tunnels and a SaaS App for immich.`,
|
||||
url: 'https://github.com/immich-app/immich/discussions/8299',
|
||||
},
|
||||
{
|
||||
title: 'Database backup in Truenas',
|
||||
description: `Create a database backup with pgAdmin in Truenas.`,
|
||||
url: 'https://github.com/immich-app/immich/discussions/8809',
|
||||
},
|
||||
{
|
||||
title: 'Unraid backup scripts',
|
||||
description: `Back up your assets in Unarid with a pre-prepared script.`,
|
||||
url: 'https://github.com/immich-app/immich/discussions/8416',
|
||||
},
|
||||
{
|
||||
title: 'Sync folders with albums',
|
||||
description: `synchronize folders in imported library with albums having the folders name.`,
|
||||
url: 'https://github.com/immich-app/immich/discussions/3382',
|
||||
},
|
||||
{
|
||||
title: 'Podman/Quadlets Install',
|
||||
description: 'Documentation for simple podman setup using quadlets.',
|
||||
url: 'https://github.com/tbelway/immich-podman-quadlets/blob/main/docs/install/podman-quadlet.md',
|
||||
},
|
||||
];
|
||||
|
||||
function CommunityGuide({ title, description, url }: CommunityGuidesProps): JSX.Element {
|
||||
return (
|
||||
<section className="flex flex-col gap-4 justify-between dark:bg-immich-dark-gray bg-immich-gray dark:border-0 border-gray-200 border border-solid rounded-2xl p-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="m-0 items-start flex gap-2">
|
||||
<span>{title}</span>
|
||||
</p>
|
||||
|
||||
<p className="m-0 text-sm text-gray-600 dark:text-gray-300">{description}</p>
|
||||
<p className="m-0 text-sm text-gray-600 dark:text-gray-300">
|
||||
<a href={url}>{url}</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Link
|
||||
className="px-4 py-2 bg-immich-primary/10 dark:bg-gray-300 rounded-full hover:no-underline text-immich-primary dark:text-immich-dark-bg font-bold uppercase"
|
||||
to={url}
|
||||
>
|
||||
View Guide
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CommunityGuides(): JSX.Element {
|
||||
return (
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||
{guides.map((guides) => (
|
||||
<CommunityGuide {...guides} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -34,9 +34,29 @@ const projects: CommunityProjectProps[] = [
|
||||
url: 'https://github.com/Salvoxia/immich-folder-album-creator',
|
||||
},
|
||||
{
|
||||
title: 'Podman/Quadlets Install',
|
||||
description: 'Documentation for simple podman setup using quadlets.',
|
||||
url: 'https://github.com/tbelway/immich-podman-quadlets/blob/main/docs/install/podman-quadlet.md',
|
||||
title: 'Lightroom Publisher: mi.Immich.Publisher',
|
||||
description: 'Lightroom plugin to publish photos from Lightroom collections to Immich albums.',
|
||||
url: 'https://github.com/midzelis/mi.Immich.Publisher',
|
||||
},
|
||||
{
|
||||
title: 'Immich Duplicate Finder',
|
||||
description: 'Webapp that uses machine learning to identify near-duplicate images.',
|
||||
url: 'https://github.com/vale46n1/immich_duplicate_finder',
|
||||
},
|
||||
{
|
||||
title: 'Immich-Tiktok-Remover',
|
||||
description: 'Script to search for and remove TikTok videos from your Immich library.',
|
||||
url: 'https://github.com/mxc2/immich-tiktok-remover',
|
||||
},
|
||||
{
|
||||
title: 'Immich Android TV',
|
||||
description: 'Unofficial Immich Android TV app.',
|
||||
url: 'https://github.com/giejay/Immich-Android-TV',
|
||||
},
|
||||
{
|
||||
title: 'Powershell Module PSImmich',
|
||||
description: 'Powershell Module for the Immich API',
|
||||
url: 'https://github.com/hanpq/PSImmich',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -51,12 +51,22 @@ import {
|
||||
mdiVideo,
|
||||
mdiWeb,
|
||||
mdiScaleBalance,
|
||||
mdiMagnifyScan,
|
||||
} from '@mdi/js';
|
||||
import Layout from '@theme/Layout';
|
||||
import React from 'react';
|
||||
import Timeline, { DateType, Item } from '../components/timeline';
|
||||
|
||||
const items: Item[] = [
|
||||
{
|
||||
icon: mdiMagnifyScan,
|
||||
description: 'Advanced search with filters by date, location and more',
|
||||
title: 'Search enhancement with advanced filters',
|
||||
release: 'v1.95.0',
|
||||
tag: 'v1.95.0',
|
||||
date: new Date(2024, 1, 20),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
icon: mdiScaleBalance,
|
||||
description: 'Immich switches to AGPLv3 license',
|
||||
|
||||
@@ -36,7 +36,7 @@ services:
|
||||
<<: *server-common
|
||||
|
||||
redis:
|
||||
image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70
|
||||
image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
|
||||
|
||||
database:
|
||||
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
||||
|
||||
847
e2e/package-lock.json
generated
847
e2e/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "1.101.0",
|
||||
"version": "1.102.3",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
@@ -43,6 +43,7 @@
|
||||
"socket.io-client": "^4.7.4",
|
||||
"supertest": "^6.3.4",
|
||||
"typescript": "^5.3.3",
|
||||
"utimes": "^5.2.1",
|
||||
"vitest": "^1.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -572,6 +572,22 @@ describe('/asset', () => {
|
||||
}
|
||||
|
||||
const tests = [
|
||||
{
|
||||
input: 'formats/avif/8bit-sRGB.avif',
|
||||
expected: {
|
||||
type: AssetTypeEnum.Image,
|
||||
originalFileName: '8bit-sRGB.avif',
|
||||
resized: true,
|
||||
exifInfo: {
|
||||
description: '',
|
||||
exifImageHeight: 1080,
|
||||
exifImageWidth: 1617,
|
||||
fileSizeInByte: 862_424,
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: 'formats/jpg/el_torcal_rocks.jpg',
|
||||
expected: {
|
||||
@@ -596,6 +612,22 @@ describe('/asset', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: 'formats/jxl/8bit-sRGB.jxl',
|
||||
expected: {
|
||||
type: AssetTypeEnum.Image,
|
||||
originalFileName: '8bit-sRGB.jxl',
|
||||
resized: true,
|
||||
exifInfo: {
|
||||
description: '',
|
||||
exifImageHeight: 1080,
|
||||
exifImageWidth: 1440,
|
||||
fileSizeInByte: 1_780_777,
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: 'formats/heic/IMG_2682.heic',
|
||||
expected: {
|
||||
@@ -681,6 +713,80 @@ describe('/asset', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: 'formats/raw/Panasonic/DMC-GH4/4_3.rw2',
|
||||
expected: {
|
||||
type: AssetTypeEnum.Image,
|
||||
originalFileName: '4_3.rw2',
|
||||
resized: true,
|
||||
fileCreatedAt: '2018-05-10T08:42:37.842Z',
|
||||
exifInfo: {
|
||||
make: 'Panasonic',
|
||||
model: 'DMC-GH4',
|
||||
exifImageHeight: 3456,
|
||||
exifImageWidth: 4608,
|
||||
exposureTime: '1/100',
|
||||
fNumber: 3.2,
|
||||
focalLength: 35,
|
||||
iso: 400,
|
||||
fileSizeInByte: 19_587_072,
|
||||
dateTimeOriginal: '2018-05-10T08:42:37.842Z',
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
orientation: '1',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: 'formats/raw/Sony/ILCE-6300/12bit-compressed-(3_2).arw',
|
||||
expected: {
|
||||
type: AssetTypeEnum.Image,
|
||||
originalFileName: '12bit-compressed-(3_2).arw',
|
||||
resized: true,
|
||||
fileCreatedAt: '2016-09-27T10:51:44.000Z',
|
||||
exifInfo: {
|
||||
make: 'SONY',
|
||||
model: 'ILCE-6300',
|
||||
exifImageHeight: 4024,
|
||||
exifImageWidth: 6048,
|
||||
exposureTime: '1/320',
|
||||
fNumber: 8,
|
||||
focalLength: 97,
|
||||
iso: 100,
|
||||
lensModel: 'E PZ 18-105mm F4 G OSS',
|
||||
fileSizeInByte: 25_001_984,
|
||||
dateTimeOriginal: '2016-09-27T10:51:44.000Z',
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
orientation: '1',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: 'formats/raw/Sony/ILCE-7M2/14bit-uncompressed-(3_2).arw',
|
||||
expected: {
|
||||
type: AssetTypeEnum.Image,
|
||||
originalFileName: '14bit-uncompressed-(3_2).arw',
|
||||
resized: true,
|
||||
fileCreatedAt: '2016-01-08T15:08:01.000Z',
|
||||
exifInfo: {
|
||||
make: 'SONY',
|
||||
model: 'ILCE-7M2',
|
||||
exifImageHeight: 4024,
|
||||
exifImageWidth: 6048,
|
||||
exposureTime: '1.3',
|
||||
fNumber: 22,
|
||||
focalLength: 25,
|
||||
iso: 100,
|
||||
lensModel: 'E 25mm F2',
|
||||
fileSizeInByte: 49_512_448,
|
||||
dateTimeOriginal: '2016-01-08T15:08:01.000Z',
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
orientation: '1',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const { input, expected } of tests) {
|
||||
@@ -816,15 +922,15 @@ describe('/asset', () => {
|
||||
});
|
||||
|
||||
it('should not include gps data for webp thumbnails', async () => {
|
||||
const { status, body, type } = await request(app)
|
||||
.get(`/asset/thumbnail/${locationAsset.id}?format=WEBP`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
await utils.waitForWebsocketEvent({
|
||||
event: 'assetUpload',
|
||||
id: locationAsset.id,
|
||||
});
|
||||
|
||||
const { status, body, type } = await request(app)
|
||||
.get(`/asset/thumbnail/${locationAsset.id}?format=WEBP`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toBeDefined();
|
||||
expect(type).toBe('image/webp');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LoginResponseDto, getAuthDevices, login, signUpAdmin } from '@immich/sdk';
|
||||
import { loginDto, signupDto, uuidDto } from 'src/fixtures';
|
||||
import { deviceDto, errorDto, loginResponseDto, signupResponseDto } from 'src/responses';
|
||||
import { app, asBearerAuth, utils } from 'src/utils';
|
||||
import { LoginResponseDto, login, signUpAdmin } from '@immich/sdk';
|
||||
import { loginDto, signupDto } from 'src/fixtures';
|
||||
import { errorDto, loginResponseDto, signupResponseDto } from 'src/responses';
|
||||
import { app, utils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
@@ -112,70 +112,29 @@ describe('/auth/*', () => {
|
||||
|
||||
const cookies = headers['set-cookie'];
|
||||
expect(cookies).toHaveLength(3);
|
||||
expect(cookies[0]).toEqual(`immich_access_token=${token}; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;`);
|
||||
expect(cookies[1]).toEqual('immich_auth_type=password; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;');
|
||||
expect(cookies[2]).toEqual('immich_is_authenticated=true; Path=/; Max-Age=34560000; SameSite=Lax;');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /auth/devices', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get('/auth/devices');
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should get a list of authorized devices', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/auth/devices')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual([deviceDto.current]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /auth/devices', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).delete(`/auth/devices`);
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should logout all devices (except the current one)', async () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await login({ loginCredentialDto: loginDto.admin });
|
||||
}
|
||||
|
||||
await expect(getAuthDevices({ headers: asBearerAuth(admin.accessToken) })).resolves.toHaveLength(6);
|
||||
|
||||
const { status } = await request(app).delete(`/auth/devices`).set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(204);
|
||||
|
||||
await expect(getAuthDevices({ headers: asBearerAuth(admin.accessToken) })).resolves.toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should throw an error for a non-existent device id', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/auth/devices/${uuidDto.notFound}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest('Not found or no authDevice.delete access'));
|
||||
});
|
||||
|
||||
it('should logout a device', async () => {
|
||||
const [device] = await getAuthDevices({
|
||||
headers: asBearerAuth(admin.accessToken),
|
||||
});
|
||||
const { status } = await request(app)
|
||||
.delete(`/auth/devices/${device.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(204);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/auth/validateToken')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(response.body).toEqual(errorDto.invalidToken);
|
||||
expect(response.status).toBe(401);
|
||||
expect(cookies[0].split(';').map((item) => item.trim())).toEqual([
|
||||
`immich_access_token=${token}`,
|
||||
'Max-Age=34560000',
|
||||
'Path=/',
|
||||
expect.stringContaining('Expires='),
|
||||
'HttpOnly',
|
||||
'SameSite=Lax',
|
||||
]);
|
||||
expect(cookies[1].split(';').map((item) => item.trim())).toEqual([
|
||||
'immich_auth_type=password',
|
||||
'Max-Age=34560000',
|
||||
'Path=/',
|
||||
expect.stringContaining('Expires='),
|
||||
'HttpOnly',
|
||||
'SameSite=Lax',
|
||||
]);
|
||||
expect(cookies[2].split(';').map((item) => item.trim())).toEqual([
|
||||
'immich_is_authenticated=true',
|
||||
'Max-Age=34560000',
|
||||
'Path=/',
|
||||
expect.stringContaining('Expires='),
|
||||
'SameSite=Lax',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -6,12 +6,13 @@ import {
|
||||
getAllLibraries,
|
||||
scanLibrary,
|
||||
} from '@immich/sdk';
|
||||
import { existsSync, rmdirSync } from 'node:fs';
|
||||
import { cpSync, existsSync } from 'node:fs';
|
||||
import { Socket } from 'socket.io-client';
|
||||
import { userDto, uuidDto } from 'src/fixtures';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { app, asBearerAuth, testAssetDir, testAssetDirInternal, utils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { utimes } from 'utimes';
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
const scan = async (accessToken: string, id: string, dto: ScanLibraryDto = {}) =>
|
||||
@@ -26,23 +27,21 @@ describe('/library', () => {
|
||||
beforeAll(async () => {
|
||||
await utils.resetDatabase();
|
||||
admin = await utils.adminSetup();
|
||||
await utils.resetAdminConfig(admin.accessToken);
|
||||
user = await utils.userSetup(admin.accessToken, userDto.user1);
|
||||
library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, type: LibraryType.External });
|
||||
websocket = await utils.connectWebsocket(admin.accessToken);
|
||||
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetA.png`);
|
||||
utils.createImageFile(`${testAssetDir}/temp/directoryB/assetB.png`);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
utils.disconnectWebsocket(websocket);
|
||||
utils.resetTempFolder();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
utils.resetEvents();
|
||||
const tempDir = `${testAssetDir}/temp`;
|
||||
if (existsSync(tempDir)) {
|
||||
rmdirSync(tempDir, { recursive: true });
|
||||
}
|
||||
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetA.png`);
|
||||
utils.createImageFile(`${testAssetDir}/temp/directoryB/assetB.png`);
|
||||
});
|
||||
|
||||
describe('GET /library', () => {
|
||||
@@ -357,95 +356,6 @@ describe('/library', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /library/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).delete(`/library/${uuidDto.notFound}`);
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should not delete the last upload library', async () => {
|
||||
const libraries = await getAllLibraries(
|
||||
{ $type: LibraryType.Upload },
|
||||
{ headers: asBearerAuth(admin.accessToken) },
|
||||
);
|
||||
|
||||
const adminLibraries = libraries.filter((library) => library.ownerId === admin.userId);
|
||||
expect(adminLibraries.length).toBeGreaterThanOrEqual(1);
|
||||
const lastLibrary = adminLibraries.pop() as LibraryResponseDto;
|
||||
|
||||
// delete all but the last upload library
|
||||
for (const library of adminLibraries) {
|
||||
const { status } = await request(app)
|
||||
.delete(`/library/${library.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(204);
|
||||
}
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/library/${lastLibrary.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(body).toEqual(errorDto.noDeleteUploadLibrary);
|
||||
expect(status).toBe(400);
|
||||
});
|
||||
|
||||
it('should delete an external library', async () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
});
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/library/${library.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(204);
|
||||
expect(body).toEqual({});
|
||||
|
||||
const libraries = await getAllLibraries({}, { headers: asBearerAuth(admin.accessToken) });
|
||||
expect(libraries).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: library.id,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should delete an external library with assets', async () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
importPaths: [`${testAssetDirInternal}/temp`],
|
||||
});
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 2 });
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/library/${library.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(204);
|
||||
expect(body).toEqual({});
|
||||
|
||||
const libraries = await getAllLibraries({}, { headers: asBearerAuth(admin.accessToken) });
|
||||
expect(libraries).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: library.id,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
// ensure no files get deleted
|
||||
expect(existsSync(`${testAssetDir}/temp/directoryA/assetA.png`)).toBe(true);
|
||||
expect(existsSync(`${testAssetDir}/temp/directoryB/assetB.png`)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /library/:id/statistics', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get(`/library/${uuidDto.notFound}/statistics`);
|
||||
@@ -549,6 +459,150 @@ describe('/library', () => {
|
||||
const { assets: newAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
expect(newAssets.count).toBe(3);
|
||||
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
|
||||
});
|
||||
|
||||
it('should offline missing files', async () => {
|
||||
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
importPaths: [`${testAssetDirInternal}/temp`],
|
||||
});
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
|
||||
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
|
||||
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
expect(assets.items).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
isOffline: true,
|
||||
originalFileName: 'assetB.png',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should scan new files', async () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
importPaths: [`${testAssetDirInternal}/temp`],
|
||||
});
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
|
||||
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetC.png`);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
|
||||
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetC.png`);
|
||||
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
expect(assets.items).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
originalFileName: 'assetC.png',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
describe('with refreshModifiedFiles=true', () => {
|
||||
it('should reimport modified files', async () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
importPaths: [`${testAssetDirInternal}/temp`],
|
||||
});
|
||||
|
||||
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
|
||||
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
|
||||
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`);
|
||||
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_001);
|
||||
|
||||
await scan(admin.accessToken, library.id, { refreshModifiedFiles: true });
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
|
||||
|
||||
const { assets } = await utils.metadataSearch(admin.accessToken, {
|
||||
libraryId: library.id,
|
||||
model: 'NIKON D750',
|
||||
});
|
||||
expect(assets.count).toBe(1);
|
||||
});
|
||||
|
||||
it('should not reimport unmodified files', async () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
importPaths: [`${testAssetDirInternal}/temp`],
|
||||
});
|
||||
|
||||
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
|
||||
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
|
||||
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`);
|
||||
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
|
||||
|
||||
await scan(admin.accessToken, library.id, { refreshModifiedFiles: true });
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
|
||||
|
||||
const { assets } = await utils.metadataSearch(admin.accessToken, {
|
||||
libraryId: library.id,
|
||||
model: 'NIKON D750',
|
||||
});
|
||||
expect(assets.count).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with refreshAllFiles=true', () => {
|
||||
it('should reimport all files', async () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
importPaths: [`${testAssetDirInternal}/temp`],
|
||||
});
|
||||
|
||||
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
|
||||
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
|
||||
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`);
|
||||
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
|
||||
|
||||
await scan(admin.accessToken, library.id, { refreshAllFiles: true });
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
|
||||
|
||||
const { assets } = await utils.metadataSearch(admin.accessToken, {
|
||||
libraryId: library.id,
|
||||
model: 'NIKON D750',
|
||||
});
|
||||
expect(assets.count).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -559,6 +613,72 @@ describe('/library', () => {
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should remove offline files', async () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
importPaths: [`${testAssetDirInternal}/temp`],
|
||||
});
|
||||
|
||||
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
|
||||
const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, {
|
||||
libraryId: library.id,
|
||||
});
|
||||
expect(initialAssets.count).toBe(3);
|
||||
|
||||
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
|
||||
const { assets: offlineAssets } = await utils.metadataSearch(admin.accessToken, {
|
||||
libraryId: library.id,
|
||||
isOffline: true,
|
||||
});
|
||||
expect(offlineAssets.count).toBe(1);
|
||||
|
||||
const { status } = await request(app)
|
||||
.post(`/library/${library.id}/removeOffline`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'backgroundTask');
|
||||
|
||||
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
expect(assets.count).toBe(2);
|
||||
});
|
||||
|
||||
it('should not remove online files', async () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
importPaths: [`${testAssetDirInternal}/temp`],
|
||||
});
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
|
||||
const { assets: assetsBefore } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
|
||||
expect(assetsBefore.count).toBeGreaterThan(1);
|
||||
|
||||
const { status } = await request(app)
|
||||
.post(`/library/${library.id}/removeOffline`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
|
||||
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
expect(assets).toEqual(assetsBefore);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /library/:id/validate', () => {
|
||||
@@ -608,4 +728,93 @@ describe('/library', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /library/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).delete(`/library/${uuidDto.notFound}`);
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should not delete the last upload library', async () => {
|
||||
const libraries = await getAllLibraries(
|
||||
{ $type: LibraryType.Upload },
|
||||
{ headers: asBearerAuth(admin.accessToken) },
|
||||
);
|
||||
|
||||
const adminLibraries = libraries.filter((library) => library.ownerId === admin.userId);
|
||||
expect(adminLibraries.length).toBeGreaterThanOrEqual(1);
|
||||
const lastLibrary = adminLibraries.pop() as LibraryResponseDto;
|
||||
|
||||
// delete all but the last upload library
|
||||
for (const library of adminLibraries) {
|
||||
const { status } = await request(app)
|
||||
.delete(`/library/${library.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(204);
|
||||
}
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/library/${lastLibrary.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(body).toEqual(errorDto.noDeleteUploadLibrary);
|
||||
expect(status).toBe(400);
|
||||
});
|
||||
|
||||
it('should delete an external library', async () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
});
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/library/${library.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(204);
|
||||
expect(body).toEqual({});
|
||||
|
||||
const libraries = await getAllLibraries({}, { headers: asBearerAuth(admin.accessToken) });
|
||||
expect(libraries).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: library.id,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should delete an external library with assets', async () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
importPaths: [`${testAssetDirInternal}/temp`],
|
||||
});
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 2 });
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/library/${library.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(204);
|
||||
expect(body).toEqual({});
|
||||
|
||||
const libraries = await getAllLibraries({}, { headers: asBearerAuth(admin.accessToken) });
|
||||
expect(libraries).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: library.id,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
// ensure no files get deleted
|
||||
expect(existsSync(`${testAssetDir}/temp/directoryA/assetA.png`)).toBe(true);
|
||||
expect(existsSync(`${testAssetDir}/temp/directoryB/assetB.png`)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AssetFileUploadResponseDto, LoginResponseDto, deleteAssets, updateAsset } from '@immich/sdk';
|
||||
import { AssetFileUploadResponseDto, LoginResponseDto, deleteAssets, getMapMarkers, updateAsset } from '@immich/sdk';
|
||||
import { DateTime } from 'luxon';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
@@ -32,6 +32,9 @@ describe('/search', () => {
|
||||
let assetGlarus: AssetFileUploadResponseDto;
|
||||
let assetSprings: AssetFileUploadResponseDto;
|
||||
let assetLast: AssetFileUploadResponseDto;
|
||||
let cities: string[];
|
||||
let states: string[];
|
||||
let countries: string[];
|
||||
|
||||
beforeAll(async () => {
|
||||
await utils.resetDatabase();
|
||||
@@ -79,7 +82,7 @@ describe('/search', () => {
|
||||
}
|
||||
|
||||
// note: the coordinates here are not the actual coordinates of the images and are random for most of them
|
||||
const cities = [
|
||||
const coordinates = [
|
||||
{ latitude: 48.853_41, longitude: 2.3488 }, // paris
|
||||
{ latitude: 63.0695, longitude: -151.0074 }, // denali
|
||||
{ latitude: 52.524_37, longitude: 13.410_53 }, // berlin
|
||||
@@ -101,7 +104,7 @@ describe('/search', () => {
|
||||
];
|
||||
|
||||
const updates = assets.map((asset, i) =>
|
||||
updateAsset({ id: asset.id, updateAssetDto: cities[i] }, { headers: asBearerAuth(admin.accessToken) }),
|
||||
updateAsset({ id: asset.id, updateAssetDto: coordinates[i] }, { headers: asBearerAuth(admin.accessToken) }),
|
||||
);
|
||||
|
||||
await Promise.all(updates);
|
||||
@@ -133,6 +136,12 @@ describe('/search', () => {
|
||||
assetLast = assets.at(-1) as AssetFileUploadResponseDto;
|
||||
|
||||
await deleteAssets({ assetBulkDeleteDto: { ids: [assetSilver.id] } }, { headers: asBearerAuth(admin.accessToken) });
|
||||
|
||||
const mapMarkers = await getMapMarkers({}, { headers: asBearerAuth(admin.accessToken) });
|
||||
const nonTrashed = mapMarkers.filter((mark) => mark.id !== assetSilver.id);
|
||||
cities = [...new Set(nonTrashed.map((mark) => mark.city).filter((entry): entry is string => !!entry))].sort();
|
||||
states = [...new Set(nonTrashed.map((mark) => mark.state).filter((entry): entry is string => !!entry))].sort();
|
||||
countries = [...new Set(nonTrashed.map((mark) => mark.country).filter((entry): entry is string => !!entry))].sort();
|
||||
}, 30_000);
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -452,21 +461,7 @@ describe('/search', () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/search/suggestions?type=country')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(body).toEqual([
|
||||
'Cuba',
|
||||
'France',
|
||||
'Georgia',
|
||||
'Germany',
|
||||
'Ghana',
|
||||
'Japan',
|
||||
'Morocco',
|
||||
"People's Republic of China",
|
||||
'Russian Federation',
|
||||
'Singapore',
|
||||
'Spain',
|
||||
'Switzerland',
|
||||
'United States of America',
|
||||
]);
|
||||
expect(body).toEqual(countries);
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
|
||||
@@ -474,23 +469,7 @@ describe('/search', () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/search/suggestions?type=state')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(body).toEqual([
|
||||
'Accra, Greater Accra',
|
||||
'Berlin',
|
||||
'Glarus, Glarus',
|
||||
'Havana',
|
||||
'Marrakech, Marrakesh-Safi',
|
||||
'Mesa County, Colorado',
|
||||
'Neshoba County, Mississippi',
|
||||
'New York',
|
||||
'Page County, Virginia',
|
||||
'Paris, Île-de-France',
|
||||
'Province of Córdoba, Andalusia',
|
||||
'Shanghai Municipality, Shanghai',
|
||||
'St.-Petersburg',
|
||||
'Tbilisi',
|
||||
'Tokyo',
|
||||
]);
|
||||
expect(body).toEqual(states);
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
|
||||
@@ -498,24 +477,7 @@ describe('/search', () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/search/suggestions?type=city')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(body).toEqual([
|
||||
'Accra',
|
||||
'Berlin',
|
||||
'Glarus',
|
||||
'Havana',
|
||||
'Marrakesh',
|
||||
'Montalbán de Córdoba',
|
||||
'New York City',
|
||||
'Palisade',
|
||||
'Paris',
|
||||
'Philadelphia',
|
||||
'Saint Petersburg',
|
||||
'Shanghai',
|
||||
'Singapore',
|
||||
'Stanley',
|
||||
'Tbilisi',
|
||||
'Tokyo',
|
||||
]);
|
||||
expect(body).toEqual(cities);
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LoginResponseDto, getServerConfig } from '@immich/sdk';
|
||||
import { LoginResponseDto } from '@immich/sdk';
|
||||
import { createUserDto } from 'src/fixtures';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { app, utils } from 'src/utils';
|
||||
@@ -162,19 +162,4 @@ describe('/server-info', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /server-info/admin-onboarding', () => {
|
||||
it('should set admin onboarding', async () => {
|
||||
const config = await getServerConfig({});
|
||||
expect(config.isOnboarded).toBe(false);
|
||||
|
||||
const { status } = await request(app)
|
||||
.post('/server-info/admin-onboarding')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(204);
|
||||
|
||||
const newConfig = await getServerConfig({});
|
||||
expect(newConfig.isOnboarded).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
75
e2e/src/api/specs/session.e2e-spec.ts
Normal file
75
e2e/src/api/specs/session.e2e-spec.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { LoginResponseDto, getSessions, login, signUpAdmin } from '@immich/sdk';
|
||||
import { loginDto, signupDto, uuidDto } from 'src/fixtures';
|
||||
import { deviceDto, errorDto } from 'src/responses';
|
||||
import { app, asBearerAuth, utils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
describe('/sessions', () => {
|
||||
let admin: LoginResponseDto;
|
||||
|
||||
beforeEach(async () => {
|
||||
await utils.resetDatabase();
|
||||
await signUpAdmin({ signUpDto: signupDto.admin });
|
||||
admin = await login({ loginCredentialDto: loginDto.admin });
|
||||
});
|
||||
|
||||
describe('GET /sessions', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get('/sessions');
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should get a list of authorized devices', async () => {
|
||||
const { status, body } = await request(app).get('/sessions').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual([deviceDto.current]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /sessions', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).delete(`/sessions`);
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should logout all devices (except the current one)', async () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await login({ loginCredentialDto: loginDto.admin });
|
||||
}
|
||||
|
||||
await expect(getSessions({ headers: asBearerAuth(admin.accessToken) })).resolves.toHaveLength(6);
|
||||
|
||||
const { status } = await request(app).delete(`/sessions`).set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(204);
|
||||
|
||||
await expect(getSessions({ headers: asBearerAuth(admin.accessToken) })).resolves.toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should throw an error for a non-existent device id', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/sessions/${uuidDto.notFound}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest('Not found or no authDevice.delete access'));
|
||||
});
|
||||
|
||||
it('should logout a device', async () => {
|
||||
const [device] = await getSessions({
|
||||
headers: asBearerAuth(admin.accessToken),
|
||||
});
|
||||
const { status } = await request(app)
|
||||
.delete(`/sessions/${device.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(204);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/auth/validateToken')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(response.body).toEqual(errorDto.invalidToken);
|
||||
expect(response.status).toBe(401);
|
||||
});
|
||||
});
|
||||
});
|
||||
76
e2e/src/api/specs/system-metadata.e2e-spec.ts
Normal file
76
e2e/src/api/specs/system-metadata.e2e-spec.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { LoginResponseDto, getServerConfig } from '@immich/sdk';
|
||||
import { createUserDto } from 'src/fixtures';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { app, utils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
describe('/server-info', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let nonAdmin: LoginResponseDto;
|
||||
|
||||
beforeAll(async () => {
|
||||
await utils.resetDatabase();
|
||||
admin = await utils.adminSetup({ onboarding: false });
|
||||
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
|
||||
});
|
||||
|
||||
describe('POST /system-metadata/admin-onboarding', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).post('/system-metadata/admin-onboarding').send({ isOnboarded: true });
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should only work for admins', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/system-metadata/admin-onboarding')
|
||||
.set('Authorization', `Bearer ${nonAdmin.accessToken}`)
|
||||
.send({ isOnboarded: true });
|
||||
expect(status).toBe(403);
|
||||
expect(body).toEqual(errorDto.forbidden);
|
||||
});
|
||||
|
||||
it('should set admin onboarding', async () => {
|
||||
const config = await getServerConfig({});
|
||||
expect(config.isOnboarded).toBe(false);
|
||||
|
||||
const { status } = await request(app)
|
||||
.post('/system-metadata/admin-onboarding')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ isOnboarded: true });
|
||||
expect(status).toBe(204);
|
||||
|
||||
const newConfig = await getServerConfig({});
|
||||
expect(newConfig.isOnboarded).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /system-metadata/reverse-geocoding-state', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get('/system-metadata/reverse-geocoding-state');
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should only work for admins', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/system-metadata/reverse-geocoding-state')
|
||||
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
||||
expect(status).toBe(403);
|
||||
expect(body).toEqual(errorDto.forbidden);
|
||||
});
|
||||
|
||||
it('should get the reverse geocoding state', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/system-metadata/reverse-geocoding-state')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
lastUpdate: expect.any(String),
|
||||
lastImportFileName: 'cities500.txt',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
19
e2e/src/immich-admin/specs/immich-admin.e2e-spec.ts
Normal file
19
e2e/src/immich-admin/specs/immich-admin.e2e-spec.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { immichAdmin, utils } from 'src/utils';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
describe(`immich-admin`, () => {
|
||||
beforeAll(async () => {
|
||||
await utils.resetDatabase();
|
||||
await utils.adminSetup();
|
||||
});
|
||||
|
||||
describe('list-users', () => {
|
||||
it('should list the admin user', async () => {
|
||||
const { stdout, stderr, exitCode } = await immichAdmin(['list-users']);
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stderr).toBe('');
|
||||
expect(stdout).toContain("email: 'admin@immich.cloud'");
|
||||
expect(stdout).toContain("name: 'Immich Admin'");
|
||||
});
|
||||
});
|
||||
});
|
||||
115
e2e/src/utils.ts
115
e2e/src/utils.ts
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
AllJobStatusResponseDto,
|
||||
AssetFileUploadResponseDto,
|
||||
AssetResponseDto,
|
||||
CreateAlbumDto,
|
||||
@@ -18,11 +19,14 @@ import {
|
||||
defaults,
|
||||
deleteAssets,
|
||||
getAllAssets,
|
||||
getAllJobsStatus,
|
||||
getAssetInfo,
|
||||
getConfigDefaults,
|
||||
login,
|
||||
searchMetadata,
|
||||
setAdminOnboarding,
|
||||
signUpAdmin,
|
||||
updateAdminOnboarding,
|
||||
updateConfig,
|
||||
validate,
|
||||
} from '@immich/sdk';
|
||||
import { BrowserContext } from '@playwright/test';
|
||||
@@ -31,6 +35,7 @@ import { createHash } from 'node:crypto';
|
||||
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path, { dirname } from 'node:path';
|
||||
import { setTimeout as setAsyncTimeout } from 'node:timers/promises';
|
||||
import { promisify } from 'node:util';
|
||||
import pg from 'pg';
|
||||
import { io, type Socket } from 'socket.io-client';
|
||||
@@ -38,7 +43,7 @@ import { loginDto, signupDto } from 'src/fixtures';
|
||||
import { makeRandomImage } from 'src/generators';
|
||||
import request from 'supertest';
|
||||
|
||||
type CliResponse = { stdout: string; stderr: string; exitCode: number | null };
|
||||
type CommandResponse = { stdout: string; stderr: string; exitCode: number | null };
|
||||
type EventType = 'assetUpload' | 'assetUpdate' | 'assetDelete' | 'userDelete';
|
||||
type WaitOptions = { event: EventType; id?: string; total?: number; timeout?: number };
|
||||
type AdminSetupOptions = { onboarding?: boolean };
|
||||
@@ -54,13 +59,15 @@ export const testAssetDirInternal = '/data/assets';
|
||||
export const tempDir = tmpdir();
|
||||
export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}` });
|
||||
export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
|
||||
export const immichCli = async (args: string[]) => {
|
||||
let _resolve: (value: CliResponse) => void;
|
||||
const deferred = new Promise<CliResponse>((resolve) => (_resolve = resolve));
|
||||
const _args = ['node_modules/.bin/immich', '-d', `/${tempDir}/immich/`, ...args];
|
||||
const child = spawn('node', _args, {
|
||||
stdio: 'pipe',
|
||||
});
|
||||
export const immichCli = (args: string[]) =>
|
||||
executeCommand('node', ['node_modules/.bin/immich', '-d', `/${tempDir}/immich/`, ...args]);
|
||||
export const immichAdmin = (args: string[]) =>
|
||||
executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]);
|
||||
|
||||
const executeCommand = (command: string, args: string[]) => {
|
||||
let _resolve: (value: CommandResponse) => void;
|
||||
const deferred = new Promise<CommandResponse>((resolve) => (_resolve = resolve));
|
||||
const child = spawn(command, args, { stdio: 'pipe' });
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
@@ -133,9 +140,10 @@ export const utils = {
|
||||
'asset_faces',
|
||||
'activity',
|
||||
'api_keys',
|
||||
'user_token',
|
||||
'sessions',
|
||||
'users',
|
||||
'system_metadata',
|
||||
'system_config',
|
||||
];
|
||||
|
||||
const sql: string[] = [];
|
||||
@@ -145,7 +153,12 @@ export const utils = {
|
||||
}
|
||||
|
||||
for (const table of tables) {
|
||||
sql.push(`DELETE FROM ${table} CASCADE;`);
|
||||
if (table === 'system_metadata') {
|
||||
// prevent reverse geocoder from being re-initialized
|
||||
sql.push(`DELETE FROM "system_metadata" where "key" != 'reverse-geocoding-state';`);
|
||||
} else {
|
||||
sql.push(`DELETE FROM ${table} CASCADE;`);
|
||||
}
|
||||
}
|
||||
|
||||
await client.query(sql.join('\n'));
|
||||
@@ -209,35 +222,33 @@ export const utils = {
|
||||
}
|
||||
},
|
||||
|
||||
waitForWebsocketEvent: async ({ event, id, total: count, timeout: ms }: WaitOptions): Promise<void> => {
|
||||
if (!id && !count) {
|
||||
throw new Error('id or count must be provided for waitForWebsocketEvent');
|
||||
}
|
||||
|
||||
const type = id ? `id=${id}` : `count=${count}`;
|
||||
console.log(`Waiting for ${event} [${type}]`);
|
||||
const set = events[event];
|
||||
if ((id && set.has(id)) || (count && set.size >= count)) {
|
||||
return;
|
||||
}
|
||||
|
||||
waitForWebsocketEvent: ({ event, id, total: count, timeout: ms }: WaitOptions): Promise<void> => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (!id && !count) {
|
||||
reject(new Error('id or count must be provided for waitForWebsocketEvent'));
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => reject(new Error(`Timed out waiting for ${event} event`)), ms || 10_000);
|
||||
const type = id ? `id=${id}` : `count=${count}`;
|
||||
console.log(`Waiting for ${event} [${type}]`);
|
||||
const set = events[event];
|
||||
const onId = () => {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
};
|
||||
if ((id && set.has(id)) || (count && set.size >= count)) {
|
||||
onId();
|
||||
return;
|
||||
}
|
||||
|
||||
if (id) {
|
||||
idCallbacks[id] = () => {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
};
|
||||
idCallbacks[id] = onId;
|
||||
}
|
||||
|
||||
if (count) {
|
||||
countCallbacks[event] = {
|
||||
count,
|
||||
callback: () => {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
},
|
||||
callback: onId,
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -253,7 +264,10 @@ export const utils = {
|
||||
await signUpAdmin({ signUpDto: signupDto.admin });
|
||||
const response = await login({ loginCredentialDto: loginDto.admin });
|
||||
if (options.onboarding) {
|
||||
await setAdminOnboarding({ headers: asBearerAuth(response.accessToken) });
|
||||
await updateAdminOnboarding(
|
||||
{ adminOnboardingUpdateDto: { isOnboarded: true } },
|
||||
{ headers: asBearerAuth(response.accessToken) },
|
||||
);
|
||||
}
|
||||
return response;
|
||||
},
|
||||
@@ -309,9 +323,7 @@ export const utils = {
|
||||
if (!existsSync(dirname(path))) {
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
}
|
||||
if (!existsSync(path)) {
|
||||
writeFileSync(path, makeRandomImage());
|
||||
}
|
||||
writeFileSync(path, makeRandomImage());
|
||||
},
|
||||
|
||||
removeImageFile: (path: string) => {
|
||||
@@ -406,6 +418,39 @@ export const utils = {
|
||||
},
|
||||
]),
|
||||
|
||||
resetTempFolder: () => {
|
||||
rmSync(`${testAssetDir}/temp`, { recursive: true, force: true });
|
||||
mkdirSync(`${testAssetDir}/temp`, { recursive: true });
|
||||
},
|
||||
|
||||
resetAdminConfig: async (accessToken: string) => {
|
||||
const defaultConfig = await getConfigDefaults({ headers: asBearerAuth(accessToken) });
|
||||
await updateConfig({ systemConfigDto: defaultConfig }, { headers: asBearerAuth(accessToken) });
|
||||
},
|
||||
|
||||
isQueueEmpty: async (accessToken: string, queue: keyof AllJobStatusResponseDto) => {
|
||||
const queues = await getAllJobsStatus({ headers: asBearerAuth(accessToken) });
|
||||
const jobCounts = queues[queue].jobCounts;
|
||||
return !jobCounts.active && !jobCounts.waiting;
|
||||
},
|
||||
|
||||
waitForQueueFinish: (accessToken: string, queue: keyof AllJobStatusResponseDto, ms?: number) => {
|
||||
return new Promise<void>(async (resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error('Timed out waiting for queue to empty')), ms || 10_000);
|
||||
|
||||
while (true) {
|
||||
const done = await utils.isQueueEmpty(accessToken, queue);
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
await setAsyncTimeout(200);
|
||||
}
|
||||
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
|
||||
cliLogin: async (accessToken: string) => {
|
||||
const key = await utils.createApiKey(accessToken);
|
||||
await immichCli(['login', app, `${key.secret}`]);
|
||||
|
||||
@@ -10,9 +10,9 @@ try {
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ['src/{api,cli}/specs/*.e2e-spec.ts'],
|
||||
include: ['src/{api,cli,immich-admin}/specs/*.e2e-spec.ts'],
|
||||
globalSetup,
|
||||
testTimeout: 10_000,
|
||||
testTimeout: 15_000,
|
||||
poolOptions: {
|
||||
threads: {
|
||||
singleThread: true,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
This project uses [Poetry](https://python-poetry.org/docs/#installation), so be sure to install it first.
|
||||
Running `poetry install --no-root --with dev --with cpu` will install everything you need in an isolated virtual environment.
|
||||
CUDA and OpenVINO are supported as acceleration APIs. To use them, you can replace `--with cpu` with either of `--with cuda` or `--with openvino`.
|
||||
CUDA and OpenVINO are supported as acceleration APIs. To use them, you can replace `--with cpu` with either of `--with cuda` or `--with openvino`. In the case of CUDA, a [compute capability](https://developer.nvidia.com/cuda-gpus) of 5.2 or higher is required.
|
||||
|
||||
To add or remove dependencies, you can use the commands `poetry add $PACKAGE_NAME` and `poetry remove $PACKAGE_NAME`, respectively.
|
||||
Be sure to commit the `poetry.lock` and `pyproject.toml` files with `poetry lock --no-update` to reflect any changes in dependencies.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM mambaorg/micromamba:bookworm-slim@sha256:987299cff637df33d00a9969a577f2ac74d37fa070db6aa45b083346ba390959 as builder
|
||||
FROM mambaorg/micromamba:bookworm-slim@sha256:4de614588f7b3d6598a6cd571297157b57ac85b1a8ff854cf10231bb0c50e3ac as builder
|
||||
|
||||
ENV NODE_ENV=production \
|
||||
TRANSFORMERS_CACHE=/cache \
|
||||
|
||||
47
machine-learning/poetry.lock
generated
47
machine-learning/poetry.lock
generated
@@ -1150,22 +1150,23 @@ test = ["objgraph", "psutil"]
|
||||
|
||||
[[package]]
|
||||
name = "gunicorn"
|
||||
version = "21.2.0"
|
||||
version = "22.0.0"
|
||||
description = "WSGI HTTP Server for UNIX"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "gunicorn-21.2.0-py3-none-any.whl", hash = "sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0"},
|
||||
{file = "gunicorn-21.2.0.tar.gz", hash = "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033"},
|
||||
{file = "gunicorn-22.0.0-py3-none-any.whl", hash = "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9"},
|
||||
{file = "gunicorn-22.0.0.tar.gz", hash = "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
packaging = "*"
|
||||
|
||||
[package.extras]
|
||||
eventlet = ["eventlet (>=0.24.1)"]
|
||||
eventlet = ["eventlet (>=0.24.1,!=0.36.0)"]
|
||||
gevent = ["gevent (>=1.4.0)"]
|
||||
setproctitle = ["setproctitle"]
|
||||
testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"]
|
||||
tornado = ["tornado (>=0.2)"]
|
||||
|
||||
[[package]]
|
||||
@@ -2847,28 +2848,28 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.3.5"
|
||||
version = "0.3.7"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.3.5-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:aef5bd3b89e657007e1be6b16553c8813b221ff6d92c7526b7e0227450981eac"},
|
||||
{file = "ruff-0.3.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:89b1e92b3bd9fca249153a97d23f29bed3992cff414b222fcd361d763fc53f12"},
|
||||
{file = "ruff-0.3.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e55771559c89272c3ebab23326dc23e7f813e492052391fe7950c1a5a139d89"},
|
||||
{file = "ruff-0.3.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dabc62195bf54b8a7876add6e789caae0268f34582333cda340497c886111c39"},
|
||||
{file = "ruff-0.3.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a05f3793ba25f194f395578579c546ca5d83e0195f992edc32e5907d142bfa3"},
|
||||
{file = "ruff-0.3.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dfd3504e881082959b4160ab02f7a205f0fadc0a9619cc481982b6837b2fd4c0"},
|
||||
{file = "ruff-0.3.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87258e0d4b04046cf1d6cc1c56fadbf7a880cc3de1f7294938e923234cf9e498"},
|
||||
{file = "ruff-0.3.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:712e71283fc7d9f95047ed5f793bc019b0b0a29849b14664a60fd66c23b96da1"},
|
||||
{file = "ruff-0.3.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a532a90b4a18d3f722c124c513ffb5e5eaff0cc4f6d3aa4bda38e691b8600c9f"},
|
||||
{file = "ruff-0.3.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:122de171a147c76ada00f76df533b54676f6e321e61bd8656ae54be326c10296"},
|
||||
{file = "ruff-0.3.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d80a6b18a6c3b6ed25b71b05eba183f37d9bc8b16ace9e3d700997f00b74660b"},
|
||||
{file = "ruff-0.3.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a7b6e63194c68bca8e71f81de30cfa6f58ff70393cf45aab4c20f158227d5936"},
|
||||
{file = "ruff-0.3.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a759d33a20c72f2dfa54dae6e85e1225b8e302e8ac655773aff22e542a300985"},
|
||||
{file = "ruff-0.3.5-py3-none-win32.whl", hash = "sha256:9d8605aa990045517c911726d21293ef4baa64f87265896e491a05461cae078d"},
|
||||
{file = "ruff-0.3.5-py3-none-win_amd64.whl", hash = "sha256:dc56bb16a63c1303bd47563c60482a1512721053d93231cf7e9e1c6954395a0e"},
|
||||
{file = "ruff-0.3.5-py3-none-win_arm64.whl", hash = "sha256:faeeae9905446b975dcf6d4499dc93439b131f1443ee264055c5716dd947af55"},
|
||||
{file = "ruff-0.3.5.tar.gz", hash = "sha256:a067daaeb1dc2baf9b82a32dae67d154d95212080c80435eb052d95da647763d"},
|
||||
{file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0e8377cccb2f07abd25e84fc5b2cbe48eeb0fea9f1719cad7caedb061d70e5ce"},
|
||||
{file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:15a4d1cc1e64e556fa0d67bfd388fed416b7f3b26d5d1c3e7d192c897e39ba4b"},
|
||||
{file = "ruff-0.3.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d28bdf3d7dc71dd46929fafeec98ba89b7c3550c3f0978e36389b5631b793663"},
|
||||
{file = "ruff-0.3.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:379b67d4f49774ba679593b232dcd90d9e10f04d96e3c8ce4a28037ae473f7bb"},
|
||||
{file = "ruff-0.3.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c060aea8ad5ef21cdfbbe05475ab5104ce7827b639a78dd55383a6e9895b7c51"},
|
||||
{file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ebf8f615dde968272d70502c083ebf963b6781aacd3079081e03b32adfe4d58a"},
|
||||
{file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d48098bd8f5c38897b03604f5428901b65e3c97d40b3952e38637b5404b739a2"},
|
||||
{file = "ruff-0.3.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da8a4fda219bf9024692b1bc68c9cff4b80507879ada8769dc7e985755d662ea"},
|
||||
{file = "ruff-0.3.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c44e0149f1d8b48c4d5c33d88c677a4aa22fd09b1683d6a7ff55b816b5d074f"},
|
||||
{file = "ruff-0.3.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3050ec0af72b709a62ecc2aca941b9cd479a7bf2b36cc4562f0033d688e44fa1"},
|
||||
{file = "ruff-0.3.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a29cc38e4c1ab00da18a3f6777f8b50099d73326981bb7d182e54a9a21bb4ff7"},
|
||||
{file = "ruff-0.3.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b15cc59c19edca917f51b1956637db47e200b0fc5e6e1878233d3a938384b0b"},
|
||||
{file = "ruff-0.3.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e491045781b1e38b72c91247cf4634f040f8d0cb3e6d3d64d38dcf43616650b4"},
|
||||
{file = "ruff-0.3.7-py3-none-win32.whl", hash = "sha256:bc931de87593d64fad3a22e201e55ad76271f1d5bfc44e1a1887edd0903c7d9f"},
|
||||
{file = "ruff-0.3.7-py3-none-win_amd64.whl", hash = "sha256:5ef0e501e1e39f35e03c2acb1d1238c595b8bb36cf7a170e7c1df1b73da00e74"},
|
||||
{file = "ruff-0.3.7-py3-none-win_arm64.whl", hash = "sha256:789e144f6dc7019d1f92a812891c645274ed08af6037d11fc65fcbc183b7d59f"},
|
||||
{file = "ruff-0.3.7.tar.gz", hash = "sha256:d5c1aebee5162c2226784800ae031f660c350e7a3402c4d1f8ea4e97e232e3ba"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "machine-learning"
|
||||
version = "1.101.0"
|
||||
version = "1.102.3"
|
||||
description = ""
|
||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
||||
readme = "README.md"
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
plugins {
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
id "kotlin-kapt"
|
||||
}
|
||||
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
if (localPropertiesFile.exists()) {
|
||||
localPropertiesFile.withReader('UTF-8') { reader ->
|
||||
localProperties.load(reader)
|
||||
}
|
||||
}
|
||||
|
||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
||||
if (flutterRoot == null) {
|
||||
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
||||
localPropertiesFile.withInputStream { localProperties.load(it) }
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
@@ -21,18 +21,12 @@ if (flutterVersionName == null) {
|
||||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
def keystoreProperties = new Properties()
|
||||
def keystorePropertiesFile = rootProject.file('key.properties')
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
keystorePropertiesFile.withInputStream { keystoreProperties.load(it) }
|
||||
}
|
||||
|
||||
|
||||
android {
|
||||
compileSdkVersion 34
|
||||
|
||||
@@ -50,7 +44,6 @@ android {
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "app.alextran.immich"
|
||||
minSdkVersion 26
|
||||
targetSdkVersion 33
|
||||
@@ -88,6 +81,13 @@ flutter {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
def kotlin_version = '1.9.23'
|
||||
def kotlin_coroutines_version = '1.8.0'
|
||||
def work_version = '2.9.0'
|
||||
def concurrent_version = '1.1.0'
|
||||
def guava_version = '33.1.0-android'
|
||||
def glide_version = '4.16.0'
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
|
||||
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||
|
||||
@@ -276,7 +276,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
|
||||
private const val NOTIFICATION_CHANNEL_ERROR_ID = "immich/backgroundServiceError"
|
||||
private const val NOTIFICATION_DEFAULT_TITLE = "Immich"
|
||||
private const val NOTIFICATION_ID = 1
|
||||
private const val NOTIFICATION_ERROR_ID = 2
|
||||
private const val NOTIFICATION_ERROR_ID = 2
|
||||
private const val NOTIFICATION_DETAIL_ID = 3
|
||||
private const val ONE_MINUTE = 60000L
|
||||
|
||||
@@ -304,7 +304,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
|
||||
val workInfoList = workInfoFuture.get(1000, TimeUnit.MILLISECONDS)
|
||||
if (workInfoList != null) {
|
||||
for (workInfo in workInfoList) {
|
||||
if (workInfo.getState() == WorkInfo.State.ENQUEUED) {
|
||||
if (workInfo.state == WorkInfo.State.ENQUEUED) {
|
||||
val workRequest = buildWorkRequest(requireWifi, requireCharging)
|
||||
wm.enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.REPLACE, workRequest)
|
||||
Log.d(TAG, "updateBackupWorker updated BackupWorker constraints")
|
||||
@@ -346,7 +346,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
|
||||
.setRequiresBatteryNotLow(true)
|
||||
.setRequiresCharging(requireCharging)
|
||||
.build();
|
||||
|
||||
|
||||
val work = OneTimeWorkRequest.Builder(BackupWorker::class.java)
|
||||
.setConstraints(constraints)
|
||||
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, ONE_MINUTE, TimeUnit.MILLISECONDS)
|
||||
@@ -359,4 +359,4 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
|
||||
}
|
||||
}
|
||||
|
||||
private const val TAG = "BackupWorker"
|
||||
private const val TAG = "BackupWorker"
|
||||
|
||||
@@ -1,21 +1,3 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.8.20'
|
||||
ext.kotlin_coroutines_version = '1.7.1'
|
||||
ext.work_version = '2.7.1'
|
||||
ext.concurrent_version = '1.1.0'
|
||||
ext.guava_version = '33.0.0-android'
|
||||
ext.glide_version = '4.14.2'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.4.2'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
@@ -34,3 +16,7 @@ subprojects {
|
||||
tasks.register("clean", Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
||||
tasks.named('wrapper') {
|
||||
distributionType = Wrapper.DistributionType.ALL
|
||||
}
|
||||
@@ -35,8 +35,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 131,
|
||||
"android.injected.version.name" => "1.101.0",
|
||||
"android.injected.version.code" => 136,
|
||||
"android.injected.version.name" => "1.102.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')
|
||||
|
||||
@@ -5,17 +5,17 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000219">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000261">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="67.515419">
|
||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="32.48099">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="35.431743">
|
||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="30.236974">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
|
||||
distributionSha256Sum=6001aba9b2204d26fa25a5800bb9382cf3ee01ccb78fe77317b2872336eb2f80
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip
|
||||
distributionSha256Sum=fe696c020f241a5f69c30f763c5a7f38eec54b490db19cd2b0962dda420d7d12
|
||||
@@ -1,11 +1,26 @@
|
||||
include ':app'
|
||||
pluginManagement {
|
||||
def flutterSdkPath = {
|
||||
def properties = new Properties()
|
||||
file("local.properties").withInputStream { properties.load(it) }
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
return flutterSdkPath
|
||||
}()
|
||||
|
||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
||||
def properties = new Properties()
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
assert localPropertiesFile.exists()
|
||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "7.4.2" apply false
|
||||
id "org.jetbrains.kotlin.android" version "1.9.23" apply false
|
||||
id "org.jetbrains.kotlin.kapt" version "1.9.23" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
|
||||
@@ -296,6 +296,7 @@
|
||||
"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_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
|
||||
"no_assets_to_show" : "No assets to show",
|
||||
"notification_permission_dialog_cancel": "Cancel",
|
||||
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
|
||||
"notification_permission_dialog_settings": "Settings",
|
||||
@@ -511,5 +512,7 @@
|
||||
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
|
||||
"viewer_remove_from_stack": "Remove from Stack",
|
||||
"viewer_stack_use_as_main_asset": "Use as Main Asset",
|
||||
"viewer_unstack": "Un-Stack"
|
||||
}
|
||||
"viewer_unstack": "Un-Stack",
|
||||
"haptic_feedback_title": "Haptic Feedback",
|
||||
"haptic_feedback_switch": "Enable haptic feedback"
|
||||
}
|
||||
|
||||
@@ -296,6 +296,7 @@
|
||||
"motion_photos_page_title": "Photos avec mouvement",
|
||||
"multiselect_grid_edit_date_time_err_read_only": "Impossible de modifier la date d'un élément d'actif en lecture seule.",
|
||||
"multiselect_grid_edit_gps_err_read_only": "Impossible de modifier l'emplacement d'un élément en lecture seule.",
|
||||
"no_assets_to_show" : "Aucun élément à afficher",
|
||||
"notification_permission_dialog_cancel": "Annuler",
|
||||
"notification_permission_dialog_content": "Pour activer les notifications, allez dans Paramètres et sélectionnez Autoriser.",
|
||||
"notification_permission_dialog_settings": "Paramètres",
|
||||
@@ -509,5 +510,7 @@
|
||||
"version_announcement_overlay_title": "Nouvelle version serveur disponible \uD83C\uDF89",
|
||||
"viewer_remove_from_stack": "Retirer de la pile",
|
||||
"viewer_stack_use_as_main_asset": "Utiliser comme élément principal",
|
||||
"viewer_unstack": "Désempiler"
|
||||
"viewer_unstack": "Désempiler",
|
||||
"haptic_feedback_title": "Retour haptique",
|
||||
"haptic_feedback_switch": "Activer le retour haptique"
|
||||
}
|
||||
Binary file not shown.
@@ -383,7 +383,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 147;
|
||||
CURRENT_PROJECT_VERSION = 150;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -525,7 +525,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 147;
|
||||
CURRENT_PROJECT_VERSION = 150;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -553,7 +553,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 147;
|
||||
CURRENT_PROJECT_VERSION = 150;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
|
||||
@@ -58,11 +58,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.101.0</string>
|
||||
<string>1.102.2</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>147</string>
|
||||
<string>150</string>
|
||||
<key>FLTEnableImpeller</key>
|
||||
<true />
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
||||
@@ -19,7 +19,7 @@ platform :ios do
|
||||
desc "iOS Beta"
|
||||
lane :beta do
|
||||
increment_version_number(
|
||||
version_number: "1.101.0"
|
||||
version_number: "1.102.3"
|
||||
)
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number + 1,
|
||||
|
||||
@@ -5,32 +5,32 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000242">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000231">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.761829">
|
||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.155919">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="4.47461">
|
||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="4.252784">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.179512">
|
||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.210502">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="4: build_app" time="165.636347">
|
||||
<testcase classname="fastlane.lanes" name="4: build_app" time="175.813647">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="77.651963">
|
||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="73.512517">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import 'package:immich_mobile/modules/asset_viewer/ui/gallery_app_bar.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_thumbnail.dart';
|
||||
import 'package:immich_mobile/shared/ui/photo_view/photo_view_gallery.dart';
|
||||
@@ -303,7 +304,9 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
scrollDirection: Axis.horizontal,
|
||||
onPageChanged: (value) async {
|
||||
final next = currentIndex.value < value ? value + 1 : value - 1;
|
||||
HapticFeedback.selectionClick();
|
||||
|
||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||
|
||||
currentIndex.value = value;
|
||||
stackIndex.value = -1;
|
||||
isPlayingVideo.value = false;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||
|
||||
class AlbumInfoCard extends HookConsumerWidget {
|
||||
@@ -21,6 +21,7 @@ class AlbumInfoCard extends HookConsumerWidget {
|
||||
ref.watch(backupProvider).selectedBackupAlbums.contains(album);
|
||||
final bool isExcluded =
|
||||
ref.watch(backupProvider).excludedBackupAlbums.contains(album);
|
||||
|
||||
final isDarkTheme = context.isDarkTheme;
|
||||
|
||||
ColorFilter selectedFilter = ColorFilter.mode(
|
||||
@@ -78,7 +79,7 @@ class AlbumInfoCard extends HookConsumerWidget {
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
HapticFeedback.selectionClick();
|
||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||
|
||||
if (isSelected) {
|
||||
ref.read(backupProvider.notifier).removeAlbumForBackup(album);
|
||||
@@ -87,7 +88,7 @@ class AlbumInfoCard extends HookConsumerWidget {
|
||||
}
|
||||
},
|
||||
onDoubleTap: () {
|
||||
HapticFeedback.selectionClick();
|
||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||
|
||||
if (isExcluded) {
|
||||
// Remove from exclude album list
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@@ -8,6 +7,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||
|
||||
class AlbumInfoListTile extends HookConsumerWidget {
|
||||
@@ -68,7 +68,7 @@ class AlbumInfoListTile extends HookConsumerWidget {
|
||||
|
||||
return GestureDetector(
|
||||
onDoubleTap: () {
|
||||
HapticFeedback.selectionClick();
|
||||
ref.watch(hapticFeedbackProvider.notifier).selectionClick();
|
||||
|
||||
if (isExcluded) {
|
||||
// Remove from exclude album list
|
||||
@@ -93,7 +93,7 @@ class AlbumInfoListTile extends HookConsumerWidget {
|
||||
tileColor: buildTileColor(),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
onTap: () {
|
||||
HapticFeedback.selectionClick();
|
||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||
if (isSelected) {
|
||||
ref.read(backupProvider.notifier).removeAlbumForBackup(album);
|
||||
} else {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart';
|
||||
|
||||
class GroupDividerTitle extends HookConsumerWidget {
|
||||
const GroupDividerTitle({
|
||||
@@ -38,7 +38,7 @@ class GroupDividerTitle extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
void handleTitleIconClick() {
|
||||
HapticFeedback.heavyImpact();
|
||||
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
|
||||
if (selected) {
|
||||
onDeselect();
|
||||
} else {
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart';
|
||||
@@ -15,6 +15,7 @@ import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_placeholder.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
import 'asset_grid_data_structure.dart';
|
||||
@@ -27,7 +28,7 @@ typedef ImmichAssetGridSelectionListener = void Function(
|
||||
Set<Asset>,
|
||||
);
|
||||
|
||||
class ImmichAssetGridView extends StatefulWidget {
|
||||
class ImmichAssetGridView extends ConsumerStatefulWidget {
|
||||
final RenderList renderList;
|
||||
final int assetsPerRow;
|
||||
final double margin;
|
||||
@@ -69,12 +70,12 @@ class ImmichAssetGridView extends StatefulWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
createState() {
|
||||
return ImmichAssetGridViewState();
|
||||
}
|
||||
}
|
||||
|
||||
class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
||||
class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
|
||||
final ItemScrollController _itemScrollController = ItemScrollController();
|
||||
final ScrollOffsetController _scrollOffsetController =
|
||||
ScrollOffsetController();
|
||||
@@ -314,7 +315,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
||||
final now = Timeline.now;
|
||||
if (now > (_hapticFeedbackTS + feedbackInterval)) {
|
||||
_hapticFeedbackTS = now;
|
||||
HapticFeedback.mediumImpact();
|
||||
ref.read(hapticFeedbackProvider.notifier).mediumImpact();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_thumbnail.dart';
|
||||
import 'package:immich_mobile/utils/storage_indicator.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
class ThumbnailImage extends StatelessWidget {
|
||||
class ThumbnailImage extends ConsumerWidget {
|
||||
final Asset asset;
|
||||
final int index;
|
||||
final Asset Function(int index) loadAsset;
|
||||
@@ -37,7 +38,7 @@ class ThumbnailImage extends StatelessWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final assetContainerColor = context.isDarkTheme
|
||||
? Colors.blueGrey
|
||||
: context.themeData.primaryColorLight;
|
||||
@@ -186,7 +187,7 @@ class ThumbnailImage extends StatelessWidget {
|
||||
},
|
||||
onLongPress: () {
|
||||
onSelect?.call();
|
||||
HapticFeedback.heavyImpact();
|
||||
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_udid/flutter_udid.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||
@@ -92,7 +91,6 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
||||
serverUrl: serverUrl,
|
||||
);
|
||||
} catch (e) {
|
||||
HapticFeedback.vibrate();
|
||||
debugPrint("Error logging in $e");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_placeholder.dart';
|
||||
import 'package:immich_mobile/modules/memories/providers/memory.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||
|
||||
class MemoryLane extends HookConsumerWidget {
|
||||
@@ -33,7 +33,9 @@ class MemoryLane extends HookConsumerWidget {
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
HapticFeedback.heavyImpact();
|
||||
ref
|
||||
.read(hapticFeedbackProvider.notifier)
|
||||
.heavyImpact();
|
||||
context.pushRoute(
|
||||
MemoryRoute(
|
||||
memories: memories,
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:immich_mobile/modules/memories/ui/memory_card.dart';
|
||||
import 'package:immich_mobile/modules/memories/ui/memory_epilogue.dart';
|
||||
import 'package:immich_mobile/modules/memories/ui/memory_progress_indicator.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||
|
||||
@RoutePage()
|
||||
@@ -127,7 +128,7 @@ class MemoryPage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
Future<void> onAssetChanged(int otherIndex) async {
|
||||
HapticFeedback.selectionClick();
|
||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||
currentAssetPage.value = otherIndex;
|
||||
updateProgressText();
|
||||
// Wait for page change animation to finish
|
||||
@@ -169,7 +170,7 @@ class MemoryPage extends HookConsumerWidget {
|
||||
scrollDirection: Axis.vertical,
|
||||
controller: memoryPageController,
|
||||
onPageChanged: (pageNumber) {
|
||||
HapticFeedback.mediumImpact();
|
||||
ref.read(hapticFeedbackProvider.notifier).mediumImpact();
|
||||
if (pageNumber < memories.length) {
|
||||
currentMemoryIndex.value = pageNumber;
|
||||
currentMemory.value = memories[pageNumber];
|
||||
|
||||
@@ -58,6 +58,7 @@ enum AppSettingsEnum<T> {
|
||||
null,
|
||||
false,
|
||||
),
|
||||
enableHapticFeedback<bool>(StoreKey.enableHapticFeedback, null, true),
|
||||
;
|
||||
|
||||
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);
|
||||
|
||||
81
mobile/lib/modules/settings/ui/language_settings.dart
Normal file
81
mobile/lib/modules/settings/ui/language_settings.dart
Normal file
@@ -0,0 +1,81 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/locales.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/backup/background_service/localization.dart';
|
||||
|
||||
class LanguageSettings extends HookConsumerWidget {
|
||||
const LanguageSettings({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final currentLocale = context.locale;
|
||||
final textController = useTextEditingController(
|
||||
text: locales.keys.firstWhere(
|
||||
(countryName) => locales[countryName] == currentLocale,
|
||||
),
|
||||
);
|
||||
|
||||
final selectedLocale = useState<Locale>(currentLocale);
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return DropdownMenu(
|
||||
width: constraints.maxWidth,
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
contentPadding: const EdgeInsets.only(left: 16),
|
||||
),
|
||||
menuStyle: MenuStyle(
|
||||
shape: MaterialStatePropertyAll<OutlinedBorder>(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
),
|
||||
backgroundColor: MaterialStatePropertyAll<Color>(
|
||||
context.isDarkTheme
|
||||
? Colors.grey[900]!
|
||||
: context.scaffoldBackgroundColor,
|
||||
),
|
||||
),
|
||||
menuHeight: context.height * 0.5,
|
||||
hintText: "Languages",
|
||||
label: const Text('Languages'),
|
||||
dropdownMenuEntries: locales.keys
|
||||
.map(
|
||||
(countryName) => DropdownMenuEntry(
|
||||
value: locales[countryName],
|
||||
label: countryName,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
controller: textController,
|
||||
onSelected: (value) {
|
||||
if (value != null) {
|
||||
selectedLocale.value = value;
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: selectedLocale.value == currentLocale
|
||||
? null
|
||||
: () {
|
||||
context.setLocale(selectedLocale.value);
|
||||
loadTranslations();
|
||||
},
|
||||
child: const Text('setting_languages_apply').tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/settings_sub_title.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart';
|
||||
import 'package:immich_mobile/modules/settings/utils/app_settings_update_hook.dart';
|
||||
|
||||
class HapticSetting extends HookConsumerWidget {
|
||||
const HapticSetting({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final hapticFeedbackSetting =
|
||||
useAppSettingsState(AppSettingsEnum.enableHapticFeedback);
|
||||
final isHapticFeedbackEnabled =
|
||||
useValueNotifier(hapticFeedbackSetting.value);
|
||||
|
||||
onHapticFeedbackChange(bool isEnabled) {
|
||||
hapticFeedbackSetting.value = isEnabled;
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SettingsSubTitle(title: "haptic_feedback_title".tr()),
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: isHapticFeedbackEnabled,
|
||||
title: 'haptic_feedback_switch'.tr(),
|
||||
onChanged: onHapticFeedbackChange,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/preference_settings/haptic_setting.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/preference_settings/theme_setting.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/settings_sub_page_scaffold.dart';
|
||||
|
||||
@@ -11,6 +12,7 @@ class PreferenceSetting extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
const preferenceSettings = [
|
||||
ThemeSetting(),
|
||||
HapticSetting(),
|
||||
];
|
||||
|
||||
return const SettingsSubPageScaffold(settings: preferenceSettings);
|
||||
|
||||
@@ -2,14 +2,12 @@ import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/locales.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/backup/background_service/localization.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/advanced_settings.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_settings.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/backup_settings/backup_settings.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/language_settings.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/notification_setting.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/preference_settings/preference_setting.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
@@ -42,70 +40,6 @@ enum SettingSection {
|
||||
const SettingSection(this.title, this.icon);
|
||||
}
|
||||
|
||||
class LanguageSettings extends HookConsumerWidget {
|
||||
const LanguageSettings({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final currentLocale = context.locale;
|
||||
final textController = useTextEditingController(
|
||||
text: locales.keys.firstWhere(
|
||||
(countryName) => locales[countryName] == currentLocale,
|
||||
),
|
||||
);
|
||||
|
||||
final selectedLocale = useState<Locale>(currentLocale);
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
DropdownMenu(
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
contentPadding: const EdgeInsets.only(left: 16),
|
||||
),
|
||||
menuStyle: MenuStyle(
|
||||
shape: MaterialStatePropertyAll<OutlinedBorder>(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
),
|
||||
),
|
||||
menuHeight: context.height * 0.5,
|
||||
hintText: "Languages",
|
||||
label: const Text('Languages'),
|
||||
dropdownMenuEntries: locales.keys
|
||||
.map(
|
||||
(countryName) => DropdownMenuEntry(
|
||||
value: locales[countryName],
|
||||
label: countryName,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
controller: textController,
|
||||
onSelected: (value) {
|
||||
if (value != null) {
|
||||
selectedLocale.value = value;
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: selectedLocale.value == currentLocale
|
||||
? null
|
||||
: () {
|
||||
context.setLocale(selectedLocale.value);
|
||||
loadTranslations();
|
||||
},
|
||||
child: const Text('setting_languages_apply').tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@RoutePage()
|
||||
class SettingsPage extends StatelessWidget {
|
||||
const SettingsPage({super.key});
|
||||
|
||||
@@ -191,6 +191,7 @@ enum StoreKey<T> {
|
||||
selectedAlbumSortReverse<bool>(123, type: bool),
|
||||
mapThemeMode<int>(124, type: int),
|
||||
mapwithPartners<bool>(125, type: bool),
|
||||
enableHapticFeedback<bool>(126, type: bool),
|
||||
;
|
||||
|
||||
const StoreKey(
|
||||
|
||||
56
mobile/lib/shared/providers/haptic_feedback.provider.dart
Normal file
56
mobile/lib/shared/providers/haptic_feedback.provider.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
|
||||
final hapticFeedbackProvider =
|
||||
StateNotifierProvider<HapticNotifier, void>((ref) {
|
||||
return HapticNotifier(ref);
|
||||
});
|
||||
|
||||
class HapticNotifier extends StateNotifier<void> {
|
||||
void build() {}
|
||||
final Ref _ref;
|
||||
|
||||
HapticNotifier(this._ref) : super(null);
|
||||
|
||||
selectionClick() {
|
||||
if (_ref
|
||||
.read(appSettingsServiceProvider)
|
||||
.getSetting(AppSettingsEnum.enableHapticFeedback)) {
|
||||
HapticFeedback.selectionClick();
|
||||
}
|
||||
}
|
||||
|
||||
lightImpact() {
|
||||
if (_ref
|
||||
.read(appSettingsServiceProvider)
|
||||
.getSetting(AppSettingsEnum.enableHapticFeedback)) {
|
||||
HapticFeedback.lightImpact();
|
||||
}
|
||||
}
|
||||
|
||||
mediumImpact() {
|
||||
if (_ref
|
||||
.read(appSettingsServiceProvider)
|
||||
.getSetting(AppSettingsEnum.enableHapticFeedback)) {
|
||||
HapticFeedback.mediumImpact();
|
||||
}
|
||||
}
|
||||
|
||||
heavyImpact() {
|
||||
if (_ref
|
||||
.read(appSettingsServiceProvider)
|
||||
.getSetting(AppSettingsEnum.enableHapticFeedback)) {
|
||||
HapticFeedback.heavyImpact();
|
||||
}
|
||||
}
|
||||
|
||||
vibrate() {
|
||||
if (_ref
|
||||
.read(appSettingsServiceProvider)
|
||||
.getSetting(AppSettingsEnum.enableHapticFeedback)) {
|
||||
HapticFeedback.vibrate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,18 +88,19 @@ class ImmichLogger {
|
||||
Future<void> shareLogs() async {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final dateTime = DateTime.now().toIso8601String();
|
||||
final filePath = '${tempDir.path}/Immich_log_$dateTime.csv';
|
||||
final filePath = '${tempDir.path}/Immich_log_$dateTime.log';
|
||||
final logFile = await File(filePath).create();
|
||||
final io = logFile.openWrite();
|
||||
try {
|
||||
// Write header
|
||||
io.write("created_at,level,context,message,stacktrace\n");
|
||||
|
||||
// Write messages
|
||||
for (final m in messages) {
|
||||
io.write(
|
||||
'${m.createdAt},${m.level},"${m.context1 ?? ""}","${m.message}","${m.context2 ?? ""}"\n',
|
||||
);
|
||||
final created = m.createdAt;
|
||||
final level = m.level.name.padRight(8);
|
||||
final logger = (m.context1 ?? "<UNKNOWN_LOGGER>").padRight(20);
|
||||
final message = m.message;
|
||||
final error = m.details != null ? " ${m.details} |" : "";
|
||||
final stack = m.context2 != null ? "\n${m.context2!}" : "";
|
||||
io.write('$created | $level | $logger | $message |$error$stack\n');
|
||||
}
|
||||
} finally {
|
||||
await io.flush();
|
||||
|
||||
@@ -39,27 +39,24 @@ class ImmichAppBarDialog extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
buildTopRow() {
|
||||
return Row(
|
||||
return Stack(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () => context.pop(),
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
size: 20,
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: InkWell(
|
||||
onTap: () => context.pop(),
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'IMMICH',
|
||||
style: TextStyle(
|
||||
fontFamily: 'SnowburstOne',
|
||||
fontWeight: FontWeight.bold,
|
||||
color: context.primaryColor,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Image.asset(
|
||||
context.isDarkTheme
|
||||
? 'assets/immich-text-dark.png'
|
||||
: 'assets/immich-text-light.png',
|
||||
height: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -63,7 +63,7 @@ class MultiselectGrid extends HookConsumerWidget {
|
||||
const Center(child: ImmichLoadingIndicator());
|
||||
|
||||
Widget buildEmptyIndicator() =>
|
||||
emptyIndicator ?? const Center(child: Text("No assets to show"));
|
||||
emptyIndicator ?? Center(child: const Text("no_assets_to_show").tr());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart';
|
||||
import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/tab.provider.dart';
|
||||
|
||||
@RoutePage()
|
||||
@@ -53,7 +53,7 @@ class TabControllerPage extends HookConsumerWidget {
|
||||
scrollToTopNotifierProvider.scrollToTop();
|
||||
}
|
||||
|
||||
HapticFeedback.selectionClick();
|
||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||
tabsRouter.setActiveIndex(index);
|
||||
ref.read(tabProvider.notifier).state = TabEnum.values[index];
|
||||
},
|
||||
@@ -107,7 +107,7 @@ class TabControllerPage extends HookConsumerWidget {
|
||||
scrollToTopNotifierProvider.scrollToTop();
|
||||
}
|
||||
|
||||
HapticFeedback.selectionClick();
|
||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||
tabsRouter.setActiveIndex(index);
|
||||
ref.read(tabProvider.notifier).state = TabEnum.values[index];
|
||||
},
|
||||
|
||||
27
mobile/openapi/.openapi-generator/FILES
generated
27
mobile/openapi/.openapi-generator/FILES
generated
@@ -13,6 +13,7 @@ doc/ActivityCreateDto.md
|
||||
doc/ActivityResponseDto.md
|
||||
doc/ActivityStatisticsResponseDto.md
|
||||
doc/AddUsersDto.md
|
||||
doc/AdminOnboardingUpdateDto.md
|
||||
doc/AlbumApi.md
|
||||
doc/AlbumCountResponseDto.md
|
||||
doc/AlbumResponseDto.md
|
||||
@@ -24,6 +25,7 @@ doc/AssetBulkUploadCheckDto.md
|
||||
doc/AssetBulkUploadCheckItem.md
|
||||
doc/AssetBulkUploadCheckResponseDto.md
|
||||
doc/AssetBulkUploadCheckResult.md
|
||||
doc/AssetDeltaSyncResponseDto.md
|
||||
doc/AssetFaceResponseDto.md
|
||||
doc/AssetFaceUpdateDto.md
|
||||
doc/AssetFaceUpdateItem.md
|
||||
@@ -40,7 +42,6 @@ doc/AssetTypeEnum.md
|
||||
doc/AudioCodec.md
|
||||
doc/AuditApi.md
|
||||
doc/AuditDeletesResponseDto.md
|
||||
doc/AuthDeviceResponseDto.md
|
||||
doc/AuthenticationApi.md
|
||||
doc/BulkIdResponseDto.md
|
||||
doc/BulkIdsDto.md
|
||||
@@ -69,6 +70,7 @@ doc/FaceApi.md
|
||||
doc/FaceDto.md
|
||||
doc/FileChecksumDto.md
|
||||
doc/FileChecksumResponseDto.md
|
||||
doc/FileReportApi.md
|
||||
doc/FileReportDto.md
|
||||
doc/FileReportFixDto.md
|
||||
doc/FileReportItemDto.md
|
||||
@@ -122,6 +124,7 @@ doc/QueueStatusDto.md
|
||||
doc/ReactionLevel.md
|
||||
doc/ReactionType.md
|
||||
doc/RecognitionConfig.md
|
||||
doc/ReverseGeocodingStateResponseDto.md
|
||||
doc/ScanLibraryDto.md
|
||||
doc/SearchAlbumResponseDto.md
|
||||
doc/SearchApi.md
|
||||
@@ -141,6 +144,8 @@ doc/ServerPingResponse.md
|
||||
doc/ServerStatsResponseDto.md
|
||||
doc/ServerThemeDto.md
|
||||
doc/ServerVersionResponseDto.md
|
||||
doc/SessionResponseDto.md
|
||||
doc/SessionsApi.md
|
||||
doc/SharedLinkApi.md
|
||||
doc/SharedLinkCreateDto.md
|
||||
doc/SharedLinkEditDto.md
|
||||
@@ -149,6 +154,7 @@ doc/SharedLinkType.md
|
||||
doc/SignUpDto.md
|
||||
doc/SmartInfoResponseDto.md
|
||||
doc/SmartSearchDto.md
|
||||
doc/SyncApi.md
|
||||
doc/SystemConfigApi.md
|
||||
doc/SystemConfigDto.md
|
||||
doc/SystemConfigFFmpegDto.md
|
||||
@@ -170,6 +176,7 @@ doc/SystemConfigTemplateStorageOptionDto.md
|
||||
doc/SystemConfigThemeDto.md
|
||||
doc/SystemConfigTrashDto.md
|
||||
doc/SystemConfigUserDto.md
|
||||
doc/SystemMetadataApi.md
|
||||
doc/TagApi.md
|
||||
doc/TagResponseDto.md
|
||||
doc/TagTypeEnum.md
|
||||
@@ -209,6 +216,7 @@ lib/api/audit_api.dart
|
||||
lib/api/authentication_api.dart
|
||||
lib/api/download_api.dart
|
||||
lib/api/face_api.dart
|
||||
lib/api/file_report_api.dart
|
||||
lib/api/job_api.dart
|
||||
lib/api/library_api.dart
|
||||
lib/api/memory_api.dart
|
||||
@@ -217,8 +225,11 @@ lib/api/partner_api.dart
|
||||
lib/api/person_api.dart
|
||||
lib/api/search_api.dart
|
||||
lib/api/server_info_api.dart
|
||||
lib/api/sessions_api.dart
|
||||
lib/api/shared_link_api.dart
|
||||
lib/api/sync_api.dart
|
||||
lib/api/system_config_api.dart
|
||||
lib/api/system_metadata_api.dart
|
||||
lib/api/tag_api.dart
|
||||
lib/api/timeline_api.dart
|
||||
lib/api/trash_api.dart
|
||||
@@ -235,6 +246,7 @@ lib/model/activity_create_dto.dart
|
||||
lib/model/activity_response_dto.dart
|
||||
lib/model/activity_statistics_response_dto.dart
|
||||
lib/model/add_users_dto.dart
|
||||
lib/model/admin_onboarding_update_dto.dart
|
||||
lib/model/album_count_response_dto.dart
|
||||
lib/model/album_response_dto.dart
|
||||
lib/model/all_job_status_response_dto.dart
|
||||
@@ -248,6 +260,7 @@ lib/model/asset_bulk_upload_check_dto.dart
|
||||
lib/model/asset_bulk_upload_check_item.dart
|
||||
lib/model/asset_bulk_upload_check_response_dto.dart
|
||||
lib/model/asset_bulk_upload_check_result.dart
|
||||
lib/model/asset_delta_sync_response_dto.dart
|
||||
lib/model/asset_face_response_dto.dart
|
||||
lib/model/asset_face_update_dto.dart
|
||||
lib/model/asset_face_update_item.dart
|
||||
@@ -263,7 +276,6 @@ lib/model/asset_stats_response_dto.dart
|
||||
lib/model/asset_type_enum.dart
|
||||
lib/model/audio_codec.dart
|
||||
lib/model/audit_deletes_response_dto.dart
|
||||
lib/model/auth_device_response_dto.dart
|
||||
lib/model/bulk_id_response_dto.dart
|
||||
lib/model/bulk_ids_dto.dart
|
||||
lib/model/change_password_dto.dart
|
||||
@@ -336,6 +348,7 @@ lib/model/queue_status_dto.dart
|
||||
lib/model/reaction_level.dart
|
||||
lib/model/reaction_type.dart
|
||||
lib/model/recognition_config.dart
|
||||
lib/model/reverse_geocoding_state_response_dto.dart
|
||||
lib/model/scan_library_dto.dart
|
||||
lib/model/search_album_response_dto.dart
|
||||
lib/model/search_asset_response_dto.dart
|
||||
@@ -353,6 +366,7 @@ lib/model/server_ping_response.dart
|
||||
lib/model/server_stats_response_dto.dart
|
||||
lib/model/server_theme_dto.dart
|
||||
lib/model/server_version_response_dto.dart
|
||||
lib/model/session_response_dto.dart
|
||||
lib/model/shared_link_create_dto.dart
|
||||
lib/model/shared_link_edit_dto.dart
|
||||
lib/model/shared_link_response_dto.dart
|
||||
@@ -411,6 +425,7 @@ test/activity_create_dto_test.dart
|
||||
test/activity_response_dto_test.dart
|
||||
test/activity_statistics_response_dto_test.dart
|
||||
test/add_users_dto_test.dart
|
||||
test/admin_onboarding_update_dto_test.dart
|
||||
test/album_api_test.dart
|
||||
test/album_count_response_dto_test.dart
|
||||
test/album_response_dto_test.dart
|
||||
@@ -427,6 +442,7 @@ test/asset_bulk_upload_check_dto_test.dart
|
||||
test/asset_bulk_upload_check_item_test.dart
|
||||
test/asset_bulk_upload_check_response_dto_test.dart
|
||||
test/asset_bulk_upload_check_result_test.dart
|
||||
test/asset_delta_sync_response_dto_test.dart
|
||||
test/asset_face_response_dto_test.dart
|
||||
test/asset_face_update_dto_test.dart
|
||||
test/asset_face_update_item_test.dart
|
||||
@@ -443,7 +459,6 @@ test/asset_type_enum_test.dart
|
||||
test/audio_codec_test.dart
|
||||
test/audit_api_test.dart
|
||||
test/audit_deletes_response_dto_test.dart
|
||||
test/auth_device_response_dto_test.dart
|
||||
test/authentication_api_test.dart
|
||||
test/bulk_id_response_dto_test.dart
|
||||
test/bulk_ids_dto_test.dart
|
||||
@@ -472,6 +487,7 @@ test/face_api_test.dart
|
||||
test/face_dto_test.dart
|
||||
test/file_checksum_dto_test.dart
|
||||
test/file_checksum_response_dto_test.dart
|
||||
test/file_report_api_test.dart
|
||||
test/file_report_dto_test.dart
|
||||
test/file_report_fix_dto_test.dart
|
||||
test/file_report_item_dto_test.dart
|
||||
@@ -525,6 +541,7 @@ test/queue_status_dto_test.dart
|
||||
test/reaction_level_test.dart
|
||||
test/reaction_type_test.dart
|
||||
test/recognition_config_test.dart
|
||||
test/reverse_geocoding_state_response_dto_test.dart
|
||||
test/scan_library_dto_test.dart
|
||||
test/search_album_response_dto_test.dart
|
||||
test/search_api_test.dart
|
||||
@@ -544,6 +561,8 @@ test/server_ping_response_test.dart
|
||||
test/server_stats_response_dto_test.dart
|
||||
test/server_theme_dto_test.dart
|
||||
test/server_version_response_dto_test.dart
|
||||
test/session_response_dto_test.dart
|
||||
test/sessions_api_test.dart
|
||||
test/shared_link_api_test.dart
|
||||
test/shared_link_create_dto_test.dart
|
||||
test/shared_link_edit_dto_test.dart
|
||||
@@ -552,6 +571,7 @@ test/shared_link_type_test.dart
|
||||
test/sign_up_dto_test.dart
|
||||
test/smart_info_response_dto_test.dart
|
||||
test/smart_search_dto_test.dart
|
||||
test/sync_api_test.dart
|
||||
test/system_config_api_test.dart
|
||||
test/system_config_dto_test.dart
|
||||
test/system_config_f_fmpeg_dto_test.dart
|
||||
@@ -573,6 +593,7 @@ test/system_config_template_storage_option_dto_test.dart
|
||||
test/system_config_theme_dto_test.dart
|
||||
test/system_config_trash_dto_test.dart
|
||||
test/system_config_user_dto_test.dart
|
||||
test/system_metadata_api_test.dart
|
||||
test/tag_api_test.dart
|
||||
test/tag_response_dto_test.dart
|
||||
test/tag_type_enum_test.dart
|
||||
|
||||
25
mobile/openapi/README.md
generated
25
mobile/openapi/README.md
generated
@@ -3,7 +3,7 @@ Immich API
|
||||
|
||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 1.101.0
|
||||
- API version: 1.102.3
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
## Requirements
|
||||
@@ -112,16 +112,10 @@ Class | Method | HTTP request | Description
|
||||
*AssetApi* | [**updateAssets**](doc//AssetApi.md#updateassets) | **PUT** /asset |
|
||||
*AssetApi* | [**updateStackParent**](doc//AssetApi.md#updatestackparent) | **PUT** /asset/stack/parent |
|
||||
*AssetApi* | [**uploadFile**](doc//AssetApi.md#uploadfile) | **POST** /asset/upload |
|
||||
*AuditApi* | [**fixAuditFiles**](doc//AuditApi.md#fixauditfiles) | **POST** /audit/file-report/fix |
|
||||
*AuditApi* | [**getAuditDeletes**](doc//AuditApi.md#getauditdeletes) | **GET** /audit/deletes |
|
||||
*AuditApi* | [**getAuditFiles**](doc//AuditApi.md#getauditfiles) | **GET** /audit/file-report |
|
||||
*AuditApi* | [**getFileChecksums**](doc//AuditApi.md#getfilechecksums) | **POST** /audit/file-report/checksum |
|
||||
*AuthenticationApi* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password |
|
||||
*AuthenticationApi* | [**getAuthDevices**](doc//AuthenticationApi.md#getauthdevices) | **GET** /auth/devices |
|
||||
*AuthenticationApi* | [**login**](doc//AuthenticationApi.md#login) | **POST** /auth/login |
|
||||
*AuthenticationApi* | [**logout**](doc//AuthenticationApi.md#logout) | **POST** /auth/logout |
|
||||
*AuthenticationApi* | [**logoutAuthDevice**](doc//AuthenticationApi.md#logoutauthdevice) | **DELETE** /auth/devices/{id} |
|
||||
*AuthenticationApi* | [**logoutAuthDevices**](doc//AuthenticationApi.md#logoutauthdevices) | **DELETE** /auth/devices |
|
||||
*AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up |
|
||||
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
|
||||
*DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive |
|
||||
@@ -129,6 +123,9 @@ Class | Method | HTTP request | Description
|
||||
*DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info |
|
||||
*FaceApi* | [**getFaces**](doc//FaceApi.md#getfaces) | **GET** /face |
|
||||
*FaceApi* | [**reassignFacesById**](doc//FaceApi.md#reassignfacesbyid) | **PUT** /face/{id} |
|
||||
*FileReportApi* | [**fixAuditFiles**](doc//FileReportApi.md#fixauditfiles) | **POST** /report/fix |
|
||||
*FileReportApi* | [**getAuditFiles**](doc//FileReportApi.md#getauditfiles) | **GET** /report |
|
||||
*FileReportApi* | [**getFileChecksums**](doc//FileReportApi.md#getfilechecksums) | **POST** /report/checksum |
|
||||
*JobApi* | [**getAllJobsStatus**](doc//JobApi.md#getalljobsstatus) | **GET** /jobs |
|
||||
*JobApi* | [**sendJobCommand**](doc//JobApi.md#sendjobcommand) | **PUT** /jobs/{id} |
|
||||
*LibraryApi* | [**createLibrary**](doc//LibraryApi.md#createlibrary) | **POST** /library |
|
||||
@@ -182,7 +179,9 @@ Class | Method | HTTP request | Description
|
||||
*ServerInfoApi* | [**getSupportedMediaTypes**](doc//ServerInfoApi.md#getsupportedmediatypes) | **GET** /server-info/media-types |
|
||||
*ServerInfoApi* | [**getTheme**](doc//ServerInfoApi.md#gettheme) | **GET** /server-info/theme |
|
||||
*ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping |
|
||||
*ServerInfoApi* | [**setAdminOnboarding**](doc//ServerInfoApi.md#setadminonboarding) | **POST** /server-info/admin-onboarding |
|
||||
*SessionsApi* | [**deleteAllSessions**](doc//SessionsApi.md#deleteallsessions) | **DELETE** /sessions |
|
||||
*SessionsApi* | [**deleteSession**](doc//SessionsApi.md#deletesession) | **DELETE** /sessions/{id} |
|
||||
*SessionsApi* | [**getSessions**](doc//SessionsApi.md#getsessions) | **GET** /sessions |
|
||||
*SharedLinkApi* | [**addSharedLinkAssets**](doc//SharedLinkApi.md#addsharedlinkassets) | **PUT** /shared-link/{id}/assets |
|
||||
*SharedLinkApi* | [**createSharedLink**](doc//SharedLinkApi.md#createsharedlink) | **POST** /shared-link |
|
||||
*SharedLinkApi* | [**getAllSharedLinks**](doc//SharedLinkApi.md#getallsharedlinks) | **GET** /shared-link |
|
||||
@@ -191,11 +190,16 @@ Class | Method | HTTP request | Description
|
||||
*SharedLinkApi* | [**removeSharedLink**](doc//SharedLinkApi.md#removesharedlink) | **DELETE** /shared-link/{id} |
|
||||
*SharedLinkApi* | [**removeSharedLinkAssets**](doc//SharedLinkApi.md#removesharedlinkassets) | **DELETE** /shared-link/{id}/assets |
|
||||
*SharedLinkApi* | [**updateSharedLink**](doc//SharedLinkApi.md#updatesharedlink) | **PATCH** /shared-link/{id} |
|
||||
*SyncApi* | [**getAllForUserFullSync**](doc//SyncApi.md#getallforuserfullsync) | **GET** /sync/full-sync |
|
||||
*SyncApi* | [**getDeltaSync**](doc//SyncApi.md#getdeltasync) | **GET** /sync/delta-sync |
|
||||
*SystemConfigApi* | [**getConfig**](doc//SystemConfigApi.md#getconfig) | **GET** /system-config |
|
||||
*SystemConfigApi* | [**getConfigDefaults**](doc//SystemConfigApi.md#getconfigdefaults) | **GET** /system-config/defaults |
|
||||
*SystemConfigApi* | [**getMapStyle**](doc//SystemConfigApi.md#getmapstyle) | **GET** /system-config/map/style.json |
|
||||
*SystemConfigApi* | [**getStorageTemplateOptions**](doc//SystemConfigApi.md#getstoragetemplateoptions) | **GET** /system-config/storage-template-options |
|
||||
*SystemConfigApi* | [**updateConfig**](doc//SystemConfigApi.md#updateconfig) | **PUT** /system-config |
|
||||
*SystemMetadataApi* | [**getAdminOnboarding**](doc//SystemMetadataApi.md#getadminonboarding) | **GET** /system-metadata/admin-onboarding |
|
||||
*SystemMetadataApi* | [**getReverseGeocodingState**](doc//SystemMetadataApi.md#getreversegeocodingstate) | **GET** /system-metadata/reverse-geocoding-state |
|
||||
*SystemMetadataApi* | [**updateAdminOnboarding**](doc//SystemMetadataApi.md#updateadminonboarding) | **POST** /system-metadata/admin-onboarding |
|
||||
*TagApi* | [**createTag**](doc//TagApi.md#createtag) | **POST** /tag |
|
||||
*TagApi* | [**deleteTag**](doc//TagApi.md#deletetag) | **DELETE** /tag/{id} |
|
||||
*TagApi* | [**getAllTags**](doc//TagApi.md#getalltags) | **GET** /tag |
|
||||
@@ -231,6 +235,7 @@ Class | Method | HTTP request | Description
|
||||
- [ActivityResponseDto](doc//ActivityResponseDto.md)
|
||||
- [ActivityStatisticsResponseDto](doc//ActivityStatisticsResponseDto.md)
|
||||
- [AddUsersDto](doc//AddUsersDto.md)
|
||||
- [AdminOnboardingUpdateDto](doc//AdminOnboardingUpdateDto.md)
|
||||
- [AlbumCountResponseDto](doc//AlbumCountResponseDto.md)
|
||||
- [AlbumResponseDto](doc//AlbumResponseDto.md)
|
||||
- [AllJobStatusResponseDto](doc//AllJobStatusResponseDto.md)
|
||||
@@ -240,6 +245,7 @@ Class | Method | HTTP request | Description
|
||||
- [AssetBulkUploadCheckItem](doc//AssetBulkUploadCheckItem.md)
|
||||
- [AssetBulkUploadCheckResponseDto](doc//AssetBulkUploadCheckResponseDto.md)
|
||||
- [AssetBulkUploadCheckResult](doc//AssetBulkUploadCheckResult.md)
|
||||
- [AssetDeltaSyncResponseDto](doc//AssetDeltaSyncResponseDto.md)
|
||||
- [AssetFaceResponseDto](doc//AssetFaceResponseDto.md)
|
||||
- [AssetFaceUpdateDto](doc//AssetFaceUpdateDto.md)
|
||||
- [AssetFaceUpdateItem](doc//AssetFaceUpdateItem.md)
|
||||
@@ -255,7 +261,6 @@ Class | Method | HTTP request | Description
|
||||
- [AssetTypeEnum](doc//AssetTypeEnum.md)
|
||||
- [AudioCodec](doc//AudioCodec.md)
|
||||
- [AuditDeletesResponseDto](doc//AuditDeletesResponseDto.md)
|
||||
- [AuthDeviceResponseDto](doc//AuthDeviceResponseDto.md)
|
||||
- [BulkIdResponseDto](doc//BulkIdResponseDto.md)
|
||||
- [BulkIdsDto](doc//BulkIdsDto.md)
|
||||
- [CLIPConfig](doc//CLIPConfig.md)
|
||||
@@ -328,6 +333,7 @@ Class | Method | HTTP request | Description
|
||||
- [ReactionLevel](doc//ReactionLevel.md)
|
||||
- [ReactionType](doc//ReactionType.md)
|
||||
- [RecognitionConfig](doc//RecognitionConfig.md)
|
||||
- [ReverseGeocodingStateResponseDto](doc//ReverseGeocodingStateResponseDto.md)
|
||||
- [ScanLibraryDto](doc//ScanLibraryDto.md)
|
||||
- [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md)
|
||||
- [SearchAssetResponseDto](doc//SearchAssetResponseDto.md)
|
||||
@@ -345,6 +351,7 @@ Class | Method | HTTP request | Description
|
||||
- [ServerStatsResponseDto](doc//ServerStatsResponseDto.md)
|
||||
- [ServerThemeDto](doc//ServerThemeDto.md)
|
||||
- [ServerVersionResponseDto](doc//ServerVersionResponseDto.md)
|
||||
- [SessionResponseDto](doc//SessionResponseDto.md)
|
||||
- [SharedLinkCreateDto](doc//SharedLinkCreateDto.md)
|
||||
- [SharedLinkEditDto](doc//SharedLinkEditDto.md)
|
||||
- [SharedLinkResponseDto](doc//SharedLinkResponseDto.md)
|
||||
|
||||
15
mobile/openapi/doc/AdminOnboardingUpdateDto.md
generated
Normal file
15
mobile/openapi/doc/AdminOnboardingUpdateDto.md
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
# openapi.model.AdminOnboardingUpdateDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**isOnboarded** | **bool** | |
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
17
mobile/openapi/doc/AssetDeltaSyncResponseDto.md
generated
Normal file
17
mobile/openapi/doc/AssetDeltaSyncResponseDto.md
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
# openapi.model.AssetDeltaSyncResponseDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**deleted** | **List<String>** | | [default to const []]
|
||||
**needsFullSync** | **bool** | |
|
||||
**upserted** | [**List<AssetResponseDto>**](AssetResponseDto.md) | | [default to const []]
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
163
mobile/openapi/doc/AuditApi.md
generated
163
mobile/openapi/doc/AuditApi.md
generated
@@ -9,66 +9,9 @@ All URIs are relative to */api*
|
||||
|
||||
Method | HTTP request | Description
|
||||
------------- | ------------- | -------------
|
||||
[**fixAuditFiles**](AuditApi.md#fixauditfiles) | **POST** /audit/file-report/fix |
|
||||
[**getAuditDeletes**](AuditApi.md#getauditdeletes) | **GET** /audit/deletes |
|
||||
[**getAuditFiles**](AuditApi.md#getauditfiles) | **GET** /audit/file-report |
|
||||
[**getFileChecksums**](AuditApi.md#getfilechecksums) | **POST** /audit/file-report/checksum |
|
||||
|
||||
|
||||
# **fixAuditFiles**
|
||||
> fixAuditFiles(fileReportFixDto)
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
// TODO Configure API key authorization: cookie
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure API key authorization: api_key
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure HTTP Bearer authorization: bearer
|
||||
// Case 1. Use String Token
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||
// Case 2. Use Function which generate token.
|
||||
// String yourTokenGeneratorFunction() { ... }
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||
|
||||
final api_instance = AuditApi();
|
||||
final fileReportFixDto = FileReportFixDto(); // FileReportFixDto |
|
||||
|
||||
try {
|
||||
api_instance.fixAuditFiles(fileReportFixDto);
|
||||
} catch (e) {
|
||||
print('Exception when calling AuditApi->fixAuditFiles: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**fileReportFixDto** | [**FileReportFixDto**](FileReportFixDto.md)| |
|
||||
|
||||
### Return type
|
||||
|
||||
void (empty response body)
|
||||
|
||||
### Authorization
|
||||
|
||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: application/json
|
||||
- **Accept**: Not defined
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **getAuditDeletes**
|
||||
> AuditDeletesResponseDto getAuditDeletes(after, entityType, userId)
|
||||
|
||||
@@ -128,109 +71,3 @@ Name | Type | Description | Notes
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **getAuditFiles**
|
||||
> FileReportDto getAuditFiles()
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
// TODO Configure API key authorization: cookie
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure API key authorization: api_key
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure HTTP Bearer authorization: bearer
|
||||
// Case 1. Use String Token
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||
// Case 2. Use Function which generate token.
|
||||
// String yourTokenGeneratorFunction() { ... }
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||
|
||||
final api_instance = AuditApi();
|
||||
|
||||
try {
|
||||
final result = api_instance.getAuditFiles();
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling AuditApi->getAuditFiles: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
This endpoint does not need any parameter.
|
||||
|
||||
### Return type
|
||||
|
||||
[**FileReportDto**](FileReportDto.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **getFileChecksums**
|
||||
> List<FileChecksumResponseDto> getFileChecksums(fileChecksumDto)
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
// TODO Configure API key authorization: cookie
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure API key authorization: api_key
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure HTTP Bearer authorization: bearer
|
||||
// Case 1. Use String Token
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||
// Case 2. Use Function which generate token.
|
||||
// String yourTokenGeneratorFunction() { ... }
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||
|
||||
final api_instance = AuditApi();
|
||||
final fileChecksumDto = FileChecksumDto(); // FileChecksumDto |
|
||||
|
||||
try {
|
||||
final result = api_instance.getFileChecksums(fileChecksumDto);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling AuditApi->getFileChecksums: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**fileChecksumDto** | [**FileChecksumDto**](FileChecksumDto.md)| |
|
||||
|
||||
### Return type
|
||||
|
||||
[**List<FileChecksumResponseDto>**](FileChecksumResponseDto.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: application/json
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
158
mobile/openapi/doc/AuthenticationApi.md
generated
158
mobile/openapi/doc/AuthenticationApi.md
generated
@@ -10,11 +10,8 @@ All URIs are relative to */api*
|
||||
Method | HTTP request | Description
|
||||
------------- | ------------- | -------------
|
||||
[**changePassword**](AuthenticationApi.md#changepassword) | **POST** /auth/change-password |
|
||||
[**getAuthDevices**](AuthenticationApi.md#getauthdevices) | **GET** /auth/devices |
|
||||
[**login**](AuthenticationApi.md#login) | **POST** /auth/login |
|
||||
[**logout**](AuthenticationApi.md#logout) | **POST** /auth/logout |
|
||||
[**logoutAuthDevice**](AuthenticationApi.md#logoutauthdevice) | **DELETE** /auth/devices/{id} |
|
||||
[**logoutAuthDevices**](AuthenticationApi.md#logoutauthdevices) | **DELETE** /auth/devices |
|
||||
[**signUpAdmin**](AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up |
|
||||
[**validateAccessToken**](AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
|
||||
|
||||
@@ -74,57 +71,6 @@ Name | Type | Description | Notes
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **getAuthDevices**
|
||||
> List<AuthDeviceResponseDto> getAuthDevices()
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
// TODO Configure API key authorization: cookie
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure API key authorization: api_key
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure HTTP Bearer authorization: bearer
|
||||
// Case 1. Use String Token
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||
// Case 2. Use Function which generate token.
|
||||
// String yourTokenGeneratorFunction() { ... }
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||
|
||||
final api_instance = AuthenticationApi();
|
||||
|
||||
try {
|
||||
final result = api_instance.getAuthDevices();
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling AuthenticationApi->getAuthDevices: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
This endpoint does not need any parameter.
|
||||
|
||||
### Return type
|
||||
|
||||
[**List<AuthDeviceResponseDto>**](AuthDeviceResponseDto.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **login**
|
||||
> LoginResponseDto login(loginCredentialDto)
|
||||
|
||||
@@ -217,110 +163,6 @@ This endpoint does not need any parameter.
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **logoutAuthDevice**
|
||||
> logoutAuthDevice(id)
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
// TODO Configure API key authorization: cookie
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure API key authorization: api_key
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure HTTP Bearer authorization: bearer
|
||||
// Case 1. Use String Token
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||
// Case 2. Use Function which generate token.
|
||||
// String yourTokenGeneratorFunction() { ... }
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||
|
||||
final api_instance = AuthenticationApi();
|
||||
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
||||
|
||||
try {
|
||||
api_instance.logoutAuthDevice(id);
|
||||
} catch (e) {
|
||||
print('Exception when calling AuthenticationApi->logoutAuthDevice: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**id** | **String**| |
|
||||
|
||||
### Return type
|
||||
|
||||
void (empty response body)
|
||||
|
||||
### Authorization
|
||||
|
||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: Not defined
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **logoutAuthDevices**
|
||||
> logoutAuthDevices()
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
// TODO Configure API key authorization: cookie
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure API key authorization: api_key
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure HTTP Bearer authorization: bearer
|
||||
// Case 1. Use String Token
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||
// Case 2. Use Function which generate token.
|
||||
// String yourTokenGeneratorFunction() { ... }
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||
|
||||
final api_instance = AuthenticationApi();
|
||||
|
||||
try {
|
||||
api_instance.logoutAuthDevices();
|
||||
} catch (e) {
|
||||
print('Exception when calling AuthenticationApi->logoutAuthDevices: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
This endpoint does not need any parameter.
|
||||
|
||||
### Return type
|
||||
|
||||
void (empty response body)
|
||||
|
||||
### Authorization
|
||||
|
||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: Not defined
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **signUpAdmin**
|
||||
> UserResponseDto signUpAdmin(signUpDto)
|
||||
|
||||
|
||||
176
mobile/openapi/doc/FileReportApi.md
generated
Normal file
176
mobile/openapi/doc/FileReportApi.md
generated
Normal file
@@ -0,0 +1,176 @@
|
||||
# openapi.api.FileReportApi
|
||||
|
||||
## Load the API package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
All URIs are relative to */api*
|
||||
|
||||
Method | HTTP request | Description
|
||||
------------- | ------------- | -------------
|
||||
[**fixAuditFiles**](FileReportApi.md#fixauditfiles) | **POST** /report/fix |
|
||||
[**getAuditFiles**](FileReportApi.md#getauditfiles) | **GET** /report |
|
||||
[**getFileChecksums**](FileReportApi.md#getfilechecksums) | **POST** /report/checksum |
|
||||
|
||||
|
||||
# **fixAuditFiles**
|
||||
> fixAuditFiles(fileReportFixDto)
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
// TODO Configure API key authorization: cookie
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure API key authorization: api_key
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure HTTP Bearer authorization: bearer
|
||||
// Case 1. Use String Token
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||
// Case 2. Use Function which generate token.
|
||||
// String yourTokenGeneratorFunction() { ... }
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||
|
||||
final api_instance = FileReportApi();
|
||||
final fileReportFixDto = FileReportFixDto(); // FileReportFixDto |
|
||||
|
||||
try {
|
||||
api_instance.fixAuditFiles(fileReportFixDto);
|
||||
} catch (e) {
|
||||
print('Exception when calling FileReportApi->fixAuditFiles: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**fileReportFixDto** | [**FileReportFixDto**](FileReportFixDto.md)| |
|
||||
|
||||
### Return type
|
||||
|
||||
void (empty response body)
|
||||
|
||||
### Authorization
|
||||
|
||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: application/json
|
||||
- **Accept**: Not defined
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **getAuditFiles**
|
||||
> FileReportDto getAuditFiles()
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
// TODO Configure API key authorization: cookie
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure API key authorization: api_key
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure HTTP Bearer authorization: bearer
|
||||
// Case 1. Use String Token
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||
// Case 2. Use Function which generate token.
|
||||
// String yourTokenGeneratorFunction() { ... }
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||
|
||||
final api_instance = FileReportApi();
|
||||
|
||||
try {
|
||||
final result = api_instance.getAuditFiles();
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling FileReportApi->getAuditFiles: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
This endpoint does not need any parameter.
|
||||
|
||||
### Return type
|
||||
|
||||
[**FileReportDto**](FileReportDto.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **getFileChecksums**
|
||||
> List<FileChecksumResponseDto> getFileChecksums(fileChecksumDto)
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
// TODO Configure API key authorization: cookie
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure API key authorization: api_key
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure HTTP Bearer authorization: bearer
|
||||
// Case 1. Use String Token
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||
// Case 2. Use Function which generate token.
|
||||
// String yourTokenGeneratorFunction() { ... }
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||
|
||||
final api_instance = FileReportApi();
|
||||
final fileChecksumDto = FileChecksumDto(); // FileChecksumDto |
|
||||
|
||||
try {
|
||||
final result = api_instance.getFileChecksums(fileChecksumDto);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling FileReportApi->getFileChecksums: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**fileChecksumDto** | [**FileChecksumDto**](FileChecksumDto.md)| |
|
||||
|
||||
### Return type
|
||||
|
||||
[**List<FileChecksumResponseDto>**](FileChecksumResponseDto.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: application/json
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
16
mobile/openapi/doc/ReverseGeocodingStateResponseDto.md
generated
Normal file
16
mobile/openapi/doc/ReverseGeocodingStateResponseDto.md
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
# openapi.model.ReverseGeocodingStateResponseDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**lastImportFileName** | **String** | |
|
||||
**lastUpdate** | **String** | |
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
51
mobile/openapi/doc/ServerInfoApi.md
generated
51
mobile/openapi/doc/ServerInfoApi.md
generated
@@ -17,7 +17,6 @@ Method | HTTP request | Description
|
||||
[**getSupportedMediaTypes**](ServerInfoApi.md#getsupportedmediatypes) | **GET** /server-info/media-types |
|
||||
[**getTheme**](ServerInfoApi.md#gettheme) | **GET** /server-info/theme |
|
||||
[**pingServer**](ServerInfoApi.md#pingserver) | **GET** /server-info/ping |
|
||||
[**setAdminOnboarding**](ServerInfoApi.md#setadminonboarding) | **POST** /server-info/admin-onboarding |
|
||||
|
||||
|
||||
# **getServerConfig**
|
||||
@@ -344,53 +343,3 @@ No authorization required
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **setAdminOnboarding**
|
||||
> setAdminOnboarding()
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
// TODO Configure API key authorization: cookie
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure API key authorization: api_key
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure HTTP Bearer authorization: bearer
|
||||
// Case 1. Use String Token
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||
// Case 2. Use Function which generate token.
|
||||
// String yourTokenGeneratorFunction() { ... }
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||
|
||||
final api_instance = ServerInfoApi();
|
||||
|
||||
try {
|
||||
api_instance.setAdminOnboarding();
|
||||
} catch (e) {
|
||||
print('Exception when calling ServerInfoApi->setAdminOnboarding: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
This endpoint does not need any parameter.
|
||||
|
||||
### Return type
|
||||
|
||||
void (empty response body)
|
||||
|
||||
### Authorization
|
||||
|
||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: Not defined
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# openapi.model.AuthDeviceResponseDto
|
||||
# openapi.model.SessionResponseDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user