Compare commits

..

9 Commits

Author SHA1 Message Date
Min Idzelis
915db962fc saving for later 2025-06-28 15:01:39 +00:00
Min Idzelis
2406bb2951 sync before lint/check 2025-06-28 04:03:26 +00:00
Min Idzelis
e52b43c075 shellcheck 2025-06-28 03:56:55 +00:00
Min Idzelis
227819343a update lockfile after merge 2025-06-28 03:56:05 +00:00
Min Idzelis
fccf698a5c Merge remote-tracking branch 'origin/main' into chore/pnpm_alt 2025-06-28 03:50:28 +00:00
Min Idzelis
6330319ee4 update gha 2025-06-28 03:42:36 +00:00
Min Idzelis
c026a53cb7 Dockerfile changes 2025-06-28 03:17:06 +00:00
Min Idzelis
1c339ff85b more progress - dockerfile 2025-06-26 22:41:24 +00:00
Min Idzelis
839db1e2c4 pnpm 2025-06-24 21:37:42 +00:00
842 changed files with 55094 additions and 58487 deletions

View File

@@ -73,8 +73,7 @@ install_dependencies() {
log "Installing dependencies" log "Installing dependencies"
( (
cd "${IMMICH_WORKSPACE}" || exit 1 cd "${IMMICH_WORKSPACE}" || exit 1
export CI=1 FROZEN=1 OFFLINE=1 CI=1 run_cmd make install-all
run_cmd make setup-web-dev setup-server-dev
) )
log "" log ""
} }

View File

@@ -8,11 +8,6 @@ services:
- IMMICH_SERVER_URL=http://127.0.0.1:2283/ - IMMICH_SERVER_URL=http://127.0.0.1:2283/
volumes: !override volumes: !override
- ..:/workspaces/immich - ..:/workspaces/immich
- cli_node_modules:/workspaces/immich/cli/node_modules
- e2e_node_modules:/workspaces/immich/e2e/node_modules
- open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules
- server_node_modules:/workspaces/immich/server/node_modules
- web_node_modules:/workspaces/immich/web/node_modules
- ${UPLOAD_LOCATION:-upload1-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/workspaces/immich/server/upload - ${UPLOAD_LOCATION:-upload1-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/workspaces/immich/server/upload
- ${UPLOAD_LOCATION:-upload2-devcontainer-volume}${UPLOAD_LOCATION:+/photos/upload}:/workspaces/immich/server/upload/upload - ${UPLOAD_LOCATION:-upload2-devcontainer-volume}${UPLOAD_LOCATION:+/photos/upload}:/workspaces/immich/server/upload/upload
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro

View File

@@ -10,8 +10,9 @@ cd "${IMMICH_WORKSPACE}/server" || (
exit 1 exit 1
) )
CI=1 pnpm install
while true; do while true; do
run_cmd node ./node_modules/.bin/nest start --debug "0.0.0.0:9230" --watch run_cmd pnpm exec nest start --debug "0.0.0.0:9230" --watch
log "Nest API Server crashed with exit code $?. Respawning in 3s ..." log "Nest API Server crashed with exit code $?. Respawning in 3s ..."
sleep 3 sleep 3
done done

View File

@@ -16,7 +16,7 @@ until curl --output /dev/null --silent --head --fail "http://127.0.0.1:${IMMICH_
done done
while true; do while true; do
run_cmd node ./node_modules/.bin/vite dev --host 0.0.0.0 --port "${DEV_PORT}" run_cmd pnpm exec vite dev --host 0.0.0.0 --port "${DEV_PORT}"
log "Web crashed with exit code $?. Respawning in 3s ..." log "Web crashed with exit code $?. Respawning in 3s ..."
sleep 3 sleep 3
done done

View File

@@ -20,7 +20,6 @@ mobile/
cli/coverage/ cli/coverage/
cli/dist/ cli/dist/
cli/node_modules/ cli/node_modules/
cli/Dockerfile
open-api/typescript-sdk/build/ open-api/typescript-sdk/build/
open-api/typescript-sdk/node_modules/ open-api/typescript-sdk/node_modules/
@@ -31,11 +30,9 @@ server/upload/
server/src/queries server/src/queries
server/dist/ server/dist/
server/www/ server/www/
server/Dockerfile
web/node_modules/ web/node_modules/
web/coverage/ web/coverage/
web/.svelte-kit web/.svelte-kit
web/build/ web/build/
web/.env web/.env
web/Dockerfile

2
.github/.nvmrc vendored
View File

@@ -1 +1 @@
22.17.0 22.16.0

View File

@@ -1,4 +0,0 @@
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

6
.github/package-lock.json generated vendored
View File

@@ -9,9 +9,9 @@
} }
}, },
"node_modules/prettier": { "node_modules/prettier": {
"version": "3.6.2", "version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {

View File

@@ -66,6 +66,12 @@ jobs:
ref: ${{ inputs.ref || github.sha }} ref: ${{ inputs.ref || github.sha }}
persist-credentials: false 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 - name: Create the Keystore
env: env:
KEY_JKS: ${{ secrets.KEY_JKS }} KEY_JKS: ${{ secrets.KEY_JKS }}
@@ -90,7 +96,7 @@ jobs:
key: build-mobile-gradle-${{ runner.os }}-main key: build-mobile-gradle-${{ runner.os }}-main
- name: Setup Flutter SDK - name: Setup Flutter SDK
uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0 uses: subosito/flutter-action@395322a6cded4e9ed503aebd4cc1965625f8e59a # v2.20.0
with: with:
channel: 'stable' channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml flutter-version-file: ./mobile/pubspec.yaml
@@ -122,17 +128,17 @@ jobs:
IS_MAIN: ${{ github.ref == 'refs/heads/main' }} IS_MAIN: ${{ github.ref == 'refs/heads/main' }}
run: | run: |
if [[ $IS_MAIN == 'true' ]]; then if [[ $IS_MAIN == 'true' ]]; then
flutter build apk --release --flavor production flutter build apk --release
flutter build apk --release --flavor production --split-per-abi --target-platform android-arm,android-arm64,android-x64 flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64
else else
flutter build apk --debug --flavor production --split-per-abi --target-platform android-arm64 flutter build apk --debug --split-per-abi --target-platform android-arm64
fi fi
- name: Publish Android Artifact - name: Publish Android Artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: release-apk-signed name: release-apk-signed
path: mobile/build/app/outputs/flutter-apk/**/*.apk path: mobile/build/app/outputs/flutter-apk/*.apk
- name: Save Gradle Cache - name: Save Gradle Cache
id: cache-gradle-save id: cache-gradle-save

View File

@@ -50,7 +50,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 uses: github/codeql-action/init@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # 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). # 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) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 uses: github/codeql-action/autobuild@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
# Command-line programs to run using the OS shell. # 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 # 📚 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 # ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
with: with:
category: '/language:${{matrix.language}}' category: '/language:${{matrix.language}}'

View File

@@ -131,7 +131,7 @@ jobs:
tag-suffix: '-rocm' tag-suffix: '-rocm'
platforms: linux/amd64 platforms: linux/amd64
runner-mapping: '{"linux/amd64": "mich"}' runner-mapping: '{"linux/amd64": "mich"}'
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@129aeda75a450666ce96e8bc8126652e717917a7 # multi-runner-build-workflow-0.1.1 uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@094bfb927b8cd75b343abaac27b3241be0fccfe9 # multi-runner-build-workflow-0.1.0
permissions: permissions:
contents: read contents: read
actions: read actions: read
@@ -154,7 +154,7 @@ jobs:
name: Build and Push Server name: Build and Push Server
needs: pre-job needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }} if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@129aeda75a450666ce96e8bc8126652e717917a7 # multi-runner-build-workflow-0.1.1 uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@094bfb927b8cd75b343abaac27b3241be0fccfe9 # multi-runner-build-workflow-0.1.0
permissions: permissions:
contents: read contents: read
actions: read actions: read

View File

@@ -60,14 +60,14 @@ jobs:
cache: 'npm' cache: 'npm'
cache-dependency-path: '**/package-lock.json' cache-dependency-path: '**/package-lock.json'
- name: Run npm install - name: Run install
run: npm ci run: pnpm install
- name: Check formatting - name: Check formatting
run: npm run format run: pnpm format
- name: Run build - name: Run build
run: npm run build run: pnpm build
- name: Upload build output - name: Upload build output
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2

View File

@@ -1,13 +0,0 @@
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

View File

@@ -14,7 +14,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- name: Require PR to have a changelog label - name: Require PR to have a changelog label
uses: mheap/github-action-required-labels@8afbe8ae6ab7647d0c9f0cfa7c2f939650d22509 # v5.5.1 uses: mheap/github-action-required-labels@fb29a14a076b0f74099f6198f77750e8fc236016 # v5.5.0
with: with:
mode: exactly mode: exactly
count: 1 count: 1

View File

@@ -42,9 +42,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
defaults:
run:
working-directory: ./mobile
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -52,29 +49,34 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Setup Flutter SDK - name: Setup Flutter SDK
uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0 uses: subosito/flutter-action@395322a6cded4e9ed503aebd4cc1965625f8e59a # v2.20.0
with: with:
channel: 'stable' channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml flutter-version-file: ./mobile/pubspec.yaml
- name: Install dependencies - name: Install dependencies
run: dart pub get run: dart pub get
working-directory: ./mobile
- name: Install DCM - name: Install DCM
uses: CQLabs/setup-dcm@8697ae0790c0852e964a6ef1d768d62a6675481a # v2.0.1 run: |
with: sudo apt-get update
github-token: ${{ secrets.GITHUB_TOKEN }} wget -qO- https://dcm.dev/pgp-key.public | sudo gpg --dearmor -o /usr/share/keyrings/dcm.gpg
version: auto 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
working-directory: ./mobile sudo apt-get update
sudo apt-get install dcm
- name: Generate translation file - name: Generate translation file
run: make translation run: make translation
working-directory: ./mobile
- name: Run Build Runner - name: Run Build Runner
run: make build run: make build
working-directory: ./mobile
- name: Generate platform API - name: Generate platform API
run: make pigeon run: make pigeon
working-directory: ./mobile
- name: Find file changes - name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
@@ -96,16 +98,19 @@ jobs:
- name: Run dart analyze - name: Run dart analyze
run: dart analyze --fatal-infos run: dart analyze --fatal-infos
working-directory: ./mobile
- name: Run dart format - name: Run dart format
run: dart format lib/ --set-exit-if-changed run: dart format lib/ --set-exit-if-changed
working-directory: ./mobile
- name: Run dart custom_lint - name: Run dart custom_lint
run: dart run custom_lint run: dart run custom_lint
working-directory: ./mobile
# TODO: Use https://github.com/CQLabs/dcm-action
- name: Run DCM - name: Run DCM
run: dcm analyze lib --fatal-style --fatal-warnings run: dcm analyze lib --fatal-style --fatal-warnings
working-directory: ./mobile
zizmor: zizmor:
name: zizmor name: zizmor
@@ -129,7 +134,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload SARIF file - name: Upload SARIF file
uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 uses: github/codeql-action/upload-sarif@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
with: with:
sarif_file: results.sarif sarif_file: results.sarif
category: zizmor category: zizmor

View File

@@ -80,30 +80,33 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
cache: 'npm' cache: 'pnpm'
cache-dependency-path: '**/package-lock.json' cache-dependency-path: '**/package-lock.json'
- name: Run npm install - name: Run package manager install
run: npm ci run: pnpm install
- name: Run linter - name: Run linter
run: npm run lint run: pnpm lint
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run formatter - name: Run formatter
run: npm run format run: pnpm format
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run tsc - name: Run tsc
run: npm run check run: pnpm check
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run small tests & coverage - name: Run small tests & coverage
run: npm test run: pnpm test
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
cli-unit-tests: cli-unit-tests:
@@ -123,34 +126,37 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: './cli/.nvmrc' node-version-file: './cli/.nvmrc'
cache: 'npm' cache: 'pnpm'
cache-dependency-path: '**/package-lock.json' cache-dependency-path: '**/package-lock.json'
- name: Setup typescript-sdk - name: Setup typescript-sdk
run: npm ci && npm run build run: pnpm install && pnpm run build
working-directory: ./open-api/typescript-sdk working-directory: ./open-api/typescript-sdk
- name: Install deps - name: Install deps
run: npm ci run: pnpm install
- name: Run linter - name: Run linter
run: npm run lint run: pnpm lint
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run formatter - name: Run formatter
run: npm run format run: pnpm format
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run tsc - name: Run tsc
run: npm run check run: pnpm check
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run unit tests & coverage - name: Run unit tests & coverage
run: npm run test run: pnpm test
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
cli-unit-tests-win: cli-unit-tests-win:
@@ -170,27 +176,30 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: './cli/.nvmrc' node-version-file: './cli/.nvmrc'
cache: 'npm' cache: 'pnpm'
cache-dependency-path: '**/package-lock.json' cache-dependency-path: '**/package-lock.json'
- name: Setup typescript-sdk - name: Setup typescript-sdk
run: npm ci && npm run build run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk working-directory: ./open-api/typescript-sdk
- name: Install deps - name: Install deps
run: npm ci run: pnpm install --frozen-lockfile
# Skip linter & formatter in Windows test. # Skip linter & formatter in Windows test.
- name: Run tsc - name: Run tsc
run: npm run check run: pnpm check
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run unit tests & coverage - name: Run unit tests & coverage
run: npm run test run: pnpm test
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
web-lint: web-lint:
@@ -210,30 +219,33 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: './web/.nvmrc' node-version-file: './web/.nvmrc'
cache: 'npm' cache: 'pnpm'
cache-dependency-path: '**/package-lock.json' cache-dependency-path: '**/package-lock.json'
- name: Run setup typescript-sdk - name: Run setup typescript-sdk
run: npm ci && npm run build run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk working-directory: ./open-api/typescript-sdk
- name: Run npm install - name: Run npm install
run: npm ci run: pnpm install --frozen-lockfile
- name: Run linter - name: Run linter
run: npm run lint:p run: pnpm lint:p
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run formatter - name: Run formatter
run: npm run format run: pnpm format
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run svelte checks - name: Run svelte checks
run: npm run check:svelte run: pnpm check:svelte
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
web-unit-tests: web-unit-tests:
@@ -253,26 +265,29 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: './web/.nvmrc' node-version-file: './web/.nvmrc'
cache: 'npm' cache: 'pnpm'
cache-dependency-path: '**/package-lock.json' cache-dependency-path: '**/package-lock.json'
- name: Run setup typescript-sdk - name: Run setup typescript-sdk
run: npm ci && npm run build run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk working-directory: ./open-api/typescript-sdk
- name: Run npm install - name: Run npm install
run: npm ci run: pnpm install --frozen-lockfile
- name: Run tsc - name: Run tsc
run: npm run check:typescript run: pnpm check:typescript
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run unit tests & coverage - name: Run unit tests & coverage
run: npm run test run: pnpm test
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
i18n-tests: i18n-tests:
@@ -288,18 +303,21 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: './web/.nvmrc' node-version-file: './web/.nvmrc'
cache: 'npm' cache: 'pnpm'
cache-dependency-path: '**/package-lock.json' cache-dependency-path: '**/package-lock.json'
- name: Install dependencies - name: Install dependencies
run: npm --prefix=web ci run: pnpm --filter=immich-web install --frozen-lockfile
- name: Format - name: Format
run: npm --prefix=web run format:i18n run: pnpm --filter=immich-web format:i18n
- name: Find file changes - name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
@@ -334,32 +352,35 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: './e2e/.nvmrc' node-version-file: './e2e/.nvmrc'
cache: 'npm' cache: 'pnpm'
cache-dependency-path: '**/package-lock.json' cache-dependency-path: '**/package-lock.json'
- name: Run setup typescript-sdk - name: Run setup typescript-sdk
run: npm ci && npm run build run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk working-directory: ./open-api/typescript-sdk
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Install dependencies - name: Install dependencies
run: npm ci run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run linter - name: Run linter
run: npm run lint run: pnpm lint
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run formatter - name: Run formatter
run: npm run format run: pnpm format
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run tsc - name: Run tsc
run: npm run check run: pnpm check
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
server-medium-tests: server-medium-tests:
@@ -379,18 +400,21 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
cache: 'npm' cache: 'pnpm'
cache-dependency-path: '**/package-lock.json' cache-dependency-path: '**/package-lock.json'
- name: Run npm install - name: Run npm install
run: npm ci run: pnpm install --frozen-lockfile
- name: Run medium tests - name: Run medium tests
run: npm run test:medium run: pnpm test:medium
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
e2e-tests-server-cli: e2e-tests-server-cli:
@@ -414,25 +438,28 @@ jobs:
persist-credentials: false persist-credentials: false
submodules: 'recursive' submodules: 'recursive'
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: './e2e/.nvmrc' node-version-file: './e2e/.nvmrc'
cache: 'npm' cache: 'pnpm'
cache-dependency-path: '**/package-lock.json' cache-dependency-path: '**/package-lock.json'
- name: Run setup typescript-sdk - name: Run setup typescript-sdk
run: npm ci && npm run build run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk working-directory: ./open-api/typescript-sdk
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run setup cli - name: Run setup cli
run: npm ci && npm run build run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./cli working-directory: ./cli
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Install dependencies - name: Install dependencies
run: npm ci run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Docker build - name: Docker build
@@ -440,7 +467,7 @@ jobs:
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run e2e tests (api & cli) - name: Run e2e tests (api & cli)
run: npm run test run: pnpm test
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
e2e-tests-web: e2e-tests-web:
@@ -464,20 +491,23 @@ jobs:
persist-credentials: false persist-credentials: false
submodules: 'recursive' submodules: 'recursive'
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: './e2e/.nvmrc' node-version-file: './e2e/.nvmrc'
cache: 'npm' cache: 'pnpm'
cache-dependency-path: '**/package-lock.json' cache-dependency-path: '**/package-lock.json'
- name: Run setup typescript-sdk - name: Run setup typescript-sdk
run: npm ci && npm run build run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk working-directory: ./open-api/typescript-sdk
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Install dependencies - name: Install dependencies
run: npm ci run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Install Playwright Browsers - name: Install Playwright Browsers
@@ -516,7 +546,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Setup Flutter SDK - name: Setup Flutter SDK
uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0 uses: subosito/flutter-action@395322a6cded4e9ed503aebd4cc1965625f8e59a # v2.20.0
with: with:
channel: 'stable' channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml flutter-version-file: ./mobile/pubspec.yaml
@@ -584,18 +614,21 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: './.github/.nvmrc' node-version-file: './.github/.nvmrc'
cache: 'npm' cache: 'pnpm'
cache-dependency-path: '**/package-lock.json' cache-dependency-path: '**/package-lock.json'
- name: Run npm install - name: Run npm install
run: npm ci run: pnpm install --frozen-lockfile
- name: Run formatter - name: Run formatter
run: npm run format run: pnpm format
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
shellcheck: shellcheck:
@@ -627,18 +660,21 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
cache: 'npm' cache: 'pnpm'
cache-dependency-path: '**/package-lock.json' cache-dependency-path: '**/package-lock.json'
- name: Install server dependencies - name: Install server dependencies
run: npm --prefix=server ci run: pnpm --filter immich install --frozen-lockfile
- name: Build the app - name: Build the app
run: npm --prefix=server run build run: pnpm --filter immich build
- name: Run API generation - name: Run API generation
run: make open-api run: make open-api
@@ -690,28 +726,31 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
cache: 'npm' cache: 'pnpm'
cache-dependency-path: '**/package-lock.json' cache-dependency-path: '**/package-lock.json'
- name: Install server dependencies - name: Install server dependencies
run: npm ci run: pnpm install --frozen-lockfile
- name: Build the app - name: Build the app
run: npm run build run: pnpm build
- name: Run existing migrations - name: Run existing migrations
run: npm run migrations:run run: pnpm migrations:run
- name: Test npm run schema:reset command works - name: Test npm run schema:reset command works
run: npm run schema:reset run: pnpm schema:reset
- name: Generate new migrations - name: Generate new migrations
continue-on-error: true continue-on-error: true
run: npm run migrations:generate src/TestMigration run: pnpm migrations:generate src/TestMigration
- name: Find file changes - name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
@@ -730,7 +769,7 @@ jobs:
exit 1 exit 1
- name: Run SQL generation - name: Run SQL generation
run: npm run sync:sql run: pnpm sync:sql
env: env:
DB_URL: postgres://postgres:postgres@localhost:5432/immich DB_URL: postgres://postgres:postgres@localhost:5432/immich

19
.vscode/launch.json vendored
View File

@@ -18,25 +18,6 @@
"name": "Immich Workers", "name": "Immich Workers",
"remoteRoot": "/usr/src/app", "remoteRoot": "/usr/src/app",
"localRoot": "${workspaceFolder}/server" "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"
],
} }
] ]
} }

View File

@@ -1,33 +1,27 @@
dev: dev:
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans || make dev-down
dev-down: dev-down:
docker compose -f ./docker/docker-compose.dev.yml down --remove-orphans docker compose -f ./docker/docker-compose.dev.yml down --remove-orphans
dev-update: dev-update:
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
dev-scale: dev-scale:
@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 docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
.PHONY: e2e .PHONY: e2e
e2e: e2e:
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans 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: prod:
@trap 'make prod-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
prod-down: prod-down:
docker compose -f ./docker/docker-compose.prod.yml down --remove-orphans docker compose -f ./docker/docker-compose.prod.yml down --remove-orphans
prod-scale: prod-scale:
@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 docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
.PHONY: open-api .PHONY: open-api
open-api: open-api:
@@ -40,41 +34,52 @@ open-api-typescript:
cd ./open-api && bash ./bin/generate-open-api.sh typescript cd ./open-api && bash ./bin/generate-open-api.sh typescript
sql: sql:
npm --prefix server run sync:sql pnpm --filter immich run sync:sql
attach-server: attach-server:
docker exec -it docker_immich-server_1 sh docker exec -it docker_immich-server_1 sh
renovate: renovate:
LOG_LEVEL=debug npx renovate --platform=local --repository-cache=reset LOG_LEVEL=debug npx renovate --platform=local --repository-cache=reset
MODULES = e2e server web cli sdk docs .github MODULES = e2e server web cli sdk docs .github
# package names mapping function
# cli = @immich/cli
# docs = documentation
# e2e = immich-e2e
# open-api/typescript-sdk = @immich/sdk
# server = immich
# web = immich-web
map-package = $(subst sdk,@immich/sdk,$(subst cli,@immich/cli,$(subst docs,documentation,$(subst e2e,immich-e2e,$(subst server,immich,$(subst web,immich-web,$1))))))
audit-%: audit-%:
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) audit fix pnpm --filter $(call map-package,$*) audit fix
install-%: install-%:
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) i pnpm --filter $(call map-package,$*) install
ci-%: ci-%:
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) ci pnpm --filter $(call map-package,$*) install --frozen-lockfile
build-cli: build-sdk build-cli: build-sdk
build-web: build-sdk build-web: build-sdk
build-%: install-% build-%: install-%
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) run build pnpm --filter $(call map-package,$*) run build
format-%: format-%:
npm --prefix $* run format:fix pnpm --filter $(call map-package,$*) run format:fix
lint-%: lint-%:
npm --prefix $* run lint:fix pnpm --filter $(call map-package,$*) run lint:fix
lint-web:
pnpm --filter $(call map-package,$*) run lint:p
check-%: check-%:
npm --prefix $* run check pnpm --filter $(call map-package,$*) run check
check-web: check-web:
npm --prefix web run check:typescript pnpm --filter immich-web run check:typescript
npm --prefix web run check:svelte pnpm --filter immich-web run check:svelte
test-%: test-%:
npm --prefix $* run test pnpm --filter $(call map-package,$*) run test
test-e2e: test-e2e:
docker compose -f ./e2e/docker-compose.yml build docker compose -f ./e2e/docker-compose.yml build
npm --prefix e2e run test pnpm --filter immich-e2e run test
npm --prefix e2e run test:web pnpm --filter immich-e2e run test:web
test-medium: test-medium:
docker run \ docker run \
--rm \ --rm \
@@ -84,19 +89,34 @@ test-medium:
-v ./server/tsconfig.json:/usr/src/app/tsconfig.json \ -v ./server/tsconfig.json:/usr/src/app/tsconfig.json \
-e NODE_ENV=development \ -e NODE_ENV=development \
immich-server:latest \ immich-server:latest \
-c "npm ci && npm run test:medium -- --run" -c "pnpm test:medium -- --run"
test-medium-dev: test-medium-dev:
docker exec -it immich_server /bin/sh -c "npm run test:medium" docker exec -it immich_server /bin/sh -c "pnpm run test:medium"
build-all: $(foreach M,$(filter-out e2e .github,$(MODULES)),build-$M) ; install-all:
install-all: $(foreach M,$(MODULES),install-$M) ; pnpm -r --filter '!documentation' install
ci-all: $(foreach M,$(filter-out .github,$(MODULES)),ci-$M) ;
check-all: $(foreach M,$(filter-out sdk cli docs .github,$(MODULES)),check-$M) ; ci-all:
lint-all: $(foreach M,$(filter-out sdk docs .github,$(MODULES)),lint-$M) ; pnpm -r --filter '!documentation' install --frozen-lockfile
format-all: $(foreach M,$(filter-out sdk,$(MODULES)),format-$M) ;
audit-all: $(foreach M,$(MODULES),audit-$M) ; build-all: $(foreach M,$(filter-out e2e docs .github,$(MODULES)),build-$M) ;
hygiene-all: lint-all format-all check-all sql audit-all;
test-all: $(foreach M,$(filter-out sdk docs .github,$(MODULES)),test-$M) ; check-all:
pnpm -r --filter '!documentation' run "/^(check|check\:svelte|check\:typescript)$/"
lint-all:
pnpm -r --filter '!documentation' run lint:fix
format-all:
pnpm -r --filter '!documentation' run format:fix
audit-all:
pnpm -r --filter '!documentation' audit fix
hygiene-all: audit-all
pnpm -r --filter '!documentation' run "/(format:fix|check|check:svelte|check:typescript|sql)/"
test-all:
pnpm -r --filter '!documentation' run "/^test/"
prune:
pnpm store prune
clean: clean:
find . -name "node_modules" -type d -prune -exec rm -rf {} + find . -name "node_modules" -type d -prune -exec rm -rf {} +
@@ -105,6 +125,3 @@ clean:
find . -name "svelte-kit" -type d -prune -exec rm -rf '{}' + find . -name "svelte-kit" -type d -prune -exec rm -rf '{}' +
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 ./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 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

View File

@@ -1 +1 @@
22.17.0 22.16.0

View File

@@ -6,8 +6,10 @@ Please see the [Immich CLI documentation](https://immich.app/docs/features/comma
Before building the CLI, you must build the immich server and the open-api client. To build the server run the following in the server folder: Before building the CLI, you must build the immich server and the open-api client. To build the server run the following in the server folder:
$ npm install # if you don't have node installed
$ npm run build $ npm install -g pnpm
$ pnpm install
$ pnpm build
Then, to build the open-api client run the following in the open-api folder: Then, to build the open-api client run the following in the open-api folder:
@@ -15,8 +17,10 @@ Then, to build the open-api client run the following in the open-api folder:
To run the Immich CLI from source, run the following in the cli folder: To run the Immich CLI from source, run the following in the cli folder:
$ npm install # if you don't have node installed
$ npm run build $ npm install -g pnpm
$ pnpm install
$ pnpm build
$ ts-node . $ ts-node .
You'll need ts-node, the easiest way to install it is to use npm: You'll need ts-node, the easiest way to install it is to use npm:

View File

@@ -1,2 +1,2 @@
#!/usr/bin/env node #!/usr/bin/env node
import '../dist/index.js'; require('../dist/index.js');

524
cli/package-lock.json generated
View File

@@ -16,7 +16,7 @@
"micromatch": "^4.0.8" "micromatch": "^4.0.8"
}, },
"bin": { "bin": {
"immich": "bin/immich" "immich": "dist/index.js"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.1.0", "@eslint/eslintrc": "^3.1.0",
@@ -27,7 +27,7 @@
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/micromatch": "^4.0.9", "@types/micromatch": "^4.0.9",
"@types/mock-fs": "^4.13.1", "@types/mock-fs": "^4.13.1",
"@types/node": "^22.15.33", "@types/node": "^22.15.32",
"@vitest/coverage-v8": "^3.0.0", "@vitest/coverage-v8": "^3.0.0",
"byte-size": "^9.0.0", "byte-size": "^9.0.0",
"cli-progress": "^3.12.0", "cli-progress": "^3.12.0",
@@ -42,7 +42,7 @@
"prettier-plugin-organize-imports": "^4.0.0", "prettier-plugin-organize-imports": "^4.0.0",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"typescript-eslint": "^8.28.0", "typescript-eslint": "^8.28.0",
"vite": "^7.0.0", "vite": "^6.0.0",
"vite-tsconfig-paths": "^5.0.0", "vite-tsconfig-paths": "^5.0.0",
"vitest": "^3.0.0", "vitest": "^3.0.0",
"vitest-fetch-mock": "^0.4.0", "vitest-fetch-mock": "^0.4.0",
@@ -61,7 +61,7 @@
"@oazapfts/runtime": "^1.0.2" "@oazapfts/runtime": "^1.0.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.15.33", "@types/node": "^22.15.32",
"typescript": "^5.3.3" "typescript": "^5.3.3"
} }
}, },
@@ -607,9 +607,9 @@
} }
}, },
"node_modules/@eslint/config-array": { "node_modules/@eslint/config-array": {
"version": "0.21.0", "version": "0.20.0",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz",
"integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@@ -622,9 +622,9 @@
} }
}, },
"node_modules/@eslint/config-helpers": { "node_modules/@eslint/config-helpers": {
"version": "0.3.0", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz",
"integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
@@ -682,9 +682,9 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "9.30.1", "version": "9.27.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.1.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz",
"integrity": "sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==", "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -1276,16 +1276,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@types/cli-progress": {
"version": "3.11.6", "version": "3.11.6",
"resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.6.tgz", "resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.6.tgz",
@@ -1296,13 +1286,6 @@
"@types/node": "*" "@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": { "node_modules/@types/estree": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
@@ -1355,9 +1338,9 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.15.34", "version": "22.15.32",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.34.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.32.tgz",
"integrity": "sha512-8Y6E5WUupYy1Dd0II32BsWAx5MWdcnRd8L84Oys3veg1YrYtNtzgO4CFhiBg6MDSjk7Ay36HYOnU7/tuOzIzcw==", "integrity": "sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1365,17 +1348,17 @@
} }
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.36.0", "version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz",
"integrity": "sha512-lZNihHUVB6ZZiPBNgOQGSxUASI7UJWhT8nHyUGCnaQ28XFCw98IfrMCG3rUl1uwUWoAvodJQby2KTs79UTcrAg==", "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.10.0", "@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.36.0", "@typescript-eslint/scope-manager": "8.32.1",
"@typescript-eslint/type-utils": "8.36.0", "@typescript-eslint/type-utils": "8.32.1",
"@typescript-eslint/utils": "8.36.0", "@typescript-eslint/utils": "8.32.1",
"@typescript-eslint/visitor-keys": "8.36.0", "@typescript-eslint/visitor-keys": "8.32.1",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^7.0.0", "ignore": "^7.0.0",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
@@ -1389,7 +1372,7 @@
"url": "https://opencollective.com/typescript-eslint" "url": "https://opencollective.com/typescript-eslint"
}, },
"peerDependencies": { "peerDependencies": {
"@typescript-eslint/parser": "^8.36.0", "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
"eslint": "^8.57.0 || ^9.0.0", "eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0" "typescript": ">=4.8.4 <5.9.0"
} }
@@ -1405,16 +1388,16 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "8.36.0", "version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz",
"integrity": "sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==", "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.36.0", "@typescript-eslint/scope-manager": "8.32.1",
"@typescript-eslint/types": "8.36.0", "@typescript-eslint/types": "8.32.1",
"@typescript-eslint/typescript-estree": "8.36.0", "@typescript-eslint/typescript-estree": "8.32.1",
"@typescript-eslint/visitor-keys": "8.36.0", "@typescript-eslint/visitor-keys": "8.32.1",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -1429,37 +1412,15 @@
"typescript": ">=4.8.4 <5.9.0" "typescript": ">=4.8.4 <5.9.0"
} }
}, },
"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/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": { "node_modules/@typescript-eslint/scope-manager": {
"version": "8.36.0", "version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz",
"integrity": "sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA==", "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.36.0", "@typescript-eslint/types": "8.32.1",
"@typescript-eslint/visitor-keys": "8.36.0" "@typescript-eslint/visitor-keys": "8.32.1"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1469,32 +1430,15 @@
"url": "https://opencollective.com/typescript-eslint" "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": { "node_modules/@typescript-eslint/type-utils": {
"version": "8.36.0", "version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz",
"integrity": "sha512-5aaGYG8cVDd6cxfk/ynpYzxBRZJk7w/ymto6uiyUFtdCozQIsQWh7M28/6r57Fwkbweng8qAzoMCPwSJfWlmsg==", "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/typescript-estree": "8.36.0", "@typescript-eslint/typescript-estree": "8.32.1",
"@typescript-eslint/utils": "8.36.0", "@typescript-eslint/utils": "8.32.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^2.1.0" "ts-api-utils": "^2.1.0"
}, },
@@ -1511,9 +1455,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "8.36.0", "version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz",
"integrity": "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==", "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -1525,16 +1469,14 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "8.36.0", "version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz",
"integrity": "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg==", "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/project-service": "8.36.0", "@typescript-eslint/types": "8.32.1",
"@typescript-eslint/tsconfig-utils": "8.36.0", "@typescript-eslint/visitor-keys": "8.32.1",
"@typescript-eslint/types": "8.36.0",
"@typescript-eslint/visitor-keys": "8.36.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@@ -1554,9 +1496,9 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "2.0.2", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1580,16 +1522,16 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "8.36.0", "version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz",
"integrity": "sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g==", "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.7.0", "@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.36.0", "@typescript-eslint/scope-manager": "8.32.1",
"@typescript-eslint/types": "8.36.0", "@typescript-eslint/types": "8.32.1",
"@typescript-eslint/typescript-estree": "8.36.0" "@typescript-eslint/typescript-estree": "8.32.1"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1604,14 +1546,14 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "8.36.0", "version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz",
"integrity": "sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==", "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.36.0", "@typescript-eslint/types": "8.32.1",
"eslint-visitor-keys": "^4.2.1" "eslint-visitor-keys": "^4.2.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1622,16 +1564,15 @@
} }
}, },
"node_modules/@vitest/coverage-v8": { "node_modules/@vitest/coverage-v8": {
"version": "3.2.4", "version": "3.1.4",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.4.tgz",
"integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", "integrity": "sha512-G4p6OtioySL+hPV7Y6JHlhpsODbJzt1ndwHAFkyk6vVjpK03PFsKnauZIzcd0PrK4zAbc5lc+jeZ+eNGiMA+iw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.3.0", "@ampproject/remapping": "^2.3.0",
"@bcoe/v8-coverage": "^1.0.2", "@bcoe/v8-coverage": "^1.0.2",
"ast-v8-to-istanbul": "^0.3.3", "debug": "^4.4.0",
"debug": "^4.4.1",
"istanbul-lib-coverage": "^3.2.2", "istanbul-lib-coverage": "^3.2.2",
"istanbul-lib-report": "^3.0.1", "istanbul-lib-report": "^3.0.1",
"istanbul-lib-source-maps": "^5.0.6", "istanbul-lib-source-maps": "^5.0.6",
@@ -1646,8 +1587,8 @@
"url": "https://opencollective.com/vitest" "url": "https://opencollective.com/vitest"
}, },
"peerDependencies": { "peerDependencies": {
"@vitest/browser": "3.2.4", "@vitest/browser": "3.1.4",
"vitest": "3.2.4" "vitest": "3.1.4"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@vitest/browser": { "@vitest/browser": {
@@ -1656,15 +1597,14 @@
} }
}, },
"node_modules/@vitest/expect": { "node_modules/@vitest/expect": {
"version": "3.2.4", "version": "3.1.4",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.4.tgz",
"integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", "integrity": "sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/chai": "^5.2.2", "@vitest/spy": "3.1.4",
"@vitest/spy": "3.2.4", "@vitest/utils": "3.1.4",
"@vitest/utils": "3.2.4",
"chai": "^5.2.0", "chai": "^5.2.0",
"tinyrainbow": "^2.0.0" "tinyrainbow": "^2.0.0"
}, },
@@ -1673,13 +1613,13 @@
} }
}, },
"node_modules/@vitest/mocker": { "node_modules/@vitest/mocker": {
"version": "3.2.4", "version": "3.1.4",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.4.tgz",
"integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", "integrity": "sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vitest/spy": "3.2.4", "@vitest/spy": "3.1.4",
"estree-walker": "^3.0.3", "estree-walker": "^3.0.3",
"magic-string": "^0.30.17" "magic-string": "^0.30.17"
}, },
@@ -1688,7 +1628,7 @@
}, },
"peerDependencies": { "peerDependencies": {
"msw": "^2.4.9", "msw": "^2.4.9",
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" "vite": "^5.0.0 || ^6.0.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"msw": { "msw": {
@@ -1700,9 +1640,9 @@
} }
}, },
"node_modules/@vitest/pretty-format": { "node_modules/@vitest/pretty-format": {
"version": "3.2.4", "version": "3.1.4",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.4.tgz",
"integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", "integrity": "sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1713,28 +1653,27 @@
} }
}, },
"node_modules/@vitest/runner": { "node_modules/@vitest/runner": {
"version": "3.2.4", "version": "3.1.4",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.4.tgz",
"integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "integrity": "sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vitest/utils": "3.2.4", "@vitest/utils": "3.1.4",
"pathe": "^2.0.3", "pathe": "^2.0.3"
"strip-literal": "^3.0.0"
}, },
"funding": { "funding": {
"url": "https://opencollective.com/vitest" "url": "https://opencollective.com/vitest"
} }
}, },
"node_modules/@vitest/snapshot": { "node_modules/@vitest/snapshot": {
"version": "3.2.4", "version": "3.1.4",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.4.tgz",
"integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", "integrity": "sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vitest/pretty-format": "3.2.4", "@vitest/pretty-format": "3.1.4",
"magic-string": "^0.30.17", "magic-string": "^0.30.17",
"pathe": "^2.0.3" "pathe": "^2.0.3"
}, },
@@ -1743,27 +1682,27 @@
} }
}, },
"node_modules/@vitest/spy": { "node_modules/@vitest/spy": {
"version": "3.2.4", "version": "3.1.4",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.4.tgz",
"integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", "integrity": "sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"tinyspy": "^4.0.3" "tinyspy": "^3.0.2"
}, },
"funding": { "funding": {
"url": "https://opencollective.com/vitest" "url": "https://opencollective.com/vitest"
} }
}, },
"node_modules/@vitest/utils": { "node_modules/@vitest/utils": {
"version": "3.2.4", "version": "3.1.4",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.4.tgz",
"integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", "integrity": "sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vitest/pretty-format": "3.2.4", "@vitest/pretty-format": "3.1.4",
"loupe": "^3.1.4", "loupe": "^3.1.3",
"tinyrainbow": "^2.0.0" "tinyrainbow": "^2.0.0"
}, },
"funding": { "funding": {
@@ -1771,9 +1710,9 @@
} }
}, },
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.15.0", "version": "8.14.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
@@ -1853,18 +1792,6 @@
"node": ">=12" "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": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -2178,9 +2105,9 @@
} }
}, },
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.1", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -2305,19 +2232,19 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "9.30.1", "version": "9.27.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.1.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz",
"integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==", "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.21.0", "@eslint/config-array": "^0.20.0",
"@eslint/config-helpers": "^0.3.0", "@eslint/config-helpers": "^0.2.1",
"@eslint/core": "^0.14.0", "@eslint/core": "^0.14.0",
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.30.1", "@eslint/js": "9.27.0",
"@eslint/plugin-kit": "^0.3.1", "@eslint/plugin-kit": "^0.3.1",
"@humanfs/node": "^0.16.6", "@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
@@ -2329,9 +2256,9 @@
"cross-spawn": "^7.0.6", "cross-spawn": "^7.0.6",
"debug": "^4.3.2", "debug": "^4.3.2",
"escape-string-regexp": "^4.0.0", "escape-string-regexp": "^4.0.0",
"eslint-scope": "^8.4.0", "eslint-scope": "^8.3.0",
"eslint-visitor-keys": "^4.2.1", "eslint-visitor-keys": "^4.2.0",
"espree": "^10.4.0", "espree": "^10.3.0",
"esquery": "^1.5.0", "esquery": "^1.5.0",
"esutils": "^2.0.2", "esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
@@ -2382,14 +2309,14 @@
} }
}, },
"node_modules/eslint-plugin-prettier": { "node_modules/eslint-plugin-prettier": {
"version": "5.5.1", "version": "5.4.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.1.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.0.tgz",
"integrity": "sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw==", "integrity": "sha512-BvQOvUhkVQM1i63iMETK9Hjud9QhqBnbtT1Zc642p9ynzBuCe5pybkOnvqZIBypXmMlsGcnU4HZ8sCTPfpAexA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"prettier-linter-helpers": "^1.0.0", "prettier-linter-helpers": "^1.0.0",
"synckit": "^0.11.7" "synckit": "^0.11.0"
}, },
"engines": { "engines": {
"node": "^14.18.0 || >=16.0.0" "node": "^14.18.0 || >=16.0.0"
@@ -2475,9 +2402,9 @@
} }
}, },
"node_modules/eslint-scope": { "node_modules/eslint-scope": {
"version": "8.4.0", "version": "8.3.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz",
"integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
@@ -2492,9 +2419,9 @@
} }
}, },
"node_modules/eslint-visitor-keys": { "node_modules/eslint-visitor-keys": {
"version": "4.2.1", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
@@ -2505,15 +2432,15 @@
} }
}, },
"node_modules/espree": { "node_modules/espree": {
"version": "10.4.0", "version": "10.3.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"acorn": "^8.15.0", "acorn": "^8.14.0",
"acorn-jsx": "^5.3.2", "acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^4.2.1" "eslint-visitor-keys": "^4.2.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2822,9 +2749,9 @@
} }
}, },
"node_modules/globals": { "node_modules/globals": {
"version": "16.3.0", "version": "16.1.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz",
"integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -3048,13 +2975,6 @@
"@pkgjs/parseargs": "^0.11.0" "@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": { "node_modules/js-yaml": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@@ -3156,9 +3076,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/loupe": { "node_modules/loupe": {
"version": "3.1.4", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz",
"integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==", "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@@ -3427,9 +3347,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/pathval": { "node_modules/pathval": {
"version": "2.0.1", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
"integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -3466,9 +3386,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.6", "version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -3486,7 +3406,7 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.8",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"source-map-js": "^1.2.1" "source-map-js": "^1.2.1"
}, },
@@ -3505,9 +3425,9 @@
} }
}, },
"node_modules/prettier": { "node_modules/prettier": {
"version": "3.6.2", "version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
@@ -3879,19 +3799,6 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/supports-color": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -3906,13 +3813,14 @@
} }
}, },
"node_modules/synckit": { "node_modules/synckit": {
"version": "0.11.8", "version": "0.11.4",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.4.tgz",
"integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==", "integrity": "sha512-Q/XQKRaJiLiFIBNN+mndW7S/RHxvwzuZS6ZwmRzUBqJBv/5QIKCEwkBC8GBf8EQJKYnaFs0wOZbKTXBPj8L9oQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@pkgr/core": "^0.2.4" "@pkgr/core": "^0.2.3",
"tslib": "^2.8.1"
}, },
"engines": { "engines": {
"node": "^14.18.0 || >=16.0.0" "node": "^14.18.0 || >=16.0.0"
@@ -3977,9 +3885,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.14", "version": "0.2.13",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -4022,9 +3930,9 @@
} }
}, },
"node_modules/tinypool": { "node_modules/tinypool": {
"version": "1.1.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz",
"integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -4042,9 +3950,9 @@
} }
}, },
"node_modules/tinyspy": { "node_modules/tinyspy": {
"version": "4.0.3", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
"integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -4097,6 +4005,13 @@
} }
} }
}, },
"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": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -4125,15 +4040,15 @@
} }
}, },
"node_modules/typescript-eslint": { "node_modules/typescript-eslint": {
"version": "8.36.0", "version": "8.32.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.36.0.tgz", "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz",
"integrity": "sha512-fTCqxthY+h9QbEgSIBfL9iV6CvKDFuoxg6bHPNpJ9HIUzS+jy2lCEyCmGyZRWEBSaykqcDPf1SJ+BfCI8DRopA==", "integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/eslint-plugin": "8.36.0", "@typescript-eslint/eslint-plugin": "8.32.1",
"@typescript-eslint/parser": "8.36.0", "@typescript-eslint/parser": "8.32.1",
"@typescript-eslint/utils": "8.36.0" "@typescript-eslint/utils": "8.32.1"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -4196,24 +4111,24 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "7.0.4", "version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.4.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA==", "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.4.6", "fdir": "^6.4.4",
"picomatch": "^4.0.2", "picomatch": "^4.0.2",
"postcss": "^8.5.6", "postcss": "^8.5.3",
"rollup": "^4.40.0", "rollup": "^4.34.9",
"tinyglobby": "^0.2.14" "tinyglobby": "^0.2.13"
}, },
"bin": { "bin": {
"vite": "bin/vite.js" "vite": "bin/vite.js"
}, },
"engines": { "engines": {
"node": "^20.19.0 || >=22.12.0" "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
}, },
"funding": { "funding": {
"url": "https://github.com/vitejs/vite?sponsor=1" "url": "https://github.com/vitejs/vite?sponsor=1"
@@ -4222,14 +4137,14 @@
"fsevents": "~2.3.3" "fsevents": "~2.3.3"
}, },
"peerDependencies": { "peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"jiti": ">=1.21.0", "jiti": ">=1.21.0",
"less": "^4.0.0", "less": "*",
"lightningcss": "^1.21.0", "lightningcss": "^1.21.0",
"sass": "^1.70.0", "sass": "*",
"sass-embedded": "^1.70.0", "sass-embedded": "*",
"stylus": ">=0.54.8", "stylus": "*",
"sugarss": "^5.0.0", "sugarss": "*",
"terser": "^5.16.0", "terser": "^5.16.0",
"tsx": "^4.8.1", "tsx": "^4.8.1",
"yaml": "^2.4.2" "yaml": "^2.4.2"
@@ -4271,17 +4186,17 @@
} }
}, },
"node_modules/vite-node": { "node_modules/vite-node": {
"version": "3.2.4", "version": "3.1.4",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.4.tgz",
"integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", "integrity": "sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cac": "^6.7.14", "cac": "^6.7.14",
"debug": "^4.4.1", "debug": "^4.4.0",
"es-module-lexer": "^1.7.0", "es-module-lexer": "^1.7.0",
"pathe": "^2.0.3", "pathe": "^2.0.3",
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" "vite": "^5.0.0 || ^6.0.0"
}, },
"bin": { "bin": {
"vite-node": "vite-node.mjs" "vite-node": "vite-node.mjs"
@@ -4314,9 +4229,9 @@
} }
}, },
"node_modules/vite/node_modules/fdir": { "node_modules/vite/node_modules/fdir": {
"version": "6.4.6", "version": "6.4.4",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
@@ -4342,34 +4257,32 @@
} }
}, },
"node_modules/vitest": { "node_modules/vitest": {
"version": "3.2.4", "version": "3.1.4",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.4.tgz",
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "integrity": "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/chai": "^5.2.2", "@vitest/expect": "3.1.4",
"@vitest/expect": "3.2.4", "@vitest/mocker": "3.1.4",
"@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.1.4",
"@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.1.4",
"@vitest/runner": "3.2.4", "@vitest/snapshot": "3.1.4",
"@vitest/snapshot": "3.2.4", "@vitest/spy": "3.1.4",
"@vitest/spy": "3.2.4", "@vitest/utils": "3.1.4",
"@vitest/utils": "3.2.4",
"chai": "^5.2.0", "chai": "^5.2.0",
"debug": "^4.4.1", "debug": "^4.4.0",
"expect-type": "^1.2.1", "expect-type": "^1.2.1",
"magic-string": "^0.30.17", "magic-string": "^0.30.17",
"pathe": "^2.0.3", "pathe": "^2.0.3",
"picomatch": "^4.0.2",
"std-env": "^3.9.0", "std-env": "^3.9.0",
"tinybench": "^2.9.0", "tinybench": "^2.9.0",
"tinyexec": "^0.3.2", "tinyexec": "^0.3.2",
"tinyglobby": "^0.2.14", "tinyglobby": "^0.2.13",
"tinypool": "^1.1.1", "tinypool": "^1.0.2",
"tinyrainbow": "^2.0.0", "tinyrainbow": "^2.0.0",
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite": "^5.0.0 || ^6.0.0",
"vite-node": "3.2.4", "vite-node": "3.1.4",
"why-is-node-running": "^2.3.0" "why-is-node-running": "^2.3.0"
}, },
"bin": { "bin": {
@@ -4385,8 +4298,8 @@
"@edge-runtime/vm": "*", "@edge-runtime/vm": "*",
"@types/debug": "^4.1.12", "@types/debug": "^4.1.12",
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"@vitest/browser": "3.2.4", "@vitest/browser": "3.1.4",
"@vitest/ui": "3.2.4", "@vitest/ui": "3.1.4",
"happy-dom": "*", "happy-dom": "*",
"jsdom": "*" "jsdom": "*"
}, },
@@ -4427,19 +4340,6 @@
"vitest": ">=2.0.0" "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": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -21,7 +21,7 @@
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/micromatch": "^4.0.9", "@types/micromatch": "^4.0.9",
"@types/mock-fs": "^4.13.1", "@types/mock-fs": "^4.13.1",
"@types/node": "^22.15.33", "@types/node": "^22.15.32",
"@vitest/coverage-v8": "^3.0.0", "@vitest/coverage-v8": "^3.0.0",
"byte-size": "^9.0.0", "byte-size": "^9.0.0",
"cli-progress": "^3.12.0", "cli-progress": "^3.12.0",
@@ -36,7 +36,7 @@
"prettier-plugin-organize-imports": "^4.0.0", "prettier-plugin-organize-imports": "^4.0.0",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"typescript-eslint": "^8.28.0", "typescript-eslint": "^8.28.0",
"vite": "^7.0.0", "vite": "^6.0.0",
"vite-tsconfig-paths": "^5.0.0", "vite-tsconfig-paths": "^5.0.0",
"vitest": "^3.0.0", "vitest": "^3.0.0",
"vitest-fetch-mock": "^0.4.0", "vitest-fetch-mock": "^0.4.0",
@@ -69,6 +69,6 @@
"micromatch": "^4.0.8" "micromatch": "^4.0.8"
}, },
"volta": { "volta": {
"node": "22.17.0" "node": "22.16.0"
} }
} }

2825
cli/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

22
docker/.env.bak Normal file
View File

@@ -0,0 +1,22 @@
# You can find documentation for all the supported env variables at https://immich.app/docs/install/environment-variables
# The location where your uploaded files are stored
UPLOAD_LOCATION=/LUNA/ALPHA/MEDIA/PHOTOS/immich_dev_library
# The location where your database files are stored. Network shares are not supported for the database
DB_DATA_LOCATION=./postgres
# To set a timezone, uncomment the next line and change Etc/UTC to a TZ identifier from this list: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List
# TZ=Etc/UTC
# The Immich version to use. You can pin this to a specific version like "v1.71.0"
IMMICH_VERSION=release
# Connection secret for postgres. You should change it to a random password
# Please use only the characters `A-Za-z0-9`, without special characters or spaces
DB_PASSWORD=postgres
# The values below this line do not need to be changed
###################################################################################
DB_USERNAME=postgres
DB_DATABASE_NAME=immich

View File

@@ -24,11 +24,11 @@ services:
build: build:
context: ../ context: ../
dockerfile: server/Dockerfile dockerfile: server/Dockerfile
target: dev target: dev-docker
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- ../server:/usr/src/app - ../server:/usr/src/app
- ../open-api:/usr/src/open-api - ../open-api/:/usr/src/open-api
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload - ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload - ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload
- /usr/src/app/node_modules - /usr/src/app/node_modules
@@ -69,7 +69,8 @@ services:
# Needed for rootless docker setup, see https://github.com/moby/moby/issues/45919 # Needed for rootless docker setup, see https://github.com/moby/moby/issues/45919
# user: 0:0 # user: 0:0
build: build:
context: ../web context: ../
dockerfile: web/Dockerfile
command: ['/usr/src/app/bin/immich-web'] command: ['/usr/src/app/bin/immich-web']
env_file: env_file:
- .env - .env
@@ -79,7 +80,7 @@ services:
volumes: volumes:
- ../web:/usr/src/app - ../web:/usr/src/app
- ../i18n:/usr/src/i18n - ../i18n:/usr/src/i18n
- ../open-api/:/usr/src/open-api/ - ../open-api/:/usr/src/open-api
# - ../../ui:/usr/ui # - ../../ui:/usr/ui
- /usr/src/app/node_modules - /usr/src/app/node_modules
ulimits: ulimits:
@@ -116,7 +117,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm@sha256:facc1d2c3462975c34e10fccb167bfa92b0e0dbd992fc282c29a61c3243afb11 image: docker.io/valkey/valkey:8-bookworm@sha256:fec42f399876eb6faf9e008570597741c87ff7662a54185593e74b09ce83d177
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1

View File

@@ -56,7 +56,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm@sha256:facc1d2c3462975c34e10fccb167bfa92b0e0dbd992fc282c29a61c3243afb11 image: docker.io/valkey/valkey:8-bookworm@sha256:fec42f399876eb6faf9e008570597741c87ff7662a54185593e74b09ce83d177
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1
restart: always restart: always
@@ -83,7 +83,7 @@ services:
container_name: immich_prometheus container_name: immich_prometheus
ports: ports:
- 9090:9090 - 9090:9090
image: prom/prometheus@sha256:63805ebb8d2b3920190daf1cb14a60871b16fd38bed42b857a3182bc621f4996 image: prom/prometheus@sha256:9abc6cf6aea7710d163dbb28d8eeb7dc5baef01e38fa4cd146a406dd9f07f70d
volumes: volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml - ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus - prometheus-data:/prometheus

View File

@@ -49,7 +49,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm@sha256:facc1d2c3462975c34e10fccb167bfa92b0e0dbd992fc282c29a61c3243afb11 image: docker.io/valkey/valkey:8-bookworm@sha256:fec42f399876eb6faf9e008570597741c87ff7662a54185593e74b09ce83d177
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1
restart: always restart: always

View File

@@ -1 +1 @@
22.17.0 22.16.0

View File

@@ -5,7 +5,7 @@ This website is built using [Docusaurus](https://docusaurus.io/), a modern stati
### Installation ### Installation
``` ```
$ npm install $ pnpm install
``` ```
### Local Development ### Local Development

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -46,12 +46,6 @@ 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. 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" /> <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.
:::

View File

@@ -62,7 +62,6 @@ 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) | | 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) | | 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**¹** | | 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**¹** | | 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) | | 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 | | Button Text | string | Login with OAuth | Text for the OAuth button on the web |

View File

@@ -200,7 +200,7 @@ When the Dev Container starts, it automatically:
1. **Runs post-create script** (`container-server-post-create.sh`): 1. **Runs post-create script** (`container-server-post-create.sh`):
- Adjusts file permissions for the `node` user - Adjusts file permissions for the `node` user
- Installs dependencies: `npm install` in all packages - Installs dependencies: `pnpm install` in all packages
- Builds TypeScript SDK: `npm run build` in `open-api/typescript-sdk` - Builds TypeScript SDK: `npm run build` in `open-api/typescript-sdk`
2. **Starts development servers** via VS Code tasks: 2. **Starts development servers** via VS Code tasks:

View File

@@ -56,7 +56,7 @@ If you only want to do web development connected to an existing, remote backend,
1. Build the Immich SDK - `cd open-api/typescript-sdk && npm i && npm run build && cd -` 1. Build the Immich SDK - `cd open-api/typescript-sdk && npm i && npm run build && cd -`
2. Enter the web directory - `cd web/` 2. Enter the web directory - `cd web/`
3. Install web dependencies - `npm i` 3. Install web dependencies - `pnpm i`
4. Start the web development server 4. Start the web development server
```bash ```bash

View File

@@ -5,7 +5,7 @@
### Unit tests ### Unit tests
Unit are run by calling `npm run test` from the `server/` directory. Unit are run by calling `npm run test` from the `server/` directory.
You need to run `npm install` (in `server/`) before _once_. You need to run `pnpm install` (in `server/`) before _once_.
### End to end tests ### End to end tests

View File

@@ -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. 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 > Folders`](https://my.immich.app/user-settings?isOpen=feature+folders) You can enable this feature under [`Account Settings > Features > Folder View`](https://my.immich.app/user-settings?isOpen=feature+folders)
## Enable folder view ## Enable folder view

View File

@@ -56,7 +56,7 @@ Internally, Immich uses the [glob](https://www.npmjs.com/package/glob) package t
### Automatic watching (EXPERIMENTAL) ### Automatic watching (EXPERIMENTAL)
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. 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.
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. 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. These actions must be performed by the Immich administrator.
- Click on your avatar in the upper right corner - Click on your avatar on the upper right corner
- Click on Administration -> External Libraries - Click on Administration -> External Libraries
- Click on Create an external library… - Click on Create an external library…
- Select which user owns the library, this can not be changed later - Select which user owns the library, this can not be changed later
@@ -159,7 +159,9 @@ 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. 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 > Folders`](https://my.immich.app/user-settings?isOpen=feature+folders) 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.
<img src={require('./img/folder-view-1.webp').default} width="100%" title='Folder-view' /> <img src={require('./img/folder-view-1.webp').default} width="100%" title='Folder-view' />
@@ -169,7 +171,7 @@ You can enable this feature under [`Account Settings > Features > Folders`](http
Only an admin can do this. Only an admin can do this.
::: :::
You can define a custom interval for the trigger external library rescan under Administration -> Settings -> External Library. You can define a custom interval for the trigger external library rescan under Administration -> Settings -> Library.
You can set the scanning interval using the preset or cron format. For more information you can refer to [Crontab Guru](https://crontab.guru/). 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' /> <img src={require('./img/library-custom-scan-interval.webp').default} width="75%" title='Set custom scan interval for external library' />

View File

@@ -16,7 +16,7 @@ For the full list, refer to the [Immich source code](https://github.com/immich-a
| `HEIC` | `.heic` | :white_check_mark: | | | `HEIC` | `.heic` | :white_check_mark: | |
| `HEIF` | `.heif` | :white_check_mark: | | | `HEIF` | `.heif` | :white_check_mark: | |
| `JPEG 2000` | `.jp2` | :white_check_mark: | | | `JPEG 2000` | `.jp2` | :white_check_mark: | |
| `JPEG` | `.jpeg` `.jpg` `.jpe` `.insp` | :white_check_mark: | | | `JPEG` | `.webp` `.jpg` `.jpe` `.insp` | :white_check_mark: | |
| `JPEG XL` | `.jxl` | :white_check_mark: | | | `JPEG XL` | `.jxl` | :white_check_mark: | |
| `PNG` | `.png` | :white_check_mark: | | | `PNG` | `.png` | :white_check_mark: | |
| `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop | | `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop |

View File

@@ -41,7 +41,7 @@ In the Immich web UI:
- Click Add path - Click Add path
<img src={require('./img/add-path-button.webp').default} width="50%" title="Add Path button" /> <img src={require('./img/add-path-button.webp').default} width="50%" title="Add Path button" />
- Enter **/home/user/photos1** as the path and click Add - Enter **/usr/src/app/external** as the path and click Add
<img src={require('./img/add-path-field.webp').default} width="50%" title="Add Path field" /> <img src={require('./img/add-path-field.webp').default} width="50%" title="Add Path field" />
- Save the new path - Save the new path

View File

@@ -72,25 +72,22 @@ Information on the current workers can be found [here](/docs/administration/jobs
## Database ## Database
| Variable | Description | Default | Containers | | Variable | Description | Default | Containers |
| :---------------------------------- | :------------------------------------------------------------------------------------- | :--------: | :----------------------------- | | :---------------------------------- | :--------------------------------------------------------------------------- | :--------: | :----------------------------- |
| `DB_URL` | Database URL | | server | | `DB_URL` | Database URL | | server |
| `DB_HOSTNAME` | Database host | `database` | server | | `DB_HOSTNAME` | Database host | `database` | server |
| `DB_PORT` | Database port | `5432` | server | | `DB_PORT` | Database port | `5432` | server |
| `DB_USERNAME` | Database user | `postgres` | server, database<sup>\*1</sup> | | `DB_USERNAME` | Database user | `postgres` | server, database<sup>\*1</sup> |
| `DB_PASSWORD` | Database password | `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_DATABASE_NAME` | Database name | `immich` | server, database<sup>\*1</sup> |
| `DB_SSL_MODE` | Database SSL mode | | server | | `DB_SSL_MODE` | Database SSL mode | | server |
| `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database vector extension (one of [`vectorchord`, `pgvector`, `pgvecto.rs`]) | | 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_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`. \*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. \*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 :::info
All `DB_` variables must be provided to all Immich workers, including `api` and `microservices`. All `DB_` variables must be provided to all Immich workers, including `api` and `microservices`.

20545
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -16,9 +16,9 @@
"write-heading-ids": "docusaurus write-heading-ids" "write-heading-ids": "docusaurus write-heading-ids"
}, },
"dependencies": { "dependencies": {
"@docusaurus/core": "~3.8.0", "@docusaurus/core": "~3.7.0",
"@docusaurus/preset-classic": "~3.8.0", "@docusaurus/preset-classic": "~3.7.0",
"@docusaurus/theme-common": "~3.8.0", "@docusaurus/theme-common": "~3.7.0",
"@mdi/js": "^7.3.67", "@mdi/js": "^7.3.67",
"@mdi/react": "^1.6.1", "@mdi/react": "^1.6.1",
"@mdx-js/react": "^3.0.0", "@mdx-js/react": "^3.0.0",
@@ -37,7 +37,7 @@
"url": "^0.11.0" "url": "^0.11.0"
}, },
"devDependencies": { "devDependencies": {
"@docusaurus/module-type-aliases": "~3.8.0", "@docusaurus/module-type-aliases": "~3.7.0",
"@docusaurus/tsconfig": "^3.7.0", "@docusaurus/tsconfig": "^3.7.0",
"@docusaurus/types": "^3.7.0", "@docusaurus/types": "^3.7.0",
"prettier": "^3.2.4", "prettier": "^3.2.4",
@@ -59,6 +59,6 @@
"node": ">=20" "node": ">=20"
}, },
"volta": { "volta": {
"node": "22.17.0" "node": "22.16.0"
} }
} }

13672
docs/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -58,12 +58,6 @@ const guides: CommunityGuidesProps[] = [
description: 'Access Immich with an end-to-end encrypted connection.', description: 'Access Immich with an end-to-end encrypted connection.',
url: 'https://meshnet.nordvpn.com/how-to/remote-files-media-access/immich-remote-access', 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 { function CommunityGuide({ title, description, url }: CommunityGuidesProps): JSX.Element {

View File

@@ -85,7 +85,6 @@ import React from 'react';
import { Item, Timeline } from '../components/timeline'; import { Item, Timeline } from '../components/timeline';
const releases = { const releases = {
'v1.135.0': new Date(2025, 5, 18),
'v1.133.0': new Date(2025, 4, 21), 'v1.133.0': new Date(2025, 4, 21),
'v1.130.0': new Date(2025, 2, 25), 'v1.130.0': new Date(2025, 2, 25),
'v1.127.0': new Date(2025, 1, 26), 'v1.127.0': new Date(2025, 1, 26),
@@ -197,6 +196,14 @@ const roadmap: Item[] = [
description: 'Automate tasks with workflows', description: 'Automate tasks with workflows',
getDateLabel: () => 'Planned for 2025', 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, done: false,
icon: mdiImageEdit, icon: mdiImageEdit,
@@ -232,26 +239,12 @@ const roadmap: Item[] = [
]; ];
const milestones: 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({ withRelease({
icon: mdiCast, icon: mdiCast,
iconColor: 'aqua', iconColor: 'aqua',
title: 'Google Cast (web and mobile)', title: 'Google Cast (web)',
description: 'Cast assets to Google Cast/Chromecast compatible devices', description: 'Cast assets to Google Cast/Chromecast compatible devices',
release: 'v1.135.0', release: 'v1.133.0',
}), }),
withRelease({ withRelease({
icon: mdiLockOutline, icon: mdiLockOutline,

View File

@@ -1,5 +1,4 @@
/docs /docs/overview/welcome 307 /docs /docs/overview/introduction 307
/docs/ /docs/overview/welcome 307
/docs/mobile-app-beta-program /docs/features/mobile-app 307 /docs/mobile-app-beta-program /docs/features/mobile-app 307
/docs/contribution-guidelines /docs/overview/support-the-project#contributing 307 /docs/contribution-guidelines /docs/overview/support-the-project#contributing 307
/docs/install /docs/install/docker-compose 307 /docs/install /docs/install/docker-compose 307

View File

@@ -1 +1 @@
22.17.0 22.16.0

View File

@@ -36,7 +36,7 @@ services:
- 2285:2285 - 2285:2285
redis: redis:
image: redis:6.2-alpine@sha256:03fd052257735b41cd19f3d8ae9782926bf9b704fb6a9dc5e29f9ccfbe8827f0 image: redis:6.2-alpine@sha256:3211c33a618c457e5d241922c975dbc4f446d0bdb2dc75694f5573ef8e2d01fa
database: database:
image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:3aef84a0a4fabbda17ef115c3019ba0c914ec73e9f6e59203674322d858b8eea image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:3aef84a0a4fabbda17ef115c3019ba0c914ec73e9f6e59203674322d858b8eea

948
e2e/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,9 +24,8 @@
"@immich/cli": "file:../cli", "@immich/cli": "file:../cli",
"@immich/sdk": "file:../open-api/typescript-sdk", "@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1", "@playwright/test": "^1.44.1",
"@socket.io/component-emitter": "^3.1.2",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/node": "^22.15.33", "@types/node": "^22.15.32",
"@types/oidc-provider": "^9.0.0", "@types/oidc-provider": "^9.0.0",
"@types/pg": "^8.15.1", "@types/pg": "^8.15.1",
"@types/pngjs": "^6.0.4", "@types/pngjs": "^6.0.4",
@@ -45,7 +44,7 @@
"pngjs": "^7.0.0", "pngjs": "^7.0.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^4.0.0", "prettier-plugin-organize-imports": "^4.0.0",
"sharp": "^0.34.0", "sharp": "^0.33.5",
"socket.io-client": "^4.7.4", "socket.io-client": "^4.7.4",
"supertest": "^7.0.0", "supertest": "^7.0.0",
"typescript": "^5.3.3", "typescript": "^5.3.3",
@@ -54,6 +53,6 @@
"vitest": "^3.0.0" "vitest": "^3.0.0"
}, },
"volta": { "volta": {
"node": "22.17.0" "node": "22.16.0"
} }
} }

4470
e2e/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,6 @@ import {
ReactionType, ReactionType,
createActivity as create, createActivity as create,
createAlbum, createAlbum,
removeAssetFromAlbum,
} from '@immich/sdk'; } from '@immich/sdk';
import { createUserDto, uuidDto } from 'src/fixtures'; import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses'; import { errorDto } from 'src/responses';
@@ -343,36 +342,5 @@ describe('/activities', () => {
expect(status).toBe(204); 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([]);
});
}); });
}); });

View File

@@ -20,7 +20,7 @@ describe('/api-keys', () => {
}); });
beforeEach(async () => { beforeEach(async () => {
await utils.resetDatabase(['api_key']); await utils.resetDatabase(['api_keys']);
}); });
describe('POST /api-keys', () => { describe('POST /api-keys', () => {

View File

@@ -227,21 +227,6 @@ describe(`/oauth`, () => {
expect(user.storageLabel).toBe('user-username'); 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 () => { it('should work with RS256 signed tokens', async () => {
await setupOAuth(admin.accessToken, { await setupOAuth(admin.accessToken, {
enabled: true, enabled: true,

View File

@@ -117,13 +117,6 @@ describe('/shared-links', () => {
const resp = await request(shareUrl).get(`/${linkWithAssets.key}`); const resp = await request(shareUrl).get(`/${linkWithAssets.key}`);
expect(resp.status).toBe(200); expect(resp.status).toBe(200);
expect(resp.header['content-type']).toContain('text/html'); 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`); expect(resp.text).toContain(`<meta property="og:image" content="https://my.immich.app`);
}); });

View File

@@ -15,6 +15,12 @@ describe('/system-config', () => {
}); });
describe('PUT /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 () => { it('should always return the new config', async () => {
const config = await getSystemConfig(admin.accessToken); const config = await getSystemConfig(admin.accessToken);

View File

@@ -37,7 +37,7 @@ describe('/tags', () => {
beforeEach(async () => { beforeEach(async () => {
// tagging assets eventually triggers metadata extraction which can impact other tests // tagging assets eventually triggers metadata extraction which can impact other tests
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
await utils.resetDatabase(['tag']); await utils.resetDatabase(['tags']);
}); });
describe('POST /tags', () => { describe('POST /tags', () => {

View File

@@ -0,0 +1,230 @@
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]);
});
});
});

View File

@@ -97,7 +97,7 @@ describe(`immich upload`, () => {
}); });
beforeEach(async () => { beforeEach(async () => {
await utils.resetDatabase(['asset', 'album']); await utils.resetDatabase(['assets', 'albums']);
}); });
describe(`immich upload /path/to/file.jpg`, () => { describe(`immich upload /path/to/file.jpg`, () => {

View File

@@ -116,7 +116,6 @@ export const deviceDto = {
createdAt: expect.any(String), createdAt: expect.any(String),
updatedAt: expect.any(String), updatedAt: expect.any(String),
current: true, current: true,
isPendingSyncReset: false,
deviceOS: '', deviceOS: '',
deviceType: '', deviceType: '',
}, },

View File

@@ -12,7 +12,6 @@ export enum OAuthUser {
NO_NAME = 'no-name', NO_NAME = 'no-name',
WITH_QUOTA = 'with-quota', WITH_QUOTA = 'with-quota',
WITH_USERNAME = 'with-username', WITH_USERNAME = 'with-username',
WITH_ROLE = 'with-role',
} }
const claims = [ const claims = [
@@ -35,12 +34,6 @@ const claims = [
preferred_username: 'user-quota', preferred_username: 'user-quota',
immich_quota: 25, immich_quota: 25,
}, },
{
sub: OAuthUser.WITH_ROLE,
email: 'oauth-with-role@immich.app',
email_verified: true,
immich_role: 'admin',
},
]; ];
const withDefaultClaims = (sub: string) => ({ const withDefaultClaims = (sub: string) => ({
@@ -71,15 +64,7 @@ const setup = async () => {
claims: { claims: {
openid: ['sub'], openid: ['sub'],
email: ['email', 'email_verified'], email: ['email', 'email_verified'],
profile: [ profile: ['name', 'given_name', 'family_name', 'preferred_username', 'immich_quota', 'immich_username'],
'name',
'given_name',
'family_name',
'preferred_username',
'immich_quota',
'immich_username',
'immich_role',
],
}, },
features: { features: {
jwtUserinfo: { jwtUserinfo: {

View File

@@ -60,7 +60,6 @@ import { io, type Socket } from 'socket.io-client';
import { loginDto, signupDto } from 'src/fixtures'; import { loginDto, signupDto } from 'src/fixtures';
import { makeRandomImage } from 'src/generators'; import { makeRandomImage } from 'src/generators';
import request from 'supertest'; import request from 'supertest';
export type { Emitter } from '@socket.io/component-emitter';
type CommandResponse = { stdout: string; stderr: string; exitCode: number | null }; type CommandResponse = { stdout: string; stderr: string; exitCode: number | null };
type EventType = 'assetUpload' | 'assetUpdate' | 'assetDelete' | 'userDelete' | 'assetHidden'; type EventType = 'assetUpload' | 'assetUpdate' | 'assetDelete' | 'userDelete' | 'assetHidden';
@@ -85,10 +84,10 @@ export const immichAdmin = (args: string[]) =>
export const specialCharStrings = ["'", '"', ',', '{', '}', '*']; export const specialCharStrings = ["'", '"', ',', '{', '}', '*'];
export const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; export const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
const executeCommand = (command: string, args: string[], options?: { cwd?: string }) => { const executeCommand = (command: string, args: string[]) => {
let _resolve: (value: CommandResponse) => void; let _resolve: (value: CommandResponse) => void;
const promise = new Promise<CommandResponse>((resolve) => (_resolve = resolve)); const promise = new Promise<CommandResponse>((resolve) => (_resolve = resolve));
const child = spawn(command, args, { stdio: 'pipe', cwd: options?.cwd }); const child = spawn(command, args, { stdio: 'pipe' });
let stdout = ''; let stdout = '';
let stderr = ''; let stderr = '';
@@ -154,19 +153,19 @@ export const utils = {
tables = tables || [ tables = tables || [
// TODO e2e test for deleting a stack, since it is quite complex // TODO e2e test for deleting a stack, since it is quite complex
'stack', 'asset_stack',
'library', 'libraries',
'shared_link', 'shared_links',
'person', 'person',
'album', 'albums',
'asset', 'assets',
'asset_face', 'asset_faces',
'activity', 'activity',
'api_key', 'api_keys',
'session', 'sessions',
'user', 'users',
'system_metadata', 'system_metadata',
'tag', 'tags',
]; ];
const sql: string[] = []; const sql: string[] = [];
@@ -175,7 +174,7 @@ export const utils = {
if (table === 'system_metadata') { if (table === 'system_metadata') {
sql.push(`DELETE FROM "system_metadata" where "key" NOT IN ('reverse-geocoding-state', 'system-flags');`); sql.push(`DELETE FROM "system_metadata" where "key" NOT IN ('reverse-geocoding-state', 'system-flags');`);
} else { } else {
sql.push(`DELETE FROM "${table}" CASCADE;`); sql.push(`DELETE FROM ${table} CASCADE;`);
} }
} }
@@ -451,7 +450,7 @@ export const utils = {
return; return;
} }
await client.query('INSERT INTO asset_face ("assetId", "personId") VALUES ($1, $2)', [assetId, personId]); await client.query('INSERT INTO asset_faces ("assetId", "personId") VALUES ($1, $2)', [assetId, personId]);
}, },
setPersonThumbnail: async (personId: string) => { setPersonThumbnail: async (personId: string) => {

View File

@@ -166,20 +166,6 @@
"metadata_settings_description": "Manage metadata settings", "metadata_settings_description": "Manage metadata settings",
"migration_job": "Migration", "migration_job": "Migration",
"migration_job_description": "Migrate thumbnails for assets and faces to the latest folder structure", "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_paths_added": "No paths added",
"no_pattern_added": "No pattern 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", "note_apply_storage_label_previous_assets": "Note: To apply the Storage Label to previously uploaded assets, run the",
@@ -210,8 +196,6 @@
"oauth_mobile_redirect_uri": "Mobile redirect URI", "oauth_mobile_redirect_uri": "Mobile redirect URI",
"oauth_mobile_redirect_uri_override": "Mobile redirect URI override", "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_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": "OAuth",
"oauth_settings_description": "Manage OAuth login settings", "oauth_settings_description": "Manage OAuth login settings",
"oauth_settings_more_details": "For more details about this feature, refer to the <link>docs</link>.", "oauth_settings_more_details": "For more details about this feature, refer to the <link>docs</link>.",
@@ -443,7 +427,6 @@
"app_settings": "App Settings", "app_settings": "App Settings",
"appears_in": "Appears in", "appears_in": "Appears in",
"archive": "Archive", "archive": "Archive",
"archive_action_prompt": "{count} added to Archive",
"archive_or_unarchive_photo": "Archive or unarchive photo", "archive_or_unarchive_photo": "Archive or unarchive photo",
"archive_page_no_archived_assets": "No archived assets found", "archive_page_no_archived_assets": "No archived assets found",
"archive_page_title": "Archive ({count})", "archive_page_title": "Archive ({count})",
@@ -719,7 +702,7 @@
"daily_title_text_date": "E, MMM dd", "daily_title_text_date": "E, MMM dd",
"daily_title_text_date_year": "E, MMM dd, yyyy", "daily_title_text_date_year": "E, MMM dd, yyyy",
"dark": "Dark", "dark": "Dark",
"dark_theme": "Toggle dark theme", "darkTheme": "Toggle dark theme",
"date_after": "Date after", "date_after": "Date after",
"date_and_time": "Date and Time", "date_and_time": "Date and Time",
"date_before": "Date before", "date_before": "Date before",
@@ -735,7 +718,6 @@
"default_locale": "Default Locale", "default_locale": "Default Locale",
"default_locale_description": "Format dates and numbers based on your browser locale", "default_locale_description": "Format dates and numbers based on your browser locale",
"delete": "Delete", "delete": "Delete",
"delete_action_prompt": "{count} deleted permanently",
"delete_album": "Delete album", "delete_album": "Delete album",
"delete_api_key_prompt": "Are you sure you want to delete this API key?", "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", "delete_dialog_alert": "These items will be permanently deleted from Immich and from your device",
@@ -749,7 +731,6 @@
"delete_key": "Delete key", "delete_key": "Delete key",
"delete_library": "Delete Library", "delete_library": "Delete Library",
"delete_link": "Delete link", "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_backed_up_only": "Delete Backed Up Only",
"delete_local_dialog_ok_force": "Delete Anyway", "delete_local_dialog_ok_force": "Delete Anyway",
"delete_others": "Delete others", "delete_others": "Delete others",
@@ -817,7 +798,6 @@
"edit_key": "Edit key", "edit_key": "Edit key",
"edit_link": "Edit link", "edit_link": "Edit link",
"edit_location": "Edit location", "edit_location": "Edit location",
"edit_location_action_prompt": "{count} location edited",
"edit_location_dialog_title": "Location", "edit_location_dialog_title": "Location",
"edit_name": "Edit name", "edit_name": "Edit name",
"edit_people": "Edit people", "edit_people": "Edit people",
@@ -1003,7 +983,6 @@
"failed_to_load_assets": "Failed to load assets", "failed_to_load_assets": "Failed to load assets",
"failed_to_load_folder": "Failed to load folder", "failed_to_load_folder": "Failed to load folder",
"favorite": "Favorite", "favorite": "Favorite",
"favorite_action_prompt": "{count} added to Favorites",
"favorite_or_unfavorite_photo": "Favorite or unfavorite photo", "favorite_or_unfavorite_photo": "Favorite or unfavorite photo",
"favorites": "Favorites", "favorites": "Favorites",
"favorites_page_no_favorites": "No favorite assets found", "favorites_page_no_favorites": "No favorite assets found",
@@ -1147,7 +1126,6 @@
"library_page_sort_created": "Created date", "library_page_sort_created": "Created date",
"library_page_sort_last_modified": "Last modified", "library_page_sort_last_modified": "Last modified",
"library_page_sort_title": "Album title", "library_page_sort_title": "Album title",
"licenses": "Licenses",
"light": "Light", "light": "Light",
"like_deleted": "Like deleted", "like_deleted": "Like deleted",
"link_motion_video": "Link motion video", "link_motion_video": "Link motion video",
@@ -1267,7 +1245,6 @@
"more": "More", "more": "More",
"move": "Move", "move": "Move",
"move_off_locked_folder": "Move out of locked folder", "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": "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", "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", "moved_to_archive": "Moved {count, plural, one {# asset} other {# assets}} to archive",
@@ -1517,9 +1494,7 @@
"remove_custom_date_range": "Remove custom date range", "remove_custom_date_range": "Remove custom date range",
"remove_deleted_assets": "Remove Deleted Assets", "remove_deleted_assets": "Remove Deleted Assets",
"remove_from_album": "Remove from album", "remove_from_album": "Remove from album",
"remove_from_album_action_prompt": "{count} removed from the album",
"remove_from_favorites": "Remove from favorites", "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": "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_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", "remove_from_shared_link": "Remove from shared link",
@@ -1862,7 +1837,6 @@
"total": "Total", "total": "Total",
"total_usage": "Total usage", "total_usage": "Total usage",
"trash": "Trash", "trash": "Trash",
"trash_action_prompt": "{count} moved to trash",
"trash_all": "Trash All", "trash_all": "Trash All",
"trash_count": "Trash {count, number}", "trash_count": "Trash {count, number}",
"trash_delete_asset": "Trash/Delete Asset", "trash_delete_asset": "Trash/Delete Asset",
@@ -1880,11 +1854,9 @@
"unable_to_change_pin_code": "Unable to change PIN code", "unable_to_change_pin_code": "Unable to change PIN code",
"unable_to_setup_pin_code": "Unable to setup PIN code", "unable_to_setup_pin_code": "Unable to setup PIN code",
"unarchive": "Unarchive", "unarchive": "Unarchive",
"unarchive_action_prompt": "{count} removed from Archive",
"unarchived_count": "{count, plural, other {Unarchived #}}", "unarchived_count": "{count, plural, other {Unarchived #}}",
"undo": "Undo", "undo": "Undo",
"unfavorite": "Unfavorite", "unfavorite": "Unfavorite",
"unfavorite_action_prompt": "{count} removed from Favorites",
"unhide_person": "Unhide person", "unhide_person": "Unhide person",
"unknown": "Unknown", "unknown": "Unknown",
"unknown_country": "Unknown Country", "unknown_country": "Unknown Country",
@@ -1903,7 +1875,6 @@
"unselect_all_in": "Unselect all in {group}", "unselect_all_in": "Unselect all in {group}",
"unstack": "Un-stack", "unstack": "Un-stack",
"unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}", "unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}",
"untagged": "Untagged",
"up_next": "Up next", "up_next": "Up next",
"updated_at": "Updated", "updated_at": "Updated",
"updated_password": "Updated password", "updated_password": "Updated password",
@@ -1940,7 +1911,6 @@
"user_usage_stats_description": "View account usage statistics", "user_usage_stats_description": "View account usage statistics",
"username": "Username", "username": "Username",
"users": "Users", "users": "Users",
"users_added_to_album_count": "Added {count, plural, one {# user} other {# users}} to the album",
"utilities": "Utilities", "utilities": "Utilities",
"validate": "Validate", "validate": "Validate",
"validate_endpoint_error": "Please enter a valid URL", "validate_endpoint_error": "Please enter a valid URL",

View File

@@ -4,12 +4,9 @@ import sys
import requests import requests
port = os.getenv("IMMICH_PORT", 3003) 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: try:
response = requests.get(f"http://{host}:{port}/ping", timeout=2) response = requests.get(f"http://localhost:{port}/ping", timeout=2)
if response.status_code == 200: if response.status_code == 200:
sys.exit(0) sys.exit(0)
sys.exit(1) sys.exit(1)

View File

@@ -517,16 +517,16 @@ wheels = [
[[package]] [[package]]
name = "fastapi" name = "fastapi"
version = "0.115.14" version = "0.115.13"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "pydantic" }, { name = "pydantic" },
{ name = "starlette" }, { name = "starlette" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
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" } 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" }
wheels = [ wheels = [
{ 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" }, { 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" },
] ]
[[package]] [[package]]
@@ -900,7 +900,7 @@ wheels = [
[[package]] [[package]]
name = "huggingface-hub" name = "huggingface-hub"
version = "0.33.2" version = "0.33.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "filelock" }, { name = "filelock" },
@@ -912,9 +912,9 @@ dependencies = [
{ name = "tqdm" }, { name = "tqdm" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
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" } 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" }
wheels = [ wheels = [
{ 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" }, { 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" },
] ]
[[package]] [[package]]
@@ -1044,7 +1044,7 @@ requires-dist = [
{ name = "onnxruntime", marker = "extra == 'armnn'", specifier = ">=1.15.0,<2" }, { name = "onnxruntime", marker = "extra == 'armnn'", specifier = ">=1.15.0,<2" },
{ name = "onnxruntime", marker = "extra == 'cpu'", 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", 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 = "onnxruntime-openvino", marker = "extra == 'openvino'", specifier = ">=1.17.1,<1.19.0" },
{ name = "opencv-python-headless", specifier = ">=4.7.0.72,<5.0" }, { name = "opencv-python-headless", specifier = ">=4.7.0.72,<5.0" },
{ name = "orjson", specifier = ">=3.9.5" }, { name = "orjson", specifier = ">=3.9.5" },
@@ -1568,7 +1568,7 @@ wheels = [
[[package]] [[package]]
name = "onnxruntime-gpu" name = "onnxruntime-gpu"
version = "1.19.2" 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 = [ dependencies = [
{ name = "coloredlogs" }, { name = "coloredlogs" },
{ name = "flatbuffers" }, { name = "flatbuffers" },
@@ -1936,16 +1936,16 @@ wheels = [
[[package]] [[package]]
name = "pydantic-settings" name = "pydantic-settings"
version = "2.10.1" version = "2.9.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "pydantic" }, { name = "pydantic" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
{ name = "typing-inspection" }, { name = "typing-inspection" },
] ]
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" } 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" }
wheels = [ wheels = [
{ 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" }, { 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" },
] ]
[[package]] [[package]]
@@ -2304,27 +2304,27 @@ wheels = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.12.2" version = "0.12.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
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" } 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" }
wheels = [ wheels = [
{ 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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" }, { 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" },
] ]
[[package]] [[package]]
@@ -2504,27 +2504,27 @@ wheels = [
[[package]] [[package]]
name = "tokenizers" name = "tokenizers"
version = "0.21.2" version = "0.21.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "huggingface-hub" }, { name = "huggingface-hub" },
] ]
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" } 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" }
wheels = [ wheels = [
{ 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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" }, { 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" },
] ]
[[package]] [[package]]
@@ -2628,16 +2628,16 @@ wheels = [
[[package]] [[package]]
name = "uvicorn" name = "uvicorn"
version = "0.35.0" version = "0.34.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "click" }, { name = "click" },
{ name = "h11" }, { name = "h11" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" },
] ]
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" } 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" }
wheels = [ wheels = [
{ 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" }, { 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" },
] ]
[package.optional-dependencies] [package.optional-dependencies]

View File

@@ -1,3 +1,3 @@
{ {
"flutter": "3.32.6" "flutter": "3.29.3"
} }

View File

@@ -1,5 +1,5 @@
{ {
"dart.flutterSdkPath": ".fvm/versions/3.32.6", "dart.flutterSdkPath": ".fvm/versions/3.29.3",
"search.exclude": { "search.exclude": {
"**/.fvm": true "**/.fvm": true
}, },

View File

@@ -66,20 +66,6 @@ android {
} }
} }
flavorDimensions "default"
productFlavors {
production {
dimension "default"
applicationId "app.alextran.immich"
}
beta {
dimension "default"
applicationId "app.alextran.immich.beta"
versionNameSuffix "-BETA"
}
}
buildTypes { buildTypes {
debug { debug {
applicationIdSuffix '.debug' applicationIdSuffix '.debug'

View File

@@ -1,5 +0,0 @@
<?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>

View File

@@ -100,24 +100,24 @@
<!-- my.immich.app deep link --> <!-- my.immich.app deep link -->
<intent-filter android:autoVerify="true"> <intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" /> <data android:scheme="https" />
<data <data
android:host="my.immich.app" android:host="my.immich.app"
android:path="/" /> android:path="/" />
<data <data
android:host="my.immich.app" android:host="my.immich.app"
android:pathPrefix="/albums/" /> android:pathPrefix="/albums/" />
<data <data
android:host="my.immich.app" android:host="my.immich.app"
android:pathPrefix="/memories/" /> android:pathPrefix="/memories/" />
<data <data
android:host="my.immich.app" android:host="my.immich.app"
android:pathPrefix="/photos/" /> android:pathPrefix="/photos/" />
</intent-filter> </intent-filter>
</activity> </activity>

View File

@@ -87,8 +87,7 @@ data class PlatformAsset (
val updatedAt: Long? = null, val updatedAt: Long? = null,
val width: Long? = null, val width: Long? = null,
val height: Long? = null, val height: Long? = null,
val durationInSeconds: Long, val durationInSeconds: Long
val orientation: Long
) )
{ {
companion object { companion object {
@@ -101,8 +100,7 @@ data class PlatformAsset (
val width = pigeonVar_list[5] as Long? val width = pigeonVar_list[5] as Long?
val height = pigeonVar_list[6] as Long? val height = pigeonVar_list[6] as Long?
val durationInSeconds = pigeonVar_list[7] as Long val durationInSeconds = pigeonVar_list[7] as Long
val orientation = pigeonVar_list[8] as Long return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds)
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation)
} }
} }
fun toList(): List<Any?> { fun toList(): List<Any?> {
@@ -115,7 +113,6 @@ data class PlatformAsset (
width, width,
height, height,
durationInSeconds, durationInSeconds,
orientation,
) )
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {

View File

@@ -5,7 +5,6 @@ import android.content.Context
import android.database.Cursor import android.database.Cursor
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log import android.util.Log
import androidx.core.database.getStringOrNull
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.security.MessageDigest import java.security.MessageDigest
@@ -40,8 +39,7 @@ open class NativeSyncApiImplBase(context: Context) {
MediaStore.MediaColumns.BUCKET_ID, MediaStore.MediaColumns.BUCKET_ID,
MediaStore.MediaColumns.WIDTH, MediaStore.MediaColumns.WIDTH,
MediaStore.MediaColumns.HEIGHT, MediaStore.MediaColumns.HEIGHT,
MediaStore.MediaColumns.DURATION, MediaStore.MediaColumns.DURATION
MediaStore.MediaColumns.ORIENTATION,
) )
const val HASH_BUFFER_SIZE = 2 * 1024 * 1024 const val HASH_BUFFER_SIZE = 2 * 1024 * 1024
@@ -75,8 +73,6 @@ open class NativeSyncApiImplBase(context: Context) {
val widthColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH) val widthColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH)
val heightColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT) val heightColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT)
val durationColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DURATION) val durationColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DURATION)
val orientationColumn =
c.getColumnIndexOrThrow(MediaStore.MediaColumns.ORIENTATION)
while (c.moveToNext()) { while (c.moveToNext()) {
val id = c.getLong(idColumn).toString() val id = c.getLong(idColumn).toString()
@@ -104,7 +100,6 @@ open class NativeSyncApiImplBase(context: Context) {
val duration = if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0 val duration = if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0
else c.getLong(durationColumn) / 1000 else c.getLong(durationColumn) / 1000
val bucketId = c.getString(bucketIdColumn) val bucketId = c.getString(bucketIdColumn)
val orientation = c.getInt(orientationColumn)
val asset = PlatformAsset( val asset = PlatformAsset(
id, id,
@@ -114,8 +109,7 @@ open class NativeSyncApiImplBase(context: Context) {
modifiedAt, modifiedAt,
width, width,
height, height,
duration, duration
orientation.toLong(),
) )
yield(AssetResult.ValidAsset(asset, bucketId)) yield(AssetResult.ValidAsset(asset, bucketId))
} }
@@ -158,8 +152,7 @@ open class NativeSyncApiImplBase(context: Context) {
continue continue
} }
// MediaStore might return null for bucket name (commonly for the Root Directory), so default to "Internal Storage" val name = cursor.getString(bucketNameColumn)
val name = cursor.getStringOrNull(bucketNameColumn) ?: "Internal Storage"
val updatedAt = cursor.getLong(dateModified) val updatedAt = cursor.getLong(dateModified)
albums.add(PlatformAlbum(id, name, updatedAt, false, 0)) albums.add(PlatformAlbum(id, name, updatedAt, false, 0))
albumsCount[id] = 1 albumsCount[id] = 1

File diff suppressed because one or more lines are too long

View File

@@ -5,34 +5,34 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _fe_analyzer_shared name: _fe_analyzer_shared
sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "82.0.0" version: "80.0.0"
analyzer: analyzer:
dependency: "direct main" dependency: "direct main"
description: description:
name: analyzer name: analyzer
sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0" sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.4.5" version: "7.3.0"
analyzer_plugin: analyzer_plugin:
dependency: "direct main" dependency: "direct main"
description: description:
name: analyzer_plugin name: analyzer_plugin
sha256: ee188b6df6c85f1441497c7171c84f1392affadc0384f71089cb10a3bc508cef sha256: b3075265c5ab222f8b3188342dcb50b476286394a40323e85d1fa725035d40a4
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.13.1" version: "0.13.0"
args: args:
dependency: transitive dependency: transitive
description: description:
name: args name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.7.0" version: "2.6.0"
async: async:
dependency: transitive dependency: transitive
description: description:
@@ -53,10 +53,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: checked_yaml name: checked_yaml
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.4" version: "2.0.3"
ci: ci:
dependency: transitive dependency: transitive
description: description:
@@ -125,10 +125,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: custom_lint_visitor name: custom_lint_visitor
sha256: cba5b6d7a6217312472bf4468cdf68c949488aed7ffb0eab792cd0b6c435054d sha256: "36282d85714af494ee2d7da8c8913630aa6694da99f104fb2ed4afcf8fc857d8"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0+7.4.5" version: "1.0.0+7.3.0"
dart_style: dart_style:
dependency: transitive dependency: transitive
description: description:
@@ -157,10 +157,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: freezed_annotation name: freezed_annotation
sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.0" version: "3.0.0"
glob: glob:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -213,18 +213,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.17.0" version: "1.16.0"
package_config: package_config:
dependency: transitive dependency: transitive
description: description:
name: package_config name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.1.1"
path: path:
dependency: transitive dependency: transitive
description: description:
@@ -317,10 +317,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.6" version: "0.7.4"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@@ -341,18 +341,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.0.2" version: "15.0.0"
watcher: watcher:
dependency: transitive dependency: transitive
description: description:
name: watcher name: watcher
sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.2" version: "1.1.1"
yaml: yaml:
dependency: transitive dependency: transitive
description: description:
@@ -362,4 +362,4 @@ packages:
source: hosted source: hosted
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.8.0 <4.0.0" dart: ">=3.8.0-0 <4.0.0"

View File

@@ -26,7 +26,6 @@
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES"> shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
@@ -44,7 +43,6 @@
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
disableMainThreadChecker = "YES" disableMainThreadChecker = "YES"
launchStyle = "0" launchStyle = "0"
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"

View File

@@ -138,7 +138,6 @@ struct PlatformAsset: Hashable {
var width: Int64? = nil var width: Int64? = nil
var height: Int64? = nil var height: Int64? = nil
var durationInSeconds: Int64 var durationInSeconds: Int64
var orientation: Int64
// swift-format-ignore: AlwaysUseLowerCamelCase // swift-format-ignore: AlwaysUseLowerCamelCase
@@ -151,7 +150,6 @@ struct PlatformAsset: Hashable {
let width: Int64? = nilOrValue(pigeonVar_list[5]) let width: Int64? = nilOrValue(pigeonVar_list[5])
let height: Int64? = nilOrValue(pigeonVar_list[6]) let height: Int64? = nilOrValue(pigeonVar_list[6])
let durationInSeconds = pigeonVar_list[7] as! Int64 let durationInSeconds = pigeonVar_list[7] as! Int64
let orientation = pigeonVar_list[8] as! Int64
return PlatformAsset( return PlatformAsset(
id: id, id: id,
@@ -161,8 +159,7 @@ struct PlatformAsset: Hashable {
updatedAt: updatedAt, updatedAt: updatedAt,
width: width, width: width,
height: height, height: height,
durationInSeconds: durationInSeconds, durationInSeconds: durationInSeconds
orientation: orientation
) )
} }
func toList() -> [Any?] { func toList() -> [Any?] {
@@ -175,7 +172,6 @@ struct PlatformAsset: Hashable {
width, width,
height, height,
durationInSeconds, durationInSeconds,
orientation,
] ]
} }
static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool { static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool {

View File

@@ -27,8 +27,7 @@ extension PHAsset {
updatedAt: modificationDate.map { Int64($0.timeIntervalSince1970) }, updatedAt: modificationDate.map { Int64($0.timeIntervalSince1970) },
width: Int64(pixelWidth), width: Int64(pixelWidth),
height: Int64(pixelHeight), height: Int64(pixelHeight),
durationInSeconds: Int64(duration), durationInSeconds: Int64(duration)
orientation: 0
) )
} }
} }
@@ -170,8 +169,7 @@ class NativeSyncApiImpl: NativeSyncApi {
id: asset.localIdentifier, id: asset.localIdentifier,
name: "", name: "",
type: 0, type: 0,
durationInSeconds: 0, durationInSeconds: 0
orientation: 0
) )
if (updatedAssets.contains(AssetWrapper(with: predicate))) { if (updatedAssets.contains(AssetWrapper(with: predicate))) {
continue continue

View File

@@ -0,0 +1,59 @@
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
}

View File

@@ -1,148 +0,0 @@
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
}

View File

@@ -1,14 +1,23 @@
import SwiftUI import SwiftUI
import WidgetKit import WidgetKit
extension Image { struct ImageEntry: TimelineEntry {
@ViewBuilder let date: Date
func tintedWidgetImageModifier() -> some View { var image: UIImage?
if #available(iOS 18.0, *) { var subtitle: String? = nil
self var error: WidgetError? = nil
.widgetAccentedRenderingMode(.accentedDesaturated) var deepLink: URL? = nil
} else {
self // 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
} }
} }
} }
@@ -20,8 +29,7 @@ struct ImmichWidgetView: View {
if entry.image == nil { if entry.image == nil {
VStack { VStack {
Image("LaunchImage") Image("LaunchImage")
.tintedWidgetImageModifier() Text(entry.error?.errorDescription ?? "")
Text(entry.metadata.error?.errorDescription ?? "")
.minimumScaleFactor(0.25) .minimumScaleFactor(0.25)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
@@ -32,13 +40,11 @@ struct ImmichWidgetView: View {
Color.clear.overlay( Color.clear.overlay(
Image(uiImage: entry.image!) Image(uiImage: entry.image!)
.resizable() .resizable()
.tintedWidgetImageModifier()
.scaledToFill() .scaledToFill()
) )
VStack { VStack {
Spacer() Spacer()
if let subtitle = entry.metadata.subtitle { if let subtitle = entry.subtitle {
Text(subtitle) Text(subtitle)
.foregroundColor(.white) .foregroundColor(.white)
.padding(8) .padding(8)
@@ -49,7 +55,7 @@ struct ImmichWidgetView: View {
} }
.padding(16) .padding(16)
} }
.widgetURL(entry.metadata.deepLink) .widgetURL(entry.deepLink)
} }
} }
} }
@@ -64,9 +70,7 @@ struct ImmichWidgetView: View {
ImageEntry( ImageEntry(
date: date, date: date,
image: UIImage(named: "ImmichLogo"), image: UIImage(named: "ImmichLogo"),
metadata: EntryMetadata( subtitle: "1 year ago"
subtitle: "1 year ago"
)
) )
} }
) )

View File

@@ -2,20 +2,14 @@ import Foundation
import SwiftUI import SwiftUI
import WidgetKit import WidgetKit
let IMMICH_SHARE_GROUP = "group.app.immich.share" enum WidgetError: Error {
enum WidgetError: Error, Codable {
case noLogin case noLogin
case fetchFailed case fetchFailed
case unknown
case albumNotFound case albumNotFound
case noAssetsAvailable
}
enum FetchError: Error {
case unableToResize case unableToResize
case invalidImage case invalidImage
case invalidURL case invalidURL
case fetchFailed
} }
extension WidgetError: LocalizedError { extension WidgetError: LocalizedError {
@@ -30,8 +24,14 @@ extension WidgetError: LocalizedError {
case .albumNotFound: case .albumNotFound:
return "Album not found" return "Album not found"
case .noAssetsAvailable: case .invalidURL:
return "No assets available" return "An invalid URL was used"
case .invalidImage:
return "An invalid image was received"
default:
return "An unknown error occured"
} }
} }
} }
@@ -52,11 +52,10 @@ struct Asset: Codable {
} }
} }
struct SearchFilter: Codable { struct SearchFilters: Codable {
var type = AssetType.image var type: AssetType = .image
var size = 1 let size: Int
var albumIds: [String] = [] var albumIds: [String] = []
var isFavorite: Bool? = nil
} }
struct MemoryResult: Codable { struct MemoryResult: Codable {
@@ -71,34 +70,9 @@ struct MemoryResult: Codable {
let data: MemoryData let data: MemoryData
} }
struct Album: Codable, Equatable { struct Album: Codable {
let id: String let id: String
let albumName: 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 // MARK: API
@@ -112,7 +86,7 @@ class ImmichAPI {
init() async throws { init() async throws {
// fetch the credentials from the UserDefaults store that dart placed here // fetch the credentials from the UserDefaults store that dart placed here
guard let defaults = UserDefaults(suiteName: IMMICH_SHARE_GROUP), guard let defaults = UserDefaults(suiteName: "group.app.immich.share"),
let serverURL = defaults.string(forKey: "widget_server_url"), let serverURL = defaults.string(forKey: "widget_server_url"),
let sessionKey = defaults.string(forKey: "widget_auth_token") let sessionKey = defaults.string(forKey: "widget_auth_token")
else { else {
@@ -156,8 +130,7 @@ class ImmichAPI {
return components?.url return components?.url
} }
func fetchSearchResults(with filters: SearchFilter = Album.NONE.filter) func fetchSearchResults(with filters: SearchFilters) async throws
async throws
-> [Asset] -> [Asset]
{ {
// get URL // get URL
@@ -203,7 +176,7 @@ class ImmichAPI {
return try JSONDecoder().decode([MemoryResult].self, from: data) return try JSONDecoder().decode([MemoryResult].self, from: data)
} }
func fetchImage(asset: Asset) async throws(FetchError) -> UIImage { func fetchImage(asset: Asset) async throws(WidgetError) -> UIImage {
let thumbnailParams = [URLQueryItem(name: "size", value: "preview")] let thumbnailParams = [URLQueryItem(name: "size", value: "preview")]
let assetEndpoint = "/assets/" + asset.id + "/thumbnail" let assetEndpoint = "/assets/" + asset.id + "/thumbnail"
@@ -217,24 +190,17 @@ class ImmichAPI {
throw .invalidURL throw .invalidURL
} }
guard let imageSource = CGImageSourceCreateWithURL(fetchURL as CFURL, nil) guard let imageSource = CGImageSourceCreateWithURL(fetchURL as CFURL, nil) else {
else {
throw .invalidURL throw .invalidURL
} }
let decodeOptions: [NSString: Any] = [ let decodeOptions: [NSString: Any] = [
kCGImageSourceCreateThumbnailFromImageAlways: true, kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceThumbnailMaxPixelSize: 512, kCGImageSourceThumbnailMaxPixelSize: 400,
kCGImageSourceCreateThumbnailWithTransform: true, kCGImageSourceCreateThumbnailWithTransform: true
] ]
guard guard let thumbnail = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, decodeOptions as CFDictionary) else {
let thumbnail = CGImageSourceCreateThumbnailAtIndex(
imageSource,
0,
decodeOptions as CFDictionary
)
else {
throw .fetchFailed throw .fetchFailed
} }

View File

@@ -2,11 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSExtension</key> <key>NSExtension</key>
<dict> <dict>
<key>NSExtensionPointIdentifier</key> <key>NSExtensionPointIdentifier</key>

View File

@@ -7,17 +7,14 @@
import UIKit import UIKit
extension UIImage { extension UIImage {
/// Crops the image to ensure width and height do not exceed maxSize. /// Crops the image to ensure width and height do not exceed maxSize.
/// Keeps original aspect ratio and crops excess equally from edges (center crop). /// Keeps original aspect ratio and crops excess equally from edges (center crop).
func resized(toWidth width: CGFloat, isOpaque: Bool = true) -> UIImage? { func resized(toWidth width: CGFloat, isOpaque: Bool = true) -> UIImage? {
let canvas = CGSize( let canvas = CGSize(width: width, height: CGFloat(ceil(width/size.width * size.height)))
width: width, let format = imageRendererFormat
height: CGFloat(ceil(width / size.width * size.height)) format.opaque = isOpaque
) return UIGraphicsImageRenderer(size: canvas, format: format).image {
let format = imageRendererFormat _ in draw(in: CGRect(origin: .zero, size: canvas))
format.opaque = isOpaque }
return UIGraphicsImageRenderer(size: canvas, format: format).image {
_ in draw(in: CGRect(origin: .zero, size: canvas))
} }
}
} }

View File

@@ -19,31 +19,28 @@ struct ImmichMemoryProvider: TimelineProvider {
in context: Context, in context: Context,
completion: @escaping @Sendable (ImageEntry) -> Void completion: @escaping @Sendable (ImageEntry) -> Void
) { ) {
let cacheKey = "memory_\(context.family.rawValue)"
Task { Task {
guard let api = try? await ImmichAPI() else { guard let api = try? await ImmichAPI() else {
completion( completion(ImageEntry(date: Date(), image: nil, error: .noLogin))
ImageEntry.handleError(for: cacheKey, error: .noLogin).entries.first!
)
return return
} }
guard let memories = try? await api.fetchMemory(for: Date.now) guard let memories = try? await api.fetchMemory(for: Date.now)
else { else {
completion(ImageEntry.handleError(for: cacheKey).entries.first!) completion(ImageEntry(date: Date(), image: nil, error: .fetchFailed))
return return
} }
for memory in memories { for memory in memories {
if let asset = memory.assets.first(where: { $0.type == .image }), if let asset = memory.assets.first(where: { $0.type == .image }),
let entry = try? await ImageEntry.build( var entry = try? await buildEntry(
api: api, api: api,
asset: asset, asset: asset,
dateOffset: 0, dateOffset: 0,
subtitle: getYearDifferenceSubtitle(assetYear: memory.data.year) subtitle: getYearDifferenceSubtitle(assetYear: memory.data.year)
) )
{ {
entry.resize()
completion(entry) completion(entry)
return return
} }
@@ -51,17 +48,26 @@ struct ImmichMemoryProvider: TimelineProvider {
// fallback to random image // fallback to random image
guard guard
let randomImage = try? await api.fetchSearchResults().first, let randomImage = try? await api.fetchSearchResults(
let imageEntry = try? await ImageEntry.build( with: SearchFilters(size: 1)
).first
else {
completion(ImageEntry(date: Date(), image: nil, error: .fetchFailed))
return
}
guard
var imageEntry = try? await buildEntry(
api: api, api: api,
asset: randomImage, asset: randomImage,
dateOffset: 0 dateOffset: 0
) )
else { else {
completion(ImageEntry.handleError(for: cacheKey).entries.first!) completion(ImageEntry(date: Date(), image: nil, error: .fetchFailed))
return return
} }
imageEntry.resize()
completion(imageEntry) completion(imageEntry)
} }
} }
@@ -74,12 +80,9 @@ struct ImmichMemoryProvider: TimelineProvider {
var entries: [ImageEntry] = [] var entries: [ImageEntry] = []
let now = Date() let now = Date()
let cacheKey = "memory_\(context.family.rawValue)"
guard let api = try? await ImmichAPI() else { guard let api = try? await ImmichAPI() else {
completion( entries.append(ImageEntry(date: now, image: nil, error: .noLogin))
ImageEntry.handleError(for: cacheKey, error: .noLogin) completion(Timeline(entries: entries, policy: .atEnd))
)
return return
} }
@@ -92,7 +95,7 @@ struct ImmichMemoryProvider: TimelineProvider {
for asset in memory.assets { for asset in memory.assets {
if asset.type == .image && totalAssets < 12 { if asset.type == .image && totalAssets < 12 {
group.addTask { group.addTask {
try? await ImageEntry.build( try? await buildEntry(
api: api, api: api,
asset: asset, asset: asset,
dateOffset: totalAssets, dateOffset: totalAssets,
@@ -117,32 +120,25 @@ struct ImmichMemoryProvider: TimelineProvider {
// If we didnt add any memory images (some failure occured or no images in memory), // If we didnt add any memory images (some failure occured or no images in memory),
// default to 12 hours of random photos // default to 12 hours of random photos
if entries.count == 0 { if entries.count == 0 {
// this must be a do/catch since we need to entries.append(
// distinguish between a network fail and an empty search contentsOf: (try? await generateRandomEntries(
do {
let search = try await generateRandomEntries(
api: api, api: api,
now: now, now: now,
count: 12 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
}
} }
// cache the last image // If we fail to fetch images, we still want to add an entry
try? entries.last!.cache(for: cacheKey) // 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()
}
completion(Timeline(entries: entries, policy: .atEnd)) completion(Timeline(entries: entries, policy: .atEnd))
} }

View File

@@ -8,21 +8,20 @@ extension Album: @unchecked Sendable, AppEntity, Identifiable {
struct AlbumQuery: EntityQuery { struct AlbumQuery: EntityQuery {
func entities(for identifiers: [Album.ID]) async throws -> [Album] { func entities(for identifiers: [Album.ID]) async throws -> [Album] {
return await suggestedEntities().filter { // use cached albums to search
var albums = (try? await AlbumCache.shared.getAlbums()) ?? []
albums.insert(NO_ALBUM, at: 0)
return albums.filter {
identifiers.contains($0.id) identifiers.contains($0.id)
} }
} }
func suggestedEntities() async -> [Album] { func suggestedEntities() async throws -> [Album] {
let albums = (try? await AlbumCache.shared.getAlbums()) ?? [] var albums = (try? await AlbumCache.shared.getAlbums(refresh: true)) ?? []
albums.insert(NO_ALBUM, at: 0)
let options = return albums
[
NONE,
FAVORITES,
] + albums
return options
} }
} }
@@ -36,6 +35,8 @@ extension Album: @unchecked Sendable, AppEntity, Identifiable {
} }
} }
let NO_ALBUM = Album(id: "NONE", albumName: "None")
struct RandomConfigurationAppIntent: WidgetConfigurationIntent { struct RandomConfigurationAppIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource { "Select Album" } static var title: LocalizedStringResource { "Select Album" }
static var description: IntentDescription { static var description: IntentDescription {
@@ -53,7 +54,7 @@ struct RandomConfigurationAppIntent: WidgetConfigurationIntent {
struct ImmichRandomProvider: AppIntentTimelineProvider { struct ImmichRandomProvider: AppIntentTimelineProvider {
func placeholder(in context: Context) -> ImageEntry { func placeholder(in context: Context) -> ImageEntry {
ImageEntry(date: Date()) ImageEntry(date: Date(), image: nil)
} }
func snapshot( func snapshot(
@@ -62,26 +63,30 @@ struct ImmichRandomProvider: AppIntentTimelineProvider {
) async ) async
-> ImageEntry -> ImageEntry
{ {
let cacheKey = "random_none_\(context.family.rawValue)"
guard let api = try? await ImmichAPI() else { guard let api = try? await ImmichAPI() else {
return ImageEntry.handleError(for: cacheKey, error: .noLogin).entries return ImageEntry(date: Date(), image: nil, error: .noLogin)
.first!
} }
guard guard
let randomImage = try? await api.fetchSearchResults( let randomImage = try? await api.fetchSearchResults(
with: Album.NONE.filter with: SearchFilters(size: 1)
).first, ).first
let entry = try? await ImageEntry.build( else {
return ImageEntry(date: Date(), image: nil, error: .fetchFailed)
}
guard
var entry = try? await buildEntry(
api: api, api: api,
asset: randomImage, asset: randomImage,
dateOffset: 0 dateOffset: 0
) )
else { else {
return ImageEntry.handleError(for: cacheKey).entries.first! return ImageEntry(date: Date(), image: nil, error: .fetchFailed)
} }
entry.resize()
return entry return entry
} }
@@ -94,41 +99,50 @@ struct ImmichRandomProvider: AppIntentTimelineProvider {
var entries: [ImageEntry] = [] var entries: [ImageEntry] = []
let now = Date() 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 // If we don't have a server config, return an entry with an error
guard let api = try? await ImmichAPI() else { guard let api = try? await ImmichAPI() else {
return ImageEntry.handleError(for: cacheKey, error: .noLogin) entries.append(ImageEntry(date: now, image: nil, error: .noLogin))
return Timeline(entries: entries, policy: .atEnd)
} }
// build entries // nil if album is NONE or nil
// this must be a do/catch since we need to let albumId =
// distinguish between a network fail and an empty search configuration.album?.id != "NONE" ? configuration.album?.id : nil
do { var albumName: String? = albumId != nil ? configuration.album?.albumName : nil
let search = try await generateRandomEntries(
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(
api: api, api: api,
now: now, now: now,
count: 12, count: 12,
filter: album.filter, albumId: albumId,
subtitle: configuration.showAlbumName ? albumName : nil subtitle: configuration.showAlbumName ? albumName : nil
) ))
?? []
)
// Load or save a cached asset for when network conditions are bad // If we fail to fetch images, we still want to add an entry with a nil image and an error
if search.count == 0 { if entries.count == 0 {
return ImageEntry.handleError(for: cacheKey, error: .noAssetsAvailable) entries.append(ImageEntry(date: now, image: nil, error: .fetchFailed))
}
entries.append(contentsOf: search)
} catch {
return ImageEntry.handleError(for: cacheKey)
} }
// cache the last image // Resize all images to something that can be stored by iOS
try? entries.last!.cache(for: cacheKey) for i in entries.indices {
entries[i].resize()
}
return Timeline(entries: entries, policy: .atEnd) return Timeline(entries: entries, policy: .atEnd)
} }

View File

@@ -12,5 +12,3 @@ enum TextSearchType {
enum AssetVisibilityEnum { timeline, hidden, archive, locked } enum AssetVisibilityEnum { timeline, hidden, archive, locked }
enum SortUserBy { id } enum SortUserBy { id }
enum ActionSource { timeline, viewer }

View File

@@ -11,7 +11,7 @@ enum AlbumUserRole {
} }
// Model for an album stored in the server // Model for an album stored in the server
class RemoteAlbum { class Album {
final String id; final String id;
final String name; final String name;
final String ownerId; final String ownerId;
@@ -21,10 +21,8 @@ class RemoteAlbum {
final String? thumbnailAssetId; final String? thumbnailAssetId;
final bool isActivityEnabled; final bool isActivityEnabled;
final AlbumAssetOrder order; final AlbumAssetOrder order;
final int assetCount;
final String ownerName;
const RemoteAlbum({ const Album({
required this.id, required this.id,
required this.name, required this.name,
required this.ownerId, required this.ownerId,
@@ -34,30 +32,26 @@ class RemoteAlbum {
this.thumbnailAssetId, this.thumbnailAssetId,
required this.isActivityEnabled, required this.isActivityEnabled,
required this.order, required this.order,
required this.assetCount,
required this.ownerName,
}); });
@override @override
String toString() { String toString() {
return '''Album { return '''Album {
id: $id, id: $id,
name: $name, name: $name,
ownerId: $ownerId, ownerId: $ownerId,
description: $description, description: $description,
createdAt: $createdAt, createdAt: $createdAt,
updatedAt: $updatedAt, updatedAt: $updatedAt,
isActivityEnabled: $isActivityEnabled, isActivityEnabled: $isActivityEnabled,
order: $order, order: $order,
thumbnailAssetId: ${thumbnailAssetId ?? "<NA>"} thumbnailAssetId: ${thumbnailAssetId ?? "<NA>"}
assetCount: $assetCount
ownerName: $ownerName
}'''; }''';
} }
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
if (other is! RemoteAlbum) return false; if (other is! Album) return false;
if (identical(this, other)) return true; if (identical(this, other)) return true;
return id == other.id && return id == other.id &&
name == other.name && name == other.name &&
@@ -67,9 +61,7 @@ class RemoteAlbum {
updatedAt == other.updatedAt && updatedAt == other.updatedAt &&
thumbnailAssetId == other.thumbnailAssetId && thumbnailAssetId == other.thumbnailAssetId &&
isActivityEnabled == other.isActivityEnabled && isActivityEnabled == other.isActivityEnabled &&
order == other.order && order == other.order;
assetCount == other.assetCount &&
ownerName == other.ownerName;
} }
@override @override
@@ -82,36 +74,6 @@ class RemoteAlbum {
updatedAt.hashCode ^ updatedAt.hashCode ^
thumbnailAssetId.hashCode ^ thumbnailAssetId.hashCode ^
isActivityEnabled.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,
);
} }
} }

View File

@@ -0,0 +1,73 @@
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;
}

View File

@@ -1,5 +1,5 @@
part 'asset.model.dart';
part 'local_asset.model.dart'; part 'local_asset.model.dart';
part 'remote_asset.model.dart';
enum AssetType { enum AssetType {
// do not change this order! // do not change this order!
@@ -25,7 +25,6 @@ sealed class BaseAsset {
final int? height; final int? height;
final int? durationInSeconds; final int? durationInSeconds;
final bool isFavorite; final bool isFavorite;
final String? livePhotoVideoId;
const BaseAsset({ const BaseAsset({
required this.name, required this.name,
@@ -37,32 +36,11 @@ sealed class BaseAsset {
this.height, this.height,
this.durationInSeconds, this.durationInSeconds,
this.isFavorite = false, this.isFavorite = false,
this.livePhotoVideoId,
}); });
bool get isImage => type == AssetType.image; bool get isImage => type == AssetType.image;
bool get isVideo => type == AssetType.video; 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; AssetState get storage;
String get heroTag;
@override @override
String toString() { String toString() {

View File

@@ -3,7 +3,6 @@ part of 'base_asset.model.dart';
class LocalAsset extends BaseAsset { class LocalAsset extends BaseAsset {
final String id; final String id;
final String? remoteId; final String? remoteId;
final int orientation;
const LocalAsset({ const LocalAsset({
required this.id, required this.id,
@@ -17,17 +16,12 @@ class LocalAsset extends BaseAsset {
super.height, super.height,
super.durationInSeconds, super.durationInSeconds,
super.isFavorite = false, super.isFavorite = false,
super.livePhotoVideoId,
this.orientation = 0,
}); });
@override @override
AssetState get storage => AssetState get storage =>
remoteId == null ? AssetState.local : AssetState.merged; remoteId == null ? AssetState.local : AssetState.merged;
@override
String get heroTag => '${id}_${remoteId ?? checksum}';
@override @override
String toString() { String toString() {
return '''LocalAsset { return '''LocalAsset {
@@ -41,7 +35,6 @@ class LocalAsset extends BaseAsset {
durationInSeconds: ${durationInSeconds ?? "<NA>"}, durationInSeconds: ${durationInSeconds ?? "<NA>"},
remoteId: ${remoteId ?? "<NA>"} remoteId: ${remoteId ?? "<NA>"}
isFavorite: $isFavorite, isFavorite: $isFavorite,
orientation: $orientation,
}'''; }''';
} }
@@ -49,12 +42,11 @@ class LocalAsset extends BaseAsset {
bool operator ==(Object other) { bool operator ==(Object other) {
if (other is! LocalAsset) return false; if (other is! LocalAsset) return false;
if (identical(this, other)) return true; if (identical(this, other)) return true;
return super == other && id == other.id && orientation == other.orientation; return super == other && id == other.id && remoteId == other.remoteId;
} }
@override @override
int get hashCode => int get hashCode => super.hashCode ^ id.hashCode ^ remoteId.hashCode;
super.hashCode ^ id.hashCode ^ remoteId.hashCode ^ orientation.hashCode;
LocalAsset copyWith({ LocalAsset copyWith({
String? id, String? id,
@@ -68,7 +60,6 @@ class LocalAsset extends BaseAsset {
int? height, int? height,
int? durationInSeconds, int? durationInSeconds,
bool? isFavorite, bool? isFavorite,
int? orientation,
}) { }) {
return LocalAsset( return LocalAsset(
id: id ?? this.id, id: id ?? this.id,
@@ -82,7 +73,6 @@ class LocalAsset extends BaseAsset {
height: height ?? this.height, height: height ?? this.height,
durationInSeconds: durationInSeconds ?? this.durationInSeconds, durationInSeconds: durationInSeconds ?? this.durationInSeconds,
isFavorite: isFavorite ?? this.isFavorite, isFavorite: isFavorite ?? this.isFavorite,
orientation: orientation ?? this.orientation,
); );
} }
} }

View File

@@ -1,117 +0,0 @@
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,
);
}
}

View File

@@ -3,8 +3,6 @@ class ExifInfo {
final int? fileSize; final int? fileSize;
final String? description; final String? description;
final bool isFlipped; final bool isFlipped;
final double? width;
final double? height;
final String? orientation; final String? orientation;
final String? timeZone; final String? timeZone;
final DateTime? dateTimeOriginal; final DateTime? dateTimeOriginal;
@@ -47,8 +45,6 @@ class ExifInfo {
this.fileSize, this.fileSize,
this.description, this.description,
this.orientation, this.orientation,
this.width,
this.height,
this.timeZone, this.timeZone,
this.dateTimeOriginal, this.dateTimeOriginal,
this.isFlipped = false, this.isFlipped = false,
@@ -72,9 +68,6 @@ class ExifInfo {
return other.fileSize == fileSize && return other.fileSize == fileSize &&
other.description == description && other.description == description &&
other.isFlipped == isFlipped &&
other.width == width &&
other.height == height &&
other.orientation == orientation && other.orientation == orientation &&
other.timeZone == timeZone && other.timeZone == timeZone &&
other.dateTimeOriginal == dateTimeOriginal && other.dateTimeOriginal == dateTimeOriginal &&
@@ -98,9 +91,6 @@ class ExifInfo {
return fileSize.hashCode ^ return fileSize.hashCode ^
description.hashCode ^ description.hashCode ^
orientation.hashCode ^ orientation.hashCode ^
isFlipped.hashCode ^
width.hashCode ^
height.hashCode ^
timeZone.hashCode ^ timeZone.hashCode ^
dateTimeOriginal.hashCode ^ dateTimeOriginal.hashCode ^
latitude.hashCode ^ latitude.hashCode ^
@@ -124,9 +114,6 @@ class ExifInfo {
fileSize: ${fileSize ?? 'NA'}, fileSize: ${fileSize ?? 'NA'},
description: ${description ?? 'NA'}, description: ${description ?? 'NA'},
orientation: ${orientation ?? 'NA'}, orientation: ${orientation ?? 'NA'},
width: ${width ?? 'NA'},
height: ${height ?? 'NA'},
isFlipped: $isFlipped,
timeZone: ${timeZone ?? 'NA'}, timeZone: ${timeZone ?? 'NA'},
dateTimeOriginal: ${dateTimeOriginal ?? 'NA'}, dateTimeOriginal: ${dateTimeOriginal ?? 'NA'},
latitude: ${latitude ?? 'NA'}, latitude: ${latitude ?? 'NA'},

View File

@@ -1,166 +0,0 @@
// 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;
}
}

View File

@@ -3,12 +3,7 @@ import 'package:immich_mobile/domain/models/store.model.dart';
enum Setting<T> { enum Setting<T> {
tilesPerRow<int>(StoreKey.tilesPerRow, 4), tilesPerRow<int>(StoreKey.tilesPerRow, 4),
groupAssetsBy<int>(StoreKey.groupAssetsBy, 0), 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); const Setting(this.storeKey, this.defaultValue);

View File

@@ -1,84 +0,0 @@
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;
}
}

View File

@@ -1,12 +1,5 @@
import 'dart:ui'; import 'dart:ui';
enum UserMetadataKey {
// do not change this order!
onboarding,
preferences,
license,
}
enum AvatarColor { enum AvatarColor {
// do not change this order or reuse indices for other purposes, adding is OK // do not change this order or reuse indices for other purposes, adding is OK
primary("primary"), primary("primary"),
@@ -38,45 +31,7 @@ enum AvatarColor {
}; };
} }
class Onboarding { class UserPreferences {
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 foldersEnabled;
final bool memoriesEnabled; final bool memoriesEnabled;
final bool peopleEnabled; final bool peopleEnabled;
@@ -86,7 +41,7 @@ class Preferences {
final AvatarColor userAvatarColor; final AvatarColor userAvatarColor;
final bool showSupportBadge; final bool showSupportBadge;
const Preferences({ const UserPreferences({
this.foldersEnabled = false, this.foldersEnabled = false,
this.memoriesEnabled = true, this.memoriesEnabled = true,
this.peopleEnabled = true, this.peopleEnabled = true,
@@ -97,7 +52,7 @@ class Preferences {
this.showSupportBadge = true, this.showSupportBadge = true,
}); });
Preferences copyWith({ UserPreferences copyWith({
bool? foldersEnabled, bool? foldersEnabled,
bool? memoriesEnabled, bool? memoriesEnabled,
bool? peopleEnabled, bool? peopleEnabled,
@@ -107,7 +62,7 @@ class Preferences {
AvatarColor? userAvatarColor, AvatarColor? userAvatarColor,
bool? showSupportBadge, bool? showSupportBadge,
}) { }) {
return Preferences( return UserPreferences(
foldersEnabled: foldersEnabled ?? this.foldersEnabled, foldersEnabled: foldersEnabled ?? this.foldersEnabled,
memoriesEnabled: memoriesEnabled ?? this.memoriesEnabled, memoriesEnabled: memoriesEnabled ?? this.memoriesEnabled,
peopleEnabled: peopleEnabled ?? this.peopleEnabled, peopleEnabled: peopleEnabled ?? this.peopleEnabled,
@@ -132,8 +87,8 @@ class Preferences {
return preferences; return preferences;
} }
factory Preferences.fromMap(Map<String, Object?> map) { factory UserPreferences.fromMap(Map<String, Object?> map) {
return Preferences( return UserPreferences(
foldersEnabled: map["folders-Enabled"] as bool? ?? false, foldersEnabled: map["folders-Enabled"] as bool? ?? false,
memoriesEnabled: map["memories-Enabled"] as bool? ?? true, memoriesEnabled: map["memories-Enabled"] as bool? ?? true,
peopleEnabled: map["people-Enabled"] as bool? ?? true, peopleEnabled: map["people-Enabled"] as bool? ?? true,
@@ -147,173 +102,4 @@ class Preferences {
showSupportBadge: map["purchase-ShowSupportBadge"] as bool? ?? true, 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;
}
} }

View File

@@ -1,68 +0,0 @@
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();
}
}

View File

@@ -61,7 +61,7 @@ class HashService {
final toHash = <_AssetToPath>[]; final toHash = <_AssetToPath>[];
for (final asset in assetsToHash) { for (final asset in assetsToHash) {
final file = await _storageRepository.getFileForAsset(asset.id); final file = await _storageRepository.getFileForAsset(asset);
if (file == null) { if (file == null) {
continue; continue;
} }

View File

@@ -1,17 +0,0 @@
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);
}
}

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