mirror of
https://github.com/immich-app/immich.git
synced 2025-12-06 17:23:10 +03:00
Compare commits
151 Commits
drift-stor
...
sqlite-rem
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
377b886bd6 | ||
|
|
5d99eabe05 | ||
|
|
184c7390a1 | ||
|
|
649221176c | ||
|
|
eae2471ab5 | ||
|
|
bfceed15da | ||
|
|
d9891f759e | ||
|
|
32f23b8d38 | ||
|
|
743b6644e9 | ||
|
|
34620e1e9a | ||
|
|
bcb968e3d1 | ||
|
|
e73abe0762 | ||
|
|
920d7de349 | ||
|
|
351701c4d6 | ||
|
|
68f249bc03 | ||
|
|
eca54871d0 | ||
|
|
b359eea124 | ||
|
|
c18f167e29 | ||
|
|
ba262fbaa8 | ||
|
|
59e7754bdc | ||
|
|
0acbf1199a | ||
|
|
daea57f7d2 | ||
|
|
82c3165247 | ||
|
|
3a854d77ac | ||
|
|
ccd0c35ca1 | ||
|
|
5f10a4cae7 | ||
|
|
9abb95d34a | ||
|
|
805ec3e351 | ||
|
|
a97ba4862f | ||
|
|
c699df002a | ||
|
|
33c29e4305 | ||
|
|
b0098d6d23 | ||
|
|
04aab6ecce | ||
|
|
47c0dc0d7e | ||
|
|
df581cc0d5 | ||
|
|
9e48ae3052 | ||
|
|
1d19d308e2 | ||
|
|
de4217cefc | ||
|
|
617a2f146d | ||
|
|
2b07d7ac63 | ||
|
|
1cc5ca14ca | ||
|
|
a625921e8f | ||
|
|
a17bba3328 | ||
|
|
4b3a4725c6 | ||
|
|
34f0f6c813 | ||
|
|
906d14c172 | ||
|
|
d087f7c870 | ||
|
|
de345a9524 | ||
|
|
badd7ea2a9 | ||
|
|
7d8f56b483 | ||
|
|
70b73145f1 | ||
|
|
d178c52ba6 | ||
|
|
55fe67dd20 | ||
|
|
ed4c7817e7 | ||
|
|
39c95f1280 | ||
|
|
4ddd3764b4 | ||
|
|
68db17028b | ||
|
|
1f50a0075e | ||
|
|
b19884d01e | ||
|
|
feff1899ee | ||
|
|
977d6452f6 | ||
|
|
f778adea92 | ||
|
|
818bdde317 | ||
|
|
fd48a33686 | ||
|
|
a918481c0b | ||
|
|
a201665b7e | ||
|
|
2a222fcfba | ||
|
|
d902e7f87d | ||
|
|
6278fe43c0 | ||
|
|
dfe6d27bbd | ||
|
|
51ab7498e9 | ||
|
|
4db76ddcf0 | ||
|
|
d03eb87058 | ||
|
|
a556de67b0 | ||
|
|
e703685d8d | ||
|
|
172388c455 | ||
|
|
df4a27e8a7 | ||
|
|
1f9813a28e | ||
|
|
bbfff45058 | ||
|
|
87dd09d103 | ||
|
|
dd94ad17aa | ||
|
|
a87c2e82cd | ||
|
|
a11ab4c3f7 | ||
|
|
ebf2f9fd7b | ||
|
|
683af67344 | ||
|
|
d149d6fa3f | ||
|
|
8c5269c002 | ||
|
|
cf91d9bdfc | ||
|
|
5579554532 | ||
|
|
7e35e6985e | ||
|
|
56756baea2 | ||
|
|
d5923241b5 | ||
|
|
cc471806fe | ||
|
|
4ce9bce414 | ||
|
|
2f5d75ce21 | ||
|
|
fb384fe90b | ||
|
|
73733370a2 | ||
|
|
4a2cf28882 | ||
|
|
181efb9010 | ||
|
|
b00d44a00c | ||
|
|
6044663e26 | ||
|
|
484529e61e | ||
|
|
445f9174ea | ||
|
|
7855974a29 | ||
|
|
ec603a008c | ||
|
|
14276f41d8 | ||
|
|
a644cabab6 | ||
|
|
b8e67d0ef9 | ||
|
|
ca78bc91b6 | ||
|
|
f2f3db3a79 | ||
|
|
c435bdb5d3 | ||
|
|
15da0d5a71 | ||
|
|
090d87f82e | ||
|
|
25efba8fe6 | ||
|
|
83afd49f5c | ||
|
|
639ede78c2 | ||
|
|
15be3437bf | ||
|
|
f59b0bab5a | ||
|
|
fa418d778b | ||
|
|
e0c4b8df6f | ||
|
|
7f9689b4bc | ||
|
|
e6f8bfdf5e | ||
|
|
8ccca04e27 | ||
|
|
53f80393bf | ||
|
|
e5e857edc3 | ||
|
|
590f96246d | ||
|
|
38d73f2bc6 | ||
|
|
96e3b96d57 | ||
|
|
36b018e355 | ||
|
|
214ca50406 | ||
|
|
29b3981609 | ||
|
|
a068a41c06 | ||
|
|
3c6e9e1191 | ||
|
|
db0415bbcc | ||
|
|
a5c431fbf5 | ||
|
|
a3d588f6bd | ||
|
|
21f500191a | ||
|
|
5011636d95 | ||
|
|
3f330c6476 | ||
|
|
bb8755021d | ||
|
|
93f9e118ad | ||
|
|
58ca1402ed | ||
|
|
32a7087883 | ||
|
|
53020852ec | ||
|
|
181a7e115f | ||
|
|
095ace8687 | ||
|
|
4c3fcdc745 | ||
|
|
fa5f30d9ca | ||
|
|
e60bc3c304 | ||
|
|
09cbc5d3f4 | ||
|
|
a2a9797fab |
@@ -73,10 +73,8 @@ install_dependencies() {
|
||||
log "Installing dependencies"
|
||||
(
|
||||
cd "${IMMICH_WORKSPACE}" || exit 1
|
||||
run_cmd make ci-server
|
||||
run_cmd make ci-sdk
|
||||
run_cmd make build-sdk
|
||||
run_cmd make ci-web
|
||||
export CI=1 FROZEN=1 OFFLINE=1
|
||||
run_cmd make setup-web-dev setup-server-dev
|
||||
)
|
||||
log ""
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ services:
|
||||
|
||||
immich-machine-learning:
|
||||
env_file: !reset []
|
||||
|
||||
|
||||
database:
|
||||
env_file: !reset []
|
||||
environment: !override
|
||||
@@ -31,7 +31,7 @@ services:
|
||||
POSTGRES_DB: ${DB_DATABASE_NAME-immich}
|
||||
POSTGRES_INITDB_ARGS: '--data-checksums'
|
||||
POSTGRES_HOST_AUTH_METHOD: md5
|
||||
volumes:
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION:-postgres-devcontainer-volume}${UPLOAD_LOCATION:+/postgres}:/var/lib/postgresql/data
|
||||
|
||||
redis:
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
design/
|
||||
docker/
|
||||
Dockerfile
|
||||
!docker/scripts
|
||||
docs/
|
||||
!docs/package.json
|
||||
@@ -19,6 +20,7 @@ mobile/
|
||||
cli/coverage/
|
||||
cli/dist/
|
||||
cli/node_modules/
|
||||
cli/Dockerfile
|
||||
|
||||
open-api/typescript-sdk/build/
|
||||
open-api/typescript-sdk/node_modules/
|
||||
@@ -29,9 +31,11 @@ server/upload/
|
||||
server/src/queries
|
||||
server/dist/
|
||||
server/www/
|
||||
server/Dockerfile
|
||||
|
||||
web/node_modules/
|
||||
web/coverage/
|
||||
web/.svelte-kit
|
||||
web/build/
|
||||
web/.env
|
||||
web/Dockerfile
|
||||
|
||||
2
.github/.nvmrc
vendored
2
.github/.nvmrc
vendored
@@ -1 +1 @@
|
||||
22.16.0
|
||||
22.17.0
|
||||
|
||||
4
.github/.prettierignore
vendored
Normal file
4
.github/.prettierignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
6
.github/package-lock.json
generated
vendored
6
.github/package-lock.json
generated
vendored
@@ -9,9 +9,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
|
||||
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
||||
16
.github/workflows/build-mobile.yml
vendored
16
.github/workflows/build-mobile.yml
vendored
@@ -66,12 +66,6 @@ jobs:
|
||||
ref: ${{ inputs.ref || github.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install missing deps
|
||||
run: |
|
||||
sudo add-apt-repository ppa:rmescandon/yq
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y yq xz-utils ninja-build zstd
|
||||
|
||||
- name: Create the Keystore
|
||||
env:
|
||||
KEY_JKS: ${{ secrets.KEY_JKS }}
|
||||
@@ -96,7 +90,7 @@ jobs:
|
||||
key: build-mobile-gradle-${{ runner.os }}-main
|
||||
|
||||
- name: Setup Flutter SDK
|
||||
uses: subosito/flutter-action@395322a6cded4e9ed503aebd4cc1965625f8e59a # v2.20.0
|
||||
uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version-file: ./mobile/pubspec.yaml
|
||||
@@ -128,17 +122,17 @@ jobs:
|
||||
IS_MAIN: ${{ github.ref == 'refs/heads/main' }}
|
||||
run: |
|
||||
if [[ $IS_MAIN == 'true' ]]; then
|
||||
flutter build apk --release
|
||||
flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64
|
||||
flutter build apk --release --flavor production
|
||||
flutter build apk --release --flavor production --split-per-abi --target-platform android-arm,android-arm64,android-x64
|
||||
else
|
||||
flutter build apk --debug --split-per-abi --target-platform android-arm64
|
||||
flutter build apk --debug --flavor production --split-per-abi --target-platform android-arm64
|
||||
fi
|
||||
|
||||
- name: Publish Android Artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: release-apk-signed
|
||||
path: mobile/build/app/outputs/flutter-apk/*.apk
|
||||
path: mobile/build/app/outputs/flutter-apk/**/*.apk
|
||||
|
||||
- name: Save Gradle Cache
|
||||
id: cache-gradle-save
|
||||
|
||||
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -50,7 +50,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||
uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||
uses: github/codeql-action/autobuild@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@@ -76,6 +76,6 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||
uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
|
||||
with:
|
||||
category: '/language:${{matrix.language}}'
|
||||
|
||||
4
.github/workflows/docker.yml
vendored
4
.github/workflows/docker.yml
vendored
@@ -131,7 +131,7 @@ jobs:
|
||||
tag-suffix: '-rocm'
|
||||
platforms: linux/amd64
|
||||
runner-mapping: '{"linux/amd64": "mich"}'
|
||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@094bfb927b8cd75b343abaac27b3241be0fccfe9 # multi-runner-build-workflow-0.1.0
|
||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@129aeda75a450666ce96e8bc8126652e717917a7 # multi-runner-build-workflow-0.1.1
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
@@ -154,7 +154,7 @@ jobs:
|
||||
name: Build and Push Server
|
||||
needs: pre-job
|
||||
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
|
||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@094bfb927b8cd75b343abaac27b3241be0fccfe9 # multi-runner-build-workflow-0.1.0
|
||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@129aeda75a450666ce96e8bc8126652e717917a7 # multi-runner-build-workflow-0.1.1
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
13
.github/workflows/org-checks.yml
vendored
Normal file
13
.github/workflows/org-checks.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
name: Org Checks
|
||||
|
||||
on:
|
||||
pull_request_review:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
check-approvals:
|
||||
name: Check for Team/Admin Review
|
||||
uses: immich-app/devtools/.github/workflows/required-approval.yml@main
|
||||
permissions:
|
||||
pull-requests: read
|
||||
contents: read
|
||||
2
.github/workflows/pr-label-validation.yml
vendored
2
.github/workflows/pr-label-validation.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Require PR to have a changelog label
|
||||
uses: mheap/github-action-required-labels@fb29a14a076b0f74099f6198f77750e8fc236016 # v5.5.0
|
||||
uses: mheap/github-action-required-labels@8afbe8ae6ab7647d0c9f0cfa7c2f939650d22509 # v5.5.1
|
||||
with:
|
||||
mode: exactly
|
||||
count: 1
|
||||
|
||||
27
.github/workflows/static_analysis.yml
vendored
27
.github/workflows/static_analysis.yml
vendored
@@ -42,6 +42,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./mobile
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
@@ -49,34 +52,29 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Flutter SDK
|
||||
uses: subosito/flutter-action@395322a6cded4e9ed503aebd4cc1965625f8e59a # v2.20.0
|
||||
uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version-file: ./mobile/pubspec.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: dart pub get
|
||||
working-directory: ./mobile
|
||||
|
||||
- name: Install DCM
|
||||
run: |
|
||||
sudo apt-get update
|
||||
wget -qO- https://dcm.dev/pgp-key.public | sudo gpg --dearmor -o /usr/share/keyrings/dcm.gpg
|
||||
echo 'deb [signed-by=/usr/share/keyrings/dcm.gpg arch=amd64] https://dcm.dev/debian stable main' | sudo tee /etc/apt/sources.list.d/dart_stable.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install dcm
|
||||
uses: CQLabs/setup-dcm@8697ae0790c0852e964a6ef1d768d62a6675481a # v2.0.1
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
version: auto
|
||||
working-directory: ./mobile
|
||||
|
||||
- name: Generate translation file
|
||||
run: make translation
|
||||
working-directory: ./mobile
|
||||
|
||||
- name: Run Build Runner
|
||||
run: make build
|
||||
working-directory: ./mobile
|
||||
|
||||
- name: Generate platform API
|
||||
run: make pigeon
|
||||
working-directory: ./mobile
|
||||
|
||||
- name: Find file changes
|
||||
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
||||
@@ -98,19 +96,16 @@ jobs:
|
||||
|
||||
- name: Run dart analyze
|
||||
run: dart analyze --fatal-infos
|
||||
working-directory: ./mobile
|
||||
|
||||
- name: Run dart format
|
||||
run: dart format lib/ --set-exit-if-changed
|
||||
working-directory: ./mobile
|
||||
|
||||
- name: Run dart custom_lint
|
||||
run: dart run custom_lint
|
||||
working-directory: ./mobile
|
||||
|
||||
# TODO: Use https://github.com/CQLabs/dcm-action
|
||||
- name: Run DCM
|
||||
run: dcm analyze lib --fatal-style --fatal-warnings
|
||||
working-directory: ./mobile
|
||||
|
||||
zizmor:
|
||||
name: zizmor
|
||||
@@ -134,7 +129,7 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload SARIF file
|
||||
uses: github/codeql-action/upload-sarif@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||
uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
category: zizmor
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -516,7 +516,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Flutter SDK
|
||||
uses: subosito/flutter-action@395322a6cded4e9ed503aebd4cc1965625f8e59a # v2.20.0
|
||||
uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version-file: ./mobile/pubspec.yaml
|
||||
|
||||
19
.vscode/launch.json
vendored
19
.vscode/launch.json
vendored
@@ -18,6 +18,25 @@
|
||||
"name": "Immich Workers",
|
||||
"remoteRoot": "/usr/src/app",
|
||||
"localRoot": "${workspaceFolder}/server"
|
||||
},
|
||||
{
|
||||
"name": "Flavor - Production",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"codeLens": {
|
||||
"for": [
|
||||
"run-test",
|
||||
"run-test-file",
|
||||
"run-file",
|
||||
"debug-test",
|
||||
"debug-test-file",
|
||||
"debug-file",
|
||||
],
|
||||
"title": "${debugType}",
|
||||
},
|
||||
"args": [
|
||||
"--flavor", "production"
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
29
Makefile
29
Makefile
@@ -1,27 +1,33 @@
|
||||
dev:
|
||||
docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans || make dev-down
|
||||
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans
|
||||
|
||||
dev-down:
|
||||
docker compose -f ./docker/docker-compose.dev.yml down --remove-orphans
|
||||
|
||||
dev-update:
|
||||
docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
|
||||
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
|
||||
|
||||
dev-scale:
|
||||
docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
|
||||
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
|
||||
|
||||
.PHONY: e2e
|
||||
e2e:
|
||||
docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
||||
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
||||
|
||||
e2e-update:
|
||||
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
||||
|
||||
e2e-down:
|
||||
docker compose -f ./e2e/docker-compose.yml down --remove-orphans
|
||||
|
||||
prod:
|
||||
docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
|
||||
@trap 'make prod-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
|
||||
|
||||
prod-down:
|
||||
docker compose -f ./docker/docker-compose.prod.yml down --remove-orphans
|
||||
|
||||
prod-scale:
|
||||
docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
|
||||
@trap 'make prod-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
|
||||
|
||||
.PHONY: open-api
|
||||
open-api:
|
||||
@@ -83,7 +89,7 @@ test-medium-dev:
|
||||
docker exec -it immich_server /bin/sh -c "npm run test:medium"
|
||||
|
||||
build-all: $(foreach M,$(filter-out e2e .github,$(MODULES)),build-$M) ;
|
||||
install-all: $(foreach M,$(MODULES),install-$M) ;
|
||||
install-all: $(foreach M,$(MODULES),install-$M) ;
|
||||
ci-all: $(foreach M,$(filter-out .github,$(MODULES)),ci-$M) ;
|
||||
check-all: $(foreach M,$(filter-out sdk cli docs .github,$(MODULES)),check-$M) ;
|
||||
lint-all: $(foreach M,$(filter-out sdk docs .github,$(MODULES)),lint-$M) ;
|
||||
@@ -93,9 +99,12 @@ hygiene-all: lint-all format-all check-all sql audit-all;
|
||||
test-all: $(foreach M,$(filter-out sdk docs .github,$(MODULES)),test-$M) ;
|
||||
|
||||
clean:
|
||||
find . -name "node_modules" -type d -prune -exec rm -rf '{}' +
|
||||
find . -name "node_modules" -type d -prune -exec rm -rf {} +
|
||||
find . -name "dist" -type d -prune -exec rm -rf '{}' +
|
||||
find . -name "build" -type d -prune -exec rm -rf '{}' +
|
||||
find . -name "svelte-kit" -type d -prune -exec rm -rf '{}' +
|
||||
docker compose -f ./docker/docker-compose.dev.yml rm -v -f || true
|
||||
docker compose -f ./e2e/docker-compose.yml rm -v -f || true
|
||||
command -v docker >/dev/null 2>&1 && docker compose -f ./docker/docker-compose.dev.yml rm -v -f || true
|
||||
command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml rm -v -f || true
|
||||
|
||||
setup-server-dev: install-server
|
||||
setup-web-dev: install-sdk build-sdk install-web
|
||||
|
||||
@@ -1 +1 @@
|
||||
22.16.0
|
||||
22.17.0
|
||||
|
||||
2
cli/bin/immich
Executable file
2
cli/bin/immich
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
import '../dist/index.js';
|
||||
526
cli/package-lock.json
generated
526
cli/package-lock.json
generated
@@ -16,7 +16,7 @@
|
||||
"micromatch": "^4.0.8"
|
||||
},
|
||||
"bin": {
|
||||
"immich": "dist/index.js"
|
||||
"immich": "bin/immich"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
@@ -27,7 +27,7 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/micromatch": "^4.0.9",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^22.15.32",
|
||||
"@types/node": "^22.15.33",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
"byte-size": "^9.0.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
@@ -42,7 +42,7 @@
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
"typescript": "^5.3.3",
|
||||
"typescript-eslint": "^8.28.0",
|
||||
"vite": "^6.0.0",
|
||||
"vite": "^7.0.0",
|
||||
"vite-tsconfig-paths": "^5.0.0",
|
||||
"vitest": "^3.0.0",
|
||||
"vitest-fetch-mock": "^0.4.0",
|
||||
@@ -61,7 +61,7 @@
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.15.32",
|
||||
"@types/node": "^22.15.33",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
@@ -607,9 +607,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array": {
|
||||
"version": "0.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz",
|
||||
"integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==",
|
||||
"version": "0.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
|
||||
"integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -622,9 +622,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-helpers": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz",
|
||||
"integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==",
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz",
|
||||
"integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
@@ -682,9 +682,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz",
|
||||
"integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==",
|
||||
"version": "9.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.1.tgz",
|
||||
"integrity": "sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1276,6 +1276,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/chai": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz",
|
||||
"integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/deep-eql": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cli-progress": {
|
||||
"version": "3.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.6.tgz",
|
||||
@@ -1286,6 +1296,13 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/deep-eql": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
||||
@@ -1338,9 +1355,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.15.32",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.32.tgz",
|
||||
"integrity": "sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA==",
|
||||
"version": "22.15.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.34.tgz",
|
||||
"integrity": "sha512-8Y6E5WUupYy1Dd0II32BsWAx5MWdcnRd8L84Oys3veg1YrYtNtzgO4CFhiBg6MDSjk7Ay36HYOnU7/tuOzIzcw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1348,17 +1365,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz",
|
||||
"integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==",
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.36.0.tgz",
|
||||
"integrity": "sha512-lZNihHUVB6ZZiPBNgOQGSxUASI7UJWhT8nHyUGCnaQ28XFCw98IfrMCG3rUl1uwUWoAvodJQby2KTs79UTcrAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.32.1",
|
||||
"@typescript-eslint/type-utils": "8.32.1",
|
||||
"@typescript-eslint/utils": "8.32.1",
|
||||
"@typescript-eslint/visitor-keys": "8.32.1",
|
||||
"@typescript-eslint/scope-manager": "8.36.0",
|
||||
"@typescript-eslint/type-utils": "8.36.0",
|
||||
"@typescript-eslint/utils": "8.36.0",
|
||||
"@typescript-eslint/visitor-keys": "8.36.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^7.0.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
@@ -1372,7 +1389,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
|
||||
"@typescript-eslint/parser": "^8.36.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
@@ -1388,16 +1405,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz",
|
||||
"integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==",
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.36.0.tgz",
|
||||
"integrity": "sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.32.1",
|
||||
"@typescript-eslint/types": "8.32.1",
|
||||
"@typescript-eslint/typescript-estree": "8.32.1",
|
||||
"@typescript-eslint/visitor-keys": "8.32.1",
|
||||
"@typescript-eslint/scope-manager": "8.36.0",
|
||||
"@typescript-eslint/types": "8.36.0",
|
||||
"@typescript-eslint/typescript-estree": "8.36.0",
|
||||
"@typescript-eslint/visitor-keys": "8.36.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1412,15 +1429,37 @@
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz",
|
||||
"integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==",
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.36.0.tgz",
|
||||
"integrity": "sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.32.1",
|
||||
"@typescript-eslint/visitor-keys": "8.32.1"
|
||||
"@typescript-eslint/tsconfig-utils": "^8.36.0",
|
||||
"@typescript-eslint/types": "^8.36.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.36.0.tgz",
|
||||
"integrity": "sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.36.0",
|
||||
"@typescript-eslint/visitor-keys": "8.36.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1430,15 +1469,32 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.36.0.tgz",
|
||||
"integrity": "sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz",
|
||||
"integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==",
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.36.0.tgz",
|
||||
"integrity": "sha512-5aaGYG8cVDd6cxfk/ynpYzxBRZJk7w/ymto6uiyUFtdCozQIsQWh7M28/6r57Fwkbweng8qAzoMCPwSJfWlmsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "8.32.1",
|
||||
"@typescript-eslint/utils": "8.32.1",
|
||||
"@typescript-eslint/typescript-estree": "8.36.0",
|
||||
"@typescript-eslint/utils": "8.36.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
@@ -1455,9 +1511,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz",
|
||||
"integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==",
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.36.0.tgz",
|
||||
"integrity": "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1469,14 +1525,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz",
|
||||
"integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==",
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.36.0.tgz",
|
||||
"integrity": "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.32.1",
|
||||
"@typescript-eslint/visitor-keys": "8.32.1",
|
||||
"@typescript-eslint/project-service": "8.36.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.36.0",
|
||||
"@typescript-eslint/types": "8.36.0",
|
||||
"@typescript-eslint/visitor-keys": "8.36.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -1496,9 +1554,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1522,16 +1580,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz",
|
||||
"integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==",
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.36.0.tgz",
|
||||
"integrity": "sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.32.1",
|
||||
"@typescript-eslint/types": "8.32.1",
|
||||
"@typescript-eslint/typescript-estree": "8.32.1"
|
||||
"@typescript-eslint/scope-manager": "8.36.0",
|
||||
"@typescript-eslint/types": "8.36.0",
|
||||
"@typescript-eslint/typescript-estree": "8.36.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1546,14 +1604,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz",
|
||||
"integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==",
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.36.0.tgz",
|
||||
"integrity": "sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.32.1",
|
||||
"eslint-visitor-keys": "^4.2.0"
|
||||
"@typescript-eslint/types": "8.36.0",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1564,15 +1622,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/coverage-v8": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.4.tgz",
|
||||
"integrity": "sha512-G4p6OtioySL+hPV7Y6JHlhpsODbJzt1ndwHAFkyk6vVjpK03PFsKnauZIzcd0PrK4zAbc5lc+jeZ+eNGiMA+iw==",
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz",
|
||||
"integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.3.0",
|
||||
"@bcoe/v8-coverage": "^1.0.2",
|
||||
"debug": "^4.4.0",
|
||||
"ast-v8-to-istanbul": "^0.3.3",
|
||||
"debug": "^4.4.1",
|
||||
"istanbul-lib-coverage": "^3.2.2",
|
||||
"istanbul-lib-report": "^3.0.1",
|
||||
"istanbul-lib-source-maps": "^5.0.6",
|
||||
@@ -1587,8 +1646,8 @@
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vitest/browser": "3.1.4",
|
||||
"vitest": "3.1.4"
|
||||
"@vitest/browser": "3.2.4",
|
||||
"vitest": "3.2.4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vitest/browser": {
|
||||
@@ -1597,14 +1656,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.4.tgz",
|
||||
"integrity": "sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==",
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
|
||||
"integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "3.1.4",
|
||||
"@vitest/utils": "3.1.4",
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/spy": "3.2.4",
|
||||
"@vitest/utils": "3.2.4",
|
||||
"chai": "^5.2.0",
|
||||
"tinyrainbow": "^2.0.0"
|
||||
},
|
||||
@@ -1613,13 +1673,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.4.tgz",
|
||||
"integrity": "sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==",
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
|
||||
"integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "3.1.4",
|
||||
"@vitest/spy": "3.2.4",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.17"
|
||||
},
|
||||
@@ -1628,7 +1688,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"msw": "^2.4.9",
|
||||
"vite": "^5.0.0 || ^6.0.0"
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"msw": {
|
||||
@@ -1640,9 +1700,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/pretty-format": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.4.tgz",
|
||||
"integrity": "sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg==",
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
|
||||
"integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1653,27 +1713,28 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.4.tgz",
|
||||
"integrity": "sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==",
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
|
||||
"integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "3.1.4",
|
||||
"pathe": "^2.0.3"
|
||||
"@vitest/utils": "3.2.4",
|
||||
"pathe": "^2.0.3",
|
||||
"strip-literal": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.4.tgz",
|
||||
"integrity": "sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==",
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
|
||||
"integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "3.1.4",
|
||||
"@vitest/pretty-format": "3.2.4",
|
||||
"magic-string": "^0.30.17",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
@@ -1682,27 +1743,27 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.4.tgz",
|
||||
"integrity": "sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==",
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
|
||||
"integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tinyspy": "^3.0.2"
|
||||
"tinyspy": "^4.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.4.tgz",
|
||||
"integrity": "sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==",
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
|
||||
"integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "3.1.4",
|
||||
"loupe": "^3.1.3",
|
||||
"@vitest/pretty-format": "3.2.4",
|
||||
"loupe": "^3.1.4",
|
||||
"tinyrainbow": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
@@ -1710,9 +1771,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.14.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -1792,6 +1853,18 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/ast-v8-to-istanbul": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.3.tgz",
|
||||
"integrity": "sha512-MuXMrSLVVoA6sYN/6Hke18vMzrT4TZNbZIj/hvh0fnYFpO+/kFXcLIaiPwXXWaQUPg4yJD8fj+lfJ7/1EBconw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"estree-walker": "^3.0.3",
|
||||
"js-tokens": "^9.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@@ -2105,9 +2178,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2232,19 +2305,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.27.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz",
|
||||
"integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==",
|
||||
"version": "9.30.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.1.tgz",
|
||||
"integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
"@eslint/config-array": "^0.20.0",
|
||||
"@eslint/config-helpers": "^0.2.1",
|
||||
"@eslint/config-array": "^0.21.0",
|
||||
"@eslint/config-helpers": "^0.3.0",
|
||||
"@eslint/core": "^0.14.0",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "9.27.0",
|
||||
"@eslint/js": "9.30.1",
|
||||
"@eslint/plugin-kit": "^0.3.1",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
@@ -2256,9 +2329,9 @@
|
||||
"cross-spawn": "^7.0.6",
|
||||
"debug": "^4.3.2",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"eslint-scope": "^8.3.0",
|
||||
"eslint-visitor-keys": "^4.2.0",
|
||||
"espree": "^10.3.0",
|
||||
"eslint-scope": "^8.4.0",
|
||||
"eslint-visitor-keys": "^4.2.1",
|
||||
"espree": "^10.4.0",
|
||||
"esquery": "^1.5.0",
|
||||
"esutils": "^2.0.2",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
@@ -2309,14 +2382,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-prettier": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.0.tgz",
|
||||
"integrity": "sha512-BvQOvUhkVQM1i63iMETK9Hjud9QhqBnbtT1Zc642p9ynzBuCe5pybkOnvqZIBypXmMlsGcnU4HZ8sCTPfpAexA==",
|
||||
"version": "5.5.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.1.tgz",
|
||||
"integrity": "sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prettier-linter-helpers": "^1.0.0",
|
||||
"synckit": "^0.11.0"
|
||||
"synckit": "^0.11.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
@@ -2402,9 +2475,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz",
|
||||
"integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==",
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
|
||||
"integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
@@ -2419,9 +2492,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-visitor-keys": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
|
||||
"integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
|
||||
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
@@ -2432,15 +2505,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/espree": {
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
|
||||
"integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
|
||||
"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"acorn": "^8.14.0",
|
||||
"acorn": "^8.15.0",
|
||||
"acorn-jsx": "^5.3.2",
|
||||
"eslint-visitor-keys": "^4.2.0"
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -2749,9 +2822,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "16.1.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz",
|
||||
"integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==",
|
||||
"version": "16.3.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz",
|
||||
"integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2975,6 +3048,13 @@
|
||||
"@pkgjs/parseargs": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
|
||||
"integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
@@ -3076,9 +3156,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/loupe": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz",
|
||||
"integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==",
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz",
|
||||
"integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -3347,9 +3427,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pathval": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
|
||||
"integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
|
||||
"integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -3386,9 +3466,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.3",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
||||
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -3406,7 +3486,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.8",
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
@@ -3425,9 +3505,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
|
||||
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -3799,6 +3879,19 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-literal": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz",
|
||||
"integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^9.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
@@ -3813,14 +3906,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/synckit": {
|
||||
"version": "0.11.4",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.4.tgz",
|
||||
"integrity": "sha512-Q/XQKRaJiLiFIBNN+mndW7S/RHxvwzuZS6ZwmRzUBqJBv/5QIKCEwkBC8GBf8EQJKYnaFs0wOZbKTXBPj8L9oQ==",
|
||||
"version": "0.11.8",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz",
|
||||
"integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pkgr/core": "^0.2.3",
|
||||
"tslib": "^2.8.1"
|
||||
"@pkgr/core": "^0.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
@@ -3885,9 +3977,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
|
||||
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
|
||||
"version": "0.2.14",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
||||
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3930,9 +4022,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinypool": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz",
|
||||
"integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==",
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
|
||||
"integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -3950,9 +4042,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinyspy": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
|
||||
"integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz",
|
||||
"integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -4005,13 +4097,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
@@ -4040,15 +4125,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.32.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz",
|
||||
"integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==",
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.36.0.tgz",
|
||||
"integrity": "sha512-fTCqxthY+h9QbEgSIBfL9iV6CvKDFuoxg6bHPNpJ9HIUzS+jy2lCEyCmGyZRWEBSaykqcDPf1SJ+BfCI8DRopA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.32.1",
|
||||
"@typescript-eslint/parser": "8.32.1",
|
||||
"@typescript-eslint/utils": "8.32.1"
|
||||
"@typescript-eslint/eslint-plugin": "8.36.0",
|
||||
"@typescript-eslint/parser": "8.36.0",
|
||||
"@typescript-eslint/utils": "8.36.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -4111,24 +4196,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.4.tgz",
|
||||
"integrity": "sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
"fdir": "^6.4.6",
|
||||
"picomatch": "^4.0.2",
|
||||
"postcss": "^8.5.3",
|
||||
"rollup": "^4.34.9",
|
||||
"tinyglobby": "^0.2.13"
|
||||
"postcss": "^8.5.6",
|
||||
"rollup": "^4.40.0",
|
||||
"tinyglobby": "^0.2.14"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
@@ -4137,14 +4222,14 @@
|
||||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "*",
|
||||
"less": "^4.0.0",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "*",
|
||||
"sass-embedded": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"sass": "^1.70.0",
|
||||
"sass-embedded": "^1.70.0",
|
||||
"stylus": ">=0.54.8",
|
||||
"sugarss": "^5.0.0",
|
||||
"terser": "^5.16.0",
|
||||
"tsx": "^4.8.1",
|
||||
"yaml": "^2.4.2"
|
||||
@@ -4186,17 +4271,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite-node": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.4.tgz",
|
||||
"integrity": "sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==",
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
|
||||
"integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cac": "^6.7.14",
|
||||
"debug": "^4.4.0",
|
||||
"debug": "^4.4.1",
|
||||
"es-module-lexer": "^1.7.0",
|
||||
"pathe": "^2.0.3",
|
||||
"vite": "^5.0.0 || ^6.0.0"
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
|
||||
},
|
||||
"bin": {
|
||||
"vite-node": "vite-node.mjs"
|
||||
@@ -4229,9 +4314,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/fdir": {
|
||||
"version": "6.4.4",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
|
||||
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
|
||||
"version": "6.4.6",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
|
||||
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
@@ -4257,32 +4342,34 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.4.tgz",
|
||||
"integrity": "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==",
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
|
||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/expect": "3.1.4",
|
||||
"@vitest/mocker": "3.1.4",
|
||||
"@vitest/pretty-format": "^3.1.4",
|
||||
"@vitest/runner": "3.1.4",
|
||||
"@vitest/snapshot": "3.1.4",
|
||||
"@vitest/spy": "3.1.4",
|
||||
"@vitest/utils": "3.1.4",
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/expect": "3.2.4",
|
||||
"@vitest/mocker": "3.2.4",
|
||||
"@vitest/pretty-format": "^3.2.4",
|
||||
"@vitest/runner": "3.2.4",
|
||||
"@vitest/snapshot": "3.2.4",
|
||||
"@vitest/spy": "3.2.4",
|
||||
"@vitest/utils": "3.2.4",
|
||||
"chai": "^5.2.0",
|
||||
"debug": "^4.4.0",
|
||||
"debug": "^4.4.1",
|
||||
"expect-type": "^1.2.1",
|
||||
"magic-string": "^0.30.17",
|
||||
"pathe": "^2.0.3",
|
||||
"picomatch": "^4.0.2",
|
||||
"std-env": "^3.9.0",
|
||||
"tinybench": "^2.9.0",
|
||||
"tinyexec": "^0.3.2",
|
||||
"tinyglobby": "^0.2.13",
|
||||
"tinypool": "^1.0.2",
|
||||
"tinyglobby": "^0.2.14",
|
||||
"tinypool": "^1.1.1",
|
||||
"tinyrainbow": "^2.0.0",
|
||||
"vite": "^5.0.0 || ^6.0.0",
|
||||
"vite-node": "3.1.4",
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
|
||||
"vite-node": "3.2.4",
|
||||
"why-is-node-running": "^2.3.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -4298,8 +4385,8 @@
|
||||
"@edge-runtime/vm": "*",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
||||
"@vitest/browser": "3.1.4",
|
||||
"@vitest/ui": "3.1.4",
|
||||
"@vitest/browser": "3.2.4",
|
||||
"@vitest/ui": "3.2.4",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*"
|
||||
},
|
||||
@@ -4340,6 +4427,19 @@
|
||||
"vitest": ">=2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/picomatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"type": "module",
|
||||
"exports": "./dist/index.js",
|
||||
"bin": {
|
||||
"immich": "dist/index.js"
|
||||
"immich": "./bin/immich"
|
||||
},
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"keywords": [
|
||||
@@ -21,7 +21,7 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/micromatch": "^4.0.9",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^22.15.32",
|
||||
"@types/node": "^22.15.33",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
"byte-size": "^9.0.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
@@ -36,7 +36,7 @@
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
"typescript": "^5.3.3",
|
||||
"typescript-eslint": "^8.28.0",
|
||||
"vite": "^6.0.0",
|
||||
"vite": "^7.0.0",
|
||||
"vite-tsconfig-paths": "^5.0.0",
|
||||
"vitest": "^3.0.0",
|
||||
"vitest-fetch-mock": "^0.4.0",
|
||||
@@ -69,6 +69,6 @@
|
||||
"micromatch": "^4.0.8"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.16.0"
|
||||
"node": "22.17.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/valkey/valkey:8-bookworm@sha256:fec42f399876eb6faf9e008570597741c87ff7662a54185593e74b09ce83d177
|
||||
image: docker.io/valkey/valkey:8-bookworm@sha256:facc1d2c3462975c34e10fccb167bfa92b0e0dbd992fc282c29a61c3243afb11
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/valkey/valkey:8-bookworm@sha256:fec42f399876eb6faf9e008570597741c87ff7662a54185593e74b09ce83d177
|
||||
image: docker.io/valkey/valkey:8-bookworm@sha256:facc1d2c3462975c34e10fccb167bfa92b0e0dbd992fc282c29a61c3243afb11
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
restart: always
|
||||
@@ -83,7 +83,7 @@ services:
|
||||
container_name: immich_prometheus
|
||||
ports:
|
||||
- 9090:9090
|
||||
image: prom/prometheus@sha256:9abc6cf6aea7710d163dbb28d8eeb7dc5baef01e38fa4cd146a406dd9f07f70d
|
||||
image: prom/prometheus@sha256:63805ebb8d2b3920190daf1cb14a60871b16fd38bed42b857a3182bc621f4996
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
- prometheus-data:/prometheus
|
||||
|
||||
@@ -49,7 +49,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/valkey/valkey:8-bookworm@sha256:fec42f399876eb6faf9e008570597741c87ff7662a54185593e74b09ce83d177
|
||||
image: docker.io/valkey/valkey:8-bookworm@sha256:facc1d2c3462975c34e10fccb167bfa92b0e0dbd992fc282c29a61c3243afb11
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
restart: always
|
||||
|
||||
@@ -1 +1 @@
|
||||
22.16.0
|
||||
22.17.0
|
||||
|
||||
@@ -150,12 +150,10 @@ for more info read the [release notes](https://github.com/immich-app/immich/rele
|
||||
- Preview images (small thumbnails and large previews) for each asset and thumbnails for recognized faces.
|
||||
- Stored in `UPLOAD_LOCATION/thumbs/<userID>`.
|
||||
- **Encoded Assets:**
|
||||
|
||||
- Videos that have been re-encoded from the original for wider compatibility. The original is not removed.
|
||||
- Stored in `UPLOAD_LOCATION/encoded-video/<userID>`.
|
||||
|
||||
- **Postgres**
|
||||
|
||||
- The Immich database containing all the information to allow the system to function properly.
|
||||
**Note:** This folder will only appear to users who have made the changes mentioned in [v1.102.0](https://github.com/immich-app/immich/discussions/8930) (an optional, non-mandatory change) or who started with this version.
|
||||
- Stored in `DB_DATA_LOCATION`.
|
||||
@@ -201,7 +199,6 @@ When you turn off the storage template engine, it will leave the assets in `UPLO
|
||||
- Temporarily located in `UPLOAD_LOCATION/upload/<userID>`.
|
||||
- Transferred to `UPLOAD_LOCATION/library/<userID>` upon successful upload.
|
||||
- **Postgres**
|
||||
|
||||
- The Immich database containing all the information to allow the system to function properly.
|
||||
**Note:** This folder will only appear to users who have made the changes mentioned in [v1.102.0](https://github.com/immich-app/immich/discussions/8930) (an optional, non-mandatory change) or who started with this version.
|
||||
- Stored in `DB_DATA_LOCATION`.
|
||||
|
||||
BIN
docs/docs/administration/img/admin-nightly-tasks.webp
Normal file
BIN
docs/docs/administration/img/admin-nightly-tasks.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
@@ -46,6 +46,12 @@ services:
|
||||
|
||||
When a new asset is uploaded it kicks off a series of jobs, which include metadata extraction, thumbnail generation, machine learning tasks, and storage template migration, if enabled. To view the status of a job navigate to the Administration -> Jobs page.
|
||||
|
||||
Additionally, some jobs run on a schedule, which is every night at midnight. This schedule, with the exception of [External Libraries](/docs/features/libraries) scanning, cannot be changed.
|
||||
|
||||
<img src={require('./img/admin-jobs.webp').default} width="60%" title="Admin jobs" />
|
||||
|
||||
Additionally, some jobs (such as memories generation) run on a schedule, which is every night at midnight by default. To change when they run or enable/disable a job navigate to System Settings -> [Nightly Tasks Settings](https://my.immich.app/admin/system-settings?isOpen=nightly-tasks).
|
||||
|
||||
<img src={require('./img/admin-nightly-tasks.webp').default} width="60%" title="Admin nightly tasks" />
|
||||
|
||||
:::note
|
||||
Some jobs ([External Libraries](/docs/features/libraries) scanning, Database Dump) are configured in their own sections in System Settings.
|
||||
:::
|
||||
|
||||
@@ -20,7 +20,6 @@ Immich supports 3rd party authentication via [OpenID Connect][oidc] (OIDC), an i
|
||||
Before enabling OAuth in Immich, a new client application needs to be configured in the 3rd-party authentication server. While the specifics of this setup vary from provider to provider, the general approach should be the same.
|
||||
|
||||
1. Create a new (Client) Application
|
||||
|
||||
1. The **Provider** type should be `OpenID Connect` or `OAuth2`
|
||||
2. The **Client type** should be `Confidential`
|
||||
3. The **Application** type should be `Web`
|
||||
@@ -29,7 +28,6 @@ Before enabling OAuth in Immich, a new client application needs to be configured
|
||||
2. Configure Redirect URIs/Origins
|
||||
|
||||
The **Sign-in redirect URIs** should include:
|
||||
|
||||
- `app.immich:///oauth-callback` - for logging in with OAuth from the [Mobile App](/docs/features/mobile-app.mdx)
|
||||
- `http://DOMAIN:PORT/auth/login` - for logging in with OAuth from the Web Client
|
||||
- `http://DOMAIN:PORT/user-settings` - for manually linking OAuth in the Web Client
|
||||
@@ -37,21 +35,17 @@ Before enabling OAuth in Immich, a new client application needs to be configured
|
||||
Redirect URIs should contain all the domains you will be using to access Immich. Some examples include:
|
||||
|
||||
Mobile
|
||||
|
||||
- `app.immich:///oauth-callback` (You **MUST** include this for iOS and Android mobile apps to work properly)
|
||||
|
||||
Localhost
|
||||
|
||||
- `http://localhost:2283/auth/login`
|
||||
- `http://localhost:2283/user-settings`
|
||||
|
||||
Local IP
|
||||
|
||||
- `http://192.168.0.200:2283/auth/login`
|
||||
- `http://192.168.0.200:2283/user-settings`
|
||||
|
||||
Hostname
|
||||
|
||||
- `https://immich.example.com/auth/login`
|
||||
- `https://immich.example.com/user-settings`
|
||||
|
||||
@@ -68,6 +62,7 @@ Once you have a new OAuth client application configured, Immich can be configure
|
||||
| Scope | string | openid email profile | Full list of scopes to send with the request (space delimited) |
|
||||
| Signing Algorithm | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
|
||||
| Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label**¹** |
|
||||
| Role Claim | string | immich_role | Claim mapping for the user's role. (should return "user" or "admin")**¹** |
|
||||
| Storage Quota Claim | string | immich_quota | Claim mapping for the user's storage**¹** |
|
||||
| Default Storage Quota (GiB) | number | 0 | Default quota for user without storage quota claim (Enter 0 for unlimited quota) |
|
||||
| Button Text | string | Login with OAuth | Text for the OAuth button on the web |
|
||||
|
||||
@@ -199,13 +199,11 @@ To use your SSH key for commit signing, see the [GitHub guide on SSH commit sign
|
||||
When the Dev Container starts, it automatically:
|
||||
|
||||
1. **Runs post-create script** (`container-server-post-create.sh`):
|
||||
|
||||
- Adjusts file permissions for the `node` user
|
||||
- Installs dependencies: `npm install` in all packages
|
||||
- Builds TypeScript SDK: `npm run build` in `open-api/typescript-sdk`
|
||||
|
||||
2. **Starts development servers** via VS Code tasks:
|
||||
|
||||
- `Immich API Server (Nest)` - API server with hot-reloading on port 2283
|
||||
- `Immich Web Server (Vite)` - Web frontend with hot-reloading on port 3000
|
||||
- Both servers watch for file changes and recompile automatically
|
||||
@@ -335,14 +333,12 @@ make install-all # Install all dependencies
|
||||
The Dev Container is pre-configured for debugging:
|
||||
|
||||
1. **API Server Debugging**:
|
||||
|
||||
- Set breakpoints in VS Code
|
||||
- Press `F5` or use "Run and Debug" panel
|
||||
- Select "Attach to Server" configuration
|
||||
- Debug port: 9231
|
||||
|
||||
2. **Worker Debugging**:
|
||||
|
||||
- Use "Attach to Workers" configuration
|
||||
- Debug port: 9230
|
||||
|
||||
@@ -428,7 +424,6 @@ While the Dev Container focuses on server and web development, you can connect m
|
||||
```
|
||||
|
||||
2. **Configure mobile app**:
|
||||
|
||||
- Server URL: `http://YOUR_IP:2283/api`
|
||||
- Ensure firewall allows port 2283
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Folder view provides an additional view besides the timeline that is similar to a file explorer. It allows you to navigate through the folders and files in the library. This feature is handy for a highly curated and customized external library or a nicely configured storage template.
|
||||
|
||||
You can enable this feature under [`Account Settings > Features > Folder View`](https://my.immich.app/user-settings?isOpen=feature+folders)
|
||||
You can enable this feature under [`Account Settings > Features > Folders`](https://my.immich.app/user-settings?isOpen=feature+folders)
|
||||
|
||||
## Enable folder view
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ Internally, Immich uses the [glob](https://www.npmjs.com/package/glob) package t
|
||||
|
||||
### Automatic watching (EXPERIMENTAL)
|
||||
|
||||
This feature - currently hidden in the config file - is considered experimental and for advanced users only. If enabled, it will allow automatic watching of the filesystem which means new assets are automatically imported to Immich without needing to rescan.
|
||||
This feature is considered experimental and for advanced users only. If enabled, it will allow automatic watching of the filesystem which means new assets are automatically imported to Immich without needing to rescan.
|
||||
|
||||
If your photos are on a network drive, automatic file watching likely won't work. In that case, you will have to rely on a periodic library refresh to pull in your changes.
|
||||
|
||||
@@ -112,7 +112,7 @@ _Remember to run `docker compose up -d` to register the changes. Make sure you c
|
||||
|
||||
These actions must be performed by the Immich administrator.
|
||||
|
||||
- Click on your avatar on the upper right corner
|
||||
- Click on your avatar in the upper right corner
|
||||
- Click on Administration -> External Libraries
|
||||
- Click on Create an external library…
|
||||
- Select which user owns the library, this can not be changed later
|
||||
@@ -159,9 +159,7 @@ Within seconds, the assets from the old-pics and videos folders should show up i
|
||||
|
||||
Folder view provides an additional view besides the timeline that is similar to a file explorer. It allows you to navigate through the folders and files in the library. This feature is handy for a highly curated and customized external library or a nicely configured storage template.
|
||||
|
||||
You can enable this feature under [`Account Settings > Features > Folder View`](https://my.immich.app/user-settings?isOpen=feature+folders)
|
||||
|
||||
The UI is currently only available for the web; mobile will come in a subsequent release.
|
||||
You can enable this feature under [`Account Settings > Features > Folders`](https://my.immich.app/user-settings?isOpen=feature+folders)
|
||||
|
||||
<img src={require('./img/folder-view-1.webp').default} width="100%" title='Folder-view' />
|
||||
|
||||
@@ -171,7 +169,7 @@ The UI is currently only available for the web; mobile will come in a subsequent
|
||||
Only an admin can do this.
|
||||
:::
|
||||
|
||||
You can define a custom interval for the trigger external library rescan under Administration -> Settings -> Library.
|
||||
You can define a custom interval for the trigger external library rescan under Administration -> Settings -> External 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.webp').default} width="75%" title='Set custom scan interval for external library' />
|
||||
|
||||
@@ -16,7 +16,7 @@ For the full list, refer to the [Immich source code](https://github.com/immich-a
|
||||
| `HEIC` | `.heic` | :white_check_mark: | |
|
||||
| `HEIF` | `.heif` | :white_check_mark: | |
|
||||
| `JPEG 2000` | `.jp2` | :white_check_mark: | |
|
||||
| `JPEG` | `.webp` `.jpg` `.jpe` `.insp` | :white_check_mark: | |
|
||||
| `JPEG` | `.jpeg` `.jpg` `.jpe` `.insp` | :white_check_mark: | |
|
||||
| `JPEG XL` | `.jxl` | :white_check_mark: | |
|
||||
| `PNG` | `.png` | :white_check_mark: | |
|
||||
| `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop |
|
||||
|
||||
@@ -41,7 +41,7 @@ In the Immich web UI:
|
||||
- Click Add path
|
||||
<img src={require('./img/add-path-button.webp').default} width="50%" title="Add Path button" />
|
||||
|
||||
- Enter **/usr/src/app/external** as the path and click Add
|
||||
- Enter **/home/user/photos1** as the path and click Add
|
||||
<img src={require('./img/add-path-field.webp').default} width="50%" title="Add Path field" />
|
||||
|
||||
- Save the new path
|
||||
|
||||
@@ -72,22 +72,25 @@ Information on the current workers can be found [here](/docs/administration/jobs
|
||||
|
||||
## Database
|
||||
|
||||
| Variable | Description | Default | Containers |
|
||||
| :---------------------------------- | :--------------------------------------------------------------------------- | :--------: | :----------------------------- |
|
||||
| `DB_URL` | Database URL | | server |
|
||||
| `DB_HOSTNAME` | Database host | `database` | server |
|
||||
| `DB_PORT` | Database port | `5432` | server |
|
||||
| `DB_USERNAME` | Database user | `postgres` | server, database<sup>\*1</sup> |
|
||||
| `DB_PASSWORD` | Database password | `postgres` | server, database<sup>\*1</sup> |
|
||||
| `DB_DATABASE_NAME` | Database name | `immich` | server, database<sup>\*1</sup> |
|
||||
| `DB_SSL_MODE` | Database SSL mode | | server |
|
||||
| `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database vector extension (one of [`vectorchord`, `pgvector`, `pgvecto.rs`]) | | server |
|
||||
| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server |
|
||||
| Variable | Description | Default | Containers |
|
||||
| :---------------------------------- | :------------------------------------------------------------------------------------- | :--------: | :----------------------------- |
|
||||
| `DB_URL` | Database URL | | server |
|
||||
| `DB_HOSTNAME` | Database host | `database` | server |
|
||||
| `DB_PORT` | Database port | `5432` | server |
|
||||
| `DB_USERNAME` | Database user | `postgres` | server, database<sup>\*1</sup> |
|
||||
| `DB_PASSWORD` | Database password | `postgres` | server, database<sup>\*1</sup> |
|
||||
| `DB_DATABASE_NAME` | Database name | `immich` | server, database<sup>\*1</sup> |
|
||||
| `DB_SSL_MODE` | Database SSL mode | | server |
|
||||
| `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database vector extension (one of [`vectorchord`, `pgvector`, `pgvecto.rs`]) | | server |
|
||||
| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server |
|
||||
| `DB_STORAGE_TYPE` | Optimize concurrent IO on SSDs or sequential IO on HDDs ([`SSD`, `HDD`])<sup>\*3</sup> | `SSD` | server |
|
||||
|
||||
\*1: The values of `DB_USERNAME`, `DB_PASSWORD`, and `DB_DATABASE_NAME` are passed to the Postgres container as the variables `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB` in `docker-compose.yml`.
|
||||
|
||||
\*2: If not provided, the appropriate extension to use is auto-detected at startup by introspecting the database. When multiple extensions are installed, the order of preference is VectorChord, pgvecto.rs, pgvector.
|
||||
|
||||
\*3: Uses either [`postgresql.ssd.conf`](https://github.com/immich-app/base-images/blob/main/postgres/postgresql.ssd.conf) or [`postgresql.hdd.conf`](https://github.com/immich-app/base-images/blob/main/postgres/postgresql.hdd.conf) which mainly controls the Postgres `effective_io_concurrency` setting to allow for concurrenct IO on SSDs and sequential IO on HDDs.
|
||||
|
||||
:::info
|
||||
|
||||
All `DB_` variables must be provided to all Immich workers, including `api` and `microservices`.
|
||||
|
||||
@@ -75,7 +75,6 @@ alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich"
|
||||
5. Click "**Save Changes**", you will be prompted to edit stack UI labels, just leave this blank and click "**Ok**"
|
||||
6. Select the cog ⚙️ next to Immich, click "**Edit Stack**", then click "**Env File**"
|
||||
7. Paste the entire contents of the [Immich example.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file into the Unraid editor, then **before saving** edit the following:
|
||||
|
||||
- `UPLOAD_LOCATION`: Create a folder in your Images Unraid share and place the **absolute** location here > For example my _"images"_ share has a folder within it called _"immich"_. If I browse to this directory in the terminal and type `pwd` the output is `/mnt/user/images/immich`. This is the exact value I need to enter as my `UPLOAD_LOCATION`
|
||||
- `DB_DATA_LOCATION`: Change this to use an Unraid share (preferably a cache pool, e.g. `/mnt/user/appdata/postgresql/data`). This uses the `appdata` share. Do also create the `postgresql` folder, by running `mkdir /mnt/user/{share_location}/postgresql/data`. If left at default it will try to use Unraid's `/boot/config/plugins/compose.manager/projects/[stack_name]/postgres` folder which it doesn't have permissions to, resulting in this container continuously restarting.
|
||||
|
||||
|
||||
1533
docs/package-lock.json
generated
1533
docs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -16,8 +16,9 @@
|
||||
"write-heading-ids": "docusaurus write-heading-ids"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "~3.7.0",
|
||||
"@docusaurus/preset-classic": "~3.7.0",
|
||||
"@docusaurus/core": "~3.8.0",
|
||||
"@docusaurus/preset-classic": "~3.8.0",
|
||||
"@docusaurus/theme-common": "~3.8.0",
|
||||
"@mdi/js": "^7.3.67",
|
||||
"@mdi/react": "^1.6.1",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
@@ -26,6 +27,7 @@
|
||||
"clsx": "^2.0.0",
|
||||
"docusaurus-lunr-search": "^3.3.2",
|
||||
"docusaurus-preset-openapi": "^0.7.5",
|
||||
"lunr": "^2.3.9",
|
||||
"postcss": "^8.4.25",
|
||||
"prism-react-renderer": "^2.3.1",
|
||||
"raw-loader": "^4.0.2",
|
||||
@@ -35,7 +37,7 @@
|
||||
"url": "^0.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "~3.7.0",
|
||||
"@docusaurus/module-type-aliases": "~3.8.0",
|
||||
"@docusaurus/tsconfig": "^3.7.0",
|
||||
"@docusaurus/types": "^3.7.0",
|
||||
"prettier": "^3.2.4",
|
||||
@@ -57,6 +59,6 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.16.0"
|
||||
"node": "22.17.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,12 @@ const guides: CommunityGuidesProps[] = [
|
||||
description: 'Access Immich with an end-to-end encrypted connection.',
|
||||
url: 'https://meshnet.nordvpn.com/how-to/remote-files-media-access/immich-remote-access',
|
||||
},
|
||||
{
|
||||
title: 'Trust Self Signed Certificates with Immich - OAuth Setup',
|
||||
description:
|
||||
'Set up Certificate Authority trust with Immich, and your private OAuth2/OpenID service, while using a private CA for HTTPS commication.',
|
||||
url: 'https://github.com/immich-app/immich/discussions/18614',
|
||||
},
|
||||
];
|
||||
|
||||
function CommunityGuide({ title, description, url }: CommunityGuidesProps): JSX.Element {
|
||||
|
||||
@@ -85,6 +85,7 @@ import React from 'react';
|
||||
import { Item, Timeline } from '../components/timeline';
|
||||
|
||||
const releases = {
|
||||
'v1.135.0': new Date(2025, 5, 18),
|
||||
'v1.133.0': new Date(2025, 4, 21),
|
||||
'v1.130.0': new Date(2025, 2, 25),
|
||||
'v1.127.0': new Date(2025, 1, 26),
|
||||
@@ -196,14 +197,6 @@ const roadmap: Item[] = [
|
||||
description: 'Automate tasks with workflows',
|
||||
getDateLabel: () => 'Planned for 2025',
|
||||
},
|
||||
{
|
||||
done: false,
|
||||
icon: mdiTableKey,
|
||||
iconColor: 'gray',
|
||||
title: 'Fine grained access controls',
|
||||
description: 'Granular access controls for users and api keys',
|
||||
getDateLabel: () => 'Planned for 2025',
|
||||
},
|
||||
{
|
||||
done: false,
|
||||
icon: mdiImageEdit,
|
||||
@@ -239,12 +232,26 @@ const roadmap: Item[] = [
|
||||
];
|
||||
|
||||
const milestones: Item[] = [
|
||||
{
|
||||
icon: mdiStar,
|
||||
iconColor: 'gold',
|
||||
title: '70,000 Stars',
|
||||
description: 'Reached 70K Stars on GitHub!',
|
||||
getDateLabel: withLanguage(new Date(2025, 6, 9)),
|
||||
},
|
||||
withRelease({
|
||||
icon: mdiTableKey,
|
||||
iconColor: 'gray',
|
||||
title: 'Fine grained access controls',
|
||||
description: 'Granular access controls for api keys',
|
||||
release: 'v1.135.0',
|
||||
}),
|
||||
withRelease({
|
||||
icon: mdiCast,
|
||||
iconColor: 'aqua',
|
||||
title: 'Google Cast (web)',
|
||||
title: 'Google Cast (web and mobile)',
|
||||
description: 'Cast assets to Google Cast/Chromecast compatible devices',
|
||||
release: 'v1.133.0',
|
||||
release: 'v1.135.0',
|
||||
}),
|
||||
withRelease({
|
||||
icon: mdiLockOutline,
|
||||
|
||||
5
docs/static/_redirects
vendored
5
docs/static/_redirects
vendored
@@ -1,4 +1,5 @@
|
||||
/docs /docs/overview/introduction 307
|
||||
/docs /docs/overview/welcome 307
|
||||
/docs/ /docs/overview/welcome 307
|
||||
/docs/mobile-app-beta-program /docs/features/mobile-app 307
|
||||
/docs/contribution-guidelines /docs/overview/support-the-project#contributing 307
|
||||
/docs/install /docs/install/docker-compose 307
|
||||
@@ -30,4 +31,4 @@
|
||||
/docs/guides/api-album-sync /docs/community-projects 307
|
||||
/docs/guides/remove-offline-files /docs/community-projects 307
|
||||
/milestones /roadmap 307
|
||||
/docs/overview/introduction /docs/overview/welcome 307
|
||||
/docs/overview/introduction /docs/overview/welcome 307
|
||||
|
||||
@@ -1 +1 @@
|
||||
22.16.0
|
||||
22.17.0
|
||||
|
||||
@@ -36,7 +36,7 @@ services:
|
||||
- 2285:2285
|
||||
|
||||
redis:
|
||||
image: redis:6.2-alpine@sha256:3211c33a618c457e5d241922c975dbc4f446d0bdb2dc75694f5573ef8e2d01fa
|
||||
image: redis:6.2-alpine@sha256:03fd052257735b41cd19f3d8ae9782926bf9b704fb6a9dc5e29f9ccfbe8827f0
|
||||
|
||||
database:
|
||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:3aef84a0a4fabbda17ef115c3019ba0c914ec73e9f6e59203674322d858b8eea
|
||||
|
||||
930
e2e/package-lock.json
generated
930
e2e/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -24,8 +24,9 @@
|
||||
"@immich/cli": "file:../cli",
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@socket.io/component-emitter": "^3.1.2",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^22.15.32",
|
||||
"@types/node": "^22.15.33",
|
||||
"@types/oidc-provider": "^9.0.0",
|
||||
"@types/pg": "^8.15.1",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
@@ -44,7 +45,7 @@
|
||||
"pngjs": "^7.0.0",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
"sharp": "^0.33.5",
|
||||
"sharp": "^0.34.0",
|
||||
"socket.io-client": "^4.7.4",
|
||||
"supertest": "^7.0.0",
|
||||
"typescript": "^5.3.3",
|
||||
@@ -53,6 +54,6 @@
|
||||
"vitest": "^3.0.0"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.16.0"
|
||||
"node": "22.17.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
ReactionType,
|
||||
createActivity as create,
|
||||
createAlbum,
|
||||
removeAssetFromAlbum,
|
||||
} from '@immich/sdk';
|
||||
import { createUserDto, uuidDto } from 'src/fixtures';
|
||||
import { errorDto } from 'src/responses';
|
||||
@@ -342,5 +343,36 @@ describe('/activities', () => {
|
||||
|
||||
expect(status).toBe(204);
|
||||
});
|
||||
|
||||
it('should return empty list when asset is removed', async () => {
|
||||
const album3 = await createAlbum(
|
||||
{
|
||||
createAlbumDto: {
|
||||
albumName: 'Album 3',
|
||||
assetIds: [asset.id],
|
||||
},
|
||||
},
|
||||
{ headers: asBearerAuth(admin.accessToken) },
|
||||
);
|
||||
|
||||
await createActivity({ albumId: album3.id, assetId: asset.id, type: ReactionType.Like });
|
||||
|
||||
await removeAssetFromAlbum(
|
||||
{
|
||||
id: album3.id,
|
||||
bulkIdsDto: {
|
||||
ids: [asset.id],
|
||||
},
|
||||
},
|
||||
{ headers: asBearerAuth(admin.accessToken) },
|
||||
);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.get('/activities')
|
||||
.query({ albumId: album.id })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toEqual(200);
|
||||
expect(body).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('/api-keys', () => {
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await utils.resetDatabase(['api_keys']);
|
||||
await utils.resetDatabase(['api_key']);
|
||||
});
|
||||
|
||||
describe('POST /api-keys', () => {
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
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';
|
||||
|
||||
const { email, password } = signupDto.admin;
|
||||
|
||||
describe(`/auth/admin-sign-up`, () => {
|
||||
beforeEach(async () => {
|
||||
await utils.resetDatabase();
|
||||
});
|
||||
|
||||
describe('POST /auth/admin-sign-up', () => {
|
||||
it(`should sign up the admin`, async () => {
|
||||
const { status, body } = await request(app).post('/auth/admin-sign-up').send(signupDto.admin);
|
||||
expect(status).toBe(201);
|
||||
expect(body).toEqual(signupResponseDto.admin);
|
||||
});
|
||||
|
||||
it('should not allow a second admin to sign up', async () => {
|
||||
await signUpAdmin({ signUpDto: signupDto.admin });
|
||||
|
||||
const { status, body } = await request(app).post('/auth/admin-sign-up').send(signupDto.admin);
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.alreadyHasAdmin);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/auth/*', () => {
|
||||
let admin: LoginResponseDto;
|
||||
|
||||
beforeEach(async () => {
|
||||
await utils.resetDatabase();
|
||||
await signUpAdmin({ signUpDto: signupDto.admin });
|
||||
admin = await login({ loginCredentialDto: loginDto.admin });
|
||||
});
|
||||
|
||||
describe(`POST /auth/login`, () => {
|
||||
it('should reject an incorrect password', async () => {
|
||||
const { status, body } = await request(app).post('/auth/login').send({ email, password: 'incorrect' });
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.incorrectLogin);
|
||||
});
|
||||
|
||||
it('should accept a correct password', async () => {
|
||||
const { status, body, headers } = await request(app).post('/auth/login').send({ email, password });
|
||||
expect(status).toBe(201);
|
||||
expect(body).toEqual(loginResponseDto.admin);
|
||||
|
||||
const token = body.accessToken;
|
||||
expect(token).toBeDefined();
|
||||
|
||||
const cookies = headers['set-cookie'];
|
||||
expect(cookies).toHaveLength(3);
|
||||
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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /auth/validateToken', () => {
|
||||
it('should reject an invalid token', async () => {
|
||||
const { status, body } = await request(app).post(`/auth/validateToken`).set('Authorization', 'Bearer 123');
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.invalidToken);
|
||||
});
|
||||
|
||||
it('should accept a valid token', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post(`/auth/validateToken`)
|
||||
.send({})
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({ authStatus: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /auth/change-password', () => {
|
||||
it('should require the current password', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post(`/auth/change-password`)
|
||||
.send({ password: 'wrong-password', newPassword: 'Password1234' })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.wrongPassword);
|
||||
});
|
||||
|
||||
it('should change the password', async () => {
|
||||
const { status } = await request(app)
|
||||
.post(`/auth/change-password`)
|
||||
.send({ password, newPassword: 'Password1234' })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
|
||||
await login({
|
||||
loginCredentialDto: {
|
||||
email: 'admin@immich.cloud',
|
||||
password: 'Password1234',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /auth/logout', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).post(`/auth/logout`);
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should logout the user', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post(`/auth/logout`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
successful: true,
|
||||
redirectUri: '/auth/login?autoLaunch=0',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -227,6 +227,21 @@ describe(`/oauth`, () => {
|
||||
expect(user.storageLabel).toBe('user-username');
|
||||
});
|
||||
|
||||
it('should set the admin status from a role claim', async () => {
|
||||
const callbackParams = await loginWithOAuth(OAuthUser.WITH_ROLE);
|
||||
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
|
||||
expect(status).toBe(201);
|
||||
expect(body).toMatchObject({
|
||||
accessToken: expect.any(String),
|
||||
userId: expect.any(String),
|
||||
userEmail: 'oauth-with-role@immich.app',
|
||||
isAdmin: true,
|
||||
});
|
||||
|
||||
const user = await getMyUser({ headers: asBearerAuth(body.accessToken) });
|
||||
expect(user.isAdmin).toBe(true);
|
||||
});
|
||||
|
||||
it('should work with RS256 signed tokens', async () => {
|
||||
await setupOAuth(admin.accessToken, {
|
||||
enabled: true,
|
||||
|
||||
@@ -117,6 +117,13 @@ describe('/shared-links', () => {
|
||||
const resp = await request(shareUrl).get(`/${linkWithAssets.key}`);
|
||||
expect(resp.status).toBe(200);
|
||||
expect(resp.header['content-type']).toContain('text/html');
|
||||
expect(resp.text).toContain(`<meta property="og:image" content="http://127.0.0.1:2285`);
|
||||
});
|
||||
|
||||
it('should fall back to my.immich.app og:image meta tag for shared asset if Host header is not present', async () => {
|
||||
const resp = await request(shareUrl).get(`/${linkWithAssets.key}`).set('Host', '');
|
||||
expect(resp.status).toBe(200);
|
||||
expect(resp.header['content-type']).toContain('text/html');
|
||||
expect(resp.text).toContain(`<meta property="og:image" content="https://my.immich.app`);
|
||||
});
|
||||
|
||||
|
||||
@@ -15,12 +15,6 @@ describe('/system-config', () => {
|
||||
});
|
||||
|
||||
describe('PUT /system-config', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).put('/system-config');
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should always return the new config', async () => {
|
||||
const config = await getSystemConfig(admin.accessToken);
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ describe('/tags', () => {
|
||||
beforeEach(async () => {
|
||||
// tagging assets eventually triggers metadata extraction which can impact other tests
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.resetDatabase(['tags']);
|
||||
await utils.resetDatabase(['tag']);
|
||||
});
|
||||
|
||||
describe('POST /tags', () => {
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
import {
|
||||
AssetMediaResponseDto,
|
||||
AssetVisibility,
|
||||
LoginResponseDto,
|
||||
SharedLinkType,
|
||||
TimeBucketAssetResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { DateTime } from 'luxon';
|
||||
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';
|
||||
|
||||
// TODO this should probably be a test util function
|
||||
const today = DateTime.fromObject({
|
||||
year: 2023,
|
||||
month: 11,
|
||||
day: 3,
|
||||
}) as DateTime<true>;
|
||||
const yesterday = today.minus({ days: 1 });
|
||||
|
||||
describe('/timeline', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let user: LoginResponseDto;
|
||||
let timeBucketUser: LoginResponseDto;
|
||||
|
||||
let user1Assets: AssetMediaResponseDto[];
|
||||
let user2Assets: AssetMediaResponseDto[];
|
||||
|
||||
beforeAll(async () => {
|
||||
await utils.resetDatabase();
|
||||
admin = await utils.adminSetup({ onboarding: false });
|
||||
[user, timeBucketUser] = await Promise.all([
|
||||
utils.userSetup(admin.accessToken, createUserDto.create('1')),
|
||||
utils.userSetup(admin.accessToken, createUserDto.create('time-bucket')),
|
||||
]);
|
||||
|
||||
user1Assets = await Promise.all([
|
||||
utils.createAsset(user.accessToken),
|
||||
utils.createAsset(user.accessToken),
|
||||
utils.createAsset(user.accessToken, {
|
||||
isFavorite: true,
|
||||
fileCreatedAt: yesterday.toISO(),
|
||||
fileModifiedAt: yesterday.toISO(),
|
||||
assetData: { filename: 'example.mp4' },
|
||||
}),
|
||||
utils.createAsset(user.accessToken),
|
||||
utils.createAsset(user.accessToken),
|
||||
]);
|
||||
|
||||
user2Assets = await Promise.all([
|
||||
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-01-01').toISOString() }),
|
||||
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-10').toISOString() }),
|
||||
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-11').toISOString() }),
|
||||
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-11').toISOString() }),
|
||||
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-12').toISOString() }),
|
||||
]);
|
||||
|
||||
await utils.deleteAssets(timeBucketUser.accessToken, [user2Assets[4].id]);
|
||||
});
|
||||
|
||||
describe('GET /timeline/buckets', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get('/timeline/buckets');
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should get time buckets by month', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/timeline/buckets')
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ count: 3, timeBucket: '1970-02-01' },
|
||||
{ count: 1, timeBucket: '1970-01-01' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not allow access for unrelated shared links', async () => {
|
||||
const sharedLink = await utils.createSharedLink(user.accessToken, {
|
||||
type: SharedLinkType.Individual,
|
||||
assetIds: user1Assets.map(({ id }) => id),
|
||||
});
|
||||
|
||||
const { status, body } = await request(app).get('/timeline/buckets').query({ key: sharedLink.key });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.noPermission);
|
||||
});
|
||||
|
||||
it('should return error if time bucket is requested with partners asset and archived', async () => {
|
||||
const req1 = await request(app)
|
||||
.get('/timeline/buckets')
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||
.query({ withPartners: true, visibility: AssetVisibility.Archive });
|
||||
|
||||
expect(req1.status).toBe(400);
|
||||
expect(req1.body).toEqual(errorDto.badRequest());
|
||||
|
||||
const req2 = await request(app)
|
||||
.get('/timeline/buckets')
|
||||
.set('Authorization', `Bearer ${user.accessToken}`)
|
||||
.query({ withPartners: true, visibility: undefined });
|
||||
|
||||
expect(req2.status).toBe(400);
|
||||
expect(req2.body).toEqual(errorDto.badRequest());
|
||||
});
|
||||
|
||||
it('should return error if time bucket is requested with partners asset and favorite', async () => {
|
||||
const req1 = await request(app)
|
||||
.get('/timeline/buckets')
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||
.query({ withPartners: true, isFavorite: true });
|
||||
|
||||
expect(req1.status).toBe(400);
|
||||
expect(req1.body).toEqual(errorDto.badRequest());
|
||||
|
||||
const req2 = await request(app)
|
||||
.get('/timeline/buckets')
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||
.query({ withPartners: true, isFavorite: false });
|
||||
|
||||
expect(req2.status).toBe(400);
|
||||
expect(req2.body).toEqual(errorDto.badRequest());
|
||||
});
|
||||
|
||||
it('should return error if time bucket is requested with partners asset and trash', async () => {
|
||||
const req = await request(app)
|
||||
.get('/timeline/buckets')
|
||||
.set('Authorization', `Bearer ${user.accessToken}`)
|
||||
.query({ withPartners: true, isTrashed: true });
|
||||
|
||||
expect(req.status).toBe(400);
|
||||
expect(req.body).toEqual(errorDto.badRequest());
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /timeline/bucket', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get('/timeline/bucket').query({
|
||||
timeBucket: '1900-01-01',
|
||||
});
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should handle 5 digit years', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/timeline/bucket')
|
||||
.query({ timeBucket: '012345-01-01' })
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
city: [],
|
||||
country: [],
|
||||
duration: [],
|
||||
id: [],
|
||||
visibility: [],
|
||||
isFavorite: [],
|
||||
isImage: [],
|
||||
isTrashed: [],
|
||||
livePhotoVideoId: [],
|
||||
fileCreatedAt: [],
|
||||
localOffsetHours: [],
|
||||
ownerId: [],
|
||||
projectionType: [],
|
||||
ratio: [],
|
||||
status: [],
|
||||
thumbhash: [],
|
||||
});
|
||||
});
|
||||
|
||||
// TODO enable date string validation while still accepting 5 digit years
|
||||
// it('should fail if time bucket is invalid', async () => {
|
||||
// const { status, body } = await request(app)
|
||||
// .get('/timeline/bucket')
|
||||
// .set('Authorization', `Bearer ${user.accessToken}`)
|
||||
// .query({ timeBucket: 'foo' });
|
||||
|
||||
// expect(status).toBe(400);
|
||||
// expect(body).toEqual(errorDto.badRequest);
|
||||
// });
|
||||
|
||||
it('should return time bucket', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/timeline/bucket')
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||
.query({ timeBucket: '1970-02-10' });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
city: [],
|
||||
country: [],
|
||||
duration: [],
|
||||
id: [],
|
||||
visibility: [],
|
||||
isFavorite: [],
|
||||
isImage: [],
|
||||
isTrashed: [],
|
||||
livePhotoVideoId: [],
|
||||
fileCreatedAt: [],
|
||||
localOffsetHours: [],
|
||||
ownerId: [],
|
||||
projectionType: [],
|
||||
ratio: [],
|
||||
status: [],
|
||||
thumbhash: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return time bucket in trash', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/timeline/bucket')
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||
.query({ timeBucket: '1970-02-01T00:00:00.000Z', isTrashed: true });
|
||||
|
||||
expect(status).toBe(200);
|
||||
|
||||
const timeBucket: TimeBucketAssetResponseDto = body;
|
||||
expect(timeBucket.isTrashed).toEqual([true]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -97,7 +97,7 @@ describe(`immich upload`, () => {
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await utils.resetDatabase(['assets', 'albums']);
|
||||
await utils.resetDatabase(['asset', 'album']);
|
||||
});
|
||||
|
||||
describe(`immich upload /path/to/file.jpg`, () => {
|
||||
|
||||
@@ -116,6 +116,7 @@ export const deviceDto = {
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
current: true,
|
||||
isPendingSyncReset: false,
|
||||
deviceOS: '',
|
||||
deviceType: '',
|
||||
},
|
||||
|
||||
@@ -12,6 +12,7 @@ export enum OAuthUser {
|
||||
NO_NAME = 'no-name',
|
||||
WITH_QUOTA = 'with-quota',
|
||||
WITH_USERNAME = 'with-username',
|
||||
WITH_ROLE = 'with-role',
|
||||
}
|
||||
|
||||
const claims = [
|
||||
@@ -34,6 +35,12 @@ const claims = [
|
||||
preferred_username: 'user-quota',
|
||||
immich_quota: 25,
|
||||
},
|
||||
{
|
||||
sub: OAuthUser.WITH_ROLE,
|
||||
email: 'oauth-with-role@immich.app',
|
||||
email_verified: true,
|
||||
immich_role: 'admin',
|
||||
},
|
||||
];
|
||||
|
||||
const withDefaultClaims = (sub: string) => ({
|
||||
@@ -64,7 +71,15 @@ const setup = async () => {
|
||||
claims: {
|
||||
openid: ['sub'],
|
||||
email: ['email', 'email_verified'],
|
||||
profile: ['name', 'given_name', 'family_name', 'preferred_username', 'immich_quota', 'immich_username'],
|
||||
profile: [
|
||||
'name',
|
||||
'given_name',
|
||||
'family_name',
|
||||
'preferred_username',
|
||||
'immich_quota',
|
||||
'immich_username',
|
||||
'immich_role',
|
||||
],
|
||||
},
|
||||
features: {
|
||||
jwtUserinfo: {
|
||||
|
||||
@@ -60,6 +60,7 @@ import { io, type Socket } from 'socket.io-client';
|
||||
import { loginDto, signupDto } from 'src/fixtures';
|
||||
import { makeRandomImage } from 'src/generators';
|
||||
import request from 'supertest';
|
||||
export type { Emitter } from '@socket.io/component-emitter';
|
||||
|
||||
type CommandResponse = { stdout: string; stderr: string; exitCode: number | null };
|
||||
type EventType = 'assetUpload' | 'assetUpdate' | 'assetDelete' | 'userDelete' | 'assetHidden';
|
||||
@@ -84,10 +85,10 @@ export const immichAdmin = (args: string[]) =>
|
||||
export const specialCharStrings = ["'", '"', ',', '{', '}', '*'];
|
||||
export const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
|
||||
const executeCommand = (command: string, args: string[]) => {
|
||||
const executeCommand = (command: string, args: string[], options?: { cwd?: string }) => {
|
||||
let _resolve: (value: CommandResponse) => void;
|
||||
const promise = new Promise<CommandResponse>((resolve) => (_resolve = resolve));
|
||||
const child = spawn(command, args, { stdio: 'pipe' });
|
||||
const child = spawn(command, args, { stdio: 'pipe', cwd: options?.cwd });
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
@@ -153,19 +154,19 @@ export const utils = {
|
||||
|
||||
tables = tables || [
|
||||
// TODO e2e test for deleting a stack, since it is quite complex
|
||||
'asset_stack',
|
||||
'libraries',
|
||||
'shared_links',
|
||||
'stack',
|
||||
'library',
|
||||
'shared_link',
|
||||
'person',
|
||||
'albums',
|
||||
'assets',
|
||||
'asset_faces',
|
||||
'album',
|
||||
'asset',
|
||||
'asset_face',
|
||||
'activity',
|
||||
'api_keys',
|
||||
'sessions',
|
||||
'users',
|
||||
'api_key',
|
||||
'session',
|
||||
'user',
|
||||
'system_metadata',
|
||||
'tags',
|
||||
'tag',
|
||||
];
|
||||
|
||||
const sql: string[] = [];
|
||||
@@ -174,7 +175,7 @@ export const utils = {
|
||||
if (table === 'system_metadata') {
|
||||
sql.push(`DELETE FROM "system_metadata" where "key" NOT IN ('reverse-geocoding-state', 'system-flags');`);
|
||||
} else {
|
||||
sql.push(`DELETE FROM ${table} CASCADE;`);
|
||||
sql.push(`DELETE FROM "${table}" CASCADE;`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,7 +451,7 @@ export const utils = {
|
||||
return;
|
||||
}
|
||||
|
||||
await client.query('INSERT INTO asset_faces ("assetId", "personId") VALUES ($1, $2)', [assetId, personId]);
|
||||
await client.query('INSERT INTO asset_face ("assetId", "personId") VALUES ($1, $2)', [assetId, personId]);
|
||||
},
|
||||
|
||||
setPersonThumbnail: async (personId: string) => {
|
||||
|
||||
32
i18n/en.json
32
i18n/en.json
@@ -166,6 +166,20 @@
|
||||
"metadata_settings_description": "Manage metadata settings",
|
||||
"migration_job": "Migration",
|
||||
"migration_job_description": "Migrate thumbnails for assets and faces to the latest folder structure",
|
||||
"nightly_tasks_cluster_faces_setting_description": "Run facial recognition on newly detected faces",
|
||||
"nightly_tasks_cluster_new_faces_setting": "Cluster new faces",
|
||||
"nightly_tasks_database_cleanup_setting": "Database cleanup tasks",
|
||||
"nightly_tasks_database_cleanup_setting_description": "Clean up old, expired data from the database",
|
||||
"nightly_tasks_generate_memories_setting": "Generate memories",
|
||||
"nightly_tasks_generate_memories_setting_description": "Create new memories from assets",
|
||||
"nightly_tasks_missing_thumbnails_setting": "Generate missing thumbnails",
|
||||
"nightly_tasks_missing_thumbnails_setting_description": "Queue assets without thumbnails for thumbnail generation",
|
||||
"nightly_tasks_settings": "Nightly Tasks Settings",
|
||||
"nightly_tasks_settings_description": "Manage nightly tasks",
|
||||
"nightly_tasks_start_time_setting": "Start time",
|
||||
"nightly_tasks_start_time_setting_description": "The time at which the server starts running the nightly tasks",
|
||||
"nightly_tasks_sync_quota_usage_setting": "Sync quota usage",
|
||||
"nightly_tasks_sync_quota_usage_setting_description": "Update user storage quota, based on current usage",
|
||||
"no_paths_added": "No paths added",
|
||||
"no_pattern_added": "No pattern added",
|
||||
"note_apply_storage_label_previous_assets": "Note: To apply the Storage Label to previously uploaded assets, run the",
|
||||
@@ -196,6 +210,8 @@
|
||||
"oauth_mobile_redirect_uri": "Mobile redirect URI",
|
||||
"oauth_mobile_redirect_uri_override": "Mobile redirect URI override",
|
||||
"oauth_mobile_redirect_uri_override_description": "Enable when OAuth provider does not allow a mobile URI, like ''{callback}''",
|
||||
"oauth_role_claim": "Role Claim",
|
||||
"oauth_role_claim_description": "Automatically grant admin access based on the presence of this claim. The claim may have either 'user' or 'admin'.",
|
||||
"oauth_settings": "OAuth",
|
||||
"oauth_settings_description": "Manage OAuth login settings",
|
||||
"oauth_settings_more_details": "For more details about this feature, refer to the <link>docs</link>.",
|
||||
@@ -427,6 +443,7 @@
|
||||
"app_settings": "App Settings",
|
||||
"appears_in": "Appears in",
|
||||
"archive": "Archive",
|
||||
"archive_action_prompt": "{count} added to Archive",
|
||||
"archive_or_unarchive_photo": "Archive or unarchive photo",
|
||||
"archive_page_no_archived_assets": "No archived assets found",
|
||||
"archive_page_title": "Archive ({count})",
|
||||
@@ -702,7 +719,7 @@
|
||||
"daily_title_text_date": "E, MMM dd",
|
||||
"daily_title_text_date_year": "E, MMM dd, yyyy",
|
||||
"dark": "Dark",
|
||||
"darkTheme": "Toggle dark theme",
|
||||
"dark_theme": "Toggle dark theme",
|
||||
"date_after": "Date after",
|
||||
"date_and_time": "Date and Time",
|
||||
"date_before": "Date before",
|
||||
@@ -718,6 +735,7 @@
|
||||
"default_locale": "Default Locale",
|
||||
"default_locale_description": "Format dates and numbers based on your browser locale",
|
||||
"delete": "Delete",
|
||||
"delete_action_prompt": "{count} deleted permanently",
|
||||
"delete_album": "Delete album",
|
||||
"delete_api_key_prompt": "Are you sure you want to delete this API key?",
|
||||
"delete_dialog_alert": "These items will be permanently deleted from Immich and from your device",
|
||||
@@ -731,6 +749,7 @@
|
||||
"delete_key": "Delete key",
|
||||
"delete_library": "Delete Library",
|
||||
"delete_link": "Delete link",
|
||||
"delete_local_action_prompt": "{count} deleted locally",
|
||||
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
|
||||
"delete_local_dialog_ok_force": "Delete Anyway",
|
||||
"delete_others": "Delete others",
|
||||
@@ -798,6 +817,7 @@
|
||||
"edit_key": "Edit key",
|
||||
"edit_link": "Edit link",
|
||||
"edit_location": "Edit location",
|
||||
"edit_location_action_prompt": "{count} location edited",
|
||||
"edit_location_dialog_title": "Location",
|
||||
"edit_name": "Edit name",
|
||||
"edit_people": "Edit people",
|
||||
@@ -983,6 +1003,7 @@
|
||||
"failed_to_load_assets": "Failed to load assets",
|
||||
"failed_to_load_folder": "Failed to load folder",
|
||||
"favorite": "Favorite",
|
||||
"favorite_action_prompt": "{count} added to Favorites",
|
||||
"favorite_or_unfavorite_photo": "Favorite or unfavorite photo",
|
||||
"favorites": "Favorites",
|
||||
"favorites_page_no_favorites": "No favorite assets found",
|
||||
@@ -1126,6 +1147,7 @@
|
||||
"library_page_sort_created": "Created date",
|
||||
"library_page_sort_last_modified": "Last modified",
|
||||
"library_page_sort_title": "Album title",
|
||||
"licenses": "Licenses",
|
||||
"light": "Light",
|
||||
"like_deleted": "Like deleted",
|
||||
"link_motion_video": "Link motion video",
|
||||
@@ -1245,6 +1267,7 @@
|
||||
"more": "More",
|
||||
"move": "Move",
|
||||
"move_off_locked_folder": "Move out of locked folder",
|
||||
"move_to_lock_folder_action_prompt": "{count} added to the locked folder",
|
||||
"move_to_locked_folder": "Move to locked folder",
|
||||
"move_to_locked_folder_confirmation": "These photos and video will be removed from all albums, and only viewable from the locked folder",
|
||||
"moved_to_archive": "Moved {count, plural, one {# asset} other {# assets}} to archive",
|
||||
@@ -1494,7 +1517,9 @@
|
||||
"remove_custom_date_range": "Remove custom date range",
|
||||
"remove_deleted_assets": "Remove Deleted Assets",
|
||||
"remove_from_album": "Remove from album",
|
||||
"remove_from_album_action_prompt": "{count} removed from the album",
|
||||
"remove_from_favorites": "Remove from favorites",
|
||||
"remove_from_lock_folder_action_prompt": "{count} removed from the locked folder",
|
||||
"remove_from_locked_folder": "Remove from locked folder",
|
||||
"remove_from_locked_folder_confirmation": "Are you sure you want to move these photos and videos out of the locked folder? They will be visible in your library.",
|
||||
"remove_from_shared_link": "Remove from shared link",
|
||||
@@ -1837,6 +1862,7 @@
|
||||
"total": "Total",
|
||||
"total_usage": "Total usage",
|
||||
"trash": "Trash",
|
||||
"trash_action_prompt": "{count} moved to trash",
|
||||
"trash_all": "Trash All",
|
||||
"trash_count": "Trash {count, number}",
|
||||
"trash_delete_asset": "Trash/Delete Asset",
|
||||
@@ -1854,9 +1880,11 @@
|
||||
"unable_to_change_pin_code": "Unable to change PIN code",
|
||||
"unable_to_setup_pin_code": "Unable to setup PIN code",
|
||||
"unarchive": "Unarchive",
|
||||
"unarchive_action_prompt": "{count} removed from Archive",
|
||||
"unarchived_count": "{count, plural, other {Unarchived #}}",
|
||||
"undo": "Undo",
|
||||
"unfavorite": "Unfavorite",
|
||||
"unfavorite_action_prompt": "{count} removed from Favorites",
|
||||
"unhide_person": "Unhide person",
|
||||
"unknown": "Unknown",
|
||||
"unknown_country": "Unknown Country",
|
||||
@@ -1875,6 +1903,7 @@
|
||||
"unselect_all_in": "Unselect all in {group}",
|
||||
"unstack": "Un-stack",
|
||||
"unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}",
|
||||
"untagged": "Untagged",
|
||||
"up_next": "Up next",
|
||||
"updated_at": "Updated",
|
||||
"updated_password": "Updated password",
|
||||
@@ -1911,6 +1940,7 @@
|
||||
"user_usage_stats_description": "View account usage statistics",
|
||||
"username": "Username",
|
||||
"users": "Users",
|
||||
"users_added_to_album_count": "Added {count, plural, one {# user} other {# users}} to the album",
|
||||
"utilities": "Utilities",
|
||||
"validate": "Validate",
|
||||
"validate_endpoint_error": "Please enter a valid URL",
|
||||
|
||||
@@ -4,9 +4,12 @@ import sys
|
||||
import requests
|
||||
|
||||
port = os.getenv("IMMICH_PORT", 3003)
|
||||
host = os.getenv("IMMICH_HOST", "0.0.0.0")
|
||||
|
||||
host = "localhost" if host == "0.0.0.0" else host
|
||||
|
||||
try:
|
||||
response = requests.get(f"http://localhost:{port}/ping", timeout=2)
|
||||
response = requests.get(f"http://{host}:{port}/ping", timeout=2)
|
||||
if response.status_code == 200:
|
||||
sys.exit(0)
|
||||
sys.exit(1)
|
||||
|
||||
98
machine-learning/uv.lock
generated
98
machine-learning/uv.lock
generated
@@ -517,16 +517,16 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.115.13"
|
||||
version = "0.115.14"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "starlette" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/20/64/ec0788201b5554e2a87c49af26b77a4d132f807a0fa9675257ac92c6aa0e/fastapi-0.115.13.tar.gz", hash = "sha256:55d1d25c2e1e0a0a50aceb1c8705cd932def273c102bff0b1c1da88b3c6eb307", size = 295680, upload-time = "2025-06-17T11:49:45.575Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ca/53/8c38a874844a8b0fa10dd8adf3836ac154082cf88d3f22b544e9ceea0a15/fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739", size = 296263, upload-time = "2025-06-26T15:29:08.21Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/59/4a/e17764385382062b0edbb35a26b7cf76d71e27e456546277a42ba6545c6e/fastapi-0.115.13-py3-none-any.whl", hash = "sha256:0a0cab59afa7bab22f5eb347f8c9864b681558c278395e94035a741fc10cd865", size = 95315, upload-time = "2025-06-17T11:49:44.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/50/b1222562c6d270fea83e9c9075b8e8600b8479150a18e4516a6138b980d1/fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca", size = 95514, upload-time = "2025-06-26T15:29:06.49Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -900,7 +900,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "huggingface-hub"
|
||||
version = "0.33.0"
|
||||
version = "0.33.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "filelock" },
|
||||
@@ -912,9 +912,9 @@ dependencies = [
|
||||
{ name = "tqdm" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/91/8a/1362d565fefabaa4185cf3ae842a98dbc5b35146f5694f7080f043a6952f/huggingface_hub-0.33.0.tar.gz", hash = "sha256:aa31f70d29439d00ff7a33837c03f1f9dd83971ce4e29ad664d63ffb17d3bb97", size = 426179, upload-time = "2025-06-11T17:08:07.913Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fa/42/8a95c5632080ae312c0498744b2b852195e10b05a20b1be11c5141092f4c/huggingface_hub-0.33.2.tar.gz", hash = "sha256:84221defaec8fa09c090390cd68c78b88e3c4c2b7befba68d3dc5aacbc3c2c5f", size = 426637, upload-time = "2025-07-02T06:26:05.156Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/33/fb/53587a89fbc00799e4179796f51b3ad713c5de6bb680b2becb6d37c94649/huggingface_hub-0.33.0-py3-none-any.whl", hash = "sha256:e8668875b40c68f9929150d99727d39e5ebb8a05a98e4191b908dc7ded9074b3", size = 514799, upload-time = "2025-06-11T17:08:05.757Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/f4/5f3f22e762ad1965f01122b42dae5bf0e009286e2dba601ce1d0dba72424/huggingface_hub-0.33.2-py3-none-any.whl", hash = "sha256:3749498bfa91e8cde2ddc2c1db92c79981f40e66434c20133b39e5928ac9bcc5", size = 515373, upload-time = "2025-07-02T06:26:03.072Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1044,7 +1044,7 @@ requires-dist = [
|
||||
{ name = "onnxruntime", marker = "extra == 'armnn'", specifier = ">=1.15.0,<2" },
|
||||
{ name = "onnxruntime", marker = "extra == 'cpu'", specifier = ">=1.15.0,<2" },
|
||||
{ name = "onnxruntime", marker = "extra == 'rknn'", specifier = ">=1.15.0,<2" },
|
||||
{ name = "onnxruntime-gpu", marker = "extra == 'cuda'", specifier = ">=1.17.0,<2", index = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/" },
|
||||
{ name = "onnxruntime-gpu", marker = "extra == 'cuda'", specifier = ">=1.17.0,<2", index = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple" },
|
||||
{ name = "onnxruntime-openvino", marker = "extra == 'openvino'", specifier = ">=1.17.1,<1.19.0" },
|
||||
{ name = "opencv-python-headless", specifier = ">=4.7.0.72,<5.0" },
|
||||
{ name = "orjson", specifier = ">=3.9.5" },
|
||||
@@ -1568,7 +1568,7 @@ wheels = [
|
||||
[[package]]
|
||||
name = "onnxruntime-gpu"
|
||||
version = "1.19.2"
|
||||
source = { registry = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/" }
|
||||
source = { registry = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple" }
|
||||
dependencies = [
|
||||
{ name = "coloredlogs" },
|
||||
{ name = "flatbuffers" },
|
||||
@@ -1936,16 +1936,16 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-settings"
|
||||
version = "2.9.1"
|
||||
version = "2.10.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234, upload-time = "2025-04-18T16:44:48.265Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356, upload-time = "2025-04-18T16:44:46.617Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2304,27 +2304,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.12.0"
|
||||
version = "0.12.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/24/90/5255432602c0b196a0da6720f6f76b93eb50baef46d3c9b0025e2f9acbf3/ruff-0.12.0.tar.gz", hash = "sha256:4d047db3662418d4a848a3fdbfaf17488b34b62f527ed6f10cb8afd78135bc5c", size = 4376101, upload-time = "2025-06-17T15:19:26.217Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6c/3d/d9a195676f25d00dbfcf3cf95fdd4c685c497fcfa7e862a44ac5e4e96480/ruff-0.12.2.tar.gz", hash = "sha256:d7b4f55cd6f325cb7621244f19c873c565a08aff5a4ba9c69aa7355f3f7afd3e", size = 4432239, upload-time = "2025-07-03T16:40:19.566Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/fd/b46bb20e14b11ff49dbc74c61de352e0dc07fb650189513631f6fb5fc69f/ruff-0.12.0-py3-none-linux_armv6l.whl", hash = "sha256:5652a9ecdb308a1754d96a68827755f28d5dfb416b06f60fd9e13f26191a8848", size = 10311554, upload-time = "2025-06-17T15:18:45.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/d3/021dde5a988fa3e25d2468d1dadeea0ae89dc4bc67d0140c6e68818a12a1/ruff-0.12.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:05ed0c914fabc602fc1f3b42c53aa219e5736cb030cdd85640c32dbc73da74a6", size = 11118435, upload-time = "2025-06-17T15:18:49.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/a2/01a5acf495265c667686ec418f19fd5c32bcc326d4c79ac28824aecd6a32/ruff-0.12.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:07a7aa9b69ac3fcfda3c507916d5d1bca10821fe3797d46bad10f2c6de1edda0", size = 10466010, upload-time = "2025-06-17T15:18:51.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/57/7caf31dd947d72e7aa06c60ecb19c135cad871a0a8a251723088132ce801/ruff-0.12.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7731c3eec50af71597243bace7ec6104616ca56dda2b99c89935fe926bdcd48", size = 10661366, upload-time = "2025-06-17T15:18:53.29Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/ba/aa393b972a782b4bc9ea121e0e358a18981980856190d7d2b6187f63e03a/ruff-0.12.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:952d0630eae628250ab1c70a7fffb641b03e6b4a2d3f3ec6c1d19b4ab6c6c807", size = 10173492, upload-time = "2025-06-17T15:18:55.262Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/50/9349ee777614bc3062fc6b038503a59b2034d09dd259daf8192f56c06720/ruff-0.12.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c021f04ea06966b02614d442e94071781c424ab8e02ec7af2f037b4c1e01cc82", size = 11761739, upload-time = "2025-06-17T15:18:58.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/8f/ad459de67c70ec112e2ba7206841c8f4eb340a03ee6a5cabc159fe558b8e/ruff-0.12.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d235618283718ee2fe14db07f954f9b2423700919dc688eacf3f8797a11315c", size = 12537098, upload-time = "2025-06-17T15:19:01.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/50/15ad9c80ebd3c4819f5bd8883e57329f538704ed57bac680d95cb6627527/ruff-0.12.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0758038f81beec8cc52ca22de9685b8ae7f7cc18c013ec2050012862cc9165", size = 12154122, upload-time = "2025-06-17T15:19:03.727Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/e6/79b91e41bc8cc3e78ee95c87093c6cacfa275c786e53c9b11b9358026b3d/ruff-0.12.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:139b3d28027987b78fc8d6cfb61165447bdf3740e650b7c480744873688808c2", size = 11363374, upload-time = "2025-06-17T15:19:05.875Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/c3/82b292ff8a561850934549aa9dc39e2c4e783ab3c21debe55a495ddf7827/ruff-0.12.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68853e8517b17bba004152aebd9dd77d5213e503a5f2789395b25f26acac0da4", size = 11587647, upload-time = "2025-06-17T15:19:08.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/42/d5760d742669f285909de1bbf50289baccb647b53e99b8a3b4f7ce1b2001/ruff-0.12.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3a9512af224b9ac4757f7010843771da6b2b0935a9e5e76bb407caa901a1a514", size = 10527284, upload-time = "2025-06-17T15:19:10.37Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/f6/fcee9935f25a8a8bba4adbae62495c39ef281256693962c2159e8b284c5f/ruff-0.12.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b08df3d96db798e5beb488d4df03011874aff919a97dcc2dd8539bb2be5d6a88", size = 10158609, upload-time = "2025-06-17T15:19:12.286Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/fb/057febf0eea07b9384787bfe197e8b3384aa05faa0d6bd844b94ceb29945/ruff-0.12.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6a315992297a7435a66259073681bb0d8647a826b7a6de45c6934b2ca3a9ed51", size = 11141462, upload-time = "2025-06-17T15:19:15.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/7c/1be8571011585914b9d23c95b15d07eec2d2303e94a03df58294bc9274d4/ruff-0.12.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e55e44e770e061f55a7dbc6e9aed47feea07731d809a3710feda2262d2d4d8a", size = 11641616, upload-time = "2025-06-17T15:19:17.6Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/ef/b960ab4818f90ff59e571d03c3f992828d4683561095e80f9ef31f3d58b7/ruff-0.12.0-py3-none-win32.whl", hash = "sha256:7162a4c816f8d1555eb195c46ae0bd819834d2a3f18f98cc63819a7b46f474fb", size = 10525289, upload-time = "2025-06-17T15:19:19.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/93/8b16034d493ef958a500f17cda3496c63a537ce9d5a6479feec9558f1695/ruff-0.12.0-py3-none-win_amd64.whl", hash = "sha256:d00b7a157b8fb6d3827b49d3324da34a1e3f93492c1f97b08e222ad7e9b291e0", size = 11598311, upload-time = "2025-06-17T15:19:21.785Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/33/4d3e79e4a84533d6cd526bfb42c020a23256ae5e4265d858bd1287831f7d/ruff-0.12.0-py3-none-win_arm64.whl", hash = "sha256:8cd24580405ad8c1cc64d61725bca091d6b6da7eb3d36f72cc605467069d7e8b", size = 10724946, upload-time = "2025-06-17T15:19:23.952Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/b6/2098d0126d2d3318fd5bec3ad40d06c25d377d95749f7a0c5af17129b3b1/ruff-0.12.2-py3-none-linux_armv6l.whl", hash = "sha256:093ea2b221df1d2b8e7ad92fc6ffdca40a2cb10d8564477a987b44fd4008a7be", size = 10369761, upload-time = "2025-07-03T16:39:38.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/4b/5da0142033dbe155dc598cfb99262d8ee2449d76920ea92c4eeb9547c208/ruff-0.12.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:09e4cf27cc10f96b1708100fa851e0daf21767e9709e1649175355280e0d950e", size = 11155659, upload-time = "2025-07-03T16:39:42.294Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/21/967b82550a503d7c5c5c127d11c935344b35e8c521f52915fc858fb3e473/ruff-0.12.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8ae64755b22f4ff85e9c52d1f82644abd0b6b6b6deedceb74bd71f35c24044cc", size = 10537769, upload-time = "2025-07-03T16:39:44.75Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/91/00cff7102e2ec71a4890fb7ba1803f2cdb122d82787c7d7cf8041fe8cbc1/ruff-0.12.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eb3a6b2db4d6e2c77e682f0b988d4d61aff06860158fdb413118ca133d57922", size = 10717602, upload-time = "2025-07-03T16:39:47.652Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/eb/928814daec4e1ba9115858adcda44a637fb9010618721937491e4e2283b8/ruff-0.12.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73448de992d05517170fc37169cbca857dfeaeaa8c2b9be494d7bcb0d36c8f4b", size = 10198772, upload-time = "2025-07-03T16:39:49.641Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/fa/f15089bc20c40f4f72334f9145dde55ab2b680e51afb3b55422effbf2fb6/ruff-0.12.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b8b94317cbc2ae4a2771af641739f933934b03555e51515e6e021c64441532d", size = 11845173, upload-time = "2025-07-03T16:39:52.069Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/9f/1f6f98f39f2b9302acc161a4a2187b1e3a97634fe918a8e731e591841cf4/ruff-0.12.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:45fc42c3bf1d30d2008023a0a9a0cfb06bf9835b147f11fe0679f21ae86d34b1", size = 12553002, upload-time = "2025-07-03T16:39:54.551Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/70/08991ac46e38ddd231c8f4fd05ef189b1b94be8883e8c0c146a025c20a19/ruff-0.12.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce48f675c394c37e958bf229fb5c1e843e20945a6d962cf3ea20b7a107dcd9f4", size = 12171330, upload-time = "2025-07-03T16:39:57.55Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/a9/5a55266fec474acfd0a1c73285f19dd22461d95a538f29bba02edd07a5d9/ruff-0.12.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793d8859445ea47591272021a81391350205a4af65a9392401f418a95dfb75c9", size = 11774717, upload-time = "2025-07-03T16:39:59.78Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/e5/0c270e458fc73c46c0d0f7cf970bb14786e5fdb88c87b5e423a4bd65232b/ruff-0.12.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6932323db80484dda89153da3d8e58164d01d6da86857c79f1961934354992da", size = 11646659, upload-time = "2025-07-03T16:40:01.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/b6/45ab96070c9752af37f0be364d849ed70e9ccede07675b0ec4e3ef76b63b/ruff-0.12.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6aa7e623a3a11538108f61e859ebf016c4f14a7e6e4eba1980190cacb57714ce", size = 10604012, upload-time = "2025-07-03T16:40:04.363Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/91/26a6e6a424eb147cc7627eebae095cfa0b4b337a7c1c413c447c9ebb72fd/ruff-0.12.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2a4a20aeed74671b2def096bdf2eac610c7d8ffcbf4fb0e627c06947a1d7078d", size = 10176799, upload-time = "2025-07-03T16:40:06.514Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/0c/9f344583465a61c8918a7cda604226e77b2c548daf8ef7c2bfccf2b37200/ruff-0.12.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:71a4c550195612f486c9d1f2b045a600aeba851b298c667807ae933478fcef04", size = 11241507, upload-time = "2025-07-03T16:40:08.708Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/b7/99c34ded8fb5f86c0280278fa89a0066c3760edc326e935ce0b1550d315d/ruff-0.12.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4987b8f4ceadf597c927beee65a5eaf994c6e2b631df963f86d8ad1bdea99342", size = 11717609, upload-time = "2025-07-03T16:40:10.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/de/8589fa724590faa057e5a6d171e7f2f6cffe3287406ef40e49c682c07d89/ruff-0.12.2-py3-none-win32.whl", hash = "sha256:369ffb69b70cd55b6c3fc453b9492d98aed98062db9fec828cdfd069555f5f1a", size = 10523823, upload-time = "2025-07-03T16:40:13.203Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/47/8abf129102ae4c90cba0c2199a1a9b0fa896f6f806238d6f8c14448cc748/ruff-0.12.2-py3-none-win_amd64.whl", hash = "sha256:dca8a3b6d6dc9810ed8f328d406516bf4d660c00caeaef36eb831cf4871b0639", size = 11629831, upload-time = "2025-07-03T16:40:15.478Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/1f/72d2946e3cc7456bb837e88000eb3437e55f80db339c840c04015a11115d/ruff-0.12.2-py3-none-win_arm64.whl", hash = "sha256:48d6c6bfb4761df68bc05ae630e24f506755e702d4fb08f08460be778c7ccb12", size = 10735334, upload-time = "2025-07-03T16:40:17.677Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2504,27 +2504,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "tokenizers"
|
||||
version = "0.21.1"
|
||||
version = "0.21.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "huggingface-hub" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/92/76/5ac0c97f1117b91b7eb7323dcd61af80d72f790b4df71249a7850c195f30/tokenizers-0.21.1.tar.gz", hash = "sha256:a1bb04dc5b448985f86ecd4b05407f5a8d97cb2c0532199b2a302a604a0165ab", size = 343256, upload-time = "2025-03-13T10:51:18.189Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ab/2d/b0fce2b8201635f60e8c95990080f58461cc9ca3d5026de2e900f38a7f21/tokenizers-0.21.2.tar.gz", hash = "sha256:fdc7cffde3e2113ba0e6cc7318c40e3438a4d74bbc62bf04bcc63bdfb082ac77", size = 351545, upload-time = "2025-06-24T10:24:52.449Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/1f/328aee25f9115bf04262e8b4e5a2050b7b7cf44b59c74e982db7270c7f30/tokenizers-0.21.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e78e413e9e668ad790a29456e677d9d3aa50a9ad311a40905d6861ba7692cf41", size = 2780767, upload-time = "2025-03-13T10:51:09.459Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/1a/4526797f3719b0287853f12c5ad563a9be09d446c44ac784cdd7c50f76ab/tokenizers-0.21.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:cd51cd0a91ecc801633829fcd1fda9cf8682ed3477c6243b9a095539de4aecf3", size = 2650555, upload-time = "2025-03-13T10:51:07.692Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/7a/a209b29f971a9fdc1da86f917fe4524564924db50d13f0724feed37b2a4d/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28da6b72d4fb14ee200a1bd386ff74ade8992d7f725f2bde2c495a9a98cf4d9f", size = 2937541, upload-time = "2025-03-13T10:50:56.679Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/1e/b788b50ffc6191e0b1fc2b0d49df8cff16fe415302e5ceb89f619d12c5bc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34d8cfde551c9916cb92014e040806122295a6800914bab5865deb85623931cf", size = 2819058, upload-time = "2025-03-13T10:50:59.525Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/aa/3626dfa09a0ecc5b57a8c58eeaeb7dd7ca9a37ad9dd681edab5acd55764c/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaa852d23e125b73d283c98f007e06d4595732104b65402f46e8ef24b588d9f8", size = 3133278, upload-time = "2025-03-13T10:51:04.678Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/4d/8fbc203838b3d26269f944a89459d94c858f5b3f9a9b6ee9728cdcf69161/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a21a15d5c8e603331b8a59548bbe113564136dc0f5ad8306dd5033459a226da0", size = 3144253, upload-time = "2025-03-13T10:51:01.261Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/1b/2bd062adeb7c7511b847b32e356024980c0ffcf35f28947792c2d8ad2288/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fdbd4c067c60a0ac7eca14b6bd18a5bebace54eb757c706b47ea93204f7a37c", size = 3398225, upload-time = "2025-03-13T10:51:03.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/63/38be071b0c8e06840bc6046991636bcb30c27f6bb1e670f4f4bc87cf49cc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dd9a0061e403546f7377df940e866c3e678d7d4e9643d0461ea442b4f89e61a", size = 3038874, upload-time = "2025-03-13T10:51:06.235Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/83/afa94193c09246417c23a3c75a8a0a96bf44ab5630a3015538d0c316dd4b/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:db9484aeb2e200c43b915a1a0150ea885e35f357a5a8fabf7373af333dcc8dbf", size = 9014448, upload-time = "2025-03-13T10:51:10.927Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/b3/0e1a37d4f84c0f014d43701c11eb8072704f6efe8d8fc2dcdb79c47d76de/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed248ab5279e601a30a4d67bdb897ecbe955a50f1e7bb62bd99f07dd11c2f5b6", size = 8937877, upload-time = "2025-03-13T10:51:12.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/33/ff08f50e6d615eb180a4a328c65907feb6ded0b8f990ec923969759dc379/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:9ac78b12e541d4ce67b4dfd970e44c060a2147b9b2a21f509566d556a509c67d", size = 9186645, upload-time = "2025-03-13T10:51:14.723Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/aa/8ae85f69a9f6012c6f8011c6f4aa1c96154c816e9eea2e1b758601157833/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e5a69c1a4496b81a5ee5d2c1f3f7fbdf95e90a0196101b0ee89ed9956b8a168f", size = 9384380, upload-time = "2025-03-13T10:51:16.526Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/5b/a5d98c89f747455e8b7a9504910c865d5e51da55e825a7ae641fb5ff0a58/tokenizers-0.21.1-cp39-abi3-win32.whl", hash = "sha256:1039a3a5734944e09de1d48761ade94e00d0fa760c0e0551151d4dd851ba63e3", size = 2239506, upload-time = "2025-03-13T10:51:20.643Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/b6/072a8e053ae600dcc2ac0da81a23548e3b523301a442a6ca900e92ac35be/tokenizers-0.21.1-cp39-abi3-win_amd64.whl", hash = "sha256:0f0dcbcc9f6e13e675a66d7a5f2f225a736745ce484c1a4e07476a89ccdad382", size = 2435481, upload-time = "2025-03-13T10:51:19.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/cc/2936e2d45ceb130a21d929743f1e9897514691bec123203e10837972296f/tokenizers-0.21.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:342b5dfb75009f2255ab8dec0041287260fed5ce00c323eb6bab639066fef8ec", size = 2875206, upload-time = "2025-06-24T10:24:42.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/e6/33f41f2cc7861faeba8988e7a77601407bf1d9d28fc79c5903f8f77df587/tokenizers-0.21.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:126df3205d6f3a93fea80c7a8a266a78c1bd8dd2fe043386bafdd7736a23e45f", size = 2732655, upload-time = "2025-06-24T10:24:41.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/2b/1791eb329c07122a75b01035b1a3aa22ad139f3ce0ece1b059b506d9d9de/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a32cd81be21168bd0d6a0f0962d60177c447a1aa1b1e48fa6ec9fc728ee0b12", size = 3019202, upload-time = "2025-06-24T10:24:31.791Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/15/fd2d8104faa9f86ac68748e6f7ece0b5eb7983c7efc3a2c197cb98c99030/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8bd8999538c405133c2ab999b83b17c08b7fc1b48c1ada2469964605a709ef91", size = 2934539, upload-time = "2025-06-24T10:24:34.567Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/2e/53e8fd053e1f3ffbe579ca5f9546f35ac67cf0039ed357ad7ec57f5f5af0/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e9944e61239b083a41cf8fc42802f855e1dca0f499196df37a8ce219abac6eb", size = 3248665, upload-time = "2025-06-24T10:24:39.024Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/15/79713359f4037aa8f4d1f06ffca35312ac83629da062670e8830917e2153/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:514cd43045c5d546f01142ff9c79a96ea69e4b5cda09e3027708cb2e6d5762ab", size = 3451305, upload-time = "2025-06-24T10:24:36.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/5f/959f3a8756fc9396aeb704292777b84f02a5c6f25c3fc3ba7530db5feb2c/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1b9405822527ec1e0f7d8d2fdb287a5730c3a6518189c968254a8441b21faae", size = 3214757, upload-time = "2025-06-24T10:24:37.784Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/74/f41a432a0733f61f3d21b288de6dfa78f7acff309c6f0f323b2833e9189f/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fed9a4d51c395103ad24f8e7eb976811c57fbec2af9f133df471afcd922e5020", size = 3121887, upload-time = "2025-06-24T10:24:40.293Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/6a/bc220a11a17e5d07b0dfb3b5c628621d4dcc084bccd27cfaead659963016/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2c41862df3d873665ec78b6be36fcc30a26e3d4902e9dd8608ed61d49a48bc19", size = 9091965, upload-time = "2025-06-24T10:24:44.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/bd/ac386d79c4ef20dc6f39c4706640c24823dca7ebb6f703bfe6b5f0292d88/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed21dc7e624e4220e21758b2e62893be7101453525e3d23264081c9ef9a6d00d", size = 9053372, upload-time = "2025-06-24T10:24:46.455Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/7b/5440bf203b2a5358f074408f7f9c42884849cd9972879e10ee6b7a8c3b3d/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:0e73770507e65a0e0e2a1affd6b03c36e3bc4377bd10c9ccf51a82c77c0fe365", size = 9298632, upload-time = "2025-06-24T10:24:48.446Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/d2/faa1acac3f96a7427866e94ed4289949b2524f0c1878512516567d80563c/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:106746e8aa9014a12109e58d540ad5465b4c183768ea96c03cbc24c44d329958", size = 9470074, upload-time = "2025-06-24T10:24:50.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/a5/896e1ef0707212745ae9f37e84c7d50269411aef2e9ccd0de63623feecdf/tokenizers-0.21.2-cp39-abi3-win32.whl", hash = "sha256:cabda5a6d15d620b6dfe711e1af52205266d05b379ea85a8a301b3593c60e962", size = 2330115, upload-time = "2025-06-24T10:24:55.069Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/c3/cc2755ee10be859c4338c962a35b9a663788c0c0b50c0bdd8078fb6870cf/tokenizers-0.21.2-cp39-abi3-win_amd64.whl", hash = "sha256:58747bb898acdb1007f37a7bbe614346e98dc28708ffb66a3fd50ce169ac6c98", size = 2509918, upload-time = "2025-06-24T10:24:53.71Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2628,16 +2628,16 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.34.3"
|
||||
version = "0.35.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "h11" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/de/ad/713be230bcda622eaa35c28f0d328c3675c371238470abdea52417f17a8e/uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a", size = 76631, upload-time = "2025-06-01T07:48:17.531Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/0d/8adfeaa62945f90d19ddc461c55f4a50c258af7662d34b6a3d5d1f8646f6/uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885", size = 62431, upload-time = "2025-06-01T07:48:15.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"flutter": "3.29.3"
|
||||
"flutter": "3.32.6"
|
||||
}
|
||||
2
mobile/.vscode/settings.json
vendored
2
mobile/.vscode/settings.json
vendored
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"dart.flutterSdkPath": ".fvm/versions/3.29.3",
|
||||
"dart.flutterSdkPath": ".fvm/versions/3.32.6",
|
||||
"search.exclude": {
|
||||
"**/.fvm": true
|
||||
},
|
||||
|
||||
@@ -66,6 +66,20 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions "default"
|
||||
productFlavors {
|
||||
production {
|
||||
dimension "default"
|
||||
applicationId "app.alextran.immich"
|
||||
}
|
||||
|
||||
beta {
|
||||
dimension "default"
|
||||
applicationId "app.alextran.immich.beta"
|
||||
versionNameSuffix "-BETA"
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix '.debug'
|
||||
|
||||
5
mobile/android/app/src/beta/AndroidManifest.xml
Normal file
5
mobile/android/app/src/beta/AndroidManifest.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<application android:label="Immich Beta" tools:replace="android:label" />
|
||||
</manifest>
|
||||
@@ -100,24 +100,24 @@
|
||||
|
||||
<!-- my.immich.app deep link -->
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="https" />
|
||||
<data android:scheme="https" />
|
||||
|
||||
<data
|
||||
android:host="my.immich.app"
|
||||
android:path="/" />
|
||||
<data
|
||||
android:host="my.immich.app"
|
||||
android:pathPrefix="/albums/" />
|
||||
<data
|
||||
android:host="my.immich.app"
|
||||
android:pathPrefix="/memories/" />
|
||||
<data
|
||||
android:host="my.immich.app"
|
||||
android:pathPrefix="/photos/" />
|
||||
<data
|
||||
android:host="my.immich.app"
|
||||
android:path="/" />
|
||||
<data
|
||||
android:host="my.immich.app"
|
||||
android:pathPrefix="/albums/" />
|
||||
<data
|
||||
android:host="my.immich.app"
|
||||
android:pathPrefix="/memories/" />
|
||||
<data
|
||||
android:host="my.immich.app"
|
||||
android:pathPrefix="/photos/" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
|
||||
@@ -87,7 +87,8 @@ data class PlatformAsset (
|
||||
val updatedAt: Long? = null,
|
||||
val width: Long? = null,
|
||||
val height: Long? = null,
|
||||
val durationInSeconds: Long
|
||||
val durationInSeconds: Long,
|
||||
val orientation: Long
|
||||
)
|
||||
{
|
||||
companion object {
|
||||
@@ -100,7 +101,8 @@ data class PlatformAsset (
|
||||
val width = pigeonVar_list[5] as Long?
|
||||
val height = pigeonVar_list[6] as Long?
|
||||
val durationInSeconds = pigeonVar_list[7] as Long
|
||||
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds)
|
||||
val orientation = pigeonVar_list[8] as Long
|
||||
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation)
|
||||
}
|
||||
}
|
||||
fun toList(): List<Any?> {
|
||||
@@ -113,6 +115,7 @@ data class PlatformAsset (
|
||||
width,
|
||||
height,
|
||||
durationInSeconds,
|
||||
orientation,
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import androidx.core.database.getStringOrNull
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.security.MessageDigest
|
||||
@@ -39,7 +40,8 @@ open class NativeSyncApiImplBase(context: Context) {
|
||||
MediaStore.MediaColumns.BUCKET_ID,
|
||||
MediaStore.MediaColumns.WIDTH,
|
||||
MediaStore.MediaColumns.HEIGHT,
|
||||
MediaStore.MediaColumns.DURATION
|
||||
MediaStore.MediaColumns.DURATION,
|
||||
MediaStore.MediaColumns.ORIENTATION,
|
||||
)
|
||||
|
||||
const val HASH_BUFFER_SIZE = 2 * 1024 * 1024
|
||||
@@ -73,6 +75,8 @@ open class NativeSyncApiImplBase(context: Context) {
|
||||
val widthColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH)
|
||||
val heightColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT)
|
||||
val durationColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DURATION)
|
||||
val orientationColumn =
|
||||
c.getColumnIndexOrThrow(MediaStore.MediaColumns.ORIENTATION)
|
||||
|
||||
while (c.moveToNext()) {
|
||||
val id = c.getLong(idColumn).toString()
|
||||
@@ -100,6 +104,7 @@ open class NativeSyncApiImplBase(context: Context) {
|
||||
val duration = if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0
|
||||
else c.getLong(durationColumn) / 1000
|
||||
val bucketId = c.getString(bucketIdColumn)
|
||||
val orientation = c.getInt(orientationColumn)
|
||||
|
||||
val asset = PlatformAsset(
|
||||
id,
|
||||
@@ -109,7 +114,8 @@ open class NativeSyncApiImplBase(context: Context) {
|
||||
modifiedAt,
|
||||
width,
|
||||
height,
|
||||
duration
|
||||
duration,
|
||||
orientation.toLong(),
|
||||
)
|
||||
yield(AssetResult.ValidAsset(asset, bucketId))
|
||||
}
|
||||
@@ -152,7 +158,8 @@ open class NativeSyncApiImplBase(context: Context) {
|
||||
continue
|
||||
}
|
||||
|
||||
val name = cursor.getString(bucketNameColumn)
|
||||
// MediaStore might return null for bucket name (commonly for the Root Directory), so default to "Internal Storage"
|
||||
val name = cursor.getStringOrNull(bucketNameColumn) ?: "Internal Storage"
|
||||
val updatedAt = cursor.getLong(dateModified)
|
||||
albums.add(PlatformAlbum(id, name, updatedAt, false, 0))
|
||||
albumsCount[id] = 1
|
||||
|
||||
2
mobile/drift_schemas/main/drift_schema_v1.json
generated
2
mobile/drift_schemas/main/drift_schema_v1.json
generated
File diff suppressed because one or more lines are too long
@@ -5,34 +5,34 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57
|
||||
sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "80.0.0"
|
||||
version: "82.0.0"
|
||||
analyzer:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e"
|
||||
sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.3.0"
|
||||
version: "7.4.5"
|
||||
analyzer_plugin:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: analyzer_plugin
|
||||
sha256: b3075265c5ab222f8b3188342dcb50b476286394a40323e85d1fa725035d40a4
|
||||
sha256: ee188b6df6c85f1441497c7171c84f1392affadc0384f71089cb10a3bc508cef
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.13.0"
|
||||
version: "0.13.1"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
|
||||
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.0"
|
||||
version: "2.7.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -53,10 +53,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: checked_yaml
|
||||
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
|
||||
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
version: "2.0.4"
|
||||
ci:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -125,10 +125,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: custom_lint_visitor
|
||||
sha256: "36282d85714af494ee2d7da8c8913630aa6694da99f104fb2ed4afcf8fc857d8"
|
||||
sha256: cba5b6d7a6217312472bf4468cdf68c949488aed7ffb0eab792cd0b6c435054d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0+7.3.0"
|
||||
version: "1.0.0+7.4.5"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -157,10 +157,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: freezed_annotation
|
||||
sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b
|
||||
sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
version: "3.1.0"
|
||||
glob:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -213,18 +213,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
version: "1.17.0"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67"
|
||||
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.2.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -317,10 +317,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.4"
|
||||
version: "0.7.6"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -341,18 +341,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
||||
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.0.0"
|
||||
version: "15.0.2"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
|
||||
sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
version: "1.1.2"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -362,4 +362,4 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.8.0-0 <4.0.0"
|
||||
dart: ">=3.8.0 <4.0.0"
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
@@ -43,6 +44,7 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
disableMainThreadChecker = "YES"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
|
||||
@@ -138,6 +138,7 @@ struct PlatformAsset: Hashable {
|
||||
var width: Int64? = nil
|
||||
var height: Int64? = nil
|
||||
var durationInSeconds: Int64
|
||||
var orientation: Int64
|
||||
|
||||
|
||||
// swift-format-ignore: AlwaysUseLowerCamelCase
|
||||
@@ -150,6 +151,7 @@ struct PlatformAsset: Hashable {
|
||||
let width: Int64? = nilOrValue(pigeonVar_list[5])
|
||||
let height: Int64? = nilOrValue(pigeonVar_list[6])
|
||||
let durationInSeconds = pigeonVar_list[7] as! Int64
|
||||
let orientation = pigeonVar_list[8] as! Int64
|
||||
|
||||
return PlatformAsset(
|
||||
id: id,
|
||||
@@ -159,7 +161,8 @@ struct PlatformAsset: Hashable {
|
||||
updatedAt: updatedAt,
|
||||
width: width,
|
||||
height: height,
|
||||
durationInSeconds: durationInSeconds
|
||||
durationInSeconds: durationInSeconds,
|
||||
orientation: orientation
|
||||
)
|
||||
}
|
||||
func toList() -> [Any?] {
|
||||
@@ -172,6 +175,7 @@ struct PlatformAsset: Hashable {
|
||||
width,
|
||||
height,
|
||||
durationInSeconds,
|
||||
orientation,
|
||||
]
|
||||
}
|
||||
static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool {
|
||||
|
||||
@@ -27,7 +27,8 @@ extension PHAsset {
|
||||
updatedAt: modificationDate.map { Int64($0.timeIntervalSince1970) },
|
||||
width: Int64(pixelWidth),
|
||||
height: Int64(pixelHeight),
|
||||
durationInSeconds: Int64(duration)
|
||||
durationInSeconds: Int64(duration),
|
||||
orientation: 0
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -169,7 +170,8 @@ class NativeSyncApiImpl: NativeSyncApi {
|
||||
id: asset.localIdentifier,
|
||||
name: "",
|
||||
type: 0,
|
||||
durationInSeconds: 0
|
||||
durationInSeconds: 0,
|
||||
orientation: 0
|
||||
)
|
||||
if (updatedAssets.contains(AssetWrapper(with: predicate))) {
|
||||
continue
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
func buildEntry(
|
||||
api: ImmichAPI,
|
||||
asset: Asset,
|
||||
dateOffset: Int,
|
||||
subtitle: String? = nil
|
||||
)
|
||||
async throws -> ImageEntry
|
||||
{
|
||||
let entryDate = Calendar.current.date(
|
||||
byAdding: .minute,
|
||||
value: dateOffset * 20,
|
||||
to: Date.now
|
||||
)!
|
||||
let image = try await api.fetchImage(asset: asset)
|
||||
|
||||
return ImageEntry(date: entryDate, image: image, subtitle: subtitle, deepLink: asset.deepLink)
|
||||
}
|
||||
|
||||
func generateRandomEntries(
|
||||
api: ImmichAPI,
|
||||
now: Date,
|
||||
count: Int,
|
||||
albumId: String? = nil,
|
||||
subtitle: String? = nil
|
||||
)
|
||||
async throws -> [ImageEntry]
|
||||
{
|
||||
|
||||
var entries: [ImageEntry] = []
|
||||
let albumIds = albumId != nil ? [albumId!] : []
|
||||
|
||||
let randomAssets = try await api.fetchSearchResults(
|
||||
with: SearchFilters(size: count, albumIds: albumIds)
|
||||
)
|
||||
|
||||
await withTaskGroup(of: ImageEntry?.self) { group in
|
||||
for (dateOffset, asset) in randomAssets.enumerated() {
|
||||
group.addTask {
|
||||
return try? await buildEntry(
|
||||
api: api,
|
||||
asset: asset,
|
||||
dateOffset: dateOffset,
|
||||
subtitle: subtitle
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for await result in group {
|
||||
if let entry = result {
|
||||
entries.append(entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
148
mobile/ios/WidgetExtension/ImageEntry.swift
Normal file
148
mobile/ios/WidgetExtension/ImageEntry.swift
Normal file
@@ -0,0 +1,148 @@
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
typealias EntryMetadata = ImageEntry.Metadata
|
||||
|
||||
struct ImageEntry: TimelineEntry {
|
||||
let date: Date
|
||||
var image: UIImage?
|
||||
var metadata: Metadata = Metadata()
|
||||
|
||||
struct Metadata: Codable {
|
||||
var subtitle: String? = nil
|
||||
var error: WidgetError? = nil
|
||||
var deepLink: URL? = nil
|
||||
}
|
||||
|
||||
static func build(
|
||||
api: ImmichAPI,
|
||||
asset: Asset,
|
||||
dateOffset: Int,
|
||||
subtitle: String? = nil
|
||||
)
|
||||
async throws -> Self
|
||||
{
|
||||
let entryDate = Calendar.current.date(
|
||||
byAdding: .minute,
|
||||
value: dateOffset * 20,
|
||||
to: Date.now
|
||||
)!
|
||||
let image = try await api.fetchImage(asset: asset)
|
||||
|
||||
return Self(
|
||||
date: entryDate,
|
||||
image: image,
|
||||
metadata: EntryMetadata(
|
||||
subtitle: subtitle,
|
||||
deepLink: asset.deepLink
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func cache(for key: String) throws {
|
||||
if let containerURL = FileManager.default.containerURL(
|
||||
forSecurityApplicationGroupIdentifier: IMMICH_SHARE_GROUP
|
||||
) {
|
||||
let imageURL = containerURL.appendingPathComponent("\(key)_image.png")
|
||||
let metadataURL = containerURL.appendingPathComponent(
|
||||
"\(key)_metadata.json"
|
||||
)
|
||||
|
||||
// build metadata JSON
|
||||
let entryMetadata = try JSONEncoder().encode(self.metadata)
|
||||
|
||||
// write to disk
|
||||
try self.image?.pngData()?.write(to: imageURL, options: .atomic)
|
||||
try entryMetadata.write(to: metadataURL, options: .atomic)
|
||||
}
|
||||
}
|
||||
|
||||
static func loadCached(for key: String, at date: Date = Date.now)
|
||||
-> ImageEntry?
|
||||
{
|
||||
if let containerURL = FileManager.default.containerURL(
|
||||
forSecurityApplicationGroupIdentifier: IMMICH_SHARE_GROUP
|
||||
) {
|
||||
let imageURL = containerURL.appendingPathComponent("\(key)_image.png")
|
||||
let metadataURL = containerURL.appendingPathComponent(
|
||||
"\(key)_metadata.json"
|
||||
)
|
||||
|
||||
guard let imageData = try? Data(contentsOf: imageURL),
|
||||
let metadataJSON = try? Data(contentsOf: metadataURL),
|
||||
let decodedMetadata = try? JSONDecoder().decode(
|
||||
Metadata.self,
|
||||
from: metadataJSON
|
||||
)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return ImageEntry(
|
||||
date: date,
|
||||
image: UIImage(data: imageData),
|
||||
metadata: decodedMetadata
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
static func handleError(
|
||||
for key: String,
|
||||
error: WidgetError = .fetchFailed
|
||||
) -> Timeline<ImageEntry> {
|
||||
var timelineEntry = ImageEntry(
|
||||
date: Date.now,
|
||||
image: nil,
|
||||
metadata: EntryMetadata(error: error)
|
||||
)
|
||||
|
||||
// use cache if generic failed error
|
||||
// we want to show the other errors to the user since without intervention,
|
||||
// it will never succeed
|
||||
if error == .fetchFailed, let cachedEntry = ImageEntry.loadCached(for: key)
|
||||
{
|
||||
timelineEntry = cachedEntry
|
||||
}
|
||||
|
||||
return Timeline(entries: [timelineEntry], policy: .atEnd)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func generateRandomEntries(
|
||||
api: ImmichAPI,
|
||||
now: Date,
|
||||
count: Int,
|
||||
filter: SearchFilter = Album.NONE.filter,
|
||||
subtitle: String? = nil
|
||||
)
|
||||
async throws -> [ImageEntry]
|
||||
{
|
||||
|
||||
var entries: [ImageEntry] = []
|
||||
|
||||
let randomAssets = try await api.fetchSearchResults(with: filter)
|
||||
|
||||
await withTaskGroup(of: ImageEntry?.self) { group in
|
||||
for (dateOffset, asset) in randomAssets.enumerated() {
|
||||
group.addTask {
|
||||
return try? await ImageEntry.build(
|
||||
api: api,
|
||||
asset: asset,
|
||||
dateOffset: dateOffset,
|
||||
subtitle: subtitle
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for await result in group {
|
||||
if let entry = result {
|
||||
entries.append(entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
@@ -1,23 +1,14 @@
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
struct ImageEntry: TimelineEntry {
|
||||
let date: Date
|
||||
var image: UIImage?
|
||||
var subtitle: String? = nil
|
||||
var error: WidgetError? = nil
|
||||
var deepLink: URL? = nil
|
||||
|
||||
// Resizes the stored image to a maximum width of 450 pixels
|
||||
mutating func resize() {
|
||||
if (image == nil || image!.size.height < 450 || image!.size.width < 450 ) {
|
||||
return
|
||||
}
|
||||
|
||||
image = image?.resized(toWidth: 450)
|
||||
|
||||
if image == nil {
|
||||
error = .unableToResize
|
||||
extension Image {
|
||||
@ViewBuilder
|
||||
func tintedWidgetImageModifier() -> some View {
|
||||
if #available(iOS 18.0, *) {
|
||||
self
|
||||
.widgetAccentedRenderingMode(.accentedDesaturated)
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +20,8 @@ struct ImmichWidgetView: View {
|
||||
if entry.image == nil {
|
||||
VStack {
|
||||
Image("LaunchImage")
|
||||
Text(entry.error?.errorDescription ?? "")
|
||||
.tintedWidgetImageModifier()
|
||||
Text(entry.metadata.error?.errorDescription ?? "")
|
||||
.minimumScaleFactor(0.25)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(.secondary)
|
||||
@@ -40,11 +32,13 @@ struct ImmichWidgetView: View {
|
||||
Color.clear.overlay(
|
||||
Image(uiImage: entry.image!)
|
||||
.resizable()
|
||||
.tintedWidgetImageModifier()
|
||||
.scaledToFill()
|
||||
|
||||
)
|
||||
VStack {
|
||||
Spacer()
|
||||
if let subtitle = entry.subtitle {
|
||||
if let subtitle = entry.metadata.subtitle {
|
||||
Text(subtitle)
|
||||
.foregroundColor(.white)
|
||||
.padding(8)
|
||||
@@ -55,7 +49,7 @@ struct ImmichWidgetView: View {
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
.widgetURL(entry.deepLink)
|
||||
.widgetURL(entry.metadata.deepLink)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,7 +64,9 @@ struct ImmichWidgetView: View {
|
||||
ImageEntry(
|
||||
date: date,
|
||||
image: UIImage(named: "ImmichLogo"),
|
||||
subtitle: "1 year ago"
|
||||
metadata: EntryMetadata(
|
||||
subtitle: "1 year ago"
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -2,14 +2,20 @@ import Foundation
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
enum WidgetError: Error {
|
||||
let IMMICH_SHARE_GROUP = "group.app.immich.share"
|
||||
|
||||
enum WidgetError: Error, Codable {
|
||||
case noLogin
|
||||
case fetchFailed
|
||||
case unknown
|
||||
case albumNotFound
|
||||
case noAssetsAvailable
|
||||
}
|
||||
|
||||
enum FetchError: Error {
|
||||
case unableToResize
|
||||
case invalidImage
|
||||
case invalidURL
|
||||
case fetchFailed
|
||||
}
|
||||
|
||||
extension WidgetError: LocalizedError {
|
||||
@@ -23,15 +29,9 @@ extension WidgetError: LocalizedError {
|
||||
|
||||
case .albumNotFound:
|
||||
return "Album not found"
|
||||
|
||||
case .invalidURL:
|
||||
return "An invalid URL was used"
|
||||
|
||||
case .invalidImage:
|
||||
return "An invalid image was received"
|
||||
|
||||
default:
|
||||
return "An unknown error occured"
|
||||
case .noAssetsAvailable:
|
||||
return "No assets available"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,16 +46,17 @@ enum AssetType: String, Codable {
|
||||
struct Asset: Codable {
|
||||
let id: String
|
||||
let type: AssetType
|
||||
|
||||
|
||||
var deepLink: URL? {
|
||||
return URL(string: "immich://asset?id=\(id)")
|
||||
}
|
||||
}
|
||||
|
||||
struct SearchFilters: Codable {
|
||||
var type: AssetType = .image
|
||||
let size: Int
|
||||
struct SearchFilter: Codable {
|
||||
var type = AssetType.image
|
||||
var size = 1
|
||||
var albumIds: [String] = []
|
||||
var isFavorite: Bool? = nil
|
||||
}
|
||||
|
||||
struct MemoryResult: Codable {
|
||||
@@ -70,9 +71,34 @@ struct MemoryResult: Codable {
|
||||
let data: MemoryData
|
||||
}
|
||||
|
||||
struct Album: Codable {
|
||||
struct Album: Codable, Equatable {
|
||||
let id: String
|
||||
let albumName: String
|
||||
|
||||
static let NONE = Album(id: "NONE", albumName: "None")
|
||||
static let FAVORITES = Album(id: "FAVORITES", albumName: "Favorites")
|
||||
|
||||
var filter: SearchFilter {
|
||||
switch self {
|
||||
case Album.NONE:
|
||||
return SearchFilter()
|
||||
case Album.FAVORITES:
|
||||
return SearchFilter(isFavorite: true)
|
||||
|
||||
// regular album
|
||||
default:
|
||||
return SearchFilter(albumIds: [id])
|
||||
}
|
||||
}
|
||||
|
||||
var isVirtual: Bool {
|
||||
switch self {
|
||||
case Album.NONE, Album.FAVORITES:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: API
|
||||
@@ -86,7 +112,7 @@ class ImmichAPI {
|
||||
|
||||
init() async throws {
|
||||
// fetch the credentials from the UserDefaults store that dart placed here
|
||||
guard let defaults = UserDefaults(suiteName: "group.app.immich.share"),
|
||||
guard let defaults = UserDefaults(suiteName: IMMICH_SHARE_GROUP),
|
||||
let serverURL = defaults.string(forKey: "widget_server_url"),
|
||||
let sessionKey = defaults.string(forKey: "widget_auth_token")
|
||||
else {
|
||||
@@ -130,7 +156,8 @@ class ImmichAPI {
|
||||
return components?.url
|
||||
}
|
||||
|
||||
func fetchSearchResults(with filters: SearchFilters) async throws
|
||||
func fetchSearchResults(with filters: SearchFilter = Album.NONE.filter)
|
||||
async throws
|
||||
-> [Asset]
|
||||
{
|
||||
// get URL
|
||||
@@ -176,7 +203,7 @@ class ImmichAPI {
|
||||
return try JSONDecoder().decode([MemoryResult].self, from: data)
|
||||
}
|
||||
|
||||
func fetchImage(asset: Asset) async throws(WidgetError) -> UIImage {
|
||||
func fetchImage(asset: Asset) async throws(FetchError) -> UIImage {
|
||||
let thumbnailParams = [URLQueryItem(name: "size", value: "preview")]
|
||||
let assetEndpoint = "/assets/" + asset.id + "/thumbnail"
|
||||
|
||||
@@ -189,18 +216,25 @@ class ImmichAPI {
|
||||
else {
|
||||
throw .invalidURL
|
||||
}
|
||||
|
||||
guard let imageSource = CGImageSourceCreateWithURL(fetchURL as CFURL, nil) else {
|
||||
|
||||
guard let imageSource = CGImageSourceCreateWithURL(fetchURL as CFURL, nil)
|
||||
else {
|
||||
throw .invalidURL
|
||||
}
|
||||
|
||||
let decodeOptions: [NSString: Any] = [
|
||||
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
||||
kCGImageSourceThumbnailMaxPixelSize: 400,
|
||||
kCGImageSourceCreateThumbnailWithTransform: true
|
||||
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
||||
kCGImageSourceThumbnailMaxPixelSize: 512,
|
||||
kCGImageSourceCreateThumbnailWithTransform: true,
|
||||
]
|
||||
|
||||
guard let thumbnail = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, decodeOptions as CFDictionary) else {
|
||||
|
||||
guard
|
||||
let thumbnail = CGImageSourceCreateThumbnailAtIndex(
|
||||
imageSource,
|
||||
0,
|
||||
decodeOptions as CFDictionary
|
||||
)
|
||||
else {
|
||||
throw .fetchFailed
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
|
||||
@@ -7,14 +7,17 @@
|
||||
import UIKit
|
||||
|
||||
extension UIImage {
|
||||
/// Crops the image to ensure width and height do not exceed maxSize.
|
||||
/// Keeps original aspect ratio and crops excess equally from edges (center crop).
|
||||
func resized(toWidth width: CGFloat, isOpaque: Bool = true) -> UIImage? {
|
||||
let canvas = CGSize(width: width, height: CGFloat(ceil(width/size.width * size.height)))
|
||||
let format = imageRendererFormat
|
||||
format.opaque = isOpaque
|
||||
return UIGraphicsImageRenderer(size: canvas, format: format).image {
|
||||
_ in draw(in: CGRect(origin: .zero, size: canvas))
|
||||
}
|
||||
/// Crops the image to ensure width and height do not exceed maxSize.
|
||||
/// Keeps original aspect ratio and crops excess equally from edges (center crop).
|
||||
func resized(toWidth width: CGFloat, isOpaque: Bool = true) -> UIImage? {
|
||||
let canvas = CGSize(
|
||||
width: width,
|
||||
height: CGFloat(ceil(width / size.width * size.height))
|
||||
)
|
||||
let format = imageRendererFormat
|
||||
format.opaque = isOpaque
|
||||
return UIGraphicsImageRenderer(size: canvas, format: format).image {
|
||||
_ in draw(in: CGRect(origin: .zero, size: canvas))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,28 +19,31 @@ struct ImmichMemoryProvider: TimelineProvider {
|
||||
in context: Context,
|
||||
completion: @escaping @Sendable (ImageEntry) -> Void
|
||||
) {
|
||||
let cacheKey = "memory_\(context.family.rawValue)"
|
||||
|
||||
Task {
|
||||
guard let api = try? await ImmichAPI() else {
|
||||
completion(ImageEntry(date: Date(), image: nil, error: .noLogin))
|
||||
completion(
|
||||
ImageEntry.handleError(for: cacheKey, error: .noLogin).entries.first!
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
guard let memories = try? await api.fetchMemory(for: Date.now)
|
||||
else {
|
||||
completion(ImageEntry(date: Date(), image: nil, error: .fetchFailed))
|
||||
completion(ImageEntry.handleError(for: cacheKey).entries.first!)
|
||||
return
|
||||
}
|
||||
|
||||
for memory in memories {
|
||||
if let asset = memory.assets.first(where: { $0.type == .image }),
|
||||
var entry = try? await buildEntry(
|
||||
let entry = try? await ImageEntry.build(
|
||||
api: api,
|
||||
asset: asset,
|
||||
dateOffset: 0,
|
||||
subtitle: getYearDifferenceSubtitle(assetYear: memory.data.year)
|
||||
)
|
||||
{
|
||||
entry.resize()
|
||||
completion(entry)
|
||||
return
|
||||
}
|
||||
@@ -48,26 +51,17 @@ struct ImmichMemoryProvider: TimelineProvider {
|
||||
|
||||
// fallback to random image
|
||||
guard
|
||||
let randomImage = try? await api.fetchSearchResults(
|
||||
with: SearchFilters(size: 1)
|
||||
).first
|
||||
else {
|
||||
completion(ImageEntry(date: Date(), image: nil, error: .fetchFailed))
|
||||
return
|
||||
}
|
||||
|
||||
guard
|
||||
var imageEntry = try? await buildEntry(
|
||||
let randomImage = try? await api.fetchSearchResults().first,
|
||||
let imageEntry = try? await ImageEntry.build(
|
||||
api: api,
|
||||
asset: randomImage,
|
||||
dateOffset: 0
|
||||
)
|
||||
else {
|
||||
completion(ImageEntry(date: Date(), image: nil, error: .fetchFailed))
|
||||
completion(ImageEntry.handleError(for: cacheKey).entries.first!)
|
||||
return
|
||||
}
|
||||
|
||||
imageEntry.resize()
|
||||
completion(imageEntry)
|
||||
}
|
||||
}
|
||||
@@ -80,9 +74,12 @@ struct ImmichMemoryProvider: TimelineProvider {
|
||||
var entries: [ImageEntry] = []
|
||||
let now = Date()
|
||||
|
||||
let cacheKey = "memory_\(context.family.rawValue)"
|
||||
|
||||
guard let api = try? await ImmichAPI() else {
|
||||
entries.append(ImageEntry(date: now, image: nil, error: .noLogin))
|
||||
completion(Timeline(entries: entries, policy: .atEnd))
|
||||
completion(
|
||||
ImageEntry.handleError(for: cacheKey, error: .noLogin)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -95,7 +92,7 @@ struct ImmichMemoryProvider: TimelineProvider {
|
||||
for asset in memory.assets {
|
||||
if asset.type == .image && totalAssets < 12 {
|
||||
group.addTask {
|
||||
try? await buildEntry(
|
||||
try? await ImageEntry.build(
|
||||
api: api,
|
||||
asset: asset,
|
||||
dateOffset: totalAssets,
|
||||
@@ -120,25 +117,32 @@ struct ImmichMemoryProvider: TimelineProvider {
|
||||
// If we didnt add any memory images (some failure occured or no images in memory),
|
||||
// default to 12 hours of random photos
|
||||
if entries.count == 0 {
|
||||
entries.append(
|
||||
contentsOf: (try? await generateRandomEntries(
|
||||
// this must be a do/catch since we need to
|
||||
// distinguish between a network fail and an empty search
|
||||
do {
|
||||
let search = try await generateRandomEntries(
|
||||
api: api,
|
||||
now: now,
|
||||
count: 12
|
||||
)) ?? []
|
||||
)
|
||||
)
|
||||
|
||||
// Load or save a cached asset for when network conditions are bad
|
||||
if search.count == 0 {
|
||||
completion(
|
||||
ImageEntry.handleError(for: cacheKey, error: .noAssetsAvailable)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
entries.append(contentsOf: search)
|
||||
} catch {
|
||||
completion(ImageEntry.handleError(for: cacheKey))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// If we fail to fetch images, we still want to add an entry
|
||||
// with a nil image and an error
|
||||
if entries.count == 0 {
|
||||
entries.append(ImageEntry(date: now, image: nil, error: .fetchFailed))
|
||||
}
|
||||
|
||||
// Resize all images to something that can be stored by iOS
|
||||
for i in entries.indices {
|
||||
entries[i].resize()
|
||||
}
|
||||
// cache the last image
|
||||
try? entries.last!.cache(for: cacheKey)
|
||||
|
||||
completion(Timeline(entries: entries, policy: .atEnd))
|
||||
}
|
||||
|
||||
@@ -8,20 +8,21 @@ extension Album: @unchecked Sendable, AppEntity, Identifiable {
|
||||
|
||||
struct AlbumQuery: EntityQuery {
|
||||
func entities(for identifiers: [Album.ID]) async throws -> [Album] {
|
||||
// use cached albums to search
|
||||
var albums = (try? await AlbumCache.shared.getAlbums()) ?? []
|
||||
albums.insert(NO_ALBUM, at: 0)
|
||||
|
||||
return albums.filter {
|
||||
return await suggestedEntities().filter {
|
||||
identifiers.contains($0.id)
|
||||
}
|
||||
}
|
||||
|
||||
func suggestedEntities() async throws -> [Album] {
|
||||
var albums = (try? await AlbumCache.shared.getAlbums(refresh: true)) ?? []
|
||||
albums.insert(NO_ALBUM, at: 0)
|
||||
func suggestedEntities() async -> [Album] {
|
||||
let albums = (try? await AlbumCache.shared.getAlbums()) ?? []
|
||||
|
||||
return albums
|
||||
let options =
|
||||
[
|
||||
NONE,
|
||||
FAVORITES,
|
||||
] + albums
|
||||
|
||||
return options
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,8 +36,6 @@ extension Album: @unchecked Sendable, AppEntity, Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
let NO_ALBUM = Album(id: "NONE", albumName: "None")
|
||||
|
||||
struct RandomConfigurationAppIntent: WidgetConfigurationIntent {
|
||||
static var title: LocalizedStringResource { "Select Album" }
|
||||
static var description: IntentDescription {
|
||||
@@ -45,7 +44,7 @@ struct RandomConfigurationAppIntent: WidgetConfigurationIntent {
|
||||
|
||||
@Parameter(title: "Album")
|
||||
var album: Album?
|
||||
|
||||
|
||||
@Parameter(title: "Show Album Name", default: false)
|
||||
var showAlbumName: Bool
|
||||
}
|
||||
@@ -54,7 +53,7 @@ struct RandomConfigurationAppIntent: WidgetConfigurationIntent {
|
||||
|
||||
struct ImmichRandomProvider: AppIntentTimelineProvider {
|
||||
func placeholder(in context: Context) -> ImageEntry {
|
||||
ImageEntry(date: Date(), image: nil)
|
||||
ImageEntry(date: Date())
|
||||
}
|
||||
|
||||
func snapshot(
|
||||
@@ -63,30 +62,26 @@ struct ImmichRandomProvider: AppIntentTimelineProvider {
|
||||
) async
|
||||
-> ImageEntry
|
||||
{
|
||||
let cacheKey = "random_none_\(context.family.rawValue)"
|
||||
|
||||
guard let api = try? await ImmichAPI() else {
|
||||
return ImageEntry(date: Date(), image: nil, error: .noLogin)
|
||||
return ImageEntry.handleError(for: cacheKey, error: .noLogin).entries
|
||||
.first!
|
||||
}
|
||||
|
||||
guard
|
||||
let randomImage = try? await api.fetchSearchResults(
|
||||
with: SearchFilters(size: 1)
|
||||
).first
|
||||
else {
|
||||
return ImageEntry(date: Date(), image: nil, error: .fetchFailed)
|
||||
}
|
||||
|
||||
guard
|
||||
var entry = try? await buildEntry(
|
||||
with: Album.NONE.filter
|
||||
).first,
|
||||
let entry = try? await ImageEntry.build(
|
||||
api: api,
|
||||
asset: randomImage,
|
||||
dateOffset: 0
|
||||
)
|
||||
else {
|
||||
return ImageEntry(date: Date(), image: nil, error: .fetchFailed)
|
||||
return ImageEntry.handleError(for: cacheKey).entries.first!
|
||||
}
|
||||
|
||||
entry.resize()
|
||||
|
||||
return entry
|
||||
}
|
||||
|
||||
@@ -99,50 +94,41 @@ struct ImmichRandomProvider: AppIntentTimelineProvider {
|
||||
var entries: [ImageEntry] = []
|
||||
let now = Date()
|
||||
|
||||
// nil if album is NONE or nil
|
||||
let album = configuration.album ?? Album.NONE
|
||||
let albumName = album.isVirtual ? nil : album.albumName
|
||||
|
||||
let cacheKey = "random_\(album.id)_\(context.family.rawValue)"
|
||||
|
||||
// If we don't have a server config, return an entry with an error
|
||||
guard let api = try? await ImmichAPI() else {
|
||||
entries.append(ImageEntry(date: now, image: nil, error: .noLogin))
|
||||
return Timeline(entries: entries, policy: .atEnd)
|
||||
return ImageEntry.handleError(for: cacheKey, error: .noLogin)
|
||||
}
|
||||
|
||||
// nil if album is NONE or nil
|
||||
let albumId =
|
||||
configuration.album?.id != "NONE" ? configuration.album?.id : nil
|
||||
var albumName: String? = albumId != nil ? configuration.album?.albumName : nil
|
||||
|
||||
if albumId != nil {
|
||||
// make sure the album exists on server, otherwise show error
|
||||
guard let albums = try? await api.fetchAlbums() else {
|
||||
entries.append(ImageEntry(date: now, image: nil, error: .fetchFailed))
|
||||
return Timeline(entries: entries, policy: .atEnd)
|
||||
}
|
||||
|
||||
if !albums.contains(where: { $0.id == albumId }) {
|
||||
entries.append(ImageEntry(date: now, image: nil, error: .albumNotFound))
|
||||
return Timeline(entries: entries, policy: .atEnd)
|
||||
}
|
||||
}
|
||||
|
||||
entries.append(
|
||||
contentsOf: (try? await generateRandomEntries(
|
||||
// build entries
|
||||
// this must be a do/catch since we need to
|
||||
// distinguish between a network fail and an empty search
|
||||
do {
|
||||
let search = try await generateRandomEntries(
|
||||
api: api,
|
||||
now: now,
|
||||
count: 12,
|
||||
albumId: albumId,
|
||||
filter: album.filter,
|
||||
subtitle: configuration.showAlbumName ? albumName : nil
|
||||
))
|
||||
?? []
|
||||
)
|
||||
)
|
||||
|
||||
// If we fail to fetch images, we still want to add an entry with a nil image and an error
|
||||
if entries.count == 0 {
|
||||
entries.append(ImageEntry(date: now, image: nil, error: .fetchFailed))
|
||||
// Load or save a cached asset for when network conditions are bad
|
||||
if search.count == 0 {
|
||||
return ImageEntry.handleError(for: cacheKey, error: .noAssetsAvailable)
|
||||
}
|
||||
|
||||
entries.append(contentsOf: search)
|
||||
} catch {
|
||||
return ImageEntry.handleError(for: cacheKey)
|
||||
}
|
||||
|
||||
// Resize all images to something that can be stored by iOS
|
||||
for i in entries.indices {
|
||||
entries[i].resize()
|
||||
}
|
||||
// cache the last image
|
||||
try? entries.last!.cache(for: cacheKey)
|
||||
|
||||
return Timeline(entries: entries, policy: .atEnd)
|
||||
}
|
||||
|
||||
@@ -12,3 +12,5 @@ enum TextSearchType {
|
||||
enum AssetVisibilityEnum { timeline, hidden, archive, locked }
|
||||
|
||||
enum SortUserBy { id }
|
||||
|
||||
enum ActionSource { timeline, viewer }
|
||||
|
||||
@@ -11,7 +11,7 @@ enum AlbumUserRole {
|
||||
}
|
||||
|
||||
// Model for an album stored in the server
|
||||
class Album {
|
||||
class RemoteAlbum {
|
||||
final String id;
|
||||
final String name;
|
||||
final String ownerId;
|
||||
@@ -21,8 +21,10 @@ class Album {
|
||||
final String? thumbnailAssetId;
|
||||
final bool isActivityEnabled;
|
||||
final AlbumAssetOrder order;
|
||||
final int assetCount;
|
||||
final String ownerName;
|
||||
|
||||
const Album({
|
||||
const RemoteAlbum({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.ownerId,
|
||||
@@ -32,26 +34,30 @@ class Album {
|
||||
this.thumbnailAssetId,
|
||||
required this.isActivityEnabled,
|
||||
required this.order,
|
||||
required this.assetCount,
|
||||
required this.ownerName,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''Album {
|
||||
id: $id,
|
||||
name: $name,
|
||||
ownerId: $ownerId,
|
||||
description: $description,
|
||||
createdAt: $createdAt,
|
||||
updatedAt: $updatedAt,
|
||||
isActivityEnabled: $isActivityEnabled,
|
||||
order: $order,
|
||||
thumbnailAssetId: ${thumbnailAssetId ?? "<NA>"}
|
||||
id: $id,
|
||||
name: $name,
|
||||
ownerId: $ownerId,
|
||||
description: $description,
|
||||
createdAt: $createdAt,
|
||||
updatedAt: $updatedAt,
|
||||
isActivityEnabled: $isActivityEnabled,
|
||||
order: $order,
|
||||
thumbnailAssetId: ${thumbnailAssetId ?? "<NA>"}
|
||||
assetCount: $assetCount
|
||||
ownerName: $ownerName
|
||||
}''';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other is! Album) return false;
|
||||
if (other is! RemoteAlbum) return false;
|
||||
if (identical(this, other)) return true;
|
||||
return id == other.id &&
|
||||
name == other.name &&
|
||||
@@ -61,7 +67,9 @@ class Album {
|
||||
updatedAt == other.updatedAt &&
|
||||
thumbnailAssetId == other.thumbnailAssetId &&
|
||||
isActivityEnabled == other.isActivityEnabled &&
|
||||
order == other.order;
|
||||
order == other.order &&
|
||||
assetCount == other.assetCount &&
|
||||
ownerName == other.ownerName;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -74,6 +82,36 @@ class Album {
|
||||
updatedAt.hashCode ^
|
||||
thumbnailAssetId.hashCode ^
|
||||
isActivityEnabled.hashCode ^
|
||||
order.hashCode;
|
||||
order.hashCode ^
|
||||
assetCount.hashCode ^
|
||||
ownerName.hashCode;
|
||||
}
|
||||
|
||||
RemoteAlbum copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
String? ownerId,
|
||||
String? description,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
String? thumbnailAssetId,
|
||||
bool? isActivityEnabled,
|
||||
AlbumAssetOrder? order,
|
||||
int? assetCount,
|
||||
String? ownerName,
|
||||
}) {
|
||||
return RemoteAlbum(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
ownerId: ownerId ?? this.ownerId,
|
||||
description: description ?? this.description,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId,
|
||||
isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled,
|
||||
order: order ?? this.order,
|
||||
assetCount: assetCount ?? this.assetCount,
|
||||
ownerName: ownerName ?? this.ownerName,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
part of 'base_asset.model.dart';
|
||||
|
||||
enum AssetVisibility {
|
||||
timeline,
|
||||
hidden,
|
||||
archive,
|
||||
locked,
|
||||
}
|
||||
|
||||
// Model for an asset stored in the server
|
||||
class Asset extends BaseAsset {
|
||||
final String id;
|
||||
final String? localId;
|
||||
final String? thumbHash;
|
||||
final AssetVisibility visibility;
|
||||
|
||||
const Asset({
|
||||
required this.id,
|
||||
this.localId,
|
||||
required super.name,
|
||||
required super.checksum,
|
||||
required super.type,
|
||||
required super.createdAt,
|
||||
required super.updatedAt,
|
||||
super.width,
|
||||
super.height,
|
||||
super.durationInSeconds,
|
||||
super.isFavorite = false,
|
||||
this.thumbHash,
|
||||
this.visibility = AssetVisibility.timeline,
|
||||
});
|
||||
|
||||
@override
|
||||
AssetState get storage =>
|
||||
localId == null ? AssetState.remote : AssetState.merged;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''Asset {
|
||||
id: $id,
|
||||
name: $name,
|
||||
type: $type,
|
||||
createdAt: $createdAt,
|
||||
updatedAt: $updatedAt,
|
||||
width: ${width ?? "<NA>"},
|
||||
height: ${height ?? "<NA>"},
|
||||
durationInSeconds: ${durationInSeconds ?? "<NA>"},
|
||||
localId: ${localId ?? "<NA>"},
|
||||
isFavorite: $isFavorite,
|
||||
thumbHash: ${thumbHash ?? "<NA>"},
|
||||
visibility: $visibility,
|
||||
}''';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other is! Asset) return false;
|
||||
if (identical(this, other)) return true;
|
||||
return super == other &&
|
||||
id == other.id &&
|
||||
localId == other.localId &&
|
||||
thumbHash == other.thumbHash &&
|
||||
visibility == other.visibility;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
super.hashCode ^
|
||||
id.hashCode ^
|
||||
localId.hashCode ^
|
||||
thumbHash.hashCode ^
|
||||
visibility.hashCode;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
part 'asset.model.dart';
|
||||
part 'local_asset.model.dart';
|
||||
part 'remote_asset.model.dart';
|
||||
|
||||
enum AssetType {
|
||||
// do not change this order!
|
||||
@@ -25,6 +25,7 @@ sealed class BaseAsset {
|
||||
final int? height;
|
||||
final int? durationInSeconds;
|
||||
final bool isFavorite;
|
||||
final String? livePhotoVideoId;
|
||||
|
||||
const BaseAsset({
|
||||
required this.name,
|
||||
@@ -36,11 +37,32 @@ sealed class BaseAsset {
|
||||
this.height,
|
||||
this.durationInSeconds,
|
||||
this.isFavorite = false,
|
||||
this.livePhotoVideoId,
|
||||
});
|
||||
|
||||
bool get isImage => type == AssetType.image;
|
||||
bool get isVideo => type == AssetType.video;
|
||||
|
||||
bool get isMotionPhoto => livePhotoVideoId != null;
|
||||
|
||||
Duration get duration {
|
||||
final durationInSeconds = this.durationInSeconds;
|
||||
if (durationInSeconds != null) {
|
||||
return Duration(seconds: durationInSeconds);
|
||||
}
|
||||
return const Duration();
|
||||
}
|
||||
|
||||
bool get hasRemote =>
|
||||
storage == AssetState.remote || storage == AssetState.merged;
|
||||
bool get hasLocal =>
|
||||
storage == AssetState.local || storage == AssetState.merged;
|
||||
bool get isLocalOnly => storage == AssetState.local;
|
||||
bool get isRemoteOnly => storage == AssetState.remote;
|
||||
|
||||
// Overridden in subclasses
|
||||
AssetState get storage;
|
||||
String get heroTag;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
|
||||
@@ -3,6 +3,7 @@ part of 'base_asset.model.dart';
|
||||
class LocalAsset extends BaseAsset {
|
||||
final String id;
|
||||
final String? remoteId;
|
||||
final int orientation;
|
||||
|
||||
const LocalAsset({
|
||||
required this.id,
|
||||
@@ -16,12 +17,17 @@ class LocalAsset extends BaseAsset {
|
||||
super.height,
|
||||
super.durationInSeconds,
|
||||
super.isFavorite = false,
|
||||
super.livePhotoVideoId,
|
||||
this.orientation = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
AssetState get storage =>
|
||||
remoteId == null ? AssetState.local : AssetState.merged;
|
||||
|
||||
@override
|
||||
String get heroTag => '${id}_${remoteId ?? checksum}';
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''LocalAsset {
|
||||
@@ -35,6 +41,7 @@ class LocalAsset extends BaseAsset {
|
||||
durationInSeconds: ${durationInSeconds ?? "<NA>"},
|
||||
remoteId: ${remoteId ?? "<NA>"}
|
||||
isFavorite: $isFavorite,
|
||||
orientation: $orientation,
|
||||
}''';
|
||||
}
|
||||
|
||||
@@ -42,11 +49,12 @@ class LocalAsset extends BaseAsset {
|
||||
bool operator ==(Object other) {
|
||||
if (other is! LocalAsset) return false;
|
||||
if (identical(this, other)) return true;
|
||||
return super == other && id == other.id && remoteId == other.remoteId;
|
||||
return super == other && id == other.id && orientation == other.orientation;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => super.hashCode ^ id.hashCode ^ remoteId.hashCode;
|
||||
int get hashCode =>
|
||||
super.hashCode ^ id.hashCode ^ remoteId.hashCode ^ orientation.hashCode;
|
||||
|
||||
LocalAsset copyWith({
|
||||
String? id,
|
||||
@@ -60,6 +68,7 @@ class LocalAsset extends BaseAsset {
|
||||
int? height,
|
||||
int? durationInSeconds,
|
||||
bool? isFavorite,
|
||||
int? orientation,
|
||||
}) {
|
||||
return LocalAsset(
|
||||
id: id ?? this.id,
|
||||
@@ -73,6 +82,7 @@ class LocalAsset extends BaseAsset {
|
||||
height: height ?? this.height,
|
||||
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
orientation: orientation ?? this.orientation,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
117
mobile/lib/domain/models/asset/remote_asset.model.dart
Normal file
117
mobile/lib/domain/models/asset/remote_asset.model.dart
Normal file
@@ -0,0 +1,117 @@
|
||||
part of 'base_asset.model.dart';
|
||||
|
||||
enum AssetVisibility {
|
||||
timeline,
|
||||
hidden,
|
||||
archive,
|
||||
locked,
|
||||
}
|
||||
|
||||
// Model for an asset stored in the server
|
||||
class RemoteAsset extends BaseAsset {
|
||||
final String id;
|
||||
final String? localId;
|
||||
final String? thumbHash;
|
||||
final AssetVisibility visibility;
|
||||
final String ownerId;
|
||||
|
||||
const RemoteAsset({
|
||||
required this.id,
|
||||
this.localId,
|
||||
required super.name,
|
||||
required this.ownerId,
|
||||
required super.checksum,
|
||||
required super.type,
|
||||
required super.createdAt,
|
||||
required super.updatedAt,
|
||||
super.width,
|
||||
super.height,
|
||||
super.durationInSeconds,
|
||||
super.isFavorite = false,
|
||||
this.thumbHash,
|
||||
this.visibility = AssetVisibility.timeline,
|
||||
super.livePhotoVideoId,
|
||||
});
|
||||
|
||||
@override
|
||||
AssetState get storage =>
|
||||
localId == null ? AssetState.remote : AssetState.merged;
|
||||
|
||||
@override
|
||||
String get heroTag => '${localId ?? checksum}_$id';
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''Asset {
|
||||
id: $id,
|
||||
name: $name,
|
||||
ownerId: $ownerId,
|
||||
type: $type,
|
||||
createdAt: $createdAt,
|
||||
updatedAt: $updatedAt,
|
||||
width: ${width ?? "<NA>"},
|
||||
height: ${height ?? "<NA>"},
|
||||
durationInSeconds: ${durationInSeconds ?? "<NA>"},
|
||||
localId: ${localId ?? "<NA>"},
|
||||
isFavorite: $isFavorite,
|
||||
thumbHash: ${thumbHash ?? "<NA>"},
|
||||
visibility: $visibility,
|
||||
}''';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other is! RemoteAsset) return false;
|
||||
if (identical(this, other)) return true;
|
||||
return super == other &&
|
||||
id == other.id &&
|
||||
ownerId == other.ownerId &&
|
||||
thumbHash == other.thumbHash &&
|
||||
visibility == other.visibility;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
super.hashCode ^
|
||||
id.hashCode ^
|
||||
ownerId.hashCode ^
|
||||
localId.hashCode ^
|
||||
thumbHash.hashCode ^
|
||||
visibility.hashCode;
|
||||
|
||||
RemoteAsset copyWith({
|
||||
String? id,
|
||||
String? localId,
|
||||
String? name,
|
||||
String? ownerId,
|
||||
String? checksum,
|
||||
AssetType? type,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
int? width,
|
||||
int? height,
|
||||
int? durationInSeconds,
|
||||
bool? isFavorite,
|
||||
String? thumbHash,
|
||||
AssetVisibility? visibility,
|
||||
String? livePhotoVideoId,
|
||||
}) {
|
||||
return RemoteAsset(
|
||||
id: id ?? this.id,
|
||||
localId: localId ?? this.localId,
|
||||
name: name ?? this.name,
|
||||
ownerId: ownerId ?? this.ownerId,
|
||||
checksum: checksum ?? this.checksum,
|
||||
type: type ?? this.type,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
width: width ?? this.width,
|
||||
height: height ?? this.height,
|
||||
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
thumbHash: thumbHash ?? this.thumbHash,
|
||||
visibility: visibility ?? this.visibility,
|
||||
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ class ExifInfo {
|
||||
final int? fileSize;
|
||||
final String? description;
|
||||
final bool isFlipped;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final String? orientation;
|
||||
final String? timeZone;
|
||||
final DateTime? dateTimeOriginal;
|
||||
@@ -45,6 +47,8 @@ class ExifInfo {
|
||||
this.fileSize,
|
||||
this.description,
|
||||
this.orientation,
|
||||
this.width,
|
||||
this.height,
|
||||
this.timeZone,
|
||||
this.dateTimeOriginal,
|
||||
this.isFlipped = false,
|
||||
@@ -68,6 +72,9 @@ class ExifInfo {
|
||||
|
||||
return other.fileSize == fileSize &&
|
||||
other.description == description &&
|
||||
other.isFlipped == isFlipped &&
|
||||
other.width == width &&
|
||||
other.height == height &&
|
||||
other.orientation == orientation &&
|
||||
other.timeZone == timeZone &&
|
||||
other.dateTimeOriginal == dateTimeOriginal &&
|
||||
@@ -91,6 +98,9 @@ class ExifInfo {
|
||||
return fileSize.hashCode ^
|
||||
description.hashCode ^
|
||||
orientation.hashCode ^
|
||||
isFlipped.hashCode ^
|
||||
width.hashCode ^
|
||||
height.hashCode ^
|
||||
timeZone.hashCode ^
|
||||
dateTimeOriginal.hashCode ^
|
||||
latitude.hashCode ^
|
||||
@@ -114,6 +124,9 @@ class ExifInfo {
|
||||
fileSize: ${fileSize ?? 'NA'},
|
||||
description: ${description ?? 'NA'},
|
||||
orientation: ${orientation ?? 'NA'},
|
||||
width: ${width ?? 'NA'},
|
||||
height: ${height ?? 'NA'},
|
||||
isFlipped: $isFlipped,
|
||||
timeZone: ${timeZone ?? 'NA'},
|
||||
dateTimeOriginal: ${dateTimeOriginal ?? 'NA'},
|
||||
latitude: ${latitude ?? 'NA'},
|
||||
|
||||
166
mobile/lib/domain/models/memory.model.dart
Normal file
166
mobile/lib/domain/models/memory.model.dart
Normal file
@@ -0,0 +1,166 @@
|
||||
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
|
||||
enum MemoryTypeEnum {
|
||||
// do not change this order!
|
||||
onThisDay,
|
||||
}
|
||||
|
||||
class MemoryData {
|
||||
final int year;
|
||||
|
||||
const MemoryData({
|
||||
required this.year,
|
||||
});
|
||||
|
||||
MemoryData copyWith({
|
||||
int? year,
|
||||
}) {
|
||||
return MemoryData(
|
||||
year: year ?? this.year,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'year': year,
|
||||
};
|
||||
}
|
||||
|
||||
factory MemoryData.fromMap(Map<String, dynamic> map) {
|
||||
return MemoryData(
|
||||
year: map['year'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory MemoryData.fromJson(String source) =>
|
||||
MemoryData.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||
|
||||
@override
|
||||
String toString() => 'MemoryData(year: $year)';
|
||||
|
||||
@override
|
||||
bool operator ==(covariant MemoryData other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.year == year;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => year.hashCode;
|
||||
}
|
||||
|
||||
// Model for a memory stored in the server
|
||||
class DriftMemory {
|
||||
final String id;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final DateTime? deletedAt;
|
||||
final String ownerId;
|
||||
|
||||
// enum
|
||||
final MemoryTypeEnum type;
|
||||
final MemoryData data;
|
||||
final bool isSaved;
|
||||
final DateTime memoryAt;
|
||||
final DateTime? seenAt;
|
||||
final DateTime? showAt;
|
||||
final DateTime? hideAt;
|
||||
final List<RemoteAsset> assets;
|
||||
|
||||
const DriftMemory({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.deletedAt,
|
||||
required this.ownerId,
|
||||
required this.type,
|
||||
required this.data,
|
||||
required this.isSaved,
|
||||
required this.memoryAt,
|
||||
this.seenAt,
|
||||
this.showAt,
|
||||
this.hideAt,
|
||||
required this.assets,
|
||||
});
|
||||
|
||||
DriftMemory copyWith({
|
||||
String? id,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String? ownerId,
|
||||
MemoryTypeEnum? type,
|
||||
MemoryData? data,
|
||||
bool? isSaved,
|
||||
DateTime? memoryAt,
|
||||
DateTime? seenAt,
|
||||
DateTime? showAt,
|
||||
DateTime? hideAt,
|
||||
List<RemoteAsset>? assets,
|
||||
}) {
|
||||
return DriftMemory(
|
||||
id: id ?? this.id,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
deletedAt: deletedAt ?? this.deletedAt,
|
||||
ownerId: ownerId ?? this.ownerId,
|
||||
type: type ?? this.type,
|
||||
data: data ?? this.data,
|
||||
isSaved: isSaved ?? this.isSaved,
|
||||
memoryAt: memoryAt ?? this.memoryAt,
|
||||
seenAt: seenAt ?? this.seenAt,
|
||||
showAt: showAt ?? this.showAt,
|
||||
hideAt: hideAt ?? this.hideAt,
|
||||
assets: assets ?? this.assets,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Memory(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, ownerId: $ownerId, type: $type, data: $data, isSaved: $isSaved, memoryAt: $memoryAt, seenAt: $seenAt, showAt: $showAt, hideAt: $hideAt, assets: $assets)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(covariant DriftMemory other) {
|
||||
if (identical(this, other)) return true;
|
||||
final listEquals = const DeepCollectionEquality().equals;
|
||||
|
||||
return other.id == id &&
|
||||
other.createdAt == createdAt &&
|
||||
other.updatedAt == updatedAt &&
|
||||
other.deletedAt == deletedAt &&
|
||||
other.ownerId == ownerId &&
|
||||
other.type == type &&
|
||||
other.data == data &&
|
||||
other.isSaved == isSaved &&
|
||||
other.memoryAt == memoryAt &&
|
||||
other.seenAt == seenAt &&
|
||||
other.showAt == showAt &&
|
||||
other.hideAt == hideAt &&
|
||||
listEquals(other.assets, assets);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return id.hashCode ^
|
||||
createdAt.hashCode ^
|
||||
updatedAt.hashCode ^
|
||||
deletedAt.hashCode ^
|
||||
ownerId.hashCode ^
|
||||
type.hashCode ^
|
||||
data.hashCode ^
|
||||
isSaved.hashCode ^
|
||||
memoryAt.hashCode ^
|
||||
seenAt.hashCode ^
|
||||
showAt.hashCode ^
|
||||
hideAt.hashCode ^
|
||||
assets.hashCode;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,12 @@ import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
enum Setting<T> {
|
||||
tilesPerRow<int>(StoreKey.tilesPerRow, 4),
|
||||
groupAssetsBy<int>(StoreKey.groupAssetsBy, 0),
|
||||
showStorageIndicator<bool>(StoreKey.storageIndicator, true);
|
||||
showStorageIndicator<bool>(StoreKey.storageIndicator, true),
|
||||
loadOriginal<bool>(StoreKey.loadOriginal, false),
|
||||
loadOriginalVideo<bool>(StoreKey.loadOriginalVideo, false),
|
||||
preferRemoteImage<bool>(StoreKey.preferRemoteImage, false),
|
||||
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, false),
|
||||
;
|
||||
|
||||
const Setting(this.storeKey, this.defaultValue);
|
||||
|
||||
|
||||
84
mobile/lib/domain/models/stack.model.dart
Normal file
84
mobile/lib/domain/models/stack.model.dart
Normal file
@@ -0,0 +1,84 @@
|
||||
import 'dart:convert';
|
||||
|
||||
// Model for a stack stored in the server
|
||||
class Stack {
|
||||
final String id;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final String ownerId;
|
||||
final String primaryAssetId;
|
||||
|
||||
const Stack({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.ownerId,
|
||||
required this.primaryAssetId,
|
||||
});
|
||||
|
||||
Stack copyWith({
|
||||
String? id,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
String? ownerId,
|
||||
String? primaryAssetId,
|
||||
}) {
|
||||
return Stack(
|
||||
id: id ?? this.id,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
ownerId: ownerId ?? this.ownerId,
|
||||
primaryAssetId: primaryAssetId ?? this.primaryAssetId,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'id': id,
|
||||
'createdAt': createdAt.millisecondsSinceEpoch,
|
||||
'updatedAt': updatedAt.millisecondsSinceEpoch,
|
||||
'ownerId': ownerId,
|
||||
'primaryAssetId': primaryAssetId,
|
||||
};
|
||||
}
|
||||
|
||||
factory Stack.fromMap(Map<String, dynamic> map) {
|
||||
return Stack(
|
||||
id: map['id'] as String,
|
||||
createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt'] as int),
|
||||
updatedAt: DateTime.fromMillisecondsSinceEpoch(map['updatedAt'] as int),
|
||||
ownerId: map['ownerId'] as String,
|
||||
primaryAssetId: map['primaryAssetId'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory Stack.fromJson(String source) =>
|
||||
Stack.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Stack(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, ownerId: $ownerId, primaryAssetId: $primaryAssetId)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(covariant Stack other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.id == id &&
|
||||
other.createdAt == createdAt &&
|
||||
other.updatedAt == updatedAt &&
|
||||
other.ownerId == ownerId &&
|
||||
other.primaryAssetId == primaryAssetId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return id.hashCode ^
|
||||
createdAt.hashCode ^
|
||||
updatedAt.hashCode ^
|
||||
ownerId.hashCode ^
|
||||
primaryAssetId.hashCode;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
import 'dart:ui';
|
||||
|
||||
enum UserMetadataKey {
|
||||
// do not change this order!
|
||||
onboarding,
|
||||
preferences,
|
||||
license,
|
||||
}
|
||||
|
||||
enum AvatarColor {
|
||||
// do not change this order or reuse indices for other purposes, adding is OK
|
||||
primary("primary"),
|
||||
@@ -31,7 +38,45 @@ enum AvatarColor {
|
||||
};
|
||||
}
|
||||
|
||||
class UserPreferences {
|
||||
class Onboarding {
|
||||
final bool isOnboarded;
|
||||
|
||||
const Onboarding({required this.isOnboarded});
|
||||
|
||||
Onboarding copyWith({bool? isOnboarded}) {
|
||||
return Onboarding(isOnboarded: isOnboarded ?? this.isOnboarded);
|
||||
}
|
||||
|
||||
Map<String, Object?> toMap() {
|
||||
final onboarding = <String, Object?>{};
|
||||
onboarding["isOnboarded"] = isOnboarded;
|
||||
return onboarding;
|
||||
}
|
||||
|
||||
factory Onboarding.fromMap(Map<String, Object?> map) {
|
||||
return Onboarding(isOnboarded: map["isOnboarded"] as bool);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''Onboarding {
|
||||
isOnboarded: $isOnboarded,
|
||||
}''';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(covariant Onboarding other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return isOnboarded == other.isOnboarded;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => isOnboarded.hashCode;
|
||||
}
|
||||
|
||||
// TODO: wait to be overwritten
|
||||
class Preferences {
|
||||
final bool foldersEnabled;
|
||||
final bool memoriesEnabled;
|
||||
final bool peopleEnabled;
|
||||
@@ -41,7 +86,7 @@ class UserPreferences {
|
||||
final AvatarColor userAvatarColor;
|
||||
final bool showSupportBadge;
|
||||
|
||||
const UserPreferences({
|
||||
const Preferences({
|
||||
this.foldersEnabled = false,
|
||||
this.memoriesEnabled = true,
|
||||
this.peopleEnabled = true,
|
||||
@@ -52,7 +97,7 @@ class UserPreferences {
|
||||
this.showSupportBadge = true,
|
||||
});
|
||||
|
||||
UserPreferences copyWith({
|
||||
Preferences copyWith({
|
||||
bool? foldersEnabled,
|
||||
bool? memoriesEnabled,
|
||||
bool? peopleEnabled,
|
||||
@@ -62,7 +107,7 @@ class UserPreferences {
|
||||
AvatarColor? userAvatarColor,
|
||||
bool? showSupportBadge,
|
||||
}) {
|
||||
return UserPreferences(
|
||||
return Preferences(
|
||||
foldersEnabled: foldersEnabled ?? this.foldersEnabled,
|
||||
memoriesEnabled: memoriesEnabled ?? this.memoriesEnabled,
|
||||
peopleEnabled: peopleEnabled ?? this.peopleEnabled,
|
||||
@@ -87,8 +132,8 @@ class UserPreferences {
|
||||
return preferences;
|
||||
}
|
||||
|
||||
factory UserPreferences.fromMap(Map<String, Object?> map) {
|
||||
return UserPreferences(
|
||||
factory Preferences.fromMap(Map<String, Object?> map) {
|
||||
return Preferences(
|
||||
foldersEnabled: map["folders-Enabled"] as bool? ?? false,
|
||||
memoriesEnabled: map["memories-Enabled"] as bool? ?? true,
|
||||
peopleEnabled: map["people-Enabled"] as bool? ?? true,
|
||||
@@ -102,4 +147,173 @@ class UserPreferences {
|
||||
showSupportBadge: map["purchase-ShowSupportBadge"] as bool? ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''Preferences: {
|
||||
foldersEnabled: $foldersEnabled,
|
||||
memoriesEnabled: $memoriesEnabled,
|
||||
peopleEnabled: $peopleEnabled,
|
||||
ratingsEnabled: $ratingsEnabled,
|
||||
sharedLinksEnabled: $sharedLinksEnabled,
|
||||
tagsEnabled: $tagsEnabled,
|
||||
userAvatarColor: $userAvatarColor,
|
||||
showSupportBadge: $showSupportBadge,
|
||||
}''';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(covariant Preferences other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.foldersEnabled == foldersEnabled &&
|
||||
other.memoriesEnabled == memoriesEnabled &&
|
||||
other.peopleEnabled == peopleEnabled &&
|
||||
other.ratingsEnabled == ratingsEnabled &&
|
||||
other.sharedLinksEnabled == sharedLinksEnabled &&
|
||||
other.tagsEnabled == tagsEnabled &&
|
||||
other.userAvatarColor == userAvatarColor &&
|
||||
other.showSupportBadge == showSupportBadge;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return foldersEnabled.hashCode ^
|
||||
memoriesEnabled.hashCode ^
|
||||
peopleEnabled.hashCode ^
|
||||
ratingsEnabled.hashCode ^
|
||||
sharedLinksEnabled.hashCode ^
|
||||
tagsEnabled.hashCode ^
|
||||
userAvatarColor.hashCode ^
|
||||
showSupportBadge.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
class License {
|
||||
final DateTime activatedAt;
|
||||
final String activationKey;
|
||||
final String licenseKey;
|
||||
|
||||
const License({
|
||||
required this.activatedAt,
|
||||
required this.activationKey,
|
||||
required this.licenseKey,
|
||||
});
|
||||
|
||||
License copyWith({
|
||||
DateTime? activatedAt,
|
||||
String? activationKey,
|
||||
String? licenseKey,
|
||||
}) {
|
||||
return License(
|
||||
activatedAt: activatedAt ?? this.activatedAt,
|
||||
activationKey: activationKey ?? this.activationKey,
|
||||
licenseKey: licenseKey ?? this.licenseKey,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, Object?> toMap() {
|
||||
final license = <String, Object?>{};
|
||||
license["activatedAt"] = activatedAt;
|
||||
license["activationKey"] = activationKey;
|
||||
license["licenseKey"] = licenseKey;
|
||||
return license;
|
||||
}
|
||||
|
||||
factory License.fromMap(Map<String, Object?> map) {
|
||||
return License(
|
||||
activatedAt: map["activatedAt"] as DateTime,
|
||||
activationKey: map["activationKey"] as String,
|
||||
licenseKey: map["licenseKey"] as String,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''License {
|
||||
activatedAt: $activatedAt,
|
||||
activationKey: $activationKey,
|
||||
licenseKey: $licenseKey,
|
||||
}''';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(covariant License other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return activatedAt == other.activatedAt &&
|
||||
activationKey == other.activationKey &&
|
||||
licenseKey == other.licenseKey;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
activatedAt.hashCode ^ activationKey.hashCode ^ licenseKey.hashCode;
|
||||
}
|
||||
|
||||
// Model for a user metadata stored in the server
|
||||
class UserMetadata {
|
||||
final String userId;
|
||||
final UserMetadataKey key;
|
||||
final Onboarding? onboarding;
|
||||
final Preferences? preferences;
|
||||
final License? license;
|
||||
|
||||
const UserMetadata({
|
||||
required this.userId,
|
||||
required this.key,
|
||||
this.onboarding,
|
||||
this.preferences,
|
||||
this.license,
|
||||
}) : assert(
|
||||
onboarding != null || preferences != null || license != null,
|
||||
'One of onboarding, preferences and license must be provided',
|
||||
);
|
||||
|
||||
UserMetadata copyWith({
|
||||
String? userId,
|
||||
UserMetadataKey? key,
|
||||
Onboarding? onboarding,
|
||||
Preferences? preferences,
|
||||
License? license,
|
||||
}) {
|
||||
return UserMetadata(
|
||||
userId: userId ?? this.userId,
|
||||
key: key ?? this.key,
|
||||
onboarding: onboarding ?? this.onboarding,
|
||||
preferences: preferences ?? this.preferences,
|
||||
license: license ?? this.license,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''UserMetadata: {
|
||||
userId: $userId,
|
||||
key: $key,
|
||||
onboarding: ${onboarding ?? "<NA>"},
|
||||
preferences: ${preferences ?? "<NA>"},
|
||||
license: ${license ?? "<NA>"},
|
||||
}''';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(covariant UserMetadata other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.userId == userId &&
|
||||
other.key == key &&
|
||||
other.onboarding == onboarding &&
|
||||
other.preferences == preferences &&
|
||||
other.license == license;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return userId.hashCode ^
|
||||
key.hashCode ^
|
||||
onboarding.hashCode ^
|
||||
preferences.hashCode ^
|
||||
license.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
68
mobile/lib/domain/services/asset.service.dart
Normal file
68
mobile/lib/domain/services/asset.service.dart
Normal file
@@ -0,0 +1,68 @@
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
|
||||
import 'package:platform/platform.dart';
|
||||
|
||||
class AssetService {
|
||||
final RemoteAssetRepository _remoteAssetRepository;
|
||||
final DriftLocalAssetRepository _localAssetRepository;
|
||||
final Platform _platform;
|
||||
|
||||
const AssetService({
|
||||
required RemoteAssetRepository remoteAssetRepository,
|
||||
required DriftLocalAssetRepository localAssetRepository,
|
||||
}) : _remoteAssetRepository = remoteAssetRepository,
|
||||
_localAssetRepository = localAssetRepository,
|
||||
_platform = const LocalPlatform();
|
||||
|
||||
Stream<BaseAsset?> watchAsset(BaseAsset asset) {
|
||||
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).id;
|
||||
return asset is LocalAsset
|
||||
? _localAssetRepository.watchAsset(id)
|
||||
: _remoteAssetRepository.watchAsset(id);
|
||||
}
|
||||
|
||||
Future<ExifInfo?> getExif(BaseAsset asset) async {
|
||||
if (!asset.hasRemote) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final id =
|
||||
asset is LocalAsset ? asset.remoteId! : (asset as RemoteAsset).id;
|
||||
return _remoteAssetRepository.getExif(id);
|
||||
}
|
||||
|
||||
Future<double> getAspectRatio(BaseAsset asset) async {
|
||||
bool isFlipped;
|
||||
double? width;
|
||||
double? height;
|
||||
|
||||
if (asset.hasRemote) {
|
||||
final exif = await getExif(asset);
|
||||
isFlipped = ExifDtoConverter.isOrientationFlipped(exif?.orientation);
|
||||
width = exif?.width ?? asset.width?.toDouble();
|
||||
height = exif?.height ?? asset.height?.toDouble();
|
||||
} else if (asset is LocalAsset) {
|
||||
isFlipped = _platform.isAndroid &&
|
||||
(asset.orientation == 90 || asset.orientation == 270);
|
||||
width = asset.width?.toDouble();
|
||||
height = asset.height?.toDouble();
|
||||
} else {
|
||||
isFlipped = false;
|
||||
}
|
||||
|
||||
final orientedWidth = isFlipped ? height : width;
|
||||
final orientedHeight = isFlipped ? width : height;
|
||||
if (orientedWidth != null && orientedHeight != null && orientedHeight > 0) {
|
||||
return orientedWidth / orientedHeight;
|
||||
}
|
||||
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
Future<List<(String, String)>> getPlaces() {
|
||||
return _remoteAssetRepository.getPlaces();
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ class HashService {
|
||||
final toHash = <_AssetToPath>[];
|
||||
|
||||
for (final asset in assetsToHash) {
|
||||
final file = await _storageRepository.getFileForAsset(asset);
|
||||
final file = await _storageRepository.getFileForAsset(asset.id);
|
||||
if (file == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
17
mobile/lib/domain/services/local_album.service.dart
Normal file
17
mobile/lib/domain/services/local_album.service.dart
Normal file
@@ -0,0 +1,17 @@
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||
|
||||
class LocalAlbumService {
|
||||
final DriftLocalAlbumRepository _repository;
|
||||
|
||||
const LocalAlbumService(this._repository);
|
||||
|
||||
Future<List<LocalAlbum>> getAll() {
|
||||
return _repository.getAll();
|
||||
}
|
||||
|
||||
Future<LocalAsset?> getThumbnail(String albumId) {
|
||||
return _repository.getThumbnail(albumId);
|
||||
}
|
||||
}
|
||||
@@ -359,6 +359,7 @@ extension on Iterable<PlatformAsset> {
|
||||
width: e.width,
|
||||
height: e.height,
|
||||
durationInSeconds: e.durationInSeconds,
|
||||
orientation: e.orientation,
|
||||
),
|
||||
).toList();
|
||||
}
|
||||
|
||||
15
mobile/lib/domain/services/memory.service.dart
Normal file
15
mobile/lib/domain/services/memory.service.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:immich_mobile/domain/models/memory.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/memory.repository.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class DriftMemoryService {
|
||||
final log = Logger("DriftMemoryService");
|
||||
|
||||
final DriftMemoryRepository _repository;
|
||||
|
||||
DriftMemoryService(this._repository);
|
||||
|
||||
Future<List<DriftMemory>> getMemoryLane(String ownerId) {
|
||||
return _repository.getAll(ownerId);
|
||||
}
|
||||
}
|
||||
150
mobile/lib/domain/services/remote_album.service.dart
Normal file
150
mobile/lib/domain/services/remote_album.service.dart
Normal file
@@ -0,0 +1,150 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||
import 'package:immich_mobile/utils/remote_album.utils.dart';
|
||||
|
||||
class RemoteAlbumService {
|
||||
final DriftRemoteAlbumRepository _repository;
|
||||
final DriftAlbumApiRepository _albumApiRepository;
|
||||
|
||||
const RemoteAlbumService(this._repository, this._albumApiRepository);
|
||||
|
||||
Stream<RemoteAlbum?> watchAlbum(String albumId) {
|
||||
return _repository.watchAlbum(albumId);
|
||||
}
|
||||
|
||||
Future<List<RemoteAlbum>> getAll() {
|
||||
return _repository.getAll();
|
||||
}
|
||||
|
||||
List<RemoteAlbum> sortAlbums(
|
||||
List<RemoteAlbum> albums,
|
||||
RemoteAlbumSortMode sortMode, {
|
||||
bool isReverse = false,
|
||||
}) {
|
||||
return sortMode.sortFn(albums, isReverse);
|
||||
}
|
||||
|
||||
List<RemoteAlbum> searchAlbums(
|
||||
List<RemoteAlbum> albums,
|
||||
String query,
|
||||
String? userId, [
|
||||
QuickFilterMode filterMode = QuickFilterMode.all,
|
||||
]) {
|
||||
final lowerQuery = query.toLowerCase();
|
||||
List<RemoteAlbum> filtered = albums;
|
||||
|
||||
// Apply text search filter
|
||||
if (query.isNotEmpty) {
|
||||
filtered = filtered
|
||||
.where(
|
||||
(album) =>
|
||||
album.name.toLowerCase().contains(lowerQuery) ||
|
||||
album.description.toLowerCase().contains(lowerQuery),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
if (userId != null) {
|
||||
switch (filterMode) {
|
||||
case QuickFilterMode.myAlbums:
|
||||
filtered =
|
||||
filtered.where((album) => album.ownerId == userId).toList();
|
||||
break;
|
||||
case QuickFilterMode.sharedWithMe:
|
||||
filtered =
|
||||
filtered.where((album) => album.ownerId != userId).toList();
|
||||
break;
|
||||
case QuickFilterMode.all:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
Future<RemoteAlbum> createAlbum({
|
||||
required String title,
|
||||
required List<String> assetIds,
|
||||
String? description,
|
||||
}) async {
|
||||
final album = await _albumApiRepository.createDriftAlbum(
|
||||
title,
|
||||
description: description,
|
||||
assetIds: assetIds,
|
||||
);
|
||||
|
||||
await _repository.create(album, assetIds);
|
||||
|
||||
return album;
|
||||
}
|
||||
|
||||
Future<RemoteAlbum> updateAlbum(
|
||||
String albumId, {
|
||||
String? name,
|
||||
String? description,
|
||||
String? thumbnailAssetId,
|
||||
bool? isActivityEnabled,
|
||||
AlbumAssetOrder? order,
|
||||
}) async {
|
||||
final updatedAlbum = await _albumApiRepository.updateAlbum(
|
||||
albumId,
|
||||
name: name,
|
||||
description: description,
|
||||
thumbnailAssetId: thumbnailAssetId,
|
||||
isActivityEnabled: isActivityEnabled,
|
||||
order: order,
|
||||
);
|
||||
|
||||
// Update the local database
|
||||
await _repository.update(updatedAlbum);
|
||||
|
||||
return updatedAlbum;
|
||||
}
|
||||
|
||||
FutureOr<(DateTime, DateTime)> getDateRange(String albumId) {
|
||||
return _repository.getDateRange(albumId);
|
||||
}
|
||||
|
||||
Future<List<UserDto>> getSharedUsers(String albumId) {
|
||||
return _repository.getSharedUsers(albumId);
|
||||
}
|
||||
|
||||
Future<List<RemoteAsset>> getAssets(String albumId) {
|
||||
return _repository.getAssets(albumId);
|
||||
}
|
||||
|
||||
Future<int> addAssets({
|
||||
required String albumId,
|
||||
required List<String> assetIds,
|
||||
}) async {
|
||||
final album = await _albumApiRepository.addAssets(
|
||||
albumId,
|
||||
assetIds,
|
||||
);
|
||||
|
||||
await _repository.addAssets(albumId, album.added);
|
||||
|
||||
return album.added.length;
|
||||
}
|
||||
|
||||
Future<void> deleteAlbum(String albumId) async {
|
||||
await _albumApiRepository.deleteAlbum(albumId);
|
||||
|
||||
await _repository.deleteAlbum(albumId);
|
||||
}
|
||||
|
||||
Future<void> addUsers({
|
||||
required String albumId,
|
||||
required List<String> userIds,
|
||||
}) async {
|
||||
await _albumApiRepository.addUsers(albumId, userIds);
|
||||
|
||||
return _repository.addUsers(albumId, userIds);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
|
||||
// Singleton instance of SettingsService, to use in places
|
||||
// where reactivity is not required
|
||||
// ignore: non_constant_identifier_names
|
||||
final AppSetting = SettingsService(storeService: StoreService.I);
|
||||
|
||||
class SettingsService {
|
||||
final StoreService _storeService;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:immich_mobile/domain/models/sync_event.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
|
||||
import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
@@ -23,7 +24,65 @@ class SyncStreamService {
|
||||
|
||||
bool get isCancelled => _cancelChecker?.call() ?? false;
|
||||
|
||||
Future<void> sync() => _syncApiRepository.streamChanges(_handleEvents);
|
||||
Future<void> sync() {
|
||||
_logger.info("Remote sync request for userr");
|
||||
DLog.log("Remote sync request for user");
|
||||
// Start the sync stream and handle events
|
||||
return _syncApiRepository.streamChanges(_handleEvents);
|
||||
}
|
||||
|
||||
Future<void> handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) async {
|
||||
if (batchData.isEmpty) return;
|
||||
|
||||
_logger.info(
|
||||
'Processing batch of ${batchData.length} AssetUploadReadyV1 events',
|
||||
);
|
||||
|
||||
final List<SyncAssetV1> assets = [];
|
||||
final List<SyncAssetExifV1> exifs = [];
|
||||
|
||||
try {
|
||||
for (final data in batchData) {
|
||||
if (data is! Map<String, dynamic>) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final payload = data;
|
||||
final assetData = payload['asset'];
|
||||
final exifData = payload['exif'];
|
||||
|
||||
if (assetData == null || exifData == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final asset = SyncAssetV1.fromJson(assetData);
|
||||
final exif = SyncAssetExifV1.fromJson(exifData);
|
||||
|
||||
if (asset != null && exif != null) {
|
||||
assets.add(asset);
|
||||
exifs.add(exif);
|
||||
}
|
||||
}
|
||||
|
||||
if (assets.isNotEmpty && exifs.isNotEmpty) {
|
||||
await _syncStreamRepository.updateAssetsV1(
|
||||
assets,
|
||||
debugLabel: 'websocket-batch',
|
||||
);
|
||||
await _syncStreamRepository.updateAssetsExifV1(
|
||||
exifs,
|
||||
debugLabel: 'websocket-batch',
|
||||
);
|
||||
_logger.info('Successfully processed ${assets.length} assets in batch');
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe(
|
||||
"Error processing AssetUploadReadyV1 websocket batch events",
|
||||
error,
|
||||
stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleEvents(List<SyncEvent> events, Function() abort) async {
|
||||
List<SyncEvent> items = [];
|
||||
@@ -146,6 +205,41 @@ class SyncStreamService {
|
||||
// to acknowledge that the client has processed all the backfill events
|
||||
case SyncEntityType.syncAckV1:
|
||||
return;
|
||||
case SyncEntityType.memoryV1:
|
||||
return _syncStreamRepository.updateMemoriesV1(data.cast());
|
||||
case SyncEntityType.memoryDeleteV1:
|
||||
return _syncStreamRepository.deleteMemoriesV1(data.cast());
|
||||
case SyncEntityType.memoryToAssetV1:
|
||||
return _syncStreamRepository.updateMemoryAssetsV1(data.cast());
|
||||
case SyncEntityType.memoryToAssetDeleteV1:
|
||||
return _syncStreamRepository.deleteMemoryAssetsV1(data.cast());
|
||||
case SyncEntityType.stackV1:
|
||||
return _syncStreamRepository.updateStacksV1(data.cast());
|
||||
case SyncEntityType.stackDeleteV1:
|
||||
return _syncStreamRepository.deleteStacksV1(data.cast());
|
||||
case SyncEntityType.partnerStackV1:
|
||||
return _syncStreamRepository.updateStacksV1(
|
||||
data.cast(),
|
||||
debugLabel: 'partner',
|
||||
);
|
||||
case SyncEntityType.partnerStackBackfillV1:
|
||||
return _syncStreamRepository.updateStacksV1(
|
||||
data.cast(),
|
||||
debugLabel: 'partner backfill',
|
||||
);
|
||||
case SyncEntityType.partnerStackDeleteV1:
|
||||
return _syncStreamRepository.deleteStacksV1(
|
||||
data.cast(),
|
||||
debugLabel: 'partner',
|
||||
);
|
||||
case SyncEntityType.userMetadataV1:
|
||||
return _syncStreamRepository.updateUserMetadatasV1(
|
||||
data.cast(),
|
||||
);
|
||||
case SyncEntityType.userMetadataDeleteV1:
|
||||
return _syncStreamRepository.deleteUserMetadatasV1(
|
||||
data.cast(),
|
||||
);
|
||||
default:
|
||||
_logger.warning("Unknown sync data type: $type");
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
import 'package:immich_mobile/domain/services/setting.service.dart';
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
|
||||
import 'package:immich_mobile/utils/async_mutex.dart';
|
||||
|
||||
@@ -17,6 +18,11 @@ typedef TimelineAssetSource = Future<List<BaseAsset>> Function(
|
||||
|
||||
typedef TimelineBucketSource = Stream<List<Bucket>> Function();
|
||||
|
||||
typedef TimelineQuery = ({
|
||||
TimelineAssetSource assetSource,
|
||||
TimelineBucketSource bucketSource,
|
||||
});
|
||||
|
||||
class TimelineFactory {
|
||||
final DriftTimelineRepository _timelineRepository;
|
||||
final SettingsService _settingsService;
|
||||
@@ -30,53 +36,94 @@ class TimelineFactory {
|
||||
GroupAssetsBy get groupBy =>
|
||||
GroupAssetsBy.values[_settingsService.get(Setting.groupAssetsBy)];
|
||||
|
||||
TimelineService main(List<String> timelineUsers) => TimelineService(
|
||||
assetSource: (offset, count) => _timelineRepository
|
||||
.getMainBucketAssets(timelineUsers, offset: offset, count: count),
|
||||
bucketSource: () => _timelineRepository.watchMainBucket(
|
||||
timelineUsers,
|
||||
groupBy: groupBy,
|
||||
),
|
||||
);
|
||||
TimelineService main(List<String> timelineUsers) =>
|
||||
TimelineService(_timelineRepository.main(timelineUsers, groupBy));
|
||||
|
||||
TimelineService localAlbum({required String albumId}) => TimelineService(
|
||||
assetSource: (offset, count) => _timelineRepository
|
||||
.getLocalBucketAssets(albumId, offset: offset, count: count),
|
||||
bucketSource: () =>
|
||||
_timelineRepository.watchLocalBucket(albumId, groupBy: groupBy),
|
||||
);
|
||||
TimelineService localAlbum({required String albumId}) =>
|
||||
TimelineService(_timelineRepository.localAlbum(albumId, groupBy));
|
||||
|
||||
TimelineService remoteAlbum({required String albumId}) => TimelineService(
|
||||
assetSource: (offset, count) => _timelineRepository
|
||||
.getRemoteBucketAssets(albumId, offset: offset, count: count),
|
||||
bucketSource: () =>
|
||||
_timelineRepository.watchRemoteBucket(albumId, groupBy: groupBy),
|
||||
);
|
||||
TimelineService remoteAlbum({required String albumId}) =>
|
||||
TimelineService(_timelineRepository.remoteAlbum(albumId, groupBy));
|
||||
|
||||
TimelineService remoteAssets(String userId) =>
|
||||
TimelineService(_timelineRepository.remote(userId, groupBy));
|
||||
|
||||
TimelineService favorite(String userId) =>
|
||||
TimelineService(_timelineRepository.favorite(userId, groupBy));
|
||||
|
||||
TimelineService trash(String userId) =>
|
||||
TimelineService(_timelineRepository.trash(userId, groupBy));
|
||||
|
||||
TimelineService archive(String userId) =>
|
||||
TimelineService(_timelineRepository.archived(userId, groupBy));
|
||||
|
||||
TimelineService lockedFolder(String userId) =>
|
||||
TimelineService(_timelineRepository.locked(userId, groupBy));
|
||||
|
||||
TimelineService video(String userId) =>
|
||||
TimelineService(_timelineRepository.video(userId, groupBy));
|
||||
|
||||
TimelineService place(String place) =>
|
||||
TimelineService(_timelineRepository.place(place, groupBy));
|
||||
}
|
||||
|
||||
class TimelineService {
|
||||
final TimelineAssetSource _assetSource;
|
||||
final TimelineBucketSource _bucketSource;
|
||||
|
||||
TimelineService({
|
||||
required TimelineAssetSource assetSource,
|
||||
required TimelineBucketSource bucketSource,
|
||||
}) : _assetSource = assetSource,
|
||||
_bucketSource = bucketSource {
|
||||
_bucketSubscription =
|
||||
_bucketSource().listen((_) => unawaited(_reloadBucket()));
|
||||
}
|
||||
|
||||
final AsyncMutex _mutex = AsyncMutex();
|
||||
int _bufferOffset = 0;
|
||||
List<BaseAsset> _buffer = [];
|
||||
StreamSubscription? _bucketSubscription;
|
||||
|
||||
Stream<List<Bucket>> Function() get watchBuckets => _bucketSource;
|
||||
int _totalAssets = 0;
|
||||
int get totalAssets => _totalAssets;
|
||||
|
||||
Future<void> _reloadBucket() => _mutex.run(() async {
|
||||
_buffer = await _assetSource(_bufferOffset, _buffer.length);
|
||||
TimelineService(TimelineQuery query)
|
||||
: this._(
|
||||
assetSource: query.assetSource,
|
||||
bucketSource: query.bucketSource,
|
||||
);
|
||||
|
||||
TimelineService._({
|
||||
required TimelineAssetSource assetSource,
|
||||
required TimelineBucketSource bucketSource,
|
||||
}) : _assetSource = assetSource,
|
||||
_bucketSource = bucketSource {
|
||||
_bucketSubscription = _bucketSource().listen((buckets) {
|
||||
_mutex.run(() async {
|
||||
final totalAssets =
|
||||
buckets.fold<int>(0, (acc, bucket) => acc + bucket.assetCount);
|
||||
|
||||
if (totalAssets == 0) {
|
||||
_bufferOffset = 0;
|
||||
_buffer.clear();
|
||||
} else {
|
||||
final int offset;
|
||||
final int count;
|
||||
// When the buffer is empty or the old bufferOffset is greater than the new total assets,
|
||||
// we need to reset the buffer and load the first batch of assets.
|
||||
if (_bufferOffset >= totalAssets || _buffer.isEmpty) {
|
||||
offset = 0;
|
||||
count = kTimelineAssetLoadBatchSize;
|
||||
} else {
|
||||
offset = _bufferOffset;
|
||||
count = math.min(
|
||||
_buffer.length,
|
||||
totalAssets - _bufferOffset,
|
||||
);
|
||||
}
|
||||
_buffer = await _assetSource(offset, count);
|
||||
_bufferOffset = offset;
|
||||
}
|
||||
|
||||
// change the state's total assets count only after the buffer is reloaded
|
||||
_totalAssets = totalAssets;
|
||||
EventStream.shared.emit(const TimelineReloadEvent());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Stream<List<Bucket>> Function() get watchBuckets => _bucketSource;
|
||||
|
||||
Future<List<BaseAsset>> loadAssets(int index, int count) =>
|
||||
_mutex.run(() => _loadAssets(index, count));
|
||||
@@ -106,15 +153,18 @@ class TimelineService {
|
||||
: (len > kTimelineAssetLoadBatchSize ? index : index + count - len),
|
||||
);
|
||||
|
||||
final assets = await _assetSource(start, len);
|
||||
_buffer = assets;
|
||||
_buffer = await _assetSource(start, len);
|
||||
_bufferOffset = start;
|
||||
|
||||
return getAssets(index, count);
|
||||
}
|
||||
|
||||
bool hasRange(int index, int count) =>
|
||||
index >= _bufferOffset && index + count <= _bufferOffset + _buffer.length;
|
||||
index >= 0 &&
|
||||
index < _totalAssets &&
|
||||
index >= _bufferOffset &&
|
||||
index + count <= _bufferOffset + _buffer.length &&
|
||||
index + count <= _totalAssets;
|
||||
|
||||
List<BaseAsset> getAssets(int index, int count) {
|
||||
if (!hasRange(index, count)) {
|
||||
@@ -124,6 +174,22 @@ class TimelineService {
|
||||
return _buffer.slice(start, start + count);
|
||||
}
|
||||
|
||||
// Pre-cache assets around the given index for asset viewer
|
||||
Future<void> preCacheAssets(int index) =>
|
||||
_mutex.run(() => _loadAssets(index, math.min(5, _totalAssets - index)));
|
||||
|
||||
BaseAsset getRandomAsset() =>
|
||||
_buffer.elementAt(math.Random().nextInt(_buffer.length));
|
||||
|
||||
BaseAsset getAsset(int index) {
|
||||
if (!hasRange(index, 1)) {
|
||||
throw RangeError(
|
||||
'TimelineService::getAsset Index $index not in buffer range [$_bufferOffset, ${_bufferOffset + _buffer.length})',
|
||||
);
|
||||
}
|
||||
return _buffer.elementAt(index - _bufferOffset);
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
await _bucketSubscription?.cancel();
|
||||
_bucketSubscription = null;
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:worker_manager/worker_manager.dart';
|
||||
|
||||
class BackgroundSyncManager {
|
||||
Cancelable<void>? _syncTask;
|
||||
Cancelable<void>? _syncWebsocketTask;
|
||||
Cancelable<void>? _deviceAlbumSyncTask;
|
||||
Cancelable<void>? _hashTask;
|
||||
|
||||
@@ -20,6 +21,12 @@ class BackgroundSyncManager {
|
||||
_syncTask?.cancel();
|
||||
_syncTask = null;
|
||||
|
||||
if (_syncWebsocketTask != null) {
|
||||
futures.add(_syncWebsocketTask!.future);
|
||||
}
|
||||
_syncWebsocketTask?.cancel();
|
||||
_syncWebsocketTask = null;
|
||||
|
||||
return Future.wait(futures);
|
||||
}
|
||||
|
||||
@@ -72,4 +79,19 @@ class BackgroundSyncManager {
|
||||
_syncTask = null;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> syncWebsocketBatch(List<dynamic> batchData) {
|
||||
if (_syncWebsocketTask != null) {
|
||||
return _syncWebsocketTask!.future;
|
||||
}
|
||||
|
||||
_syncWebsocketTask = runInIsolateGentle(
|
||||
computation: (ref) => ref
|
||||
.read(syncStreamServiceProvider)
|
||||
.handleWsAssetUploadReadyV1Batch(batchData),
|
||||
);
|
||||
return _syncWebsocketTask!.whenComplete(() {
|
||||
_syncWebsocketTask = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
52
mobile/lib/domain/utils/event_stream.dart
Normal file
52
mobile/lib/domain/utils/event_stream.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
import 'dart:async';
|
||||
|
||||
sealed class Event {
|
||||
const Event();
|
||||
}
|
||||
|
||||
class TimelineReloadEvent extends Event {
|
||||
const TimelineReloadEvent();
|
||||
}
|
||||
|
||||
class ViewerOpenBottomSheetEvent extends Event {
|
||||
const ViewerOpenBottomSheetEvent();
|
||||
}
|
||||
|
||||
class EventStream {
|
||||
EventStream._();
|
||||
|
||||
static final EventStream shared = EventStream._();
|
||||
|
||||
final StreamController<Event> _controller =
|
||||
StreamController<Event>.broadcast();
|
||||
|
||||
void emit(Event event) {
|
||||
_controller.add(event);
|
||||
}
|
||||
|
||||
Stream<T> where<T extends Event>() {
|
||||
if (T == Event) {
|
||||
return _controller.stream as Stream<T>;
|
||||
}
|
||||
return _controller.stream.where((event) => event is T).cast<T>();
|
||||
}
|
||||
|
||||
StreamSubscription<T> listen<T extends Event>(
|
||||
void Function(T event)? onData, {
|
||||
Function? onError,
|
||||
void Function()? onDone,
|
||||
bool? cancelOnError,
|
||||
}) {
|
||||
return where<T>().listen(
|
||||
onData,
|
||||
onError: onError,
|
||||
onDone: onDone,
|
||||
cancelOnError: cancelOnError,
|
||||
);
|
||||
}
|
||||
|
||||
/// Closes the stream controller
|
||||
void dispose() {
|
||||
_controller.close();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user