Compare commits

..

1 Commits

Author SHA1 Message Date
midzelis
19f276b543 feat: socket.io redis->postgres socket.io, add broadcastchannel option 2026-02-14 18:50:30 +00:00
688 changed files with 17332 additions and 24156 deletions

View File

@@ -2,7 +2,6 @@
"name": "Immich - Backend, Frontend and ML", "name": "Immich - Backend, Frontend and ML",
"service": "immich-server", "service": "immich-server",
"runServices": [ "runServices": [
"immich-init",
"immich-server", "immich-server",
"redis", "redis",
"database", "database",
@@ -32,8 +31,29 @@
"tasks": { "tasks": {
"version": "2.0.0", "version": "2.0.0",
"tasks": [ "tasks": [
{
"label": "Fix Permissions, Install Dependencies",
"type": "shell",
"command": "[ -f /immich-devcontainer/container-start.sh ] && /immich-devcontainer/container-start.sh || exit 0",
"isBackground": true,
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "dedicated",
"showReuseMessage": true,
"clear": false,
"group": "Devcontainer tasks",
"close": true
},
"runOptions": {
"runOn": "default"
},
"problemMatcher": []
},
{ {
"label": "Immich API Server (Nest)", "label": "Immich API Server (Nest)",
"dependsOn": ["Fix Permissions, Install Dependencies"],
"type": "shell", "type": "shell",
"command": "[ -f /immich-devcontainer/container-start-backend.sh ] && /immich-devcontainer/container-start-backend.sh || exit 0", "command": "[ -f /immich-devcontainer/container-start-backend.sh ] && /immich-devcontainer/container-start-backend.sh || exit 0",
"isBackground": true, "isBackground": true,
@@ -54,6 +74,7 @@
}, },
{ {
"label": "Immich Web Server (Vite)", "label": "Immich Web Server (Vite)",
"dependsOn": ["Fix Permissions, Install Dependencies"],
"type": "shell", "type": "shell",
"command": "[ -f /immich-devcontainer/container-start-frontend.sh ] && /immich-devcontainer/container-start-frontend.sh || exit 0", "command": "[ -f /immich-devcontainer/container-start-frontend.sh ] && /immich-devcontainer/container-start-frontend.sh || exit 0",
"isBackground": true, "isBackground": true,
@@ -109,8 +130,8 @@
} }
}, },
"overrideCommand": true, "overrideCommand": true,
"workspaceFolder": "/usr/src/app", "workspaceFolder": "/workspaces/immich",
"remoteUser": "root", "remoteUser": "node",
"userEnvProbe": "loginInteractiveShell", "userEnvProbe": "loginInteractiveShell",
"remoteEnv": { "remoteEnv": {
// The location where your uploaded files are stored // The location where your uploaded files are stored

View File

@@ -1,17 +1,23 @@
services: services:
immich-app-base:
image: busybox
immich-server: immich-server:
extends:
service: immich-app-base
profiles: !reset []
image: immich-server-dev:latest
build: build:
target: dev-container-mobile target: dev-container-mobile
environment: environment:
- IMMICH_SERVER_URL=http://127.0.0.1:2283/ - IMMICH_SERVER_URL=http://127.0.0.1:2283/
volumes: volumes: !override # bind mount host to /workspaces/immich
- ..:/workspaces/immich
- ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data - ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data
- pnpm-store:/usr/src/app/.pnpm-store
- server-node_modules:/usr/src/app/server/node_modules
- web-node_modules:/usr/src/app/web/node_modules
- github-node_modules:/usr/src/app/.github/node_modules
- cli-node_modules:/usr/src/app/cli/node_modules
- docs-node_modules:/usr/src/app/docs/node_modules
- e2e-node_modules:/usr/src/app/e2e/node_modules
- sdk-node_modules:/usr/src/app/open-api/typescript-sdk/node_modules
- app-node_modules:/usr/src/app/node_modules
- sveltekit:/usr/src/app/web/.svelte-kit
- coverage:/usr/src/app/web/coverage
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
immich-web: immich-web:
env_file: !reset [] env_file: !reset []

View File

@@ -2,7 +2,6 @@
"name": "Immich - Mobile", "name": "Immich - Mobile",
"service": "immich-server", "service": "immich-server",
"runServices": [ "runServices": [
"immich-init",
"immich-server", "immich-server",
"redis", "redis",
"database", "database",
@@ -36,7 +35,7 @@
}, },
"forwardPorts": [], "forwardPorts": [],
"overrideCommand": true, "overrideCommand": true,
"workspaceFolder": "/usr/src/app", "workspaceFolder": "/workspaces/immich",
"remoteUser": "node", "remoteUser": "node",
"userEnvProbe": "loginInteractiveShell", "userEnvProbe": "loginInteractiveShell",
"remoteEnv": { "remoteEnv": {

View File

@@ -2,6 +2,11 @@
export IMMICH_PORT="${DEV_SERVER_PORT:-2283}" export IMMICH_PORT="${DEV_SERVER_PORT:-2283}"
export DEV_PORT="${DEV_PORT:-3000}" export DEV_PORT="${DEV_PORT:-3000}"
# search for immich directory inside workspace.
# /workspaces/immich is the bind mount, but other directories can be mounted if runing
# Devcontainer: Clone [repository|pull request] in container volumne
WORKSPACES_DIR="/workspaces"
IMMICH_DIR="$WORKSPACES_DIR/immich"
IMMICH_DEVCONTAINER_LOG="$HOME/immich-devcontainer.log" IMMICH_DEVCONTAINER_LOG="$HOME/immich-devcontainer.log"
log() { log() {
@@ -25,8 +30,52 @@ run_cmd() {
return "${PIPESTATUS[0]}" return "${PIPESTATUS[0]}"
} }
export IMMICH_WORKSPACE="/usr/src/app" # Find directories excluding /workspaces/immich
mapfile -t other_dirs < <(find "$WORKSPACES_DIR" -mindepth 1 -maxdepth 1 -type d ! -path "$IMMICH_DIR" ! -name ".*")
if [ ${#other_dirs[@]} -gt 1 ]; then
log "Error: More than one directory found in $WORKSPACES_DIR other than $IMMICH_DIR."
exit 1
elif [ ${#other_dirs[@]} -eq 1 ]; then
export IMMICH_WORKSPACE="${other_dirs[0]}"
else
export IMMICH_WORKSPACE="$IMMICH_DIR"
fi
log "Found immich workspace in $IMMICH_WORKSPACE" log "Found immich workspace in $IMMICH_WORKSPACE"
log "" log ""
fix_permissions() {
log "Fixing permissions for ${IMMICH_WORKSPACE}"
# Change ownership for directories that exist
for dir in "${IMMICH_WORKSPACE}/.vscode" \
"${IMMICH_WORKSPACE}/server/upload" \
"${IMMICH_WORKSPACE}/.pnpm-store" \
"${IMMICH_WORKSPACE}/.github/node_modules" \
"${IMMICH_WORKSPACE}/cli/node_modules" \
"${IMMICH_WORKSPACE}/e2e/node_modules" \
"${IMMICH_WORKSPACE}/open-api/typescript-sdk/node_modules" \
"${IMMICH_WORKSPACE}/server/node_modules" \
"${IMMICH_WORKSPACE}/server/dist" \
"${IMMICH_WORKSPACE}/web/node_modules" \
"${IMMICH_WORKSPACE}/web/dist"; do
if [ -d "$dir" ]; then
run_cmd sudo chown node -R "$dir"
fi
done
log ""
}
install_dependencies() {
log "Installing dependencies"
(
cd "${IMMICH_WORKSPACE}" || exit 1
export CI=1 FROZEN=1 OFFLINE=1
run_cmd make setup-web-dev setup-server-dev
)
log ""
}

View File

@@ -1,21 +1,26 @@
services: services:
immich-app-base:
image: busybox
immich-server: immich-server:
extends:
service: immich-app-base
profiles: !reset []
image: immich-server-dev:latest
build: build:
target: dev-container-server target: dev-container-server
env_file: !reset [] env_file: !reset []
hostname: immich-dev hostname: immich-dev
environment: environment:
- IMMICH_SERVER_URL=http://127.0.0.1:2283/ - IMMICH_SERVER_URL=http://127.0.0.1:2283/
volumes: volumes: !override
- ..:/workspaces/immich
- ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data - ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
- pnpm_store_server:/buildcache/pnpm-store - pnpm-store:/usr/src/app/.pnpm-store
- server-node_modules:/usr/src/app/server/node_modules
- web-node_modules:/usr/src/app/web/node_modules
- github-node_modules:/usr/src/app/.github/node_modules
- cli-node_modules:/usr/src/app/cli/node_modules
- docs-node_modules:/usr/src/app/docs/node_modules
- e2e-node_modules:/usr/src/app/e2e/node_modules
- sdk-node_modules:/usr/src/app/open-api/typescript-sdk/node_modules
- app-node_modules:/usr/src/app/node_modules
- sveltekit:/usr/src/app/web/.svelte-kit
- coverage:/usr/src/app/web/coverage
- ../plugins:/build/corePlugin - ../plugins:/build/corePlugin
immich-web: immich-web:
env_file: !reset [] env_file: !reset []

View File

@@ -0,0 +1,17 @@
#!/bin/bash
# shellcheck source=common.sh
# shellcheck disable=SC1091
source /immich-devcontainer/container-common.sh
log "Setting up Immich dev container..."
fix_permissions
log "Setup complete, please wait while backend and frontend services automatically start"
log
log "If necessary, the services may be manually started using"
log
log "$ /immich-devcontainer/container-start-backend.sh"
log "$ /immich-devcontainer/container-start-frontend.sh"
log
log "From different terminal windows, as these scripts automatically restart the server"
log "on error, and will continuously run in a loop"

2
.github/.nvmrc vendored
View File

@@ -1 +1 @@
24.13.1 24.13.0

View File

@@ -51,14 +51,14 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }} should_run: ${{ steps.check.outputs.should_run }}
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Check what should run - name: Check what should run
id: check id: check
uses: immich-app/devtools/actions/pre-job@eed0f8b8165ffcb951f2ba854b2dd031935e1d73 # pre-job-action-v2.0.2 uses: immich-app/devtools/actions/pre-job@08bac802a312fc89808e0dd589271ca0974087b5 # pre-job-action-v2.0.0
with: with:
github-token: ${{ steps.token.outputs.token }} github-token: ${{ steps.token.outputs.token }}
filters: | filters: |
@@ -79,12 +79,12 @@ jobs:
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
ref: ${{ inputs.ref || github.sha }} ref: ${{ inputs.ref || github.sha }}
persist-credentials: false persist-credentials: false
@@ -96,14 +96,14 @@ jobs:
working-directory: ./mobile working-directory: ./mobile
run: printf "%s" $KEY_JKS | base64 -d > android/key.jks run: printf "%s" $KEY_JKS | base64 -d > android/key.jks
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 - uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0
with: with:
distribution: 'zulu' distribution: 'zulu'
java-version: '17' java-version: '17'
- name: Restore Gradle Cache - name: Restore Gradle Cache
id: cache-gradle-restore id: cache-gradle-restore
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with: with:
path: | path: |
~/.gradle/caches ~/.gradle/caches
@@ -160,7 +160,7 @@ jobs:
- name: Save Gradle Cache - name: Save Gradle Cache
id: cache-gradle-save id: cache-gradle-save
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
if: github.ref == 'refs/heads/main' if: github.ref == 'refs/heads/main'
with: with:
path: | path: |
@@ -185,7 +185,7 @@ jobs:
run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with: with:
ref: ${{ inputs.ref || github.sha }} ref: ${{ inputs.ref || github.sha }}
persist-credentials: false persist-credentials: false

View File

@@ -19,13 +19,13 @@ jobs:
actions: write actions: write
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Check out code - name: Check out code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}

View File

@@ -1,32 +0,0 @@
name: Check OpenAPI
on:
workflow_dispatch:
pull_request:
paths:
- 'open-api/**'
- '.github/workflows/check-openapi.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {}
jobs:
check-openapi:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Check for breaking API changes
# sha is pinning to a commit instead of a tag since the action does not tag versions
uses: oasdiff/oasdiff-action/breaking@ccb863950ce437a50f8f1a40d2a1112117e06ce4
with:
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
revision: open-api/immich-openapi-specs.json
fail-on: ERR

View File

@@ -31,12 +31,12 @@ jobs:
working-directory: ./cli working-directory: ./cli
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
@@ -45,7 +45,7 @@ jobs:
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './cli/.nvmrc' node-version-file: './cli/.nvmrc'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
@@ -71,13 +71,13 @@ jobs:
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout - name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
@@ -89,7 +89,7 @@ jobs:
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
if: ${{ !github.event.pull_request.head.repo.fork }} if: ${{ !github.event.pull_request.head.repo.fork }}
with: with:
registry: ghcr.io registry: ghcr.io
@@ -115,7 +115,7 @@ jobs:
type=raw,value=latest,enable=${{ github.event_name == 'release' }} type=raw,value=latest,enable=${{ github.event_name == 'release' }}
- name: Build and push image - name: Build and push image
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with: with:
file: cli/Dockerfile file: cli/Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64

View File

@@ -35,7 +35,7 @@ jobs:
needs: [get_body, should_run] needs: [get_body, should_run]
if: ${{ needs.should_run.outputs.should_run == 'true' }} if: ${{ needs.should_run.outputs.should_run == 'true' }}
container: container:
image: ghcr.io/immich-app/mdq:main@sha256:4f9860d04c88f7f87861f8ee84bfeedaec15ed7ca5ca87bc7db44b036f81645f image: ghcr.io/immich-app/mdq:main@sha256:ab9f163cd5d5cec42704a26ca2769ecf3f10aa8e7bae847f1d527cdf075946e6
outputs: outputs:
checked: ${{ steps.get_checkbox.outputs.checked }} checked: ${{ steps.get_checkbox.outputs.checked }}
steps: steps:

View File

@@ -1,7 +1,7 @@
name: Close LLM-generated PRs name: Close LLM-generated PRs
on: on:
pull_request_target: pull_request:
types: [labeled] types: [labeled]
permissions: {} permissions: {}
@@ -20,7 +20,7 @@ jobs:
run: | run: |
gh api graphql \ gh api graphql \
-f prId="$NODE_ID" \ -f prId="$NODE_ID" \
-f body="Thank you for your interest in contributing to Immich! Unfortunately this PR looks like it was generated using an LLM. As noted in our [CONTRIBUTING.md](https://github.com/immich-app/immich/blob/main/CONTRIBUTING.md#use-of-generative-ai), we request that you don't use LLMs to generate PRs as those are not a good use of maintainer time." \ -f body="Thank you for your interest in contributing to Immich! Unfortunately this PR looks like it was generated using an LLM. As noted in our CONTRIBUTING.md, we request that you don't use LLMs to generate PRs as those are not a good use of maintainer time." \
-f query=' -f query='
mutation CommentAndClosePR($prId: ID!, $body: String!) { mutation CommentAndClosePR($prId: ID!, $body: String!) {
addComment(input: { addComment(input: {

View File

@@ -44,20 +44,20 @@ jobs:
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@@ -70,7 +70,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 uses: github/codeql-action/autobuild@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -83,6 +83,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh # ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
with: with:
category: '/language:${{matrix.language}}' category: '/language:${{matrix.language}}'

View File

@@ -23,14 +23,14 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }} should_run: ${{ steps.check.outputs.should_run }}
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Check what should run - name: Check what should run
id: check id: check
uses: immich-app/devtools/actions/pre-job@eed0f8b8165ffcb951f2ba854b2dd031935e1d73 # pre-job-action-v2.0.2 uses: immich-app/devtools/actions/pre-job@08bac802a312fc89808e0dd589271ca0974087b5 # pre-job-action-v2.0.0
with: with:
github-token: ${{ steps.token.outputs.token }} github-token: ${{ steps.token.outputs.token }}
filters: | filters: |
@@ -60,7 +60,7 @@ jobs:
suffix: ['', '-cuda', '-rocm', '-openvino', '-armnn', '-rknn'] suffix: ['', '-cuda', '-rocm', '-openvino', '-armnn', '-rknn']
steps: steps:
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -90,7 +90,7 @@ jobs:
suffix: [''] suffix: ['']
steps: steps:
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -132,7 +132,7 @@ jobs:
suffixes: '-rocm' suffixes: '-rocm'
platforms: linux/amd64 platforms: linux/amd64
runner-mapping: '{"linux/amd64": "pokedex-giant"}' runner-mapping: '{"linux/amd64": "pokedex-giant"}'
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@bd49ed7a5a6022149f79b6564df48177476a822b # multi-runner-build-workflow-v2.2.1 uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@0477486d82313fba68f7c82c034120a4b8981297 # multi-runner-build-workflow-v2.1.0
permissions: permissions:
contents: read contents: read
actions: read actions: read
@@ -155,7 +155,7 @@ jobs:
name: Build and Push Server name: Build and Push Server
needs: pre-job needs: pre-job
if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }} if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }}
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@bd49ed7a5a6022149f79b6564df48177476a822b # multi-runner-build-workflow-v2.2.1 uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@0477486d82313fba68f7c82c034120a4b8981297 # multi-runner-build-workflow-v2.1.0
permissions: permissions:
contents: read contents: read
actions: read actions: read

View File

@@ -21,14 +21,14 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }} should_run: ${{ steps.check.outputs.should_run }}
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Check what should run - name: Check what should run
id: check id: check
uses: immich-app/devtools/actions/pre-job@eed0f8b8165ffcb951f2ba854b2dd031935e1d73 # pre-job-action-v2.0.2 uses: immich-app/devtools/actions/pre-job@08bac802a312fc89808e0dd589271ca0974087b5 # pre-job-action-v2.0.0
with: with:
github-token: ${{ steps.token.outputs.token }} github-token: ${{ steps.token.outputs.token }}
filters: | filters: |
@@ -54,13 +54,13 @@ jobs:
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
@@ -70,7 +70,7 @@ jobs:
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './docs/.nvmrc' node-version-file: './docs/.nvmrc'
cache: 'pnpm' cache: 'pnpm'

View File

@@ -20,7 +20,7 @@ jobs:
artifact: ${{ steps.get-artifact.outputs.result }} artifact: ${{ steps.get-artifact.outputs.result }}
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@@ -119,19 +119,19 @@ jobs:
if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }} if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }}
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - name: Setup Mise
uses: immich-app/devtools/actions/use-mise@dab18118da6476e8237ac94080fd937983fecd42 # use-mise-action-v1.1.2 uses: immich-app/devtools/actions/use-mise@cd24790a7f5f6439ac32cc94f5523cb2de8bfa8c # use-mise-action-v1.1.0
- name: Load parameters - name: Load parameters
id: parameters id: parameters
@@ -192,13 +192,16 @@ jobs:
' >> $GITHUB_OUTPUT ' >> $GITHUB_OUTPUT
- name: Publish to Cloudflare Pages - name: Publish to Cloudflare Pages
working-directory: docs # TODO: Action is deprecated
env: uses: cloudflare/pages-action@f0a1cd58cd66095dee69bfa18fa5efd1dde93bca # v1.5.0
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN_PAGES_UPLOAD }} with:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN_PAGES_UPLOAD }}
PROJECT_NAME: ${{ steps.docs-output.outputs.projectName }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
BRANCH_NAME: ${{ steps.parameters.outputs.name }} projectName: ${{ steps.docs-output.outputs.projectName }}
run: mise run //docs:deploy workingDirectory: 'docs'
directory: 'build'
branch: ${{ steps.parameters.outputs.name }}
wranglerVersion: '3'
- name: Deploy Docs Release Domain - name: Deploy Docs Release Domain
if: ${{ steps.parameters.outputs.event == 'release' }} if: ${{ steps.parameters.outputs.event == 'release' }}

View File

@@ -17,19 +17,19 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - name: Setup Mise
uses: immich-app/devtools/actions/use-mise@dab18118da6476e8237ac94080fd937983fecd42 # use-mise-action-v1.1.2 uses: immich-app/devtools/actions/use-mise@cd24790a7f5f6439ac32cc94f5523cb2de8bfa8c # use-mise-action-v1.1.0
- name: Destroy Docs Subdomain - name: Destroy Docs Subdomain
env: env:

View File

@@ -22,7 +22,7 @@ jobs:
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: 'Checkout' - name: 'Checkout'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
ref: ${{ github.event.pull_request.head.ref }} ref: ${{ github.event.pull_request.head.ref }}
token: ${{ steps.generate-token.outputs.token }} token: ${{ steps.generate-token.outputs.token }}
@@ -32,14 +32,14 @@ jobs:
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
cache: 'pnpm' cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml' cache-dependency-path: '**/pnpm-lock.yaml'
- name: Fix formatting - name: Fix formatting
run: pnpm --recursive install && pnpm run --recursive --if-present --parallel format:fix run: pnpm --recursive install && pnpm run --recursive --parallel fix:format
- name: Commit and push - name: Commit and push
uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4 uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4

View File

@@ -14,7 +14,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

View File

@@ -56,20 +56,20 @@ jobs:
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout - name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
token: ${{ steps.generate-token.outputs.token }} token: ${{ steps.generate-token.outputs.token }}
persist-credentials: true persist-credentials: true
ref: main ref: main
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
cache: 'pnpm' cache: 'pnpm'
@@ -130,7 +130,7 @@ jobs:
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout - name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
token: ${{ steps.generate-token.outputs.token }} token: ${{ steps.generate-token.outputs.token }}
persist-credentials: false persist-credentials: false

View File

@@ -14,7 +14,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@@ -32,7 +32,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

View File

@@ -23,20 +23,20 @@ jobs:
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout - name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
token: ${{ steps.generate-token.outputs.token }} token: ${{ steps.generate-token.outputs.token }}
persist-credentials: true persist-credentials: true
ref: main ref: main
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
cache: 'pnpm' cache: 'pnpm'
@@ -159,7 +159,7 @@ jobs:
- name: Create PR - name: Create PR
id: create-pr id: create-pr
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
with: with:
token: ${{ steps.generate-token.outputs.token }} token: ${{ steps.generate-token.outputs.token }}
commit-message: 'chore: release ${{ steps.bump-type.outputs.next }}' commit-message: 'chore: release ${{ steps.bump-type.outputs.next }}'

View File

@@ -58,7 +58,7 @@ jobs:
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout - name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
token: ${{ steps.generate-token.outputs.token }} token: ${{ steps.generate-token.outputs.token }}
persist-credentials: false persist-credentials: false
@@ -88,7 +88,6 @@ jobs:
draft: true draft: true
files: | files: |
docker/docker-compose.yml docker/docker-compose.yml
docker/docker-compose.rootless.yml
docker/example.env docker/example.env
docker/hwaccel.ml.yml docker/hwaccel.ml.yml
docker/hwaccel.transcoding.yml docker/hwaccel.transcoding.yml

View File

@@ -19,12 +19,12 @@ jobs:
working-directory: ./open-api/typescript-sdk working-directory: ./open-api/typescript-sdk
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
@@ -33,7 +33,7 @@ jobs:
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
# Setup .npmrc file to publish to npm # Setup .npmrc file to publish to npm
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './open-api/typescript-sdk/.nvmrc' node-version-file: './open-api/typescript-sdk/.nvmrc'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'

View File

@@ -20,14 +20,14 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }} should_run: ${{ steps.check.outputs.should_run }}
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Check what should run - name: Check what should run
id: check id: check
uses: immich-app/devtools/actions/pre-job@eed0f8b8165ffcb951f2ba854b2dd031935e1d73 # pre-job-action-v2.0.2 uses: immich-app/devtools/actions/pre-job@08bac802a312fc89808e0dd589271ca0974087b5 # pre-job-action-v2.0.0
with: with:
github-token: ${{ steps.token.outputs.token }} github-token: ${{ steps.token.outputs.token }}
filters: | filters: |
@@ -49,13 +49,13 @@ jobs:
working-directory: ./mobile working-directory: ./mobile
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
@@ -69,14 +69,6 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: dart pub get run: dart pub get
- name: Install dependencies for UI package
run: dart pub get
working-directory: ./mobile/packages/ui
- name: Install dependencies for UI Showcase
run: dart pub get
working-directory: ./mobile/packages/ui/showcase
- name: Install DCM - name: Install DCM
uses: CQLabs/setup-dcm@8697ae0790c0852e964a6ef1d768d62a6675481a # v2.0.1 uses: CQLabs/setup-dcm@8697ae0790c0852e964a6ef1d768d62a6675481a # v2.0.1
with: with:

View File

@@ -17,14 +17,14 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }} should_run: ${{ steps.check.outputs.should_run }}
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Check what should run - name: Check what should run
id: check id: check
uses: immich-app/devtools/actions/pre-job@eed0f8b8165ffcb951f2ba854b2dd031935e1d73 # pre-job-action-v2.0.2 uses: immich-app/devtools/actions/pre-job@08bac802a312fc89808e0dd589271ca0974087b5 # pre-job-action-v2.0.0
with: with:
github-token: ${{ steps.token.outputs.token }} github-token: ${{ steps.token.outputs.token }}
filters: | filters: |
@@ -63,13 +63,13 @@ jobs:
working-directory: ./server working-directory: ./server
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
@@ -77,7 +77,7 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
cache: 'pnpm' cache: 'pnpm'
@@ -108,20 +108,20 @@ jobs:
working-directory: ./cli working-directory: ./cli
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './cli/.nvmrc' node-version-file: './cli/.nvmrc'
cache: 'pnpm' cache: 'pnpm'
@@ -155,20 +155,20 @@ jobs:
working-directory: ./cli working-directory: ./cli
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './cli/.nvmrc' node-version-file: './cli/.nvmrc'
cache: 'pnpm' cache: 'pnpm'
@@ -197,20 +197,20 @@ jobs:
working-directory: ./web working-directory: ./web
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './web/.nvmrc' node-version-file: './web/.nvmrc'
cache: 'pnpm' cache: 'pnpm'
@@ -241,20 +241,20 @@ jobs:
working-directory: ./web working-directory: ./web
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './web/.nvmrc' node-version-file: './web/.nvmrc'
cache: 'pnpm' cache: 'pnpm'
@@ -279,20 +279,20 @@ jobs:
contents: read contents: read
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './web/.nvmrc' node-version-file: './web/.nvmrc'
cache: 'pnpm' cache: 'pnpm'
@@ -327,20 +327,20 @@ jobs:
working-directory: ./e2e working-directory: ./e2e
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './e2e/.nvmrc' node-version-file: './e2e/.nvmrc'
cache: 'pnpm' cache: 'pnpm'
@@ -373,13 +373,13 @@ jobs:
working-directory: ./server working-directory: ./server
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
submodules: 'recursive' submodules: 'recursive'
@@ -387,7 +387,7 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
cache: 'pnpm' cache: 'pnpm'
@@ -412,13 +412,13 @@ jobs:
runner: [ubuntu-latest, ubuntu-24.04-arm] runner: [ubuntu-latest, ubuntu-24.04-arm]
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
submodules: 'recursive' submodules: 'recursive'
@@ -426,7 +426,7 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './e2e/.nvmrc' node-version-file: './e2e/.nvmrc'
cache: 'pnpm' cache: 'pnpm'
@@ -446,29 +446,12 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Start Docker Compose - name: Docker build
run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300 run: docker compose build
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run e2e tests (api & cli) - name: Run e2e tests (api & cli)
env:
VITEST_DISABLE_DOCKER_SETUP: true
run: pnpm test run: pnpm test
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run e2e tests (maintenance)
env:
VITEST_DISABLE_DOCKER_SETUP: true
run: pnpm test:maintenance
if: ${{ !cancelled() }}
- name: Capture Docker logs
if: always()
run: docker compose logs --no-color > docker-compose-logs.txt
working-directory: ./e2e
- name: Archive Docker logs
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
if: always()
with:
name: e2e-server-docker-logs-${{ matrix.runner }}
path: e2e/docker-compose-logs.txt
e2e-tests-web: e2e-tests-web:
name: End-to-End Tests (Web) name: End-to-End Tests (Web)
needs: pre-job needs: pre-job
@@ -484,13 +467,13 @@ jobs:
runner: [ubuntu-latest, ubuntu-24.04-arm] runner: [ubuntu-latest, ubuntu-24.04-arm]
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
submodules: 'recursive' submodules: 'recursive'
@@ -498,7 +481,7 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './e2e/.nvmrc' node-version-file: './e2e/.nvmrc'
cache: 'pnpm' cache: 'pnpm'
@@ -511,15 +494,16 @@ jobs:
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Install Playwright Browsers - name: Install Playwright Browsers
run: pnpm exec playwright install chromium --only-shell run: npx playwright install chromium --only-shell
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Docker build - name: Docker build
run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300 run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run e2e tests (web) - name: Run e2e tests (web)
env: env:
CI: true
PLAYWRIGHT_DISABLE_WEBSERVER: true PLAYWRIGHT_DISABLE_WEBSERVER: true
run: pnpm test:web run: npx playwright test --project=web
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Archive e2e test (web) results - name: Archive e2e test (web) results
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
@@ -529,8 +513,9 @@ jobs:
path: e2e/playwright-report/ path: e2e/playwright-report/
- name: Run ui tests (web) - name: Run ui tests (web)
env: env:
CI: true
PLAYWRIGHT_DISABLE_WEBSERVER: true PLAYWRIGHT_DISABLE_WEBSERVER: true
run: pnpm test:web:ui run: npx playwright test --project=ui
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Archive ui test (web) results - name: Archive ui test (web) results
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
@@ -540,8 +525,9 @@ jobs:
path: e2e/playwright-report/ path: e2e/playwright-report/
- name: Run maintenance tests - name: Run maintenance tests
env: env:
CI: true
PLAYWRIGHT_DISABLE_WEBSERVER: true PLAYWRIGHT_DISABLE_WEBSERVER: true
run: pnpm test:web:maintenance run: npx playwright test --project=maintenance
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Archive maintenance tests (web) results - name: Archive maintenance tests (web) results
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
@@ -557,7 +543,7 @@ jobs:
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
if: always() if: always()
with: with:
name: e2e-web-docker-logs-${{ matrix.runner }} name: docker-compose-logs-${{ matrix.runner }}
path: e2e/docker-compose-logs.txt path: e2e/docker-compose-logs.txt
success-check-e2e: success-check-e2e:
name: End-to-End Tests Success name: End-to-End Tests Success
@@ -578,12 +564,12 @@ jobs:
contents: read contents: read
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
@@ -610,17 +596,17 @@ jobs:
working-directory: ./machine-learning working-directory: ./machine-learning
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
with: with:
python-version: 3.11 python-version: 3.11
- name: Install dependencies - name: Install dependencies
@@ -650,20 +636,20 @@ jobs:
working-directory: ./.github working-directory: ./.github
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './.github/.nvmrc' node-version-file: './.github/.nvmrc'
cache: 'pnpm' cache: 'pnpm'
@@ -680,12 +666,12 @@ jobs:
contents: read contents: read
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
@@ -701,20 +687,20 @@ jobs:
contents: read contents: read
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
cache: 'pnpm' cache: 'pnpm'
@@ -763,20 +749,20 @@ jobs:
working-directory: ./server working-directory: ./server
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
cache: 'pnpm' cache: 'pnpm'

View File

@@ -24,14 +24,14 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }} should_run: ${{ steps.check.outputs.should_run }}
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Check what should run - name: Check what should run
id: check id: check
uses: immich-app/devtools/actions/pre-job@eed0f8b8165ffcb951f2ba854b2dd031935e1d73 # pre-job-action-v2.0.2 uses: immich-app/devtools/actions/pre-job@08bac802a312fc89808e0dd589271ca0974087b5 # pre-job-action-v2.0.0
with: with:
github-token: ${{ steps.token.outputs.token }} github-token: ${{ steps.token.outputs.token }}
filters: | filters: |
@@ -47,7 +47,7 @@ jobs:
if: ${{ fromJSON(needs.pre-job.outputs.should_run).i18n == true }} if: ${{ fromJSON(needs.pre-job.outputs.should_run).i18n == true }}
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

View File

@@ -4,18 +4,12 @@ module.exports = {
if (!pkg.name) { if (!pkg.name) {
return pkg; return pkg;
} }
// make exiftool-vendored.pl a regular dependency since Docker prod
// images build with --no-optional to reduce image size
if (pkg.name === "exiftool-vendored") { if (pkg.name === "exiftool-vendored") {
const binaryPackage = if (pkg.optionalDependencies["exiftool-vendored.pl"]) {
process.platform === "win32" // make exiftool-vendored.pl a regular dependency
? "exiftool-vendored.exe" pkg.dependencies["exiftool-vendored.pl"] =
: "exiftool-vendored.pl"; pkg.optionalDependencies["exiftool-vendored.pl"];
delete pkg.optionalDependencies["exiftool-vendored.pl"];
if (pkg.optionalDependencies[binaryPackage]) {
pkg.dependencies[binaryPackage] =
pkg.optionalDependencies[binaryPackage];
delete pkg.optionalDependencies[binaryPackage];
} }
} }
return pkg; return pkg;

View File

@@ -52,7 +52,7 @@ attach-server:
docker exec -it docker_immich-server_1 sh docker exec -it docker_immich-server_1 sh
renovate: renovate:
LOG_LEVEL=debug pnpm exec renovate --platform=local --repository-cache=reset LOG_LEVEL=debug npx renovate --platform=local --repository-cache=reset
# Directories that need to be created for volumes or build output # Directories that need to be created for volumes or build output
VOLUME_DIRS = \ VOLUME_DIRS = \

View File

@@ -1 +1 @@
24.13.1 24.13.0

View File

@@ -13,23 +13,23 @@
"cli" "cli"
], ],
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.0", "@eslint/js": "^9.8.0",
"@immich/sdk": "workspace:*", "@immich/sdk": "file:../open-api/typescript-sdk",
"@types/byte-size": "^8.1.0", "@types/byte-size": "^8.1.0",
"@types/cli-progress": "^3.11.0", "@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/micromatch": "^4.0.9", "@types/micromatch": "^4.0.9",
"@types/mock-fs": "^4.13.1", "@types/mock-fs": "^4.13.1",
"@types/node": "^24.10.13", "@types/node": "^24.10.11",
"@vitest/coverage-v8": "^3.0.0", "@vitest/coverage-v8": "^3.0.0",
"byte-size": "^9.0.0", "byte-size": "^9.0.0",
"cli-progress": "^3.12.0", "cli-progress": "^3.12.0",
"commander": "^12.0.0", "commander": "^12.0.0",
"eslint": "^10.0.0", "eslint": "^9.14.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^63.0.0", "eslint-plugin-unicorn": "^62.0.0",
"globals": "^17.0.0", "globals": "^16.0.0",
"mock-fs": "^5.2.0", "mock-fs": "^5.2.0",
"prettier": "^3.7.4", "prettier": "^3.7.4",
"prettier-plugin-organize-imports": "^4.0.0", "prettier-plugin-organize-imports": "^4.0.0",
@@ -45,8 +45,8 @@
"build": "vite build", "build": "vite build",
"build:dev": "vite build --sourcemap true", "build:dev": "vite build --sourcemap true",
"lint": "eslint \"src/**/*.ts\" --max-warnings 0", "lint": "eslint \"src/**/*.ts\" --max-warnings 0",
"lint:fix": "pnpm run lint --fix", "lint:fix": "npm run lint -- --fix",
"prepack": "pnpm run build", "prepack": "npm run build",
"test": "vitest", "test": "vitest",
"test:cov": "vitest --coverage", "test:cov": "vitest --coverage",
"format": "prettier --check .", "format": "prettier --check .",
@@ -69,6 +69,6 @@
"micromatch": "^4.0.8" "micromatch": "^4.0.8"
}, },
"volta": { "volta": {
"node": "24.13.1" "node": "24.13.0"
} }
} }

View File

@@ -7,15 +7,7 @@ import { describe, expect, it, MockedFunction, vi } from 'vitest';
import { Action, checkBulkUpload, defaults, getSupportedMediaTypes, Reason } from '@immich/sdk'; import { Action, checkBulkUpload, defaults, getSupportedMediaTypes, Reason } from '@immich/sdk';
import createFetchMock from 'vitest-fetch-mock'; import createFetchMock from 'vitest-fetch-mock';
import { import { checkForDuplicates, getAlbumName, startWatch, uploadFiles, UploadOptionsDto } from 'src/commands/asset';
checkForDuplicates,
deleteFiles,
findSidecar,
getAlbumName,
startWatch,
uploadFiles,
UploadOptionsDto,
} from 'src/commands/asset';
vi.mock('@immich/sdk'); vi.mock('@immich/sdk');
@@ -317,85 +309,3 @@ describe('startWatch', () => {
await fs.promises.rm(testFolder, { recursive: true, force: true }); await fs.promises.rm(testFolder, { recursive: true, force: true });
}); });
}); });
describe('findSidecar', () => {
let testDir: string;
let testFilePath: string;
beforeEach(() => {
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-sidecar-'));
testFilePath = path.join(testDir, 'test.jpg');
fs.writeFileSync(testFilePath, 'test');
});
afterEach(() => {
fs.rmSync(testDir, { recursive: true, force: true });
});
it('should find sidecar file with photo.xmp naming convention', () => {
const sidecarPath = path.join(testDir, 'test.xmp');
fs.writeFileSync(sidecarPath, 'xmp data');
const result = findSidecar(testFilePath);
expect(result).toBe(sidecarPath);
});
it('should find sidecar file with photo.ext.xmp naming convention', () => {
const sidecarPath = path.join(testDir, 'test.jpg.xmp');
fs.writeFileSync(sidecarPath, 'xmp data');
const result = findSidecar(testFilePath);
expect(result).toBe(sidecarPath);
});
it('should prefer photo.ext.xmp over photo.xmp when both exist', () => {
const sidecarPath1 = path.join(testDir, 'test.xmp');
const sidecarPath2 = path.join(testDir, 'test.jpg.xmp');
fs.writeFileSync(sidecarPath1, 'xmp data 1');
fs.writeFileSync(sidecarPath2, 'xmp data 2');
const result = findSidecar(testFilePath);
// Should return the first one found (photo.xmp) based on the order in the code
expect(result).toBe(sidecarPath1);
});
it('should return undefined when no sidecar file exists', () => {
const result = findSidecar(testFilePath);
expect(result).toBeUndefined();
});
});
describe('deleteFiles', () => {
let testDir: string;
let testFilePath: string;
beforeEach(() => {
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-delete-'));
testFilePath = path.join(testDir, 'test.jpg');
fs.writeFileSync(testFilePath, 'test');
});
afterEach(() => {
fs.rmSync(testDir, { recursive: true, force: true });
});
it('should delete asset and sidecar file when main file is deleted', async () => {
const sidecarPath = path.join(testDir, 'test.xmp');
fs.writeFileSync(sidecarPath, 'xmp data');
await deleteFiles([{ id: 'test-id', filepath: testFilePath }], [], { delete: true, concurrency: 1 });
expect(fs.existsSync(testFilePath)).toBe(false);
expect(fs.existsSync(sidecarPath)).toBe(false);
});
it('should not delete sidecar file when delete option is false', async () => {
const sidecarPath = path.join(testDir, 'test.xmp');
fs.writeFileSync(sidecarPath, 'xmp data');
await deleteFiles([{ id: 'test-id', filepath: testFilePath }], [], { delete: false, concurrency: 1 });
expect(fs.existsSync(testFilePath)).toBe(true);
expect(fs.existsSync(sidecarPath)).toBe(true);
});
});

View File

@@ -17,7 +17,7 @@ import { Matcher, watch as watchFs } from 'chokidar';
import { MultiBar, Presets, SingleBar } from 'cli-progress'; import { MultiBar, Presets, SingleBar } from 'cli-progress';
import { chunk } from 'lodash-es'; import { chunk } from 'lodash-es';
import micromatch from 'micromatch'; import micromatch from 'micromatch';
import { Stats, createReadStream, existsSync } from 'node:fs'; import { Stats, createReadStream } from 'node:fs';
import { stat, unlink } from 'node:fs/promises'; import { stat, unlink } from 'node:fs/promises';
import path, { basename } from 'node:path'; import path, { basename } from 'node:path';
import { Queue } from 'src/queue'; import { Queue } from 'src/queue';
@@ -403,6 +403,23 @@ export const uploadFiles = async (
const uploadFile = async (input: string, stats: Stats): Promise<AssetMediaResponseDto> => { const uploadFile = async (input: string, stats: Stats): Promise<AssetMediaResponseDto> => {
const { baseUrl, headers } = defaults; const { baseUrl, headers } = defaults;
const assetPath = path.parse(input);
const noExtension = path.join(assetPath.dir, assetPath.name);
const sidecarsFiles = await Promise.all(
// XMP sidecars can come in two filename formats. For a photo named photo.ext, the filenames are photo.ext.xmp and photo.xmp
[`${noExtension}.xmp`, `${input}.xmp`].map(async (sidecarPath) => {
try {
const stats = await stat(sidecarPath);
return new UploadFile(sidecarPath, stats.size);
} catch {
return false;
}
}),
);
const sidecarData = sidecarsFiles.find((file): file is UploadFile => file !== false);
const formData = new FormData(); const formData = new FormData();
formData.append('deviceAssetId', `${basename(input)}-${stats.size}`.replaceAll(/\s+/g, '')); formData.append('deviceAssetId', `${basename(input)}-${stats.size}`.replaceAll(/\s+/g, ''));
formData.append('deviceId', 'CLI'); formData.append('deviceId', 'CLI');
@@ -412,15 +429,8 @@ const uploadFile = async (input: string, stats: Stats): Promise<AssetMediaRespon
formData.append('isFavorite', 'false'); formData.append('isFavorite', 'false');
formData.append('assetData', new UploadFile(input, stats.size)); formData.append('assetData', new UploadFile(input, stats.size));
const sidecarPath = findSidecar(input); if (sidecarData) {
if (sidecarPath) { formData.append('sidecarData', sidecarData);
try {
const stats = await stat(sidecarPath);
const sidecarData = new UploadFile(sidecarPath, stats.size);
formData.append('sidecarData', sidecarData);
} catch {
// noop
}
} }
const response = await fetch(`${baseUrl}/assets`, { const response = await fetch(`${baseUrl}/assets`, {
@@ -436,19 +446,7 @@ const uploadFile = async (input: string, stats: Stats): Promise<AssetMediaRespon
return response.json(); return response.json();
}; };
export const findSidecar = (filepath: string): string | undefined => { const deleteFiles = async (uploaded: Asset[], duplicates: Asset[], options: UploadOptionsDto): Promise<void> => {
const assetPath = path.parse(filepath);
const noExtension = path.join(assetPath.dir, assetPath.name);
// XMP sidecars can come in two filename formats. For a photo named photo.ext, the filenames are photo.ext.xmp and photo.xmp
for (const sidecarPath of [`${noExtension}.xmp`, `${filepath}.xmp`]) {
if (existsSync(sidecarPath)) {
return sidecarPath;
}
}
};
export const deleteFiles = async (uploaded: Asset[], duplicates: Asset[], options: UploadOptionsDto): Promise<void> => {
let fileCount = 0; let fileCount = 0;
if (options.delete) { if (options.delete) {
fileCount += uploaded.length; fileCount += uploaded.length;
@@ -476,15 +474,7 @@ export const deleteFiles = async (uploaded: Asset[], duplicates: Asset[], option
const chunkDelete = async (files: Asset[]) => { const chunkDelete = async (files: Asset[]) => {
for (const assetBatch of chunk(files, options.concurrency)) { for (const assetBatch of chunk(files, options.concurrency)) {
await Promise.all( await Promise.all(assetBatch.map((input: Asset) => unlink(input.filepath)));
assetBatch.map(async (input: Asset) => {
await unlink(input.filepath);
const sidecarPath = findSidecar(input.filepath);
if (sidecarPath) {
await unlink(sidecarPath);
}
}),
);
deletionProgress.update(assetBatch.length); deletionProgress.update(assetBatch.length);
} }
}; };

View File

@@ -14,65 +14,33 @@
name: immich-dev name: immich-dev
services: services:
immich-app-base:
profiles: ['_base']
tmpfs:
- /tmp
volumes:
- ..:/usr/src/app
- pnpm_cache:/buildcache/pnpm_cache
- server_node_modules:/usr/src/app/server/node_modules
- web_node_modules:/usr/src/app/web/node_modules
- github_node_modules:/usr/src/app/.github/node_modules
- cli_node_modules:/usr/src/app/cli/node_modules
- docs_node_modules:/usr/src/app/docs/node_modules
- e2e_node_modules:/usr/src/app/e2e/node_modules
- sdk_node_modules:/usr/src/app/open-api/typescript-sdk/node_modules
- app_node_modules:/usr/src/app/node_modules
- sveltekit:/usr/src/app/web/.svelte-kit
- coverage:/usr/src/app/web/coverage
immich-init:
extends:
service: immich-app-base
profiles: !reset []
container_name: immich_init
image: immich-server-dev:latest
build:
context: ../
dockerfile: server/Dockerfile.dev
target: dev
command:
- |
pnpm install
touch /tmp/init-complete
exec tail -f /dev/null
volumes:
- pnpm_store_server:/buildcache/pnpm-store
restart: 'no'
healthcheck:
test: ['CMD', 'test', '-f', '/tmp/init-complete']
interval: 2s
timeout: 3s
retries: 300
start_period: 300s
immich-server: immich-server:
extends:
service: immich-app-base
profiles: !reset []
container_name: immich_server container_name: immich_server
command: ['immich-dev'] command: ['immich-dev']
image: immich-server-dev:latest image: immich-server-dev:latest
# extends:
# file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
build: build:
context: ../ context: ../
dockerfile: server/Dockerfile.dev dockerfile: server/Dockerfile.dev
target: dev target: dev
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- ..:/usr/src/app
- ${UPLOAD_LOCATION}/photos:/data - ${UPLOAD_LOCATION}/photos:/data
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
- pnpm_store_server:/buildcache/pnpm-store - pnpm-store:/usr/src/app/.pnpm-store
- server-node_modules:/usr/src/app/server/node_modules
- web-node_modules:/usr/src/app/web/node_modules
- github-node_modules:/usr/src/app/.github/node_modules
- cli-node_modules:/usr/src/app/cli/node_modules
- docs-node_modules:/usr/src/app/docs/node_modules
- e2e-node_modules:/usr/src/app/e2e/node_modules
- sdk-node_modules:/usr/src/app/open-api/typescript-sdk/node_modules
- app-node_modules:/usr/src/app/node_modules
- sveltekit:/usr/src/app/web/.svelte-kit
- coverage:/usr/src/app/web/coverage
- ../plugins:/build/corePlugin - ../plugins:/build/corePlugin
env_file: env_file:
- .env - .env
@@ -95,8 +63,6 @@ services:
- 9231:9231 - 9231:9231
- 2283:2283 - 2283:2283
depends_on: depends_on:
immich-init:
condition: service_healthy
redis: redis:
condition: service_started condition: service_started
database: database:
@@ -105,9 +71,6 @@ services:
disable: false disable: false
immich-web: immich-web:
extends:
service: immich-app-base
profiles: !reset []
container_name: immich_web container_name: immich_web
image: immich-web-dev:latest image: immich-web-dev:latest
build: build:
@@ -121,11 +84,20 @@ services:
- 3000:3000 - 3000:3000
- 24678:24678 - 24678:24678
volumes: volumes:
- pnpm_store_web:/buildcache/pnpm-store - ..:/usr/src/app
- pnpm-store:/usr/src/app/.pnpm-store
- server-node_modules:/usr/src/app/server/node_modules
- web-node_modules:/usr/src/app/web/node_modules
- github-node_modules:/usr/src/app/.github/node_modules
- cli-node_modules:/usr/src/app/cli/node_modules
- docs-node_modules:/usr/src/app/docs/node_modules
- e2e-node_modules:/usr/src/app/e2e/node_modules
- sdk-node_modules:/usr/src/app/open-api/typescript-sdk/node_modules
- app-node_modules:/usr/src/app/node_modules
- sveltekit:/usr/src/app/web/.svelte-kit
- coverage:/usr/src/app/web/coverage
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
immich-init:
condition: service_healthy
immich-server: immich-server:
condition: service_started condition: service_started
@@ -144,7 +116,7 @@ services:
- 3003:3003 - 3003:3003
volumes: volumes:
- ../machine-learning/immich_ml:/usr/src/immich_ml - ../machine-learning/immich_ml:/usr/src/immich_ml
- model_cache:/cache - model-cache:/cache
env_file: env_file:
- .env - .env
depends_on: depends_on:
@@ -184,7 +156,7 @@ services:
# image: prom/prometheus # image: prom/prometheus
# volumes: # volumes:
# - ./prometheus.yml:/etc/prometheus/prometheus.yml # - ./prometheus.yml:/etc/prometheus/prometheus.yml
# - prometheus_data:/prometheus # - prometheus-data:/prometheus
# first login uses admin/admin # first login uses admin/admin
# add data source for http://immich-prometheus:9090 to get started # add data source for http://immich-prometheus:9090 to get started
@@ -195,22 +167,20 @@ services:
# - 3000:3000 # - 3000:3000
# image: grafana/grafana:10.3.3-ubuntu # image: grafana/grafana:10.3.3-ubuntu
# volumes: # volumes:
# - grafana_data:/var/lib/grafana # - grafana-data:/var/lib/grafana
volumes: volumes:
model_cache: model-cache:
prometheus_data: prometheus-data:
grafana_data: grafana-data:
pnpm_cache: pnpm-store:
pnpm_store_server: server-node_modules:
pnpm_store_web: web-node_modules:
server_node_modules: github-node_modules:
web_node_modules: cli-node_modules:
github_node_modules: docs-node_modules:
cli_node_modules: e2e-node_modules:
docs_node_modules: sdk-node_modules:
e2e_node_modules: app-node_modules:
sdk_node_modules:
app_node_modules:
sveltekit: sveltekit:
coverage: coverage:

View File

@@ -1 +1 @@
24.13.1 24.13.0

View File

@@ -44,7 +44,7 @@ While this guide focuses on VS Code, you have many options for Dev Container dev
**Self-Hostable Options:** **Self-Hostable Options:**
- [Coder](https://coder.com) - Enterprise-focused, requires Terraform knowledge, self-managed - [Coder](https://coder.com) - Enterprise-focused, requires Terraform knowledge, self-managed
- [DevPod](https://devpod.sh) - Client-only tool with excellent devcontainer.json support, works with any provider (local, cloud, or on-premise). Check [quick-start guide](#quick-start-guide-for-devpod-with-docker) - [DevPod](https://devpod.sh) - Client-only tool with excellent devcontainer.json support, works with any provider (local, cloud, or on-premise)
::: :::
## Dev Container Services ## Dev Container Services
@@ -408,27 +408,7 @@ If you encounter issues:
1. Check container logs: View → Output → Select "Dev Containers" 1. Check container logs: View → Output → Select "Dev Containers"
2. Rebuild without cache: "Dev Containers: Rebuild Container Without Cache" 2. Rebuild without cache: "Dev Containers: Rebuild Container Without Cache"
3. Review [common Docker issues](https://docs.docker.com/desktop/troubleshoot/) 3. Review [common Docker issues](https://docs.docker.com/desktop/troubleshoot/)
4. Ask in [Discord](https://discord.immich.app) `#contributing` channel 4. Ask in [Discord](https://discord.immich.app) `#help-desk-support` channel
### Quick-start guide for DevPod with docker
You will need DevPod CLI (check [DevPod CLI installation guide](https://devpod.sh/docs/getting-started/install)) and Docker Desktop.
```sh
# Step 1: Clone the Repository
git clone https://github.com/immich-app/immich.git
cd immich
# Step 2: Prepare DevPod (if you haven't already)
devpod provider add docker
devpod provider use docker
# Step 3: Build 'immich-server-dev' docker image first manually
docker build -f server/Dockerfile.dev -t immich-server-dev .
# Step 4: Now you can start devcontainer
devpod up .
```
## Mobile Development ## Mobile Development

View File

@@ -80,10 +80,6 @@ There is an automatic scan job that is scheduled to run once a day. Its schedule
This job also cleans up any libraries stuck in deletion. It is possible to trigger the cleanup by clicking "Scan all libraries" in the library management page. This job also cleans up any libraries stuck in deletion. It is possible to trigger the cleanup by clicking "Scan all libraries" in the library management page.
### Deleting a Library
When deleting an external library, all assets inside are immediately deleted along with the library. Note that while a library can take a long time to fully delete in the background, it is immediately removed from the library list. If the deletion process is interrupted (for example, due to server restart), it will be cleaned up in the next nightly cron job. The cleanup process can also be manually initiated by clicking the "Scan All Libraries" button in the library list.
## Usage ## Usage
Let's show a concrete example where we add an existing gallery to Immich. Here, we have the following folders we want to add: Let's show a concrete example where we add an existing gallery to Immich. Here, we have the following folders we want to add:

View File

@@ -38,7 +38,6 @@ For the full list, refer to the [Immich source code](https://github.com/immich-a
| `MP2T` | `.mts` `.m2ts` `.m2t` | :white_check_mark: | | | `MP2T` | `.mts` `.m2ts` `.m2t` | :white_check_mark: | |
| `MP4` | `.mp4` `.insv` | :white_check_mark: | | | `MP4` | `.mp4` `.insv` | :white_check_mark: | |
| `MPEG` | `.mpg` `.mpe` `.mpeg` | :white_check_mark: | | | `MPEG` | `.mpg` `.mpe` `.mpeg` | :white_check_mark: | |
| `MXF` | `.mxf` | :white_check_mark: | |
| `QUICKTIME` | `.mov` | :white_check_mark: | | | `QUICKTIME` | `.mov` | :white_check_mark: | |
| `WEBM` | `.webm` | :white_check_mark: | | | `WEBM` | `.webm` | :white_check_mark: | |
| `WMV` | `.wmv` | :white_check_mark: | | | `WMV` | `.wmv` | :white_check_mark: | |

View File

@@ -8,8 +8,7 @@ A config file can be provided as an alternative to the UI configuration.
### Step 1 - Create a new config file ### Step 1 - Create a new config file
In JSON format, create a new config file (e.g. `immich.json`) and put it in a location mounted in the container that can be accessed by Immich. In JSON format, create a new config file (e.g. `immich.json`) and put it in a location that can be accessed by Immich.
YAML-formatted config files are also supported.
The default configuration looks like this: The default configuration looks like this:
<details> <details>
@@ -252,15 +251,6 @@ So you can just grab it from there, paste it into a file and you're pretty much
In your `.env` file, set the variable `IMMICH_CONFIG_FILE` to the path of your config. In your `.env` file, set the variable `IMMICH_CONFIG_FILE` to the path of your config.
For more information, refer to the [Environment Variables](/install/environment-variables.md) section. For more information, refer to the [Environment Variables](/install/environment-variables.md) section.
:::info Docker Compose :::tip
In your `.env` file, the variables `UPLOAD_LOCATION` and `DB_DATA_LOCATION` concern the location on the host. YAML-formatted config files are also supported.
However, the variable `IMMICH_CONFIG_FILE` concerns the location inside the container, and informs the `immich-server` container that a configuration file is present. :::
It is recommended to reuse this variable in your `docker-compose.yml`:
```yaml
volumes:
- ./configuration.yml:${IMMICH_CONFIG_FILE}
```
::

View File

@@ -8,6 +8,8 @@ sidebar_position: 85
This is a community contribution and not officially supported by the Immich team, but included here for convenience. 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/). 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. 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.

View File

@@ -23,9 +23,3 @@ run = "prettier --check ."
[tasks."format-fix"] [tasks."format-fix"]
env._.path = "./node_modules/.bin" env._.path = "./node_modules/.bin"
run = "prettier --write ." run = "prettier --write ."
[tasks.deploy]
run = "wrangler pages deploy build --project-name=${PROJECT_NAME} --branch=${BRANCH_NAME}"
[tools]
wrangler = "4.66.0"

View File

@@ -8,7 +8,7 @@
"format:fix": "prettier --write .", "format:fix": "prettier --write .",
"start": "docusaurus start --port 3005", "start": "docusaurus start --port 3005",
"copy:openapi": "jq -c < ../open-api/immich-openapi-specs.json > ./static/openapi.json || exit 0", "copy:openapi": "jq -c < ../open-api/immich-openapi-specs.json > ./static/openapi.json || exit 0",
"build": "pnpm run copy:openapi && docusaurus build", "build": "npm run copy:openapi && docusaurus build",
"swizzle": "docusaurus swizzle", "swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy", "deploy": "docusaurus deploy",
"clear": "docusaurus clear", "clear": "docusaurus clear",
@@ -58,6 +58,6 @@
"node": ">=20" "node": ">=20"
}, },
"volta": { "volta": {
"node": "24.13.1" "node": "24.13.0"
} }
} }

View File

@@ -1 +1 @@
24.13.1 24.13.0

View File

@@ -1,77 +1,86 @@
name: immich-e2e name: immich-e2e
services: services:
immich-app-base:
extends:
file: ../docker/docker-compose.dev.yml
service: immich-app-base
immich-init:
extends:
file: ../docker/docker-compose.dev.yml
service: immich-init
container_name: immich-e2e-init
immich-server: immich-server:
extends:
file: ../docker/docker-compose.dev.yml
service: immich-server
container_name: immich-e2e-server container_name: immich-e2e-server
ports: !reset [] command: ['immich-dev']
env_file: !reset [] image: immich-server-dev:latest
build:
context: ../
dockerfile: server/Dockerfile.dev
target: dev
environment: environment:
DB_HOSTNAME: database - DB_HOSTNAME=database
DB_USERNAME: postgres - DB_USERNAME=postgres
DB_PASSWORD: postgres - DB_PASSWORD=postgres
DB_DATABASE_NAME: immich - DB_DATABASE_NAME=immich
IMMICH_MACHINE_LEARNING_ENABLED: 'false' - IMMICH_MACHINE_LEARNING_ENABLED=false
IMMICH_TELEMETRY_INCLUDE: all - IMMICH_TELEMETRY_INCLUDE=all
IMMICH_ENV: testing - IMMICH_ENV=testing
IMMICH_PORT: '2285' - IMMICH_PORT=2285
IMMICH_IGNORE_MOUNT_CHECK_ERRORS: 'true' - IMMICH_IGNORE_MOUNT_CHECK_ERRORS=true
volumes: volumes:
- ./test-assets:/test-assets - ./test-assets:/test-assets
- ..:/usr/src/app
- ${UPLOAD_LOCATION}/photos:/data
- /etc/localtime:/etc/localtime:ro
- pnpm-store:/usr/src/app/.pnpm-store
- server-node_modules:/usr/src/app/server/node_modules
- web-node_modules:/usr/src/app/web/node_modules
- github-node_modules:/usr/src/app/.github/node_modules
- cli-node_modules:/usr/src/app/cli/node_modules
- docs-node_modules:/usr/src/app/docs/node_modules
- e2e-node_modules:/usr/src/app/e2e/node_modules
- sdk-node_modules:/usr/src/app/open-api/typescript-sdk/node_modules
- app-node_modules:/usr/src/app/node_modules
- sveltekit:/usr/src/app/web/.svelte-kit
- coverage:/usr/src/app/web/coverage
- ../plugins:/build/corePlugin
depends_on: depends_on:
immich-init:
condition: service_healthy
redis: redis:
condition: service_started condition: service_started
database: database:
condition: service_healthy condition: service_healthy
immich-web: immich-web:
extends:
file: ../docker/docker-compose.dev.yml
service: immich-web
container_name: immich-e2e-web container_name: immich-e2e-web
ports: !override image: immich-web-dev:latest
build:
context: ../
dockerfile: server/Dockerfile.dev
target: dev
command: ['immich-web']
ports:
- 2285:3000 - 2285:3000
environment: environment:
IMMICH_SERVER_URL: http://immich-server:2285/ - IMMICH_SERVER_URL=http://immich-server:2285/
depends_on: volumes:
immich-init: - ..:/usr/src/app
condition: service_healthy - pnpm-store:/usr/src/app/.pnpm-store
- server-node_modules:/usr/src/app/server/node_modules
- web-node_modules:/usr/src/app/web/node_modules
- github-node_modules:/usr/src/app/.github/node_modules
- cli-node_modules:/usr/src/app/cli/node_modules
- docs-node_modules:/usr/src/app/docs/node_modules
- e2e-node_modules:/usr/src/app/e2e/node_modules
- sdk-node_modules:/usr/src/app/open-api/typescript-sdk/node_modules
- app-node_modules:/usr/src/app/node_modules
- sveltekit:/usr/src/app/web/.svelte-kit
- coverage:/usr/src/app/web/coverage
restart: unless-stopped restart: unless-stopped
redis: redis:
extends: image: redis:6.2-alpine@sha256:46884be93652d02a96a176ccf173d1040bef365c5706aa7b6a1931caec8bfeef
file: ../docker/docker-compose.dev.yml
service: redis
container_name: immich-e2e-redis
database: database:
extends: image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:6f3e9d2c2177af16c2988ff71425d79d89ca630ec2f9c8db03209ab716542338
file: ../docker/docker-compose.dev.yml
service: database
container_name: immich-e2e-postgres
command: -c fsync=off -c shared_preload_libraries=vchord.so -c config_file=/var/lib/postgresql/data/postgresql.conf command: -c fsync=off -c shared_preload_libraries=vchord.so -c config_file=/var/lib/postgresql/data/postgresql.conf
env_file: !reset []
ports: !override
- 5435:5432
environment: environment:
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres POSTGRES_USER: postgres
POSTGRES_DB: immich POSTGRES_DB: immich
ports:
- 5435:5432
healthcheck: healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres -d immich'] test: ['CMD-SHELL', 'pg_isready -U postgres -d immich']
interval: 1s interval: 1s
@@ -80,19 +89,17 @@ services:
start_period: 10s start_period: 10s
volumes: volumes:
model_cache: model-cache:
prometheus_data: prometheus-data:
grafana_data: grafana-data:
pnpm_cache: pnpm-store:
pnpm_store_server: server-node_modules:
pnpm_store_web: web-node_modules:
server_node_modules: github-node_modules:
web_node_modules: cli-node_modules:
github_node_modules: docs-node_modules:
cli_node_modules: e2e-node_modules:
docs_node_modules: sdk-node_modules:
e2e_node_modules: app-node_modules:
sdk_node_modules:
app_node_modules:
sveltekit: sveltekit:
coverage: coverage:

View File

@@ -2,7 +2,6 @@ name: immich-e2e
services: services:
e2e-auth-server: e2e-auth-server:
container_name: immich-e2e-auth-server
build: build:
context: ../e2e-auth-server context: ../e2e-auth-server
ports: ports:
@@ -11,6 +10,7 @@ services:
immich-server: immich-server:
container_name: immich-e2e-server container_name: immich-e2e-server
image: immich-server:latest image: immich-server:latest
shm_size: 128mb
build: build:
context: ../ context: ../
dockerfile: server/Dockerfile dockerfile: server/Dockerfile
@@ -23,15 +23,15 @@ services:
- BUILD_SOURCE_REF=e2e - BUILD_SOURCE_REF=e2e
- BUILD_SOURCE_COMMIT=e2eeeeeeeeeeeeeeeeee - BUILD_SOURCE_COMMIT=e2eeeeeeeeeeeeeeeeee
environment: environment:
DB_HOSTNAME: database - DB_HOSTNAME=database
DB_USERNAME: postgres - DB_USERNAME=postgres
DB_PASSWORD: postgres - DB_PASSWORD=postgres
DB_DATABASE_NAME: immich - DB_DATABASE_NAME=immich
IMMICH_MACHINE_LEARNING_ENABLED: 'false' - IMMICH_MACHINE_LEARNING_ENABLED=false
IMMICH_TELEMETRY_INCLUDE: all - IMMICH_TELEMETRY_INCLUDE=all
IMMICH_ENV: testing - IMMICH_ENV=testing
IMMICH_PORT: '2285' - IMMICH_PORT=2285
IMMICH_IGNORE_MOUNT_CHECK_ERRORS: 'true' - IMMICH_IGNORE_MOUNT_CHECK_ERRORS=true
volumes: volumes:
- ./test-assets:/test-assets - ./test-assets:/test-assets
depends_on: depends_on:
@@ -43,14 +43,10 @@ services:
- 2285:2285 - 2285:2285
redis: redis:
container_name: immich-e2e-redis image: redis:6.2-alpine@sha256:46884be93652d02a96a176ccf173d1040bef365c5706aa7b6a1931caec8bfeef
image: docker.io/valkey/valkey:9@sha256:930b41430fb727f533c5982fe509b6f04233e26d0f7354e04de4b0d5c706e44e
healthcheck:
test: redis-cli ping || exit 1
database: database:
container_name: immich-e2e-postgres image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:6f3e9d2c2177af16c2988ff71425d79d89ca630ec2f9c8db03209ab716542338
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:bcf63357191b76a916ae5eb93464d65c07511da41e3bf7a8416db519b40b1c23
command: -c fsync=off -c shared_preload_libraries=vchord.so -c config_file=/var/lib/postgresql/data/postgresql.conf command: -c fsync=off -c shared_preload_libraries=vchord.so -c config_file=/var/lib/postgresql/data/postgresql.conf
environment: environment:
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres

View File

@@ -7,42 +7,37 @@
"scripts": { "scripts": {
"test": "vitest --run", "test": "vitest --run",
"test:watch": "vitest", "test:watch": "vitest",
"test:maintenance": "vitest --run --config vitest.maintenance.config.ts", "test:web": "npx playwright test",
"test:web": "pnpm exec playwright test --project=web", "start:web": "npx playwright test --ui",
"test:web:maintenance": "pnpm exec playwright test --project=maintenance",
"test:web:ui": "pnpm exec playwright test --project=ui",
"start:web": "pnpm exec playwright test --ui --project=web",
"start:web:maintenance": "pnpm exec playwright test --ui --project=maintenance",
"start:web:ui": "pnpm exec playwright test --ui --project=ui",
"format": "prettier --check .", "format": "prettier --check .",
"format:fix": "prettier --write .", "format:fix": "prettier --write .",
"lint": "eslint \"src/**/*.ts\" --max-warnings 0", "lint": "eslint \"src/**/*.ts\" --max-warnings 0",
"lint:fix": "pnpm run lint --fix", "lint:fix": "npm run lint -- --fix",
"check": "tsc --noEmit" "check": "tsc --noEmit"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.0", "@eslint/js": "^9.8.0",
"@faker-js/faker": "^10.1.0", "@faker-js/faker": "^10.1.0",
"@immich/cli": "workspace:*", "@immich/cli": "file:../cli",
"@immich/e2e-auth-server": "workspace:*", "@immich/e2e-auth-server": "file:../e2e-auth-server",
"@immich/sdk": "workspace:*", "@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1", "@playwright/test": "^1.44.1",
"@socket.io/component-emitter": "^3.1.2", "@socket.io/component-emitter": "^3.1.2",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/node": "^24.10.13", "@types/node": "^24.10.11",
"@types/pg": "^8.15.1", "@types/pg": "^8.15.1",
"@types/pngjs": "^6.0.4", "@types/pngjs": "^6.0.4",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"eslint": "^10.0.0", "eslint": "^9.14.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^63.0.0", "eslint-plugin-unicorn": "^62.0.0",
"exiftool-vendored": "^35.0.0", "exiftool-vendored": "^34.3.0",
"globals": "^17.0.0", "globals": "^16.0.0",
"luxon": "^3.4.4", "luxon": "^3.4.4",
"pg": "^8.11.3", "pg": "^8.11.3",
"pngjs": "^7.0.0", "pngjs": "^7.0.0",
@@ -57,6 +52,6 @@
"vitest": "^3.0.0" "vitest": "^3.0.0"
}, },
"volta": { "volta": {
"node": "24.13.1" "node": "24.13.0"
} }
} }

View File

@@ -3,7 +3,7 @@ import dotenv from 'dotenv';
import { cpus } from 'node:os'; import { cpus } from 'node:os';
import { resolve } from 'node:path'; import { resolve } from 'node:path';
dotenv.config({ quiet: true, path: resolve(import.meta.dirname, '.env') }); dotenv.config({ path: resolve(import.meta.dirname, '.env') });
export const playwrightHost = process.env.PLAYWRIGHT_HOST ?? '127.0.0.1'; export const playwrightHost = process.env.PLAYWRIGHT_HOST ?? '127.0.0.1';
export const playwrightDbHost = process.env.PLAYWRIGHT_DB_HOST ?? '127.0.0.1'; export const playwrightDbHost = process.env.PLAYWRIGHT_DB_HOST ?? '127.0.0.1';
@@ -48,7 +48,7 @@ const config: PlaywrightTestConfig = {
{ {
name: 'maintenance', name: 'maintenance',
use: { ...devices['Desktop Chrome'] }, use: { ...devices['Desktop Chrome'] },
testDir: './src/specs/maintenance/web', testDir: './src/specs/maintenance',
workers: 1, workers: 1,
}, },
], ],

View File

@@ -253,8 +253,7 @@ describe('/asset', () => {
expect(status).toBe(200); expect(status).toBe(200);
expect(body.id).toEqual(facesAsset.id); expect(body.id).toEqual(facesAsset.id);
const sortedPeople = body.people.toSorted((a: any, b: any) => a.name.localeCompare(b.name)); expect(body.people).toMatchObject(expectedFaces);
expect(sortedPeople).toMatchObject(expectedFaces);
}); });
}); });

View File

@@ -45,7 +45,8 @@ test.describe('Shared Links', () => {
await page.goto(`/share/${sharedLink.key}`); await page.goto(`/share/${sharedLink.key}`);
await page.getByRole('heading', { name: 'Test Album' }).waitFor(); await page.getByRole('heading', { name: 'Test Album' }).waitFor();
await page.locator(`[data-asset-id="${asset.id}"]`).hover(); await page.locator(`[data-asset-id="${asset.id}"]`).hover();
await page.waitForSelector(`[data-asset-id="${asset.id}"] [role="checkbox"]`); await page.waitForSelector('[data-group] svg');
await page.getByRole('checkbox').click();
await Promise.all([page.waitForEvent('download'), page.getByRole('button', { name: 'Download' }).click()]); await Promise.all([page.waitForEvent('download'), page.getByRole('button', { name: 'Download' }).click()]);
}); });

View File

@@ -438,7 +438,7 @@ test.describe('Timeline', () => {
const asset = getAsset(timelineRestData, album.assetIds[0])!; const asset = getAsset(timelineRestData, album.assetIds[0])!;
await pageUtils.goToAsset(page, asset.fileCreatedAt); await pageUtils.goToAsset(page, asset.fileCreatedAt);
await thumbnailUtils.expectInViewport(page, asset.id); await thumbnailUtils.expectInViewport(page, asset.id);
await thumbnailUtils.expectSelectedDisabled(page, asset.id); await thumbnailUtils.expectSelectedReadonly(page, asset.id);
}); });
test('Add photos to album', async ({ page }) => { test('Add photos to album', async ({ page }) => {
const album = timelineRestData.album; const album = timelineRestData.album;
@@ -447,7 +447,7 @@ test.describe('Timeline', () => {
const asset = getAsset(timelineRestData, album.assetIds[0])!; const asset = getAsset(timelineRestData, album.assetIds[0])!;
await pageUtils.goToAsset(page, asset.fileCreatedAt); await pageUtils.goToAsset(page, asset.fileCreatedAt);
await thumbnailUtils.expectInViewport(page, asset.id); await thumbnailUtils.expectInViewport(page, asset.id);
await thumbnailUtils.expectSelectedDisabled(page, asset.id); await thumbnailUtils.expectSelectedReadonly(page, asset.id);
await pageUtils.selectDay(page, 'Tue, Feb 27, 2024'); await pageUtils.selectDay(page, 'Tue, Feb 27, 2024');
const put = pageRoutePromise(page, `**/api/albums/${album.id}/assets`, async (route, request) => { const put = pageRoutePromise(page, `**/api/albums/${album.id}/assets`, async (route, request) => {
const requestJson = request.postDataJSON(); const requestJson = request.postDataJSON();

View File

@@ -65,7 +65,7 @@ export const thumbnailUtils = {
return page.locator(`[data-thumbnail-focus-container][data-asset="${assetId}"] button`); return page.locator(`[data-thumbnail-focus-container][data-asset="${assetId}"] button`);
}, },
selectedAsset(page: Page) { selectedAsset(page: Page) {
return page.locator('[data-thumbnail-focus-container][data-selected]'); return page.locator('[data-thumbnail-focus-container]:has(button[aria-checked])');
}, },
async clickAssetId(page: Page, assetId: string) { async clickAssetId(page: Page, assetId: string) {
await thumbnailUtils.withAssetId(page, assetId).click(); await thumbnailUtils.withAssetId(page, assetId).click();
@@ -102,9 +102,12 @@ export const thumbnailUtils = {
async expectThumbnailIsNotArchive(page: Page, assetId: string) { async expectThumbnailIsNotArchive(page: Page, assetId: string) {
await expect(thumbnailUtils.withAssetId(page, assetId).locator('[data-icon-archive]')).toHaveCount(0); await expect(thumbnailUtils.withAssetId(page, assetId).locator('[data-icon-archive]')).toHaveCount(0);
}, },
async expectSelectedDisabled(page: Page, assetId: string) { async expectSelectedReadonly(page: Page, assetId: string) {
// todo - need a data attribute for selected
await expect( await expect(
page.locator(`[data-thumbnail-focus-container][data-asset="${assetId}"][data-selected][data-disabled]`), page.locator(
`[data-thumbnail-focus-container][data-asset="${assetId}"] > .group.cursor-not-allowed > .rounded-xl`,
),
).toBeVisible(); ).toBeVisible();
}, },
async expectTimelineHasOnScreenAssets(page: Page) { async expectTimelineHasOnScreenAssets(page: Page) {

View File

@@ -1,20 +1,15 @@
import { defineConfig } from 'vitest/config'; import { defineConfig } from 'vitest/config';
const skipDockerSetup = process.env.VITEST_DISABLE_DOCKER_SETUP === 'true'; // skip `docker compose up` if `make e2e` was already run
// skip `docker compose up` if `make e2e` was already run or if VITEST_DISABLE_DOCKER_SETUP is set
const globalSetup: string[] = []; const globalSetup: string[] = [];
if (!skipDockerSetup) { try {
try { await fetch('http://127.0.0.1:2285/api/server/ping');
await fetch('http://127.0.0.1:2285/api/server/ping'); } catch {
} catch { globalSetup.push('src/docker-compose.ts');
globalSetup.push('src/docker-compose.ts');
}
} }
export default defineConfig({ export default defineConfig({
test: { test: {
retry: process.env.CI ? 4 : 0,
include: ['src/specs/server/**/*.e2e-spec.ts'], include: ['src/specs/server/**/*.e2e-spec.ts'],
globalSetup, globalSetup,
testTimeout: 15_000, testTimeout: 15_000,

View File

@@ -1,28 +0,0 @@
import { defineConfig } from 'vitest/config';
const skipDockerSetup = process.env.VITEST_DISABLE_DOCKER_SETUP === 'true';
// skip `docker compose up` if `make e2e` was already run or if VITEST_DISABLE_DOCKER_SETUP is set
const globalSetup: string[] = [];
if (!skipDockerSetup) {
try {
await fetch('http://127.0.0.1:2285/api/server/ping');
} catch {
globalSetup.push('src/docker-compose.ts');
}
}
export default defineConfig({
test: {
retry: process.env.CI ? 4 : 0,
include: ['src/specs/maintenance/server/**/*.e2e-spec.ts'],
globalSetup,
testTimeout: 15_000,
pool: 'threads',
poolOptions: {
threads: {
singleThread: true,
},
},
},
});

View File

@@ -1050,7 +1050,6 @@
"cant_get_number_of_comments": "Can't get number of comments", "cant_get_number_of_comments": "Can't get number of comments",
"cant_search_people": "Can't search people", "cant_search_people": "Can't search people",
"cant_search_places": "Can't search places", "cant_search_places": "Can't search places",
"enable_webgl_for_map": "Enable WebGL to load the map.{isAdmin, select, true { To hide this warning, disable the map feature.} other {}}",
"error_adding_assets_to_album": "Error adding assets to album", "error_adding_assets_to_album": "Error adding assets to album",
"error_adding_users_to_album": "Error adding users to album", "error_adding_users_to_album": "Error adding users to album",
"error_deleting_shared_user": "Error deleting shared user", "error_deleting_shared_user": "Error deleting shared user",
@@ -1075,7 +1074,6 @@
"failed_to_update_notification_status": "Failed to update notification status", "failed_to_update_notification_status": "Failed to update notification status",
"incorrect_email_or_password": "Incorrect email or password", "incorrect_email_or_password": "Incorrect email or password",
"library_folder_already_exists": "This import path already exists.", "library_folder_already_exists": "This import path already exists.",
"page_not_found": "Page not found :/",
"paths_validation_failed": "{paths, plural, one {# path} other {# paths}} failed validation", "paths_validation_failed": "{paths, plural, one {# path} other {# paths}} failed validation",
"profile_picture_transparent_pixels": "Profile pictures cannot have transparent pixels. Please zoom in and/or move the image.", "profile_picture_transparent_pixels": "Profile pictures cannot have transparent pixels. Please zoom in and/or move the image.",
"quota_higher_than_disk_size": "You set a quota higher than the disk size", "quota_higher_than_disk_size": "You set a quota higher than the disk size",
@@ -1220,7 +1218,6 @@
"filter_description": "Conditions to filter the target assets", "filter_description": "Conditions to filter the target assets",
"filter_people": "Filter people", "filter_people": "Filter people",
"filter_places": "Filter places", "filter_places": "Filter places",
"filter_tags": "Filter tags",
"filters": "Filters", "filters": "Filters",
"find_them_fast": "Find them fast by name with search", "find_them_fast": "Find them fast by name with search",
"first": "First", "first": "First",
@@ -1246,7 +1243,6 @@
"go_back": "Go back", "go_back": "Go back",
"go_to_folder": "Go to folder", "go_to_folder": "Go to folder",
"go_to_search": "Go to search", "go_to_search": "Go to search",
"go_to_settings": "Go to settings",
"gps": "GPS", "gps": "GPS",
"gps_missing": "No GPS", "gps_missing": "No GPS",
"grant_permission": "Grant permission", "grant_permission": "Grant permission",
@@ -1812,8 +1808,9 @@
"rate_asset": "Rate Asset", "rate_asset": "Rate Asset",
"rating": "Star rating", "rating": "Star rating",
"rating_clear": "Clear rating", "rating_clear": "Clear rating",
"rating_count": "{count, plural, =0 {Unrated} one {# star} other {# stars}}", "rating_count": "{count, plural, one {# star} other {# stars}}",
"rating_description": "Display the EXIF rating in the info panel", "rating_description": "Display the EXIF rating in the info panel",
"rating_set": "Rating set to {rating, plural, one {# star} other {# stars}}",
"reaction_options": "Reaction options", "reaction_options": "Reaction options",
"read_changelog": "Read Changelog", "read_changelog": "Read Changelog",
"readonly_mode_disabled": "Read-only mode disabled", "readonly_mode_disabled": "Read-only mode disabled",
@@ -1948,7 +1945,6 @@
"search_filter_ocr": "Search by OCR", "search_filter_ocr": "Search by OCR",
"search_filter_people_title": "Select people", "search_filter_people_title": "Select people",
"search_filter_star_rating": "Star Rating", "search_filter_star_rating": "Star Rating",
"search_filter_tags_title": "Select tags",
"search_for": "Search for", "search_for": "Search for",
"search_for_existing_person": "Search for existing person", "search_for_existing_person": "Search for existing person",
"search_no_more_result": "No more results", "search_no_more_result": "No more results",
@@ -2028,9 +2024,6 @@
"set_profile_picture": "Set profile picture", "set_profile_picture": "Set profile picture",
"set_slideshow_to_fullscreen": "Set Slideshow to fullscreen", "set_slideshow_to_fullscreen": "Set Slideshow to fullscreen",
"set_stack_primary_asset": "Set as primary asset", "set_stack_primary_asset": "Set as primary asset",
"setting_image_navigation_enable_subtitle": "If enabled, you can navigate to the previous/next image by tapping the leftmost/rightmost quarter of the screen.",
"setting_image_navigation_enable_title": "Tap to Navigate",
"setting_image_navigation_title": "Image Navigation",
"setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).", "setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).",
"setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).", "setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).",
"setting_image_viewer_original_title": "Load original image", "setting_image_viewer_original_title": "Load original image",
@@ -2309,7 +2302,6 @@
"unstack_action_prompt": "{count} unstacked", "unstack_action_prompt": "{count} unstacked",
"unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}", "unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}",
"unsupported_field_type": "Unsupported field type", "unsupported_field_type": "Unsupported field type",
"unsupported_file_type": "File {file} can't be uploaded because its file type {type} is not supported.",
"untagged": "Untagged", "untagged": "Untagged",
"untitled_workflow": "Untitled workflow", "untitled_workflow": "Untitled workflow",
"up_next": "Up next", "up_next": "Up next",

View File

@@ -8,6 +8,7 @@ readme = "README.md"
dependencies = [ dependencies = [
"aiocache>=0.12.1,<1.0", "aiocache>=0.12.1,<1.0",
"fastapi>=0.95.2,<1.0", "fastapi>=0.95.2,<1.0",
"ftfy>=6.1.1",
"gunicorn>=21.1.0", "gunicorn>=21.1.0",
"huggingface-hub>=0.20.1,<1.0", "huggingface-hub>=0.20.1,<1.0",
"insightface>=0.7.3,<1.0", "insightface>=0.7.3,<1.0",

View File

@@ -654,6 +654,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/70/25/fab23259a52ece5670dcb8452e1af34b89e6135ecc17cd4b54b4b479eac6/fsspec-2023.12.2-py3-none-any.whl", hash = "sha256:d800d87f72189a745fa3d6b033b9dc4a34ad069f60ca60b943a63599f5501960", size = 168979, upload-time = "2023-12-11T21:19:52.446Z" }, { url = "https://files.pythonhosted.org/packages/70/25/fab23259a52ece5670dcb8452e1af34b89e6135ecc17cd4b54b4b479eac6/fsspec-2023.12.2-py3-none-any.whl", hash = "sha256:d800d87f72189a745fa3d6b033b9dc4a34ad069f60ca60b943a63599f5501960", size = 168979, upload-time = "2023-12-11T21:19:52.446Z" },
] ]
[[package]]
name = "ftfy"
version = "6.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "wcwidth" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a5/d3/8650919bc3c7c6e90ee3fa7fd618bf373cbbe55dff043bd67353dbb20cd8/ftfy-6.3.1.tar.gz", hash = "sha256:9b3c3d90f84fb267fe64d375a07b7f8912d817cf86009ae134aa03e1819506ec", size = 308927, upload-time = "2024-10-26T00:50:35.149Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ab/6e/81d47999aebc1b155f81eca4477a616a70f238a2549848c38983f3c22a82/ftfy-6.3.1-py3-none-any.whl", hash = "sha256:7c70eb532015cd2f9adb53f101fb6c7945988d023a085d127d1573dc49dd0083", size = 44821, upload-time = "2024-10-26T00:50:33.425Z" },
]
[[package]] [[package]]
name = "gevent" name = "gevent"
version = "24.10.3" version = "24.10.3"
@@ -776,14 +788,14 @@ wheels = [
[[package]] [[package]]
name = "gunicorn" name = "gunicorn"
version = "25.1.0" version = "23.0.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "packaging" }, { name = "packaging" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/66/13/ef67f59f6a7896fdc2c1d62b5665c5219d6b0a9a1784938eb9a28e55e128/gunicorn-25.1.0.tar.gz", hash = "sha256:1426611d959fa77e7de89f8c0f32eed6aa03ee735f98c01efba3e281b1c47616", size = 594377, upload-time = "2026-02-13T11:09:58.989Z" } sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/da/73/4ad5b1f6a2e21cf1e85afdaad2b7b1a933985e2f5d679147a1953aaa192c/gunicorn-25.1.0-py3-none-any.whl", hash = "sha256:d0b1236ccf27f72cfe14bce7caadf467186f19e865094ca84221424e839b8b8b", size = 197067, upload-time = "2026-02-13T11:09:57.146Z" }, { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" },
] ]
[[package]] [[package]]
@@ -927,6 +939,7 @@ source = { editable = "." }
dependencies = [ dependencies = [
{ name = "aiocache" }, { name = "aiocache" },
{ name = "fastapi" }, { name = "fastapi" },
{ name = "ftfy" },
{ name = "gunicorn" }, { name = "gunicorn" },
{ name = "huggingface-hub" }, { name = "huggingface-hub" },
{ name = "insightface" }, { name = "insightface" },
@@ -1005,6 +1018,7 @@ types = [
requires-dist = [ requires-dist = [
{ name = "aiocache", specifier = ">=0.12.1,<1.0" }, { name = "aiocache", specifier = ">=0.12.1,<1.0" },
{ name = "fastapi", specifier = ">=0.95.2,<1.0" }, { name = "fastapi", specifier = ">=0.95.2,<1.0" },
{ name = "ftfy", specifier = ">=6.1.1" },
{ name = "gunicorn", specifier = ">=21.1.0" }, { name = "gunicorn", specifier = ">=21.1.0" },
{ name = "huggingface-hub", specifier = ">=0.20.1,<1.0" }, { name = "huggingface-hub", specifier = ">=0.20.1,<1.0" },
{ name = "insightface", specifier = ">=0.7.3,<1.0" }, { name = "insightface", specifier = ">=0.7.3,<1.0" },

View File

@@ -14,15 +14,15 @@ config_roots = [
] ]
[tools] [tools]
node = "24.13.1" node = "24.13.0"
flutter = "3.35.7" flutter = "3.35.7"
pnpm = "10.30.0" pnpm = "10.28.2"
terragrunt = "0.98.0" terragrunt = "0.98.0"
opentofu = "1.11.4" opentofu = "1.11.4"
java = "21.0.2" java = "21.0.2"
[tools."github:CQLabs/homebrew-dcm"] [tools."github:CQLabs/homebrew-dcm"]
version = "1.35.1" version = "1.30.0"
bin = "dcm" bin = "dcm"
postinstall = "chmod +x $MISE_TOOL_INSTALL_PATH/dcm" postinstall = "chmod +x $MISE_TOOL_INSTALL_PATH/dcm"
@@ -37,12 +37,13 @@ run = "pnpm install --filter @immich/sdk --frozen-lockfile"
[tasks."sdk:build"] [tasks."sdk:build"]
dir = "open-api/typescript-sdk" dir = "open-api/typescript-sdk"
run = "pnpm run build" env._.path = "./node_modules/.bin"
run = "tsc"
# i18n tasks # i18n tasks
[tasks."i18n:format"] [tasks."i18n:format"]
dir = "i18n" dir = "i18n"
run = "pnpm run format" run = { task = ":i18n:format-fix" }
[tasks."i18n:format-fix"] [tasks."i18n:format-fix"]
dir = "i18n" dir = "i18n"

View File

@@ -48,6 +48,7 @@ fun Bitmap.toNativeBuffer(): Map<String, Long> {
try { try {
val buffer = NativeBuffer.wrap(pointer, size) val buffer = NativeBuffer.wrap(pointer, size)
copyPixelsToBuffer(buffer) copyPixelsToBuffer(buffer)
recycle()
return mapOf( return mapOf(
"pointer" to pointer, "pointer" to pointer,
"width" to width.toLong(), "width" to width.toLong(),
@@ -56,9 +57,8 @@ fun Bitmap.toNativeBuffer(): Map<String, Long> {
) )
} catch (e: Exception) { } catch (e: Exception) {
NativeBuffer.free(pointer) NativeBuffer.free(pointer)
throw e
} finally {
recycle() recycle()
throw e
} }
} }

View File

@@ -1 +1 @@
version: '>=1.29.0 <=1.36.0' version: '>=1.29.0 <=1.30.0'

File diff suppressed because one or more lines are too long

View File

@@ -18,5 +18,3 @@ enum ActionSource { timeline, viewer }
enum CleanupStep { selectDate, scan, delete } enum CleanupStep { selectDate, scan, delete }
enum AssetKeepType { none, photosOnly, videosOnly } enum AssetKeepType { none, photosOnly, videosOnly }
enum AssetDateAggregation { start, end }

View File

@@ -16,8 +16,9 @@ class ScrollToDateEvent extends Event {
} }
// Asset Viewer Events // Asset Viewer Events
class ViewerShowDetailsEvent extends Event { class ViewerOpenBottomSheetEvent extends Event {
const ViewerShowDetailsEvent(); final bool activitiesMode;
const ViewerOpenBottomSheetEvent({this.activitiesMode = false});
} }
class ViewerReloadAssetEvent extends Event { class ViewerReloadAssetEvent extends Event {

View File

@@ -73,9 +73,6 @@ enum StoreKey<T> {
autoPlayVideo<bool>._(139), autoPlayVideo<bool>._(139),
albumGridView<bool>._(140), albumGridView<bool>._(140),
// Image viewer navigation settings
tapToNavigate<bool>._(141),
// Experimental stuff // Experimental stuff
photoManagerCustomFilter<bool>._(1000), photoManagerCustomFilter<bool>._(1000),
betaPromptShown<bool>._(1001), betaPromptShown<bool>._(1001),

View File

@@ -1,29 +0,0 @@
import 'package:openapi/api.dart';
class Tag {
final String id;
final String value;
const Tag({required this.id, required this.value});
@override
String toString() {
return 'Tag(id: $id, value: $value)';
}
@override
bool operator ==(covariant Tag other) {
if (identical(this, other)) return true;
return other.id == id && other.value == value;
}
@override
int get hashCode {
return id.hashCode ^ value.hashCode;
}
static Tag fromDto(TagResponseDto dto) {
return Tag(id: dto.id, value: dto.value);
}
}

View File

@@ -43,8 +43,8 @@ class RemoteAlbumService {
AlbumSortMode.title => albums.sortedBy((album) => album.name), AlbumSortMode.title => albums.sortedBy((album) => album.name),
AlbumSortMode.lastModified => albums.sortedBy((album) => album.updatedAt), AlbumSortMode.lastModified => albums.sortedBy((album) => album.updatedAt),
AlbumSortMode.assetCount => albums.sortedBy((album) => album.assetCount), AlbumSortMode.assetCount => albums.sortedBy((album) => album.assetCount),
AlbumSortMode.mostRecent => await _sortByAssetDate(albums, aggregation: AssetDateAggregation.end), AlbumSortMode.mostRecent => await _sortByNewestAsset(albums),
AlbumSortMode.mostOldest => await _sortByAssetDate(albums, aggregation: AssetDateAggregation.start), AlbumSortMode.mostOldest => await _sortByOldestAsset(albums),
}; };
final effectiveOrder = isReverse ? sortMode.defaultOrder.reverse() : sortMode.defaultOrder; final effectiveOrder = isReverse ? sortMode.defaultOrder.reverse() : sortMode.defaultOrder;
@@ -172,25 +172,46 @@ class RemoteAlbumService {
return _repository.getAlbumsContainingAsset(assetId); return _repository.getAlbumsContainingAsset(assetId);
} }
Future<List<RemoteAlbum>> _sortByAssetDate( Future<List<RemoteAlbum>> _sortByNewestAsset(List<RemoteAlbum> albums) async {
List<RemoteAlbum> albums, { // map album IDs to their newest asset dates
required AssetDateAggregation aggregation, final Map<String, Future<DateTime?>> assetTimestampFutures = {};
}) async { for (final album in albums) {
if (albums.isEmpty) return []; assetTimestampFutures[album.id] = _repository.getNewestAssetTimestamp(album.id);
final albumIds = albums.map((e) => e.id).toList();
final sortedIds = await _repository.getSortedAlbumIds(albumIds, aggregation: aggregation);
final albumMap = Map<String, RemoteAlbum>.fromEntries(albums.map((a) => MapEntry(a.id, a)));
final sortedAlbums = sortedIds.map((id) => albumMap[id]).whereType<RemoteAlbum>().toList();
if (sortedAlbums.length < albums.length) {
final returnedIdSet = sortedIds.toSet();
final emptyAlbums = albums.where((a) => !returnedIdSet.contains(a.id));
sortedAlbums.addAll(emptyAlbums);
} }
return sortedAlbums; // await all database queries
final entries = await Future.wait(
assetTimestampFutures.entries.map((entry) async => MapEntry(entry.key, await entry.value)),
);
final assetTimestamps = Map.fromEntries(entries);
final sorted = albums.sorted((a, b) {
final aDate = assetTimestamps[a.id] ?? DateTime.fromMillisecondsSinceEpoch(0);
final bDate = assetTimestamps[b.id] ?? DateTime.fromMillisecondsSinceEpoch(0);
return aDate.compareTo(bDate);
});
return sorted;
}
Future<List<RemoteAlbum>> _sortByOldestAsset(List<RemoteAlbum> albums) async {
// map album IDs to their oldest asset dates
final Map<String, Future<DateTime?>> assetTimestampFutures = {
for (final album in albums) album.id: _repository.getOldestAssetTimestamp(album.id),
};
// await all database queries
final entries = await Future.wait(
assetTimestampFutures.entries.map((entry) async => MapEntry(entry.key, await entry.value)),
);
final assetTimestamps = Map.fromEntries(entries);
final sorted = albums.sorted((a, b) {
final aDate = assetTimestamps[a.id] ?? DateTime.fromMillisecondsSinceEpoch(0);
final bDate = assetTimestamps[b.id] ?? DateTime.fromMillisecondsSinceEpoch(0);
return aDate.compareTo(bDate);
});
return sorted;
} }
} }

View File

@@ -68,12 +68,12 @@ class SyncStreamService {
return false; return false;
} }
final serverSemVer = SemVer(major: serverVersion.major, minor: serverVersion.minor, patch: serverVersion.patch_); final semVer = SemVer(major: serverVersion.major, minor: serverVersion.minor, patch: serverVersion.patch_);
final value = Store.get(StoreKey.syncMigrationStatus, "[]"); final value = Store.get(StoreKey.syncMigrationStatus, "[]");
final migrations = (jsonDecode(value) as List).cast<String>(); final migrations = (jsonDecode(value) as List).cast<String>();
int previousLength = migrations.length; int previousLength = migrations.length;
await _runPreSyncTasks(migrations, serverSemVer); await _runPreSyncTasks(migrations, semVer);
if (migrations.length != previousLength) { if (migrations.length != previousLength) {
_logger.info("Updated pre-sync migration status: $migrations"); _logger.info("Updated pre-sync migration status: $migrations");
@@ -82,14 +82,10 @@ class SyncStreamService {
// Start the sync stream and handle events // Start the sync stream and handle events
bool shouldReset = false; bool shouldReset = false;
await _syncApiRepository.streamChanges( await _syncApiRepository.streamChanges(_handleEvents, onReset: () => shouldReset = true);
_handleEvents,
serverVersion: serverSemVer,
onReset: () => shouldReset = true,
);
if (shouldReset) { if (shouldReset) {
_logger.info("Resetting sync state as requested by server"); _logger.info("Resetting sync state as requested by server");
await _syncApiRepository.streamChanges(_handleEvents, serverVersion: serverSemVer); await _syncApiRepository.streamChanges(_handleEvents);
} }
previousLength = migrations.length; previousLength = migrations.length;
@@ -286,8 +282,6 @@ class SyncStreamService {
return _syncStreamRepository.deletePeopleV1(data.cast()); return _syncStreamRepository.deletePeopleV1(data.cast());
case SyncEntityType.assetFaceV1: case SyncEntityType.assetFaceV1:
return _syncStreamRepository.updateAssetFacesV1(data.cast()); return _syncStreamRepository.updateAssetFacesV1(data.cast());
case SyncEntityType.assetFaceV2:
return _syncStreamRepository.updateAssetFacesV2(data.cast());
case SyncEntityType.assetFaceDeleteV1: case SyncEntityType.assetFaceDeleteV1:
return _syncStreamRepository.deleteAssetFacesV1(data.cast()); return _syncStreamRepository.deleteAssetFacesV1(data.cast());
default: default:

View File

@@ -183,8 +183,8 @@ class TimelineService {
return _buffer.slice(start, start + count); return _buffer.slice(start, start + count);
} }
// Preload assets around the given index for asset viewer // Pre-cache assets around the given index for asset viewer
Future<void> preloadAssets(int index) => _mutex.run(() => _loadAssets(index, math.min(5, _totalAssets - index))); Future<void> preCacheAssets(int index) => _mutex.run(() => _loadAssets(index, math.min(5, _totalAssets - index)));
BaseAsset getRandomAsset() => _buffer.elementAt(math.Random().nextInt(_buffer.length)); BaseAsset getRandomAsset() => _buffer.elementAt(math.Random().nextInt(_buffer.length));

View File

@@ -32,125 +32,3 @@ class FastClampingScrollPhysics extends ClampingScrollPhysics {
damping: 80, damping: 80,
); );
} }
class SnapScrollPhysics extends ScrollPhysics {
static const _minFlingVelocity = 700.0;
static const minSnapDistance = 30.0;
static final _spring = SpringDescription.withDampingRatio(mass: .5, stiffness: 300);
const SnapScrollPhysics({super.parent});
@override
SnapScrollPhysics applyTo(ScrollPhysics? ancestor) {
return SnapScrollPhysics(parent: buildParent(ancestor));
}
@override
Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) {
assert(
position is SnapScrollPosition,
'SnapScrollPhysics can only be used with Scrollables that use a '
'controller whose createScrollPosition returns a SnapScrollPosition',
);
final snapOffset = (position as SnapScrollPosition).snapOffset;
if (snapOffset <= 0) {
return super.createBallisticSimulation(position, velocity);
}
if (position.pixels >= snapOffset) {
final simulation = super.createBallisticSimulation(position, velocity);
if (simulation == null || simulation.x(double.infinity) >= snapOffset) {
return simulation;
}
}
return ScrollSpringSimulation(
_spring,
position.pixels,
target(position, velocity, snapOffset),
velocity,
tolerance: toleranceFor(position),
);
}
static double target(ScrollMetrics position, double velocity, double snapOffset) {
if (velocity > _minFlingVelocity) return snapOffset;
if (velocity < -_minFlingVelocity) return position.pixels < snapOffset ? 0.0 : snapOffset;
return position.pixels < minSnapDistance ? 0.0 : snapOffset;
}
}
class SnapScrollPosition extends ScrollPositionWithSingleContext {
double snapOffset;
SnapScrollPosition({this.snapOffset = 0.0, required super.physics, required super.context, super.oldPosition});
}
class ProxyScrollController extends ScrollController {
final ScrollController scrollController;
ProxyScrollController({required this.scrollController});
SnapScrollPosition get snapPosition => position as SnapScrollPosition;
@override
ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition? oldPosition) {
return ProxyScrollPosition(
scrollController: scrollController,
physics: physics,
context: context,
oldPosition: oldPosition,
);
}
@override
void dispose() {
scrollController.dispose();
super.dispose();
}
}
class ProxyScrollPosition extends SnapScrollPosition {
final ScrollController scrollController;
ProxyScrollPosition({
required this.scrollController,
required super.physics,
required super.context,
super.oldPosition,
});
@override
double setPixels(double newPixels) {
final overscroll = super.setPixels(newPixels);
if (scrollController.hasClients && scrollController.position.pixels != pixels) {
scrollController.position.forcePixels(pixels);
}
return overscroll;
}
@override
void forcePixels(double value) {
super.forcePixels(value);
if (scrollController.hasClients && scrollController.position.pixels != pixels) {
scrollController.position.forcePixels(pixels);
}
}
@override
double get maxScrollExtent => scrollController.hasClients && scrollController.position.hasContentDimensions
? scrollController.position.maxScrollExtent
: super.maxScrollExtent;
@override
double get minScrollExtent => scrollController.hasClients && scrollController.position.hasContentDimensions
? scrollController.position.minScrollExtent
: super.minScrollExtent;
@override
double get viewportDimension => scrollController.hasClients && scrollController.position.hasViewportDimension
? scrollController.position.viewportDimension
: super.viewportDimension;
}

View File

@@ -28,10 +28,6 @@ class AssetFaceEntity extends Table with DriftDefaultsMixin {
TextColumn get sourceType => text()(); TextColumn get sourceType => text()();
BoolColumn get isVisible => boolean().withDefault(const Constant(true))();
DateTimeColumn get deletedAt => dateTime().nullable()();
@override @override
Set<Column> get primaryKey => {id}; Set<Column> get primaryKey => {id};
} }

View File

@@ -5,12 +5,11 @@ import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.da
as i1; as i1;
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.dart' import 'package:immich_mobile/infrastructure/entities/asset_face.entity.dart'
as i2; as i2;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
as i4; as i3;
import 'package:drift/internal/modular.dart' as i5; import 'package:drift/internal/modular.dart' as i4;
import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'
as i6; as i5;
typedef $$AssetFaceEntityTableCreateCompanionBuilder = typedef $$AssetFaceEntityTableCreateCompanionBuilder =
i1.AssetFaceEntityCompanion Function({ i1.AssetFaceEntityCompanion Function({
@@ -24,8 +23,6 @@ typedef $$AssetFaceEntityTableCreateCompanionBuilder =
required int boundingBoxX2, required int boundingBoxX2,
required int boundingBoxY2, required int boundingBoxY2,
required String sourceType, required String sourceType,
i0.Value<bool> isVisible,
i0.Value<DateTime?> deletedAt,
}); });
typedef $$AssetFaceEntityTableUpdateCompanionBuilder = typedef $$AssetFaceEntityTableUpdateCompanionBuilder =
i1.AssetFaceEntityCompanion Function({ i1.AssetFaceEntityCompanion Function({
@@ -39,8 +36,6 @@ typedef $$AssetFaceEntityTableUpdateCompanionBuilder =
i0.Value<int> boundingBoxX2, i0.Value<int> boundingBoxX2,
i0.Value<int> boundingBoxY2, i0.Value<int> boundingBoxY2,
i0.Value<String> sourceType, i0.Value<String> sourceType,
i0.Value<bool> isVisible,
i0.Value<DateTime?> deletedAt,
}); });
final class $$AssetFaceEntityTableReferences final class $$AssetFaceEntityTableReferences
@@ -56,29 +51,29 @@ final class $$AssetFaceEntityTableReferences
super.$_typedResult, super.$_typedResult,
); );
static i4.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) => static i3.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) =>
i5.ReadDatabaseContainer(db) i4.ReadDatabaseContainer(db)
.resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity') .resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity')
.createAlias( .createAlias(
i0.$_aliasNameGenerator( i0.$_aliasNameGenerator(
i5.ReadDatabaseContainer(db) i4.ReadDatabaseContainer(db)
.resultSet<i1.$AssetFaceEntityTable>('asset_face_entity') .resultSet<i1.$AssetFaceEntityTable>('asset_face_entity')
.assetId, .assetId,
i5.ReadDatabaseContainer( i4.ReadDatabaseContainer(
db, db,
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity').id, ).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity').id,
), ),
); );
i4.$$RemoteAssetEntityTableProcessedTableManager get assetId { i3.$$RemoteAssetEntityTableProcessedTableManager get assetId {
final $_column = $_itemColumn<String>('asset_id')!; final $_column = $_itemColumn<String>('asset_id')!;
final manager = i4 final manager = i3
.$$RemoteAssetEntityTableTableManager( .$$RemoteAssetEntityTableTableManager(
$_db, $_db,
i5.ReadDatabaseContainer( i4.ReadDatabaseContainer(
$_db, $_db,
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'), ).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
) )
.filter((f) => f.id.sqlEquals($_column)); .filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_assetIdTable($_db)); final item = $_typedResult.readTableOrNull(_assetIdTable($_db));
@@ -88,29 +83,29 @@ final class $$AssetFaceEntityTableReferences
); );
} }
static i6.$PersonEntityTable _personIdTable(i0.GeneratedDatabase db) => static i5.$PersonEntityTable _personIdTable(i0.GeneratedDatabase db) =>
i5.ReadDatabaseContainer(db) i4.ReadDatabaseContainer(db)
.resultSet<i6.$PersonEntityTable>('person_entity') .resultSet<i5.$PersonEntityTable>('person_entity')
.createAlias( .createAlias(
i0.$_aliasNameGenerator( i0.$_aliasNameGenerator(
i5.ReadDatabaseContainer(db) i4.ReadDatabaseContainer(db)
.resultSet<i1.$AssetFaceEntityTable>('asset_face_entity') .resultSet<i1.$AssetFaceEntityTable>('asset_face_entity')
.personId, .personId,
i5.ReadDatabaseContainer( i4.ReadDatabaseContainer(
db, db,
).resultSet<i6.$PersonEntityTable>('person_entity').id, ).resultSet<i5.$PersonEntityTable>('person_entity').id,
), ),
); );
i6.$$PersonEntityTableProcessedTableManager? get personId { i5.$$PersonEntityTableProcessedTableManager? get personId {
final $_column = $_itemColumn<String>('person_id'); final $_column = $_itemColumn<String>('person_id');
if ($_column == null) return null; if ($_column == null) return null;
final manager = i6 final manager = i5
.$$PersonEntityTableTableManager( .$$PersonEntityTableTableManager(
$_db, $_db,
i5.ReadDatabaseContainer( i4.ReadDatabaseContainer(
$_db, $_db,
).resultSet<i6.$PersonEntityTable>('person_entity'), ).resultSet<i5.$PersonEntityTable>('person_entity'),
) )
.filter((f) => f.id.sqlEquals($_column)); .filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_personIdTable($_db)); final item = $_typedResult.readTableOrNull(_personIdTable($_db));
@@ -170,34 +165,24 @@ class $$AssetFaceEntityTableFilterComposer
builder: (column) => i0.ColumnFilters(column), builder: (column) => i0.ColumnFilters(column),
); );
i0.ColumnFilters<bool> get isVisible => $composableBuilder( i3.$$RemoteAssetEntityTableFilterComposer get assetId {
column: $table.isVisible, final i3.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder(
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<DateTime> get deletedAt => $composableBuilder(
column: $table.deletedAt,
builder: (column) => i0.ColumnFilters(column),
);
i4.$$RemoteAssetEntityTableFilterComposer get assetId {
final i4.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder(
composer: this, composer: this,
getCurrentColumn: (t) => t.assetId, getCurrentColumn: (t) => t.assetId,
referencedTable: i5.ReadDatabaseContainer( referencedTable: i4.ReadDatabaseContainer(
$db, $db,
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'), ).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
getReferencedColumn: (t) => t.id, getReferencedColumn: (t) => t.id,
builder: builder:
( (
joinBuilder, { joinBuilder, {
$addJoinBuilderToRootComposer, $addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer, $removeJoinBuilderFromRootComposer,
}) => i4.$$RemoteAssetEntityTableFilterComposer( }) => i3.$$RemoteAssetEntityTableFilterComposer(
$db: $db, $db: $db,
$table: i5.ReadDatabaseContainer( $table: i4.ReadDatabaseContainer(
$db, $db,
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'), ).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder, joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer:
@@ -207,24 +192,24 @@ class $$AssetFaceEntityTableFilterComposer
return composer; return composer;
} }
i6.$$PersonEntityTableFilterComposer get personId { i5.$$PersonEntityTableFilterComposer get personId {
final i6.$$PersonEntityTableFilterComposer composer = $composerBuilder( final i5.$$PersonEntityTableFilterComposer composer = $composerBuilder(
composer: this, composer: this,
getCurrentColumn: (t) => t.personId, getCurrentColumn: (t) => t.personId,
referencedTable: i5.ReadDatabaseContainer( referencedTable: i4.ReadDatabaseContainer(
$db, $db,
).resultSet<i6.$PersonEntityTable>('person_entity'), ).resultSet<i5.$PersonEntityTable>('person_entity'),
getReferencedColumn: (t) => t.id, getReferencedColumn: (t) => t.id,
builder: builder:
( (
joinBuilder, { joinBuilder, {
$addJoinBuilderToRootComposer, $addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer, $removeJoinBuilderFromRootComposer,
}) => i6.$$PersonEntityTableFilterComposer( }) => i5.$$PersonEntityTableFilterComposer(
$db: $db, $db: $db,
$table: i5.ReadDatabaseContainer( $table: i4.ReadDatabaseContainer(
$db, $db,
).resultSet<i6.$PersonEntityTable>('person_entity'), ).resultSet<i5.$PersonEntityTable>('person_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder, joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer:
@@ -284,35 +269,25 @@ class $$AssetFaceEntityTableOrderingComposer
builder: (column) => i0.ColumnOrderings(column), builder: (column) => i0.ColumnOrderings(column),
); );
i0.ColumnOrderings<bool> get isVisible => $composableBuilder( i3.$$RemoteAssetEntityTableOrderingComposer get assetId {
column: $table.isVisible, final i3.$$RemoteAssetEntityTableOrderingComposer composer =
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<DateTime> get deletedAt => $composableBuilder(
column: $table.deletedAt,
builder: (column) => i0.ColumnOrderings(column),
);
i4.$$RemoteAssetEntityTableOrderingComposer get assetId {
final i4.$$RemoteAssetEntityTableOrderingComposer composer =
$composerBuilder( $composerBuilder(
composer: this, composer: this,
getCurrentColumn: (t) => t.assetId, getCurrentColumn: (t) => t.assetId,
referencedTable: i5.ReadDatabaseContainer( referencedTable: i4.ReadDatabaseContainer(
$db, $db,
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'), ).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
getReferencedColumn: (t) => t.id, getReferencedColumn: (t) => t.id,
builder: builder:
( (
joinBuilder, { joinBuilder, {
$addJoinBuilderToRootComposer, $addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer, $removeJoinBuilderFromRootComposer,
}) => i4.$$RemoteAssetEntityTableOrderingComposer( }) => i3.$$RemoteAssetEntityTableOrderingComposer(
$db: $db, $db: $db,
$table: i5.ReadDatabaseContainer( $table: i4.ReadDatabaseContainer(
$db, $db,
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'), ).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder, joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer:
@@ -322,24 +297,24 @@ class $$AssetFaceEntityTableOrderingComposer
return composer; return composer;
} }
i6.$$PersonEntityTableOrderingComposer get personId { i5.$$PersonEntityTableOrderingComposer get personId {
final i6.$$PersonEntityTableOrderingComposer composer = $composerBuilder( final i5.$$PersonEntityTableOrderingComposer composer = $composerBuilder(
composer: this, composer: this,
getCurrentColumn: (t) => t.personId, getCurrentColumn: (t) => t.personId,
referencedTable: i5.ReadDatabaseContainer( referencedTable: i4.ReadDatabaseContainer(
$db, $db,
).resultSet<i6.$PersonEntityTable>('person_entity'), ).resultSet<i5.$PersonEntityTable>('person_entity'),
getReferencedColumn: (t) => t.id, getReferencedColumn: (t) => t.id,
builder: builder:
( (
joinBuilder, { joinBuilder, {
$addJoinBuilderToRootComposer, $addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer, $removeJoinBuilderFromRootComposer,
}) => i6.$$PersonEntityTableOrderingComposer( }) => i5.$$PersonEntityTableOrderingComposer(
$db: $db, $db: $db,
$table: i5.ReadDatabaseContainer( $table: i4.ReadDatabaseContainer(
$db, $db,
).resultSet<i6.$PersonEntityTable>('person_entity'), ).resultSet<i5.$PersonEntityTable>('person_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder, joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer:
@@ -397,31 +372,25 @@ class $$AssetFaceEntityTableAnnotationComposer
builder: (column) => column, builder: (column) => column,
); );
i0.GeneratedColumn<bool> get isVisible => i3.$$RemoteAssetEntityTableAnnotationComposer get assetId {
$composableBuilder(column: $table.isVisible, builder: (column) => column); final i3.$$RemoteAssetEntityTableAnnotationComposer composer =
i0.GeneratedColumn<DateTime> get deletedAt =>
$composableBuilder(column: $table.deletedAt, builder: (column) => column);
i4.$$RemoteAssetEntityTableAnnotationComposer get assetId {
final i4.$$RemoteAssetEntityTableAnnotationComposer composer =
$composerBuilder( $composerBuilder(
composer: this, composer: this,
getCurrentColumn: (t) => t.assetId, getCurrentColumn: (t) => t.assetId,
referencedTable: i5.ReadDatabaseContainer( referencedTable: i4.ReadDatabaseContainer(
$db, $db,
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'), ).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
getReferencedColumn: (t) => t.id, getReferencedColumn: (t) => t.id,
builder: builder:
( (
joinBuilder, { joinBuilder, {
$addJoinBuilderToRootComposer, $addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer, $removeJoinBuilderFromRootComposer,
}) => i4.$$RemoteAssetEntityTableAnnotationComposer( }) => i3.$$RemoteAssetEntityTableAnnotationComposer(
$db: $db, $db: $db,
$table: i5.ReadDatabaseContainer( $table: i4.ReadDatabaseContainer(
$db, $db,
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'), ).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder, joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer:
@@ -431,24 +400,24 @@ class $$AssetFaceEntityTableAnnotationComposer
return composer; return composer;
} }
i6.$$PersonEntityTableAnnotationComposer get personId { i5.$$PersonEntityTableAnnotationComposer get personId {
final i6.$$PersonEntityTableAnnotationComposer composer = $composerBuilder( final i5.$$PersonEntityTableAnnotationComposer composer = $composerBuilder(
composer: this, composer: this,
getCurrentColumn: (t) => t.personId, getCurrentColumn: (t) => t.personId,
referencedTable: i5.ReadDatabaseContainer( referencedTable: i4.ReadDatabaseContainer(
$db, $db,
).resultSet<i6.$PersonEntityTable>('person_entity'), ).resultSet<i5.$PersonEntityTable>('person_entity'),
getReferencedColumn: (t) => t.id, getReferencedColumn: (t) => t.id,
builder: builder:
( (
joinBuilder, { joinBuilder, {
$addJoinBuilderToRootComposer, $addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer, $removeJoinBuilderFromRootComposer,
}) => i6.$$PersonEntityTableAnnotationComposer( }) => i5.$$PersonEntityTableAnnotationComposer(
$db: $db, $db: $db,
$table: i5.ReadDatabaseContainer( $table: i4.ReadDatabaseContainer(
$db, $db,
).resultSet<i6.$PersonEntityTable>('person_entity'), ).resultSet<i5.$PersonEntityTable>('person_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder, joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer:
@@ -499,8 +468,6 @@ class $$AssetFaceEntityTableTableManager
i0.Value<int> boundingBoxX2 = const i0.Value.absent(), i0.Value<int> boundingBoxX2 = const i0.Value.absent(),
i0.Value<int> boundingBoxY2 = const i0.Value.absent(), i0.Value<int> boundingBoxY2 = const i0.Value.absent(),
i0.Value<String> sourceType = const i0.Value.absent(), i0.Value<String> sourceType = const i0.Value.absent(),
i0.Value<bool> isVisible = const i0.Value.absent(),
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
}) => i1.AssetFaceEntityCompanion( }) => i1.AssetFaceEntityCompanion(
id: id, id: id,
assetId: assetId, assetId: assetId,
@@ -512,8 +479,6 @@ class $$AssetFaceEntityTableTableManager
boundingBoxX2: boundingBoxX2, boundingBoxX2: boundingBoxX2,
boundingBoxY2: boundingBoxY2, boundingBoxY2: boundingBoxY2,
sourceType: sourceType, sourceType: sourceType,
isVisible: isVisible,
deletedAt: deletedAt,
), ),
createCompanionCallback: createCompanionCallback:
({ ({
@@ -527,8 +492,6 @@ class $$AssetFaceEntityTableTableManager
required int boundingBoxX2, required int boundingBoxX2,
required int boundingBoxY2, required int boundingBoxY2,
required String sourceType, required String sourceType,
i0.Value<bool> isVisible = const i0.Value.absent(),
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
}) => i1.AssetFaceEntityCompanion.insert( }) => i1.AssetFaceEntityCompanion.insert(
id: id, id: id,
assetId: assetId, assetId: assetId,
@@ -540,8 +503,6 @@ class $$AssetFaceEntityTableTableManager
boundingBoxX2: boundingBoxX2, boundingBoxX2: boundingBoxX2,
boundingBoxY2: boundingBoxY2, boundingBoxY2: boundingBoxY2,
sourceType: sourceType, sourceType: sourceType,
isVisible: isVisible,
deletedAt: deletedAt,
), ),
withReferenceMapper: (p0) => p0 withReferenceMapper: (p0) => p0
.map( .map(
@@ -748,33 +709,6 @@ class $AssetFaceEntityTable extends i2.AssetFaceEntity
type: i0.DriftSqlType.string, type: i0.DriftSqlType.string,
requiredDuringInsert: true, requiredDuringInsert: true,
); );
static const i0.VerificationMeta _isVisibleMeta = const i0.VerificationMeta(
'isVisible',
);
@override
late final i0.GeneratedColumn<bool> isVisible = i0.GeneratedColumn<bool>(
'is_visible',
aliasedName,
false,
type: i0.DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'CHECK ("is_visible" IN (0, 1))',
),
defaultValue: const i3.Constant(true),
);
static const i0.VerificationMeta _deletedAtMeta = const i0.VerificationMeta(
'deletedAt',
);
@override
late final i0.GeneratedColumn<DateTime> deletedAt =
i0.GeneratedColumn<DateTime>(
'deleted_at',
aliasedName,
true,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
);
@override @override
List<i0.GeneratedColumn> get $columns => [ List<i0.GeneratedColumn> get $columns => [
id, id,
@@ -787,8 +721,6 @@ class $AssetFaceEntityTable extends i2.AssetFaceEntity
boundingBoxX2, boundingBoxX2,
boundingBoxY2, boundingBoxY2,
sourceType, sourceType,
isVisible,
deletedAt,
]; ];
@override @override
String get aliasedName => _alias ?? actualTableName; String get aliasedName => _alias ?? actualTableName;
@@ -892,18 +824,6 @@ class $AssetFaceEntityTable extends i2.AssetFaceEntity
} else if (isInserting) { } else if (isInserting) {
context.missing(_sourceTypeMeta); context.missing(_sourceTypeMeta);
} }
if (data.containsKey('is_visible')) {
context.handle(
_isVisibleMeta,
isVisible.isAcceptableOrUnknown(data['is_visible']!, _isVisibleMeta),
);
}
if (data.containsKey('deleted_at')) {
context.handle(
_deletedAtMeta,
deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta),
);
}
return context; return context;
} }
@@ -953,14 +873,6 @@ class $AssetFaceEntityTable extends i2.AssetFaceEntity
i0.DriftSqlType.string, i0.DriftSqlType.string,
data['${effectivePrefix}source_type'], data['${effectivePrefix}source_type'],
)!, )!,
isVisible: attachedDatabase.typeMapping.read(
i0.DriftSqlType.bool,
data['${effectivePrefix}is_visible'],
)!,
deletedAt: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime,
data['${effectivePrefix}deleted_at'],
),
); );
} }
@@ -987,8 +899,6 @@ class AssetFaceEntityData extends i0.DataClass
final int boundingBoxX2; final int boundingBoxX2;
final int boundingBoxY2; final int boundingBoxY2;
final String sourceType; final String sourceType;
final bool isVisible;
final DateTime? deletedAt;
const AssetFaceEntityData({ const AssetFaceEntityData({
required this.id, required this.id,
required this.assetId, required this.assetId,
@@ -1000,8 +910,6 @@ class AssetFaceEntityData extends i0.DataClass
required this.boundingBoxX2, required this.boundingBoxX2,
required this.boundingBoxY2, required this.boundingBoxY2,
required this.sourceType, required this.sourceType,
required this.isVisible,
this.deletedAt,
}); });
@override @override
Map<String, i0.Expression> toColumns(bool nullToAbsent) { Map<String, i0.Expression> toColumns(bool nullToAbsent) {
@@ -1018,10 +926,6 @@ class AssetFaceEntityData extends i0.DataClass
map['bounding_box_x2'] = i0.Variable<int>(boundingBoxX2); map['bounding_box_x2'] = i0.Variable<int>(boundingBoxX2);
map['bounding_box_y2'] = i0.Variable<int>(boundingBoxY2); map['bounding_box_y2'] = i0.Variable<int>(boundingBoxY2);
map['source_type'] = i0.Variable<String>(sourceType); map['source_type'] = i0.Variable<String>(sourceType);
map['is_visible'] = i0.Variable<bool>(isVisible);
if (!nullToAbsent || deletedAt != null) {
map['deleted_at'] = i0.Variable<DateTime>(deletedAt);
}
return map; return map;
} }
@@ -1041,8 +945,6 @@ class AssetFaceEntityData extends i0.DataClass
boundingBoxX2: serializer.fromJson<int>(json['boundingBoxX2']), boundingBoxX2: serializer.fromJson<int>(json['boundingBoxX2']),
boundingBoxY2: serializer.fromJson<int>(json['boundingBoxY2']), boundingBoxY2: serializer.fromJson<int>(json['boundingBoxY2']),
sourceType: serializer.fromJson<String>(json['sourceType']), sourceType: serializer.fromJson<String>(json['sourceType']),
isVisible: serializer.fromJson<bool>(json['isVisible']),
deletedAt: serializer.fromJson<DateTime?>(json['deletedAt']),
); );
} }
@override @override
@@ -1059,8 +961,6 @@ class AssetFaceEntityData extends i0.DataClass
'boundingBoxX2': serializer.toJson<int>(boundingBoxX2), 'boundingBoxX2': serializer.toJson<int>(boundingBoxX2),
'boundingBoxY2': serializer.toJson<int>(boundingBoxY2), 'boundingBoxY2': serializer.toJson<int>(boundingBoxY2),
'sourceType': serializer.toJson<String>(sourceType), 'sourceType': serializer.toJson<String>(sourceType),
'isVisible': serializer.toJson<bool>(isVisible),
'deletedAt': serializer.toJson<DateTime?>(deletedAt),
}; };
} }
@@ -1075,8 +975,6 @@ class AssetFaceEntityData extends i0.DataClass
int? boundingBoxX2, int? boundingBoxX2,
int? boundingBoxY2, int? boundingBoxY2,
String? sourceType, String? sourceType,
bool? isVisible,
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
}) => i1.AssetFaceEntityData( }) => i1.AssetFaceEntityData(
id: id ?? this.id, id: id ?? this.id,
assetId: assetId ?? this.assetId, assetId: assetId ?? this.assetId,
@@ -1088,8 +986,6 @@ class AssetFaceEntityData extends i0.DataClass
boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2,
boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2,
sourceType: sourceType ?? this.sourceType, sourceType: sourceType ?? this.sourceType,
isVisible: isVisible ?? this.isVisible,
deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt,
); );
AssetFaceEntityData copyWithCompanion(i1.AssetFaceEntityCompanion data) { AssetFaceEntityData copyWithCompanion(i1.AssetFaceEntityCompanion data) {
return AssetFaceEntityData( return AssetFaceEntityData(
@@ -1117,8 +1013,6 @@ class AssetFaceEntityData extends i0.DataClass
sourceType: data.sourceType.present sourceType: data.sourceType.present
? data.sourceType.value ? data.sourceType.value
: this.sourceType, : this.sourceType,
isVisible: data.isVisible.present ? data.isVisible.value : this.isVisible,
deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt,
); );
} }
@@ -1134,9 +1028,7 @@ class AssetFaceEntityData extends i0.DataClass
..write('boundingBoxY1: $boundingBoxY1, ') ..write('boundingBoxY1: $boundingBoxY1, ')
..write('boundingBoxX2: $boundingBoxX2, ') ..write('boundingBoxX2: $boundingBoxX2, ')
..write('boundingBoxY2: $boundingBoxY2, ') ..write('boundingBoxY2: $boundingBoxY2, ')
..write('sourceType: $sourceType, ') ..write('sourceType: $sourceType')
..write('isVisible: $isVisible, ')
..write('deletedAt: $deletedAt')
..write(')')) ..write(')'))
.toString(); .toString();
} }
@@ -1153,8 +1045,6 @@ class AssetFaceEntityData extends i0.DataClass
boundingBoxX2, boundingBoxX2,
boundingBoxY2, boundingBoxY2,
sourceType, sourceType,
isVisible,
deletedAt,
); );
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
@@ -1169,9 +1059,7 @@ class AssetFaceEntityData extends i0.DataClass
other.boundingBoxY1 == this.boundingBoxY1 && other.boundingBoxY1 == this.boundingBoxY1 &&
other.boundingBoxX2 == this.boundingBoxX2 && other.boundingBoxX2 == this.boundingBoxX2 &&
other.boundingBoxY2 == this.boundingBoxY2 && other.boundingBoxY2 == this.boundingBoxY2 &&
other.sourceType == this.sourceType && other.sourceType == this.sourceType);
other.isVisible == this.isVisible &&
other.deletedAt == this.deletedAt);
} }
class AssetFaceEntityCompanion class AssetFaceEntityCompanion
@@ -1186,8 +1074,6 @@ class AssetFaceEntityCompanion
final i0.Value<int> boundingBoxX2; final i0.Value<int> boundingBoxX2;
final i0.Value<int> boundingBoxY2; final i0.Value<int> boundingBoxY2;
final i0.Value<String> sourceType; final i0.Value<String> sourceType;
final i0.Value<bool> isVisible;
final i0.Value<DateTime?> deletedAt;
const AssetFaceEntityCompanion({ const AssetFaceEntityCompanion({
this.id = const i0.Value.absent(), this.id = const i0.Value.absent(),
this.assetId = const i0.Value.absent(), this.assetId = const i0.Value.absent(),
@@ -1199,8 +1085,6 @@ class AssetFaceEntityCompanion
this.boundingBoxX2 = const i0.Value.absent(), this.boundingBoxX2 = const i0.Value.absent(),
this.boundingBoxY2 = const i0.Value.absent(), this.boundingBoxY2 = const i0.Value.absent(),
this.sourceType = const i0.Value.absent(), this.sourceType = const i0.Value.absent(),
this.isVisible = const i0.Value.absent(),
this.deletedAt = const i0.Value.absent(),
}); });
AssetFaceEntityCompanion.insert({ AssetFaceEntityCompanion.insert({
required String id, required String id,
@@ -1213,8 +1097,6 @@ class AssetFaceEntityCompanion
required int boundingBoxX2, required int boundingBoxX2,
required int boundingBoxY2, required int boundingBoxY2,
required String sourceType, required String sourceType,
this.isVisible = const i0.Value.absent(),
this.deletedAt = const i0.Value.absent(),
}) : id = i0.Value(id), }) : id = i0.Value(id),
assetId = i0.Value(assetId), assetId = i0.Value(assetId),
imageWidth = i0.Value(imageWidth), imageWidth = i0.Value(imageWidth),
@@ -1235,8 +1117,6 @@ class AssetFaceEntityCompanion
i0.Expression<int>? boundingBoxX2, i0.Expression<int>? boundingBoxX2,
i0.Expression<int>? boundingBoxY2, i0.Expression<int>? boundingBoxY2,
i0.Expression<String>? sourceType, i0.Expression<String>? sourceType,
i0.Expression<bool>? isVisible,
i0.Expression<DateTime>? deletedAt,
}) { }) {
return i0.RawValuesInsertable({ return i0.RawValuesInsertable({
if (id != null) 'id': id, if (id != null) 'id': id,
@@ -1249,8 +1129,6 @@ class AssetFaceEntityCompanion
if (boundingBoxX2 != null) 'bounding_box_x2': boundingBoxX2, if (boundingBoxX2 != null) 'bounding_box_x2': boundingBoxX2,
if (boundingBoxY2 != null) 'bounding_box_y2': boundingBoxY2, if (boundingBoxY2 != null) 'bounding_box_y2': boundingBoxY2,
if (sourceType != null) 'source_type': sourceType, if (sourceType != null) 'source_type': sourceType,
if (isVisible != null) 'is_visible': isVisible,
if (deletedAt != null) 'deleted_at': deletedAt,
}); });
} }
@@ -1265,8 +1143,6 @@ class AssetFaceEntityCompanion
i0.Value<int>? boundingBoxX2, i0.Value<int>? boundingBoxX2,
i0.Value<int>? boundingBoxY2, i0.Value<int>? boundingBoxY2,
i0.Value<String>? sourceType, i0.Value<String>? sourceType,
i0.Value<bool>? isVisible,
i0.Value<DateTime?>? deletedAt,
}) { }) {
return i1.AssetFaceEntityCompanion( return i1.AssetFaceEntityCompanion(
id: id ?? this.id, id: id ?? this.id,
@@ -1279,8 +1155,6 @@ class AssetFaceEntityCompanion
boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2,
boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2,
sourceType: sourceType ?? this.sourceType, sourceType: sourceType ?? this.sourceType,
isVisible: isVisible ?? this.isVisible,
deletedAt: deletedAt ?? this.deletedAt,
); );
} }
@@ -1317,12 +1191,6 @@ class AssetFaceEntityCompanion
if (sourceType.present) { if (sourceType.present) {
map['source_type'] = i0.Variable<String>(sourceType.value); map['source_type'] = i0.Variable<String>(sourceType.value);
} }
if (isVisible.present) {
map['is_visible'] = i0.Variable<bool>(isVisible.value);
}
if (deletedAt.present) {
map['deleted_at'] = i0.Variable<DateTime>(deletedAt.value);
}
return map; return map;
} }
@@ -1338,9 +1206,7 @@ class AssetFaceEntityCompanion
..write('boundingBoxY1: $boundingBoxY1, ') ..write('boundingBoxY1: $boundingBoxY1, ')
..write('boundingBoxX2: $boundingBoxX2, ') ..write('boundingBoxX2: $boundingBoxX2, ')
..write('boundingBoxY2: $boundingBoxY2, ') ..write('boundingBoxY2: $boundingBoxY2, ')
..write('sourceType: $sourceType, ') ..write('sourceType: $sourceType')
..write('isVisible: $isVisible, ')
..write('deletedAt: $deletedAt')
..write(')')) ..write(')'))
.toString(); .toString();
} }

View File

@@ -97,7 +97,7 @@ class Drift extends $Drift implements IDatabaseRepository {
} }
@override @override
int get schemaVersion => 20; int get schemaVersion => 19;
@override @override
MigrationStrategy get migration => MigrationStrategy( MigrationStrategy get migration => MigrationStrategy(
@@ -226,10 +226,6 @@ class Drift extends $Drift implements IDatabaseRepository {
await m.createIndex(v19.idxRemoteAssetLocalDateTimeMonth); await m.createIndex(v19.idxRemoteAssetLocalDateTimeMonth);
await m.createIndex(v19.idxStackPrimaryAssetId); await m.createIndex(v19.idxStackPrimaryAssetId);
}, },
from19To20: (m, v20) async {
await m.addColumn(v20.assetFaceEntity, v20.assetFaceEntity.isVisible);
await m.addColumn(v20.assetFaceEntity, v20.assetFaceEntity.deletedAt);
},
), ),
); );

View File

@@ -8360,550 +8360,6 @@ final class Schema19 extends i0.VersionedSchema {
); );
} }
final class Schema20 extends i0.VersionedSchema {
Schema20({required super.database}) : super(version: 20);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
userEntity,
remoteAssetEntity,
stackEntity,
localAssetEntity,
remoteAlbumEntity,
localAlbumEntity,
localAlbumAssetEntity,
idxLocalAlbumAssetAlbumAsset,
idxRemoteAlbumOwnerId,
idxLocalAssetChecksum,
idxLocalAssetCloudId,
idxStackPrimaryAssetId,
idxRemoteAssetOwnerChecksum,
uQRemoteAssetsOwnerChecksum,
uQRemoteAssetsOwnerLibraryChecksum,
idxRemoteAssetChecksum,
idxRemoteAssetStackId,
idxRemoteAssetLocalDateTimeDay,
idxRemoteAssetLocalDateTimeMonth,
authUserEntity,
userMetadataEntity,
partnerEntity,
remoteExifEntity,
remoteAlbumAssetEntity,
remoteAlbumUserEntity,
remoteAssetCloudIdEntity,
memoryEntity,
memoryAssetEntity,
personEntity,
assetFaceEntity,
storeEntity,
trashedLocalAssetEntity,
idxPartnerSharedWithId,
idxLatLng,
idxRemoteAlbumAssetAlbumAsset,
idxRemoteAssetCloudId,
idxPersonOwnerId,
idxAssetFacePersonId,
idxAssetFaceAssetId,
idxTrashedLocalAssetChecksum,
idxTrashedLocalAssetAlbum,
];
late final Shape20 userEntity = Shape20(
source: i0.VersionedTable(
entityName: 'user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_3,
_column_84,
_column_85,
_column_91,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape28 remoteAssetEntity = Shape28(
source: i0.VersionedTable(
entityName: 'remote_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_1,
_column_8,
_column_9,
_column_5,
_column_10,
_column_11,
_column_12,
_column_0,
_column_13,
_column_14,
_column_15,
_column_16,
_column_17,
_column_18,
_column_19,
_column_20,
_column_21,
_column_86,
_column_101,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape3 stackEntity = Shape3(
source: i0.VersionedTable(
entityName: 'stack_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [_column_0, _column_9, _column_5, _column_15, _column_75],
attachedDatabase: database,
),
alias: null,
);
late final Shape26 localAssetEntity = Shape26(
source: i0.VersionedTable(
entityName: 'local_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_1,
_column_8,
_column_9,
_column_5,
_column_10,
_column_11,
_column_12,
_column_0,
_column_22,
_column_14,
_column_23,
_column_98,
_column_96,
_column_46,
_column_47,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape9 remoteAlbumEntity = Shape9(
source: i0.VersionedTable(
entityName: 'remote_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_56,
_column_9,
_column_5,
_column_15,
_column_57,
_column_58,
_column_59,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape19 localAlbumEntity = Shape19(
source: i0.VersionedTable(
entityName: 'local_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_5,
_column_31,
_column_32,
_column_90,
_column_33,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape22 localAlbumAssetEntity = Shape22(
source: i0.VersionedTable(
entityName: 'local_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_34, _column_35, _column_33],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxLocalAlbumAssetAlbumAsset = i1.Index(
'idx_local_album_asset_album_asset',
'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)',
);
final i1.Index idxRemoteAlbumOwnerId = i1.Index(
'idx_remote_album_owner_id',
'CREATE INDEX IF NOT EXISTS idx_remote_album_owner_id ON remote_album_entity (owner_id)',
);
final i1.Index idxLocalAssetChecksum = i1.Index(
'idx_local_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
);
final i1.Index idxLocalAssetCloudId = i1.Index(
'idx_local_asset_cloud_id',
'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)',
);
final i1.Index idxStackPrimaryAssetId = i1.Index(
'idx_stack_primary_asset_id',
'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)',
);
final i1.Index idxRemoteAssetOwnerChecksum = i1.Index(
'idx_remote_asset_owner_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
);
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
'UQ_remote_assets_owner_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
);
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
'UQ_remote_assets_owner_library_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
);
final i1.Index idxRemoteAssetChecksum = i1.Index(
'idx_remote_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
);
final i1.Index idxRemoteAssetStackId = i1.Index(
'idx_remote_asset_stack_id',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)',
);
final i1.Index idxRemoteAssetLocalDateTimeDay = i1.Index(
'idx_remote_asset_local_date_time_day',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME(\'%Y-%m-%d\', local_date_time))',
);
final i1.Index idxRemoteAssetLocalDateTimeMonth = i1.Index(
'idx_remote_asset_local_date_time_month',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME(\'%Y-%m\', local_date_time))',
);
late final Shape21 authUserEntity = Shape21(
source: i0.VersionedTable(
entityName: 'auth_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_3,
_column_2,
_column_84,
_column_85,
_column_92,
_column_93,
_column_7,
_column_94,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape4 userMetadataEntity = Shape4(
source: i0.VersionedTable(
entityName: 'user_metadata_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
columns: [_column_25, _column_26, _column_27],
attachedDatabase: database,
),
alias: null,
);
late final Shape5 partnerEntity = Shape5(
source: i0.VersionedTable(
entityName: 'partner_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
columns: [_column_28, _column_29, _column_30],
attachedDatabase: database,
),
alias: null,
);
late final Shape8 remoteExifEntity = Shape8(
source: i0.VersionedTable(
entityName: 'remote_exif_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id)'],
columns: [
_column_36,
_column_37,
_column_38,
_column_39,
_column_40,
_column_41,
_column_11,
_column_10,
_column_42,
_column_43,
_column_44,
_column_45,
_column_46,
_column_47,
_column_48,
_column_49,
_column_50,
_column_51,
_column_52,
_column_53,
_column_54,
_column_55,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape7 remoteAlbumAssetEntity = Shape7(
source: i0.VersionedTable(
entityName: 'remote_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_36, _column_60],
attachedDatabase: database,
),
alias: null,
);
late final Shape10 remoteAlbumUserEntity = Shape10(
source: i0.VersionedTable(
entityName: 'remote_album_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
columns: [_column_60, _column_25, _column_61],
attachedDatabase: database,
),
alias: null,
);
late final Shape27 remoteAssetCloudIdEntity = Shape27(
source: i0.VersionedTable(
entityName: 'remote_asset_cloud_id_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id)'],
columns: [
_column_36,
_column_99,
_column_100,
_column_96,
_column_46,
_column_47,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape11 memoryEntity = Shape11(
source: i0.VersionedTable(
entityName: 'memory_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_9,
_column_5,
_column_18,
_column_15,
_column_8,
_column_62,
_column_63,
_column_64,
_column_65,
_column_66,
_column_67,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape12 memoryAssetEntity = Shape12(
source: i0.VersionedTable(
entityName: 'memory_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
columns: [_column_36, _column_68],
attachedDatabase: database,
),
alias: null,
);
late final Shape14 personEntity = Shape14(
source: i0.VersionedTable(
entityName: 'person_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_9,
_column_5,
_column_15,
_column_1,
_column_69,
_column_71,
_column_72,
_column_73,
_column_74,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape29 assetFaceEntity = Shape29(
source: i0.VersionedTable(
entityName: 'asset_face_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_36,
_column_76,
_column_77,
_column_78,
_column_79,
_column_80,
_column_81,
_column_82,
_column_83,
_column_102,
_column_18,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape18 storeEntity = Shape18(
source: i0.VersionedTable(
entityName: 'store_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [_column_87, _column_88, _column_89],
attachedDatabase: database,
),
alias: null,
);
late final Shape25 trashedLocalAssetEntity = Shape25(
source: i0.VersionedTable(
entityName: 'trashed_local_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id, album_id)'],
columns: [
_column_1,
_column_8,
_column_9,
_column_5,
_column_10,
_column_11,
_column_12,
_column_0,
_column_95,
_column_22,
_column_14,
_column_23,
_column_97,
],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxPartnerSharedWithId = i1.Index(
'idx_partner_shared_with_id',
'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)',
);
final i1.Index idxLatLng = i1.Index(
'idx_lat_lng',
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
);
final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index(
'idx_remote_album_asset_album_asset',
'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)',
);
final i1.Index idxRemoteAssetCloudId = i1.Index(
'idx_remote_asset_cloud_id',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)',
);
final i1.Index idxPersonOwnerId = i1.Index(
'idx_person_owner_id',
'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)',
);
final i1.Index idxAssetFacePersonId = i1.Index(
'idx_asset_face_person_id',
'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)',
);
final i1.Index idxAssetFaceAssetId = i1.Index(
'idx_asset_face_asset_id',
'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
);
final i1.Index idxTrashedLocalAssetChecksum = i1.Index(
'idx_trashed_local_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)',
);
final i1.Index idxTrashedLocalAssetAlbum = i1.Index(
'idx_trashed_local_asset_album',
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)',
);
}
class Shape29 extends i0.VersionedTable {
Shape29({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get assetId =>
columnsByName['asset_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get personId =>
columnsByName['person_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get imageWidth =>
columnsByName['image_width']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get imageHeight =>
columnsByName['image_height']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get boundingBoxX1 =>
columnsByName['bounding_box_x1']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get boundingBoxY1 =>
columnsByName['bounding_box_y1']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get boundingBoxX2 =>
columnsByName['bounding_box_x2']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get boundingBoxY2 =>
columnsByName['bounding_box_y2']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get sourceType =>
columnsByName['source_type']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get isVisible =>
columnsByName['is_visible']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<DateTime> get deletedAt =>
columnsByName['deleted_at']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<bool> _column_102(String aliasedName) =>
i1.GeneratedColumn<bool>(
'is_visible',
aliasedName,
false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("is_visible" IN (0, 1))',
),
defaultValue: const CustomExpression('1'),
);
i0.MigrationStepWithVersion migrationSteps({ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2, required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3, required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@@ -8923,7 +8379,6 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema17 schema) from16To17, required Future<void> Function(i1.Migrator m, Schema17 schema) from16To17,
required Future<void> Function(i1.Migrator m, Schema18 schema) from17To18, required Future<void> Function(i1.Migrator m, Schema18 schema) from17To18,
required Future<void> Function(i1.Migrator m, Schema19 schema) from18To19, required Future<void> Function(i1.Migrator m, Schema19 schema) from18To19,
required Future<void> Function(i1.Migrator m, Schema20 schema) from19To20,
}) { }) {
return (currentVersion, database) async { return (currentVersion, database) async {
switch (currentVersion) { switch (currentVersion) {
@@ -9017,11 +8472,6 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema); final migrator = i1.Migrator(database, schema);
await from18To19(migrator, schema); await from18To19(migrator, schema);
return 19; return 19;
case 19:
final schema = Schema20(database: database);
final migrator = i1.Migrator(database, schema);
await from19To20(migrator, schema);
return 20;
default: default:
throw ArgumentError.value('Unknown migration from $currentVersion'); throw ArgumentError.value('Unknown migration from $currentVersion');
} }
@@ -9047,7 +8497,6 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema17 schema) from16To17, required Future<void> Function(i1.Migrator m, Schema17 schema) from16To17,
required Future<void> Function(i1.Migrator m, Schema18 schema) from17To18, required Future<void> Function(i1.Migrator m, Schema18 schema) from17To18,
required Future<void> Function(i1.Migrator m, Schema19 schema) from18To19, required Future<void> Function(i1.Migrator m, Schema19 schema) from18To19,
required Future<void> Function(i1.Migrator m, Schema20 schema) from19To20,
}) => i0.VersionedSchema.stepByStepHelper( }) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps( step: migrationSteps(
from1To2: from1To2, from1To2: from1To2,
@@ -9068,6 +8517,5 @@ i1.OnUpgrade stepByStep({
from16To17: from16To17, from16To17: from16To17,
from17To18: from17To18, from17To18: from17To18,
from18To19: from18To19, from18To19: from18To19,
from19To20: from19To20,
), ),
); );

View File

@@ -184,8 +184,7 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
} }
if (keepFavorites) { if (keepFavorites) {
whereClause = whereClause = whereClause & _db.localAssetEntity.isFavorite.equals(false);
whereClause & _db.localAssetEntity.isFavorite.equals(false) & _db.remoteAssetEntity.isFavorite.equals(false);
} }
query.where(whereClause); query.where(whereClause);

View File

@@ -16,15 +16,9 @@ class DriftPeopleRepository extends DriftDatabaseRepository {
} }
Future<List<DriftPerson>> getAssetPeople(String assetId) async { Future<List<DriftPerson>> getAssetPeople(String assetId) async {
final query = final query = _db.select(_db.assetFaceEntity).join([
_db.select(_db.assetFaceEntity).join([ innerJoin(_db.personEntity, _db.personEntity.id.equalsExp(_db.assetFaceEntity.personId)),
innerJoin(_db.personEntity, _db.personEntity.id.equalsExp(_db.assetFaceEntity.personId)), ])..where(_db.assetFaceEntity.assetId.equals(assetId) & _db.personEntity.isHidden.equals(false));
])..where(
_db.assetFaceEntity.assetId.equals(assetId) &
_db.assetFaceEntity.isVisible.equals(true) &
_db.assetFaceEntity.deletedAt.isNull() &
_db.personEntity.isHidden.equals(false),
);
return query.map((row) { return query.map((row) {
final person = row.readTable(_db.personEntity); final person = row.readTable(_db.personEntity);
@@ -45,9 +39,7 @@ class DriftPeopleRepository extends DriftDatabaseRepository {
..where( ..where(
people.isHidden.equals(false) & people.isHidden.equals(false) &
assets.deletedAt.isNull() & assets.deletedAt.isNull() &
assets.visibility.equalsValue(AssetVisibility.timeline) & assets.visibility.equalsValue(AssetVisibility.timeline),
faces.isVisible.equals(true) &
faces.deletedAt.isNull(),
) )
..groupBy([people.id], having: faces.id.count().isBiggerOrEqualValue(3) | people.name.equals('').not()) ..groupBy([people.id], having: faces.id.count().isBiggerOrEqualValue(3) | people.name.equals('').not())
..orderBy([ ..orderBy([

View File

@@ -1,8 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart';
@@ -323,32 +321,26 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
}).watchSingleOrNull(); }).watchSingleOrNull();
} }
Future<List<String>> getSortedAlbumIds(List<String> albumIds, {required AssetDateAggregation aggregation}) async { Future<DateTime?> getNewestAssetTimestamp(String albumId) {
if (albumIds.isEmpty) return []; final query = _db.remoteAlbumAssetEntity.selectOnly()
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId))
..addColumns([_db.remoteAssetEntity.localDateTime.max()])
..join([
innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)),
]);
final jsonIds = jsonEncode(albumIds); return query.map((row) => row.read(_db.remoteAssetEntity.localDateTime.max())).getSingleOrNull();
final sqlAgg = aggregation == AssetDateAggregation.start ? 'MIN' : 'MAX'; }
final rows = await _db Future<DateTime?> getOldestAssetTimestamp(String albumId) {
.customSelect( final query = _db.remoteAlbumAssetEntity.selectOnly()
''' ..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId))
SELECT ..addColumns([_db.remoteAssetEntity.localDateTime.min()])
raae.album_id, ..join([
$sqlAgg(rae.local_date_time) AS asset_date innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)),
FROM json_each(?) ids ]);
INNER JOIN remote_album_asset_entity raae
ON raae.album_id = ids.value
INNER JOIN remote_asset_entity rae
ON rae.id = raae.asset_id
GROUP BY raae.album_id
ORDER BY asset_date ASC
''',
variables: [Variable<String>(jsonIds)],
readsFrom: {_db.remoteAlbumAssetEntity, _db.remoteAssetEntity},
)
.get();
return rows.map((row) => row.read<String>('album_id')).toList(); return query.map((row) => row.read(_db.remoteAssetEntity.localDateTime.min())).getSingleOrNull();
} }
Future<int> getCount() { Future<int> getCount() {

View File

@@ -35,7 +35,6 @@ class SearchApiRepository extends ApiRepository {
isFavorite: filter.display.isFavorite ? true : null, isFavorite: filter.display.isFavorite ? true : null,
isNotInAlbum: filter.display.isNotInAlbum ? true : null, isNotInAlbum: filter.display.isNotInAlbum ? true : null,
personIds: filter.people.map((e) => e.id).toList(), personIds: filter.people.map((e) => e.id).toList(),
tagIds: filter.tagIds,
type: type, type: type,
page: page, page: page,
size: 100, size: 100,
@@ -60,7 +59,6 @@ class SearchApiRepository extends ApiRepository {
isFavorite: filter.display.isFavorite ? true : null, isFavorite: filter.display.isFavorite ? true : null,
isNotInAlbum: filter.display.isNotInAlbum ? true : null, isNotInAlbum: filter.display.isNotInAlbum ? true : null,
personIds: filter.people.map((e) => e.id).toList(), personIds: filter.people.map((e) => e.id).toList(),
tagIds: filter.tagIds,
type: type, type: type,
page: page, page: page,
size: 1000, size: 1000,

View File

@@ -7,7 +7,6 @@ import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/sync_event.model.dart'; import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/semver.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
@@ -26,7 +25,6 @@ class SyncApiRepository {
Future<void> streamChanges( Future<void> streamChanges(
Future<void> Function(List<SyncEvent>, Function() abort, Function() reset) onData, { Future<void> Function(List<SyncEvent>, Function() abort, Function() reset) onData, {
required SemVer serverVersion,
Function()? onReset, Function()? onReset,
int batchSize = kSyncEventBatchSize, int batchSize = kSyncEventBatchSize,
http.Client? httpClient, http.Client? httpClient,
@@ -66,8 +64,7 @@ class SyncApiRepository {
SyncRequestType.partnerStacksV1, SyncRequestType.partnerStacksV1,
SyncRequestType.userMetadataV1, SyncRequestType.userMetadataV1,
SyncRequestType.peopleV1, SyncRequestType.peopleV1,
if (serverVersion < const SemVer(major: 2, minor: 6, patch: 0)) SyncRequestType.assetFacesV1, SyncRequestType.assetFacesV1,
if (serverVersion >= const SemVer(major: 2, minor: 6, patch: 0)) SyncRequestType.assetFacesV2,
], ],
reset: shouldReset, reset: shouldReset,
).toJson(), ).toJson(),
@@ -193,7 +190,6 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
SyncEntityType.personV1: SyncPersonV1.fromJson, SyncEntityType.personV1: SyncPersonV1.fromJson,
SyncEntityType.personDeleteV1: SyncPersonDeleteV1.fromJson, SyncEntityType.personDeleteV1: SyncPersonDeleteV1.fromJson,
SyncEntityType.assetFaceV1: SyncAssetFaceV1.fromJson, SyncEntityType.assetFaceV1: SyncAssetFaceV1.fromJson,
SyncEntityType.assetFaceV2: SyncAssetFaceV2.fromJson,
SyncEntityType.assetFaceDeleteV1: SyncAssetFaceDeleteV1.fromJson, SyncEntityType.assetFaceDeleteV1: SyncAssetFaceDeleteV1.fromJson,
SyncEntityType.syncCompleteV1: _SyncEmptyDto.fromJson, SyncEntityType.syncCompleteV1: _SyncEmptyDto.fromJson,
}; };

View File

@@ -652,37 +652,6 @@ class SyncStreamRepository extends DriftDatabaseRepository {
} }
} }
Future<void> updateAssetFacesV2(Iterable<SyncAssetFaceV2> data) async {
try {
await _db.batch((batch) {
for (final assetFace in data) {
final companion = AssetFaceEntityCompanion(
assetId: Value(assetFace.assetId),
personId: Value(assetFace.personId),
imageWidth: Value(assetFace.imageWidth),
imageHeight: Value(assetFace.imageHeight),
boundingBoxX1: Value(assetFace.boundingBoxX1),
boundingBoxY1: Value(assetFace.boundingBoxY1),
boundingBoxX2: Value(assetFace.boundingBoxX2),
boundingBoxY2: Value(assetFace.boundingBoxY2),
sourceType: Value(assetFace.sourceType),
deletedAt: Value(assetFace.deletedAt),
isVisible: Value(assetFace.isVisible),
);
batch.insert(
_db.assetFaceEntity,
companion.copyWith(id: Value(assetFace.id)),
onConflict: DoUpdate((_) => companion),
);
}
});
} catch (error, stack) {
_logger.severe('Error: updateAssetFacesV2', error, stack);
rethrow;
}
}
Future<void> deleteAssetFacesV1(Iterable<SyncAssetFaceDeleteV1> data) async { Future<void> deleteAssetFacesV1(Iterable<SyncAssetFaceDeleteV1> data) async {
try { try {
await _db.batch((batch) { await _db.batch((batch) {

View File

@@ -1,17 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/infrastructure/repositories/api.repository.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:openapi/api.dart';
final tagsApiRepositoryProvider = Provider<TagsApiRepository>(
(ref) => TagsApiRepository(ref.read(apiServiceProvider).tagsApi),
);
class TagsApiRepository extends ApiRepository {
final TagsApi _api;
const TagsApiRepository(this._api);
Future<List<TagResponseDto>?> getAllTags() async {
return await _api.getAllTags();
}
}

View File

@@ -323,7 +323,6 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
row.deletedAt.isNull() & row.ownerId.equals(userId) & row.visibility.equalsValue(AssetVisibility.archive), row.deletedAt.isNull() & row.ownerId.equals(userId) & row.visibility.equalsValue(AssetVisibility.archive),
groupBy: groupBy, groupBy: groupBy,
origin: TimelineOrigin.archive, origin: TimelineOrigin.archive,
joinLocal: true,
); );
TimelineQuery locked(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder( TimelineQuery locked(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder(
@@ -422,9 +421,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
_db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.ownerId.equals(userId) & _db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) & _db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
_db.assetFaceEntity.personId.equals(personId) & _db.assetFaceEntity.personId.equals(personId),
_db.assetFaceEntity.isVisible.equals(true) &
_db.assetFaceEntity.deletedAt.isNull(),
); );
return query.map((row) { return query.map((row) {
@@ -449,9 +446,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
_db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.ownerId.equals(userId) & _db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) & _db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
_db.assetFaceEntity.personId.equals(personId) & _db.assetFaceEntity.personId.equals(personId),
_db.assetFaceEntity.isVisible.equals(true) &
_db.assetFaceEntity.deletedAt.isNull(),
) )
..groupBy([dateExp]) ..groupBy([dateExp])
..orderBy([OrderingTerm.desc(dateExp)]); ..orderBy([OrderingTerm.desc(dateExp)]);
@@ -481,9 +476,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
_db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.ownerId.equals(userId) & _db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) & _db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
_db.assetFaceEntity.personId.equals(personId) & _db.assetFaceEntity.personId.equals(personId),
_db.assetFaceEntity.isVisible.equals(true) &
_db.assetFaceEntity.deletedAt.isNull(),
) )
..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)]) ..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)])
..limit(count, offset: offset); ..limit(count, offset: offset);

View File

@@ -214,7 +214,6 @@ class SearchFilter {
String? ocr; String? ocr;
String? language; String? language;
String? assetId; String? assetId;
List<String>? tagIds;
Set<PersonDto> people; Set<PersonDto> people;
SearchLocationFilter location; SearchLocationFilter location;
SearchCameraFilter camera; SearchCameraFilter camera;
@@ -232,7 +231,6 @@ class SearchFilter {
this.ocr, this.ocr,
this.language, this.language,
this.assetId, this.assetId,
this.tagIds,
required this.people, required this.people,
required this.location, required this.location,
required this.camera, required this.camera,
@@ -248,7 +246,6 @@ class SearchFilter {
(description == null || (description!.isEmpty)) && (description == null || (description!.isEmpty)) &&
(assetId == null || (assetId!.isEmpty)) && (assetId == null || (assetId!.isEmpty)) &&
(ocr == null || (ocr!.isEmpty)) && (ocr == null || (ocr!.isEmpty)) &&
(tagIds ?? []).isEmpty &&
people.isEmpty && people.isEmpty &&
location.country == null && location.country == null &&
location.state == null && location.state == null &&
@@ -272,7 +269,6 @@ class SearchFilter {
String? ocr, String? ocr,
String? assetId, String? assetId,
Set<PersonDto>? people, Set<PersonDto>? people,
List<String>? tagIds,
SearchLocationFilter? location, SearchLocationFilter? location,
SearchCameraFilter? camera, SearchCameraFilter? camera,
SearchDateFilter? date, SearchDateFilter? date,
@@ -294,13 +290,12 @@ class SearchFilter {
display: display ?? this.display, display: display ?? this.display,
rating: rating ?? this.rating, rating: rating ?? this.rating,
mediaType: mediaType ?? this.mediaType, mediaType: mediaType ?? this.mediaType,
tagIds: tagIds ?? this.tagIds,
); );
} }
@override @override
String toString() { String toString() {
return 'SearchFilter(context: $context, filename: $filename, description: $description, language: $language, ocr: $ocr, people: $people, location: $location, tagIds: $tagIds, camera: $camera, date: $date, display: $display, rating: $rating, mediaType: $mediaType, assetId: $assetId)'; return 'SearchFilter(context: $context, filename: $filename, description: $description, language: $language, ocr: $ocr, people: $people, location: $location, camera: $camera, date: $date, display: $display, rating: $rating, mediaType: $mediaType, assetId: $assetId)';
} }
@override @override
@@ -314,7 +309,6 @@ class SearchFilter {
other.ocr == ocr && other.ocr == ocr &&
other.assetId == assetId && other.assetId == assetId &&
other.people == people && other.people == people &&
other.tagIds == tagIds &&
other.location == location && other.location == location &&
other.camera == camera && other.camera == camera &&
other.date == date && other.date == date &&
@@ -332,7 +326,6 @@ class SearchFilter {
ocr.hashCode ^ ocr.hashCode ^
assetId.hashCode ^ assetId.hashCode ^
people.hashCode ^ people.hashCode ^
tagIds.hashCode ^
location.hashCode ^ location.hashCode ^
camera.hashCode ^ camera.hashCode ^
date.hashCode ^ date.hashCode ^

View File

@@ -6,7 +6,6 @@ class ServerFeatures {
final bool oauthEnabled; final bool oauthEnabled;
final bool passwordLogin; final bool passwordLogin;
final bool ocr; final bool ocr;
final bool smartSearch;
const ServerFeatures({ const ServerFeatures({
required this.trash, required this.trash,
@@ -14,30 +13,21 @@ class ServerFeatures {
required this.oauthEnabled, required this.oauthEnabled,
required this.passwordLogin, required this.passwordLogin,
this.ocr = false, this.ocr = false,
this.smartSearch = false,
}); });
ServerFeatures copyWith({ ServerFeatures copyWith({bool? trash, bool? map, bool? oauthEnabled, bool? passwordLogin, bool? ocr}) {
bool? trash,
bool? map,
bool? oauthEnabled,
bool? passwordLogin,
bool? ocr,
bool? smartSearch,
}) {
return ServerFeatures( return ServerFeatures(
trash: trash ?? this.trash, trash: trash ?? this.trash,
map: map ?? this.map, map: map ?? this.map,
oauthEnabled: oauthEnabled ?? this.oauthEnabled, oauthEnabled: oauthEnabled ?? this.oauthEnabled,
passwordLogin: passwordLogin ?? this.passwordLogin, passwordLogin: passwordLogin ?? this.passwordLogin,
ocr: ocr ?? this.ocr, ocr: ocr ?? this.ocr,
smartSearch: smartSearch ?? this.smartSearch,
); );
} }
@override @override
String toString() { String toString() {
return 'ServerFeatures(trash: $trash, map: $map, oauthEnabled: $oauthEnabled, passwordLogin: $passwordLogin, ocr: $ocr, smartSearch: $smartSearch)'; return 'ServerFeatures(trash: $trash, map: $map, oauthEnabled: $oauthEnabled, passwordLogin: $passwordLogin, ocr: $ocr)';
} }
ServerFeatures.fromDto(ServerFeaturesDto dto) ServerFeatures.fromDto(ServerFeaturesDto dto)
@@ -45,8 +35,7 @@ class ServerFeatures {
map = dto.map, map = dto.map,
oauthEnabled = dto.oauth, oauthEnabled = dto.oauth,
passwordLogin = dto.passwordLogin, passwordLogin = dto.passwordLogin,
ocr = dto.ocr, ocr = dto.ocr;
smartSearch = dto.smartSearch;
@override @override
bool operator ==(covariant ServerFeatures other) { bool operator ==(covariant ServerFeatures other) {
@@ -56,17 +45,11 @@ class ServerFeatures {
other.map == map && other.map == map &&
other.oauthEnabled == oauthEnabled && other.oauthEnabled == oauthEnabled &&
other.passwordLogin == passwordLogin && other.passwordLogin == passwordLogin &&
other.ocr == ocr && other.ocr == ocr;
other.smartSearch == smartSearch;
} }
@override @override
int get hashCode { int get hashCode {
return trash.hashCode ^ return trash.hashCode ^ map.hashCode ^ oauthEnabled.hashCode ^ passwordLogin.hashCode ^ ocr.hashCode;
map.hashCode ^
oauthEnabled.hashCode ^
passwordLogin.hashCode ^
ocr.hashCode ^
smartSearch.hashCode;
} }
} }

View File

@@ -14,7 +14,6 @@ class SharedLink {
final String key; final String key;
final bool showMetadata; final bool showMetadata;
final SharedLinkSource type; final SharedLinkSource type;
final String? slug;
const SharedLink({ const SharedLink({
required this.id, required this.id,
@@ -28,7 +27,6 @@ class SharedLink {
required this.key, required this.key,
required this.showMetadata, required this.showMetadata,
required this.type, required this.type,
required this.slug,
}); });
SharedLink copyWith({ SharedLink copyWith({
@@ -43,7 +41,6 @@ class SharedLink {
String? key, String? key,
bool? showMetadata, bool? showMetadata,
SharedLinkSource? type, SharedLinkSource? type,
String? slug,
}) { }) {
return SharedLink( return SharedLink(
id: id ?? this.id, id: id ?? this.id,
@@ -57,7 +54,6 @@ class SharedLink {
key: key ?? this.key, key: key ?? this.key,
showMetadata: showMetadata ?? this.showMetadata, showMetadata: showMetadata ?? this.showMetadata,
type: type ?? this.type, type: type ?? this.type,
slug: slug ?? this.slug,
); );
} }
@@ -70,7 +66,6 @@ class SharedLink {
expiresAt = dto.expiresAt, expiresAt = dto.expiresAt,
key = dto.key, key = dto.key,
showMetadata = dto.showMetadata, showMetadata = dto.showMetadata,
slug = dto.slug,
type = dto.type == SharedLinkType.ALBUM ? SharedLinkSource.album : SharedLinkSource.individual, type = dto.type == SharedLinkType.ALBUM ? SharedLinkSource.album : SharedLinkSource.individual,
title = dto.type == SharedLinkType.ALBUM title = dto.type == SharedLinkType.ALBUM
? dto.album?.albumName.toUpperCase() ?? "UNKNOWN SHARE" ? dto.album?.albumName.toUpperCase() ?? "UNKNOWN SHARE"
@@ -83,7 +78,7 @@ class SharedLink {
@override @override
String toString() => String toString() =>
'SharedLink(id=$id, title=$title, thumbAssetId=$thumbAssetId, allowDownload=$allowDownload, allowUpload=$allowUpload, description=$description, password=$password, expiresAt=$expiresAt, key=$key, showMetadata=$showMetadata, type=$type, slug=$slug)'; 'SharedLink(id=$id, title=$title, thumbAssetId=$thumbAssetId, allowDownload=$allowDownload, allowUpload=$allowUpload, description=$description, password=$password, expiresAt=$expiresAt, key=$key, showMetadata=$showMetadata, type=$type)';
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
@@ -99,8 +94,7 @@ class SharedLink {
other.expiresAt == expiresAt && other.expiresAt == expiresAt &&
other.key == key && other.key == key &&
other.showMetadata == showMetadata && other.showMetadata == showMetadata &&
other.type == type && other.type == type;
other.slug == slug;
@override @override
int get hashCode => int get hashCode =>
@@ -114,6 +108,5 @@ class SharedLink {
expiresAt.hashCode ^ expiresAt.hashCode ^
key.hashCode ^ key.hashCode ^
showMetadata.hashCode ^ showMetadata.hashCode ^
type.hashCode ^ type.hashCode;
slug.hashCode;
} }

View File

@@ -134,7 +134,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final user = sharedUsers.value[index]; final user = sharedUsers.value[index];
return ListTile( return ListTile(
leading: UserCircleAvatar(user: user), leading: UserCircleAvatar(user: user, radius: 22),
title: Text(user.name, style: const TextStyle(fontWeight: FontWeight.w500)), title: Text(user.name, style: const TextStyle(fontWeight: FontWeight.w500)),
subtitle: Text(user.email, style: TextStyle(color: context.colorScheme.onSurfaceSecondary)), subtitle: Text(user.email, style: TextStyle(color: context.colorScheme.onSurfaceSecondary)),
trailing: userId == user.id || isOwner ? const Icon(Icons.more_horiz_rounded) : const SizedBox(), trailing: userId == user.id || isOwner ? const Icon(Icons.more_horiz_rounded) : const SizedBox(),

View File

@@ -41,7 +41,7 @@ class AlbumSharedUserIcons extends HookConsumerWidget {
itemBuilder: ((context, index) { itemBuilder: ((context, index) {
return Padding( return Padding(
padding: const EdgeInsets.only(right: 8.0), padding: const EdgeInsets.only(right: 8.0),
child: UserCircleAvatar(user: sharedUsers.value[index], size: 36), child: UserCircleAvatar(user: sharedUsers.value[index], radius: 18, size: 36),
); );
}), }),
itemCount: sharedUsers.value.length, itemCount: sharedUsers.value.length,

View File

@@ -221,37 +221,8 @@ class GalleryViewerPage extends HookConsumerWidget {
onDragUpdate: (_, details, __) { onDragUpdate: (_, details, __) {
handleSwipeUpDown(details); handleSwipeUpDown(details);
}, },
onTapDown: (ctx, tapDownDetails, _) { onTapDown: (_, __, ___) {
final tapToNavigate = ref.read(appSettingsServiceProvider).getSetting<bool>(AppSettingsEnum.tapToNavigate); ref.read(showControlsProvider.notifier).toggle();
if (!tapToNavigate) {
ref.read(showControlsProvider.notifier).toggle();
return;
}
double tapX = tapDownDetails.globalPosition.dx;
double screenWidth = ctx.width;
// We want to change images if the user taps in the leftmost or
// rightmost quarter of the screen
bool tappedLeftSide = tapX < screenWidth / 4;
bool tappedRightSide = tapX > screenWidth * (3 / 4);
int? currentPage = controller.page?.toInt();
int maxPage = renderList.totalAssets - 1;
if (tappedLeftSide && currentPage != null) {
// Nested if because we don't want to fallback to show/hide controls
if (currentPage != 0) {
controller.jumpToPage(currentPage - 1);
}
} else if (tappedRightSide && currentPage != null) {
// Nested if because we don't want to fallback to show/hide controls
if (currentPage != maxPage) {
controller.jumpToPage(currentPage + 1);
}
} else {
ref.read(showControlsProvider.notifier).toggle();
}
}, },
onLongPressStart: asset.isMotionPhoto onLongPressStart: asset.isMotionPhoto
? (_, __, ___) { ? (_, __, ___) {

View File

@@ -109,43 +109,9 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
if (context.router.current.name == SplashScreenRoute.name) { if (context.router.current.name == SplashScreenRoute.name) {
final needBetaMigration = Store.get(StoreKey.needBetaMigration, false); final needBetaMigration = Store.get(StoreKey.needBetaMigration, false);
if (needBetaMigration) { if (needBetaMigration) {
bool migrate =
(await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text("New Timeline Experience"),
content: const Text(
"The old timeline has been deprecated and will be removed in an upcoming release. Would you like to switch to the new timeline now?",
),
actions: [
TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: const Text("No")),
ElevatedButton(onPressed: () => Navigator.of(ctx).pop(true), child: const Text("Yes")),
],
),
)) ??
false;
if (migrate != true) {
migrate =
(await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text("Are you sure?"),
content: const Text(
"If you choose to remain on the old timeline, you will be automatically migrated to the new timeline in an upcoming release. Would you like to switch now?",
),
actions: [
TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: const Text("No")),
ElevatedButton(onPressed: () => Navigator.of(ctx).pop(true), child: const Text("Yes")),
],
),
)) ??
false;
}
await Store.put(StoreKey.needBetaMigration, false); await Store.put(StoreKey.needBetaMigration, false);
if (migrate) { unawaited(context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: true)]));
unawaited(context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: true)])); return;
return;
}
} }
unawaited(context.replaceRoute(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute())); unawaited(context.replaceRoute(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute()));

View File

@@ -1,4 +1,6 @@
import 'dart:async';
import 'dart:typed_data'; import 'dart:typed_data';
import 'dart:ui';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
@@ -10,7 +12,6 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/repositories/file_media.repository.dart'; import 'package:immich_mobile/repositories/file_media.repository.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/image_converter.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
@@ -29,10 +30,27 @@ class EditImagePage extends ConsumerWidget {
final bool isEdited; final bool isEdited;
const EditImagePage({super.key, required this.asset, required this.image, required this.isEdited}); const EditImagePage({super.key, required this.asset, required this.image, required this.isEdited});
Future<Uint8List> _imageToUint8List(Image image) async {
final Completer<Uint8List> completer = Completer();
image.image
.resolve(const ImageConfiguration())
.addListener(
ImageStreamListener((ImageInfo info, bool _) {
info.image.toByteData(format: ImageByteFormat.png).then((byteData) {
if (byteData != null) {
completer.complete(byteData.buffer.asUint8List());
} else {
completer.completeError('Failed to convert image to bytes');
}
});
}, onError: (exception, stackTrace) => completer.completeError(exception)),
);
return completer.future;
}
Future<void> _saveEditedImage(BuildContext context, Asset asset, Image image, WidgetRef ref) async { Future<void> _saveEditedImage(BuildContext context, Asset asset, Image image, WidgetRef ref) async {
try { try {
final Uint8List imageData = await imageToUint8List(image); final Uint8List imageData = await _imageToUint8List(image);
await ref await ref
.read(fileMediaRepositoryProvider) .read(fileMediaRepositoryProvider)
.saveImage(imageData, title: "${p.withoutExtension(asset.fileName)}_edited.jpg"); .saveImage(imageData, title: "${p.withoutExtension(asset.fileName)}_edited.jpg");

View File

@@ -29,8 +29,6 @@ class SharedLinkEditPage extends HookConsumerWidget {
final descriptionController = useTextEditingController(text: existingLink?.description ?? ""); final descriptionController = useTextEditingController(text: existingLink?.description ?? "");
final descriptionFocusNode = useFocusNode(); final descriptionFocusNode = useFocusNode();
final passwordController = useTextEditingController(text: existingLink?.password ?? ""); final passwordController = useTextEditingController(text: existingLink?.password ?? "");
final slugController = useTextEditingController(text: existingLink?.slug ?? "");
final slugFocusNode = useFocusNode();
final showMetadata = useState(existingLink?.showMetadata ?? true); final showMetadata = useState(existingLink?.showMetadata ?? true);
final allowDownload = useState(existingLink?.allowDownload ?? true); final allowDownload = useState(existingLink?.allowDownload ?? true);
final allowUpload = useState(existingLink?.allowUpload ?? false); final allowUpload = useState(existingLink?.allowUpload ?? false);
@@ -110,26 +108,6 @@ class SharedLinkEditPage extends HookConsumerWidget {
); );
} }
Widget buildSlugField() {
return TextField(
controller: slugController,
enabled: newShareLink.value.isEmpty,
focusNode: slugFocusNode,
textInputAction: TextInputAction.done,
autofocus: false,
decoration: InputDecoration(
labelText: 'custom_url'.tr(),
labelStyle: TextStyle(fontWeight: FontWeight.bold, color: colorScheme.primary),
floatingLabelBehavior: FloatingLabelBehavior.always,
border: const OutlineInputBorder(),
hintText: 'custom_url'.tr(),
hintStyle: const TextStyle(fontWeight: FontWeight.normal, fontSize: 14),
disabledBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.grey.withValues(alpha: 0.5))),
),
onTapOutside: (_) => slugFocusNode.unfocus(),
);
}
Widget buildShowMetaButton() { Widget buildShowMetaButton() {
return SwitchListTile.adaptive( return SwitchListTile.adaptive(
value: showMetadata.value, value: showMetadata.value,
@@ -283,7 +261,6 @@ class SharedLinkEditPage extends HookConsumerWidget {
allowUpload: allowUpload.value, allowUpload: allowUpload.value,
description: descriptionController.text.isEmpty ? null : descriptionController.text, description: descriptionController.text.isEmpty ? null : descriptionController.text,
password: passwordController.text.isEmpty ? null : passwordController.text, password: passwordController.text.isEmpty ? null : passwordController.text,
slug: slugController.text.isEmpty ? null : slugController.text,
expiresAt: expiryAfter.value == 0 ? null : calculateExpiry(), expiresAt: expiryAfter.value == 0 ? null : calculateExpiry(),
); );
ref.invalidate(sharedLinksStateProvider); ref.invalidate(sharedLinksStateProvider);
@@ -297,10 +274,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
} }
if (newLink != null && serverUrl != null) { if (newLink != null && serverUrl != null) {
final hasSlug = newLink.slug?.isNotEmpty == true; newShareLink.value = "${serverUrl}share/${newLink.key}";
final urlPath = hasSlug ? newLink.slug : newLink.key;
final basePath = hasSlug ? 's' : 'share';
newShareLink.value = "$serverUrl$basePath/$urlPath";
copyLinkToClipboard(); copyLinkToClipboard();
} else if (newLink == null) { } else if (newLink == null) {
ImmichToast.show( ImmichToast.show(
@@ -318,7 +292,6 @@ class SharedLinkEditPage extends HookConsumerWidget {
bool? meta; bool? meta;
String? desc; String? desc;
String? password; String? password;
String? slug;
DateTime? expiry; DateTime? expiry;
bool? changeExpiry; bool? changeExpiry;
@@ -342,12 +315,6 @@ class SharedLinkEditPage extends HookConsumerWidget {
password = passwordController.text; password = passwordController.text;
} }
if (slugController.text != (existingLink!.slug ?? "")) {
slug = slugController.text.isEmpty ? null : slugController.text;
} else {
slug = existingLink!.slug;
}
if (editExpiry.value) { if (editExpiry.value) {
expiry = expiryAfter.value == 0 ? null : calculateExpiry(); expiry = expiryAfter.value == 0 ? null : calculateExpiry();
changeExpiry = true; changeExpiry = true;
@@ -362,7 +329,6 @@ class SharedLinkEditPage extends HookConsumerWidget {
allowUpload: upload, allowUpload: upload,
description: desc, description: desc,
password: password, password: password,
slug: slug,
expiresAt: expiry, expiresAt: expiry,
changeExpiry: changeExpiry, changeExpiry: changeExpiry,
); );
@@ -383,7 +349,6 @@ class SharedLinkEditPage extends HookConsumerWidget {
Padding(padding: const EdgeInsets.all(padding), child: buildLinkTitle()), Padding(padding: const EdgeInsets.all(padding), child: buildLinkTitle()),
Padding(padding: const EdgeInsets.all(padding), child: buildDescriptionField()), Padding(padding: const EdgeInsets.all(padding), child: buildDescriptionField()),
Padding(padding: const EdgeInsets.all(padding), child: buildPasswordField()), Padding(padding: const EdgeInsets.all(padding), child: buildPasswordField()),
Padding(padding: const EdgeInsets.all(padding), child: buildSlugField()),
Padding( Padding(
padding: const EdgeInsets.only(left: padding, right: padding, bottom: padding), padding: const EdgeInsets.only(left: padding, right: padding, bottom: padding),
child: buildShowMetaButton(), child: buildShowMetaButton(),

View File

@@ -118,7 +118,7 @@ class MapPage extends HookConsumerWidget {
} }
// finds the nearest asset marker from the tap point and store it as the selectedMarker // finds the nearest asset marker from the tap point and store it as the selectedMarker
Future<void> onMarkerClicked(Point<double> point, LatLng _) async { Future<void> onMarkerClicked(Point<double> point, LatLng coords) async {
// Guard map not created // Guard map not created
if (mapController.value == null) { if (mapController.value == null) {
return; return;

View File

@@ -28,7 +28,7 @@ class MapLocationPickerPage extends HookConsumerWidget {
marker.value = await controller.value?.addMarkerAtLatLng(initialLatLng); marker.value = await controller.value?.addMarkerAtLatLng(initialLatLng);
} }
Future<void> onMapClick(Point<num> _, LatLng centre) async { Future<void> onMapClick(Point<num> point, LatLng centre) async {
selectedLatLng.value = centre; selectedLatLng.value = centre;
await controller.value?.animateCamera(CameraUpdate.newLatLng(centre)); await controller.value?.animateCamera(CameraUpdate.newLatLng(centre));
if (marker.value != null) { if (marker.value != null) {

View File

@@ -0,0 +1,100 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_ui/immich_ui.dart';
List<Widget> _showcaseBuilder(Function(ImmichVariant variant, ImmichColor color) builder) {
final children = <Widget>[];
final items = [
(variant: ImmichVariant.filled, title: "Filled Variant"),
(variant: ImmichVariant.ghost, title: "Ghost Variant"),
];
for (final (:variant, :title) in items) {
children.add(Text(title));
children.add(Row(spacing: 10, children: [for (var color in ImmichColor.values) builder(variant, color)]));
}
return children;
}
class _ComponentTitle extends StatelessWidget {
final String title;
const _ComponentTitle(this.title);
@override
Widget build(BuildContext context) {
return Text(title, style: context.textTheme.titleLarge);
}
}
@RoutePage()
class ImmichUIShowcasePage extends StatelessWidget {
const ImmichUIShowcasePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Immich UI Showcase')),
body: Padding(
padding: const EdgeInsets.all(20),
child: SingleChildScrollView(
child: Column(
spacing: 10,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const _ComponentTitle("IconButton"),
..._showcaseBuilder(
(variant, color) =>
ImmichIconButton(icon: Icons.favorite, color: color, variant: variant, onPressed: () {}),
),
const _ComponentTitle("CloseButton"),
..._showcaseBuilder(
(variant, color) => ImmichCloseButton(color: color, variant: variant, onPressed: () {}),
),
const _ComponentTitle("TextButton"),
ImmichTextButton(
labelText: "Text Button",
onPressed: () {},
variant: ImmichVariant.filled,
color: ImmichColor.primary,
),
ImmichTextButton(
labelText: "Text Button",
onPressed: () {},
variant: ImmichVariant.filled,
color: ImmichColor.primary,
loading: true,
),
ImmichTextButton(
labelText: "Text Button",
onPressed: () {},
variant: ImmichVariant.ghost,
color: ImmichColor.primary,
),
ImmichTextButton(
labelText: "Text Button",
onPressed: () {},
variant: ImmichVariant.ghost,
color: ImmichColor.primary,
loading: true,
),
const _ComponentTitle("Form"),
ImmichForm(
onSubmit: () {},
child: const Column(
spacing: 10,
children: [ImmichTextInput(label: "Title", hintText: "Enter a title")],
),
),
],
),
),
),
);
}
}

View File

@@ -14,15 +14,13 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da
@RoutePage() @RoutePage()
class DriftActivitiesPage extends HookConsumerWidget { class DriftActivitiesPage extends HookConsumerWidget {
final RemoteAlbum album; final RemoteAlbum album;
final String? assetId;
final String? assetName;
const DriftActivitiesPage({super.key, required this.album, this.assetId, this.assetName}); const DriftActivitiesPage({super.key, required this.album});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final activityNotifier = ref.read(albumActivityProvider(album.id, assetId).notifier); final activityNotifier = ref.read(albumActivityProvider(album.id).notifier);
final activities = ref.watch(albumActivityProvider(album.id, assetId)); final activities = ref.watch(albumActivityProvider(album.id));
final listViewScrollController = useScrollController(); final listViewScrollController = useScrollController();
void scrollToBottom() { void scrollToBottom() {
@@ -38,13 +36,7 @@ class DriftActivitiesPage extends HookConsumerWidget {
overrides: [currentRemoteAlbumScopedProvider.overrideWithValue(album)], overrides: [currentRemoteAlbumScopedProvider.overrideWithValue(album)],
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: Column( title: Text(album.name),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(album.name),
if (assetName != null) Text(assetName!, style: context.textTheme.bodySmall),
],
),
actions: [const LikeActivityActionButton(iconOnly: true)], actions: [const LikeActivityActionButton(iconOnly: true)],
actionsPadding: const EdgeInsets.only(right: 8), actionsPadding: const EdgeInsets.only(right: 8),
), ),
@@ -55,7 +47,7 @@ class DriftActivitiesPage extends HookConsumerWidget {
activityWidgets.add( activityWidgets.add(
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: CommentBubble(activity: activity, isAssetActivity: assetId != null), child: CommentBubble(activity: activity),
), ),
); );
} }

View File

@@ -44,8 +44,8 @@ class _DriftAlbumsPageState extends ConsumerState<DriftAlbumsPage> {
pinned: true, pinned: true,
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.add_rounded, size: 28),
onPressed: () => context.pushRoute(const DriftCreateAlbumRoute()), onPressed: () => context.pushRoute(const DriftCreateAlbumRoute()),
icon: const Icon(Icons.add_rounded),
), ),
], ],
showUploadButton: false, showUploadButton: false,

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