Compare commits

..

1 Commits

Author SHA1 Message Date
Alex Tran
7277ea3d7a feat(server): asset_user table 2025-01-28 22:04:21 -06:00
460 changed files with 11301 additions and 13592 deletions

View File

@@ -1 +1,2 @@
blank_issues_enabled: false
blank_pull_request_template_enabled: false

View File

@@ -0,0 +1,22 @@
## Description
<!--- Describe your changes in detail -->
<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->
Fixes # (issue)
## How Has This Been Tested?
<!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration -->
- [ ] Test A
- [ ] Test B
## Screenshots (if appropriate):
## Checklist:
- [ ] I have performed a self-review of my own code
- [ ] I have made corresponding changes to the documentation if applicable

View File

@@ -1,36 +0,0 @@
## Description
<!--- Describe your changes in detail -->
<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->
Fixes # (issue)
## How Has This Been Tested?
<!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration -->
- [ ] Test A
- [ ] Test B
<details><summary><h2>Screenshots (if appropriate)</h2></summary>
<!-- Images go below this line. -->
</details>
<!-- API endpoint changes (if relevant)
## API Changes
The `/api/something` endpoint is now `/api/something-else`
-->
## Checklist:
- [ ] I have performed a self-review of my own code
- [ ] I have made corresponding changes to the documentation if applicable
- [ ] I have no unrelated changes in the PR.
- [ ] I have confirmed that any new dependencies are strictly necessary.
- [ ] I have written tests for new code (if applicable)
- [ ] I have followed naming conventions/patterns in the surrounding code
- [ ] All code in `src/services` uses repositories implementations for database calls, filesystem operations, etc.
- [ ] All code in `src/repositories/` is pretty basic/simple and does not have any immich specific logic (that belongs in `src/services`)

View File

@@ -29,11 +29,9 @@ jobs:
filters: |
mobile:
- 'mobile/**'
workflow:
- '.github/workflows/build-mobile.yml'
- name: Check if we should force jobs to run
id: should_force
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT"
run: echo "should_force=${{ github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT"
build-sign-android:
name: Build and sign Android

View File

@@ -56,10 +56,10 @@ jobs:
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.4.0
uses: docker/setup-qemu-action@v3.3.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.9.0
uses: docker/setup-buildx-action@v3.8.0
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
@@ -88,7 +88,7 @@ jobs:
type=raw,value=latest,enable=${{ github.event_name == 'release' }}
- name: Build and push image
uses: docker/build-push-action@v6.13.0
uses: docker/build-push-action@v6.12.0
with:
file: cli/Dockerfile
platforms: linux/amd64,linux/arm64

73
.github/workflows/docker-cleanup.yml vendored Normal file
View File

@@ -0,0 +1,73 @@
# This workflow runs on certain conditions to check for and potentially
# delete container images from the GHCR which no longer have an associated
# code branch.
# Requires a PAT with the correct scope set in the secrets.
#
# This workflow will not trigger runs on forked repos.
name: Docker Cleanup
on:
pull_request:
types:
- "closed"
push:
paths:
- ".github/workflows/docker-cleanup.yml"
concurrency:
group: registry-tags-cleanup
cancel-in-progress: false
jobs:
cleanup-images:
name: Cleanup Stale Images Tags for ${{ matrix.primary-name }}
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
include:
- primary-name: "immich-server"
- primary-name: "immich-machine-learning"
env:
# Requires a personal access token with the OAuth scope delete:packages
TOKEN: ${{ secrets.PACKAGE_DELETE_TOKEN }}
steps:
- name: Clean temporary images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/ephemeral@v0.9.0
with:
token: "${{ env.TOKEN }}"
owner: "immich-app"
is_org: "true"
do_delete: "true"
package_name: "${{ matrix.primary-name }}"
scheme: "pull_request"
repo_name: "immich"
match_regex: '^pr-(\d+)$|^(\d+)$'
cleanup-untagged-images:
name: Cleanup Untagged Images Tags for ${{ matrix.primary-name }}
runs-on: ubuntu-24.04
needs:
- cleanup-images
strategy:
fail-fast: false
matrix:
include:
- primary-name: "immich-server"
- primary-name: "immich-machine-learning"
- primary-name: "immich-build-cache"
env:
# Requires a personal access token with the OAuth scope delete:packages
TOKEN: ${{ secrets.PACKAGE_DELETE_TOKEN }}
steps:
- name: Clean untagged images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/untagged@v0.9.0
with:
token: "${{ env.TOKEN }}"
owner: "immich-app"
do_delete: "true"
is_org: "true"
package_name: "${{ matrix.primary-name }}"

View File

@@ -36,12 +36,10 @@ jobs:
- 'i18n/**'
machine-learning:
- 'machine-learning/**'
workflow:
- '.github/workflows/docker.yml'
- name: Check if we should force jobs to run
id: should_force
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
run: echo "should_force=${{ github.event_name == 'workflow_dispatch' || github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
retag_ml:
name: Re-Tag ML
@@ -63,10 +61,8 @@ jobs:
REGISTRY_NAME="ghcr.io"
REPOSITORY=${{ github.repository_owner }}/immich-machine-learning
TAG_OLD=main${{ matrix.suffix }}
TAG_PR=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
TAG_COMMIT=commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }}
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_PR $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_COMMIT $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
TAG_NEW=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_NEW $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
retag_server:
name: Re-Tag Server
@@ -88,100 +84,107 @@ jobs:
REGISTRY_NAME="ghcr.io"
REPOSITORY=${{ github.repository_owner }}/immich-server
TAG_OLD=main${{ matrix.suffix }}
TAG_PR=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
TAG_COMMIT=commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }}
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_PR $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_COMMIT $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
TAG_NEW=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_NEW $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
build_and_push_ml:
name: Build and Push ML
needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }}
runs-on: ${{ matrix.runner }}
runs-on: ubuntu-latest
env:
image: immich-machine-learning
context: machine-learning
file: machine-learning/Dockerfile
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-machine-learning
strategy:
# Prevent a failure in one image from stopping the other builds
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
- platforms: linux/amd64,linux/arm64
device: cpu
- platform: linux/arm64
runner: ubuntu-24.04-arm
device: cpu
- platform: linux/amd64
runner: ubuntu-latest
- platforms: linux/amd64
device: cuda
suffix: -cuda
- platform: linux/amd64
runner: ubuntu-latest
- platforms: linux/amd64
device: openvino
suffix: -openvino
- platform: linux/arm64
runner: ubuntu-24.04-arm
- platforms: linux/arm64
device: armnn
suffix: -armnn
steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.3.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.9.0
uses: docker/setup-buildx-action@v3.8.0
- name: Login to Docker Hub
# Only push to Docker Hub when making a release
if: ${{ github.event_name == 'release' }}
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
# Skip when PR from a fork
if: ${{ !github.event.pull_request.head.repo.fork }}
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate cache key suffix
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "CACHE_KEY_SUFFIX=pr-${{ github.event.number }}" >> $GITHUB_ENV
else
echo "CACHE_KEY_SUFFIX=$(echo ${{ github.ref_name }} | sed 's/[^a-zA-Z0-9]/-/g')" >> $GITHUB_ENV
fi
- name: Generate docker image tags
id: metadata
uses: docker/metadata-action@v5
with:
flavor: |
# Disable latest tag
latest=false
images: |
name=ghcr.io/${{ github.repository_owner }}/${{env.image}}
name=altran1502/${{env.image}},enable=${{ github.event_name == 'release' }}
tags: |
# Tag with branch name
type=ref,event=branch,suffix=${{ matrix.suffix }}
# Tag with pr-number
type=ref,event=pr,suffix=${{ matrix.suffix }}
# Tag with git tag on release
type=ref,event=tag,suffix=${{ matrix.suffix }}
type=raw,value=release,enable=${{ github.event_name == 'release' }},suffix=${{ matrix.suffix }}
- name: Generate cache target
- name: Determine build cache output
id: cache-target
run: |
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
# Essentially just ignore the cache output (forks can't write to registry cache)
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
# Essentially just ignore the cache output (PR can't write to registry cache)
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
else
echo "cache-to=type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-${{ env.CACHE_KEY_SUFFIX }},mode=max,compression=zstd" >> $GITHUB_OUTPUT
echo "cache-to=type=registry,mode=max,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{ env.image }}" >> $GITHUB_OUTPUT
fi
- name: Build and push image
id: build
uses: docker/build-push-action@v6.13.0
uses: docker/build-push-action@v6.12.0
with:
context: ${{ env.context }}
file: ${{ env.file }}
platforms: ${{ matrix.platforms }}
labels: ${{ steps.metadata.outputs.labels }}
# Skip pushing when PR from a fork
push: ${{ !github.event.pull_request.head.repo.fork }}
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{env.image}}
cache-to: ${{ steps.cache-target.outputs.cache-to }}
cache-from: |
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-${{ env.CACHE_KEY_SUFFIX }}
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-main
outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=${{ !github.event.pull_request.head.repo.fork }}
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
build-args: |
DEVICE=${{ matrix.device }}
BUILD_ID=${{ github.run_id }}
@@ -189,245 +192,100 @@ jobs:
BUILD_SOURCE_REF=${{ github.ref_name }}
BUILD_SOURCE_COMMIT=${{ github.sha }}
- name: Export digest
run: |
mkdir -p ${{ runner.temp }}/digests
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: ml-digests-${{ matrix.device }}-${{ env.PLATFORM_PAIR }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
merge_ml:
name: Merge & Push ML
runs-on: ubuntu-latest
if: ${{ needs.pre-job.outputs.should_run_ml == 'true' && !github.event.pull_request.head.repo.fork }}
env:
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-machine-learning
DOCKER_REPO: altran1502/immich-machine-learning
strategy:
matrix:
include:
- device: cpu
- device: cuda
suffix: -cuda
- device: openvino
suffix: -openvino
- device: armnn
suffix: -armnn
needs:
- build_and_push_ml
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: ${{ runner.temp }}/digests
pattern: ml-digests-${{ matrix.device }}-*
merge-multiple: true
- name: Login to Docker Hub
if: ${{ github.event_name == 'release' }}
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Generate docker image tags
id: meta
uses: docker/metadata-action@v5
with:
flavor: |
# Disable latest tag
latest=false
images: |
name=${{ env.GHCR_REPO }}
name=${{ env.DOCKER_REPO }},enable=${{ github.event_name == 'release' }}
tags: |
# Tag with branch name
type=ref,event=branch,suffix=${{ matrix.suffix }}
# Tag with pr-number
type=ref,event=pr,suffix=${{ matrix.suffix }}
# Tag with long commit sha hash
type=sha,format=long,prefix=commit-,suffix=${{ matrix.suffix }}
# Tag with git tag on release
type=ref,event=tag,suffix=${{ matrix.suffix }}
type=raw,value=release,enable=${{ github.event_name == 'release' }},suffix=${{ matrix.suffix }}
- name: Create manifest list and push
working-directory: ${{ runner.temp }}/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.GHCR_REPO }}@sha256:%s ' *)
build_and_push_server:
name: Build and Push Server
runs-on: ${{ matrix.runner }}
runs-on: ubuntu-latest
needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
env:
image: immich-server
context: .
file: server/Dockerfile
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-server
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm
- platforms: linux/amd64,linux/arm64
device: cpu
steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.3.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
if: ${{ !github.event.pull_request.head.repo.fork }}
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate cache key suffix
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "CACHE_KEY_SUFFIX=pr-${{ github.event.number }}" >> $GITHUB_ENV
else
echo "CACHE_KEY_SUFFIX=$(echo ${{ github.ref_name }} | sed 's/[^a-zA-Z0-9]/-/g')" >> $GITHUB_ENV
fi
- name: Generate cache target
id: cache-target
run: |
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
# Essentially just ignore the cache output (forks can't write to registry cache)
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
else
echo "cache-to=type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-${{ env.CACHE_KEY_SUFFIX }},mode=max,compression=zstd" >> $GITHUB_OUTPUT
fi
- name: Build and push image
id: build
uses: docker/build-push-action@v6.13.0
with:
context: ${{ env.context }}
file: ${{ env.file }}
platforms: ${{ matrix.platform }}
labels: ${{ steps.metadata.outputs.labels }}
cache-to: ${{ steps.cache-target.outputs.cache-to }}
cache-from: |
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ env.CACHE_KEY_SUFFIX }}
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-main
outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=${{ !github.event.pull_request.head.repo.fork }}
build-args: |
DEVICE=cpu
BUILD_ID=${{ github.run_id }}
BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }}
BUILD_SOURCE_REF=${{ github.ref_name }}
BUILD_SOURCE_COMMIT=${{ github.sha }}
- name: Export digest
run: |
mkdir -p ${{ runner.temp }}/digests
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: server-digests-${{ env.PLATFORM_PAIR }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
merge_server:
name: Merge & Push Server
runs-on: ubuntu-latest
if: ${{ needs.pre-job.outputs.should_run_server == 'true' && !github.event.pull_request.head.repo.fork }}
env:
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-server
DOCKER_REPO: altran1502/immich-server
needs:
- build_and_push_server
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: ${{ runner.temp }}/digests
pattern: server-digests-*
merge-multiple: true
uses: docker/setup-buildx-action@v3.8.0
- name: Login to Docker Hub
# Only push to Docker Hub when making a release
if: ${{ github.event_name == 'release' }}
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
# Skip when PR from a fork
if: ${{ !github.event.pull_request.head.repo.fork }}
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Generate docker image tags
id: meta
id: metadata
uses: docker/metadata-action@v5
with:
flavor: |
# Disable latest tag
latest=false
images: |
name=${{ env.GHCR_REPO }}
name=${{ env.DOCKER_REPO }},enable=${{ github.event_name == 'release' }}
name=ghcr.io/${{ github.repository_owner }}/${{env.image}}
name=altran1502/${{env.image}},enable=${{ github.event_name == 'release' }}
tags: |
# Tag with branch name
type=ref,event=branch,suffix=${{ matrix.suffix }}
# Tag with pr-number
type=ref,event=pr,suffix=${{ matrix.suffix }}
# Tag with long commit sha hash
type=sha,format=long,prefix=commit-,suffix=${{ matrix.suffix }}
# Tag with git tag on release
type=ref,event=tag,suffix=${{ matrix.suffix }}
type=raw,value=release,enable=${{ github.event_name == 'release' }},suffix=${{ matrix.suffix }}
- name: Create manifest list and push
working-directory: ${{ runner.temp }}/digests
- name: Determine build cache output
id: cache-target
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.GHCR_REPO }}@sha256:%s ' *)
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
# Essentially just ignore the cache output (PR can't write to registry cache)
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
else
echo "cache-to=type=registry,mode=max,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{ env.image }}" >> $GITHUB_OUTPUT
fi
- name: Build and push image
uses: docker/build-push-action@v6.12.0
with:
context: ${{ env.context }}
file: ${{ env.file }}
platforms: ${{ matrix.platforms }}
# Skip pushing when PR from a fork
push: ${{ !github.event.pull_request.head.repo.fork }}
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{env.image}}
cache-to: ${{ steps.cache-target.outputs.cache-to }}
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
build-args: |
DEVICE=${{ matrix.device }}
BUILD_ID=${{ github.run_id }}
BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }}
BUILD_SOURCE_REF=${{ github.ref_name }}
BUILD_SOURCE_COMMIT=${{ github.sha }}
success-check-server:
name: Docker Build & Push Server Success
needs: [merge_server, retag_server]
needs: [build_and_push_server, retag_server]
runs-on: ubuntu-latest
if: always()
steps:
@@ -440,7 +298,7 @@ jobs:
success-check-ml:
name: Docker Build & Push ML Success
needs: [merge_ml, retag_ml]
needs: [build_and_push_ml, retag_ml]
runs-on: ubuntu-latest
if: always()
steps:

View File

@@ -15,7 +15,7 @@ jobs:
pre-job:
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.found_paths.outputs.docs == 'true' || steps.should_force.outputs.should_force == 'true' }}
should_run: ${{ steps.found_paths.outputs.docs == 'true' || steps.should_force.outputs.should_force == 'true' }}
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -25,11 +25,9 @@ jobs:
filters: |
docs:
- 'docs/**'
workflow:
- '.github/workflows/docs-build.yml'
- name: Check if we should force jobs to run
id: should_force
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'release' || github.ref_name == 'main' }}" >> "$GITHUB_OUTPUT"
run: echo "should_force=${{ github.event_name == 'release' || github.ref_name == 'main' }}" >> "$GITHUB_OUTPUT"
build:
name: Docs Build

View File

@@ -1,17 +0,0 @@
name: Preview comment
on:
pull_request:
types: [labeled]
jobs:
comment-status:
runs-on: ubuntu-latest
if: ${{ github.event.label.name == 'preview' }}
permissions:
pull-requests: write
steps:
- uses: mshick/add-pr-comment@v2
with:
message-id: "preview-status"
message: "Deploying preview environment to https://pr-${{ github.event.pull_request.number }}.preview.internal.immich.cloud/"

View File

@@ -23,11 +23,9 @@ jobs:
filters: |
mobile:
- 'mobile/**'
workflow:
- '.github/workflows/static_analysis.yml'
- name: Check if we should force jobs to run
id: should_force
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
run: echo "should_force=${{ github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
mobile-dart-analyze:
name: Run Dart Code Analysis

View File

@@ -43,12 +43,10 @@ jobs:
- 'mobile/**'
machine-learning:
- 'machine-learning/**'
workflow:
- '.github/workflows/test.yml'
- name: Check if we should force jobs to run
id: should_force
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT"
run: echo "should_force=${{ github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT"
server-unit-tests:
name: Test & Lint Server

View File

@@ -1 +1 @@
22.14.0
22.13.1

295
cli/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@immich/cli",
"version": "2.2.50",
"version": "2.2.47",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/cli",
"version": "2.2.50",
"version": "2.2.47",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"fast-glob": "^3.3.2",
@@ -24,7 +24,7 @@
"@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12",
"@types/mock-fs": "^4.13.1",
"@types/node": "^22.13.2",
"@types/node": "^22.10.9",
"@typescript-eslint/eslint-plugin": "^8.15.0",
"@typescript-eslint/parser": "^8.15.0",
"@vitest/coverage-v8": "^3.0.0",
@@ -52,14 +52,14 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.126.1",
"version": "1.125.6",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^22.13.2",
"@types/node": "^22.10.9",
"typescript": "^5.3.3"
}
},
@@ -881,9 +881,9 @@
}
},
"node_modules/@eslint/js": {
"version": "9.20.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz",
"integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==",
"version": "9.18.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.18.0.tgz",
"integrity": "sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1482,9 +1482,9 @@
}
},
"node_modules/@types/node": {
"version": "22.13.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz",
"integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==",
"version": "22.10.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.9.tgz",
"integrity": "sha512-Ir6hwgsKyNESl/gLOcEz3krR4CBGgliDqBQ2ma4wIhEx0w+xnoeTq3tdrNw15kU3SxogDjOgv9sqdtLW8mIHaw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1498,21 +1498,21 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.0.tgz",
"integrity": "sha512-aFcXEJJCI4gUdXgoo/j9udUYIHgF23MFkg09LFz2dzEmU0+1Plk4rQWv/IYKvPHAtlkkGoB3m5e6oUp+JPsNaQ==",
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.20.0.tgz",
"integrity": "sha512-naduuphVw5StFfqp4Gq4WhIBE2gN1GEmMUExpJYknZJdRnc+2gDzB8Z3+5+/Kv33hPQRDGzQO/0opHE72lZZ6A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.24.0",
"@typescript-eslint/type-utils": "8.24.0",
"@typescript-eslint/utils": "8.24.0",
"@typescript-eslint/visitor-keys": "8.24.0",
"@typescript-eslint/scope-manager": "8.20.0",
"@typescript-eslint/type-utils": "8.20.0",
"@typescript-eslint/utils": "8.20.0",
"@typescript-eslint/visitor-keys": "8.20.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.0.1"
"ts-api-utils": "^2.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1528,16 +1528,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.24.0.tgz",
"integrity": "sha512-MFDaO9CYiard9j9VepMNa9MTcqVvSny2N4hkY6roquzj8pdCBRENhErrteaQuu7Yjn1ppk0v1/ZF9CG3KIlrTA==",
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.20.0.tgz",
"integrity": "sha512-gKXG7A5HMyjDIedBi6bUrDcun8GIjnI8qOwVLiY3rx6T/sHP/19XLJOnIq/FgQvWLHja5JN/LSE7eklNBr612g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.24.0",
"@typescript-eslint/types": "8.24.0",
"@typescript-eslint/typescript-estree": "8.24.0",
"@typescript-eslint/visitor-keys": "8.24.0",
"@typescript-eslint/scope-manager": "8.20.0",
"@typescript-eslint/types": "8.20.0",
"@typescript-eslint/typescript-estree": "8.20.0",
"@typescript-eslint/visitor-keys": "8.20.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1553,14 +1553,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.0.tgz",
"integrity": "sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw==",
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.20.0.tgz",
"integrity": "sha512-J7+VkpeGzhOt3FeG1+SzhiMj9NzGD/M6KoGn9f4dbz3YzK9hvbhVTmLj/HiTp9DazIzJ8B4XcM80LrR9Dm1rJw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.24.0",
"@typescript-eslint/visitor-keys": "8.24.0"
"@typescript-eslint/types": "8.20.0",
"@typescript-eslint/visitor-keys": "8.20.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1571,16 +1571,16 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.24.0.tgz",
"integrity": "sha512-8fitJudrnY8aq0F1wMiPM1UUgiXQRJ5i8tFjq9kGfRajU+dbPyOuHbl0qRopLEidy0MwqgTHDt6CnSeXanNIwA==",
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.20.0.tgz",
"integrity": "sha512-bPC+j71GGvA7rVNAHAtOjbVXbLN5PkwqMvy1cwGeaxUoRQXVuKCebRoLzm+IPW/NtFFpstn1ummSIasD5t60GA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.24.0",
"@typescript-eslint/utils": "8.24.0",
"@typescript-eslint/typescript-estree": "8.20.0",
"@typescript-eslint/utils": "8.20.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.0.1"
"ts-api-utils": "^2.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1595,9 +1595,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.0.tgz",
"integrity": "sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw==",
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.20.0.tgz",
"integrity": "sha512-cqaMiY72CkP+2xZRrFt3ExRBu0WmVitN/rYPZErA80mHjHx/Svgp8yfbzkJmDoQ/whcytOPO9/IZXnOc+wigRA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1609,20 +1609,20 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.0.tgz",
"integrity": "sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ==",
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.20.0.tgz",
"integrity": "sha512-Y7ncuy78bJqHI35NwzWol8E0X7XkRVS4K4P4TCyzWkOJih5NDvtoRDW4Ba9YJJoB2igm9yXDdYI/+fkiiAxPzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.24.0",
"@typescript-eslint/visitor-keys": "8.24.0",
"@typescript-eslint/types": "8.20.0",
"@typescript-eslint/visitor-keys": "8.20.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
"ts-api-utils": "^2.0.1"
"ts-api-utils": "^2.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1636,16 +1636,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.24.0.tgz",
"integrity": "sha512-07rLuUBElvvEb1ICnafYWr4hk8/U7X9RDCOqd9JcAMtjh/9oRmcfN4yGzbPVirgMR0+HLVHehmu19CWeh7fsmQ==",
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.20.0.tgz",
"integrity": "sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "8.24.0",
"@typescript-eslint/types": "8.24.0",
"@typescript-eslint/typescript-estree": "8.24.0"
"@typescript-eslint/scope-manager": "8.20.0",
"@typescript-eslint/types": "8.20.0",
"@typescript-eslint/typescript-estree": "8.20.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1660,13 +1660,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.0.tgz",
"integrity": "sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg==",
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.20.0.tgz",
"integrity": "sha512-v/BpkeeYAsPkKCkR8BDwcno0llhzWVqPOamQrAEMdpZav2Y9OVjd9dwJyBLJWwf335B5DmlifECIkZRJCaGaHA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.24.0",
"@typescript-eslint/types": "8.20.0",
"eslint-visitor-keys": "^4.2.0"
},
"engines": {
@@ -1691,9 +1691,9 @@
}
},
"node_modules/@vitest/coverage-v8": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.5.tgz",
"integrity": "sha512-zOOWIsj5fHh3jjGwQg+P+J1FW3s4jBu1Zqga0qW60yutsBtqEqNEJKWYh7cYn1yGD+1bdPsPdC/eL4eVK56xMg==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.3.tgz",
"integrity": "sha512-uVbJ/xhImdNtzPnLyxCZJMTeTIYdgcC2nWtBBBpR1H6z0w8m7D+9/zrDIx2nNxgMg9r+X8+RY2qVpUDeW2b3nw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1714,8 +1714,8 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@vitest/browser": "3.0.5",
"vitest": "3.0.5"
"@vitest/browser": "3.0.3",
"vitest": "3.0.3"
},
"peerDependenciesMeta": {
"@vitest/browser": {
@@ -1724,14 +1724,14 @@
}
},
"node_modules/@vitest/expect": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.5.tgz",
"integrity": "sha512-nNIOqupgZ4v5jWuQx2DSlHLEs7Q4Oh/7AYwNyE+k0UQzG7tSmjPXShUikn1mpNGzYEN2jJbTvLejwShMitovBA==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.3.tgz",
"integrity": "sha512-SbRCHU4qr91xguu+dH3RUdI5dC86zm8aZWydbp961aIR7G8OYNN6ZiayFuf9WAngRbFOfdrLHCGgXTj3GtoMRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "3.0.5",
"@vitest/utils": "3.0.5",
"@vitest/spy": "3.0.3",
"@vitest/utils": "3.0.3",
"chai": "^5.1.2",
"tinyrainbow": "^2.0.0"
},
@@ -1740,13 +1740,13 @@
}
},
"node_modules/@vitest/mocker": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.5.tgz",
"integrity": "sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.3.tgz",
"integrity": "sha512-XT2XBc4AN9UdaxJAeIlcSZ0ILi/GzmG5G8XSly4gaiqIvPV3HMTSIDZWJVX6QRJ0PX1m+W8Cy0K9ByXNb/bPIA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "3.0.5",
"@vitest/spy": "3.0.3",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.17"
},
@@ -1767,9 +1767,9 @@
}
},
"node_modules/@vitest/pretty-format": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.5.tgz",
"integrity": "sha512-CjUtdmpOcm4RVtB+up8r2vVDLR16Mgm/bYdkGFe3Yj/scRfCpbSi2W/BDSDcFK7ohw8UXvjMbOp9H4fByd/cOA==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.3.tgz",
"integrity": "sha512-gCrM9F7STYdsDoNjGgYXKPq4SkSxwwIU5nkaQvdUxiQ0EcNlez+PdKOVIsUJvh9P9IeIFmjn4IIREWblOBpP2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1780,38 +1780,38 @@
}
},
"node_modules/@vitest/runner": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.5.tgz",
"integrity": "sha512-BAiZFityFexZQi2yN4OX3OkJC6scwRo8EhRB0Z5HIGGgd2q+Nq29LgHU/+ovCtd0fOfXj5ZI6pwdlUmC5bpi8A==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.3.tgz",
"integrity": "sha512-Rgi2kOAk5ZxWZlwPguRJFOBmWs6uvvyAAR9k3MvjRvYrG7xYvKChZcmnnpJCS98311CBDMqsW9MzzRFsj2gX3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "3.0.5",
"pathe": "^2.0.2"
"@vitest/utils": "3.0.3",
"pathe": "^2.0.1"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/snapshot": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.5.tgz",
"integrity": "sha512-GJPZYcd7v8QNUJ7vRvLDmRwl+a1fGg4T/54lZXe+UOGy47F9yUfE18hRCtXL5aHN/AONu29NGzIXSVFh9K0feA==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.3.tgz",
"integrity": "sha512-kNRcHlI4txBGztuJfPEJ68VezlPAXLRT1u5UCx219TU3kOG2DplNxhWLwDf2h6emwmTPogzLnGVwP6epDaJN6Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "3.0.5",
"@vitest/pretty-format": "3.0.3",
"magic-string": "^0.30.17",
"pathe": "^2.0.2"
"pathe": "^2.0.1"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/spy": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.5.tgz",
"integrity": "sha512-5fOzHj0WbUNqPK6blI/8VzZdkBlQLnT25knX0r4dbZI9qoZDf3qAdjoMmDcLG5A83W6oUUFJgUd0EYBc2P5xqg==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.3.tgz",
"integrity": "sha512-7/dgux8ZBbF7lEIKNnEqQlyRaER9nkAL9eTmdKJkDO3hS8p59ATGwKOCUDHcBLKr7h/oi/6hP+7djQk8049T2A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1822,13 +1822,13 @@
}
},
"node_modules/@vitest/utils": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.5.tgz",
"integrity": "sha512-N9AX0NUoUtVwKwy21JtwzaqR5L5R5A99GAbrHfCCXK1lp593i/3AZAXhSP43wRQuxYsflrdzEfXZFo1reR1Nkg==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.3.tgz",
"integrity": "sha512-f+s8CvyzPtMFY1eZKkIHGhPsQgYo5qCm6O8KZoim9qm1/jT64qBgGpO5tHscNH6BzRHM+edLNOP+3vO8+8pE/A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "3.0.5",
"@vitest/pretty-format": "3.0.3",
"loupe": "^3.1.2",
"tinyrainbow": "^2.0.0"
},
@@ -2334,18 +2334,18 @@
}
},
"node_modules/eslint": {
"version": "9.20.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.1.tgz",
"integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==",
"version": "9.18.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.18.0.tgz",
"integrity": "sha512-+waTfRWQlSbpt3KWE+CjrPPYnbq9kfZIYUqapc0uBXyjTp8aYXZDsUH16m39Ryq3NjAVP4tjuF7KaukeqoCoaA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.19.0",
"@eslint/core": "^0.11.0",
"@eslint/core": "^0.10.0",
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "9.20.0",
"@eslint/js": "9.18.0",
"@eslint/plugin-kit": "^0.2.5",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
@@ -2407,9 +2407,9 @@
}
},
"node_modules/eslint-plugin-prettier": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz",
"integrity": "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw==",
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.2.tgz",
"integrity": "sha512-1yI3/hf35wmlq66C8yOyrujQnel+v5l1Vop5Cl2I6ylyNTT1JbuUUnV3/41PzwTzcyDp/oF0jWE3HXvcH5AQOQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2500,19 +2500,6 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint/node_modules/@eslint/core": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz",
"integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/eslint/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -2698,9 +2685,9 @@
"dev": true
},
"node_modules/fastq": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz",
"integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==",
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz",
"integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==",
"license": "ISC",
"dependencies": {
"reusify": "^1.0.4"
@@ -2841,9 +2828,9 @@
}
},
"node_modules/globals": {
"version": "15.15.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz",
"integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==",
"version": "15.14.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz",
"integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3193,9 +3180,9 @@
"dev": true
},
"node_modules/loupe": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz",
"integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz",
"integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==",
"dev": true,
"license": "MIT"
},
@@ -3298,9 +3285,9 @@
}
},
"node_modules/mock-fs": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.5.0.tgz",
"integrity": "sha512-d/P1M/RacgM3dB0sJ8rjeRNXxtapkPCUnMGmIN0ixJ16F/E4GUZCvWcSGfWGz8eaXYvn1s9baUwNjI4LOPEjiA==",
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.4.1.tgz",
"integrity": "sha512-sz/Q8K1gXXXHR+qr0GZg2ysxCRr323kuN10O7CtQjraJsFDJ4SJ+0I5MzALz7aRp9lHk8Cc/YdsT95h9Ka1aFw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3582,9 +3569,9 @@
}
},
"node_modules/prettier": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.1.tgz",
"integrity": "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw==",
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
"integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
"dev": true,
"license": "MIT",
"bin": {
@@ -4179,9 +4166,9 @@
}
},
"node_modules/ts-api-utils": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz",
"integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz",
"integrity": "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4301,15 +4288,15 @@
}
},
"node_modules/vite": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.1.0.tgz",
"integrity": "sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ==",
"version": "6.0.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.0.11.tgz",
"integrity": "sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.24.2",
"postcss": "^8.5.1",
"rollup": "^4.30.1"
"postcss": "^8.4.49",
"rollup": "^4.23.0"
},
"bin": {
"vite": "bin/vite.js"
@@ -4373,16 +4360,16 @@
}
},
"node_modules/vite-node": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.5.tgz",
"integrity": "sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.3.tgz",
"integrity": "sha512-0sQcwhwAEw/UJGojbhOrnq3HtiZ3tC7BzpAa0lx3QaTX0S3YX70iGcik25UBdB96pmdwjyY2uyKNYruxCDmiEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cac": "^6.7.14",
"debug": "^4.4.0",
"es-module-lexer": "^1.6.0",
"pathe": "^2.0.2",
"pathe": "^2.0.1",
"vite": "^5.0.0 || ^6.0.0"
},
"bin": {
@@ -4416,31 +4403,31 @@
}
},
"node_modules/vitest": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.5.tgz",
"integrity": "sha512-4dof+HvqONw9bvsYxtkfUp2uHsTN9bV2CZIi1pWgoFpL1Lld8LA1ka9q/ONSsoScAKG7NVGf2stJTI7XRkXb2Q==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.3.tgz",
"integrity": "sha512-dWdwTFUW9rcnL0LyF2F+IfvNQWB0w9DERySCk8VMG75F8k25C7LsZoh6XfCjPvcR8Nb+Lqi9JKr6vnzH7HSrpQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/expect": "3.0.5",
"@vitest/mocker": "3.0.5",
"@vitest/pretty-format": "^3.0.5",
"@vitest/runner": "3.0.5",
"@vitest/snapshot": "3.0.5",
"@vitest/spy": "3.0.5",
"@vitest/utils": "3.0.5",
"@vitest/expect": "3.0.3",
"@vitest/mocker": "3.0.3",
"@vitest/pretty-format": "^3.0.3",
"@vitest/runner": "3.0.3",
"@vitest/snapshot": "3.0.3",
"@vitest/spy": "3.0.3",
"@vitest/utils": "3.0.3",
"chai": "^5.1.2",
"debug": "^4.4.0",
"expect-type": "^1.1.0",
"magic-string": "^0.30.17",
"pathe": "^2.0.2",
"pathe": "^2.0.1",
"std-env": "^3.8.0",
"tinybench": "^2.9.0",
"tinyexec": "^0.3.2",
"tinypool": "^1.0.2",
"tinyrainbow": "^2.0.0",
"vite": "^5.0.0 || ^6.0.0",
"vite-node": "3.0.5",
"vite-node": "3.0.3",
"why-is-node-running": "^2.3.0"
},
"bin": {
@@ -4454,10 +4441,9 @@
},
"peerDependencies": {
"@edge-runtime/vm": "*",
"@types/debug": "^4.1.12",
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"@vitest/browser": "3.0.5",
"@vitest/ui": "3.0.5",
"@vitest/browser": "3.0.3",
"@vitest/ui": "3.0.3",
"happy-dom": "*",
"jsdom": "*"
},
@@ -4465,9 +4451,6 @@
"@edge-runtime/vm": {
"optional": true
},
"@types/debug": {
"optional": true
},
"@types/node": {
"optional": true
},

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/cli",
"version": "2.2.50",
"version": "2.2.47",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",
@@ -20,7 +20,7 @@
"@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12",
"@types/mock-fs": "^4.13.1",
"@types/node": "^22.13.2",
"@types/node": "^22.10.9",
"@typescript-eslint/eslint-plugin": "^8.15.0",
"@typescript-eslint/parser": "^8.15.0",
"@vitest/coverage-v8": "^3.0.0",
@@ -67,6 +67,6 @@
"lodash-es": "^4.17.21"
},
"volta": {
"node": "22.14.0"
"node": "22.13.1"
}
}

View File

@@ -2,37 +2,37 @@
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.52.0"
constraints = "4.52.0"
version = "4.50.0"
constraints = "4.50.0"
hashes = [
"h1:2BEJyXJtYC4B4nda/WCYUmuJYDaYk88F8t1pwPzr0iQ=",
"h1:4IASk5SESeWKQ7JU0+M7KApuF5mZyklvwMXPBabim3c=",
"h1:5ImZxxALSnWfH/4EXw/wFirSmk5Tr0ACmcysy51AafE=",
"h1:6TJ3dxLSin4ZKBJLsZDn95H2ZYnGm8S7GGHvvXuuMQU=",
"h1:IzTUjg9kQ4N3qizP9CjYLeHwjsuGgtxwXvfUQWyOLcA=",
"h1:NTaOQfYINA0YTG/V1/9+SYtgX1it63+cBugj4WK4FWc=",
"h1:PXH48LuJn329sCfMXprdMDk51EZaWFyajVvS03qhQLs=",
"h1:Pi5M+GeoMSN2eJ6QnIeXjBf19O+rby/74CfB2ocpv20=",
"h1:ShXZ2ZjBvm3thfoPPzPT8+OhyismnydQVkUAfI8X12w=",
"h1:WQ9hu0Wge2msBbODfottCSKgu8oKUrw4Opz+fDPVVHk=",
"h1:Z5yXML2DE0uH9UU+M0ut9JMQAORcwVZz1CxBHzeBmao=",
"h1:jqI2qKknpleS3JDSplyGYHMu0u9K/tor1ZOjFwDgEMk=",
"h1:kgfutDh14Q5nw4eg6qGFamFxIiY8Ae0FPKRBLDOzpcI=",
"h1:zCAO7GZmfYhWb+i6TfqlqhMeDyPZWGio2IzEzAh3YTs=",
"zh:19be1a91c982b902c42aba47766860dfa5dc151eed1e95fd39ca642229381ef0",
"zh:1de451c4d1ecf7efbe67b6dace3426ba810711afdd644b0f1b870364c8ae91f8",
"zh:352b4a2120173298622e669258744554339d959ac3a95607b117a48ee4a83238",
"zh:3c6f1346d9154afbd2d558fabb4b0150fc8d559aa961254144fe1bc17fe6032f",
"zh:4c4c92d53fb535b1e0eff26f222bbd627b97d3b4c891ec9c321268676d06152f",
"zh:53276f68006c9ceb7cdb10a6ccf91a5c1eadd1407a28edb5741e84e88d7e29e8",
"zh:7925a97773948171a63d4f65bb81ee92fd6d07a447e36012977313293a5435c9",
"zh:7dfb0a4496cfe032437386d0a2cd9229a1956e9c30bd920923c141b0f0440060",
"h1:0qvD5ZKn2tMZ8cOjQrUSITIC9tKCZbrSaSswV9lOyiU=",
"h1:4N0gplrZ0zOsJv3Kx1VfIx2FwrZHbYU0Un2yfiLZIGQ=",
"h1:81AMQq4kNKU/35U8ElQegUxG4E6xB0erIjG5xVmjIyo=",
"h1:EEQNADUmV3IL6x00yzy04i7OCSLeOMgM9XQkV3w71gA=",
"h1:HD0KI7td6oiSSAnJNn8UPSGf+hKiTo4JVQYfAiU1SqM=",
"h1:Hl+o5LtcvZg2f3l1hh9vaG/DFK6k+dTIZSeM0lXyfpo=",
"h1:ZUO2oIJ6jtZdvl816h0cEIiIeZ/fFCF64+abGEVxZZM=",
"h1:Zio80fnEeUKdlSOhTVskMEFSLUQ6TMsMKnXc+Dy2P2A=",
"h1:aLLvg36evTyqjtXGV2MjAV8imktXFmry7p/xCu9GQC4=",
"h1:azL05eWyy2V8SWkbZZImPWvv8ynG4eqmrbZhjXBDFug=",
"h1:ckMysHY4fJmr7o58XMi+DdgOTB/U/Mf1u1JA9ly3g/I=",
"h1:jxOwjDNjt5WCb4YjjiMsman91O8Y+MAPz6UwJ4a6F+0=",
"h1:u4OfnjSLa4Wk1IUFAzrvMnGgr8MvRHEWVDHEScPK2E8=",
"h1:wQkR1oeSkzlHn3rnVuLJRJLBHlg4EHt7Y64DeTjfkjQ=",
"zh:0ef99ed39472a94e6a0d6fa733cf0a46bce3bf66eba2873efae8846efdddc237",
"zh:2929cbbffcead171d45c88e4a7a59e9c013ea775dafa68b10da8db7cd04b6140",
"zh:462601c87118088e1a718842e367af7d8e7620598d426980a6d6b33de759865e",
"zh:56766eb62a74a9d88d9efb8486dd3a0c5c9db873d0a980ae9ef1e8af27d74231",
"zh:6b4e8810d99498a5a20a5872982a0f1354e79cfc4a7dfe7cc656f1c7eaae47d8",
"zh:6d65bdb4ec94b6eecc8abe26d94e2ca09262dc1e7a9934db829f418be0119920",
"zh:71adeaf31e41a358ec6095004062e43f56ee7d4b2504e5613ab351d511695641",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:8d4aa79f0a414bb4163d771063c70cd991c8fac6c766e685bac2ee12903c5bd6",
"zh:a67540c13565616a7e7e51ee9366e88b0dc60046e1d75c72680e150bd02725bb",
"zh:a936383a4767f5393f38f622e92bf2d0c03fe04b69c284951f27345766c7b31b",
"zh:d4887d73c466ff036eecf50ad6404ba38fd82ea4855296b1846d244b0f13c380",
"zh:e9093c8bd5b6cd99c81666e315197791781b8f93afa14fc2e0f732d1bb2a44b7",
"zh:efd3b3f1ec59a37f635aa1d4efcf178734c2fcf8ddb0d56ea690bec342da8672",
"zh:89761c15908ccc2cf9c50bb5cb3be45d3ad0c45fc7c608c6b95f48c0288b7160",
"zh:8cc5d7c5939da89cfd01f3e51c84f3576564783acea9db86bd9e32049805ed96",
"zh:987cff8225b1dd436cdcb4fc6228689ae7e4281de6896412a2a9a3325c49f05e",
"zh:991e83ebb89867d71e01a1c215ed159efb425683b0a44707be8579eb0a337f06",
"zh:ab8177ae2d8f5cfa90043a6f867435012cae115f6061b832a7e2462e0ae87a67",
"zh:d1ca34df1398f201274a6a18102975148c10ca15aa43cfc56cc9897620929509",
"zh:d34946f70201baf6dda03e3b294c6bbe40d95d0278e97b9f636ded94822b24ac",
]
}

View File

@@ -5,7 +5,7 @@ terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "4.52.0"
version = "4.50.0"
}
}
}

View File

@@ -2,37 +2,37 @@
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.52.0"
constraints = "4.52.0"
version = "4.50.0"
constraints = "4.50.0"
hashes = [
"h1:2BEJyXJtYC4B4nda/WCYUmuJYDaYk88F8t1pwPzr0iQ=",
"h1:4IASk5SESeWKQ7JU0+M7KApuF5mZyklvwMXPBabim3c=",
"h1:5ImZxxALSnWfH/4EXw/wFirSmk5Tr0ACmcysy51AafE=",
"h1:6TJ3dxLSin4ZKBJLsZDn95H2ZYnGm8S7GGHvvXuuMQU=",
"h1:IzTUjg9kQ4N3qizP9CjYLeHwjsuGgtxwXvfUQWyOLcA=",
"h1:NTaOQfYINA0YTG/V1/9+SYtgX1it63+cBugj4WK4FWc=",
"h1:PXH48LuJn329sCfMXprdMDk51EZaWFyajVvS03qhQLs=",
"h1:Pi5M+GeoMSN2eJ6QnIeXjBf19O+rby/74CfB2ocpv20=",
"h1:ShXZ2ZjBvm3thfoPPzPT8+OhyismnydQVkUAfI8X12w=",
"h1:WQ9hu0Wge2msBbODfottCSKgu8oKUrw4Opz+fDPVVHk=",
"h1:Z5yXML2DE0uH9UU+M0ut9JMQAORcwVZz1CxBHzeBmao=",
"h1:jqI2qKknpleS3JDSplyGYHMu0u9K/tor1ZOjFwDgEMk=",
"h1:kgfutDh14Q5nw4eg6qGFamFxIiY8Ae0FPKRBLDOzpcI=",
"h1:zCAO7GZmfYhWb+i6TfqlqhMeDyPZWGio2IzEzAh3YTs=",
"zh:19be1a91c982b902c42aba47766860dfa5dc151eed1e95fd39ca642229381ef0",
"zh:1de451c4d1ecf7efbe67b6dace3426ba810711afdd644b0f1b870364c8ae91f8",
"zh:352b4a2120173298622e669258744554339d959ac3a95607b117a48ee4a83238",
"zh:3c6f1346d9154afbd2d558fabb4b0150fc8d559aa961254144fe1bc17fe6032f",
"zh:4c4c92d53fb535b1e0eff26f222bbd627b97d3b4c891ec9c321268676d06152f",
"zh:53276f68006c9ceb7cdb10a6ccf91a5c1eadd1407a28edb5741e84e88d7e29e8",
"zh:7925a97773948171a63d4f65bb81ee92fd6d07a447e36012977313293a5435c9",
"zh:7dfb0a4496cfe032437386d0a2cd9229a1956e9c30bd920923c141b0f0440060",
"h1:0qvD5ZKn2tMZ8cOjQrUSITIC9tKCZbrSaSswV9lOyiU=",
"h1:4N0gplrZ0zOsJv3Kx1VfIx2FwrZHbYU0Un2yfiLZIGQ=",
"h1:81AMQq4kNKU/35U8ElQegUxG4E6xB0erIjG5xVmjIyo=",
"h1:EEQNADUmV3IL6x00yzy04i7OCSLeOMgM9XQkV3w71gA=",
"h1:HD0KI7td6oiSSAnJNn8UPSGf+hKiTo4JVQYfAiU1SqM=",
"h1:Hl+o5LtcvZg2f3l1hh9vaG/DFK6k+dTIZSeM0lXyfpo=",
"h1:ZUO2oIJ6jtZdvl816h0cEIiIeZ/fFCF64+abGEVxZZM=",
"h1:Zio80fnEeUKdlSOhTVskMEFSLUQ6TMsMKnXc+Dy2P2A=",
"h1:aLLvg36evTyqjtXGV2MjAV8imktXFmry7p/xCu9GQC4=",
"h1:azL05eWyy2V8SWkbZZImPWvv8ynG4eqmrbZhjXBDFug=",
"h1:ckMysHY4fJmr7o58XMi+DdgOTB/U/Mf1u1JA9ly3g/I=",
"h1:jxOwjDNjt5WCb4YjjiMsman91O8Y+MAPz6UwJ4a6F+0=",
"h1:u4OfnjSLa4Wk1IUFAzrvMnGgr8MvRHEWVDHEScPK2E8=",
"h1:wQkR1oeSkzlHn3rnVuLJRJLBHlg4EHt7Y64DeTjfkjQ=",
"zh:0ef99ed39472a94e6a0d6fa733cf0a46bce3bf66eba2873efae8846efdddc237",
"zh:2929cbbffcead171d45c88e4a7a59e9c013ea775dafa68b10da8db7cd04b6140",
"zh:462601c87118088e1a718842e367af7d8e7620598d426980a6d6b33de759865e",
"zh:56766eb62a74a9d88d9efb8486dd3a0c5c9db873d0a980ae9ef1e8af27d74231",
"zh:6b4e8810d99498a5a20a5872982a0f1354e79cfc4a7dfe7cc656f1c7eaae47d8",
"zh:6d65bdb4ec94b6eecc8abe26d94e2ca09262dc1e7a9934db829f418be0119920",
"zh:71adeaf31e41a358ec6095004062e43f56ee7d4b2504e5613ab351d511695641",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:8d4aa79f0a414bb4163d771063c70cd991c8fac6c766e685bac2ee12903c5bd6",
"zh:a67540c13565616a7e7e51ee9366e88b0dc60046e1d75c72680e150bd02725bb",
"zh:a936383a4767f5393f38f622e92bf2d0c03fe04b69c284951f27345766c7b31b",
"zh:d4887d73c466ff036eecf50ad6404ba38fd82ea4855296b1846d244b0f13c380",
"zh:e9093c8bd5b6cd99c81666e315197791781b8f93afa14fc2e0f732d1bb2a44b7",
"zh:efd3b3f1ec59a37f635aa1d4efcf178734c2fcf8ddb0d56ea690bec342da8672",
"zh:89761c15908ccc2cf9c50bb5cb3be45d3ad0c45fc7c608c6b95f48c0288b7160",
"zh:8cc5d7c5939da89cfd01f3e51c84f3576564783acea9db86bd9e32049805ed96",
"zh:987cff8225b1dd436cdcb4fc6228689ae7e4281de6896412a2a9a3325c49f05e",
"zh:991e83ebb89867d71e01a1c215ed159efb425683b0a44707be8579eb0a337f06",
"zh:ab8177ae2d8f5cfa90043a6f867435012cae115f6061b832a7e2462e0ae87a67",
"zh:d1ca34df1398f201274a6a18102975148c10ca15aa43cfc56cc9897620929509",
"zh:d34946f70201baf6dda03e3b294c6bbe40d95d0278e97b9f636ded94822b24ac",
]
}

View File

@@ -5,7 +5,7 @@ terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "4.52.0"
version = "4.50.0"
}
}
}

View File

@@ -1,13 +1,4 @@
#
# WARNING: To install Immich, follow our guide: https://immich.app/docs/install/docker-compose
#
# Make sure to use the docker-compose.yml of the current release:
#
# https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
#
# The compose file on main may not be compatible with the latest release.
# For development see:
# See:
# - https://immich.app/docs/developer/setup
# - https://immich.app/docs/developer/troubleshooting
@@ -116,7 +107,7 @@ services:
redis:
container_name: immich_redis
image: redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8
image: redis:6.2-alpine@sha256:905c4ee67b8e0aa955331960d2aa745781e6bd89afc44a8584bfd13bc890f0ae
healthcheck:
test: redis-cli ping || exit 1

View File

@@ -1,12 +1,3 @@
#
# WARNING: To install Immich, follow our guide: https://immich.app/docs/install/docker-compose
#
# Make sure to use the docker-compose.yml of the current release:
#
# https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
#
# The compose file on main may not be compatible with the latest release.
name: immich-prod
services:
@@ -56,7 +47,7 @@ services:
redis:
container_name: immich_redis
image: redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8
image: redis:6.2-alpine@sha256:905c4ee67b8e0aa955331960d2aa745781e6bd89afc44a8584bfd13bc890f0ae
healthcheck:
test: redis-cli ping || exit 1
restart: always
@@ -100,7 +91,7 @@ services:
container_name: immich_prometheus
ports:
- 9090:9090
image: prom/prometheus@sha256:5888c188cf09e3f7eebc97369c3b2ce713e844cdbd88ccf36f5047c958aea120
image: prom/prometheus@sha256:6559acbd5d770b15bb3c954629ce190ac3cbbdb2b7f1c30f0385c4e05104e218
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
@@ -112,7 +103,7 @@ services:
command: ['./run.sh', '-disable-reporting']
ports:
- 3000:3000
image: grafana/grafana:11.5.1-ubuntu@sha256:9a4ab78cec1a2ec7d1ca5dfd5aacec6412706a1bc9e971fc7184e2f6696a63f5
image: grafana/grafana:11.4.0-ubuntu@sha256:afccec22ba0e4815cca1d2bf3836e414322390dc78d77f1851976ffa8d61051c
volumes:
- grafana-data:/var/lib/grafana

View File

@@ -1,11 +1,10 @@
#
# WARNING: To install Immich, follow our guide: https://immich.app/docs/install/docker-compose
#
# Make sure to use the docker-compose.yml of the current release:
# WARNING: Make sure to use the docker-compose.yml of the current release:
#
# https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
#
# The compose file on main may not be compatible with the latest release.
#
name: immich
@@ -49,7 +48,7 @@ services:
redis:
container_name: immich_redis
image: docker.io/redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8
image: docker.io/redis:6.2-alpine@sha256:905c4ee67b8e0aa955331960d2aa745781e6bd89afc44a8584bfd13bc890f0ae
healthcheck:
test: redis-cli ping || exit 1
restart: always

View File

@@ -48,7 +48,6 @@ services:
vaapi-wsl: # use this for VAAPI if you're running Immich in WSL2
devices:
- /dev/dri:/dev/dri
- /dev/dxg:/dev/dxg
volumes:
- /usr/lib/wsl:/usr/lib/wsl
environment:

View File

@@ -1 +1 @@
22.14.0
22.13.1

View File

@@ -77,7 +77,9 @@ docker start immich_postgres # Start Postgres server
sleep 10 # Wait for Postgres server to start up
docker exec -it immich_postgres bash # Enter the Docker shell and run the following command
# Check the database user if you deviated from the default. If your backup ends in `.gz`, replace `cat` with `gunzip`
cat < "/dump.sql" | sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" | psql --dbname=postgres --username=<DB_USERNAME>
cat < "/dump.sql" \
| sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" \
| psql --dbname=postgres --username=<DB_USERNAME> # Restore Backup
exit # Exit the Docker shell
docker compose up -d # Start remainder of Immich apps
```

View File

@@ -70,4 +70,4 @@ When installing a new version of pgvecto.rs, you will need to manually update th
If you get the error `driverError: error: permission denied for view pg_vector_index_stat`, you can fix this by connecting to the Immich database and running `GRANT SELECT ON TABLE pg_vector_index_stat TO <immichdbusername>;`.
[vectors-install]: https://docs.vectorchord.ai/getting-started/installation.html
[vectors-install]: https://docs.pgvecto.rs/getting-started/installation.html

View File

@@ -50,18 +50,19 @@ The Immich CLI is an [npm](https://www.npmjs.com/) package that lets users contr
The Immich backend is divided into several services, which are run as individual docker containers.
1. `immich-server` - Handle and respond to REST API requests, execute background jobs (thumbnail generation, metadata extraction, transcoding, etc.)
1. `immich-server` - Handle and respond to REST API requests
1. `immich-microservices` - Execute background jobs (thumbnail generation, metadata extraction, transcoding, etc.)
1. `immich-machine-learning` - Execute machine learning models
1. `postgres` - Persistent data storage
1. `redis`- Queue management for background jobs
1. `redis`- Queue management for `immich-microservices`
### Immich Server
The Immich Server is a [TypeScript](https://www.typescriptlang.org/) project written for [Node.js](https://nodejs.org/). It uses the [Nest.js](https://nestjs.com) framework, [Express](https://expressjs.com/) server, and the query builder [Kysely](https://kysely.dev/). The server codebase also loosely follows the [Hexagonal Architecture](<https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)>). Specifically, we aim to separate technology specific implementations (`src/repositories`) from core business logic (`src/services`).
The Immich Server is a [TypeScript](https://www.typescriptlang.org/) project written for [Node.js](https://nodejs.org/). It uses the [Nest.js](https://nestjs.com) framework, with [TypeORM](https://typeorm.io/) for database management. The server codebase also loosely follows the [Hexagonal Architecture](<https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)>). Specifically, we aim to separate technology specific implementations (`infra/`) from core business logic (`domain/`).
### API Endpoints
#### REST Endpoints
An incoming HTTP request is mapped to a controller (`src/controllers`). Controllers are collections of HTTP endpoints. Each controller usually implements the following CRUD operations for its respective resource type:
The server is a list of HTTP endpoints and associated handlers (controllers). Each controller usually implements the following CRUD operations:
- `POST` `/<type>` - **Create**
- `GET` `/<type>` - **Read** (all)
@@ -69,13 +70,13 @@ An incoming HTTP request is mapped to a controller (`src/controllers`). Controll
- `PUT` `/<type>/:id` - **Updated** (by id)
- `DELETE` `/<type>/:id` - **Delete** (by id)
### Domain Transfer Objects (DTOs)
#### DTOs
The server uses [Domain Transfer Objects](https://en.wikipedia.org/wiki/Data_transfer_object) as public interfaces for the inputs (query, params, and body) and outputs (response) for each endpoint. DTOs translate to [OpenAPI](./open-api.md) schemas and control the generated code used by each client.
### Background Jobs
### Microservices
Immich uses a [worker](https://github.com/immich-app/immich/blob/main/server/src/utils/misc.ts#L266) to run background jobs. These jobs include:
The Immich Microservices image uses the same `Dockerfile` as the Immich Server, but with a different entrypoint. The Immich Microservices service mainly handles executing jobs, which include the following:
- Thumbnail Generation
- Metadata Extraction

View File

@@ -76,7 +76,7 @@ To see local changes to `@immich/ui` in Immich, do the following:
### Mobile app
The mobile app `(/mobile)` will required Flutter toolchain 3.13.x and FVM to be installed on your system.
The mobile app `(/mobile)` will required Flutter toolchain 3.13.x to be installed on your system.
Please refer to the [Flutter's official documentation](https://flutter.dev/docs/get-started/install) for more information on setting up the toolchain on your machine.

View File

@@ -58,7 +58,7 @@ If your photos are on a network drive, automatic file watching likely won't work
#### Troubleshooting
If you encounter an `ENOSPC` error, you need to increase your file watcher limit. In sysctl, this key is called `fs.inotify.max_user_watches` and has a default value of 8192. Increase this number to a suitable value greater than the number of files you will be watching. Note that Immich has to watch all files in your import paths including any ignored files.
If you encounter an `ENOSPC` error, you need to increase your file watcher limit. In sysctl, this key is called `fs.inotify.max_user_watched` and has a default value of 8192. Increase this number to a suitable value greater than the number of files you will be watching. Note that Immich has to watch all files in your import paths including any ignored files.
```
ERROR [LibraryService] Library watcher for library c69faf55-f96d-4aa0-b83b-2d80cbc27d98 encountered error: Error: ENOSPC: System limit for number of file watchers reached, watch '/media/photo.jpg'

View File

@@ -11,7 +11,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele
- ARM NN (Mali)
- CUDA (NVIDIA GPUs with [compute capability](https://developer.nvidia.com/cuda-gpus) 5.2 or higher)
- OpenVINO (Intel GPUs such as Iris Xe and Arc)
- OpenVINO (Intel discrete GPUs such as Iris Xe and Arc)
## Limitations
@@ -43,9 +43,8 @@ You do not need to redo any machine learning jobs after enabling hardware accele
#### OpenVINO
- Integrated GPUs are more likely to experience issues than discrete GPUs, especially for older processors or servers with low RAM.
- The server must have a discrete GPU, i.e. Iris Xe or Arc. Expect issues when attempting to use integrated graphics.
- Ensure the server's kernel version is new enough to use the device for hardware accceleration.
- Expect higher RAM usage when using OpenVINO compared to CPU processing.
## Setup

View File

@@ -8,23 +8,22 @@ For the full list, refer to the [Immich source code](https://github.com/immich-a
## Image formats
| Format | Extension(s) | Supported? | Notes |
| :---------- | :---------------------------- | :----------------: | :-------------- |
| `AVIF` | `.avif` | :white_check_mark: | |
| `BMP` | `.bmp` | :white_check_mark: | |
| `GIF` | `.gif` | :white_check_mark: | |
| `HEIC` | `.heic` | :white_check_mark: | |
| `HEIF` | `.heif` | :white_check_mark: | |
| `JPEG 2000` | `.jp2` | :white_check_mark: | |
| `JPEG` | `.webp` `.jpg` `.jpe` `.insp` | :white_check_mark: | |
| `JPEG XL` | `.jxl` | :white_check_mark: | |
| `PNG` | `.webp` | :white_check_mark: | |
| `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop |
| `RAW` | `.raw` | :white_check_mark: | |
| `RW2` | `.rw2` | :white_check_mark: | |
| `SVG` | `.svg` | :white_check_mark: | |
| `TIFF` | `.tif` `.tiff` | :white_check_mark: | |
| `WEBP` | `.webp` | :white_check_mark: | |
| Format | Extension(s) | Supported? | Notes |
| :-------- | :---------------------------- | :----------------: | :-------------- |
| `AVIF` | `.avif` | :white_check_mark: | |
| `BMP` | `.bmp` | :white_check_mark: | |
| `GIF` | `.gif` | :white_check_mark: | |
| `HEIC` | `.heic` | :white_check_mark: | |
| `HEIF` | `.heif` | :white_check_mark: | |
| `JPEG` | `.webp` `.jpg` `.jpe` `.insp` | :white_check_mark: | |
| `JPEG XL` | `.jxl` | :white_check_mark: | |
| `PNG` | `.webp` | :white_check_mark: | |
| `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop |
| `RAW` | `.raw` | :white_check_mark: | |
| `RW2` | `.rw2` | :white_check_mark: | |
| `SVG` | `.svg` | :white_check_mark: | |
| `TIFF` | `.tif` `.tiff` | :white_check_mark: | |
| `WEBP` | `.webp` | :white_check_mark: | |
## Video formats

View File

@@ -27,10 +27,6 @@ SELECT * FROM "assets" WHERE "originalPath" = 'upload/library/admin/2023/2023-09
SELECT * FROM "assets" WHERE "originalPath" LIKE 'upload/library/admin/2023/%';
```
```sql title="Find by ID"
SELECT * FROM "assets" WHERE "id" = '9f94e60f-65b6-47b7-ae44-a4df7b57f0e9';
```
:::note
You can calculate the checksum for a particular file by using the command `sha1sum <filename>`.
:::

View File

@@ -1,76 +0,0 @@
---
sidebar_position: 85
---
# Synology [Community]
:::note
This is a community contribution and not officially supported by the Immich team, but included here for convenience.
Community support can be found in the dedicated channel on the [Discord Server](https://discord.immich.app/).
**Please report app issues to the corresponding [Github Repository](https://github.com/truenas/charts/tree/master/community/immich).**
:::
Immich can easily be installed on a Synology NAS using Container Manager within DSM. If you have not installed Container Manager already, you can install it in the Packages Center. Refer to the [Container Manager docs](https://kb.synology.com/en-us/DSM/help/ContainerManager/docker_desc?version=7) for more information on using Container Manager.
## Step 1 - Download the required files
Create a directory of your choice (e.g. `./immich-app`) to house Immich. In general, it's a best practice to have all Docker-based applications running under the `./docker` directory, so in this case, your directory structure will look like `./docker/immich-app`.
Now create a `./postgres` and `./library` directory as sub-directories of the `./docker/immich-app`.
When you're all done, you should have the following:
- `./docker/immich-app/postgres`
- `./docker/immich-app/library`
Download [`docker-compose.yml`](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml) and [`example.env`](https://github.com/immich-app/immich/releases/latest/download/example.env) to your computer. Upload the files to the `./docker/immich-app` directory.
## Step 2 - Populate the .env file with custom values
Follow [Step 2 in Docker Compose](./docker-compose#step-2---populate-the-env-file-with-custom-values) for instructions on customizing the `.env` file, and then return back to this guide to continue.
## Step 3 - Create a new project in Container Manager
Open Container Manager, and select the "**Project**" action on the left navigation bar and then click "**Create**".
![Create Project](../../static/img/synology-container-manager-create-project.png)
In the settings of your new project, set "**Project name**" to a name you'll remember, such as _immich-app_. When setting the "**Path**", select the `./docker/immich-app` directory you created earlier. Doing so will prompt a message to use the existing `docker-compose.yml` already present in the directory for your project. Click "**OK**" to continue.
![Set Path](../../static/img/synology-container-manager-set-path.png)
The following screen will give you the option to further customize your `docker-compose.yml` file, giving you a warning regarding the `start_interval` property. Under the `healthcheck` heading, remove the `start_interval: 30s` completely and click "**Next**".
![start interval](../../static/img/synology-container-manager-customize-docker-compose.png)
Skip the section asking to set-up a portal for Web Station, and then complete the wizard which will build and start the containers for your project.
Once your containers are successfully running, navigate to the "**Container**" section of Container Manager, right-click on the "**immich-server**" container, and choose the "**Details**".
Scroll to the bottom of the "**Details**" section, and find the `IP Address` of the container, located in the `Network` section. Take note of the container's IP address as you will need it for **Step 4**.
![Container Details](../../static/img/synology-container-manager-container-details.png)
## Step 4 - Configure Firewall Settings
Once your project completes the build process, your containers will start. In order to be able to access Immich from your browser, you need to configure the firewall settings for your Synology NAS.
Open "**Control Panel**" on your Synology NAS, and select "**Security**". Navigate to "**Firewall**"
![Firewall rules](../../static/img/synology-firewall-rules.png)
Click "**Edit Rules**" and add the following firewall rules:
- Add a "**Source IP**" rule for the IP address of your container that you obtained in Step 3 above
- Add a "**Ports**" rule for the port specified in the `docker-compose.yml`, which should be `2283`
## Next Steps
Read the [Post Installation](/docs/install/post-install.mdx) steps or setup optional features below.
### Setting up optional features
- [External Libraries](/docs/features/libraries.md): Adding your existing photo library to Immich
- [Hardware Transcoding](/docs/features/hardware-transcoding.md): Speeding up video transcoding
- [Hardware-Accelerated Machine Learning](/docs/features/ml-hardware-acceleration.md): Speeding up various machine learning tasks in Immich

View File

@@ -41,7 +41,7 @@ className="border rounded-xl"
:::info Permissions
The **pgData** dataset must be owned by the user `netdata` (UID 999) for postgres to start. The other datasets must be owned by the user `root` (UID 0) or a group that includes the user `root` (UID 0) for immich to have the necessary permissions.
If the **library** dataset uses ACL it must have [ACL mode](https://www.truenas.com/docs/core/coretutorials/storage/pools/permissions/#access-control-lists) set to `Passthrough` if you plan on using a [storage template](/docs/administration/storage-template.mdx) and the dataset is configured for network sharing (its ACL type is set to `SMB/NFSv4`). When the template is applied and files need to be moved from **upload** to **library**, Immich performs `chmod` internally and needs to be allowed to execute the command. [More info.](https://github.com/immich-app/immich/pull/13017)
If the **library** dataset uses ACL it must have [ACL mode](https://www.truenas.com/docs/core/coretutorials/storage/pools/permissions/#access-control-lists) set to `Passthrough` if you plan on using a [storage template](/docs/administration/storage-template.mdx) and the dataset is configured for network sharing (its ACL type is set to `SMB/NFSv4`). When the template is applied and files need to be moved from **upload** to **library**, immich performs `chmod` internally and needs to be allowed to execute the command. [More info.](https://github.com/immich-app/immich/pull/13017)
:::
## Installing the Immich Application
@@ -160,10 +160,6 @@ The image above has example values.
### Additional Storage [(External Libraries)](/docs/features/libraries)
:::danger Advanced Users Only
This feature should only be used by advanced users. If this is your first time installing Immich, then DO NOT mount an external library until you have a working setup. Also, your mount path MUST be something unique and should NOT be your library or upload location or a Linux directory like `/lib`. The picture below shows a valid example.
:::
<img
src={require('./img/truenas10.webp').default}
width="40%"
@@ -172,7 +168,7 @@ className="border rounded-xl"
/>
You may configure [External Libraries](/docs/features/libraries) by mounting them using **Additional Storage**.
The **Mount Path** is the location you will need to copy and paste into the External Library settings within Immich.
The **Mount Path** is the loaction you will need to copy and paste into the External Library settings within Immich.
The **Host Path** is the location on the TrueNAS SCALE server where your external library is located.
<!-- A section for Labels would go here but I don't know what they do. -->

View File

@@ -72,7 +72,7 @@ alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich"
</ul>
</details>
5. Click "**Save Changes**", you will be prompted to edit stack UI labels, just leave this blank and click "**Ok**"
5. Click "**Save Changes**", you will be promoted to edit stack UI labels, just leave this blank and click "**Ok**"
6. Select the cog ⚙️ next to Immich, click "**Edit Stack**", then click "**Env File**"
7. Paste the entire contents of the [Immich example.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file into the Unraid editor, then **before saving** edit the following:

View File

@@ -110,9 +110,9 @@ const config = {
label: 'API',
},
{
href: 'https://immich.store',
to: '/blog',
position: 'right',
label: 'Merch',
label: 'Blog',
},
{
href: 'https://github.com/immich-app/immich',

12
docs/package-lock.json generated
View File

@@ -14061,9 +14061,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.2",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz",
"integrity": "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==",
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
"integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
"funding": [
{
"type": "opencollective",
@@ -15725,9 +15725,9 @@
}
},
"node_modules/prettier": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.1.tgz",
"integrity": "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw==",
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
"integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
"dev": true,
"license": "MIT",
"bin": {

View File

@@ -55,6 +55,6 @@
"node": ">=20"
},
"volta": {
"node": "22.14.0"
"node": "22.13.1"
}
}

View File

@@ -44,12 +44,12 @@ export default function VersionSwitcher(): JSX.Element {
return (
versions.length > 0 && (
<DropdownNavbarItem
className="version-switcher-34ab39"
className="navbar__item"
label={label}
mobile={windowSize === 'mobile'}
items={versions.map(({ label, url }) => ({
label,
to: url + location.pathname + location.hash,
to: url,
target: '_self',
}))}
/>

View File

@@ -75,11 +75,6 @@ div[class^='announcementBar_'] {
font-weight: 500;
}
/* workaround for version switcher PR 15894 */
div[class*='navbar__items'] > li:has(a[class*='version-switcher-34ab39']) {
display: none;
}
code {
font-weight: 600;
}

View File

@@ -50,13 +50,6 @@ function HomepageHeader() {
>
Demo
</Link>
<Link
className="flex place-items-center place-content-center py-3 px-8 border bg-immich-primary/10 dark:bg-gray-300 rounded-xl hover:no-underline text-immich-primary dark:text-immich-dark-bg font-bold uppercase"
to="https://immich.store"
>
Buy Merch
</Link>
</div>
<div className="my-12 flex gap-1 font-medium place-items-center place-content-center text-immich-primary dark:text-immich-dark-primary">

View File

@@ -1,16 +1,4 @@
[
{
"label": "v1.126.1",
"url": "https://v1.126.1.archive.immich.app"
},
{
"label": "v1.126.0",
"url": "https://v1.126.0.archive.immich.app"
},
{
"label": "v1.125.7",
"url": "https://v1.125.7.archive.immich.app"
},
{
"label": "v1.125.6",
"url": "https://v1.125.6.archive.immich.app"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -1 +1 @@
22.14.0
22.13.1

View File

@@ -34,7 +34,7 @@ services:
- 2285:2285
redis:
image: redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8
image: redis:6.2-alpine@sha256:905c4ee67b8e0aa955331960d2aa745781e6bd89afc44a8584bfd13bc890f0ae
database:
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0

868
e2e/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "1.126.1",
"version": "1.125.6",
"description": "",
"main": "index.js",
"type": "module",
@@ -25,7 +25,7 @@
"@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1",
"@types/luxon": "^3.4.2",
"@types/node": "^22.13.2",
"@types/node": "^22.10.9",
"@types/oidc-provider": "^8.5.1",
"@types/pg": "^8.11.0",
"@types/pngjs": "^6.0.4",
@@ -53,6 +53,6 @@
"vitest": "^3.0.0"
},
"volta": {
"node": "22.14.0"
"node": "22.13.1"
}
}

View File

@@ -1,86 +0,0 @@
import { JobCommand, JobName, LoginResponseDto } from '@immich/sdk';
import { readFile } from 'node:fs/promises';
import { basename } from 'node:path';
import { errorDto } from 'src/responses';
import { app, testAssetDir, utils } from 'src/utils';
import request from 'supertest';
import { afterEach, beforeAll, describe, expect, it } from 'vitest';
describe('/jobs', () => {
let admin: LoginResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup({ onboarding: false });
});
describe('PUT /jobs', () => {
afterEach(async () => {
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
command: JobCommand.Resume,
force: false,
});
});
it('should require authentication', async () => {
const { status, body } = await request(app).put('/jobs/metadataExtraction');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should queue metadata extraction for missing assets', async () => {
const path1 = `${testAssetDir}/formats/raw/Nikon/D700/philadelphia.nef`;
const path2 = `${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`;
await utils.createAsset(admin.accessToken, {
assetData: { bytes: await readFile(path1), filename: basename(path1) },
});
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
command: JobCommand.Pause,
force: false,
});
const { id } = await utils.createAsset(admin.accessToken, {
assetData: { bytes: await readFile(path2), filename: basename(path2) },
});
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
{
const asset = await utils.getAssetInfo(admin.accessToken, id);
expect(asset.exifInfo).toBeDefined();
expect(asset.exifInfo?.make).toBeNull();
}
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
command: JobCommand.Empty,
force: false,
});
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
command: JobCommand.Resume,
force: false,
});
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
command: JobCommand.Start,
force: false,
});
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
{
const asset = await utils.getAssetInfo(admin.accessToken, id);
expect(asset.exifInfo).toBeDefined();
expect(asset.exifInfo?.make).toBe('NIKON CORPORATION');
}
});
});
});

View File

@@ -1,4 +1,4 @@
import { LibraryResponseDto, LoginResponseDto, getAllLibraries } from '@immich/sdk';
import { LibraryResponseDto, LoginResponseDto, getAllLibraries, scanLibrary } from '@immich/sdk';
import { cpSync, existsSync, rmSync, unlinkSync } from 'node:fs';
import { Socket } from 'socket.io-client';
import { userDto, uuidDto } from 'src/fixtures';
@@ -8,6 +8,8 @@ import request from 'supertest';
import { utimes } from 'utimes';
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
const scan = async (accessToken: string, id: string) => scanLibrary({ id }, { headers: asBearerAuth(accessToken) });
describe('/libraries', () => {
let admin: LoginResponseDto;
let user: LoginResponseDto;
@@ -296,8 +298,6 @@ describe('/libraries', () => {
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets } = await utils.searchAssets(admin.accessToken, {
originalPath: `${testAssetDirInternal}/temp/directoryA/assetA.png`,
@@ -312,7 +312,15 @@ describe('/libraries', () => {
importPaths: [`${testAssetDirInternal}/temp/directoryA`],
});
await utils.scan(admin.accessToken, library.id);
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration');
const { assets } = await utils.searchAssets(admin.accessToken, {
originalPath: `${testAssetDirInternal}/temp/directoryA/assetA.png`,
@@ -332,7 +340,13 @@ describe('/libraries', () => {
exclusionPatterns: ['**/directoryA'],
});
await utils.scan(admin.accessToken, library.id);
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
@@ -346,7 +360,13 @@ describe('/libraries', () => {
importPaths: [`${testAssetDirInternal}/temp/directoryA`, `${testAssetDirInternal}/temp/directoryB`],
});
await utils.scan(admin.accessToken, library.id);
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
@@ -365,7 +385,13 @@ describe('/libraries', () => {
utils.createImageFile(`${testAssetDir}/temp/folder, a/assetA.png`);
utils.createImageFile(`${testAssetDir}/temp/folder, b/assetB.png`);
await utils.scan(admin.accessToken, library.id);
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
@@ -387,7 +413,13 @@ describe('/libraries', () => {
utils.createImageFile(`${testAssetDir}/temp/folder{ a/assetA.png`);
utils.createImageFile(`${testAssetDir}/temp/folder} b/assetB.png`);
await utils.scan(admin.accessToken, library.id);
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
@@ -439,7 +471,13 @@ describe('/libraries', () => {
utils.createImageFile(`${testAssetDir}/temp/folder${char}1/asset1.png`);
utils.createImageFile(`${testAssetDir}/temp/folder${char}2/asset2.png`);
await utils.scan(admin.accessToken, library.id);
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
@@ -463,12 +501,23 @@ describe('/libraries', () => {
utils.createImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
await utils.scan(admin.accessToken, library.id);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/reimport/asset.jpg`);
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_001);
await utils.scan(admin.accessToken, library.id);
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets } = await utils.searchAssets(admin.accessToken, {
libraryId: library.id,
@@ -499,12 +548,21 @@ describe('/libraries', () => {
utils.createImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
await utils.scan(admin.accessToken, library.id);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/reimport/asset.jpg`);
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
await utils.scan(admin.accessToken, library.id);
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets } = await utils.searchAssets(admin.accessToken, {
libraryId: library.id,
@@ -534,14 +592,21 @@ describe('/libraries', () => {
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
await utils.scan(admin.accessToken, library.id);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(1);
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
await utils.scan(admin.accessToken, library.id);
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
expect(trashedAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
@@ -559,7 +624,8 @@ describe('/libraries', () => {
importPaths: [`${testAssetDirInternal}/temp/offline`],
});
await utils.scan(admin.accessToken, library.id);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(1);
@@ -570,7 +636,13 @@ describe('/libraries', () => {
importPaths: [`${testAssetDirInternal}/temp/another-path/`],
});
await utils.scan(admin.accessToken, library.id);
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
expect(trashedAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
@@ -590,7 +662,8 @@ describe('/libraries', () => {
importPaths: [`${testAssetDirInternal}/temp`],
});
await utils.scan(admin.accessToken, library.id);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.searchAssets(admin.accessToken, {
libraryId: library.id,
@@ -600,7 +673,8 @@ describe('/libraries', () => {
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/directoryB/**'] });
await utils.scan(admin.accessToken, library.id);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
expect(trashedAsset.isTrashed).toBe(true);
@@ -622,12 +696,19 @@ describe('/libraries', () => {
importPaths: [`${testAssetDirInternal}/temp`],
});
await utils.scan(admin.accessToken, library.id);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets: assetsBefore } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
expect(assetsBefore.count).toBeGreaterThan(1);
await utils.scan(admin.accessToken, library.id);
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
@@ -644,7 +725,11 @@ describe('/libraries', () => {
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await utils.scan(admin.accessToken, library.id);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
@@ -667,7 +752,10 @@ describe('/libraries', () => {
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await utils.scan(admin.accessToken, library.id);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
@@ -691,7 +779,10 @@ describe('/libraries', () => {
cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await utils.scan(admin.accessToken, library.id);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
@@ -715,13 +806,19 @@ describe('/libraries', () => {
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
await utils.scan(admin.accessToken, library.id);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
unlinkSync(`${testAssetDir}/temp/xmp/glarus.xmp`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
await utils.scan(admin.accessToken, library.id);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
@@ -744,12 +841,18 @@ describe('/libraries', () => {
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
await utils.scan(admin.accessToken, library.id);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
await utils.scan(admin.accessToken, library.id);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
@@ -772,12 +875,18 @@ describe('/libraries', () => {
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
await utils.scan(admin.accessToken, library.id);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
await utils.scan(admin.accessToken, library.id);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
@@ -801,13 +910,19 @@ describe('/libraries', () => {
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
await utils.scan(admin.accessToken, library.id);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
unlinkSync(`${testAssetDir}/temp/xmp/glarus.nef.xmp`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
await utils.scan(admin.accessToken, library.id);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
@@ -831,12 +946,18 @@ describe('/libraries', () => {
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
await utils.scan(admin.accessToken, library.id);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
unlinkSync(`${testAssetDir}/temp/xmp/glarus.nef.xmp`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
await utils.scan(admin.accessToken, library.id);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
@@ -860,12 +981,18 @@ describe('/libraries', () => {
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
await utils.scan(admin.accessToken, library.id);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
unlinkSync(`${testAssetDir}/temp/xmp/glarus.xmp`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
await utils.scan(admin.accessToken, library.id);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
@@ -888,13 +1015,22 @@ describe('/libraries', () => {
importPaths: [`${testAssetDirInternal}/temp/offline`],
});
await utils.scan(admin.accessToken, library.id);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
utils.renameImageFile(`${testAssetDir}/temp/offline/offline.png`, `${testAssetDir}/temp/offline.png`);
await utils.scan(admin.accessToken, library.id);
{
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
}
await utils.waitForQueueFinish(admin.accessToken, 'library');
const offlineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
expect(offlineAsset.isTrashed).toBe(true);
@@ -908,7 +1044,15 @@ describe('/libraries', () => {
utils.renameImageFile(`${testAssetDir}/temp/offline.png`, `${testAssetDir}/temp/offline/offline.png`);
await utils.scan(admin.accessToken, library.id);
{
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
}
await utils.waitForQueueFinish(admin.accessToken, 'library');
const backOnlineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
@@ -930,13 +1074,22 @@ describe('/libraries', () => {
importPaths: [`${testAssetDirInternal}/temp/offline`],
});
await utils.scan(admin.accessToken, library.id);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
utils.renameImageFile(`${testAssetDir}/temp/offline/offline.png`, `${testAssetDir}/temp/offline.png`);
await utils.scan(admin.accessToken, library.id);
{
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
}
await utils.waitForQueueFinish(admin.accessToken, 'library');
{
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id, withDeleted: true });
@@ -957,7 +1110,15 @@ describe('/libraries', () => {
importPaths: [`${testAssetDirInternal}/temp/another-path`],
});
await utils.scan(admin.accessToken, library.id);
{
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
}
await utils.waitForQueueFinish(admin.accessToken, 'library');
const stillOfflineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
@@ -981,13 +1142,22 @@ describe('/libraries', () => {
importPaths: [`${testAssetDirInternal}/temp/offline`],
});
await utils.scan(admin.accessToken, library.id);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
utils.renameImageFile(`${testAssetDir}/temp/offline/offline.png`, `${testAssetDir}/temp/offline.png`);
await utils.scan(admin.accessToken, library.id);
{
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
}
await utils.waitForQueueFinish(admin.accessToken, 'library');
{
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id, withDeleted: true });
@@ -1004,7 +1174,15 @@ describe('/libraries', () => {
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] });
await utils.scan(admin.accessToken, library.id);
{
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
}
await utils.waitForQueueFinish(admin.accessToken, 'library');
const stillOfflineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
@@ -1124,7 +1302,8 @@ describe('/libraries', () => {
importPaths: [`${testAssetDirInternal}/temp`],
});
await utils.scan(admin.accessToken, library.id);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { status, body } = await request(app)
.delete(`/libraries/${library.id}`)

View File

@@ -1,7 +1,7 @@
import { getPerson, LoginResponseDto, PersonResponseDto } from '@immich/sdk';
import { LoginResponseDto, PersonResponseDto } from '@immich/sdk';
import { uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils';
import { app, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
@@ -195,7 +195,6 @@ describe('/people', () => {
.send({
name: 'New Person',
birthDate: '1990-01-01',
color: '#333',
});
expect(status).toBe(201);
expect(body).toMatchObject({
@@ -204,22 +203,6 @@ describe('/people', () => {
birthDate: '1990-01-01T00:00:00.000Z',
});
});
it('should create a favorite person', async () => {
const { status, body } = await request(app)
.post(`/people`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({
name: 'New Favorite Person',
isFavorite: true,
});
expect(status).toBe(201);
expect(body).toMatchObject({
id: expect.any(String),
name: 'New Favorite Person',
isFavorite: true,
});
});
});
describe('PUT /people/:id', () => {
@@ -233,7 +216,6 @@ describe('/people', () => {
{ key: 'name', type: 'string' },
{ key: 'featureFaceAssetId', type: 'string' },
{ key: 'isHidden', type: 'boolean value' },
{ key: 'isFavorite', type: 'boolean value' },
]) {
it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app)
@@ -273,42 +255,6 @@ describe('/people', () => {
expect(status).toBe(200);
expect(body).toMatchObject({ birthDate: null });
});
it('should set a color', async () => {
const { status, body } = await request(app)
.put(`/people/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ color: '#555' });
expect(status).toBe(200);
expect(body).toMatchObject({ color: '#555' });
});
it('should clear a color', async () => {
const { status, body } = await request(app)
.put(`/people/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ color: null });
expect(status).toBe(200);
expect(body.color).toBeUndefined();
});
it('should mark a person as favorite', async () => {
const person = await utils.createPerson(admin.accessToken, {
name: 'visible_person',
});
expect(person.isFavorite).toBe(false);
const { status, body } = await request(app)
.put(`/people/${person.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ isFavorite: true });
expect(status).toBe(200);
expect(body).toMatchObject({ isFavorite: true });
const person2 = await getPerson({ id: person.id }, { headers: asBearerAuth(admin.accessToken) });
expect(person2).toMatchObject({ id: person.id, isFavorite: true });
});
});
describe('POST /people/:id/merge', () => {

View File

@@ -89,7 +89,7 @@ describe('/shared-links', () => {
await deleteUserAdmin({ id: user2.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) });
});
describe('GET /share/:key', () => {
describe('GET /share/${key}', () => {
it('should have correct asset count in meta tag for non-empty album', async () => {
const resp = await request(shareUrl).get(`/${linkWithMetadata.key}`);
expect(resp.status).toBe(200);
@@ -139,10 +139,7 @@ describe('/shared-links', () => {
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: linkWithAlbum.id }),
expect.objectContaining({
id: linkWithAssets.id,
assets: expect.arrayContaining([expect.objectContaining({ id: asset1.id })]),
}),
expect.objectContaining({ id: linkWithAssets.id }),
expect.objectContaining({ id: linkWithPassword.id }),
expect.objectContaining({ id: linkWithMetadata.id }),
expect.objectContaining({ id: linkWithoutMetadata.id }),
@@ -150,30 +147,6 @@ describe('/shared-links', () => {
);
});
it('should filter on albumId', async () => {
const { status, body } = await request(app)
.get(`/shared-links?albumId=${album.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(2);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: linkWithAlbum.id }),
expect.objectContaining({ id: linkWithPassword.id }),
]),
);
});
it('should find 0 albums', async () => {
const { status, body } = await request(app)
.get(`/shared-links?albumId=${uuidDto.notFound}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(0);
});
it('should not get shared links created by other users', async () => {
const { status, body } = await request(app)
.get('/shared-links')
@@ -204,12 +177,6 @@ describe('/shared-links', () => {
);
});
it('should increment the view count', async () => {
const request1 = await request(app).get('/shared-links/me').query({ key: linkWithAlbum.key });
const request2 = await request(app).get('/shared-links/me').query({ key: linkWithAlbum.key });
expect(request2.body.viewCount).toBe(request1.body.viewCount + 1);
});
it('should return unauthorized for incorrect shared link', async () => {
const { status, body } = await request(app)
.get('/shared-links/me')

View File

@@ -356,24 +356,5 @@ describe('/admin/users', () => {
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should restore a user', async () => {
const user = await utils.userSetup(admin.accessToken, createUserDto.create('restore'));
await deleteUserAdmin({ id: user.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) });
const { status, body } = await request(app)
.post(`/admin/users/${user.userId}/restore`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
id: user.userId,
email: user.userEmail,
status: 'active',
deletedAt: null,
}),
);
});
});
});

View File

@@ -6,8 +6,6 @@ import {
CheckExistingAssetsDto,
CreateAlbumDto,
CreateLibraryDto,
JobCommandDto,
JobName,
MetadataSearchDto,
Permission,
PersonCreateDto,
@@ -30,9 +28,7 @@ import {
getAssetInfo,
getConfigDefaults,
login,
scanLibrary,
searchAssets,
sendJobCommand,
setBaseUrl,
signUpAdmin,
tagAssets,
@@ -479,9 +475,6 @@ export const utils = {
tagAssets: (accessToken: string, tagId: string, assetIds: string[]) =>
tagAssets({ id: tagId, bulkIdsDto: { ids: assetIds } }, { headers: asBearerAuth(accessToken) }),
jobCommand: async (accessToken: string, jobName: JobName, jobCommandDto: JobCommandDto) =>
sendJobCommand({ id: jobName, jobCommandDto }, { headers: asBearerAuth(accessToken) }),
setAuthCookies: async (context: BrowserContext, accessToken: string, domain = '127.0.0.1') =>
await context.addCookies([
{
@@ -554,14 +547,6 @@ export const utils = {
await immichCli(['login', app, `${key.secret}`]);
return key.secret;
},
scan: async (accessToken: string, id: string) => {
await scanLibrary({ id }, { headers: asBearerAuth(accessToken) });
await utils.waitForQueueFinish(accessToken, 'library');
await utils.waitForQueueFinish(accessToken, 'sidecar');
await utils.waitForQueueFinish(accessToken, 'metadataExtraction');
},
};
utils.initSdk();

View File

@@ -1,6 +1,4 @@
{
"search_by_description_example": "Hiking day in Sapa",
"search_by_description": "Search by description",
"about": "About",
"account": "Account",
"account_settings": "Account Settings",
@@ -436,7 +434,6 @@
"back_close_deselect": "Back, close, or deselect",
"backward": "Backward",
"birthdate_saved": "Date of birth saved successfully",
"show_shared_links": "Show shared links",
"birthdate_set_description": "Date of birth is used to calculate the age of this person at the time of a photo.",
"blurred_background": "Blurred background",
"bugs_and_feature_requests": "Bugs & Feature Requests",
@@ -769,10 +766,8 @@
"go_to_search": "Go to search",
"go_to_folder": "Go to folder",
"group_albums_by": "Group albums by...",
"group_country": "Group by country",
"group_no": "No grouping",
"group_owner": "Group by owner",
"group_places_by": "Group places by...",
"group_year": "Group by year",
"has_quota": "Has quota",
"hi_user": "Hi {name} ({email})",
@@ -805,7 +800,6 @@
"include_shared_albums": "Include shared albums",
"include_shared_partner_assets": "Include shared partner assets",
"individual_share": "Individual share",
"individual_shares": "Individual shares",
"info": "Info",
"interval": {
"day_at_onepm": "Every day at 1pm",
@@ -816,7 +810,6 @@
"invite_people": "Invite People",
"invite_to_album": "Invite to album",
"items_count": "{count, plural, one {# item} other {# items}}",
"views_count": "{count, plural, one {# view} other {# views}}",
"jobs": "Jobs",
"keep": "Keep",
"keep_all": "Keep All",
@@ -992,7 +985,6 @@
"pick_a_location": "Pick a location",
"place": "Place",
"places": "Places",
"places_count": "{count, plural, one {{count, number} Place} other {{count, number} Places}}",
"play": "Play",
"play_memories": "Play memories",
"play_motion_photo": "Play Motion Photo",
@@ -1175,7 +1167,6 @@
"shared_from_partner": "Photos from {partner}",
"shared_link_options": "Shared link options",
"shared_links": "Shared links",
"shared_links_description": "Share photos and videos with a link",
"shared_photos_and_videos_count": "{assetCount, plural, other {# shared photos & videos.}}",
"shared_with_partner": "Shared with {partner}",
"sharing": "Sharing",
@@ -1285,7 +1276,6 @@
"unfavorite": "Unfavorite",
"unhide_person": "Unhide person",
"unknown": "Unknown",
"unknown_country": "Unknown Country",
"unknown_year": "Unknown Year",
"unlimited": "Unlimited",
"unlink_motion_video": "Unlink motion video",
@@ -1360,4 +1350,4 @@
"yes": "Yes",
"you_dont_have_any_shared_links": "You don't have any shared links",
"zoom_image": "Zoom Image"
}
}

View File

@@ -312,157 +312,157 @@
"admin_password": "رمز عبور مدیر",
"administration": "مدیریت",
"advanced": "پیشرفته",
"album_added": "آلبوم اضافه شد",
"album_added": "",
"album_added_notification_setting_description": "",
"album_cover_updated": "جلد آلبوم به‌روزرسانی شد",
"album_info_updated": "اطلاعات آلبوم به‌روزرسانی شد",
"album_name": "نام آلبوم",
"album_options": "گزینه‌های آلبوم",
"album_updated": "آلبوم به‌روزرسانی شد",
"album_cover_updated": "",
"album_info_updated": "",
"album_name": "",
"album_options": "",
"album_updated": "",
"album_updated_setting_description": "",
"albums": "آلبوم‌ها",
"albums": "",
"albums_count": "",
"all": "همه",
"all_people": "همه افراد",
"allow_dark_mode": "اجازه دادن به حالت تاریک",
"allow_edits": "اجازه ویرایش",
"api_key": "کلید API",
"api_keys": "کلیدهای API",
"app_settings": "تنظیمات برنامه",
"appears_in": "ظاهر می‌شود در",
"archive": "بایگانی",
"all": "",
"all_people": "",
"allow_dark_mode": "",
"allow_edits": "",
"api_key": "",
"api_keys": "",
"app_settings": "",
"appears_in": "",
"archive": "",
"archive_or_unarchive_photo": "",
"archive_size": "اندازه بایگانی",
"archive_size": "",
"archive_size_description": "",
"asset_offline": "محتوا آفلاین",
"assets": "محتواها",
"authorized_devices": "دستگاه‌های مجاز",
"back": "بازگشت",
"backward": "عقب",
"blurred_background": "پس‌زمینه محو",
"asset_offline": "",
"assets": "",
"authorized_devices": "",
"back": "",
"backward": "",
"blurred_background": "",
"bulk_delete_duplicates_confirmation": "",
"bulk_keep_duplicates_confirmation": "",
"bulk_trash_duplicates_confirmation": "",
"camera": "دوربین",
"camera_brand": "برند دوربین",
"camera_model": "مدل دوربین",
"cancel": "لغو",
"cancel_search": "لغو جستجو",
"cannot_merge_people": "نمی‌توان افراد را ادغام کرد",
"cannot_update_the_description": "نمی‌توان توضیحات را به‌روزرسانی کرد",
"change_date": "تغییر تاریخ",
"change_expiration_time": "تغییر زمان انقضا",
"change_location": "تغییر مکان",
"change_name": "تغییر نام",
"change_name_successfully": "نام با موفقیت تغییر یافت",
"change_password": "تغییر رمز عبور",
"change_your_password": "رمز عبور خود را تغییر دهید",
"camera": "",
"camera_brand": "",
"camera_model": "",
"cancel": "",
"cancel_search": "",
"cannot_merge_people": "",
"cannot_update_the_description": "",
"change_date": "",
"change_expiration_time": "",
"change_location": "",
"change_name": "",
"change_name_successfully": "",
"change_password": "",
"change_your_password": "",
"changed_visibility_successfully": "",
"check_all": "انتخاب همه",
"check_logs": "بررسی لاگ‌ها",
"check_all": "",
"check_logs": "",
"choose_matching_people_to_merge": "",
"city": "شهر",
"clear": "پاک کردن",
"clear_all": "پاک کردن همه",
"clear_message": "پاک کردن پیام",
"clear_value": "پاک کردن مقدار",
"close": "بستن",
"collapse_all": "جمع کردن همه",
"color_theme": "تم رنگ",
"comment_options": "گزینه‌های نظر",
"comments_are_disabled": "نظرات غیرفعال هستند",
"confirm": "تأیید",
"confirm_admin_password": "تأیید رمز عبور مدیر",
"city": "",
"clear": "",
"clear_all": "",
"clear_message": "",
"clear_value": "",
"close": "",
"collapse_all": "",
"color_theme": "",
"comment_options": "",
"comments_are_disabled": "",
"confirm": "",
"confirm_admin_password": "",
"confirm_delete_shared_link": "",
"confirm_password": "تأیید رمز عبور",
"contain": "شامل",
"context": "زمینه",
"continue": "ادامه",
"copied_image_to_clipboard": "تصویر به کلیپ‌بورد کپی شد.",
"copied_to_clipboard": "به کلیپ‌بورد کپی شد!",
"copy_error": "خطا در کپی",
"copy_file_path": "کپی مسیر فایل",
"copy_image": "کپی تصویر",
"copy_link": "کپی لینک",
"copy_link_to_clipboard": "کپی لینک به کلیپ‌بورد",
"copy_password": "کپی رمز عبور",
"copy_to_clipboard": "کپی به کلیپ‌بورد",
"country": "کشور",
"cover": "جلد",
"covers": "جلدها",
"create": "ایجاد",
"create_album": "ایجاد آلبوم",
"create_library": "ایجاد کتابخانه",
"create_link": "ایجاد لینک",
"create_link_to_share": "ایجاد لینک برای اشتراک‌گذاری",
"create_new_person": "ایجاد فرد جدید",
"create_new_user": "ایجاد کاربر جدید",
"create_user": "ایجاد کاربر",
"created": "ایجاد شد",
"current_device": "دستگاه فعلی",
"confirm_password": "",
"contain": "",
"context": "",
"continue": "",
"copied_image_to_clipboard": "",
"copied_to_clipboard": "",
"copy_error": "",
"copy_file_path": "",
"copy_image": "",
"copy_link": "",
"copy_link_to_clipboard": "",
"copy_password": "",
"copy_to_clipboard": "",
"country": "",
"cover": "",
"covers": "",
"create": "",
"create_album": "",
"create_library": "",
"create_link": "",
"create_link_to_share": "",
"create_new_person": "",
"create_new_user": "",
"create_user": "",
"created": "",
"current_device": "",
"custom_locale": "",
"custom_locale_description": "",
"dark": "تاریک",
"date_after": "تاریخ پس از",
"date_and_time": "تاریخ و زمان",
"date_before": "تاریخ قبل از",
"date_range": "بازه زمانی",
"day": "روز",
"deduplicate_all": "حذف تکراری‌ها به صورت کامل",
"dark": "",
"date_after": "",
"date_and_time": "",
"date_before": "",
"date_range": "",
"day": "",
"deduplicate_all": "",
"default_locale": "",
"default_locale_description": "",
"delete": "حذف",
"delete_album": "حذف آلبوم",
"delete": "",
"delete_album": "",
"delete_api_key_prompt": "",
"delete_duplicates_confirmation": "",
"delete_key": "حذف کلید",
"delete_library": "حذف کتابخانه",
"delete_link": "حذف لینک",
"delete_shared_link": "حذف لینک اشتراکی",
"delete_user": "حذف کاربر",
"deleted_shared_link": "لینک اشتراکی حذف شد",
"description": "توضیحات",
"details": "جزئیات",
"direction": "جهت",
"disabled": "غیرفعال",
"disallow_edits": "عدم اجازه ویرایش",
"discover": "کشف کردن",
"dismiss_all_errors": "رد تمام خطاها",
"dismiss_error": "رد خطا",
"display_options": "گزینه‌های نمایش",
"display_order": "ترتیب نمایش",
"display_original_photos": "نمایش عکس‌های اصلی",
"delete_key": "",
"delete_library": "",
"delete_link": "",
"delete_shared_link": "",
"delete_user": "",
"deleted_shared_link": "",
"description": "",
"details": "",
"direction": "",
"disabled": "",
"disallow_edits": "",
"discover": "",
"dismiss_all_errors": "",
"dismiss_error": "",
"display_options": "",
"display_order": "",
"display_original_photos": "",
"display_original_photos_setting_description": "",
"done": "انجام شد",
"download": "دانلود",
"download_settings": "تنظیمات دانلود",
"download_settings_description": "مدیریت تنظیمات مرتبط با دانلود محتوا",
"downloading": "در حال دانلود",
"duplicates": "تکراری‌ها",
"done": "",
"download": "",
"download_settings": "",
"download_settings_description": "",
"downloading": "",
"duplicates": "",
"duplicates_description": "",
"duration": "مدت زمان",
"edit_album": "ویرایش آلبوم",
"edit_avatar": "ویرایش آواتار",
"edit_date": "ویرایش تاریخ",
"edit_date_and_time": "ویرایش تاریخ و زمان",
"edit_exclusion_pattern": "ویرایش الگوی استثناء",
"edit_faces": "ویرایش چهره‌ها",
"duration": "",
"edit_album": "",
"edit_avatar": "",
"edit_date": "",
"edit_date_and_time": "",
"edit_exclusion_pattern": "",
"edit_faces": "",
"edit_import_path": "",
"edit_import_paths": "",
"edit_key": "ویرایش کلید",
"edit_link": "ویرایش لینک",
"edit_location": "ویرایش مکان",
"edit_name": "ویرایش نام",
"edit_people": "ویرایش افراد",
"edit_title": "ویرایش عنوان",
"edit_user": "ویرایش کاربر",
"edited": "ویرایش شد",
"editor": "ویرایشگر",
"email": "ایمیل",
"empty_trash": "خالی کردن سطل زباله",
"end_date": "تاریخ پایان",
"error": "خطا",
"error_loading_image": "خطا در بارگذاری تصویر",
"edit_key": "",
"edit_link": "",
"edit_location": "",
"edit_name": "",
"edit_people": "",
"edit_title": "",
"edit_user": "",
"edited": "",
"editor": "",
"email": "",
"empty_trash": "",
"end_date": "",
"error": "",
"error_loading_image": "",
"errors": {
"exclusion_pattern_already_exists": "",
"import_path_already_exists": "",
@@ -530,400 +530,400 @@
"unable_to_update_timeline_display_status": "",
"unable_to_update_user": ""
},
"exit_slideshow": "خروج از نمایش اسلاید",
"expand_all": "باز کردن همه",
"expire_after": "منقضی شدن بعد از",
"expired": "منقضی شده",
"explore": "کاوش کردن",
"export": "صادر کردن",
"export_as_json": "صادر کردن به‌صورت JSON",
"extension": "پسوند",
"external": "خارجی",
"external_libraries": "کتابخانه‌های خارجی",
"favorite": "علاقه‌مندی",
"exit_slideshow": "",
"expand_all": "",
"expire_after": "",
"expired": "",
"explore": "",
"export": "",
"export_as_json": "",
"extension": "",
"external": "",
"external_libraries": "",
"favorite": "",
"favorite_or_unfavorite_photo": "",
"favorites": "علاقه‌مندی‌ها",
"favorites": "",
"feature_photo_updated": "",
"file_name": "نام فایل",
"file_name_or_extension": "نام فایل یا پسوند",
"filename": "نام فایل",
"filetype": "نوع فایل",
"filter_people": "فیلتر افراد",
"file_name": "",
"file_name_or_extension": "",
"filename": "",
"filetype": "",
"filter_people": "",
"find_them_fast": "",
"fix_incorrect_match": "رفع تطابق نادرست",
"forward": "جلو",
"general": "عمومی",
"get_help": "دریافت کمک",
"getting_started": "شروع به کار",
"go_back": "بازگشت",
"go_to_search": "رفتن به جستجو",
"group_albums_by": "گروه‌بندی آلبوم‌ها براساس...",
"has_quota": "دارای سهمیه",
"hide_gallery": "پنهان کردن گالری",
"hide_password": "پنهان کردن رمز عبور",
"hide_person": "پنهان کردن فرد",
"host": "میزبان",
"hour": "ساعت",
"image": "تصویر",
"immich_logo": "لوگوی Immich",
"immich_web_interface": "رابط وب Immich",
"import_from_json": "وارد کردن از JSON",
"import_path": "مسیر وارد کردن",
"fix_incorrect_match": "",
"forward": "",
"general": "",
"get_help": "",
"getting_started": "",
"go_back": "",
"go_to_search": "",
"group_albums_by": "",
"has_quota": "",
"hide_gallery": "",
"hide_password": "",
"hide_person": "",
"host": "",
"hour": "",
"image": "",
"immich_logo": "",
"immich_web_interface": "",
"import_from_json": "",
"import_path": "",
"in_albums": "",
"in_archive": "در بایگانی",
"include_archived": "شامل بایگانی شده‌ها",
"include_shared_albums": "شامل آلبوم‌های اشتراکی",
"in_archive": "",
"include_archived": "",
"include_shared_albums": "",
"include_shared_partner_assets": "",
"individual_share": "اشتراک فردی",
"info": "اطلاعات",
"individual_share": "",
"info": "",
"interval": {
"day_at_onepm": "",
"hours": "",
"night_at_midnight": "",
"night_at_twoam": ""
},
"invite_people": "دعوت افراد",
"invite_to_album": "دعوت به آلبوم",
"jobs": "وظایف",
"keep": "نگه داشتن",
"keep_all": "نگه داشتن همه",
"keyboard_shortcuts": "میانبرهای صفحه‌کلید",
"language": "زبان",
"language_setting_description": "انتخاب زبان دلخواه شما",
"last_seen": "آخرین مشاهده",
"leave": "ترک کردن",
"let_others_respond": "اجازه به دیگران برای پاسخ‌گویی",
"level": "سطح",
"library": "کتابخانه",
"library_options": "گزینه‌های کتابخانه",
"light": "روشن",
"link_options": "گزینه‌های لینک",
"link_to_oauth": "اتصال به OAuth",
"linked_oauth_account": "حساب OAuth متصل شده",
"list": "لیست",
"loading": "در حال بارگذاری",
"loading_search_results_failed": "بارگذاری نتایج جستجو ناموفق بود",
"log_out": "خروج از سیستم",
"log_out_all_devices": "خروج از همه دستگاه‌ها",
"login_has_been_disabled": "ورود غیرفعال شده است.",
"look": "نگاه کردن",
"loop_videos": "پخش مداوم ویدئوها",
"invite_people": "",
"invite_to_album": "",
"jobs": "",
"keep": "",
"keep_all": "",
"keyboard_shortcuts": "",
"language": "",
"language_setting_description": "",
"last_seen": "",
"leave": "",
"let_others_respond": "",
"level": "",
"library": "",
"library_options": "",
"light": "",
"link_options": "",
"link_to_oauth": "",
"linked_oauth_account": "",
"list": "",
"loading": "",
"loading_search_results_failed": "",
"log_out": "",
"log_out_all_devices": "",
"login_has_been_disabled": "",
"look": "",
"loop_videos": "",
"loop_videos_description": "",
"make": "ساختن",
"manage_shared_links": "مدیریت لینک‌های اشتراکی",
"make": "",
"manage_shared_links": "",
"manage_sharing_with_partners": "",
"manage_the_app_settings": "مدیریت تنظیمات برنامه",
"manage_your_account": "مدیریت حساب کاربری شما",
"manage_your_api_keys": "مدیریت کلیدهای API شما",
"manage_your_devices": "مدیریت دستگاه‌های متصل",
"manage_your_oauth_connection": "مدیریت اتصال OAuth شما",
"map": "نقشه",
"manage_the_app_settings": "",
"manage_your_account": "",
"manage_your_api_keys": "",
"manage_your_devices": "",
"manage_your_oauth_connection": "",
"map": "",
"map_marker_with_image": "",
"map_settings": "تنظیمات نقشه",
"matches": "تطابق‌ها",
"media_type": "نوع رسانه",
"memories": "خاطرات",
"map_settings": "",
"matches": "",
"media_type": "",
"memories": "",
"memories_setting_description": "",
"memory": "خاطره",
"menu": "منو",
"merge": "ادغام",
"merge_people": "ادغام افراد",
"memory": "",
"menu": "",
"merge": "",
"merge_people": "",
"merge_people_limit": "",
"merge_people_prompt": "",
"merge_people_successfully": "ادغام افراد با موفقیت انجام شد",
"minimize": "کوچک کردن",
"minute": "دقیقه",
"missing": "گمشده",
"model": "مدل",
"month": "ماه",
"more": "بیشتر",
"moved_to_trash": "به سطل زباله منتقل شد",
"my_albums": "آلبوم‌های من",
"name": "نام",
"name_or_nickname": "نام یا لقب",
"never": "هرگز",
"new_api_key": "کلید API جدید",
"new_password": "رمز عبور جدید",
"new_person": "فرد جدید",
"new_user_created": "کاربر جدید ایجاد شد",
"newest_first": "جدیدترین ابتدا",
"next": "بعدی",
"next_memory": "خاطره بعدی",
"no": "خیر",
"merge_people_successfully": "",
"minimize": "",
"minute": "",
"missing": "",
"model": "",
"month": "",
"more": "",
"moved_to_trash": "",
"my_albums": "",
"name": "",
"name_or_nickname": "",
"never": "",
"new_api_key": "",
"new_password": "",
"new_person": "",
"new_user_created": "",
"newest_first": "",
"next": "",
"next_memory": "",
"no": "",
"no_albums_message": "",
"no_archived_assets_message": "",
"no_assets_message": "",
"no_duplicates_found": "هیچ تکراری یافت نشد.",
"no_exif_info_available": "اطلاعات EXIF موجود نیست",
"no_duplicates_found": "",
"no_exif_info_available": "",
"no_explore_results_message": "",
"no_favorites_message": "",
"no_libraries_message": "",
"no_name": "بدون نام",
"no_places": "مکانی یافت نشد",
"no_results": "نتیجه‌ای یافت نشد",
"no_name": "",
"no_places": "",
"no_results": "",
"no_shared_albums_message": "",
"not_in_any_album": "در هیچ آلبومی نیست",
"not_in_any_album": "",
"note_apply_storage_label_to_previously_uploaded assets": "",
"note_unlimited_quota": "",
"notes": "یادداشت‌ها",
"notification_toggle_setting_description": "اعلان‌های ایمیلی را فعال کنید",
"notifications": "اعلان‌ها",
"notifications_setting_description": "مدیریت اعلان‌ها",
"oauth": "OAuth",
"offline": "آفلاین",
"offline_paths": "مسیرهای آفلاین",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "",
"notifications_setting_description": "",
"oauth": "",
"offline": "",
"offline_paths": "",
"offline_paths_description": "",
"ok": "تأیید",
"oldest_first": "قدیمی‌ترین ابتدا",
"online": "آنلاین",
"only_favorites": "فقط علاقه‌مندی‌ها",
"open_the_search_filters": "باز کردن فیلترهای جستجو",
"options": "گزینه‌ها",
"organize_your_library": "کتابخانه خود را سازماندهی کنید",
"other": "دیگر",
"other_devices": "دستگاه‌های دیگر",
"other_variables": "متغیرهای دیگر",
"owned": "مالکیت",
"owner": "مالک",
"partner": "شریک",
"partner_can_access": "{partner} می‌تواند دسترسی داشته باشد",
"ok": "",
"oldest_first": "",
"online": "",
"only_favorites": "",
"open_the_search_filters": "",
"options": "",
"organize_your_library": "",
"other": "",
"other_devices": "",
"other_variables": "",
"owned": "",
"owner": "",
"partner": "",
"partner_can_access": "",
"partner_can_access_assets": "",
"partner_can_access_location": "مکان‌هایی که عکس‌های شما گرفته شده‌اند",
"partner_sharing": "اشتراک‌گذاری با شریک",
"partners": "شرکا",
"password": "رمز عبور",
"password_does_not_match": "رمز عبور مطابقت ندارد",
"password_required": "رمز عبور مورد نیاز است",
"password_reset_success": "بازنشانی رمز عبور موفقیت‌آمیز بود",
"partner_can_access_location": "",
"partner_sharing": "",
"partners": "",
"password": "",
"password_does_not_match": "",
"password_required": "",
"password_reset_success": "",
"past_durations": {
"days": "",
"hours": "",
"years": ""
},
"path": "مسیر",
"pattern": "الگو",
"pause": "توقف",
"pause_memories": "توقف خاطرات",
"paused": "متوقف شده",
"pending": "در انتظار",
"people": "افراد",
"path": "",
"pattern": "",
"pause": "",
"pause_memories": "",
"paused": "",
"pending": "",
"people": "",
"people_sidebar_description": "",
"permanent_deletion_warning": "هشدار حذف دائمی",
"permanent_deletion_warning_setting_description": "نمایش هشدار هنگام حذف دائمی محتواها",
"permanently_delete": "حذف دائمی",
"permanently_deleted_asset": "محتوای حذف شده دائمی",
"person": "فرد",
"photos": "عکس‌ها",
"permanent_deletion_warning": "",
"permanent_deletion_warning_setting_description": "",
"permanently_delete": "",
"permanently_deleted_asset": "",
"person": "",
"photos": "",
"photos_count": "",
"photos_from_previous_years": "عکس‌های سال‌های گذشته",
"pick_a_location": "یک مکان انتخاب کنید",
"place": "مکان",
"places": "مکان‌ها",
"play": "پخش",
"play_memories": "پخش خاطرات",
"play_motion_photo": "پخش عکس متحرک",
"play_or_pause_video": "پخش یا توقف ویدیو",
"port": "پورت",
"preset": "پیش‌فرض",
"preview": "پیش‌نمایش",
"previous": "قبلی",
"previous_memory": "خاطره قبلی",
"previous_or_next_photo": "عکس قبلی یا بعدی",
"primary": "اصلی",
"profile_picture_set": "تصویر پروفایل تنظیم شد.",
"public_share": "اشتراک عمومی",
"reaction_options": "گزینه‌های واکنش",
"read_changelog": "مطالعه تغییرات نسخه",
"recent": "اخیر",
"recent_searches": "جستجوهای اخیر",
"refresh": "تازه سازی",
"refreshed": "تازه سازی شد",
"photos_from_previous_years": "",
"pick_a_location": "",
"place": "",
"places": "",
"play": "",
"play_memories": "",
"play_motion_photo": "",
"play_or_pause_video": "",
"port": "",
"preset": "",
"preview": "",
"previous": "",
"previous_memory": "",
"previous_or_next_photo": "",
"primary": "",
"profile_picture_set": "",
"public_share": "",
"reaction_options": "",
"read_changelog": "",
"recent": "",
"recent_searches": "",
"refresh": "",
"refreshed": "",
"refreshes_every_file": "",
"remove": "حذف",
"remove_deleted_assets": "حذف محتواهای حذف‌شده",
"remove_from_album": "حذف از آلبوم",
"remove_from_favorites": "حذف از علاقه‌مندی‌ها",
"remove": "",
"remove_deleted_assets": "",
"remove_from_album": "",
"remove_from_favorites": "",
"remove_from_shared_link": "",
"removed_api_key": "",
"rename": "تغییر نام",
"repair": "تعمیر",
"rename": "",
"repair": "",
"repair_no_results_message": "",
"replace_with_upload": "جایگزینی با آپلود",
"replace_with_upload": "",
"require_password": "",
"require_user_to_change_password_on_first_login": "",
"reset": "بازنشانی",
"reset_password": "بازنشانی رمز عبور",
"reset": "",
"reset_password": "",
"reset_people_visibility": "",
"resolved_all_duplicates": "",
"restore": "بازیابی",
"restore_all": "بازیابی همه",
"restore_user": "بازیابی کاربر",
"resume": "ادامه",
"restore": "",
"restore_all": "",
"restore_user": "",
"resume": "",
"retry_upload": "",
"review_duplicates": "بررسی تکراری‌ها",
"role": "نقش",
"save": "ذخیره",
"review_duplicates": "",
"role": "",
"save": "",
"saved_api_key": "",
"saved_profile": "پروفایل ذخیره شد",
"saved_settings": "تنظیمات ذخیره شد",
"say_something": "چیزی بگویید",
"scan_all_libraries": "اسکن همه کتابخانه‌ها",
"scan_settings": "تنظیمات اسکن",
"saved_profile": "",
"saved_settings": "",
"say_something": "",
"scan_all_libraries": "",
"scan_settings": "",
"scanning_for_album": "",
"search": "جستجو",
"search_albums": "جستجوی آلبوم‌ها",
"search_by_context": "جستجو براساس زمینه",
"search_camera_make": "جستجوی برند دوربین...",
"search_camera_model": "جستجوی مدل دوربین...",
"search_city": "جستجوی شهر...",
"search_country": "جستجوی کشور...",
"search_for_existing_person": "جستجوی فرد موجود",
"search_people": "جستجوی افراد",
"search_places": "جستجوی مکان‌ها",
"search_state": "جستجوی ایالت...",
"search_timezone": "جستجوی منطقه زمانی...",
"search_type": "نوع جستجو",
"search": "",
"search_albums": "",
"search_by_context": "",
"search_camera_make": "",
"search_camera_model": "",
"search_city": "",
"search_country": "",
"search_for_existing_person": "",
"search_people": "",
"search_places": "",
"search_state": "",
"search_timezone": "",
"search_type": "",
"search_your_photos": "",
"searching_locales": "",
"second": "ثانیه",
"select_album_cover": "انتخاب جلد آلبوم",
"select_all": "انتخاب همه",
"select_avatar_color": "انتخاب رنگ آواتار",
"select_face": "انتخاب چهره",
"select_featured_photo": "انتخاب عکس ویژه",
"select_keep_all": "انتخاب نگهداری همه",
"select_library_owner": "انتخاب مالک کتابخانه",
"select_new_face": "انتخاب چهره جدید",
"select_photos": "انتخاب عکس‌ها",
"second": "",
"select_album_cover": "",
"select_all": "",
"select_avatar_color": "",
"select_face": "",
"select_featured_photo": "",
"select_keep_all": "",
"select_library_owner": "",
"select_new_face": "",
"select_photos": "",
"select_trash_all": "",
"selected": "انتخاب شده",
"send_message": "ارسال پیام",
"send_welcome_email": "ارسال ایمیل خوش‌آمدگویی",
"server_stats": "آمار سرور",
"set": "تنظیم",
"selected": "",
"send_message": "",
"send_welcome_email": "",
"server_stats": "",
"set": "",
"set_as_album_cover": "",
"set_as_profile_picture": "",
"set_date_of_birth": "تنظیم تاریخ تولد",
"set_profile_picture": "تنظیم تصویر پروفایل",
"set_date_of_birth": "",
"set_profile_picture": "",
"set_slideshow_to_fullscreen": "",
"settings": "تنظیمات",
"settings_saved": "تنظیمات ذخیره شد",
"share": "اشتراک‌گذاری",
"shared": "مشترک",
"shared_by": "مشترک توسط",
"settings": "",
"settings_saved": "",
"share": "",
"shared": "",
"shared_by": "",
"shared_by_you": "",
"shared_from_partner": "عکس‌ها از {partner}",
"shared_links": "لینک‌های اشتراکی",
"shared_from_partner": "",
"shared_links": "",
"shared_photos_and_videos_count": "",
"shared_with_partner": "مشترک با {partner}",
"sharing": "اشتراک‌گذاری",
"shared_with_partner": "",
"sharing": "",
"sharing_sidebar_description": "",
"show_album_options": "نمایش گزینه‌های آلبوم",
"show_album_options": "",
"show_and_hide_people": "",
"show_file_location": "نمایش مسیر فایل",
"show_gallery": "نمایش گالری",
"show_hidden_people": "نمایش افراد پنهان",
"show_file_location": "",
"show_gallery": "",
"show_hidden_people": "",
"show_in_timeline": "",
"show_in_timeline_setting_description": "",
"show_keyboard_shortcuts": "",
"show_metadata": "نمایش اطلاعات متا",
"show_metadata": "",
"show_or_hide_info": "",
"show_password": "نمایش رمز عبور",
"show_password": "",
"show_person_options": "",
"show_progress_bar": "نمایش نوار پیشرفت",
"show_search_options": "نمایش گزینه‌های جستجو",
"shuffle": "تصادفی",
"sign_out": "خروج",
"sign_up": "ثبت‌نام",
"size": "اندازه",
"skip_to_content": "رفتن به محتوا",
"slideshow": "نمایش اسلاید",
"slideshow_settings": "تنظیمات نمایش اسلاید",
"show_progress_bar": "",
"show_search_options": "",
"shuffle": "",
"sign_out": "",
"sign_up": "",
"size": "",
"skip_to_content": "",
"slideshow": "",
"slideshow_settings": "",
"sort_albums_by": "",
"stack": "پشته",
"stack": "",
"stack_selected_photos": "",
"stacktrace": "",
"start": "شروع",
"start_date": "تاریخ شروع",
"state": "ایالت",
"status": "وضعیت",
"stop_motion_photo": "توقف عکس متحرک",
"start": "",
"start_date": "",
"state": "",
"status": "",
"stop_motion_photo": "",
"stop_photo_sharing": "",
"stop_photo_sharing_description": "",
"stop_sharing_photos_with_user": "",
"storage": "فضای ذخیره‌سازی",
"storage_label": "برچسب فضای ذخیره‌سازی",
"storage": "",
"storage_label": "",
"storage_usage": "",
"submit": "ارسال",
"suggestions": "پیشنهادات",
"submit": "",
"suggestions": "",
"sunrise_on_the_beach": "",
"swap_merge_direction": "تغییر جهت ادغام",
"sync": "همگام‌سازی",
"template": "الگو",
"theme": "تم",
"theme_selection": "انتخاب تم",
"swap_merge_direction": "",
"sync": "",
"template": "",
"theme": "",
"theme_selection": "",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "منطقه زمانی",
"to_archive": "بایگانی",
"to_favorite": "به علاقه‌مندی‌ها",
"timezone": "",
"to_archive": "",
"to_favorite": "",
"to_trash": "",
"toggle_settings": "تغییر تنظیمات",
"toggle_theme": "تغییر تم تاریک",
"total_usage": "استفاده کلی",
"trash": "سطل زباله",
"toggle_settings": "",
"toggle_theme": "",
"total_usage": "",
"trash": "",
"trash_all": "",
"trash_count": "",
"trash_no_results_message": "",
"trashed_items_will_be_permanently_deleted_after": "",
"type": "نوع",
"type": "",
"unarchive": "",
"unfavorite": "حذف از علاقه‌مندی‌ها",
"unhide_person": "آشکار کردن فرد",
"unknown": "ناشناخته",
"unknown_year": "سال نامشخص",
"unlimited": "نامحدود",
"unlink_oauth": "لغو اتصال OAuth",
"unfavorite": "",
"unhide_person": "",
"unknown": "",
"unknown_year": "",
"unlimited": "",
"unlink_oauth": "",
"unlinked_oauth_account": "",
"unnamed_album": "آلبوم بدون نام",
"unnamed_share": "اشتراک بدون نام",
"unselect_all": "لغو انتخاب همه",
"unnamed_album": "",
"unnamed_share": "",
"unselect_all": "",
"unstack": "",
"untracked_files": "",
"untracked_files_decription": "",
"up_next": "مورد بعدی",
"up_next": "",
"updated_password": "",
"upload": "آپلود",
"upload_concurrency": "تعداد آپلود همزمان",
"url": "آدرس",
"usage": "استفاده",
"user": "کاربر",
"user_id": "شناسه کاربر",
"user_usage_detail": "جزئیات استفاده کاربر",
"username": "نام کاربری",
"users": "کاربران",
"utilities": "ابزارها",
"validate": "اعتبارسنجی",
"variables": "متغیرها",
"version": "نسخه",
"upload": "",
"upload_concurrency": "",
"url": "",
"usage": "",
"user": "",
"user_id": "",
"user_usage_detail": "",
"username": "",
"users": "",
"utilities": "",
"validate": "",
"variables": "",
"version": "",
"version_announcement_message": "",
"video": "ویدیو",
"video": "",
"video_hover_setting": "",
"video_hover_setting_description": "",
"videos": "ویدیوها",
"videos": "",
"videos_count": "",
"view": "مشاهده",
"view_all": "مشاهده همه",
"view_all_users": "مشاهده همه کاربران",
"view_links": "مشاهده لینک‌ها",
"view_next_asset": "مشاهده محتوای بعدی",
"view_previous_asset": "مشاهده محتوای قبلی",
"waiting": "در انتظار",
"week": "هفته",
"welcome": "خوش آمدید",
"view": "",
"view_all": "",
"view_all_users": "",
"view_links": "",
"view_next_asset": "",
"view_previous_asset": "",
"waiting": "",
"week": "",
"welcome": "",
"welcome_to_immich": "",
"year": "سال",
"yes": "بله",
"year": "",
"yes": "",
"you_dont_have_any_shared_links": "",
"zoom_image": "بزرگنمایی تصویر"
}

View File

@@ -10,14 +10,14 @@
"activity_changed": "L'attività è {enabled, select, true {abilitata} other {disabilitata}}",
"add": "Aggiungi",
"add_a_description": "Aggiungi una descrizione",
"add_a_location": "Aggiungi una posizione",
"add_a_location": "Aggiungi un luogo",
"add_a_name": "Aggiungi un nome",
"add_a_title": "Aggiungi un titolo",
"add_exclusion_pattern": "Aggiungi un pattern di esclusione",
"add_import_path": "Aggiungi un percorso di importazione",
"add_location": "Aggiungi posizione",
"add_more_users": "Aggiungi altri utenti",
"add_partner": "Aggiungi partner",
"add_partner": "Aggiungi un partner",
"add_path": "Aggiungi percorso",
"add_photos": "Aggiungi foto",
"add_to": "Aggiungi a...",
@@ -374,11 +374,11 @@
"album_name": "Nome Album",
"album_options": "Impostazioni Album",
"album_remove_user": "Rimuovi l'utente?",
"album_remove_user_confirmation": "Sicuro di voler rimuovere l'utente {user}?",
"album_remove_user_confirmation": "Sicuro di voler cancellare l'utente {user}?",
"album_share_no_users": "Sembra che tu abbia condiviso questo album con tutti gli utenti oppure non hai nessun utente con cui condividere.",
"album_updated": "Album aggiornato",
"album_updated_setting_description": "Ricevi una notifica email quando un album condiviso ha nuovi media",
"album_user_left": "{album} abbandonato",
"album_updated_setting_description": "Ricevi una notifica email quando un album condiviso ha nuovi asset",
"album_user_left": "Abbandona {album}",
"album_user_removed": "Utente {user} rimosso",
"album_with_link_access": "Permetti a chiunque possieda il link di visualizzare le foto e le persone dell'album.",
"albums": "Album",
@@ -391,10 +391,10 @@
"allow_edits": "Permetti Modifiche",
"allow_public_user_to_download": "Permetti agli utenti pubblici di scaricare",
"allow_public_user_to_upload": "Permetti agli utenti pubblici di caricare",
"anti_clockwise": "Senso anti-orario",
"anti_clockwise": "Senso Anti-Orario",
"api_key": "Chiave API",
"api_key_description": "Il valore verrà mostrato solo una volta. Assicurati di copiarlo prima di chiudere la finestra.",
"api_key_empty": "Il nome della chiave API non può essere vuoto",
"api_key_description": "Il campo verrà mostrato solo una volta. Abbi cura di copiarlo prima di chiudere la finestra.",
"api_key_empty": "Il Nome dell'API Key non può essere vuoto",
"api_keys": "Chiavi API",
"app_settings": "Impostazioni Applicazione",
"appears_in": "Compare in",
@@ -407,14 +407,14 @@
"are_you_sure_to_do_this": "Sei sicuro di voler procedere?",
"asset_added_to_album": "Aggiunto all'album",
"asset_adding_to_album": "In aggiunta all'album...",
"asset_description_updated": "La descrizione del media è stata aggiornata",
"asset_description_updated": "La descrizione del media non è stata aggiornata",
"asset_filename_is_offline": "Il media {filename} è offline",
"asset_has_unassigned_faces": "Il media ha dei volti non categorizzati",
"asset_hashing": "Hashing in corso ...",
"asset_offline": "Risorsa Offline",
"asset_offline_description": "Questo media non è stato trovato nel disco. Contatta il tuo amministratore di Immich per assistenza.",
"asset_skipped": "Saltato",
"asset_skipped_in_trash": "Nel cestino",
"asset_skipped_in_trash": "In cestino",
"asset_uploaded": "Caricato",
"asset_uploading": "Caricamento...",
"assets": "Risorse",
@@ -434,7 +434,7 @@
"back_close_deselect": "Indietro, chiudi o deseleziona",
"backward": "Indietro",
"birthdate_saved": "Data di nascita salvata con successo",
"birthdate_set_description": "La data di nascita è usata per calcolare l'età di questa persona al momento dello scatto della foto.",
"birthdate_set_description": "La data di nascita è usata per calcolare l'età di questa persona nel momento dello scatto della foto.",
"blurred_background": "Sfondo sfocato",
"bugs_and_feature_requests": "Bug & Richieste di nuove funzionalità",
"build": "Compilazione",
@@ -442,7 +442,7 @@
"bulk_delete_duplicates_confirmation": "Sei sicuro di voler cancellare {count, plural, one {# asset duplicato} other {# assets duplicati}}? Questa operazione manterrà l'asset più pesante di ogni gruppo e cancellerà permanentemente tutti gli altri duplicati. Non puoi annullare questa operazione!",
"bulk_keep_duplicates_confirmation": "Sei sicuro di voler tenere {count, plural, one {# asset duplicato} other {# assets duplicati}}? Questa operazione risolverà tutti i gruppi duplicati senza cancellare nulla.",
"bulk_trash_duplicates_confirmation": "Sei davvero sicuro di voler cancellare {count, plural, one {# asset duplicato} other {# assets duplicati}}? Questa operazione manterrà l'asset più pesante di ogni gruppo e cancellerà permanentemente tutti gli altri duplicati.",
"buy": "Acquista Immich",
"buy": "Acquistare Immich",
"camera": "Fotocamera",
"camera_brand": "Marca fotocamera",
"camera_model": "Modello fotocamera",

View File

@@ -1,6 +1,6 @@
{
"about": "เกี่ยวกับ",
"account": "บัญชีผู้ใช้",
"account": "บัญชี",
"account_settings": "การตั้งค่าบัญชี",
"acknowledge": "รับทราบ",
"action": "การดำเนินการ",
@@ -155,7 +155,7 @@
"migration_job_description": "ย้ายภาพตัวอย่างสื่อและใบหน้าไปยังโครงสร้างโฟลเดอร์ล่าสุด",
"no_paths_added": "ไม่ได้เพิ่มพาธ",
"no_pattern_added": "ไม่ได้เพิ่มรูปแบบ",
"note_apply_storage_label_previous_assets": "หากต้องการใช้ Storage Label กับไฟล์ที่อัโหลดก่อนหน้านี้ ให้รันคำสั่งนี้",
"note_apply_storage_label_previous_assets": "หมายเหตุ: หากจะแปะฉลากจัดเก็บใส่สื่อที่อัโหลดก่อนหน้านี้ ให้",
"note_cannot_be_changed_later": "หมายเหตุ: ไม่สามารถเปลี่ยนภายหลังได้!",
"note_unlimited_quota": "หมายเหตุ: ใส่เลข 0 สําหรับโควต้าไม่จํากัด",
"notification_email_from_address": "จากที่อยู่",
@@ -193,8 +193,8 @@
"oauth_settings_description": "จัดการการตั้งค่าล็อกอินผ่าน OAuth",
"oauth_settings_more_details": "สำหรับรายละเอียดเพิ่มเติม ให้อ้างถึง<link>เอกสาร</link>",
"oauth_signing_algorithm": "อัลกอริทึมการลงนาม",
"oauth_storage_label_claim": "สิทธิ์ที่ใช้อ้างถึงป้ายกำกับการจัดเก็บ",
"oauth_storage_label_claim_description": "ตั้งป้ายกำกับการจัดเก็บของผู้ใช้งานตามสิทธิ์ที่ใช้อ้างถึงโดยอัตโนมัติ",
"oauth_storage_label_claim": "สิทธิ์ที่ใช้อ้างถึงฉลากการจัดเก็บ",
"oauth_storage_label_claim_description": "ตั้งฉลากการจัดเก็บของผู้ใช้งานตามสิทธิ์ที่ใช้อ้างถึงโดยอัตโนมัติ",
"oauth_storage_quota_claim": "สิทธิ์ที่ใช้อ้างถึงโควต้าพื้นที่จัดเก็บ",
"oauth_storage_quota_claim_description": "ตั้งโควต้าพื้นที่จัดเก็บของผู้ใช้งานตามสิทธิ์ที่ใช้อ้างถึงโดยอัตโนมัติ",
"oauth_storage_quota_default": "โควต้าพื้นที่เก็บข้อมูลเริ่มต้น (GiB)",
@@ -235,7 +235,7 @@
"storage_template_hash_verification_enabled": "ตรวจสอบ hash ไม่ผ่าน",
"storage_template_hash_verification_enabled_description": "เปิดใช้งานการตรวจสอบ hash ห้ามปิดใช้งานเว้นแต่คุณจะเข้าใจผลกระทบ",
"storage_template_migration": "การย้ายเทมเพลตที่เก็บข้อมูล",
"storage_template_migration_description": "ใช้<link>{template}</link>ปัจจุบันกับสื่อที่อัโหลดก่อนหน้านี้",
"storage_template_migration_description": "ใช้<link>{template}</link>ปัจจุบันกับสื่อที่อัโหลดก่อนหน้านี้",
"storage_template_migration_job": "",
"storage_template_path_length": "ขีดจำกัดของความยาวพาธโดยประมาณ: <b>{length, number}</b>/{limit, number}",
"storage_template_settings": "เทมเพลตการจัดเก็บข้อมูล",
@@ -313,9 +313,6 @@
"user_delete_delay_settings_description": "จํานวนวันหลังจากที่เอาออกเพื่อลบบัญชีผู้ใช้และสื่อถาวร งานลบบัญชีผู้ใช้ทํางานทุกเที่ยงคืนเพื่อตรวจสอบผู้ใช้ที่พร้อมที่จะถูกลบข้อมูลแล้ว การตั้งค่าครั้งนี้จะมีผลครั้งต่อไป",
"user_delete_immediately": "บัญชีและสื่อของ <b>{user}</b> จะอยู่ในคิวสำหรับการลบถาวร <b>โดยทันที</b>",
"user_settings": "การตั้งค่าผู้ใช้",
"user_management": "การจัดการผู้ใช้",
"user_password_has_been_reset": "รหัสผ่านของผู้ใช้ <b>{user}</b> ถูกตั้งค่าใหม่แล้ว",
"user_password_reset_description": "รหัสผ่านของผู้ใช้จะถูกตั้งค่าใหม่และส่งไปยังอีเมลที่ลงทะเบียน",
"user_settings_description": "จัดการการตั้งค่าผู้ใช้",
"version_check_enabled_description": "เช็ค GitHub เป็นระยะ ๆ เพื่อตรวจสอบรุ่นใหม่",
"version_check_settings": "ตรวจสอบรุ่น",
@@ -350,14 +347,12 @@
"allow_public_user_to_download": "อนุญาตให้ผู้ใช้สาธารณะดาวน์โหลดได้",
"allow_public_user_to_upload": "อนุญาตให้ผู้ใช้สาธารณะอัปโหลดได้",
"anti_clockwise": "ทวนเข็มนาฬิกา",
"api_key": "API key",
"api_keys": "API Keys",
"api_key": "กุญแจ API",
"api_keys": "กุญแจ API",
"app_settings": "การตั้งค่าแอป",
"appears_in": "อยู่ใน",
"archive": "เก็บถาวร",
"archive_or_unarchive_photo": "เก็บ/ไม่เก็บภาพถาวร",
"archive_size": "ขนาดเก็บถาวร",
"archive_size_description": "ตั้งค่าขนาดสูงสุดสำหรับการดาวน์โหลด (GiB)",
"are_these_the_same_person": "เป็นคนเดียวกันหรือไม่?",
"are_you_sure_to_do_this": "คุณแน่ใจว่าต้องการทำสิ่งนี้หรือไม่?",
"asset_added_to_album": "เพิ่มไปยังอัลบั้มแล้ว",
@@ -385,19 +380,19 @@
"change_name": "เปลี่ยนชื่อ",
"change_name_successfully": "เปลี่ยนชื่อเรียบร้อยแล้ว",
"change_password": "เปลี่ยนรหัสผ่าน",
"change_your_password": "เปลี่ยนรหัสผ่านของคุณ",
"changed_visibility_successfully": "เปลี่ยนการมองเห็นเรียบร้อยแล้ว",
"check_logs": "ตรวจสอบบันทึก",
"change_your_password": "",
"changed_visibility_successfully": "",
"check_logs": "",
"city": "เมือง",
"clear": "ล้าง",
"clear_all": "ล้างทั้งหมด",
"clear_message": "ล้างข้อความ",
"clear_value": "ล้างค่า",
"clear_all": "",
"clear_message": "",
"clear_value": "",
"close": "ปิด",
"collapse": "ย่อ",
"collapse_all": "ย่อทั้งหมด",
"color": "สี",
"color_theme": "สีธีม",
"color_theme": "",
"comment_deleted": "ลบความคิดเห็นแล้ว",
"comment_options": "",
"comments_and_likes": "ความคิดเห็นและการถูกใจ",
@@ -455,17 +450,13 @@
"discover": "ค้นพบ",
"dismiss_all_errors": "ปฏิเสธข้อผิดพลาดทั้งหมด",
"dismiss_error": "ปฏิเสธข้อผิดพลาด",
"display_options": "ตัวเลือกการแสดง",
"display_order": "ลำดับการแสดงผล",
"display_original_photos": "แสดงภาพต้นฉบับ",
"display_original_photos_setting_description": "การตั้งค่าแสดงผลรูปภาพต้นฉบับ เมื่อเปิดรูปภาพ การตั้งค่านี้อาจจะทำให้การแสดงภาพได้ช้าลง",
"done": "ดำเนินการสำเร็จ",
"display_options": "",
"display_order": "",
"display_original_photos": "",
"display_original_photos_setting_description": "เมื่อดูสื่อให้แสดงภาพต้นฉบับแทนภาพตัวอย่างเมื่อไฟล์สื่อเปิดได้บนเว็บ อาจทําให้แสดง ภาพได้ช้าลง",
"done": "เร็จ",
"download": "ดาวน์โหลด",
"download_include_embedded_motion_videos": "รวมวิดีโอที่ฝังอยู่ในภาพเคลื่อนไหว",
"download_include_embedded_motion_videos_description": "รวมวิดีโอที่ฝังอยู่ในภาพเคลื่อนไหวเมื่อดาวน์โหลดอัลบั้ม",
"downloading": "กำลังดาวน์โหลด",
"download_settings": "การตั้งค่าการดาวน์โหลด",
"download_settings_description": "จัดการการตั้งค่าการดาวน์โหลด",
"duration": "ระยะเวลา",
"edit_album": "แก้ไขอัลบั้ม",
"edit_avatar": "แก้ไขตัวละคร",
@@ -492,38 +483,6 @@
"error": "เกิดข้อผิดพลาด",
"error_loading_image": "เกิดข้อผิดพลาดระหว่างโหลดภาพ",
"errors": {
"cannot_navigate_next_asset": "ไม่สามารถเปลี่ยนเส้นทางได้",
"cannot_navigate_previous_asset": "ไม่สามารถเปลี่ยนเส้นทางก่อนหน้าได้",
"cant_apply_changes": "เกิดข้อผิดพลาดในการเปลี่ยนแปลง",
"cant_change_activity": "Can't {enabled, select, true {disable} other {enable}} activity",
"cant_change_asset_favorite": "ไม่สามารถเปลี่ยนสื่อที่ชื่นชอบได้",
"cant_change_metadata_assets_count": "Can't change metadata of {count, plural, one {# asset} other {# assets}}",
"cant_get_faces": "เกิดข้อผิดพลาดในการเรียกดูใบหน้า",
"cant_get_number_of_comments": "ไม่สามารถเรียกดูจำนวนความคิดเห็นได้",
"cant_search_people": "ไม่สามารถค้นหาบุคคลคนได้",
"cant_search_places": "ไม่สามารถค้นหาสถานที่ได้",
"cleared_jobs": "ล้างงาน: {job} สำเร็จ",
"error_adding_assets_to_album": "เกิดข้อผิดพลาดในการเพิ่มสื่อไปยังอัลบั้ม",
"error_adding_users_to_album": "เกิดข้อผิดพลาดในการเพิ่มผู้ใช้ไปยังอัลบั้ม",
"error_deleting_shared_user": "เกิดข้อผิดพลาดในการลบผู้ใช้ที่แชร์",
"error_downloading": "ไม่สามารถดาวน์โหลด {filename} ได้",
"error_hiding_buy_button": "Error hiding buy button",
"error_removing_assets_from_album": "เกิดข้อผิดพลาดในการลบสื่อจากอัลบั้ม",
"error_selecting_all_assets": "เกิดข้อผิดพลาดในการเลือกสื่อทั้งหมด",
"exclusion_pattern_already_exists": "ข้อยกเว้นนี้มีอยู่แล้ว",
"failed_job_command": "คำสั่ง {command} ผิดพลาด สำหรับ: {job}",
"failed_to_create_album": "ไม่สามารถสร้างอัลบั้มได้",
"failed_to_create_shared_link": "ไม่สามารถสร้างลิงก์ที่แชร์ได้",
"failed_to_edit_shared_link": "ไม่สามารถแก้ไขลิงก์ที่แชร์ได้",
"failed_to_get_people": "ไม่สามารถเรียกดูบุคคลได้",
"failed_to_keep_this_delete_others": "ไม่สามารถเก็บหรือลบได้",
"failed_to_load_asset": "ไม่สามารถโหลดสื่อได้",
"failed_to_load_assets": "ไม่สามารถโหลดสื่อได้",
"failed_to_load_people": "ไม่สามารถโหลดบุคคลได้",
"failed_to_remove_product_key": "Failed to remove product key",
"failed_to_stack_assets": "Failed to stack assets",
"failed_to_unstack_assets": "Failed to un-stack assets",
"incorrect_email_or_password": "อีเมลหรือรหัสผ่านไม่ถูกต้อง",
"import_path_already_exists": "พาธนำเข้านี้มีอยู่แล้ว",
"unable_to_add_album_users": "ไม่สามารถเพิ่มผู้ใช้ไปยังอัลบั้มได้",
"unable_to_add_comment": "ไม่สามารถเพิ่มความเห็นได้",
@@ -531,7 +490,7 @@
"unable_to_change_album_user_role": "ไม่สามารถเปลี่ยนบทบาทผู้ใช้ในอัลบั้มได้",
"unable_to_change_date": "ไม่สามารถเปลี่ยนวันที่ได้",
"unable_to_change_location": "ไม่สามารถเปลี่ยนตําแหน่งได้",
"unable_to_create_admin_account": "ไม่สามารถสร้างบัญชีผู้ดูแลระบบได้",
"unable_to_create_admin_account": "",
"unable_to_create_library": "ไม่สามารถสร้างคลังภาพได้",
"unable_to_create_user": "ไม่สามารถสร้างผู้ใช้ได้",
"unable_to_delete_album": "ไม่สามารถลบอัลบั้มได้",
@@ -572,8 +531,8 @@
"unable_to_update_settings": "ไม่สามารถอัพเดทการตั้งค่าได้",
"unable_to_update_user": "ไม่สามารถอัพเดทผู้ใช้ได้"
},
"exit_slideshow": "ออกจากการนำเสนอ",
"expand_all": "ขยายทั้งหมด",
"exit_slideshow": "",
"expand_all": "",
"expire_after": "หมดอายุหลังจาก",
"expired": "หมดอายุแล้ว",
"explore": "สํารวจ",
@@ -583,25 +542,22 @@
"favorite_or_unfavorite_photo": "โปรดหรือไม่โปรดภาพ",
"favorites": "รายการโปรด",
"feature_photo_updated": "อัพเดทภาพเด่นแล้ว",
"file_name": "ชื่อไฟล์",
"file_name_or_extension": "นามสกุลหรือชื่อไฟล์",
"file_name": "",
"file_name_or_extension": "",
"filename": "ชื่อไฟล์",
"filetype": "ชนิดไฟล์",
"filter_people": "กรองผู้คน",
"fix_incorrect_match": "แก้ไขการจับคู่ที่ไม่ถูกต้อง",
"fix_incorrect_match": "",
"forward": "ไปข้างหน้า",
"general": "ทั่วไป",
"get_help": "ขอความช่วยเหลือ",
"getting_started": "เริ่มต้นใช้งาน",
"go_back": "กลับ",
"go_to_search": "กลับไปยังการค้นหา",
"group_albums_by": "จัดกลุ่มอัลบั้มตาม",
"group_no": "ไม่จัดกลุ่ม",
"group_owner": "จัดกลุ่มโดยเจ้าของ",
"group_year": "จัดกลุ่มตามปี",
"has_quota": "เหลือพื้นที่",
"get_help": "",
"getting_started": "",
"go_back": "",
"go_to_search": "",
"group_albums_by": "",
"has_quota": "",
"hide_gallery": "ซ่อนคลังภาพ",
"hide_password": "ซ่อนรหัสผ่าน",
"hide_password": "",
"hide_person": "ซ่อนบุคคล",
"host": "โฮสต์",
"hour": "ชั่วโมง",
@@ -698,7 +654,7 @@
"no_assets_message": "กดเพื่อใส่ภาพคุณภาพแรก",
"no_duplicates_found": "ไม่พบรายการที่ซ้ำกัน",
"no_exif_info_available": "ไม่มีข้อมูล exif",
"no_explore_results_message": "ไม่พบผลลัพธ์ ลองใช้คำค้นหาอื่น ๆ",
"no_explore_results_message": "",
"no_favorites_message": "เพิ่มรายการโปรดเพื่อค้นหาภาพและวิดีโอที่ดีที่สุดของคุณอย่างรวดเร็ว",
"no_libraries_message": "สร้างคลังภาพภายนอกเพื่อดูภาพถ่ายและวิดีโอต่าง ๆ ของคุณ",
"no_name": "ไม่มีชื่อ",
@@ -714,9 +670,9 @@
"oauth": "OAuth",
"official_immich_resources": "แหล่งข้อมูล Immich อย่างเป็นทางการ",
"offline": "ออฟไลน์",
"ok": "ตกลง",
"oldest_first": "เรียงเก่าสุดก่อน",
"onboarding_welcome_user": "ยินดีต้อนรับคุณ {user}",
"ok": "โอเค",
"oldest_first": "เก่าสุดก่อน",
"onboarding_welcome_user": "ยินดีต้อนรับ {user}",
"online": "ออนไลน์",
"only_favorites": "รายการโปรดเท่านั้น",
"open_in_openstreetmap": "เปิดใน OpenStreetMap",
@@ -746,9 +702,9 @@
"years": "{years, plural, one {ปี} other {# ปี}}ที่ผ่านมา"
},
"path": "",
"pattern": "รูปแบบ",
"pattern": "",
"pause": "หยุด",
"pause_memories": "หยุดดูความทรงจํา",
"pause_memories": "",
"paused": "หยุด",
"pending": "กำลังรอ",
"people": "ผู้คน",
@@ -767,7 +723,7 @@
"play_motion_photo": "เล่นภาพวัตถุเคลื่อนไหว",
"play_or_pause_video": "เล่นหรือหยุดวิดีโอ",
"port": "พอร์ต",
"preset": "พรีเซ็ต",
"preset": "",
"preview": "ตัวอย่าง",
"previous": "ก่อนหน้า",
"previous_memory": "ความทรงจําก่อนหน้า",
@@ -783,65 +739,61 @@
"refreshed": "รีเฟรช",
"refreshes_every_file": "รีเฟรชทุกไฟล์",
"remove": "ลบ",
"remove_deleted_assets": "ลบสื่อที่ถูกลบ",
"remove_deleted_assets": "",
"remove_from_album": "ลบออกจากอัลบั้ม",
"remove_from_favorites": "เอาออกจากรายการโปรด",
"remove_from_shared_link": "ลบออกจากลิงก์ที่แชร์",
"repair": "ซ่อม",
"repair_no_results_message": "ไม่สามารถซ่อมแซมได้",
"replace_with_upload": "อัปโหลดทับรูปหรือวิดีโอนี้",
"repair_no_results_message": "",
"replace_with_upload": "",
"require_password": "ต้องการรหัสผ่าน",
"reset": "รีเซ็ต",
"reset_password": "ตั้งค่ารหัสผ่านใหม่",
"reset_people_visibility": "ปรับการมองเห็นใหม่",
"restore": "เรียกคืน",
"restore_all": "เรียกคืนทั้งหมด",
"restore_user": "เรียกคืนผู้ใช้",
"retry_upload": "ลองอัโหลดใหม่",
"review_duplicates": "ตรวจสอบรายการที่ซ้ำกัน",
"retry_upload": "ลองอัโหลดใหม่",
"review_duplicates": "",
"role": "บทบาท",
"save": "บันทึก",
"saved_profile": "แก้ไขโปรไฟล์สำเร็จ",
"saved_settings": "บันทึกการตั้งค่าสำเร็จ",
"saved_profile": "โพรไฟล์ที่บันทึกไว้",
"saved_settings": "การตั้งค่าที่บันทึกไว้",
"say_something": "พูดอะไรสักอย่าง",
"scan_all_libraries": "สแกนคลังภาพทั้งหมด",
"scan_settings": "ตั้งค่าการสแกน",
"search": "ค้นหา",
"search_albums": "ค้นหาอัลบั้ม",
"search_by_context": "ค้นหาตามบริบท",
"search_albums": "",
"search_by_context": "",
"search_camera_make": "",
"search_camera_model": "",
"search_city": "ค้นหาตามเมือง",
"search_country": "ค้นหาตามประเทศ",
"search_for_existing_person": "ค้นหาบุคคลที่มีอยู่",
"search_no_people": "ไม่พบบุคคลคน",
"search_no_people_named": "ไม่พบ \"{name}\"",
"search_options": "ตัวเลือกการค้นหา",
"search_city": "",
"search_country": "",
"search_for_existing_person": "",
"search_people": "ค้นหาผู้คน",
"search_places": "ค้นหาสถานที่",
"search_state": "ค้นหาตามรัฐ",
"search_timezone": "ค้นหาตามวันที่และเวลา",
"search_type": "ค้นหาตามประเภท",
"search_places": "",
"search_state": "",
"search_timezone": "",
"search_type": "",
"search_your_photos": "ค้นหารูปภาพของคุณ",
"searching_locales": "ค้นหาตามภูมิภาค",
"searching_locales": "",
"second": "วินาที",
"select_album_cover": "เลือกภาพปกอัลบั้ม",
"select_all": "เลือกทั้งหมด",
"select_avatar_color": "เลือกสีพื้นหลังของรูปโปรไฟล์",
"select_face": "เลือกใบหน้า",
"select_album_cover": "",
"select_all": "",
"select_avatar_color": "",
"select_face": "",
"select_featured_photo": "เลือกภาพเด่น",
"select_library_owner": "เลือกเจ้าของคลังภาพ",
"select_new_face": "เลือกใบหน้าใหม่",
"select_new_face": "",
"select_photos": "เลือกรูปภาพ",
"selected": "เลือก",
"send_message": "ส่งข้อความ",
"server_stats": "สถิติเซิร์ฟเวอร์",
"send_message": "",
"server_stats": "",
"set": "",
"set_as_album_cover": "ตั้งเป็นภาพปกอัลบั้ม",
"set_as_profile_picture": "ตั้งเป็นรูปโปรไฟล์",
"set_date_of_birth": "ตั้งวันเกิด",
"set_profile_picture": "ตั้งรูปโปรไฟล์",
"set_slideshow_to_fullscreen": "ตั้งค่าการนำเสนอเต็มจอ",
"set_as_album_cover": "",
"set_as_profile_picture": "",
"set_date_of_birth": "",
"set_profile_picture": "",
"set_slideshow_to_fullscreen": "",
"settings": "ตั้งค่า",
"settings_saved": "บันทึกการตั้งค่าแล้ว",
"share": "แชร์",
@@ -865,21 +817,12 @@
"show_progress_bar": "แสดงความคืบหน้า แถบ",
"show_search_options": "แสดงตัวเลือกการค้นหา",
"shuffle": "สับเปลี่ยน",
"sidebar": "แถบด้านข้าง",
"sidebar_display_description": "เปิดหรือปิดแถบด้านข้าง",
"sign_out": "ออกจากระบบ",
"sign_up": "ลงทะเบียน",
"size": "ขนาด",
"skip_to_content": "ข้ามไปยังเนื้อหา",
"slideshow": "สไลด์",
"slideshow_settings": "ตั้งค่าสไลด์",
"sort_created": "จัดเรียงตามวันที่สร้าง",
"sort_items": "จัดเรียงรายการ",
"sort_modified": "จัดเรียงตามวันที่แก้ไข",
"sort_oldest": "จัดเรียงตามเก่าสุด",
"sort_people_by_similarity": "จุดเรียงบุคคลตามความคล้ายคลึง",
"sort_recent": "จัดเรียงใหม่ล่าสุด",
"sort_albums_by": "จัดเรียงอัลบั้มโดย...",
"sort_albums_by": "เรียงอัลบั้มโดย...",
"stack": "ซ้อน",
"stack_selected_photos": "",
"stacktrace": "",
@@ -888,30 +831,25 @@
"status": "สถานะ",
"stop_motion_photo": "ภาพวัตถุเคลื่อนไหว",
"stop_photo_sharing": "หยุดแชร์รูปภาพ?",
"storage": "พื้นที่จัดเก็บ",
"storage_label": "เนื้อที่จัดเก็บ",
"storage_usage": "ใช้ไป {used} จาก {available} ",
"storage": "ที่จัดเก็บ",
"storage_label": "ฉลากจัดเก็บ",
"submit": "ส่ง",
"suggestions": "ข้อเสนอแนะ",
"sunrise_on_the_beach": "พระอาทิตย์ขึ้นบนชายหาด",
"swap_merge_direction": "สลับด้านรวม",
"sync": "ซิงค์",
"template": "แทมแพลค",
"template": "แม่แบบ",
"theme": "ธีม",
"theme_selection": "การเลือกธีม",
"theme_selection_description": "ตั้งค่าธีมให้สว่างหรือมืดโดยอัตโนมัติ อิงจากค่าของเบราว์เซอร์ของคุณ",
"time_based_memories": "ความทรงจําตามเวลา",
"timezone": "เขตเวลา",
"timeline": "Timeline",
"to_archive": "จัดเก็บถาวร",
"to_change_password": "Change password",
"toggle_settings": "สลับการตั้งค่า",
"toggle_theme": "สลับธีม",
"total_usage": "การใช้งานรวม",
"trash": "ถังขยะ",
"trash": "ขยะ",
"trash_all": "ทิ้งทั้งหมด",
"trash_no_results_message": "รูปภาพหรือวิดีโอที่ถูกลบจะอยู่ที่นี่",
"trashed_items_will_be_permanently_deleted_after": "รายการที่ถูกลบจะถูกลบทิ้งภายใน {days, plural, one {# วัน} other {# วัน}}.",
"trash_no_results_message": "รูปและวีดีโอที่ถูกทิ้งจะมาโผล่ที่นี่",
"type": "ประเภท",
"unarchive": "นำออกจากที่เก็บถาวร",
"unfavorite": "นำออกจากรายการโปรด",
@@ -924,8 +862,8 @@
"unstack": "หยุดซ้อน",
"up_next": "ต่อไป",
"updated_password": "รหัสผ่านเปลี่ยนแล้ว",
"upload": "อัโหลด",
"upload_concurrency": "อัโหลดพร้อมกัน",
"upload": "อัโหลด",
"upload_concurrency": "อัโหลดพร้อมกัน",
"url": "URL",
"usage": "การใช้งาน",
"user": "ผู้ใช้",
@@ -935,7 +873,7 @@
"user_usage_stats_description": "ดูสถิติการใช้งานบัญชี",
"username": "ชื่อผู้ใช้",
"users": "ผู้ใช้",
"utilities": "เครื่องมือ",
"utilities": "",
"validate": "ตรวจสอบ",
"variables": "ตัวแปร",
"version": "รุ่น",

View File

@@ -51,10 +51,6 @@ start_docker_compose() {
show_friendly_message() {
local ip_address
ip_address=$(hostname -I | awk '{print $1}')
# If length of ip_address is 0, then we are on a Mac
if [ ${#ip_address} -eq 0 ]; then
ip_address=$(ipconfig getifaddr en0)
fi
cat <<EOF
Successfully deployed Immich!
You can access the website or the mobile app at http://$ip_address:2283

View File

@@ -96,8 +96,8 @@ download:
locale_code: ro-RO
- file: mobile/assets/i18n/id-ID.json
locale_code: id-ID
- file: mobile/assets/i18n/gl-ES.json
locale_code: gl-ES
- file: mobile/assets/i18n/gl.json
locale_code: gl
- file: mobile/assets/i18n/ga.json
locale_code: ga
- file: mobile/assets/i18n/tr-TR.json

View File

@@ -1,6 +1,6 @@
ARG DEVICE=cpu
FROM python:3.11-bookworm@sha256:14b4620f59a90f163dfa6bd252b68743f9a41d494a9fde935f9d7669d98094bb AS builder-cpu
FROM python:3.11-bookworm@sha256:adb581d8ed80edd03efd4dcad66db115b9ce8de8522b01720b9f3e6146f0884c AS builder-cpu
FROM builder-cpu AS builder-openvino
@@ -34,7 +34,7 @@ RUN python3 -m venv /opt/venv
COPY poetry.lock pyproject.toml ./
RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev
FROM python:3.11-slim-bookworm@sha256:42420f737ba91d509fc60d5ed65ed0492678a90c561e1fa08786ae8ba8b52eda AS prod-cpu
FROM python:3.11-slim-bookworm@sha256:6ed5bff4d7d377e2a27d9285553b8c21cfccc4f00881de1b24c9bc8d90016e82 AS prod-cpu
FROM prod-cpu AS prod-openvino
@@ -106,22 +106,6 @@ COPY --from=builder /opt/venv /opt/venv
COPY ann/ann.py /usr/src/ann/ann.py
COPY start.sh log_conf.json gunicorn_conf.py ./
COPY app .
ARG BUILD_ID
ARG BUILD_IMAGE
ARG BUILD_SOURCE_REF
ARG BUILD_SOURCE_COMMIT
ENV IMMICH_BUILD=${BUILD_ID}
ENV IMMICH_BUILD_URL=https://github.com/immich-app/immich/actions/runs/${BUILD_ID}
ENV IMMICH_BUILD_IMAGE=${BUILD_IMAGE}
ENV IMMICH_BUILD_IMAGE_URL=https://github.com/immich-app/immich/pkgs/container/immich-machine-learning
ENV IMMICH_REPOSITORY=immich-app/immich
ENV IMMICH_REPOSITORY_URL=https://github.com/immich-app/immich
ENV IMMICH_SOURCE_REF=${BUILD_SOURCE_REF}
ENV IMMICH_SOURCE_COMMIT=${BUILD_SOURCE_COMMIT}
ENV IMMICH_SOURCE_URL=https://github.com/immich-app/immich/commit/${BUILD_SOURCE_COMMIT}
ENTRYPOINT ["tini", "--"]
CMD ["./start.sh"]

View File

@@ -758,23 +758,23 @@ test = ["pytest (>=6)"]
[[package]]
name = "fastapi"
version = "0.115.8"
version = "0.115.6"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false
python-versions = ">=3.8"
files = [
{file = "fastapi-0.115.8-py3-none-any.whl", hash = "sha256:753a96dd7e036b34eeef8babdfcfe3f28ff79648f86551eb36bfc1b0bf4a8cbf"},
{file = "fastapi-0.115.8.tar.gz", hash = "sha256:0ce9111231720190473e222cdf0f07f7206ad7e53ea02beb1d2dc36e2f0741e9"},
{file = "fastapi-0.115.6-py3-none-any.whl", hash = "sha256:e9240b29e36fa8f4bb7290316988e90c381e5092e0cbe84e7818cc3713bcf305"},
{file = "fastapi-0.115.6.tar.gz", hash = "sha256:9ec46f7addc14ea472958a96aae5b5de65f39721a46aaf5705c480d9a8b76654"},
]
[package.dependencies]
pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
starlette = ">=0.40.0,<0.46.0"
starlette = ">=0.40.0,<0.42.0"
typing-extensions = ">=4.8.0"
[package.extras]
all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"]
[[package]]
name = "filelock"
@@ -1331,13 +1331,13 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "huggingface-hub"
version = "0.28.1"
version = "0.27.1"
description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
optional = false
python-versions = ">=3.8.0"
files = [
{file = "huggingface_hub-0.28.1-py3-none-any.whl", hash = "sha256:aa6b9a3ffdae939b72c464dbb0d7f99f56e649b55c3d52406f49e0a5a620c0a7"},
{file = "huggingface_hub-0.28.1.tar.gz", hash = "sha256:893471090c98e3b6efbdfdacafe4052b20b84d59866fb6f54c33d9af18c303ae"},
{file = "huggingface_hub-0.27.1-py3-none-any.whl", hash = "sha256:1c5155ca7d60b60c2e2fc38cbb3ffb7f7c3adf48f824015b219af9061771daec"},
{file = "huggingface_hub-0.27.1.tar.gz", hash = "sha256:c004463ca870283909d715d20f066ebd6968c2207dae9393fdffb3c1d4d8f98b"},
]
[package.dependencies]
@@ -1350,13 +1350,13 @@ tqdm = ">=4.42.1"
typing-extensions = ">=3.7.4.3"
[package.extras]
all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio (>=4.0.0)", "jedi", "libcst (==1.4.0)", "mypy (==1.5.1)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.9.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"]
all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio (>=4.0.0)", "jedi", "libcst (==1.4.0)", "mypy (==1.5.1)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.5.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"]
cli = ["InquirerPy (==0.3.4)"]
dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio (>=4.0.0)", "jedi", "libcst (==1.4.0)", "mypy (==1.5.1)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.9.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"]
dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio (>=4.0.0)", "jedi", "libcst (==1.4.0)", "mypy (==1.5.1)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.5.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"]
fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"]
hf-transfer = ["hf-transfer (>=0.1.4)"]
inference = ["aiohttp"]
quality = ["libcst (==1.4.0)", "mypy (==1.5.1)", "ruff (>=0.9.0)"]
quality = ["libcst (==1.4.0)", "mypy (==1.5.1)", "ruff (>=0.5.0)"]
tensorflow = ["graphviz", "pydot", "tensorflow"]
tensorflow-testing = ["keras (<3.0)", "tensorflow"]
testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio (>=4.0.0)", "jedi", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"]
@@ -1625,13 +1625,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
[[package]]
name = "locust"
version = "2.32.9"
version = "2.32.6"
description = "Developer-friendly load testing framework"
optional = false
python-versions = ">=3.9"
files = [
{file = "locust-2.32.9-py3-none-any.whl", hash = "sha256:d9447c26d2bbaec5a0ace7cadefa1a31820ed392234257b309965a43d5e8d26f"},
{file = "locust-2.32.9.tar.gz", hash = "sha256:4c297afa5cdc3de15dfa79279576e5f33c1d69dd70006b51d079dcbd212201cc"},
{file = "locust-2.32.6-py3-none-any.whl", hash = "sha256:d5c0e4f73134415d250087034431cf3ea42ca695d3dee7f10812287cacb6c4ef"},
{file = "locust-2.32.6.tar.gz", hash = "sha256:6600cc308398e724764aacc56ccddf6cfcd0127c4c92dedd5c4979dd37ef5b15"},
]
[package.dependencies]
@@ -1649,8 +1649,8 @@ psutil = ">=5.9.1"
pywin32 = {version = "*", markers = "sys_platform == \"win32\""}
pyzmq = ">=25.0.0"
requests = [
{version = ">=2.26.0", markers = "python_full_version <= \"3.11.0\""},
{version = ">=2.32.2", markers = "python_full_version > \"3.11.0\""},
{version = ">=2.26.0", markers = "python_full_version <= \"3.11.0\""},
]
setuptools = ">=70.0.0"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
@@ -1893,43 +1893,49 @@ files = [
[[package]]
name = "mypy"
version = "1.15.0"
version = "1.14.1"
description = "Optional static typing for Python"
optional = false
python-versions = ">=3.9"
python-versions = ">=3.8"
files = [
{file = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"},
{file = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"},
{file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b"},
{file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3"},
{file = "mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b"},
{file = "mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828"},
{file = "mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f"},
{file = "mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5"},
{file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e"},
{file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c"},
{file = "mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f"},
{file = "mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f"},
{file = "mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd"},
{file = "mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f"},
{file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464"},
{file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee"},
{file = "mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e"},
{file = "mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22"},
{file = "mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445"},
{file = "mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d"},
{file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5"},
{file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036"},
{file = "mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357"},
{file = "mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf"},
{file = "mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078"},
{file = "mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba"},
{file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5"},
{file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b"},
{file = "mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2"},
{file = "mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980"},
{file = "mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"},
{file = "mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43"},
{file = "mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"},
{file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"},
{file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d"},
{file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b"},
{file = "mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427"},
{file = "mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f"},
{file = "mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c"},
{file = "mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1"},
{file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8"},
{file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f"},
{file = "mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1"},
{file = "mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae"},
{file = "mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14"},
{file = "mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9"},
{file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11"},
{file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e"},
{file = "mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89"},
{file = "mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b"},
{file = "mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255"},
{file = "mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34"},
{file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a"},
{file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9"},
{file = "mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd"},
{file = "mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107"},
{file = "mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31"},
{file = "mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6"},
{file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319"},
{file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac"},
{file = "mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b"},
{file = "mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837"},
{file = "mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35"},
{file = "mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc"},
{file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9"},
{file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb"},
{file = "mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60"},
{file = "mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c"},
{file = "mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1"},
{file = "mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6"},
]
[package.dependencies]
@@ -2175,98 +2181,94 @@ files = [
[package.dependencies]
numpy = [
{version = ">=1.26.0", markers = "python_version >= \"3.12\""},
{version = ">=1.23.5", markers = "python_version >= \"3.11\" and python_version < \"3.12\""},
{version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""},
{version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""},
{version = ">=1.26.0", markers = "python_version >= \"3.12\""},
]
[[package]]
name = "orjson"
version = "3.10.15"
version = "3.10.14"
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
optional = false
python-versions = ">=3.8"
files = [
{file = "orjson-3.10.15-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:552c883d03ad185f720d0c09583ebde257e41b9521b74ff40e08b7dec4559c04"},
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e3e8d438d02e4854f70bfdc03a6bcdb697358dbaa6bcd19cbe24d24ece1f8"},
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c2c79fa308e6edb0ffab0a31fd75a7841bf2a79a20ef08a3c6e3b26814c8ca8"},
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cb85490aa6bf98abd20607ab5c8324c0acb48d6da7863a51be48505646c814"},
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763dadac05e4e9d2bc14938a45a2d0560549561287d41c465d3c58aec818b164"},
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a330b9b4734f09a623f74a7490db713695e13b67c959713b78369f26b3dee6bf"},
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a61a4622b7ff861f019974f73d8165be1bd9a0855e1cad18ee167acacabeb061"},
{file = "orjson-3.10.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acd271247691574416b3228db667b84775c497b245fa275c6ab90dc1ffbbd2b3"},
{file = "orjson-3.10.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4759b109c37f635aa5c5cc93a1b26927bfde24b254bcc0e1149a9fada253d2d"},
{file = "orjson-3.10.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9e992fd5cfb8b9f00bfad2fd7a05a4299db2bbe92e6440d9dd2fab27655b3182"},
{file = "orjson-3.10.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f95fb363d79366af56c3f26b71df40b9a583b07bbaaf5b317407c4d58497852e"},
{file = "orjson-3.10.15-cp310-cp310-win32.whl", hash = "sha256:f9875f5fea7492da8ec2444839dcc439b0ef298978f311103d0b7dfd775898ab"},
{file = "orjson-3.10.15-cp310-cp310-win_amd64.whl", hash = "sha256:17085a6aa91e1cd70ca8533989a18b5433e15d29c574582f76f821737c8d5806"},
{file = "orjson-3.10.15-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c4cc83960ab79a4031f3119cc4b1a1c627a3dc09df125b27c4201dff2af7eaa6"},
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddbeef2481d895ab8be5185f2432c334d6dec1f5d1933a9c83014d188e102cef"},
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e590a0477b23ecd5b0ac865b1b907b01b3c5535f5e8a8f6ab0e503efb896334"},
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6be38bd103d2fd9bdfa31c2720b23b5d47c6796bcb1d1b598e3924441b4298d"},
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ff4f6edb1578960ed628a3b998fa54d78d9bb3e2eb2cfc5c2a09732431c678d0"},
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0482b21d0462eddd67e7fce10b89e0b6ac56570424662b685a0d6fccf581e13"},
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bb5cc3527036ae3d98b65e37b7986a918955f85332c1ee07f9d3f82f3a6899b5"},
{file = "orjson-3.10.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d569c1c462912acdd119ccbf719cf7102ea2c67dd03b99edcb1a3048651ac96b"},
{file = "orjson-3.10.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1e6d33efab6b71d67f22bf2962895d3dc6f82a6273a965fab762e64fa90dc399"},
{file = "orjson-3.10.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c33be3795e299f565681d69852ac8c1bc5c84863c0b0030b2b3468843be90388"},
{file = "orjson-3.10.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eea80037b9fae5339b214f59308ef0589fc06dc870578b7cce6d71eb2096764c"},
{file = "orjson-3.10.15-cp311-cp311-win32.whl", hash = "sha256:d5ac11b659fd798228a7adba3e37c010e0152b78b1982897020a8e019a94882e"},
{file = "orjson-3.10.15-cp311-cp311-win_amd64.whl", hash = "sha256:cf45e0214c593660339ef63e875f32ddd5aa3b4adc15e662cdb80dc49e194f8e"},
{file = "orjson-3.10.15-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9d11c0714fc85bfcf36ada1179400862da3288fc785c30e8297844c867d7505a"},
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dba5a1e85d554e3897fa9fe6fbcff2ed32d55008973ec9a2b992bd9a65d2352d"},
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7723ad949a0ea502df656948ddd8b392780a5beaa4c3b5f97e525191b102fff0"},
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6fd9bc64421e9fe9bd88039e7ce8e58d4fead67ca88e3a4014b143cec7684fd4"},
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dadba0e7b6594216c214ef7894c4bd5f08d7c0135f4dd0145600be4fbcc16767"},
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b48f59114fe318f33bbaee8ebeda696d8ccc94c9e90bc27dbe72153094e26f41"},
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:035fb83585e0f15e076759b6fedaf0abb460d1765b6a36f48018a52858443514"},
{file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d13b7fe322d75bf84464b075eafd8e7dd9eae05649aa2a5354cfa32f43c59f17"},
{file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7066b74f9f259849629e0d04db6609db4cf5b973248f455ba5d3bd58a4daaa5b"},
{file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:88dc3f65a026bd3175eb157fea994fca6ac7c4c8579fc5a86fc2114ad05705b7"},
{file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b342567e5465bd99faa559507fe45e33fc76b9fb868a63f1642c6bc0735ad02a"},
{file = "orjson-3.10.15-cp312-cp312-win32.whl", hash = "sha256:0a4f27ea5617828e6b58922fdbec67b0aa4bb844e2d363b9244c47fa2180e665"},
{file = "orjson-3.10.15-cp312-cp312-win_amd64.whl", hash = "sha256:ef5b87e7aa9545ddadd2309efe6824bd3dd64ac101c15dae0f2f597911d46eaa"},
{file = "orjson-3.10.15-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bae0e6ec2b7ba6895198cd981b7cca95d1487d0147c8ed751e5632ad16f031a6"},
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f93ce145b2db1252dd86af37d4165b6faa83072b46e3995ecc95d4b2301b725a"},
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c203f6f969210128af3acae0ef9ea6aab9782939f45f6fe02d05958fe761ef9"},
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8918719572d662e18b8af66aef699d8c21072e54b6c82a3f8f6404c1f5ccd5e0"},
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f71eae9651465dff70aa80db92586ad5b92df46a9373ee55252109bb6b703307"},
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e117eb299a35f2634e25ed120c37c641398826c2f5a3d3cc39f5993b96171b9e"},
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:13242f12d295e83c2955756a574ddd6741c81e5b99f2bef8ed8d53e47a01e4b7"},
{file = "orjson-3.10.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7946922ada8f3e0b7b958cc3eb22cfcf6c0df83d1fe5521b4a100103e3fa84c8"},
{file = "orjson-3.10.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b7155eb1623347f0f22c38c9abdd738b287e39b9982e1da227503387b81b34ca"},
{file = "orjson-3.10.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:208beedfa807c922da4e81061dafa9c8489c6328934ca2a562efa707e049e561"},
{file = "orjson-3.10.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eca81f83b1b8c07449e1d6ff7074e82e3fd6777e588f1a6632127f286a968825"},
{file = "orjson-3.10.15-cp313-cp313-win32.whl", hash = "sha256:c03cd6eea1bd3b949d0d007c8d57049aa2b39bd49f58b4b2af571a5d3833d890"},
{file = "orjson-3.10.15-cp313-cp313-win_amd64.whl", hash = "sha256:fd56a26a04f6ba5fb2045b0acc487a63162a958ed837648c5781e1fe3316cfbf"},
{file = "orjson-3.10.15-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:5e8afd6200e12771467a1a44e5ad780614b86abb4b11862ec54861a82d677746"},
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da9a18c500f19273e9e104cca8c1f0b40a6470bcccfc33afcc088045d0bf5ea6"},
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb00b7bfbdf5d34a13180e4805d76b4567025da19a197645ca746fc2fb536586"},
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33aedc3d903378e257047fee506f11e0833146ca3e57a1a1fb0ddb789876c1e1"},
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd0099ae6aed5eb1fc84c9eb72b95505a3df4267e6962eb93cdd5af03be71c98"},
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c864a80a2d467d7786274fce0e4f93ef2a7ca4ff31f7fc5634225aaa4e9e98c"},
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c25774c9e88a3e0013d7d1a6c8056926b607a61edd423b50eb5c88fd7f2823ae"},
{file = "orjson-3.10.15-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:e78c211d0074e783d824ce7bb85bf459f93a233eb67a5b5003498232ddfb0e8a"},
{file = "orjson-3.10.15-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:43e17289ffdbbac8f39243916c893d2ae41a2ea1a9cbb060a56a4d75286351ae"},
{file = "orjson-3.10.15-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:781d54657063f361e89714293c095f506c533582ee40a426cb6489c48a637b81"},
{file = "orjson-3.10.15-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6875210307d36c94873f553786a808af2788e362bd0cf4c8e66d976791e7b528"},
{file = "orjson-3.10.15-cp38-cp38-win32.whl", hash = "sha256:305b38b2b8f8083cc3d618927d7f424349afce5975b316d33075ef0f73576b60"},
{file = "orjson-3.10.15-cp38-cp38-win_amd64.whl", hash = "sha256:5dd9ef1639878cc3efffed349543cbf9372bdbd79f478615a1c633fe4e4180d1"},
{file = "orjson-3.10.15-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ffe19f3e8d68111e8644d4f4e267a069ca427926855582ff01fc012496d19969"},
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d433bf32a363823863a96561a555227c18a522a8217a6f9400f00ddc70139ae2"},
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da03392674f59a95d03fa5fb9fe3a160b0511ad84b7a3914699ea5a1b3a38da2"},
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a63bb41559b05360ded9132032239e47983a39b151af1201f07ec9370715c82"},
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3766ac4702f8f795ff3fa067968e806b4344af257011858cc3d6d8721588b53f"},
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a1c73dcc8fadbd7c55802d9aa093b36878d34a3b3222c41052ce6b0fc65f8e8"},
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b299383825eafe642cbab34be762ccff9fd3408d72726a6b2a4506d410a71ab3"},
{file = "orjson-3.10.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:abc7abecdbf67a173ef1316036ebbf54ce400ef2300b4e26a7b843bd446c2480"},
{file = "orjson-3.10.15-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:3614ea508d522a621384c1d6639016a5a2e4f027f3e4a1c93a51867615d28829"},
{file = "orjson-3.10.15-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:295c70f9dc154307777ba30fe29ff15c1bcc9dfc5c48632f37d20a607e9ba85a"},
{file = "orjson-3.10.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:63309e3ff924c62404923c80b9e2048c1f74ba4b615e7584584389ada50ed428"},
{file = "orjson-3.10.15-cp39-cp39-win32.whl", hash = "sha256:a2f708c62d026fb5340788ba94a55c23df4e1869fec74be455e0b2f5363b8507"},
{file = "orjson-3.10.15-cp39-cp39-win_amd64.whl", hash = "sha256:efcf6c735c3d22ef60c4aa27a5238f1a477df85e9b15f2142f9d669beb2d13fd"},
{file = "orjson-3.10.15.tar.gz", hash = "sha256:05ca7fe452a2e9d8d9d706a2984c95b9c2ebc5db417ce0b7a49b91d50642a23e"},
{file = "orjson-3.10.14-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:849ea7845a55f09965826e816cdc7689d6cf74fe9223d79d758c714af955bcb6"},
{file = "orjson-3.10.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5947b139dfa33f72eecc63f17e45230a97e741942955a6c9e650069305eb73d"},
{file = "orjson-3.10.14-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cde6d76910d3179dae70f164466692f4ea36da124d6fb1a61399ca589e81d69a"},
{file = "orjson-3.10.14-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6dfbaeb7afa77ca608a50e2770a0461177b63a99520d4928e27591b142c74b1"},
{file = "orjson-3.10.14-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa45e489ef80f28ff0e5ba0a72812b8cfc7c1ef8b46a694723807d1b07c89ebb"},
{file = "orjson-3.10.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f5007abfdbb1d866e2aa8990bd1c465f0f6da71d19e695fc278282be12cffa5"},
{file = "orjson-3.10.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1b49e2af011c84c3f2d541bb5cd1e3c7c2df672223e7e3ea608f09cf295e5f8a"},
{file = "orjson-3.10.14-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:164ac155109226b3a2606ee6dda899ccfbe6e7e18b5bdc3fbc00f79cc074157d"},
{file = "orjson-3.10.14-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6b1225024cf0ef5d15934b5ffe9baf860fe8bc68a796513f5ea4f5056de30bca"},
{file = "orjson-3.10.14-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d6546e8073dc382e60fcae4a001a5a1bc46da5eab4a4878acc2d12072d6166d5"},
{file = "orjson-3.10.14-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9f1d2942605c894162252d6259b0121bf1cb493071a1ea8cb35d79cb3e6ac5bc"},
{file = "orjson-3.10.14-cp310-cp310-win32.whl", hash = "sha256:397083806abd51cf2b3bbbf6c347575374d160331a2d33c5823e22249ad3118b"},
{file = "orjson-3.10.14-cp310-cp310-win_amd64.whl", hash = "sha256:fa18f949d3183a8d468367056be989666ac2bef3a72eece0bade9cdb733b3c28"},
{file = "orjson-3.10.14-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f506fd666dd1ecd15a832bebc66c4df45c1902fd47526292836c339f7ba665a9"},
{file = "orjson-3.10.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efe5fd254cfb0eeee13b8ef7ecb20f5d5a56ddda8a587f3852ab2cedfefdb5f6"},
{file = "orjson-3.10.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ddc8c866d7467f5ee2991397d2ea94bcf60d0048bdd8ca555740b56f9042725"},
{file = "orjson-3.10.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af8e42ae4363773658b8d578d56dedffb4f05ceeb4d1d4dd3fb504950b45526"},
{file = "orjson-3.10.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84dd83110503bc10e94322bf3ffab8bc49150176b49b4984dc1cce4c0a993bf9"},
{file = "orjson-3.10.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36f5bfc0399cd4811bf10ec7a759c7ab0cd18080956af8ee138097d5b5296a95"},
{file = "orjson-3.10.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:868943660fb2a1e6b6b965b74430c16a79320b665b28dd4511d15ad5038d37d5"},
{file = "orjson-3.10.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:33449c67195969b1a677533dee9d76e006001213a24501333624623e13c7cc8e"},
{file = "orjson-3.10.14-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e4c9f60f9fb0b5be66e416dcd8c9d94c3eabff3801d875bdb1f8ffc12cf86905"},
{file = "orjson-3.10.14-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0de4d6315cfdbd9ec803b945c23b3a68207fd47cbe43626036d97e8e9561a436"},
{file = "orjson-3.10.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83adda3db595cb1a7e2237029b3249c85afbe5c747d26b41b802e7482cb3933e"},
{file = "orjson-3.10.14-cp311-cp311-win32.whl", hash = "sha256:998019ef74a4997a9d741b1473533cdb8faa31373afc9849b35129b4b8ec048d"},
{file = "orjson-3.10.14-cp311-cp311-win_amd64.whl", hash = "sha256:9d034abdd36f0f0f2240f91492684e5043d46f290525d1117712d5b8137784eb"},
{file = "orjson-3.10.14-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2ad4b7e367efba6dc3f119c9a0fcd41908b7ec0399a696f3cdea7ec477441b09"},
{file = "orjson-3.10.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f496286fc85e93ce0f71cc84fc1c42de2decf1bf494094e188e27a53694777a7"},
{file = "orjson-3.10.14-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c7f189bbfcded40e41a6969c1068ba305850ba016665be71a217918931416fbf"},
{file = "orjson-3.10.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8cc8204f0b75606869c707da331058ddf085de29558b516fc43c73ee5ee2aadb"},
{file = "orjson-3.10.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deaa2899dff7f03ab667e2ec25842d233e2a6a9e333efa484dfe666403f3501c"},
{file = "orjson-3.10.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1c3ea52642c9714dc6e56de8a451a066f6d2707d273e07fe8a9cc1ba073813d"},
{file = "orjson-3.10.14-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9d3f9ed72e7458ded9a1fb1b4d4ed4c4fdbaf82030ce3f9274b4dc1bff7ace2b"},
{file = "orjson-3.10.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:07520685d408a2aba514c17ccc16199ff2934f9f9e28501e676c557f454a37fe"},
{file = "orjson-3.10.14-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:76344269b550ea01488d19a2a369ab572c1ac4449a72e9f6ac0d70eb1cbfb953"},
{file = "orjson-3.10.14-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e2979d0f2959990620f7e62da6cd954e4620ee815539bc57a8ae46e2dacf90e3"},
{file = "orjson-3.10.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03f61ca3674555adcb1aa717b9fc87ae936aa7a63f6aba90a474a88701278780"},
{file = "orjson-3.10.14-cp312-cp312-win32.whl", hash = "sha256:d5075c54edf1d6ad81d4c6523ce54a748ba1208b542e54b97d8a882ecd810fd1"},
{file = "orjson-3.10.14-cp312-cp312-win_amd64.whl", hash = "sha256:175cafd322e458603e8ce73510a068d16b6e6f389c13f69bf16de0e843d7d406"},
{file = "orjson-3.10.14-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:0905ca08a10f7e0e0c97d11359609300eb1437490a7f32bbaa349de757e2e0c7"},
{file = "orjson-3.10.14-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92d13292249f9f2a3e418cbc307a9fbbef043c65f4bd8ba1eb620bc2aaba3d15"},
{file = "orjson-3.10.14-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90937664e776ad316d64251e2fa2ad69265e4443067668e4727074fe39676414"},
{file = "orjson-3.10.14-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9ed3d26c4cb4f6babaf791aa46a029265850e80ec2a566581f5c2ee1a14df4f1"},
{file = "orjson-3.10.14-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:56ee546c2bbe9599aba78169f99d1dc33301853e897dbaf642d654248280dc6e"},
{file = "orjson-3.10.14-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:901e826cb2f1bdc1fcef3ef59adf0c451e8f7c0b5deb26c1a933fb66fb505eae"},
{file = "orjson-3.10.14-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:26336c0d4b2d44636e1e1e6ed1002f03c6aae4a8a9329561c8883f135e9ff010"},
{file = "orjson-3.10.14-cp313-cp313-win32.whl", hash = "sha256:e2bc525e335a8545c4e48f84dd0328bc46158c9aaeb8a1c2276546e94540ea3d"},
{file = "orjson-3.10.14-cp313-cp313-win_amd64.whl", hash = "sha256:eca04dfd792cedad53dc9a917da1a522486255360cb4e77619343a20d9f35364"},
{file = "orjson-3.10.14-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9a0fba3b8a587a54c18585f077dcab6dd251c170d85cfa4d063d5746cd595a0f"},
{file = "orjson-3.10.14-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:175abf3d20e737fec47261d278f95031736a49d7832a09ab684026528c4d96db"},
{file = "orjson-3.10.14-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29ca1a93e035d570e8b791b6c0feddd403c6a5388bfe870bf2aa6bba1b9d9b8e"},
{file = "orjson-3.10.14-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f77202c80e8ab5a1d1e9faf642343bee5aaf332061e1ada4e9147dbd9eb00c46"},
{file = "orjson-3.10.14-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e2ec73b7099b6a29b40a62e08a23b936423bd35529f8f55c42e27acccde7954"},
{file = "orjson-3.10.14-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2d1679df9f9cd9504f8dff24555c1eaabba8aad7f5914f28dab99e3c2552c9d"},
{file = "orjson-3.10.14-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:691ab9a13834310a263664313e4f747ceb93662d14a8bdf20eb97d27ed488f16"},
{file = "orjson-3.10.14-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:b11ed82054fce82fb74cea33247d825d05ad6a4015ecfc02af5fbce442fbf361"},
{file = "orjson-3.10.14-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:e70a1d62b8288677d48f3bea66c21586a5f999c64ecd3878edb7393e8d1b548d"},
{file = "orjson-3.10.14-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:16642f10c1ca5611251bd835de9914a4b03095e28a34c8ba6a5500b5074338bd"},
{file = "orjson-3.10.14-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3871bad546aa66c155e3f36f99c459780c2a392d502a64e23fb96d9abf338511"},
{file = "orjson-3.10.14-cp38-cp38-win32.whl", hash = "sha256:0293a88815e9bb5c90af4045f81ed364d982f955d12052d989d844d6c4e50945"},
{file = "orjson-3.10.14-cp38-cp38-win_amd64.whl", hash = "sha256:6169d3868b190d6b21adc8e61f64e3db30f50559dfbdef34a1cd6c738d409dfc"},
{file = "orjson-3.10.14-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:06d4ec218b1ec1467d8d64da4e123b4794c781b536203c309ca0f52819a16c03"},
{file = "orjson-3.10.14-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:962c2ec0dcaf22b76dee9831fdf0c4a33d4bf9a257a2bc5d4adc00d5c8ad9034"},
{file = "orjson-3.10.14-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:21d3be4132f71ef1360385770474f29ea1538a242eef72ac4934fe142800e37f"},
{file = "orjson-3.10.14-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c28ed60597c149a9e3f5ad6dd9cebaee6fb2f0e3f2d159a4a2b9b862d4748860"},
{file = "orjson-3.10.14-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e947f70167fe18469f2023644e91ab3d24f9aed69a5e1c78e2c81b9cea553fb"},
{file = "orjson-3.10.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64410696c97a35af2432dea7bdc4ce32416458159430ef1b4beb79fd30093ad6"},
{file = "orjson-3.10.14-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8050a5d81c022561ee29cd2739de5b4445f3c72f39423fde80a63299c1892c52"},
{file = "orjson-3.10.14-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b49a28e30d3eca86db3fe6f9b7f4152fcacbb4a467953cd1b42b94b479b77956"},
{file = "orjson-3.10.14-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ca041ad20291a65d853a9523744eebc3f5a4b2f7634e99f8fe88320695ddf766"},
{file = "orjson-3.10.14-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:d313a2998b74bb26e9e371851a173a9b9474764916f1fc7971095699b3c6e964"},
{file = "orjson-3.10.14-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7796692136a67b3e301ef9052bde6fe8e7bd5200da766811a3a608ffa62aaff0"},
{file = "orjson-3.10.14-cp39-cp39-win32.whl", hash = "sha256:eee4bc767f348fba485ed9dc576ca58b0a9eac237f0e160f7a59bce628ed06b3"},
{file = "orjson-3.10.14-cp39-cp39-win_amd64.whl", hash = "sha256:96a1c0ee30fb113b3ae3c748fd75ca74a157ff4c58476c47db4d61518962a011"},
{file = "orjson-3.10.14.tar.gz", hash = "sha256:cf31f6f071a6b8e7aa1ead1fa27b935b48d00fbfa6a28ce856cfff2d5dd68eed"},
]
[[package]]
@@ -2496,13 +2498,13 @@ files = [
[[package]]
name = "pydantic"
version = "2.10.6"
version = "2.10.5"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.8"
files = [
{file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"},
{file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"},
{file = "pydantic-2.10.5-py3-none-any.whl", hash = "sha256:4dd4e322dbe55472cb7ca7e73f4b63574eecccf2835ffa2af9021ce113c83c53"},
{file = "pydantic-2.10.5.tar.gz", hash = "sha256:278b38dbbaec562011d659ee05f63346951b3a248a6f3642e1bc68894ea2b4ff"},
]
[package.dependencies]
@@ -2710,13 +2712,13 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments
[[package]]
name = "pytest-asyncio"
version = "0.25.3"
version = "0.25.2"
description = "Pytest support for asyncio"
optional = false
python-versions = ">=3.9"
files = [
{file = "pytest_asyncio-0.25.3-py3-none-any.whl", hash = "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3"},
{file = "pytest_asyncio-0.25.3.tar.gz", hash = "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a"},
{file = "pytest_asyncio-0.25.2-py3-none-any.whl", hash = "sha256:0d0bb693f7b99da304a0634afc0a4b19e49d5e0de2d670f38dc4bfa5727c5075"},
{file = "pytest_asyncio-0.25.2.tar.gz", hash = "sha256:3f8ef9a98f45948ea91a0ed3dc4268b5326c0e7bce73892acc654df4262ad45f"},
]
[package.dependencies]
@@ -3047,29 +3049,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "ruff"
version = "0.9.6"
version = "0.9.2"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.9.6-py3-none-linux_armv6l.whl", hash = "sha256:2f218f356dd2d995839f1941322ff021c72a492c470f0b26a34f844c29cdf5ba"},
{file = "ruff-0.9.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b908ff4df65dad7b251c9968a2e4560836d8f5487c2f0cc238321ed951ea0504"},
{file = "ruff-0.9.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b109c0ad2ececf42e75fa99dc4043ff72a357436bb171900714a9ea581ddef83"},
{file = "ruff-0.9.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de4367cca3dac99bcbd15c161404e849bb0bfd543664db39232648dc00112dc"},
{file = "ruff-0.9.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3ee4d7c2c92ddfdaedf0bf31b2b176fa7aa8950efc454628d477394d35638b"},
{file = "ruff-0.9.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dc1edd1775270e6aa2386119aea692039781429f0be1e0949ea5884e011aa8e"},
{file = "ruff-0.9.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4a091729086dffa4bd070aa5dab7e39cc6b9d62eb2bef8f3d91172d30d599666"},
{file = "ruff-0.9.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1bbc6808bf7b15796cef0815e1dfb796fbd383e7dbd4334709642649625e7c5"},
{file = "ruff-0.9.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:589d1d9f25b5754ff230dce914a174a7c951a85a4e9270613a2b74231fdac2f5"},
{file = "ruff-0.9.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc61dd5131742e21103fbbdcad683a8813be0e3c204472d520d9a5021ca8b217"},
{file = "ruff-0.9.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5e2d9126161d0357e5c8f30b0bd6168d2c3872372f14481136d13de9937f79b6"},
{file = "ruff-0.9.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:68660eab1a8e65babb5229a1f97b46e3120923757a68b5413d8561f8a85d4897"},
{file = "ruff-0.9.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c4cae6c4cc7b9b4017c71114115db0445b00a16de3bcde0946273e8392856f08"},
{file = "ruff-0.9.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19f505b643228b417c1111a2a536424ddde0db4ef9023b9e04a46ed8a1cb4656"},
{file = "ruff-0.9.6-py3-none-win32.whl", hash = "sha256:194d8402bceef1b31164909540a597e0d913c0e4952015a5b40e28c146121b5d"},
{file = "ruff-0.9.6-py3-none-win_amd64.whl", hash = "sha256:03482d5c09d90d4ee3f40d97578423698ad895c87314c4de39ed2af945633caa"},
{file = "ruff-0.9.6-py3-none-win_arm64.whl", hash = "sha256:0e2bb706a2be7ddfea4a4af918562fdc1bcb16df255e5fa595bbd800ce322a5a"},
{file = "ruff-0.9.6.tar.gz", hash = "sha256:81761592f72b620ec8fa1068a6fd00e98a5ebee342a3642efd84454f3031dca9"},
{file = "ruff-0.9.2-py3-none-linux_armv6l.whl", hash = "sha256:80605a039ba1454d002b32139e4970becf84b5fee3a3c3bf1c2af6f61a784347"},
{file = "ruff-0.9.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b9aab82bb20afd5f596527045c01e6ae25a718ff1784cb92947bff1f83068b00"},
{file = "ruff-0.9.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fbd337bac1cfa96be615f6efcd4bc4d077edbc127ef30e2b8ba2a27e18c054d4"},
{file = "ruff-0.9.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b35259b0cbf8daa22a498018e300b9bb0174c2bbb7bcba593935158a78054d"},
{file = "ruff-0.9.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b6a9701d1e371bf41dca22015c3f89769da7576884d2add7317ec1ec8cb9c3c"},
{file = "ruff-0.9.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9cc53e68b3c5ae41e8faf83a3b89f4a5d7b2cb666dff4b366bb86ed2a85b481f"},
{file = "ruff-0.9.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8efd9da7a1ee314b910da155ca7e8953094a7c10d0c0a39bfde3fcfd2a015684"},
{file = "ruff-0.9.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3292c5a22ea9a5f9a185e2d131dc7f98f8534a32fb6d2ee7b9944569239c648d"},
{file = "ruff-0.9.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a605fdcf6e8b2d39f9436d343d1f0ff70c365a1e681546de0104bef81ce88df"},
{file = "ruff-0.9.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c547f7f256aa366834829a08375c297fa63386cbe5f1459efaf174086b564247"},
{file = "ruff-0.9.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d18bba3d3353ed916e882521bc3e0af403949dbada344c20c16ea78f47af965e"},
{file = "ruff-0.9.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b338edc4610142355ccf6b87bd356729b62bf1bc152a2fad5b0c7dc04af77bfe"},
{file = "ruff-0.9.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:492a5e44ad9b22a0ea98cf72e40305cbdaf27fac0d927f8bc9e1df316dcc96eb"},
{file = "ruff-0.9.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:af1e9e9fe7b1f767264d26b1075ac4ad831c7db976911fa362d09b2d0356426a"},
{file = "ruff-0.9.2-py3-none-win32.whl", hash = "sha256:71cbe22e178c5da20e1514e1e01029c73dc09288a8028a5d3446e6bba87a5145"},
{file = "ruff-0.9.2-py3-none-win_amd64.whl", hash = "sha256:c5e1d6abc798419cf46eed03f54f2e0c3adb1ad4b801119dedf23fcaf69b55b5"},
{file = "ruff-0.9.2-py3-none-win_arm64.whl", hash = "sha256:a1b63fa24149918f8b37cef2ee6fff81f24f0d74b6f0bdc37bc3e1f2143e41c6"},
{file = "ruff-0.9.2.tar.gz", hash = "sha256:b5eceb334d55fae5f316f783437392642ae18e16dcf4f1858d55d3c2a0f8f5d0"},
]
[[package]]

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "machine-learning"
version = "1.126.1"
version = "1.125.6"
description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md"

View File

@@ -1,7 +1,5 @@
#!/usr/bin/env sh
echo "Initializing Immich ML $IMMICH_SOURCE_REF"
lib_path="/usr/lib/$(arch)-linux-gnu/libmimalloc.so.2"
# mimalloc seems to increase memory usage dramatically with openvino, need to investigate
if ! [ "$DEVICE" = "openvino" ]; then

View File

@@ -77,7 +77,7 @@ custom_lint:
- test/**.dart
# refactor the remaining providers
- lib/providers/{archive,asset,authentication,db,favorite,partner,trash,user}.provider.dart
- lib/providers/{asset_viewer/render_list,backup/backup,search/all_motion_photos,search/recently_added_asset}.provider.dart
- lib/providers/{album/album,album/shared_album,asset_viewer/asset_stack,asset_viewer/render_list,backup/backup,search/all_motion_photos,search/recently_added_asset}.provider.dart
- import_rule_openapi:
message: openapi must only be used through ApiRepositories
@@ -110,81 +110,51 @@ custom_lint:
- test/**.dart
dart_code_metrics:
extends:
- recommended
metrics:
cyclomatic-complexity: 20
number-of-parameters: 4
maximum-nesting-level: 5
rules:
# Common
- arguments-ordering:
last:
- child
- children
- avoid-accessing-collections-by-constant-index
- avoid-accessing-other-classes-private-members
- avoid-assigning-to-static-field
- avoid-assignments-as-conditions
- avoid-async-call-in-sync-function
- avoid-cascade-after-if-null
- avoid-collapsible-if
- avoid-collection-equality-checks
- avoid-complex-loop-conditions
- avoid-declaring-call-method
- avoid-extensions-on-records
- avoid-function-type-in-records
- avoid-future-ignore
- avoid-collection-methods-with-unrelated-types
- avoid-double-slash-imports
- avoid-duplicate-cascades
- avoid-duplicate-patterns
- avoid-generics-shadowing
- avoid-global-state
- avoid-inverted-boolean-checks
- avoid-late-final-reassignment
- avoid-local-functions
- avoid-negated-conditions
- avoid-nested-streams-and-futures
- avoid-referencing-subclasses
- avoid-unnecessary-continue
- avoid-unnecessary-nullable-return-type: false
- binary-expression-operand-order
- move-variable-outside-iteration
- pattern-fields-ordering
- prefer-abstract-final-static-class
- prefer-commenting-future-delayed
- prefer-early-return
- prefer-first
- prefer-immediate-return
- prefer-last
- prefer-simpler-boolean-expressions
- prefer-switch-expression
- prefer-type-over-var
- use-existing-destructuring
- use-existing-variable
# Flutter
- add-copy-with:
file-name-pattern: '.model.dart'
- always-remove-listener
- avoid-border-all
- avoid-complex-arithmetic-expressions
- avoid-empty-setstate
- avoid-expanded-as-spacer
- avoid-if-with-many-branches
- avoid-incomplete-copy-with
- avoid-inherited-widget-in-initstate
- avoid-late-context
- avoid-recursive-widget-calls
- avoid-returning-widgets
- avoid-shrink-wrap-in-lists
- avoid-single-child-column-or-row
- avoid-state-constructors
- avoid-stateless-widget-initialized-fields
- avoid-unnecessary-overrides-in-state
- avoid-unnecessary-stateful-widgets
- avoid-wrapping-in-padding
- prefer-align-over-container
- dispose-fields
- prefer-const-border-radius
- prefer-correct-callback-field-name: false
- prefer-correct-edge-insets-constructor
- prefer-dedicated-media-query-methods
- prefer-define-hero-tag
- prefer-extracting-callbacks
- prefer-for-loop-in-children
- prefer-match-file-name: false
- prefer-single-widget-per-file:
ignore-private-widgets: true
- prefer-sliver-prefix
- prefer-spacing
- prefer-text-rich
- prefer-transform-over-container
- prefer-using-list-view
- prefer-widget-private-members:
ignore-static: true
- use-closest-build-context
# riverpod
- avoid-calling-notifier-members-inside-build
- avoid-notifier-constructors
- avoid-ref-read-inside-build
- avoid-ref-watch-outside-build
- avoid-unnecessary-consumer-widgets
- dispose-provided-instances
- use-ref-read-synchronously
- proper-super-calls
- use-setstate-synchronously

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 184,
"android.injected.version.name" => "1.126.1",
"android.injected.version.code" => 182,
"android.injected.version.name" => "1.125.6",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')

View File

@@ -1,10 +1,4 @@
{
"search_filter_contextual": "Search by context",
"search_filter_filename": "Search by file name",
"search_filter_description": "Search by description",
"search_no_result": "No results found, try a different search term or combination",
"description_search": "Hiking day in Sapa",
"search_no_more_result": "No more results",
"action_common_back": "Back",
"action_common_cancel": "Cancel",
"action_common_clear": "Clear",
@@ -288,9 +282,9 @@
"header_settings_field_validator_msg": "Value cannot be empty",
"header_settings_header_name_input": "Header name",
"header_settings_header_value_input": "Header value",
"header_settings_page_title": "Proxy Headers",
"header_settings_page_title": "Proxy Headers (EXPERIMENTAL)",
"headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request",
"headers_settings_tile_title": "Custom proxy headers (EXPERIMENTAL)",
"headers_settings_tile_title": "Custom proxy headers",
"home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.",
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
"home_page_add_to_album_success": "Added {added} assets to album {album}.",
@@ -482,7 +476,6 @@
"search_filter_media_type_video": "Video",
"search_filter_people": "People",
"search_filter_people_title": "Select people",
"search_filter_people_hint": "Filter people",
"search_page_categories": "Categories",
"search_page_favorites": "Favorites",
"search_page_motion_photos": "Motion Photos",

View File

@@ -1,5 +1,5 @@
import 'package:analyzer/error/error.dart' show ErrorSeverity;
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/error/error.dart' show ErrorSeverity;
import 'package:custom_lint_builder/custom_lint_builder.dart';
// ignore: depend_on_referenced_packages
import 'package:glob/glob.dart';
@@ -65,8 +65,7 @@ class ImportRule extends DartLintRule {
) {
if (_rootOffset == -1) {
const project = "/immich/mobile/";
_rootOffset =
resolver.path.toLowerCase().indexOf(project) + project.length;
_rootOffset = resolver.path.indexOf(project) + project.length;
}
final path = resolver.path.substring(_rootOffset);

View File

@@ -5,7 +5,7 @@ environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
analyzer: ^6.0.0
analyzer: ^7.0.0
analyzer_plugin: ^0.11.3
custom_lint_builder: ^0.6.4
glob: ^2.1.2

View File

@@ -541,7 +541,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 194;
CURRENT_PROJECT_VERSION = 190;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
@@ -685,7 +685,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 194;
CURRENT_PROJECT_VERSION = 190;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
@@ -715,7 +715,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 194;
CURRENT_PROJECT_VERSION = 190;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
@@ -748,7 +748,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 194;
CURRENT_PROJECT_VERSION = 190;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -791,7 +791,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 194;
CURRENT_PROJECT_VERSION = 190;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -831,7 +831,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 194;
CURRENT_PROJECT_VERSION = 190;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;

View File

@@ -78,7 +78,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.126.1</string>
<string>1.125.2</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@@ -93,7 +93,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>194</string>
<string>190</string>
<key>FLTEnableImpeller</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key>

View File

@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Release"
lane :release do
increment_version_number(
version_number: "1.126.1"
version_number: "1.125.6"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,

View File

@@ -2,9 +2,3 @@ enum SortOrder {
asc,
desc,
}
enum TextSearchType {
context,
filename,
description,
}

View File

@@ -545,13 +545,19 @@ enum AssetType {
}
extension AssetTypeEnumHelper on AssetTypeEnum {
AssetType toAssetType() => switch (this) {
AssetTypeEnum.IMAGE => AssetType.image,
AssetTypeEnum.VIDEO => AssetType.video,
AssetTypeEnum.AUDIO => AssetType.audio,
AssetTypeEnum.OTHER => AssetType.other,
_ => throw Exception(),
};
AssetType toAssetType() {
switch (this) {
case AssetTypeEnum.IMAGE:
return AssetType.image;
case AssetTypeEnum.VIDEO:
return AssetType.video;
case AssetTypeEnum.AUDIO:
return AssetType.audio;
case AssetTypeEnum.OTHER:
return AssetType.other;
}
throw Exception();
}
}
/// Describes where the information of this asset came from:

View File

@@ -96,16 +96,25 @@ class StoreValue {
int? intValue;
String? strValue;
T? _extract<T>(StoreKey<T> key) => switch (key.type) {
const (int) => intValue as T?,
const (bool) => intValue == null ? null : (intValue! == 1) as T,
const (DateTime) => intValue == null
T? _extract<T>(StoreKey<T> key) {
switch (key.type) {
case const (int):
return intValue as T?;
case const (bool):
return intValue == null ? null : (intValue! == 1) as T;
case const (DateTime):
return intValue == null
? null
: DateTime.fromMicrosecondsSinceEpoch(intValue!) as T,
const (String) => strValue as T?,
_ when key.fromDb != null => key.fromDb!.call(Store._db, intValue!),
_ => throw TypeError(),
};
: DateTime.fromMicrosecondsSinceEpoch(intValue!) as T;
case const (String):
return strValue as T?;
default:
if (key.fromDb != null) {
return key.fromDb!.call(Store._db, intValue!);
}
}
throw TypeError();
}
static Future<StoreValue> _of<T>(T? value, StoreKey<T> key) async {
int? i;

View File

@@ -149,33 +149,56 @@ enum AvatarColorEnum {
}
extension AvatarColorEnumHelper on UserAvatarColor {
AvatarColorEnum toAvatarColor() => switch (this) {
UserAvatarColor.primary => AvatarColorEnum.primary,
UserAvatarColor.pink => AvatarColorEnum.pink,
UserAvatarColor.red => AvatarColorEnum.red,
UserAvatarColor.yellow => AvatarColorEnum.yellow,
UserAvatarColor.blue => AvatarColorEnum.blue,
UserAvatarColor.green => AvatarColorEnum.green,
UserAvatarColor.purple => AvatarColorEnum.purple,
UserAvatarColor.orange => AvatarColorEnum.orange,
UserAvatarColor.gray => AvatarColorEnum.gray,
UserAvatarColor.amber => AvatarColorEnum.amber,
_ => AvatarColorEnum.primary,
};
AvatarColorEnum toAvatarColor() {
switch (this) {
case UserAvatarColor.primary:
return AvatarColorEnum.primary;
case UserAvatarColor.pink:
return AvatarColorEnum.pink;
case UserAvatarColor.red:
return AvatarColorEnum.red;
case UserAvatarColor.yellow:
return AvatarColorEnum.yellow;
case UserAvatarColor.blue:
return AvatarColorEnum.blue;
case UserAvatarColor.green:
return AvatarColorEnum.green;
case UserAvatarColor.purple:
return AvatarColorEnum.purple;
case UserAvatarColor.orange:
return AvatarColorEnum.orange;
case UserAvatarColor.gray:
return AvatarColorEnum.gray;
case UserAvatarColor.amber:
return AvatarColorEnum.amber;
}
return AvatarColorEnum.primary;
}
}
extension AvatarColorToColorHelper on AvatarColorEnum {
Color toColor([bool isDarkTheme = false]) => switch (this) {
AvatarColorEnum.primary =>
isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF),
AvatarColorEnum.pink => const Color.fromARGB(255, 244, 114, 182),
AvatarColorEnum.red => const Color.fromARGB(255, 239, 68, 68),
AvatarColorEnum.yellow => const Color.fromARGB(255, 234, 179, 8),
AvatarColorEnum.blue => const Color.fromARGB(255, 59, 130, 246),
AvatarColorEnum.green => const Color.fromARGB(255, 22, 163, 74),
AvatarColorEnum.purple => const Color.fromARGB(255, 147, 51, 234),
AvatarColorEnum.orange => const Color.fromARGB(255, 234, 88, 12),
AvatarColorEnum.gray => const Color.fromARGB(255, 75, 85, 99),
AvatarColorEnum.amber => const Color.fromARGB(255, 217, 119, 6),
};
Color toColor([bool isDarkTheme = false]) {
switch (this) {
case AvatarColorEnum.primary:
return isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF);
case AvatarColorEnum.pink:
return const Color.fromARGB(255, 244, 114, 182);
case AvatarColorEnum.red:
return const Color.fromARGB(255, 239, 68, 68);
case AvatarColorEnum.yellow:
return const Color.fromARGB(255, 234, 179, 8);
case AvatarColorEnum.blue:
return const Color.fromARGB(255, 59, 130, 246);
case AvatarColorEnum.green:
return const Color.fromARGB(255, 22, 163, 74);
case AvatarColorEnum.purple:
return const Color.fromARGB(255, 147, 51, 234);
case AvatarColorEnum.orange:
return const Color.fromARGB(255, 234, 88, 12);
case AvatarColorEnum.gray:
return const Color.fromARGB(255, 75, 85, 99);
case AvatarColorEnum.amber:
return const Color.fromARGB(255, 217, 119, 6);
}
}
}

View File

@@ -3,7 +3,6 @@ import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/database.interface.dart';
import 'package:immich_mobile/models/albums/album_search.model.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
abstract interface class IAlbumRepository implements IDatabaseRepository {
Future<Album> create(Album album);
@@ -43,16 +42,6 @@ abstract interface class IAlbumRepository implements IDatabaseRepository {
Future<Album> recalculateMetadata(Album album);
Future<List<Album>> search(String searchTerm, QuickFilterMode filterMode);
Stream<List<Album>> watchRemoteAlbums();
Stream<List<Album>> watchLocalAlbums();
Stream<Album?> watchAlbum(int id);
Stream<RenderList> getRenderListStream(Album album);
Future<void> clearTable();
}
enum AlbumSort { remoteId, localId }

View File

@@ -41,7 +41,7 @@ abstract interface class IAssetRepository implements IDatabaseRepository {
Future<void> deleteAllByRemoteId(List<String> ids, {AssetState? state});
Future<void> deleteByIds(List<int> ids);
Future<void> deleteById(List<int> ids);
Future<List<Asset>> getMatches({
required List<Asset> assets,
@@ -57,12 +57,6 @@ abstract interface class IAssetRepository implements IDatabaseRepository {
Future<void> upsertDuplicatedAssets(Iterable<String> duplicatedAssets);
Future<List<String>> getAllDuplicatedAssetIds();
Future<List<Asset>> getStackAssets(String stackId);
Future<void> clearTable();
Stream<Asset?> watchAsset(int id, {bool fireImmediately = false});
}
enum AssetSort { checksum, ownerIdChecksum }

View File

@@ -11,6 +11,4 @@ abstract interface class IETagRepository implements IDatabaseRepository {
Future<void> upsertAll(List<ETag> etags);
Future<void> deleteByIds(List<String> ids);
Future<void> clearTable();
}

View File

@@ -9,6 +9,4 @@ abstract interface class IExifInfoRepository implements IDatabaseRepository {
Future<List<ExifInfo>> updateAll(List<ExifInfo> exifInfos);
Future<void> delete(int id);
Future<void> clearTable();
}

View File

@@ -1,6 +1,3 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';
abstract interface class IPersonApiRepository {
Future<List<Person>> getAll();
Future<Person> update(String id, {String? name});
@@ -9,10 +6,10 @@ abstract interface class IPersonApiRepository {
class Person {
Person({
required this.id,
this.birthDate,
required this.isHidden,
required this.name,
required this.thumbnailPath,
this.birthDate,
this.updatedAt,
});
@@ -22,80 +19,4 @@ class Person {
final String name;
final String thumbnailPath;
final DateTime? updatedAt;
@override
String toString() {
return 'Person(id: $id, birthDate: $birthDate, isHidden: $isHidden, name: $name, thumbnailPath: $thumbnailPath, updatedAt: $updatedAt)';
}
Person copyWith({
String? id,
DateTime? birthDate,
bool? isHidden,
String? name,
String? thumbnailPath,
DateTime? updatedAt,
}) {
return Person(
id: id ?? this.id,
birthDate: birthDate ?? this.birthDate,
isHidden: isHidden ?? this.isHidden,
name: name ?? this.name,
thumbnailPath: thumbnailPath ?? this.thumbnailPath,
updatedAt: updatedAt ?? this.updatedAt,
);
}
Map<String, dynamic> toMap() {
return <String, dynamic>{
'id': id,
'birthDate': birthDate?.millisecondsSinceEpoch,
'isHidden': isHidden,
'name': name,
'thumbnailPath': thumbnailPath,
'updatedAt': updatedAt?.millisecondsSinceEpoch,
};
}
factory Person.fromMap(Map<String, dynamic> map) {
return Person(
id: map['id'] as String,
birthDate: map['birthDate'] != null
? DateTime.fromMillisecondsSinceEpoch(map['birthDate'] as int)
: null,
isHidden: map['isHidden'] as bool,
name: map['name'] as String,
thumbnailPath: map['thumbnailPath'] as String,
updatedAt: map['updatedAt'] != null
? DateTime.fromMillisecondsSinceEpoch(map['updatedAt'] as int)
: null,
);
}
String toJson() => json.encode(toMap());
factory Person.fromJson(String source) =>
Person.fromMap(json.decode(source) as Map<String, dynamic>);
@override
bool operator ==(covariant Person other) {
if (identical(this, other)) return true;
return other.id == id &&
other.birthDate == birthDate &&
other.isHidden == isHidden &&
other.name == name &&
other.thumbnailPath == thumbnailPath &&
other.updatedAt == updatedAt;
}
@override
int get hashCode {
return id.hashCode ^
birthDate.hashCode ^
isHidden.hashCode ^
name.hashCode ^
thumbnailPath.hashCode ^
updatedAt.hashCode;
}
}

View File

@@ -18,8 +18,6 @@ abstract interface class IUserRepository implements IDatabaseRepository {
Future<void> deleteById(List<int> ids);
Future<User> me();
Future<void> clearTable();
}
enum UserSort { id }

View File

@@ -235,7 +235,6 @@ class SearchDisplayFilters {
class SearchFilter {
String? context;
String? filename;
String? description;
Set<Person> people;
SearchLocationFilter location;
SearchCameraFilter camera;
@@ -248,7 +247,6 @@ class SearchFilter {
SearchFilter({
this.context,
this.filename,
this.description,
required this.people,
required this.location,
required this.camera,
@@ -257,28 +255,9 @@ class SearchFilter {
required this.mediaType,
});
bool get isEmpty {
return (context == null || (context != null && context!.isEmpty)) &&
(filename == null || (filename!.isEmpty)) &&
(description == null || (description!.isEmpty)) &&
people.isEmpty &&
location.country == null &&
location.state == null &&
location.city == null &&
camera.make == null &&
camera.model == null &&
date.takenBefore == null &&
date.takenAfter == null &&
display.isNotInAlbum == false &&
display.isArchive == false &&
display.isFavorite == false &&
mediaType == AssetType.other;
}
SearchFilter copyWith({
String? context,
String? filename,
String? description,
Set<Person>? people,
SearchLocationFilter? location,
SearchCameraFilter? camera,
@@ -289,7 +268,6 @@ class SearchFilter {
return SearchFilter(
context: context ?? this.context,
filename: filename ?? this.filename,
description: description ?? this.description,
people: people ?? this.people,
location: location ?? this.location,
camera: camera ?? this.camera,
@@ -301,7 +279,7 @@ class SearchFilter {
@override
String toString() {
return 'SearchFilter(context: $context, filename: $filename, description: $description, people: $people, location: $location, camera: $camera, date: $date, display: $display, mediaType: $mediaType)';
return 'SearchFilter(context: $context, filename: $filename, people: $people, location: $location, camera: $camera, date: $date, display: $display, mediaType: $mediaType)';
}
@override
@@ -310,7 +288,6 @@ class SearchFilter {
return other.context == context &&
other.filename == filename &&
other.description == description &&
other.people == people &&
other.location == location &&
other.camera == camera &&
@@ -323,7 +300,6 @@ class SearchFilter {
int get hashCode {
return context.hashCode ^
filename.hashCode ^
description.hashCode ^
people.hashCode ^
location.hashCode ^
camera.hashCode ^

View File

@@ -36,19 +36,32 @@ class AppLogPage extends HookConsumerWidget {
);
}
Widget buildLeadingIcon(LogLevel level) => switch (level) {
LogLevel.INFO => colorStatusIndicator(context.primaryColor),
LogLevel.SEVERE => colorStatusIndicator(Colors.redAccent),
LogLevel.WARNING => colorStatusIndicator(Colors.orangeAccent),
_ => colorStatusIndicator(Colors.grey),
};
Widget buildLeadingIcon(LogLevel level) {
switch (level) {
case LogLevel.INFO:
return colorStatusIndicator(context.primaryColor);
case LogLevel.SEVERE:
return colorStatusIndicator(Colors.redAccent);
Color getTileColor(LogLevel level) => switch (level) {
LogLevel.INFO => Colors.transparent,
LogLevel.SEVERE => Colors.redAccent.withOpacity(0.25),
LogLevel.WARNING => Colors.orangeAccent.withOpacity(0.25),
_ => context.primaryColor.withOpacity(0.1),
};
case LogLevel.WARNING:
return colorStatusIndicator(Colors.orangeAccent);
default:
return colorStatusIndicator(Colors.grey);
}
}
getTileColor(LogLevel level) {
switch (level) {
case LogLevel.INFO:
return Colors.transparent;
case LogLevel.SEVERE:
return Colors.redAccent.withOpacity(0.25);
case LogLevel.WARNING:
return Colors.orangeAccent.withOpacity(0.25);
default:
return context.primaryColor.withOpacity(0.1);
}
}
return Scaffold(
appBar: AppBar(

View File

@@ -74,16 +74,26 @@ class DownloadTaskTile extends StatelessWidget {
Widget build(BuildContext context) {
final progressPercent = (progress * 100).round();
String getStatusText() => switch (status) {
TaskStatus.running => 'downloading'.tr(),
TaskStatus.complete => 'download_complete'.tr(),
TaskStatus.failed => 'download_failed'.tr(),
TaskStatus.canceled => 'download_canceled'.tr(),
TaskStatus.paused => 'download_paused'.tr(),
TaskStatus.enqueued => 'download_enqueue'.tr(),
TaskStatus.notFound => 'download_notfound'.tr(),
TaskStatus.waitingToRetry => 'download_waiting_to_retry'.tr(),
};
getStatusText() {
switch (status) {
case TaskStatus.running:
return 'downloading'.tr();
case TaskStatus.complete:
return 'download_complete'.tr();
case TaskStatus.failed:
return 'download_failed'.tr();
case TaskStatus.canceled:
return 'download_canceled'.tr();
case TaskStatus.paused:
return 'download_paused'.tr();
case TaskStatus.enqueued:
return 'download_enqueue'.tr();
case TaskStatus.notFound:
return 'download_notfound'.tr();
case TaskStatus.waitingToRetry:
return 'download_waiting_to_retry'.tr();
}
}
return SizedBox(
key: const ValueKey('download_progress'),

View File

@@ -16,8 +16,6 @@ class LargeLeadingTile extends StatelessWidget {
this.trailing,
this.selected = false,
this.disabled = false,
this.selectedTileColor,
this.tileColor,
});
final Widget leading;
@@ -29,9 +27,6 @@ class LargeLeadingTile extends StatelessWidget {
final Widget? trailing;
final bool selected;
final bool disabled;
final Color? selectedTileColor;
final Color? tileColor;
@override
Widget build(BuildContext context) {
return InkWell(
@@ -40,9 +35,8 @@ class LargeLeadingTile extends StatelessWidget {
child: Container(
decoration: BoxDecoration(
color: selected
? selectedTileColor ??
Theme.of(context).primaryColor.withAlpha(30)
: tileColor ?? Colors.transparent,
? Theme.of(context).primaryColor.withAlpha(30)
: Colors.transparent,
borderRadius: BorderRadius.circular(borderRadius),
),
child: Row(

View File

@@ -26,7 +26,6 @@ import 'package:wakelock_plus/wakelock_plus.dart';
class NativeVideoViewerPage extends HookConsumerWidget {
final Asset asset;
final bool showControls;
final int playbackDelayFactor;
final Widget image;
const NativeVideoViewerPage({
@@ -34,7 +33,6 @@ class NativeVideoViewerPage extends HookConsumerWidget {
required this.asset,
required this.image,
this.showControls = true,
this.playbackDelayFactor = 1,
});
@override
@@ -319,16 +317,12 @@ class NativeVideoViewerPage extends HookConsumerWidget {
}
// Delay the video playback to avoid a stutter in the swipe animation
// Note, in some circumstances a longer delay is needed (eg: memories),
// the playbackDelayFactor can be used for this
// This delay seems like a hacky way to resolve underlying bugs in video
// playback, but other resolutions failed thus far
Timer(
Platform.isIOS
? Duration(milliseconds: 300 * playbackDelayFactor)
? const Duration(milliseconds: 300)
: imageToVideo
? Duration(milliseconds: 200 * playbackDelayFactor)
: Duration(milliseconds: 400 * playbackDelayFactor), () {
? const Duration(milliseconds: 200)
: const Duration(milliseconds: 400), () {
if (!context.mounted) {
return;
}

View File

@@ -20,8 +20,6 @@ class TabControllerPage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final isRefreshingAssets = ref.watch(assetProvider);
final isRefreshingRemoteAlbums = ref.watch(isRefreshingRemoteAlbumProvider);
final isScreenLandscape =
MediaQuery.orientationOf(context) == Orientation.landscape;
Widget buildIcon({required Widget icon, required bool isProcessing}) {
if (!isProcessing) return icon;
@@ -47,7 +45,7 @@ class TabControllerPage extends HookConsumerWidget {
);
}
void onNavigationSelected(TabsRouter router, int index) {
onNavigationSelected(TabsRouter router, int index) {
// On Photos page menu tapped
if (router.activeIndex == 0 && index == 0) {
scrollToTopNotifierProvider.scrollToTop();
@@ -63,82 +61,62 @@ class TabControllerPage extends HookConsumerWidget {
ref.read(tabProvider.notifier).state = TabEnum.values[index];
}
final navigationDestinations = [
NavigationDestination(
label: 'tab_controller_nav_photos'.tr(),
icon: const Icon(
Icons.photo_library_outlined,
),
selectedIcon: buildIcon(
isProcessing: isRefreshingAssets,
icon: Icon(
Icons.photo_library,
color: context.primaryColor,
),
),
),
NavigationDestination(
label: 'tab_controller_nav_search'.tr(),
icon: const Icon(
Icons.search_rounded,
),
selectedIcon: Icon(
Icons.search,
color: context.primaryColor,
),
),
NavigationDestination(
label: 'albums'.tr(),
icon: const Icon(
Icons.photo_album_outlined,
),
selectedIcon: buildIcon(
isProcessing: isRefreshingRemoteAlbums,
icon: Icon(
Icons.photo_album_rounded,
color: context.primaryColor,
),
),
),
NavigationDestination(
label: 'library'.tr(),
icon: const Icon(
Icons.space_dashboard_outlined,
),
selectedIcon: buildIcon(
isProcessing: isRefreshingAssets,
icon: Icon(
Icons.space_dashboard_rounded,
color: context.primaryColor,
),
),
),
];
Widget bottomNavigationBar(TabsRouter tabsRouter) {
bottomNavigationBar(TabsRouter tabsRouter) {
return NavigationBar(
selectedIndex: tabsRouter.activeIndex,
onDestinationSelected: (index) =>
onNavigationSelected(tabsRouter, index),
destinations: navigationDestinations,
);
}
Widget navigationRail(TabsRouter tabsRouter) {
return NavigationRail(
destinations: navigationDestinations
.map(
(e) => NavigationRailDestination(
icon: e.icon,
label: Text(e.label),
destinations: [
NavigationDestination(
label: 'tab_controller_nav_photos'.tr(),
icon: const Icon(
Icons.photo_library_outlined,
),
selectedIcon: buildIcon(
isProcessing: isRefreshingAssets,
icon: Icon(
Icons.photo_library,
color: context.primaryColor,
),
)
.toList(),
onDestinationSelected: (index) =>
onNavigationSelected(tabsRouter, index),
selectedIndex: tabsRouter.activeIndex,
labelType: NavigationRailLabelType.all,
groupAlignment: 0.0,
),
),
NavigationDestination(
label: 'tab_controller_nav_search'.tr(),
icon: const Icon(
Icons.search_rounded,
),
selectedIcon: Icon(
Icons.search,
color: context.primaryColor,
),
),
NavigationDestination(
label: 'albums'.tr(),
icon: const Icon(
Icons.photo_album_outlined,
),
selectedIcon: buildIcon(
isProcessing: isRefreshingRemoteAlbums,
icon: Icon(
Icons.photo_album_rounded,
color: context.primaryColor,
),
),
),
NavigationDestination(
label: 'library'.tr(),
icon: const Icon(
Icons.space_dashboard_outlined,
),
selectedIcon: buildIcon(
isProcessing: isRefreshingAssets,
icon: Icon(
Icons.space_dashboard_rounded,
color: context.primaryColor,
),
),
),
],
);
}
@@ -157,27 +135,17 @@ class TabControllerPage extends HookConsumerWidget {
),
builder: (context, child) {
final tabsRouter = AutoTabsRouter.of(context);
final heroedChild = HeroControllerScope(
controller: HeroController(),
child: child,
);
return PopScope(
canPop: tabsRouter.activeIndex == 0,
onPopInvokedWithResult: (didPop, _) =>
!didPop ? tabsRouter.setActiveIndex(0) : null,
child: Scaffold(
body: isScreenLandscape
? Row(
children: [
navigationRail(tabsRouter),
const VerticalDivider(),
Expanded(child: heroedChild),
],
)
: heroedChild,
bottomNavigationBar: multiselectEnabled || isScreenLandscape
? null
: bottomNavigationBar(tabsRouter),
body: HeroControllerScope(
controller: HeroController(),
child: child,
),
bottomNavigationBar:
multiselectEnabled ? null : bottomNavigationBar(tabsRouter),
),
);
},

View File

@@ -174,19 +174,33 @@ class _AspectRatioButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
IconData iconData;
switch (label) {
case 'Free':
iconData = Icons.crop_free_rounded;
break;
case '1:1':
iconData = Icons.crop_square_rounded;
break;
case '16:9':
iconData = Icons.crop_16_9_rounded;
break;
case '3:2':
iconData = Icons.crop_3_2_rounded;
break;
case '7:5':
iconData = Icons.crop_7_5_rounded;
break;
default:
iconData = Icons.crop_free_rounded;
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(
switch (label) {
'Free' => Icons.crop_free_rounded,
'1:1' => Icons.crop_square_rounded,
'16:9' => Icons.crop_16_9_rounded,
'3:2' => Icons.crop_3_2_rounded,
'7:5' => Icons.crop_7_5_rounded,
_ => Icons.crop_free_rounded,
},
iconData,
color: aspectRatio.value == ratio
? context.primaryColor
: context.themeData.iconTheme.color,

View File

@@ -136,16 +136,23 @@ class PermissionOnboardingPage extends HookConsumerWidget {
);
}
final Widget child = switch (permission) {
PermissionStatus.limited => buildPermissionLimited(),
PermissionStatus.denied => buildRequestPermission(),
PermissionStatus.granted ||
PermissionStatus.provisional =>
buildPermissionGranted(),
PermissionStatus.restricted ||
PermissionStatus.permanentlyDenied =>
buildPermissionDenied()
};
final Widget child;
switch (permission) {
case PermissionStatus.limited:
child = buildPermissionLimited();
break;
case PermissionStatus.denied:
child = buildRequestPermission();
break;
case PermissionStatus.granted:
case PermissionStatus.provisional:
child = buildPermissionGranted();
break;
case PermissionStatus.restricted:
case PermissionStatus.permanentlyDenied:
child = buildPermissionDenied();
break;
}
return Scaffold(
body: SafeArea(

View File

@@ -5,8 +5,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/models/memories/memory.model.dart';
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/widgets/common/immich_image.dart';
import 'package:immich_mobile/widgets/memories/memory_bottom_info.dart';
@@ -15,8 +13,6 @@ import 'package:immich_mobile/widgets/memories/memory_epilogue.dart';
import 'package:immich_mobile/widgets/memories/memory_progress_indicator.dart';
@RoutePage()
/// Expects [currentAssetProvider] to be set before navigating to this page
class MemoryPage extends HookConsumerWidget {
final List<Memory> memories;
final int memoryIndex;
@@ -36,7 +32,6 @@ class MemoryPage extends HookConsumerWidget {
"${currentAssetPage.value + 1}|${currentMemory.value.assets.length}",
);
const bgColor = Colors.black;
final currentAsset = useState<Asset?>(null);
/// The list of all of the asset page controllers
final memoryAssetPageControllers =
@@ -140,14 +135,6 @@ class MemoryPage extends HookConsumerWidget {
ref.read(hapticFeedbackProvider.notifier).selectionClick();
currentAssetPage.value = otherIndex;
updateProgressText();
final asset = currentMemory.value.assets[otherIndex];
currentAsset.value = asset;
ref.read(currentAssetProvider.notifier).set(asset);
if (asset.isVideo || asset.isMotionPhoto) {
ref.read(videoPlaybackValueProvider.notifier).reset();
}
// Wait for page change animation to finish
await Future.delayed(const Duration(milliseconds: 400));
// And then precache the next asset
@@ -287,16 +274,6 @@ class MemoryPage extends HookConsumerWidget {
),
),
),
if (currentAsset.value != null &&
currentAsset.value!.isVideo)
Positioned(
bottom: 24,
right: 32,
child: Icon(
Icons.videocam_outlined,
color: Colors.grey[200],
),
),
],
),
),

View File

@@ -5,7 +5,6 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
@@ -32,8 +31,7 @@ class SearchPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final textSearchType = useState<TextSearchType>(TextSearchType.context);
final searchHintText = useState<String>('contextual_search'.tr());
final isContextualSearch = useState(true);
final textSearchController = useTextEditingController();
final filter = useState<SearchFilter>(
SearchFilter(
@@ -51,7 +49,7 @@ class SearchPage extends HookConsumerWidget {
),
);
final previousFilter = useState<SearchFilter?>(null);
final previousFilter = useState(filter.value);
final peopleCurrentFilterWidget = useState<Widget?>(null);
final dateRangeCurrentFilterWidget = useState<Widget?>(null);
@@ -62,55 +60,19 @@ class SearchPage extends HookConsumerWidget {
final isSearching = useState(false);
SnackBar searchInfoSnackBar(String message) {
return SnackBar(
content: Text(
message,
style: context.textTheme.labelLarge,
),
showCloseIcon: true,
behavior: SnackBarBehavior.fixed,
closeIconColor: context.colorScheme.onSurface,
);
}
search() async {
if (filter.value.isEmpty) {
return;
}
if (prefilter == null && filter.value == previousFilter.value) {
return;
}
if (prefilter == null && filter.value == previousFilter.value) return;
isSearching.value = true;
ref.watch(paginatedSearchProvider.notifier).clear();
final hasResult = await ref
.watch(paginatedSearchProvider.notifier)
.search(filter.value);
if (!hasResult) {
context.showSnackBar(
searchInfoSnackBar('search_no_result'.tr()),
);
}
await ref.watch(paginatedSearchProvider.notifier).search(filter.value);
previousFilter.value = filter.value;
isSearching.value = false;
}
loadMoreSearchResult() async {
isSearching.value = true;
final hasResult = await ref
.watch(paginatedSearchProvider.notifier)
.search(filter.value);
if (!hasResult) {
context.showSnackBar(
searchInfoSnackBar('search_no_more_result'.tr()),
);
}
await ref.watch(paginatedSearchProvider.notifier).search(filter.value);
isSearching.value = false;
}
@@ -480,148 +442,37 @@ class SearchPage extends HookConsumerWidget {
}
handleTextSubmitted(String value) {
switch (textSearchType.value) {
case TextSearchType.context:
filter.value = filter.value.copyWith(
filename: '',
context: value,
description: '',
);
break;
case TextSearchType.filename:
filter.value = filter.value.copyWith(
filename: value,
context: '',
description: '',
);
break;
case TextSearchType.description:
filter.value = filter.value.copyWith(
filename: '',
context: '',
description: value,
);
break;
if (isContextualSearch.value) {
filter.value = filter.value.copyWith(
filename: '',
context: value,
);
} else {
filter.value = filter.value.copyWith(
filename: value,
context: '',
);
}
search();
}
IconData getSearchPrefixIcon() {
switch (textSearchType.value) {
case TextSearchType.context:
return Icons.image_search_rounded;
case TextSearchType.filename:
return Icons.abc_rounded;
case TextSearchType.description:
return Icons.text_snippet_outlined;
default:
return Icons.search_rounded;
}
}
return Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar(
automaticallyImplyLeading: true,
actions: [
Padding(
padding: const EdgeInsets.only(right: 16.0),
child: MenuAnchor(
style: MenuStyle(
elevation: const WidgetStatePropertyAll(1),
shape: WidgetStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
),
padding: const WidgetStatePropertyAll(
EdgeInsets.all(4),
),
),
builder: (
BuildContext context,
MenuController controller,
Widget? child,
) {
return IconButton(
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
icon: const Icon(Icons.more_vert_rounded),
tooltip: 'Show text search menu',
);
padding: const EdgeInsets.only(right: 14.0),
child: IconButton(
key: const Key('contextual_search_button'),
icon: isContextualSearch.value
? const Icon(Icons.abc_rounded)
: const Icon(Icons.image_search_rounded),
onPressed: () {
isContextualSearch.value = !isContextualSearch.value;
textSearchController.clear();
},
menuChildren: [
MenuItemButton(
child: ListTile(
leading: const Icon(Icons.image_search_rounded),
title: Text(
'search_filter_contextual'.tr(),
style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
color: textSearchType.value == TextSearchType.context
? context.colorScheme.primary
: null,
),
),
selectedColor: context.colorScheme.primary,
selected: textSearchType.value == TextSearchType.context,
),
onPressed: () {
textSearchType.value = TextSearchType.context;
searchHintText.value = 'contextual_search'.tr();
},
),
MenuItemButton(
child: ListTile(
leading: const Icon(Icons.abc_rounded),
title: Text(
'search_filter_filename'.tr(),
style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
color: textSearchType.value == TextSearchType.filename
? context.colorScheme.primary
: null,
),
),
selectedColor: context.colorScheme.primary,
selected: textSearchType.value == TextSearchType.filename,
),
onPressed: () {
textSearchType.value = TextSearchType.filename;
searchHintText.value = 'filename_search'.tr();
},
),
MenuItemButton(
child: ListTile(
leading: const Icon(Icons.text_snippet_outlined),
title: Text(
'search_filter_description'.tr(),
style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
color:
textSearchType.value == TextSearchType.description
? context.colorScheme.primary
: null,
),
),
selectedColor: context.colorScheme.primary,
selected:
textSearchType.value == TextSearchType.description,
),
onPressed: () {
textSearchType.value = TextSearchType.description;
searchHintText.value = 'description_search'.tr();
},
),
],
),
),
],
@@ -652,10 +503,12 @@ class SearchPage extends HookConsumerWidget {
prefixIcon: prefilter != null
? null
: Icon(
getSearchPrefixIcon(),
Icons.search_rounded,
color: context.colorScheme.primary,
),
hintText: searchHintText.value,
hintText: isContextualSearch.value
? 'contextual_search'.tr()
: 'filename_search'.tr(),
hintStyle: context.textTheme.bodyLarge?.copyWith(
color: context.themeData.colorScheme.onSurfaceSecondary,
),
@@ -743,15 +596,10 @@ class SearchPage extends HookConsumerWidget {
),
),
),
if (isSearching.value)
const Expanded(
child: Center(child: CircularProgressIndicator.adaptive()),
)
else
SearchResultGrid(
onScrollEnd: loadMoreSearchResult,
isSearching: isSearching.value,
),
SearchResultGrid(
onScrollEnd: loadMoreSearchResult,
isSearching: isSearching.value,
),
],
),
);

View File

@@ -8,40 +8,43 @@ import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/utils/renderlist_generator.dart';
import 'package:isar/isar.dart';
final isRefreshingRemoteAlbumProvider = StateProvider<bool>((ref) => false);
class AlbumNotifier extends StateNotifier<List<Album>> {
AlbumNotifier(this.albumService, this.ref) : super([]) {
albumService.getAllRemoteAlbums().then((value) {
AlbumNotifier(this._albumService, this.db, this.ref) : super([]) {
final query = db.albums.filter().remoteIdIsNotNull();
query.findAll().then((value) {
if (mounted) {
state = value;
}
});
_streamSub =
albumService.watchRemoteAlbums().listen((data) => state = data);
_streamSub = query.watch().listen((data) => state = data);
}
final AlbumService albumService;
final AlbumService _albumService;
final Isar db;
final Ref ref;
late final StreamSubscription<List<Album>> _streamSub;
Future<void> refreshRemoteAlbums() async {
ref.read(isRefreshingRemoteAlbumProvider.notifier).state = true;
await albumService.refreshRemoteAlbums();
await _albumService.refreshRemoteAlbums();
ref.read(isRefreshingRemoteAlbumProvider.notifier).state = false;
}
Future<void> refreshDeviceAlbums() => albumService.refreshDeviceAlbums();
Future<void> refreshDeviceAlbums() => _albumService.refreshDeviceAlbums();
Future<bool> deleteAlbum(Album album) => albumService.deleteAlbum(album);
Future<bool> deleteAlbum(Album album) => _albumService.deleteAlbum(album);
Future<Album?> createAlbum(
String albumTitle,
Set<Asset> assets,
) =>
albumService.createAlbum(albumTitle, assets, []);
_albumService.createAlbum(albumTitle, assets, []);
Future<Album?> getAlbumByName(
String albumName, {
@@ -49,7 +52,7 @@ class AlbumNotifier extends StateNotifier<List<Album>> {
bool? shared,
bool? owner,
}) =>
albumService.getAlbumByName(
_albumService.getAlbumByName(
albumName,
remote: remote,
shared: shared,
@@ -71,7 +74,7 @@ class AlbumNotifier extends StateNotifier<List<Album>> {
}
Future<bool> leaveAlbum(Album album) async {
var res = await albumService.leaveAlbum(album);
var res = await _albumService.leaveAlbum(album);
if (res) {
await deleteAlbum(album);
@@ -82,15 +85,15 @@ class AlbumNotifier extends StateNotifier<List<Album>> {
}
void searchAlbums(String searchTerm, QuickFilterMode filterMode) async {
state = await albumService.search(searchTerm, filterMode);
state = await _albumService.search(searchTerm, filterMode);
}
Future<void> addUsers(Album album, List<String> userIds) async {
await albumService.addUsers(album, userIds);
await _albumService.addUsers(album, userIds);
}
Future<bool> removeUser(Album album, User user) async {
final isRemoved = await albumService.removeUser(album, user);
final isRemoved = await _albumService.removeUser(album, user);
if (isRemoved && album.sharedUsers.isEmpty) {
state = state.where((element) => element.id != album.id).toList();
@@ -100,25 +103,25 @@ class AlbumNotifier extends StateNotifier<List<Album>> {
}
Future<void> addAssets(Album album, Iterable<Asset> assets) async {
await albumService.addAssets(album, assets);
await _albumService.addAssets(album, assets);
}
Future<bool> removeAsset(Album album, Iterable<Asset> assets) async {
return await albumService.removeAsset(album, assets);
return await _albumService.removeAsset(album, assets);
}
Future<bool> setActivitystatus(
Album album,
bool enabled,
) {
return albumService.setActivityStatus(album, enabled);
return _albumService.setActivityStatus(album, enabled);
}
Future<Album?> toggleSortOrder(Album album) {
final order =
album.sortOrder == SortOrder.asc ? SortOrder.desc : SortOrder.asc;
return albumService.updateSortOrder(album, order);
return _albumService.updateSortOrder(album, order);
}
@override
@@ -132,49 +135,57 @@ final albumProvider =
StateNotifierProvider.autoDispose<AlbumNotifier, List<Album>>((ref) {
return AlbumNotifier(
ref.watch(albumServiceProvider),
ref.watch(dbProvider),
ref,
);
});
final albumWatcher =
StreamProvider.autoDispose.family<Album, int>((ref, id) async* {
final albumService = ref.watch(albumServiceProvider);
final album = await albumService.getAlbumById(id);
if (album != null) {
yield album;
}
await for (final album in albumService.watchAlbum(id)) {
if (album != null) {
yield album;
}
StreamProvider.autoDispose.family<Album, int>((ref, albumId) async* {
final db = ref.watch(dbProvider);
final a = await db.albums.get(albumId);
if (a != null) yield a;
await for (final a in db.albums.watchObject(albumId, fireImmediately: true)) {
if (a != null) yield a;
}
});
final albumRenderlistProvider =
StreamProvider.autoDispose.family<RenderList, int>((ref, id) {
final album = ref.watch(albumWatcher(id)).value;
StreamProvider.autoDispose.family<RenderList, int>((ref, albumId) {
final album = ref.watch(albumWatcher(albumId)).value;
if (album != null) {
return ref.watch(albumServiceProvider).getRenderListGenerator(album);
final query = album.assets.filter().isTrashedEqualTo(false);
if (album.sortOrder == SortOrder.asc) {
return renderListGeneratorWithGroupBy(
query.sortByFileCreatedAt(),
GroupAssetsBy.none,
);
} else if (album.sortOrder == SortOrder.desc) {
return renderListGeneratorWithGroupBy(
query.sortByFileCreatedAtDesc(),
GroupAssetsBy.none,
);
}
}
return const Stream.empty();
});
class LocalAlbumsNotifier extends StateNotifier<List<Album>> {
LocalAlbumsNotifier(this.albumService) : super([]) {
albumService.getAllLocalAlbums().then((value) {
LocalAlbumsNotifier(this.db) : super([]) {
final query = db.albums.where().remoteIdIsNull();
query.findAll().then((value) {
if (mounted) {
state = value;
}
});
_streamSub = albumService.watchLocalAlbums().listen((data) => state = data);
_streamSub = query.watch().listen((data) => state = data);
}
final AlbumService albumService;
final Isar db;
late final StreamSubscription<List<Album>> _streamSub;
@override
@@ -186,5 +197,5 @@ class LocalAlbumsNotifier extends StateNotifier<List<Album>> {
final localAlbumsProvider =
StateNotifierProvider.autoDispose<LocalAlbumsNotifier, List<Album>>((ref) {
return LocalAlbumsNotifier(ref.watch(albumServiceProvider));
return LocalAlbumsNotifier(ref.watch(dbProvider));
});

View File

@@ -2,40 +2,28 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/locale_provider.dart';
import 'package:immich_mobile/providers/memory.provider.dart';
import 'package:immich_mobile/repositories/asset_media.repository.dart';
import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/services/asset.service.dart';
import 'package:immich_mobile/services/etag.service.dart';
import 'package:immich_mobile/services/exif.service.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/services/sync.service.dart';
import 'package:immich_mobile/services/user.service.dart';
import 'package:immich_mobile/utils/db.dart';
import 'package:immich_mobile/utils/renderlist_generator.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
final assetProvider = StateNotifierProvider<AssetNotifier, bool>((ref) {
return AssetNotifier(
ref.watch(assetServiceProvider),
ref.watch(albumServiceProvider),
ref.watch(userServiceProvider),
ref.watch(syncServiceProvider),
ref.watch(etagServiceProvider),
ref.watch(exifServiceProvider),
ref,
);
});
class AssetNotifier extends StateNotifier<bool> {
final AssetService _assetService;
final AlbumService _albumService;
final UserService _userService;
final SyncService _syncService;
final ETagService _etagService;
final ExifService _exifService;
final Isar _db;
final StateNotifierProviderRef _ref;
final log = Logger('AssetNotifier');
bool _getAllAssetInProgress = false;
@@ -46,8 +34,7 @@ class AssetNotifier extends StateNotifier<bool> {
this._albumService,
this._userService,
this._syncService,
this._etagService,
this._exifService,
this._db,
this._ref,
) : super(false);
@@ -61,7 +48,7 @@ class AssetNotifier extends StateNotifier<bool> {
_getAllAssetInProgress = true;
state = true;
if (clear) {
await clearAllAssets();
await clearAssetsAndAlbums(_db);
log.info("Manual refresh requested, cleared assets and albums from db");
}
final bool changedUsers = await _userService.refreshUsers();
@@ -81,15 +68,8 @@ class AssetNotifier extends StateNotifier<bool> {
}
}
Future<void> clearAllAssets() async {
await Store.delete(StoreKey.assetETag);
await Future.wait([
_assetService.clearTable(),
_exifService.clearTable(),
_albumService.clearTable(),
_userService.clearTable(),
_etagService.clearTable(),
]);
Future<void> clearAllAsset() {
return clearAssetsAndAlbums(_db);
}
Future<void> onNewAssetUploaded(Asset newAsset) async {
@@ -98,43 +78,102 @@ class AssetNotifier extends StateNotifier<bool> {
await _syncService.syncNewAssetToDb(newAsset);
}
Future<bool> deleteLocalAssets(List<Asset> assets) async {
_deleteInProgress = true;
state = true;
try {
await _assetService.deleteLocalAssets(assets);
return true;
} catch (error) {
log.severe("Failed to delete local assets", error);
return false;
} finally {
_deleteInProgress = false;
state = false;
}
}
/// Delete remote asset only
///
/// Default behavior is trashing the asset
Future<bool> deleteRemoteAssets(
Future<bool> deleteLocalOnlyAssets(
Iterable<Asset> deleteAssets, {
bool shouldDeletePermanently = false,
bool onlyBackedUp = false,
}) async {
_deleteInProgress = true;
state = true;
try {
await _assetService.deleteRemoteAssets(
deleteAssets,
shouldDeletePermanently: shouldDeletePermanently,
);
return true;
} catch (error) {
log.severe("Failed to delete remote assets", error);
return false;
// Filter the assets based on the backed-up status
final assets = onlyBackedUp
? deleteAssets.where((e) => e.storage == AssetState.merged)
: deleteAssets;
if (assets.isEmpty) {
return false; // No assets to delete
}
// Proceed with local deletion of the filtered assets
final localDeleted = await _deleteLocalAssets(assets);
if (localDeleted.isNotEmpty) {
final localOnlyIds = assets
.where((e) => e.storage == AssetState.local)
.map((e) => e.id)
.toList();
// Update merged assets to remote-only
final mergedAssets =
assets.where((e) => e.storage == AssetState.merged).map((e) {
e.localId = null;
return e;
}).toList();
// Update the local database
await _db.writeTxn(() async {
if (mergedAssets.isNotEmpty) {
await _db.assets
.putAll(mergedAssets); // Use the filtered merged assets
}
await _db.exifInfos.deleteAll(localOnlyIds);
await _db.assets.deleteAll(localOnlyIds);
});
return true;
}
} finally {
_deleteInProgress = false;
state = false;
}
return false;
}
Future<bool> deleteRemoteOnlyAssets(
Iterable<Asset> deleteAssets, {
bool force = false,
}) async {
_deleteInProgress = true;
state = true;
try {
final remoteDeleted = await _deleteRemoteAssets(deleteAssets, force);
if (remoteDeleted.isNotEmpty) {
final assetsToUpdate = force
/// If force, only update merged only assets and remove remote assets
? remoteDeleted
.where((e) => e.storage == AssetState.merged)
.map((e) {
e.remoteId = null;
return e;
})
// If not force, trash everything
: remoteDeleted.where((e) => e.isRemote).map((e) {
e.isTrashed = true;
return e;
});
await _db.writeTxn(() async {
if (assetsToUpdate.isNotEmpty) {
await _db.assets.putAll(assetsToUpdate.toList());
}
if (force) {
final remoteOnly = remoteDeleted
.where((e) => e.storage == AssetState.remote)
.map((e) => e.id)
.toList();
await _db.exifInfos.deleteAll(remoteOnly);
await _db.assets.deleteAll(remoteOnly);
}
});
return true;
}
} finally {
_deleteInProgress = false;
state = false;
}
return false;
}
Future<bool> deleteAssets(
@@ -144,18 +183,111 @@ class AssetNotifier extends StateNotifier<bool> {
_deleteInProgress = true;
state = true;
try {
await _assetService.deleteAssets(
deleteAssets,
shouldDeletePermanently: force,
);
return true;
} catch (error) {
log.severe("Failed to delete assets", error);
return false;
final hasLocal = deleteAssets.any((a) => a.storage != AssetState.remote);
final localDeleted = await _deleteLocalAssets(deleteAssets);
final remoteDeleted = (hasLocal && localDeleted.isNotEmpty) || !hasLocal
? await _deleteRemoteAssets(deleteAssets, force)
: [];
if (localDeleted.isNotEmpty || remoteDeleted.isNotEmpty) {
final dbIds = <int>[];
final dbUpdates = <Asset>[];
// Local assets are removed
if (localDeleted.isNotEmpty) {
// Permanently remove local only assets from isar
dbIds.addAll(
deleteAssets
.where((a) => a.storage == AssetState.local)
.map((e) => e.id),
);
if (remoteDeleted.any((e) => e.isLocal)) {
// Force delete: Add all local assets including merged assets
if (force) {
dbIds.addAll(remoteDeleted.map((e) => e.id));
// Soft delete: Remove local Id from asset and trash it
} else {
dbUpdates.addAll(
remoteDeleted.map((e) {
e.localId = null;
e.isTrashed = true;
return e;
}),
);
}
}
}
// Handle remote deletion
if (remoteDeleted.isNotEmpty) {
if (force) {
// Remove remote only assets
dbIds.addAll(
deleteAssets
.where((a) => a.storage == AssetState.remote)
.map((e) => e.id),
);
// Local assets are not removed and there are merged assets
final hasLocal = remoteDeleted.any((e) => e.isLocal);
if (localDeleted.isEmpty && hasLocal) {
// Remove remote Id from local assets
dbUpdates.addAll(
remoteDeleted.map((e) {
e.remoteId = null;
// Remove from trashed if remote asset is removed
e.isTrashed = false;
return e;
}),
);
}
} else {
dbUpdates.addAll(
remoteDeleted.map((e) {
e.isTrashed = true;
return e;
}),
);
}
}
await _db.writeTxn(() async {
await _db.assets.putAll(dbUpdates);
await _db.exifInfos.deleteAll(dbIds);
await _db.assets.deleteAll(dbIds);
});
return true;
}
} finally {
_deleteInProgress = false;
state = false;
}
return false;
}
Future<List<String>> _deleteLocalAssets(
Iterable<Asset> assetsToDelete,
) async {
final List<String> local =
assetsToDelete.where((a) => a.isLocal).map((a) => a.localId!).toList();
// Delete asset from device
if (local.isNotEmpty) {
try {
return await _ref.read(assetMediaRepositoryProvider).deleteAll(local);
} catch (e, stack) {
log.severe("Failed to delete asset from device", e, stack);
}
}
return [];
}
Future<List<Asset>> _deleteRemoteAssets(
Iterable<Asset> assetsToDelete,
bool? force,
) async {
final Iterable<Asset> remote = assetsToDelete.where((e) => e.isRemote);
final isSuccess = await _assetService.deleteAssets(remote, force: force);
return isSuccess ? remote.toList() : [];
}
Future<void> toggleFavorite(List<Asset> assets, [bool? status]) {
@@ -169,40 +301,41 @@ class AssetNotifier extends StateNotifier<bool> {
}
}
final assetProvider = StateNotifierProvider<AssetNotifier, bool>((ref) {
return AssetNotifier(
ref.watch(assetServiceProvider),
ref.watch(albumServiceProvider),
ref.watch(userServiceProvider),
ref.watch(syncServiceProvider),
ref.watch(dbProvider),
ref,
);
});
final assetDetailProvider =
StreamProvider.autoDispose.family<Asset, Asset>((ref, asset) async* {
final assetService = ref.watch(assetServiceProvider);
yield await assetService.loadExif(asset);
await for (final asset in assetService.watchAsset(asset.id)) {
if (asset != null) {
yield await ref.watch(assetServiceProvider).loadExif(asset);
yield await ref.watch(assetServiceProvider).loadExif(asset);
final db = ref.watch(dbProvider);
await for (final a in db.assets.watchObject(asset.id)) {
if (a != null) {
yield await ref.watch(assetServiceProvider).loadExif(a);
}
}
});
final assetWatcher =
StreamProvider.autoDispose.family<Asset?, Asset>((ref, asset) {
final assetService = ref.watch(assetServiceProvider);
return assetService.watchAsset(asset.id, fireImmediately: true);
final db = ref.watch(dbProvider);
return db.assets.watchObject(asset.id, fireImmediately: true);
});
final assetsProvider = StreamProvider.family<RenderList, int?>(
(ref, userId) {
if (userId == null) return const Stream.empty();
ref.watch(localeProvider);
final query = ref
.watch(dbProvider)
.assets
.where()
.ownerIdEqualToAnyChecksum(userId)
.filter()
.isArchivedEqualTo(false)
.isTrashedEqualTo(false)
.stackPrimaryAssetIdIsNull()
.sortByFileCreatedAtDesc();
final query = _commonFilterAndSort(
_assets(ref).where().ownerIdEqualToAnyChecksum(userId),
);
return renderListGenerator(query, ref);
},
dependencies: [localeProvider],
@@ -212,17 +345,11 @@ final multiUserAssetsProvider = StreamProvider.family<RenderList, List<int>>(
(ref, userIds) {
if (userIds.isEmpty) return const Stream.empty();
ref.watch(localeProvider);
final query = ref
.watch(dbProvider)
.assets
.where()
.anyOf(userIds, (q, u) => q.ownerIdEqualToAnyChecksum(u))
.filter()
.isArchivedEqualTo(false)
.isTrashedEqualTo(false)
.stackPrimaryAssetIdIsNull()
.sortByFileCreatedAtDesc();
final query = _commonFilterAndSort(
_assets(ref)
.where()
.anyOf(userIds, (q, u) => q.ownerIdEqualToAnyChecksum(u)),
);
return renderListGenerator(query, ref);
},
dependencies: [localeProvider],
@@ -244,3 +371,17 @@ QueryBuilder<Asset, Asset, QAfterSortBy>? getRemoteAssetQuery(WidgetRef ref) {
.stackPrimaryAssetIdIsNull()
.sortByFileCreatedAtDesc();
}
IsarCollection<Asset> _assets(StreamProviderRef<RenderList> ref) =>
ref.watch(dbProvider).assets;
QueryBuilder<Asset, Asset, QAfterSortBy> _commonFilterAndSort(
QueryBuilder<Asset, Asset, QAfterWhereClause> query,
) {
return query
.filter()
.isArchivedEqualTo(false)
.isTrashedEqualTo(false)
.stackPrimaryAssetIdIsNull()
.sortByFileCreatedAtDesc();
}

View File

@@ -1,15 +1,16 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/services/asset.service.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:isar/isar.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'asset_stack.provider.g.dart';
class AssetStackNotifier extends StateNotifier<List<Asset>> {
final AssetService assetService;
final String _stackId;
final Ref _ref;
AssetStackNotifier(this.assetService, this._stackId) : super([]) {
AssetStackNotifier(this._stackId, this._ref) : super([]) {
_fetchStack(_stackId);
}
@@ -18,7 +19,7 @@ class AssetStackNotifier extends StateNotifier<List<Asset>> {
return;
}
final stack = await assetService.getStackAssets(stackId);
final stack = await _ref.read(assetStackProvider(stackId).future);
if (stack.isNotEmpty) {
state = stack;
}
@@ -34,10 +35,24 @@ class AssetStackNotifier extends StateNotifier<List<Asset>> {
final assetStackStateProvider = StateNotifierProvider.autoDispose
.family<AssetStackNotifier, List<Asset>, String>(
(ref, stackId) =>
AssetStackNotifier(ref.watch(assetServiceProvider), stackId),
(ref, stackId) => AssetStackNotifier(stackId, ref),
);
final assetStackProvider =
FutureProvider.autoDispose.family<List<Asset>, String>((ref, stackId) {
return ref
.watch(dbProvider)
.assets
.filter()
.isArchivedEqualTo(false)
.isTrashedEqualTo(false)
.stackIdEqualTo(stackId)
// orders primary asset first as its ID is null
.sortByStackPrimaryAssetId()
.thenByFileCreatedAtDesc()
.findAll();
});
@riverpod
int assetStackIndex(AssetStackIndexRef ref, Asset asset) {
return -1;

View File

@@ -19,23 +19,17 @@ class PaginatedSearchNotifier extends StateNotifier<SearchResult> {
PaginatedSearchNotifier(this._searchService)
: super(SearchResult(assets: [], nextPage: 1));
Future<bool> search(SearchFilter filter) async {
if (state.nextPage == null) {
return false;
}
search(SearchFilter filter) async {
if (state.nextPage == null) return;
final result = await _searchService.search(filter, state.nextPage!);
if (result == null) {
return false;
}
if (result == null) return;
state = SearchResult(
assets: [...state.assets, ...result.assets],
nextPage: result.nextPage,
);
return true;
}
clear() {

View File

@@ -57,7 +57,7 @@ class TrashNotifier extends StateNotifier<bool> {
final isRemoved = await _ref
.read(assetProvider.notifier)
.deleteRemoteAssets(assetList, shouldDeletePermanently: true);
.deleteRemoteOnlyAssets(assetList, force: true);
if (isRemoved) {
final idsToRemove =

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