Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9bd298a160 | |||
| f78b151b64 | |||
| dfd9ed988e | |||
| a25f14e1b9 |
@@ -49,11 +49,10 @@ fix_permissions() {
|
|||||||
|
|
||||||
log "Fixing permissions for ${IMMICH_WORKSPACE}"
|
log "Fixing permissions for ${IMMICH_WORKSPACE}"
|
||||||
|
|
||||||
|
run_cmd sudo find "${IMMICH_WORKSPACE}/server/upload" -not -path "${IMMICH_WORKSPACE}/server/upload/postgres/*" -not -path "${IMMICH_WORKSPACE}/server/upload/postgres" -exec chown node {} +
|
||||||
|
|
||||||
# Change ownership for directories that exist
|
# Change ownership for directories that exist
|
||||||
for dir in "${IMMICH_WORKSPACE}/.vscode" \
|
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}/cli/node_modules" \
|
||||||
"${IMMICH_WORKSPACE}/e2e/node_modules" \
|
"${IMMICH_WORKSPACE}/e2e/node_modules" \
|
||||||
"${IMMICH_WORKSPACE}/open-api/typescript-sdk/node_modules" \
|
"${IMMICH_WORKSPACE}/open-api/typescript-sdk/node_modules" \
|
||||||
|
|||||||
@@ -8,13 +8,21 @@ services:
|
|||||||
- IMMICH_SERVER_URL=http://127.0.0.1:2283/
|
- IMMICH_SERVER_URL=http://127.0.0.1:2283/
|
||||||
volumes: !override
|
volumes: !override
|
||||||
- ..:/workspaces/immich
|
- ..:/workspaces/immich
|
||||||
|
- cli_node_modules:/workspaces/immich/cli/node_modules
|
||||||
|
- e2e_node_modules:/workspaces/immich/e2e/node_modules
|
||||||
|
- open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules
|
||||||
|
- server_node_modules:/workspaces/immich/server/node_modules
|
||||||
|
- web_node_modules:/workspaces/immich/web/node_modules
|
||||||
- ${UPLOAD_LOCATION:-upload1-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data
|
- ${UPLOAD_LOCATION:-upload1-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data
|
||||||
- ${UPLOAD_LOCATION:-upload2-devcontainer-volume}${UPLOAD_LOCATION:+/photos/upload}:/data/upload
|
- ${UPLOAD_LOCATION:-upload2-devcontainer-volume}${UPLOAD_LOCATION:+/photos/upload}:/data/upload
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
|
||||||
immich-web:
|
immich-web:
|
||||||
env_file: !reset []
|
env_file: !reset []
|
||||||
|
|
||||||
immich-machine-learning:
|
immich-machine-learning:
|
||||||
env_file: !reset []
|
env_file: !reset []
|
||||||
|
|
||||||
database:
|
database:
|
||||||
env_file: !reset []
|
env_file: !reset []
|
||||||
environment: !override
|
environment: !override
|
||||||
@@ -25,10 +33,17 @@ services:
|
|||||||
POSTGRES_HOST_AUTH_METHOD: md5
|
POSTGRES_HOST_AUTH_METHOD: md5
|
||||||
volumes:
|
volumes:
|
||||||
- ${UPLOAD_LOCATION:-postgres-devcontainer-volume}${UPLOAD_LOCATION:+/postgres}:/var/lib/postgresql/data
|
- ${UPLOAD_LOCATION:-postgres-devcontainer-volume}${UPLOAD_LOCATION:+/postgres}:/var/lib/postgresql/data
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
env_file: !reset []
|
env_file: !reset []
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
# Node modules for each service to avoid conflicts and ensure consistent dependencies
|
# Node modules for each service to avoid conflicts and ensure consistent dependencies
|
||||||
|
cli_node_modules:
|
||||||
|
e2e_node_modules:
|
||||||
|
open_api_node_modules:
|
||||||
|
server_node_modules:
|
||||||
|
web_node_modules:
|
||||||
upload1-devcontainer-volume:
|
upload1-devcontainer-volume:
|
||||||
upload2-devcontainer-volume:
|
upload2-devcontainer-volume:
|
||||||
postgres-devcontainer-volume:
|
postgres-devcontainer-volume:
|
||||||
|
|||||||
@@ -3,20 +3,15 @@
|
|||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
source /immich-devcontainer/container-common.sh
|
source /immich-devcontainer/container-common.sh
|
||||||
|
|
||||||
log "Preparing Immich Nest API Server"
|
|
||||||
log ""
|
|
||||||
export CI=1
|
|
||||||
run_cmd pnpm --filter immich install
|
|
||||||
|
|
||||||
log "Starting Nest API Server"
|
log "Starting Nest API Server"
|
||||||
log ""
|
log ""
|
||||||
cd "${IMMICH_WORKSPACE}/server" || (
|
cd "${IMMICH_WORKSPACE}/server" || (
|
||||||
log "Immich workspace not found"jj
|
log "Immich workspace not found"
|
||||||
exit 1
|
exit 1
|
||||||
)
|
)
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
run_cmd pnpm --filter immich exec nest start --debug "0.0.0.0:9230" --watch
|
run_cmd node ./node_modules/.bin/nest start --debug "0.0.0.0:9230" --watch
|
||||||
log "Nest API Server crashed with exit code $?. Respawning in 3s ..."
|
log "Nest API Server crashed with exit code $?. Respawning in 3s ..."
|
||||||
sleep 3
|
sleep 3
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -3,13 +3,6 @@
|
|||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
source /immich-devcontainer/container-common.sh
|
source /immich-devcontainer/container-common.sh
|
||||||
|
|
||||||
export CI=1
|
|
||||||
log "Preparing Immich Web Frontend"
|
|
||||||
log ""
|
|
||||||
run_cmd pnpm --filter @immich/sdk install
|
|
||||||
run_cmd pnpm --filter @immich/sdk build
|
|
||||||
run_cmd pnpm --filter immich-web install
|
|
||||||
|
|
||||||
log "Starting Immich Web Frontend"
|
log "Starting Immich Web Frontend"
|
||||||
log ""
|
log ""
|
||||||
cd "${IMMICH_WORKSPACE}/web" || (
|
cd "${IMMICH_WORKSPACE}/web" || (
|
||||||
@@ -23,7 +16,7 @@ until curl --output /dev/null --silent --head --fail "http://127.0.0.1:${IMMICH_
|
|||||||
done
|
done
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
run_cmd pnpm --filter immich-web exec vite dev --host 0.0.0.0 --port "${DEV_PORT}"
|
run_cmd node ./node_modules/.bin/vite dev --host 0.0.0.0 --port "${DEV_PORT}"
|
||||||
log "Web crashed with exit code $?. Respawning in 3s ..."
|
log "Web crashed with exit code $?. Respawning in 3s ..."
|
||||||
sleep 3
|
sleep 3
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ source /immich-devcontainer/container-common.sh
|
|||||||
log "Setting up Immich dev container..."
|
log "Setting up Immich dev container..."
|
||||||
fix_permissions
|
fix_permissions
|
||||||
|
|
||||||
|
log "Installing npm dependencies (node_modules)..."
|
||||||
|
install_dependencies
|
||||||
|
|
||||||
log "Setup complete, please wait while backend and frontend services automatically start"
|
log "Setup complete, please wait while backend and frontend services automatically start"
|
||||||
log
|
log
|
||||||
log "If necessary, the services may be manually started using"
|
log "If necessary, the services may be manually started using"
|
||||||
|
|||||||
@@ -64,11 +64,6 @@ body:
|
|||||||
- label: Web
|
- label: Web
|
||||||
- label: Mobile
|
- label: Mobile
|
||||||
|
|
||||||
- type: input
|
|
||||||
attributes:
|
|
||||||
label: Device make and model
|
|
||||||
placeholder: Samsung S25 Android 16
|
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
Generated
+28
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": ".github",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"devDependencies": {
|
||||||
|
"prettier": "^3.5.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin/prettier.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
should_run: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
should_run: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
runs-on: mich
|
runs-on: mich
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.ref || github.sha }}
|
ref: ${{ inputs.ref || github.sha }}
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -79,7 +79,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Restore Gradle Cache
|
- name: Restore Gradle Cache
|
||||||
id: cache-gradle-restore
|
id: cache-gradle-restore
|
||||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4
|
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.gradle/caches
|
~/.gradle/caches
|
||||||
@@ -106,7 +106,7 @@ jobs:
|
|||||||
run: flutter pub get
|
run: flutter pub get
|
||||||
|
|
||||||
- name: Generate translation file
|
- name: Generate translation file
|
||||||
run: dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart
|
run: make translation
|
||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
|
|
||||||
- name: Generate platform APIs
|
- name: Generate platform APIs
|
||||||
@@ -136,7 +136,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Save Gradle Cache
|
- name: Save Gradle Cache
|
||||||
id: cache-gradle-save
|
id: cache-gradle-save
|
||||||
uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4
|
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||||
if: github.ref == 'refs/heads/main'
|
if: github.ref == 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
actions: write
|
actions: write
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|||||||
+15
-18
@@ -29,28 +29,25 @@ jobs:
|
|||||||
working-directory: ./cli
|
working-directory: ./cli
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup pnpm
|
# Setup .npmrc file to publish to npm
|
||||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.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'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|
||||||
- name: Setup typescript-sdk
|
- name: Prepare SDK
|
||||||
run: pnpm install && pnpm run build
|
run: npm ci --prefix ../open-api/typescript-sdk/
|
||||||
working-directory: ./open-api/typescript-sdk
|
- name: Build SDK
|
||||||
|
run: npm run build --prefix ../open-api/typescript-sdk/
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: npm ci
|
||||||
- run: pnpm build
|
- run: npm run build
|
||||||
- run: pnpm publish
|
- run: npm publish
|
||||||
if: ${{ github.event_name == 'release' }}
|
if: ${{ github.event_name == 'release' }}
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
@@ -65,7 +62,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -76,7 +73,7 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.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
|
||||||
@@ -91,7 +88,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate docker image tags
|
- name: Generate docker image tags
|
||||||
id: metadata
|
id: metadata
|
||||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||||
with:
|
with:
|
||||||
flavor: |
|
flavor: |
|
||||||
latest=false
|
latest=false
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: get_body
|
needs: get_body
|
||||||
container:
|
container:
|
||||||
image: yshavit/mdq:0.8.0@sha256:c69224d34224a0043d9a3ee46679ba4a2a25afaac445f293d92afe13cd47fcea
|
image: yshavit/mdq:0.7.2
|
||||||
outputs:
|
outputs:
|
||||||
json: ${{ steps.get_checkbox.outputs.json }}
|
json: ${{ steps.get_checkbox.outputs.json }}
|
||||||
steps:
|
steps:
|
||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
gh api graphql \
|
gh api graphql \
|
||||||
-f issueId="$NODE_ID" \
|
-f issueId="$NODE_ID" \
|
||||||
-f body="This issue has automatically been closed as it is likely a duplicate. We get a lot of duplicate threads each day, which is why we ask you in the template to confirm that you searched for duplicates before opening one. If you're sure this is not a duplicate, please leave a comment and we will reopen the thread if necessary." \
|
-f body="This issue has automatically been closed as it is likely a duplicate. We get a lot of duplicate threads each day, which is why we ask you in the template to confirm that you searched for duplicates before opening one." \
|
||||||
-f query='
|
-f query='
|
||||||
mutation CommentAndCloseIssue($issueId: ID!, $body: String!) {
|
mutation CommentAndCloseIssue($issueId: ID!, $body: String!) {
|
||||||
addComment(input: {
|
addComment(input: {
|
||||||
@@ -77,7 +77,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
gh api graphql \
|
gh api graphql \
|
||||||
-f discussionId="$NODE_ID" \
|
-f discussionId="$NODE_ID" \
|
||||||
-f body="This discussion has automatically been closed as it is likely a duplicate. We get a lot of duplicate threads each day, which is why we ask you in the template to confirm that you searched for duplicates before opening one. If you're sure this is not a duplicate, please leave a comment and we will reopen the thread if necessary." \
|
-f body="This discussion has automatically been closed as it is likely a duplicate. We get a lot of duplicate threads each day, which is why we ask you in the template to confirm that you searched for duplicates before opening one." \
|
||||||
-f query='
|
-f query='
|
||||||
mutation CommentAndCloseDiscussion($discussionId: ID!, $body: String!) {
|
mutation CommentAndCloseDiscussion($discussionId: ID!, $body: String!) {
|
||||||
addDiscussionComment(input: {
|
addDiscussionComment(input: {
|
||||||
|
|||||||
@@ -44,13 +44,13 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@df559355d593797519d70b90fc8edd5db049e7a2 # v3.29.9
|
uses: github/codeql-action/init@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@df559355d593797519d70b90fc8edd5db049e7a2 # v3.29.9
|
uses: github/codeql-action/autobuild@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
@@ -76,6 +76,6 @@ jobs:
|
|||||||
# ./location_of_script_within_repo/buildscript.sh
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@df559355d593797519d70b90fc8edd5db049e7a2 # v3.29.9
|
uses: github/codeql-action/analyze@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
|
||||||
with:
|
with:
|
||||||
category: '/language:${{matrix.language}}'
|
category: '/language:${{matrix.language}}'
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
should_run_ml: ${{ steps.found_paths.outputs.machine-learning == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
should_run_ml: ${{ steps.found_paths.outputs.machine-learning == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- id: found_paths
|
- id: found_paths
|
||||||
@@ -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@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
suffix: ['']
|
suffix: ['']
|
||||||
steps:
|
steps:
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
should_run: ${{ steps.found_paths.outputs.docs == 'true' || steps.found_paths.outputs.open-api == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
should_run: ${{ steps.found_paths.outputs.docs == 'true' || steps.found_paths.outputs.open-api == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- id: found_paths
|
- id: found_paths
|
||||||
@@ -51,28 +51,25 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './docs/.nvmrc'
|
node-version-file: './docs/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|
||||||
- name: Run install
|
- name: Run npm install
|
||||||
run: pnpm install
|
run: npm ci
|
||||||
|
|
||||||
- name: Check formatting
|
- name: Check formatting
|
||||||
run: pnpm format
|
run: npm run format
|
||||||
|
|
||||||
- name: Run build
|
- name: Run build
|
||||||
run: pnpm build
|
run: npm run build
|
||||||
|
|
||||||
- name: Upload build output
|
- name: Upload build output
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ 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:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Generate a token
|
- name: Generate a token
|
||||||
id: generate-token
|
id: generate-token
|
||||||
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1
|
uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
|
||||||
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@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
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,8 +32,8 @@ jobs:
|
|||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './server/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|
||||||
- name: Fix formatting
|
- name: Fix formatting
|
||||||
run: make install-all && make format-all
|
run: make install-all && make format-all
|
||||||
|
|||||||
@@ -32,13 +32,13 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Generate a token
|
- name: Generate a token
|
||||||
id: generate-token
|
id: generate-token
|
||||||
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1
|
uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
|
||||||
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@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
persist-credentials: true
|
persist-credentials: true
|
||||||
@@ -83,13 +83,13 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Generate a token
|
- name: Generate a token
|
||||||
id: generate-token
|
id: generate-token
|
||||||
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1
|
uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
|
||||||
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@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|||||||
@@ -16,25 +16,22 @@ jobs:
|
|||||||
run:
|
run:
|
||||||
working-directory: ./open-api/typescript-sdk
|
working-directory: ./open-api/typescript-sdk
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
|
||||||
|
|
||||||
# Setup .npmrc file to publish to npm
|
# Setup .npmrc file to publish to npm
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.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'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
- name: Install deps
|
- name: Install deps
|
||||||
run: pnpm install --frozen-lockfile
|
run: npm ci
|
||||||
- name: Build
|
- name: Build
|
||||||
run: pnpm build
|
run: npm run build
|
||||||
- name: Publish
|
- name: Publish
|
||||||
run: pnpm publish
|
run: npm publish
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
should_run: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
should_run: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- id: found_paths
|
- id: found_paths
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ jobs:
|
|||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
|
|
||||||
- name: Generate translation file
|
- name: Generate translation file
|
||||||
run: dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart
|
run: make translation
|
||||||
|
|
||||||
- name: Run Build Runner
|
- name: Run Build Runner
|
||||||
run: make build
|
run: make build
|
||||||
@@ -116,7 +116,7 @@ jobs:
|
|||||||
actions: read
|
actions: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ jobs:
|
|||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Upload SARIF file
|
- name: Upload SARIF file
|
||||||
uses: github/codeql-action/upload-sarif@df559355d593797519d70b90fc8edd5db049e7a2 # v3.29.9
|
uses: github/codeql-action/upload-sarif@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
category: zizmor
|
category: zizmor
|
||||||
|
|||||||
+290
-203
@@ -4,10 +4,13 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pre-job:
|
pre-job:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -26,9 +29,10 @@ jobs:
|
|||||||
should_run_.github: ${{ steps.found_paths.outputs['.github'] == 'true' || steps.should_force.outputs.should_force == 'true' }} # redundant to have should_force but if someone changes the trigger then this won't have to be changed
|
should_run_.github: ${{ steps.found_paths.outputs['.github'] == 'true' || steps.should_force.outputs.should_force == 'true' }} # redundant to have should_force but if someone changes the trigger then this won't have to be changed
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- id: found_paths
|
- id: found_paths
|
||||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||||
with:
|
with:
|
||||||
@@ -54,9 +58,11 @@ jobs:
|
|||||||
- '.github/workflows/test.yml'
|
- '.github/workflows/test.yml'
|
||||||
.github:
|
.github:
|
||||||
- '.github/**'
|
- '.github/**'
|
||||||
|
|
||||||
- name: Check if we should force jobs to run
|
- name: Check if we should force jobs to run
|
||||||
id: should_force
|
id: should_force
|
||||||
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT"
|
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
server-unit-tests:
|
server-unit-tests:
|
||||||
name: Test & Lint Server
|
name: Test & Lint Server
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
@@ -67,33 +73,39 @@ jobs:
|
|||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./server
|
working-directory: ./server
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './server/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
- name: Run package manager install
|
|
||||||
run: pnpm install
|
- name: Run npm install
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: pnpm lint
|
run: npm run lint
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run formatter
|
- name: Run formatter
|
||||||
run: pnpm format
|
run: npm run format
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run tsc
|
- name: Run tsc
|
||||||
run: pnpm check
|
run: npm run check
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run small tests & coverage
|
- name: Run small tests & coverage
|
||||||
run: pnpm test
|
run: npm test
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
cli-unit-tests:
|
cli-unit-tests:
|
||||||
name: Unit Test CLI
|
name: Unit Test CLI
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
@@ -104,36 +116,43 @@ jobs:
|
|||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./cli
|
working-directory: ./cli
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './cli/.nvmrc'
|
node-version-file: './cli/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|
||||||
- name: Setup typescript-sdk
|
- name: Setup typescript-sdk
|
||||||
run: pnpm install && pnpm run build
|
run: npm ci && npm run build
|
||||||
working-directory: ./open-api/typescript-sdk
|
working-directory: ./open-api/typescript-sdk
|
||||||
|
|
||||||
- name: Install deps
|
- name: Install deps
|
||||||
run: pnpm install
|
run: npm ci
|
||||||
|
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: pnpm lint
|
run: npm run lint
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run formatter
|
- name: Run formatter
|
||||||
run: pnpm format
|
run: npm run format
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run tsc
|
- name: Run tsc
|
||||||
run: pnpm check
|
run: npm run check
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run unit tests & coverage
|
- name: Run unit tests & coverage
|
||||||
run: pnpm test
|
run: npm run test
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
cli-unit-tests-win:
|
cli-unit-tests-win:
|
||||||
name: Unit Test CLI (Windows)
|
name: Unit Test CLI (Windows)
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
@@ -144,31 +163,36 @@ jobs:
|
|||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./cli
|
working-directory: ./cli
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './cli/.nvmrc'
|
node-version-file: './cli/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|
||||||
- name: Setup typescript-sdk
|
- name: Setup typescript-sdk
|
||||||
run: pnpm install --frozen-lockfile && pnpm build
|
run: npm ci && npm run build
|
||||||
working-directory: ./open-api/typescript-sdk
|
working-directory: ./open-api/typescript-sdk
|
||||||
|
|
||||||
- name: Install deps
|
- name: Install deps
|
||||||
run: pnpm install --frozen-lockfile
|
run: npm ci
|
||||||
|
|
||||||
# Skip linter & formatter in Windows test.
|
# Skip linter & formatter in Windows test.
|
||||||
- name: Run tsc
|
- name: Run tsc
|
||||||
run: pnpm check
|
run: npm run check
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run unit tests & coverage
|
- name: Run unit tests & coverage
|
||||||
run: pnpm test
|
run: npm run test
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
web-lint:
|
web-lint:
|
||||||
name: Lint Web
|
name: Lint Web
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
@@ -179,33 +203,39 @@ jobs:
|
|||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./web
|
working-directory: ./web
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './web/.nvmrc'
|
node-version-file: './web/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|
||||||
- name: Run setup typescript-sdk
|
- name: Run setup typescript-sdk
|
||||||
run: pnpm install --frozen-lockfile && pnpm build
|
run: npm ci && npm run build
|
||||||
working-directory: ./open-api/typescript-sdk
|
working-directory: ./open-api/typescript-sdk
|
||||||
- name: Run pnpm install
|
|
||||||
run: pnpm rebuild && pnpm install --frozen-lockfile
|
- name: Run npm install
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: pnpm lint:p
|
run: npm run lint:p
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run formatter
|
- name: Run formatter
|
||||||
run: pnpm format
|
run: npm run format
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run svelte checks
|
- name: Run svelte checks
|
||||||
run: pnpm check:svelte
|
run: npm run check:svelte
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
web-unit-tests:
|
web-unit-tests:
|
||||||
name: Test Web
|
name: Test Web
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
@@ -216,30 +246,35 @@ jobs:
|
|||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./web
|
working-directory: ./web
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './web/.nvmrc'
|
node-version-file: './web/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|
||||||
- name: Run setup typescript-sdk
|
- name: Run setup typescript-sdk
|
||||||
run: pnpm install --frozen-lockfile && pnpm build
|
run: npm ci && npm run build
|
||||||
working-directory: ./open-api/typescript-sdk
|
working-directory: ./open-api/typescript-sdk
|
||||||
|
|
||||||
- name: Run npm install
|
- name: Run npm install
|
||||||
run: pnpm install --frozen-lockfile
|
run: npm ci
|
||||||
|
|
||||||
- name: Run tsc
|
- name: Run tsc
|
||||||
run: pnpm check:typescript
|
run: npm run check:typescript
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run unit tests & coverage
|
- name: Run unit tests & coverage
|
||||||
run: pnpm test
|
run: npm run test
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
i18n-tests:
|
i18n-tests:
|
||||||
name: Test i18n
|
name: Test i18n
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
@@ -249,27 +284,30 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './web/.nvmrc'
|
node-version-file: './web/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm --filter=immich-web install --frozen-lockfile
|
run: npm --prefix=web ci
|
||||||
|
|
||||||
- name: Format
|
- name: Format
|
||||||
run: pnpm --filter=immich-web format:i18n
|
run: npm --prefix=web run format:i18n
|
||||||
|
|
||||||
- name: Find file changes
|
- name: Find file changes
|
||||||
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
||||||
id: verify-changed-files
|
id: verify-changed-files
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
i18n/**
|
i18n/**
|
||||||
|
|
||||||
- name: Verify files have not changed
|
- name: Verify files have not changed
|
||||||
if: steps.verify-changed-files.outputs.files_changed == 'true'
|
if: steps.verify-changed-files.outputs.files_changed == 'true'
|
||||||
env:
|
env:
|
||||||
@@ -278,6 +316,7 @@ jobs:
|
|||||||
echo "ERROR: i18n files not up to date!"
|
echo "ERROR: i18n files not up to date!"
|
||||||
echo "Changed files: ${CHANGED_FILES}"
|
echo "Changed files: ${CHANGED_FILES}"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
e2e-tests-lint:
|
e2e-tests-lint:
|
||||||
name: End-to-End Lint
|
name: End-to-End Lint
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
@@ -288,35 +327,41 @@ jobs:
|
|||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./e2e
|
working-directory: ./e2e
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './e2e/.nvmrc'
|
node-version-file: './e2e/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|
||||||
- name: Run setup typescript-sdk
|
- name: Run setup typescript-sdk
|
||||||
run: pnpm install --frozen-lockfile && pnpm build
|
run: npm ci && npm run build
|
||||||
working-directory: ./open-api/typescript-sdk
|
working-directory: ./open-api/typescript-sdk
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: npm ci
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: pnpm lint
|
run: npm run lint
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run formatter
|
- name: Run formatter
|
||||||
run: pnpm format
|
run: npm run format
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run tsc
|
- name: Run tsc
|
||||||
run: pnpm check
|
run: npm run check
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
server-medium-tests:
|
server-medium-tests:
|
||||||
name: Medium Tests (Server)
|
name: Medium Tests (Server)
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
@@ -327,24 +372,27 @@ jobs:
|
|||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./server
|
working-directory: ./server
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './server/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
- name: Run pnpm install
|
|
||||||
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile
|
- name: Run npm install
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
- name: Run medium tests
|
- name: Run medium tests
|
||||||
run: pnpm test:medium
|
run: npm run test:medium
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
e2e-tests-server-cli:
|
e2e-tests-server-cli:
|
||||||
name: End-to-End Tests (Server & CLI)
|
name: End-to-End Tests (Server & CLI)
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
@@ -358,41 +406,43 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './e2e/.nvmrc'
|
node-version-file: './e2e/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|
||||||
- name: Run setup typescript-sdk
|
- name: Run setup typescript-sdk
|
||||||
run: pnpm install --frozen-lockfile && pnpm build
|
run: npm ci && npm run build
|
||||||
working-directory: ./open-api/typescript-sdk
|
working-directory: ./open-api/typescript-sdk
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Run setup web
|
|
||||||
run: pnpm install --frozen-lockfile && pnpm exec svelte-kit sync
|
|
||||||
working-directory: ./web
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
- name: Run setup cli
|
- name: Run setup cli
|
||||||
run: pnpm install --frozen-lockfile && pnpm build
|
run: npm ci && npm run build
|
||||||
working-directory: ./cli
|
working-directory: ./cli
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: npm ci
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Docker build
|
- name: Docker build
|
||||||
run: docker compose build
|
run: docker compose build
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run e2e tests (api & cli)
|
- name: Run e2e tests (api & cli)
|
||||||
run: pnpm test
|
run: npm run test
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
e2e-tests-web:
|
e2e-tests-web:
|
||||||
name: End-to-End Tests (Web)
|
name: End-to-End Tests (Web)
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
@@ -406,36 +456,42 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './e2e/.nvmrc'
|
node-version-file: './e2e/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|
||||||
- name: Run setup typescript-sdk
|
- name: Run setup typescript-sdk
|
||||||
run: pnpm install --frozen-lockfile && pnpm build
|
run: npm ci && npm run build
|
||||||
working-directory: ./open-api/typescript-sdk
|
working-directory: ./open-api/typescript-sdk
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: npm ci
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
run: npx 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 build
|
run: docker compose build
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run e2e tests (web)
|
- name: Run e2e tests (web)
|
||||||
run: npx playwright test
|
run: npx playwright test
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
success-check-e2e:
|
success-check-e2e:
|
||||||
name: End-to-End Tests Success
|
name: End-to-End Tests Success
|
||||||
needs: [e2e-tests-server-cli, e2e-tests-web]
|
needs: [e2e-tests-server-cli, e2e-tests-web]
|
||||||
@@ -446,6 +502,7 @@ jobs:
|
|||||||
- uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4
|
- uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4
|
||||||
with:
|
with:
|
||||||
needs: ${{ toJSON(needs) }}
|
needs: ${{ toJSON(needs) }}
|
||||||
|
|
||||||
mobile-unit-tests:
|
mobile-unit-tests:
|
||||||
name: Unit Test Mobile
|
name: Unit Test Mobile
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
@@ -454,20 +511,24 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Flutter SDK
|
- name: Setup Flutter SDK
|
||||||
uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0
|
uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0
|
||||||
with:
|
with:
|
||||||
channel: 'stable'
|
channel: 'stable'
|
||||||
flutter-version-file: ./mobile/pubspec.yaml
|
flutter-version-file: ./mobile/pubspec.yaml
|
||||||
|
|
||||||
- name: Generate translation file
|
- name: Generate translation file
|
||||||
run: dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart
|
run: make translation
|
||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
run: flutter test -j 1
|
run: flutter test -j 1
|
||||||
|
|
||||||
ml-unit-tests:
|
ml-unit-tests:
|
||||||
name: Unit Test ML
|
name: Unit Test ML
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
@@ -479,9 +540,10 @@ jobs:
|
|||||||
run:
|
run:
|
||||||
working-directory: ./machine-learning
|
working-directory: ./machine-learning
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
||||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||||
@@ -504,6 +566,7 @@ jobs:
|
|||||||
- name: Run tests and coverage
|
- name: Run tests and coverage
|
||||||
run: |
|
run: |
|
||||||
uv run pytest --cov=immich_ml --cov-report term-missing
|
uv run pytest --cov=immich_ml --cov-report term-missing
|
||||||
|
|
||||||
github-files-formatting:
|
github-files-formatting:
|
||||||
name: .github Files Formatting
|
name: .github Files Formatting
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
@@ -514,38 +577,45 @@ jobs:
|
|||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./.github
|
working-directory: ./.github
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './.github/.nvmrc'
|
node-version-file: './.github/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
- name: Run pnpm install
|
|
||||||
run: pnpm install --frozen-lockfile
|
- name: Run npm install
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
- name: Run formatter
|
- name: Run formatter
|
||||||
run: pnpm format
|
run: npm run format
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
shellcheck:
|
shellcheck:
|
||||||
name: ShellCheck
|
name: ShellCheck
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Run ShellCheck
|
- name: Run ShellCheck
|
||||||
uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # 2.0.0
|
uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # 2.0.0
|
||||||
with:
|
with:
|
||||||
ignore_paths: >-
|
ignore_paths: >-
|
||||||
**/open-api/** **/openapi** **/node_modules/**
|
**/open-api/**
|
||||||
|
**/openapi**
|
||||||
|
**/node_modules/**
|
||||||
|
|
||||||
generated-api-up-to-date:
|
generated-api-up-to-date:
|
||||||
name: OpenAPI Clients
|
name: OpenAPI Clients
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -553,23 +623,26 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './server/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|
||||||
- name: Install server dependencies
|
- name: Install server dependencies
|
||||||
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter immich install --frozen-lockfile
|
run: npm --prefix=server ci
|
||||||
|
|
||||||
- name: Build the app
|
- name: Build the app
|
||||||
run: pnpm --filter immich build
|
run: npm --prefix=server run build
|
||||||
|
|
||||||
- name: Run API generation
|
- name: Run API generation
|
||||||
run: make open-api
|
run: make open-api
|
||||||
|
|
||||||
- name: Find file changes
|
- name: Find file changes
|
||||||
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
||||||
id: verify-changed-files
|
id: verify-changed-files
|
||||||
@@ -578,6 +651,7 @@ jobs:
|
|||||||
mobile/openapi
|
mobile/openapi
|
||||||
open-api/typescript-sdk
|
open-api/typescript-sdk
|
||||||
open-api/immich-openapi-specs.json
|
open-api/immich-openapi-specs.json
|
||||||
|
|
||||||
- name: Verify files have not changed
|
- name: Verify files have not changed
|
||||||
if: steps.verify-changed-files.outputs.files_changed == 'true'
|
if: steps.verify-changed-files.outputs.files_changed == 'true'
|
||||||
env:
|
env:
|
||||||
@@ -586,6 +660,7 @@ jobs:
|
|||||||
echo "ERROR: Generated files not up to date!"
|
echo "ERROR: Generated files not up to date!"
|
||||||
echo "Changed files: ${CHANGED_FILES}"
|
echo "Changed files: ${CHANGED_FILES}"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
sql-schema-up-to-date:
|
sql-schema-up-to-date:
|
||||||
name: SQL Schema Checks
|
name: SQL Schema Checks
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -599,36 +674,45 @@ jobs:
|
|||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
POSTGRES_DB: immich
|
POSTGRES_DB: immich
|
||||||
options: >-
|
options: >-
|
||||||
--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./server
|
working-directory: ./server
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './server/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|
||||||
- name: Install server dependencies
|
- name: Install server dependencies
|
||||||
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile
|
run: npm ci
|
||||||
|
|
||||||
- name: Build the app
|
- name: Build the app
|
||||||
run: pnpm build
|
run: npm run build
|
||||||
|
|
||||||
- name: Run existing migrations
|
- name: Run existing migrations
|
||||||
run: pnpm migrations:run
|
run: npm run migrations:run
|
||||||
|
|
||||||
- name: Test npm run schema:reset command works
|
- name: Test npm run schema:reset command works
|
||||||
run: pnpm schema:reset
|
run: npm run schema:reset
|
||||||
|
|
||||||
- name: Generate new migrations
|
- name: Generate new migrations
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: pnpm migrations:generate src/TestMigration
|
run: npm run migrations:generate src/TestMigration
|
||||||
|
|
||||||
- name: Find file changes
|
- name: Find file changes
|
||||||
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
||||||
id: verify-changed-files
|
id: verify-changed-files
|
||||||
@@ -644,16 +728,19 @@ jobs:
|
|||||||
echo "Changed files: ${CHANGED_FILES}"
|
echo "Changed files: ${CHANGED_FILES}"
|
||||||
cat ./src/*-TestMigration.ts
|
cat ./src/*-TestMigration.ts
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Run SQL generation
|
- name: Run SQL generation
|
||||||
run: pnpm sync:sql
|
run: npm run sync:sql
|
||||||
env:
|
env:
|
||||||
DB_URL: postgres://postgres:postgres@localhost:5432/immich
|
DB_URL: postgres://postgres:postgres@localhost:5432/immich
|
||||||
|
|
||||||
- name: Find file changes
|
- name: Find file changes
|
||||||
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
||||||
id: verify-changed-sql-files
|
id: verify-changed-sql-files
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
server/src/queries
|
server/src/queries
|
||||||
|
|
||||||
- name: Verify SQL files have not changed
|
- name: Verify SQL files have not changed
|
||||||
if: steps.verify-changed-sql-files.outputs.files_changed == 'true'
|
if: steps.verify-changed-sql-files.outputs.files_changed == 'true'
|
||||||
env:
|
env:
|
||||||
@@ -664,77 +751,77 @@ jobs:
|
|||||||
git diff
|
git diff
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
# mobile-integration-tests:
|
# mobile-integration-tests:
|
||||||
# name: Run mobile end-to-end integration tests
|
# name: Run mobile end-to-end integration tests
|
||||||
# runs-on: macos-latest
|
# runs-on: macos-latest
|
||||||
# steps:
|
# steps:
|
||||||
# - uses: actions/checkout@v4
|
# - uses: actions/checkout@v4
|
||||||
# - uses: actions/setup-java@v3
|
# - uses: actions/setup-java@v3
|
||||||
# with:
|
# with:
|
||||||
# distribution: 'zulu'
|
# distribution: 'zulu'
|
||||||
# java-version: '12.x'
|
# java-version: '12.x'
|
||||||
# cache: 'gradle'
|
# cache: 'gradle'
|
||||||
# - name: Cache android SDK
|
# - name: Cache android SDK
|
||||||
# uses: actions/cache@v3
|
# uses: actions/cache@v3
|
||||||
# id: android-sdk
|
# id: android-sdk
|
||||||
# with:
|
# with:
|
||||||
# key: android-sdk
|
# key: android-sdk
|
||||||
# path: |
|
# path: |
|
||||||
# /usr/local/lib/android/
|
# /usr/local/lib/android/
|
||||||
# ~/.android
|
# ~/.android
|
||||||
# - name: Cache Gradle
|
# - name: Cache Gradle
|
||||||
# uses: actions/cache@v3
|
# uses: actions/cache@v3
|
||||||
# with:
|
# with:
|
||||||
# path: |
|
# path: |
|
||||||
# ./mobile/build/
|
# ./mobile/build/
|
||||||
# ./mobile/android/.gradle/
|
# ./mobile/android/.gradle/
|
||||||
# key: ${{ runner.os }}-flutter-${{ hashFiles('**/*.gradle*', 'pubspec.lock') }}
|
# key: ${{ runner.os }}-flutter-${{ hashFiles('**/*.gradle*', 'pubspec.lock') }}
|
||||||
# - name: Setup Android SDK
|
# - name: Setup Android SDK
|
||||||
# if: steps.android-sdk.outputs.cache-hit != 'true'
|
# if: steps.android-sdk.outputs.cache-hit != 'true'
|
||||||
# uses: android-actions/setup-android@v2
|
# uses: android-actions/setup-android@v2
|
||||||
# - name: AVD cache
|
# - name: AVD cache
|
||||||
# uses: actions/cache@v3
|
# uses: actions/cache@v3
|
||||||
# id: avd-cache
|
# id: avd-cache
|
||||||
# with:
|
# with:
|
||||||
# path: |
|
# path: |
|
||||||
# ~/.android/avd/*
|
# ~/.android/avd/*
|
||||||
# ~/.android/adb*
|
# ~/.android/adb*
|
||||||
# key: avd-29
|
# key: avd-29
|
||||||
# - name: create AVD and generate snapshot for caching
|
# - name: create AVD and generate snapshot for caching
|
||||||
# if: steps.avd-cache.outputs.cache-hit != 'true'
|
# if: steps.avd-cache.outputs.cache-hit != 'true'
|
||||||
# uses: reactivecircus/android-emulator-runner@v2.27.0
|
# uses: reactivecircus/android-emulator-runner@v2.27.0
|
||||||
# with:
|
# with:
|
||||||
# working-directory: ./mobile
|
# working-directory: ./mobile
|
||||||
# cores: 2
|
# cores: 2
|
||||||
# api-level: 29
|
# api-level: 29
|
||||||
# arch: x86_64
|
# arch: x86_64
|
||||||
# profile: pixel
|
# profile: pixel
|
||||||
# target: default
|
# target: default
|
||||||
# force-avd-creation: false
|
# force-avd-creation: false
|
||||||
# emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
# emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
||||||
# disable-animations: false
|
# disable-animations: false
|
||||||
# script: echo "Generated AVD snapshot for caching."
|
# script: echo "Generated AVD snapshot for caching."
|
||||||
# - name: Setup Flutter SDK
|
# - name: Setup Flutter SDK
|
||||||
# uses: subosito/flutter-action@v2
|
# uses: subosito/flutter-action@v2
|
||||||
# with:
|
# with:
|
||||||
# channel: 'stable'
|
# channel: 'stable'
|
||||||
# flutter-version: '3.7.3'
|
# flutter-version: '3.7.3'
|
||||||
# cache: true
|
# cache: true
|
||||||
# - name: Run integration tests
|
# - name: Run integration tests
|
||||||
# uses: Wandalen/wretry.action@master
|
# uses: Wandalen/wretry.action@master
|
||||||
# with:
|
# with:
|
||||||
# action: reactivecircus/android-emulator-runner@v2.27.0
|
# action: reactivecircus/android-emulator-runner@v2.27.0
|
||||||
# with: |
|
# with: |
|
||||||
# working-directory: ./mobile
|
# working-directory: ./mobile
|
||||||
# cores: 2
|
# cores: 2
|
||||||
# api-level: 29
|
# api-level: 29
|
||||||
# arch: x86_64
|
# arch: x86_64
|
||||||
# profile: pixel
|
# profile: pixel
|
||||||
# target: default
|
# target: default
|
||||||
# force-avd-creation: false
|
# force-avd-creation: false
|
||||||
# emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
# emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
||||||
# disable-animations: true
|
# disable-animations: true
|
||||||
# script: |
|
# script: |
|
||||||
# flutter pub get
|
# flutter pub get
|
||||||
# flutter test integration_test
|
# flutter test integration_test
|
||||||
# attempt_limit: 3
|
# attempt_limit: 3
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
should_run: ${{ steps.found_paths.outputs.i18n == 'true' && github.head_ref != 'chore/translations'}}
|
should_run: ${{ steps.found_paths.outputs.i18n == 'true' && github.head_ref != 'chore/translations'}}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- id: found_paths
|
- id: found_paths
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
hooks: {
|
|
||||||
readPackage: (pkg) => {
|
|
||||||
if (!pkg.name) {
|
|
||||||
return pkg;
|
|
||||||
}
|
|
||||||
switch (pkg.name) {
|
|
||||||
case "exiftool-vendored":
|
|
||||||
if (pkg.optionalDependencies["exiftool-vendored.pl"]) {
|
|
||||||
// make exiftool-vendored.pl a regular dependency
|
|
||||||
pkg.dependencies["exiftool-vendored.pl"] =
|
|
||||||
pkg.optionalDependencies["exiftool-vendored.pl"];
|
|
||||||
delete pkg.optionalDependencies["exiftool-vendored.pl"];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "sharp":
|
|
||||||
const optionalDeps = Object.keys(pkg.optionalDependencies).filter(
|
|
||||||
(dep) => dep.startsWith("@img")
|
|
||||||
);
|
|
||||||
for (const dep of optionalDeps) {
|
|
||||||
// remove all optionalDependencies from sharp (they will be compiled from source), except:
|
|
||||||
// include the precompiled musl version of sharp, for web
|
|
||||||
// include precompiled linux-x64 version of sharp, for server (stage: web-prod)
|
|
||||||
// include precompiled linux-arm64 version of sharp, for server (stage: web-prod)
|
|
||||||
if (
|
|
||||||
dep.includes("musl") ||
|
|
||||||
dep.includes("linux-x64") ||
|
|
||||||
dep.includes("linux-arm64")
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
delete pkg.optionalDependencies[dep];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return pkg;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
Vendored
+1
-2
@@ -56,8 +56,7 @@
|
|||||||
"explorer.fileNesting.enabled": true,
|
"explorer.fileNesting.enabled": true,
|
||||||
"explorer.fileNesting.patterns": {
|
"explorer.fileNesting.patterns": {
|
||||||
"*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart",
|
"*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart",
|
||||||
"*.ts": "${capture}.spec.ts,${capture}.mock.ts",
|
"*.ts": "${capture}.spec.ts,${capture}.mock.ts"
|
||||||
"package.json": "package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lockb, bun.lock, pnpm-workspace.yaml, .pnpmfile.cjs"
|
|
||||||
},
|
},
|
||||||
"svelte.enable-ts-plugin": true,
|
"svelte.enable-ts-plugin": true,
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative"
|
"typescript.preferences.importModuleSpecifier": "non-relative"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ dev-update:
|
|||||||
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
|
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
dev-scale:
|
dev-scale:
|
||||||
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
|
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
|
||||||
|
|
||||||
dev-docs:
|
dev-docs:
|
||||||
npm --prefix docs run start
|
npm --prefix docs run start
|
||||||
@@ -43,7 +43,7 @@ open-api-typescript:
|
|||||||
cd ./open-api && bash ./bin/generate-open-api.sh typescript
|
cd ./open-api && bash ./bin/generate-open-api.sh typescript
|
||||||
|
|
||||||
sql:
|
sql:
|
||||||
pnpm --filter immich run sync:sql
|
npm --prefix server run sync:sql
|
||||||
|
|
||||||
attach-server:
|
attach-server:
|
||||||
docker exec -it docker_immich-server_1 sh
|
docker exec -it docker_immich-server_1 sh
|
||||||
@@ -53,40 +53,31 @@ renovate:
|
|||||||
|
|
||||||
MODULES = e2e server web cli sdk docs .github
|
MODULES = e2e server web cli sdk docs .github
|
||||||
|
|
||||||
# directory to package name mapping function
|
|
||||||
# cli = @immich/cli
|
|
||||||
# docs = documentation
|
|
||||||
# e2e = immich-e2e
|
|
||||||
# open-api/typescript-sdk = @immich/sdk
|
|
||||||
# server = immich
|
|
||||||
# web = immich-web
|
|
||||||
map-package = $(subst sdk,@immich/sdk,$(subst cli,@immich/cli,$(subst docs,documentation,$(subst e2e,immich-e2e,$(subst server,immich,$(subst web,immich-web,$1))))))
|
|
||||||
|
|
||||||
audit-%:
|
audit-%:
|
||||||
pnpm --filter $(call map-package,$*) audit fix
|
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) audit fix
|
||||||
install-%:
|
install-%:
|
||||||
pnpm --filter $(call map-package,$*) install $(if $(FROZEN),--frozen-lockfile) $(if $(OFFLINE),--offline)
|
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) i
|
||||||
|
ci-%:
|
||||||
|
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) ci
|
||||||
build-cli: build-sdk
|
build-cli: build-sdk
|
||||||
build-web: build-sdk
|
build-web: build-sdk
|
||||||
build-%: install-%
|
build-%: install-%
|
||||||
pnpm --filter $(call map-package,$*) run build
|
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) run build
|
||||||
format-%:
|
format-%:
|
||||||
pnpm --filter $(call map-package,$*) run format:fix
|
npm --prefix $* run format:fix
|
||||||
lint-%:
|
lint-%:
|
||||||
pnpm --filter $(call map-package,$*) run lint:fix
|
npm --prefix $* run lint:fix
|
||||||
lint-web:
|
|
||||||
pnpm --filter $(call map-package,$*) run lint:p
|
|
||||||
check-%:
|
check-%:
|
||||||
pnpm --filter $(call map-package,$*) run check
|
npm --prefix $* run check
|
||||||
check-web:
|
check-web:
|
||||||
pnpm --filter immich-web run check:typescript
|
npm --prefix web run check:typescript
|
||||||
pnpm --filter immich-web run check:svelte
|
npm --prefix web run check:svelte
|
||||||
test-%:
|
test-%:
|
||||||
pnpm --filter $(call map-package,$*) run test
|
npm --prefix $* run test
|
||||||
test-e2e:
|
test-e2e:
|
||||||
docker compose -f ./e2e/docker-compose.yml build
|
docker compose -f ./e2e/docker-compose.yml build
|
||||||
pnpm --filter immich-e2e run test
|
npm --prefix e2e run test
|
||||||
pnpm --filter immich-e2e run test:web
|
npm --prefix e2e run test:web
|
||||||
test-medium:
|
test-medium:
|
||||||
docker run \
|
docker run \
|
||||||
--rm \
|
--rm \
|
||||||
@@ -96,36 +87,25 @@ test-medium:
|
|||||||
-v ./server/tsconfig.json:/usr/src/app/tsconfig.json \
|
-v ./server/tsconfig.json:/usr/src/app/tsconfig.json \
|
||||||
-e NODE_ENV=development \
|
-e NODE_ENV=development \
|
||||||
immich-server:latest \
|
immich-server:latest \
|
||||||
-c "pnpm test:medium -- --run"
|
-c "npm ci && npm run test:medium -- --run"
|
||||||
test-medium-dev:
|
test-medium-dev:
|
||||||
docker exec -it immich_server /bin/sh -c "pnpm run test:medium"
|
docker exec -it immich_server /bin/sh -c "npm run test:medium"
|
||||||
|
|
||||||
install-all:
|
build-all: $(foreach M,$(filter-out e2e .github,$(MODULES)),build-$M) ;
|
||||||
pnpm -r --filter '!documentation' install
|
install-all: $(foreach M,$(MODULES),install-$M) ;
|
||||||
|
ci-all: $(foreach M,$(filter-out .github,$(MODULES)),ci-$M) ;
|
||||||
build-all: $(foreach M,$(filter-out e2e docs .github,$(MODULES)),build-$M) ;
|
check-all: $(foreach M,$(filter-out sdk cli docs .github,$(MODULES)),check-$M) ;
|
||||||
|
lint-all: $(foreach M,$(filter-out sdk docs .github,$(MODULES)),lint-$M) ;
|
||||||
check-all:
|
format-all: $(foreach M,$(filter-out sdk,$(MODULES)),format-$M) ;
|
||||||
pnpm -r --filter '!documentation' run "/^(check|check\:svelte|check\:typescript)$/"
|
audit-all: $(foreach M,$(MODULES),audit-$M) ;
|
||||||
lint-all:
|
hygiene-all: lint-all format-all check-all sql audit-all;
|
||||||
pnpm -r --filter '!documentation' run lint:fix
|
test-all: $(foreach M,$(filter-out sdk docs .github,$(MODULES)),test-$M) ;
|
||||||
format-all:
|
|
||||||
pnpm -r --filter '!documentation' run format:fix
|
|
||||||
audit-all:
|
|
||||||
pnpm -r --filter '!documentation' audit fix
|
|
||||||
hygiene-all: audit-all
|
|
||||||
pnpm -r --filter '!documentation' run "/(format:fix|check|check:svelte|check:typescript|sql)/"
|
|
||||||
|
|
||||||
test-all:
|
|
||||||
pnpm -r --filter '!documentation' run "/^test/"
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
find . -name "node_modules" -type d -prune -exec rm -rf {} +
|
find . -name "node_modules" -type d -prune -exec rm -rf {} +
|
||||||
find . -name "dist" -type d -prune -exec rm -rf '{}' +
|
find . -name "dist" -type d -prune -exec rm -rf '{}' +
|
||||||
find . -name "build" -type d -prune -exec rm -rf '{}' +
|
find . -name "build" -type d -prune -exec rm -rf '{}' +
|
||||||
find . -name ".svelte-kit" -type d -prune -exec rm -rf '{}' +
|
find . -name "svelte-kit" -type d -prune -exec rm -rf '{}' +
|
||||||
find . -name "coverage" -type d -prune -exec rm -rf '{}' +
|
|
||||||
find . -name ".pnpm-store" -type d -prune -exec rm -rf '{}' +
|
|
||||||
command -v docker >/dev/null 2>&1 && docker compose -f ./docker/docker-compose.dev.yml rm -v -f || true
|
command -v docker >/dev/null 2>&1 && docker compose -f ./docker/docker-compose.dev.yml rm -v -f || true
|
||||||
command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml rm -v -f || true
|
command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml rm -v -f || true
|
||||||
|
|
||||||
|
|||||||
+13
-8
@@ -1,14 +1,19 @@
|
|||||||
FROM node:22.16.0-alpine3.20@sha256:2289fb1fba0f4633b08ec47b94a89c7e20b829fc5679f9b7b298eaa2f1ed8b7e AS core
|
FROM node:22.16.0-alpine3.20@sha256:2289fb1fba0f4633b08ec47b94a89c7e20b829fc5679f9b7b298eaa2f1ed8b7e AS core
|
||||||
|
|
||||||
|
WORKDIR /usr/src/open-api/typescript-sdk
|
||||||
|
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY open-api/typescript-sdk/ ./
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
COPY package* pnpm* .pnpmfile.cjs ./
|
|
||||||
COPY ./cli ./cli/
|
COPY cli/package.json cli/package-lock.json ./
|
||||||
COPY ./open-api/typescript-sdk ./open-api/typescript-sdk/
|
RUN npm ci
|
||||||
RUN corepack enable pnpm && \
|
|
||||||
pnpm install --filter @immich/sdk --filter @immich/cli --frozen-lockfile && \
|
COPY cli .
|
||||||
pnpm --filter @immich/sdk build && \
|
RUN npm run build
|
||||||
pnpm --filter @immich/cli build
|
|
||||||
|
|
||||||
WORKDIR /import
|
WORKDIR /import
|
||||||
|
|
||||||
ENTRYPOINT ["node", "/usr/src/app/cli/dist"]
|
ENTRYPOINT ["node", "/usr/src/app/dist"]
|
||||||
|
|||||||
Generated
+416
-426
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.80",
|
"version": "2.2.77",
|
||||||
"description": "Command Line Interface (CLI) for Immich",
|
"description": "Command Line Interface (CLI) for Immich",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": "./dist/index.js",
|
"exports": "./dist/index.js",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/micromatch": "^4.0.9",
|
"@types/micromatch": "^4.0.9",
|
||||||
"@types/mock-fs": "^4.13.1",
|
"@types/mock-fs": "^4.13.1",
|
||||||
"@types/node": "^22.17.1",
|
"@types/node": "^22.17.0",
|
||||||
"@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",
|
||||||
|
|||||||
@@ -21,16 +21,17 @@ services:
|
|||||||
# extends:
|
# extends:
|
||||||
# file: hwaccel.transcoding.yml
|
# file: hwaccel.transcoding.yml
|
||||||
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
|
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
|
||||||
user: '${UID:-1000}:${GID:-1000}'
|
|
||||||
build:
|
build:
|
||||||
context: ../
|
context: ../
|
||||||
dockerfile: server/Dockerfile
|
dockerfile: server/Dockerfile
|
||||||
target: dev
|
target: dev
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- ..:/usr/src/app
|
- ../server:/usr/src/app/server
|
||||||
|
- ../open-api:/usr/src/app/open-api
|
||||||
- ${UPLOAD_LOCATION}/photos:/data
|
- ${UPLOAD_LOCATION}/photos:/data
|
||||||
- ${UPLOAD_LOCATION}/photos/upload:/data/upload
|
- ${UPLOAD_LOCATION}/photos/upload:/data/upload
|
||||||
|
- /usr/src/app/server/node_modules
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
@@ -57,12 +58,8 @@ services:
|
|||||||
- 9231:9231
|
- 9231:9231
|
||||||
- 2283:2283
|
- 2283:2283
|
||||||
depends_on:
|
depends_on:
|
||||||
redis:
|
- redis
|
||||||
condition: service_started
|
- database
|
||||||
database:
|
|
||||||
condition: service_started
|
|
||||||
init:
|
|
||||||
condition: service_completed_successfully
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
disable: false
|
disable: false
|
||||||
|
|
||||||
@@ -71,11 +68,9 @@ services:
|
|||||||
image: immich-web-dev:latest
|
image: immich-web-dev:latest
|
||||||
# Needed for rootless docker setup, see https://github.com/moby/moby/issues/45919
|
# Needed for rootless docker setup, see https://github.com/moby/moby/issues/45919
|
||||||
# user: 0:0
|
# user: 0:0
|
||||||
user: '${UID:-1000}:${GID:-1000}'
|
|
||||||
build:
|
build:
|
||||||
context: ../
|
context: ../
|
||||||
dockerfile: server/Dockerfile
|
dockerfile: web/Dockerfile
|
||||||
target: dev
|
|
||||||
command: ['immich-web']
|
command: ['immich-web']
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
@@ -83,17 +78,18 @@ services:
|
|||||||
- 3000:3000
|
- 3000:3000
|
||||||
- 24678:24678
|
- 24678:24678
|
||||||
volumes:
|
volumes:
|
||||||
- ..:/usr/src/app
|
- ../web:/usr/src/app/web
|
||||||
|
- ../i18n:/usr/src/app/i18n
|
||||||
|
- ../open-api/:/usr/src/app/open-api/
|
||||||
|
# - ../../ui:/usr/ui
|
||||||
|
- /usr/src/app/web/node_modules
|
||||||
ulimits:
|
ulimits:
|
||||||
nofile:
|
nofile:
|
||||||
soft: 1048576
|
soft: 1048576
|
||||||
hard: 1048576
|
hard: 1048576
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
immich-server:
|
- immich-server
|
||||||
condition: service_started
|
|
||||||
init:
|
|
||||||
condition: service_completed_successfully
|
|
||||||
|
|
||||||
immich-machine-learning:
|
immich-machine-learning:
|
||||||
container_name: immich_machine_learning
|
container_name: immich_machine_learning
|
||||||
@@ -121,7 +117,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/valkey/valkey:8-bookworm@sha256:a137a2b60aca1a75130022d6bb96af423fefae4eb55faf395732db3544803280
|
image: docker.io/valkey/valkey:8-bookworm@sha256:facc1d2c3462975c34e10fccb167bfa92b0e0dbd992fc282c29a61c3243afb11
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
|
|
||||||
@@ -161,14 +157,6 @@ services:
|
|||||||
# volumes:
|
# volumes:
|
||||||
# - grafana-data:/var/lib/grafana
|
# - grafana-data:/var/lib/grafana
|
||||||
|
|
||||||
init:
|
|
||||||
container_name: init
|
|
||||||
image: busybox
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
user: 0:0
|
|
||||||
command: sh -c 'for path in /usr/src/app/.pnpm-store /usr/src/app/server/node_modules /usr/src/app/.github/node_modules /usr/src/app/cli/node_modules /usr/src/app/docs/node_modules /usr/src/app/e2e/node_modules /usr/src/app/open-api/typescript-sdk/node_modules /usr/src/app/web/.svelte-kit /usr/src/app/web/coverage /usr/src/app/node_modules /usr/src/app/web/node_modules; do [ -e "$$path" ] && chown -R ${UID:-1000}:${GID:-1000} "$$path" || true; done'
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
model-cache:
|
model-cache:
|
||||||
prometheus-data:
|
prometheus-data:
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/valkey/valkey:8-bookworm@sha256:a137a2b60aca1a75130022d6bb96af423fefae4eb55faf395732db3544803280
|
image: docker.io/valkey/valkey:8-bookworm@sha256:facc1d2c3462975c34e10fccb167bfa92b0e0dbd992fc282c29a61c3243afb11
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
restart: always
|
restart: always
|
||||||
@@ -95,7 +95,7 @@ services:
|
|||||||
command: ['./run.sh', '-disable-reporting']
|
command: ['./run.sh', '-disable-reporting']
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
image: grafana/grafana:12.1.1-ubuntu@sha256:d1da838234ff2de93e0065ee1bf0e66d38f948dcc5d718c25fa6237e14b4424a
|
image: grafana/grafana:12.1.0-ubuntu@sha256:397aa30dd1af16cb6c5c9879498e467973a7f87eacf949f6d5a29407a3843809
|
||||||
volumes:
|
volumes:
|
||||||
- grafana-data:/var/lib/grafana
|
- grafana-data:/var/lib/grafana
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/valkey/valkey:8-bookworm@sha256:a137a2b60aca1a75130022d6bb96af423fefae4eb55faf395732db3544803280
|
image: docker.io/valkey/valkey:8-bookworm@sha256:facc1d2c3462975c34e10fccb167bfa92b0e0dbd992fc282c29a61c3243afb11
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
restart: always
|
restart: always
|
||||||
|
|||||||
+5
-5
@@ -5,13 +5,13 @@ This website is built using [Docusaurus](https://docusaurus.io/), a modern stati
|
|||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
```
|
```
|
||||||
$ pnpm install
|
$ npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
### Local Development
|
### Local Development
|
||||||
|
|
||||||
```
|
```
|
||||||
$ pnpm run start
|
$ npm run start
|
||||||
```
|
```
|
||||||
|
|
||||||
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
||||||
@@ -19,7 +19,7 @@ This command starts a local development server and opens up a browser window. Mo
|
|||||||
### Build
|
### Build
|
||||||
|
|
||||||
```
|
```
|
||||||
$ pnpm run build
|
$ npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
This command generates static content into the `build` directory and can be served using any static contents hosting service.
|
This command generates static content into the `build` directory and can be served using any static contents hosting service.
|
||||||
@@ -29,13 +29,13 @@ This command generates static content into the `build` directory and can be serv
|
|||||||
Using SSH:
|
Using SSH:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ USE_SSH=true pnpm run deploy
|
$ USE_SSH=true npm run deploy
|
||||||
```
|
```
|
||||||
|
|
||||||
Not using SSH:
|
Not using SSH:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ GIT_USER=<Your GitHub username> pnpm run deploy
|
$ GIT_USER=<Your GitHub username> npm run deploy
|
||||||
```
|
```
|
||||||
|
|
||||||
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
|
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ After making any changes in the `server/src/schema`, a database migration need t
|
|||||||
1. Run the command
|
1. Run the command
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm run migrations:generate <migration-name>
|
npm run migrations:generate <migration-name>
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Check if the migration file makes sense.
|
2. Check if the migration file makes sense.
|
||||||
|
|||||||
@@ -204,8 +204,8 @@ When the Dev Container starts, it automatically:
|
|||||||
|
|
||||||
1. **Runs post-create script** (`container-server-post-create.sh`):
|
1. **Runs post-create script** (`container-server-post-create.sh`):
|
||||||
- Adjusts file permissions for the `node` user
|
- Adjusts file permissions for the `node` user
|
||||||
- Installs dependencies: `pnpm install` in all packages
|
- Installs dependencies: `npm install` in all packages
|
||||||
- Builds TypeScript SDK: `pnpm run build` in `open-api/typescript-sdk`
|
- Builds TypeScript SDK: `npm run build` in `open-api/typescript-sdk`
|
||||||
|
|
||||||
2. **Starts development servers** via VS Code tasks:
|
2. **Starts development servers** via VS Code tasks:
|
||||||
- `Immich API Server (Nest)` - API server with hot-reloading on port 2283
|
- `Immich API Server (Nest)` - API server with hot-reloading on port 2283
|
||||||
@@ -243,7 +243,7 @@ To connect the mobile app to your Dev Container:
|
|||||||
|
|
||||||
- **Server code** (`/server`): Changes trigger automatic restart
|
- **Server code** (`/server`): Changes trigger automatic restart
|
||||||
- **Web code** (`/web`): Changes trigger hot module replacement
|
- **Web code** (`/web`): Changes trigger hot module replacement
|
||||||
- **Database migrations**: Run `pnpm run sync:sql` in the server directory
|
- **Database migrations**: Run `npm run sync:sql` in the server directory
|
||||||
- **API changes**: Regenerate TypeScript SDK with `make open-api`
|
- **API changes**: Regenerate TypeScript SDK with `make open-api`
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
@@ -273,19 +273,19 @@ make test-medium-dev # End-to-end tests
|
|||||||
```bash
|
```bash
|
||||||
# Server tests
|
# Server tests
|
||||||
cd /workspaces/immich/server
|
cd /workspaces/immich/server
|
||||||
pnpm test # Run all tests
|
npm test # Run all tests
|
||||||
pnpm run test:watch # Watch mode
|
npm run test:watch # Watch mode
|
||||||
pnpm run test:cov # Coverage report
|
npm run test:cov # Coverage report
|
||||||
|
|
||||||
# Web tests
|
# Web tests
|
||||||
cd /workspaces/immich/web
|
cd /workspaces/immich/web
|
||||||
pnpm test # Run all tests
|
npm test # Run all tests
|
||||||
pnpm run test:watch # Watch mode
|
npm run test:watch # Watch mode
|
||||||
|
|
||||||
# E2E tests
|
# E2E tests
|
||||||
cd /workspaces/immich/e2e
|
cd /workspaces/immich/e2e
|
||||||
pnpm run test # Run API tests
|
npm run test # Run API tests
|
||||||
pnpm run test:web # Run web UI tests
|
npm run test:web # Run web UI tests
|
||||||
```
|
```
|
||||||
|
|
||||||
### Code Quality Commands
|
### Code Quality Commands
|
||||||
|
|||||||
@@ -8,34 +8,34 @@ When contributing code through a pull request, please check the following:
|
|||||||
|
|
||||||
## Web Checks
|
## Web Checks
|
||||||
|
|
||||||
- [ ] `pnpm run lint` (linting via ESLint)
|
- [ ] `npm run lint` (linting via ESLint)
|
||||||
- [ ] `pnpm run format` (formatting via Prettier)
|
- [ ] `npm run format` (formatting via Prettier)
|
||||||
- [ ] `pnpm run check:svelte` (Type checking via SvelteKit)
|
- [ ] `npm run check:svelte` (Type checking via SvelteKit)
|
||||||
- [ ] `pnpm run check:typescript` (check typescript)
|
- [ ] `npm run check:typescript` (check typescript)
|
||||||
- [ ] `pnpm test` (unit tests)
|
- [ ] `npm test` (unit tests)
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- [ ] `pnpm run format` (formatting via Prettier)
|
- [ ] `npm run format` (formatting via Prettier)
|
||||||
- [ ] Update the `_redirects` file if you have renamed a page or removed it from the documentation.
|
- [ ] Update the `_redirects` file if you have renamed a page or removed it from the documentation.
|
||||||
|
|
||||||
:::tip AIO
|
:::tip AIO
|
||||||
Run all web checks with `pnpm run check:all`
|
Run all web checks with `npm run check:all`
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## Server Checks
|
## Server Checks
|
||||||
|
|
||||||
- [ ] `pnpm run lint` (linting via ESLint)
|
- [ ] `npm run lint` (linting via ESLint)
|
||||||
- [ ] `pnpm run format` (formatting via Prettier)
|
- [ ] `npm run format` (formatting via Prettier)
|
||||||
- [ ] `pnpm run check` (Type checking via `tsc`)
|
- [ ] `npm run check` (Type checking via `tsc`)
|
||||||
- [ ] `pnpm test` (unit tests)
|
- [ ] `npm test` (unit tests)
|
||||||
|
|
||||||
:::tip AIO
|
:::tip AIO
|
||||||
Run all server checks with `pnpm run check:all`
|
Run all server checks with `npm run check:all`
|
||||||
:::
|
:::
|
||||||
|
|
||||||
:::info Auto Fix
|
:::info Auto Fix
|
||||||
You can use `pnpm run __:fix` to potentially correct some issues automatically for `pnpm run format` and `lint`.
|
You can use `npm run __:fix` to potentially correct some issues automatically for `npm run format` and `lint`.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## Mobile Checks
|
## Mobile Checks
|
||||||
|
|||||||
@@ -54,20 +54,20 @@ You can access the web from `http://your-machine-ip:3000` or `http://localhost:3
|
|||||||
|
|
||||||
If you only want to do web development connected to an existing, remote backend, follow these steps:
|
If you only want to do web development connected to an existing, remote backend, follow these steps:
|
||||||
|
|
||||||
1. Build the Immich SDK - `cd open-api/typescript-sdk && pnpm i && pnpm run build && cd -`
|
1. Build the Immich SDK - `cd open-api/typescript-sdk && npm i && npm run build && cd -`
|
||||||
2. Enter the web directory - `cd web/`
|
2. Enter the web directory - `cd web/`
|
||||||
3. Install web dependencies - `pnpm i`
|
3. Install web dependencies - `npm i`
|
||||||
4. Start the web development server
|
4. Start the web development server
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
IMMICH_SERVER_URL=https://demo.immich.app/ pnpm run dev
|
IMMICH_SERVER_URL=https://demo.immich.app/ npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
If you're using PowerShell on Windows you may need to set the env var separately like so:
|
If you're using PowerShell on Windows you may need to set the env var separately like so:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$env:IMMICH_SERVER_URL = "https://demo.immich.app/"
|
$env:IMMICH_SERVER_URL = "https://demo.immich.app/"
|
||||||
pnpm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `@immich/ui`
|
#### `@immich/ui`
|
||||||
@@ -75,12 +75,12 @@ pnpm run dev
|
|||||||
To see local changes to `@immich/ui` in Immich, do the following:
|
To see local changes to `@immich/ui` in Immich, do the following:
|
||||||
|
|
||||||
1. Install `@immich/ui` as a sibling to `immich/`, for example `/home/user/immich` and `/home/user/ui`
|
1. Install `@immich/ui` as a sibling to `immich/`, for example `/home/user/immich` and `/home/user/ui`
|
||||||
2. Build the `@immich/ui` project via `pnpm run build`
|
2. Build the `@immich/ui` project via `npm run build`
|
||||||
3. Uncomment the corresponding volume in web service of the `docker/docker-compose.dev.yaml` file (`../../ui:/usr/ui`)
|
3. Uncomment the corresponding volume in web service of the `docker/docker-compose.dev.yaml` file (`../../ui:/usr/ui`)
|
||||||
4. Uncomment the corresponding alias in the `web/vite.config.js` file (`'@immich/ui': path.resolve(\_\_dirname, '../../ui')`)
|
4. Uncomment the corresponding alias in the `web/vite.config.js` file (`'@immich/ui': path.resolve(\_\_dirname, '../../ui')`)
|
||||||
5. Uncomment the import statement in `web/src/app.css` file `@import '/usr/ui/dist/theme/default.css';` and comment out `@import '@immich/ui/theme/default.css';`
|
5. Uncomment the import statement in `web/src/app.css` file `@import '/usr/ui/dist/theme/default.css';` and comment out `@import '@immich/ui/theme/default.css';`
|
||||||
6. Start up the stack via `make dev`
|
6. Start up the stack via `make dev`
|
||||||
7. After making changes in `@immich/ui`, rebuild it (`pnpm run build`)
|
7. After making changes in `@immich/ui`, rebuild it (`npm run build`)
|
||||||
|
|
||||||
### Mobile app
|
### Mobile app
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
### Unit tests
|
### Unit tests
|
||||||
|
|
||||||
Unit are run by calling `pnpm run test` from the `server/` directory.
|
Unit are run by calling `npm run test` from the `server/` directory.
|
||||||
You need to run `pnpm install` (in `server/`) before _once_.
|
You need to run `npm install` (in `server/`) before _once_.
|
||||||
|
|
||||||
### End to end tests
|
### End to end tests
|
||||||
|
|
||||||
@@ -17,14 +17,14 @@ make e2e
|
|||||||
|
|
||||||
Before you can run the tests, you need to run the following commands _once_:
|
Before you can run the tests, you need to run the following commands _once_:
|
||||||
|
|
||||||
- `pnpm install` (in `e2e/`)
|
- `npm install` (in `e2e/`)
|
||||||
- `make open-api` (in the project root `/`)
|
- `make open-api` (in the project root `/`)
|
||||||
|
|
||||||
Once the test environment is running, the e2e tests can be run via:
|
Once the test environment is running, the e2e tests can be run via:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd e2e/
|
cd e2e/
|
||||||
pnpm test
|
npm test
|
||||||
```
|
```
|
||||||
|
|
||||||
The tests check various things including:
|
The tests check various things including:
|
||||||
|
|||||||
@@ -27,102 +27,3 @@ docker image prune
|
|||||||
[watchtower]: https://containrrr.dev/watchtower/
|
[watchtower]: https://containrrr.dev/watchtower/
|
||||||
[breaking]: https://github.com/immich-app/immich/discussions?discussions_q=label%3Achangelog%3Abreaking-change+sort%3Adate_created
|
[breaking]: https://github.com/immich-app/immich/discussions?discussions_q=label%3Achangelog%3Abreaking-change+sort%3Adate_created
|
||||||
[releases]: https://github.com/immich-app/immich/releases
|
[releases]: https://github.com/immich-app/immich/releases
|
||||||
|
|
||||||
## Migrating to VectorChord
|
|
||||||
|
|
||||||
:::info
|
|
||||||
If you deploy Immich using Docker Compose, see `ghcr.io/immich-app/postgres` in the `docker-compose.yml` file and have not explicitly set the `DB_VECTOR_EXTENSION` environmental variable, your Immich database is already using VectorChord and this section does not apply to you.
|
|
||||||
:::
|
|
||||||
|
|
||||||
:::important
|
|
||||||
If you do not deploy Immich using Docker Compose and see a deprecation warning for pgvecto.rs on server startup, you should refer to the maintainers of the Immich distribution for guidance (if using a turnkey solution) or adapt the instructions for your specific setup.
|
|
||||||
:::
|
|
||||||
|
|
||||||
Immich has migrated off of the deprecated pgvecto.rs database extension to its successor, [VectorChord](https://github.com/tensorchord/VectorChord), which comes with performance improvements in almost every aspect. This section will guide you on how to make this change in a Docker Compose setup.
|
|
||||||
|
|
||||||
Before making any changes, please [back up your database](/docs/administration/backup-and-restore). While every effort has been made to make this migration as smooth as possible, there’s always a chance that something can go wrong.
|
|
||||||
|
|
||||||
After making a backup, please modify your `docker-compose.yml` file with the following information.
|
|
||||||
|
|
||||||
```diff
|
|
||||||
[...]
|
|
||||||
|
|
||||||
database:
|
|
||||||
container_name: immich_postgres
|
|
||||||
- image: docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52
|
|
||||||
+ image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
|
|
||||||
environment:
|
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
|
||||||
POSTGRES_USER: ${DB_USERNAME}
|
|
||||||
POSTGRES_DB: ${DB_DATABASE_NAME}
|
|
||||||
POSTGRES_INITDB_ARGS: '--data-checksums'
|
|
||||||
+ # Uncomment the DB_STORAGE_TYPE: 'HDD' var if your database isn't stored on SSDs
|
|
||||||
+ # DB_STORAGE_TYPE: 'HDD'
|
|
||||||
volumes:
|
|
||||||
# Do not edit the next line. If you want to change the database storage location on your system, edit the value of DB_DATA_LOCATION in the .env file
|
|
||||||
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
|
|
||||||
- healthcheck:
|
|
||||||
- test: >-
|
|
||||||
- pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1;
|
|
||||||
- Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align
|
|
||||||
- --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')";
|
|
||||||
- echo "checksum failure count is $$Chksum";
|
|
||||||
- [ "$$Chksum" = '0' ] || exit 1
|
|
||||||
- interval: 5m
|
|
||||||
- start_interval: 30s
|
|
||||||
- start_period: 5m
|
|
||||||
- command: >-
|
|
||||||
- postgres
|
|
||||||
- -c shared_preload_libraries=vectors.so
|
|
||||||
- -c 'search_path="$$user", public, vectors'
|
|
||||||
- -c logging_collector=on
|
|
||||||
- -c max_wal_size=2GB
|
|
||||||
- -c shared_buffers=512MB
|
|
||||||
- -c wal_compression=on
|
|
||||||
+ shm_size: 128mb
|
|
||||||
restart: always
|
|
||||||
|
|
||||||
[...]
|
|
||||||
```
|
|
||||||
|
|
||||||
:::important
|
|
||||||
If you deviated from the defaults of pg14 or pgvectors0.2.0, you must adjust the pg major version and pgvecto.rs version. If you are still using the default `docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0` image, you can just follow the changes above. For example, if the previous image is `docker.io/tensorchord/pgvecto-rs:pg16-v0.3.0`, the new image should be `ghcr.io/immich-app/postgres:16-vectorchord0.3.0-pgvectors0.3.0` instead of the image specified in the diff.
|
|
||||||
:::
|
|
||||||
|
|
||||||
After making these changes, you can start Immich as normal. Immich will make some changes to the DB during startup, which can take seconds to minutes to finish, depending on hardware and library size. In particular, it’s normal for the server logs to be seemingly stuck at `Reindexing clip_index` and `Reindexing face_index`for some time if you have over 100k assets in Immich and/or Immich is on a relatively weak server. If you see these logs and there are no errors, just give it time.
|
|
||||||
|
|
||||||
:::danger
|
|
||||||
After switching to VectorChord, you should not downgrade Immich below 1.133.0.
|
|
||||||
:::
|
|
||||||
|
|
||||||
Please don’t hesitate to contact us on [GitHub](https://github.com/immich-app/immich/discussions) or [Discord](https://discord.immich.app/) if you encounter migration issues.
|
|
||||||
|
|
||||||
### VectorChord FAQ
|
|
||||||
|
|
||||||
#### I have a separate PostgreSQL instance shared with multiple services. How can I switch to VectorChord?
|
|
||||||
|
|
||||||
Please see the [standalone PostgreSQL documentation](/docs/administration/postgres-standalone#migrating-to-vectorchord) for migration instructions. The migration path will be different depending on whether you’re currently using pgvecto.rs or pgvector, as well as whether Immich has superuser DB permissions.
|
|
||||||
|
|
||||||
#### Why are so many lines removed from the `docker-compose.yml` file? Does this mean the health check is removed?
|
|
||||||
|
|
||||||
These lines are now incorporated into the image itself along with some additional tuning.
|
|
||||||
|
|
||||||
#### What does this change mean for my existing DB backups?
|
|
||||||
|
|
||||||
The new DB image includes pgvector and pgvecto.rs in addition to VectorChord, so you can use this image to restore from existing backups that used either of these extensions. However, backups made after switching to VectorChord require an image containing VectorChord to restore successfully.
|
|
||||||
|
|
||||||
#### Do I still need pgvecto.rs installed after migrating to VectorChord?
|
|
||||||
|
|
||||||
pgvecto.rs only needs to be available during the migration, or if you need to restore from a backup that used pgvecto.rs. For a leaner DB and a smaller image, you can optionally switch to an image variant that doesn’t have pgvecto.rs installed after you’ve performed the migration and started Immich: `ghcr.io/immich-app/postgres:14-vectorchord0.4.3`, changing the PostgreSQL version as appropriate.
|
|
||||||
|
|
||||||
#### Why does it matter whether my database is on an SSD or an HDD?
|
|
||||||
|
|
||||||
These storage mediums have different performance characteristics. As a result, the optimal settings for an SSD are not the same as those for an HDD. Either configuration is compatible with SSD and HDD, but using the right configuration will make Immich snappier. As a general tip, we recommend users store the database on an SSD whenever possible.
|
|
||||||
|
|
||||||
#### Can I use the new database image as a general PostgreSQL image outside of Immich?
|
|
||||||
|
|
||||||
It’s a standard PostgreSQL container image that additionally contains the VectorChord, pgvector, and (optionally) pgvecto.rs extensions. If you were using the previous pgvecto.rs image for other purposes, you can similarly do so with this image.
|
|
||||||
|
|
||||||
#### If pgvecto.rs and pgvector still work, why should I switch to VectorChord?
|
|
||||||
|
|
||||||
VectorChord is faster, more stable, uses less RAM, and (with the settings Immich uses) offers higher-quality results than pgvector and pgvecto.rs. This translates to better search and facial recognition experiences. In addition, pgvecto.rs support will be dropped in the future, so changing it sooner will avoid disruption.
|
|
||||||
|
|||||||
Generated
+20545
File diff suppressed because it is too large
Load Diff
@@ -16,9 +16,6 @@ import {
|
|||||||
mdiCloudKeyOutline,
|
mdiCloudKeyOutline,
|
||||||
mdiRegex,
|
mdiRegex,
|
||||||
mdiCodeJson,
|
mdiCodeJson,
|
||||||
mdiClockOutline,
|
|
||||||
mdiAccountOutline,
|
|
||||||
mdiRestart,
|
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import Layout from '@theme/Layout';
|
import Layout from '@theme/Layout';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@@ -29,42 +26,6 @@ const withLanguage = (date: Date) => (language: string) => date.toLocaleDateStri
|
|||||||
type Item = Omit<TimelineItem, 'done' | 'getDateLabel'> & { date: Date };
|
type Item = Omit<TimelineItem, 'done' | 'getDateLabel'> & { date: Date };
|
||||||
|
|
||||||
const items: Item[] = [
|
const items: Item[] = [
|
||||||
{
|
|
||||||
icon: mdiClockOutline,
|
|
||||||
iconColor: 'gray',
|
|
||||||
title: 'setTimeout is cursed',
|
|
||||||
description:
|
|
||||||
'The setTimeout method in JavaScript is cursed when used with small values because the implementation may or may not actually wait the specified time.',
|
|
||||||
link: {
|
|
||||||
url: 'https://github.com/immich-app/immich/pull/20655',
|
|
||||||
text: '#20655',
|
|
||||||
},
|
|
||||||
date: new Date(2025, 7, 4),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: mdiAccountOutline,
|
|
||||||
iconColor: '#DAB1DA',
|
|
||||||
title: 'PostgreSQL USER is cursed',
|
|
||||||
description:
|
|
||||||
'The USER keyword in PostgreSQL is cursed because you can select from it like a table, which leads to confusion if you have a table name user as well.',
|
|
||||||
link: {
|
|
||||||
url: 'https://github.com/immich-app/immich/pull/19891',
|
|
||||||
text: '#19891',
|
|
||||||
},
|
|
||||||
date: new Date(2025, 7, 4),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: mdiRestart,
|
|
||||||
iconColor: '#8395e3',
|
|
||||||
title: 'PostgreSQL RESET is cursed',
|
|
||||||
description:
|
|
||||||
'PostgreSQL RESET is cursed because it is impossible to RESET a PostgreSQL extension parameter if the extension has been uninstalled.',
|
|
||||||
link: {
|
|
||||||
url: 'https://github.com/immich-app/immich/pull/19363',
|
|
||||||
text: '#19363',
|
|
||||||
},
|
|
||||||
date: new Date(2025, 5, 20),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: mdiRegex,
|
icon: mdiRegex,
|
||||||
iconColor: 'purple',
|
iconColor: 'purple',
|
||||||
|
|||||||
Vendored
-12
@@ -1,16 +1,4 @@
|
|||||||
[
|
[
|
||||||
{
|
|
||||||
"label": "v1.139.0",
|
|
||||||
"url": "https://v1.139.0.archive.immich.app"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "v1.138.1",
|
|
||||||
"url": "https://v1.138.1.archive.immich.app"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "v1.138.0",
|
|
||||||
"url": "https://v1.138.0.archive.immich.app"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"label": "v1.137.3",
|
"label": "v1.137.3",
|
||||||
"url": "https://v1.137.3.archive.immich.app"
|
"url": "https://v1.137.3.archive.immich.app"
|
||||||
|
|||||||
@@ -3,4 +3,3 @@ node_modules/
|
|||||||
/playwright-report/
|
/playwright-report/
|
||||||
/blob-report/
|
/blob-report/
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
/dist
|
|
||||||
|
|||||||
Generated
+535
-535
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.139.0",
|
"version": "1.137.3",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
"@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": "^22.17.1",
|
"@types/node": "^22.17.0",
|
||||||
"@types/oidc-provider": "^9.0.0",
|
"@types/oidc-provider": "^9.0.0",
|
||||||
"@types/pg": "^8.15.1",
|
"@types/pg": "^8.15.1",
|
||||||
"@types/pngjs": "^6.0.4",
|
"@types/pngjs": "^6.0.4",
|
||||||
|
|||||||
@@ -683,7 +683,7 @@ describe('/albums', () => {
|
|||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
.send({ role: AlbumUserRole.Editor });
|
.send({ role: AlbumUserRole.Editor });
|
||||||
|
|
||||||
expect(status).toBe(204);
|
expect(status).toBe(200);
|
||||||
|
|
||||||
// Get album to verify the role change
|
// Get album to verify the role change
|
||||||
const { body } = await request(app)
|
const { body } = await request(app)
|
||||||
|
|||||||
@@ -555,7 +555,7 @@ describe('/asset', () => {
|
|||||||
expect(body).toMatchObject({ id: user1Assets[0].id, livePhotoVideoId: null });
|
expect(body).toMatchObject({ id: user1Assets[0].id, livePhotoVideoId: null });
|
||||||
});
|
});
|
||||||
|
|
||||||
it.skip('should update date time original when sidecar file contains DateTimeOriginal', async () => {
|
it('should update date time original when sidecar file contains DateTimeOriginal', async () => {
|
||||||
const sidecarData = `<?xpacket begin='?' id='W5M0MpCehiHzreSzNTczkc9d'?>
|
const sidecarData = `<?xpacket begin='?' id='W5M0MpCehiHzreSzNTczkc9d'?>
|
||||||
<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 12.40'>
|
<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 12.40'>
|
||||||
<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
|
<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ describe('/partners', () => {
|
|||||||
.delete(`/partners/${user3.userId}`)
|
.delete(`/partners/${user3.userId}`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(204);
|
expect(status).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw a bad request if partner not found', async () => {
|
it('should throw a bad request if partner not found', async () => {
|
||||||
|
|||||||
@@ -485,7 +485,7 @@ describe('/shared-links', () => {
|
|||||||
.delete(`/shared-links/${linkWithAlbum.id}`)
|
.delete(`/shared-links/${linkWithAlbum.id}`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(204);
|
expect(status).toBe(200);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ describe('/users', () => {
|
|||||||
const { status } = await request(app)
|
const { status } = await request(app)
|
||||||
.delete(`/users/me/license`)
|
.delete(`/users/me/license`)
|
||||||
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
||||||
expect(status).toBe(204);
|
expect(status).toBe(200);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -79,7 +79,7 @@ export const tempDir = tmpdir();
|
|||||||
export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}` });
|
export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}` });
|
||||||
export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
|
export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
|
||||||
export const immichCli = (args: string[]) =>
|
export const immichCli = (args: string[]) =>
|
||||||
executeCommand('pnpm', ['exec', 'immich', '-d', `/${tempDir}/immich/`, ...args], { cwd: '../cli' }).promise;
|
executeCommand('node', ['node_modules/.bin/immich', '-d', `/${tempDir}/immich/`, ...args]).promise;
|
||||||
export const immichAdmin = (args: string[]) =>
|
export const immichAdmin = (args: string[]) =>
|
||||||
executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]);
|
executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]);
|
||||||
export const specialCharStrings = ["'", '"', ',', '{', '}', '*'];
|
export const specialCharStrings = ["'", '"', ',', '{', '}', '*'];
|
||||||
|
|||||||
+3
-22
@@ -28,9 +28,6 @@
|
|||||||
"add_to_album": "Add to album",
|
"add_to_album": "Add to album",
|
||||||
"add_to_album_bottom_sheet_added": "Added to {album}",
|
"add_to_album_bottom_sheet_added": "Added to {album}",
|
||||||
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
|
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
|
||||||
"add_to_album_toggle": "Toggle selection for {album}",
|
|
||||||
"add_to_albums": "Add to albums",
|
|
||||||
"add_to_albums_count": "Add to albums ({count})",
|
|
||||||
"add_to_shared_album": "Add to shared album",
|
"add_to_shared_album": "Add to shared album",
|
||||||
"add_url": "Add URL",
|
"add_url": "Add URL",
|
||||||
"added_to_archive": "Added to archive",
|
"added_to_archive": "Added to archive",
|
||||||
@@ -358,9 +355,6 @@
|
|||||||
"trash_number_of_days_description": "Number of days to keep the assets in trash before permanently removing them",
|
"trash_number_of_days_description": "Number of days to keep the assets in trash before permanently removing them",
|
||||||
"trash_settings": "Trash Settings",
|
"trash_settings": "Trash Settings",
|
||||||
"trash_settings_description": "Manage trash settings",
|
"trash_settings_description": "Manage trash settings",
|
||||||
"unlink_all_oauth_accounts": "Unlink all OAuth accounts",
|
|
||||||
"unlink_all_oauth_accounts_description": "Remember to unlink all OAuth accounts before migrating to a new provider.",
|
|
||||||
"unlink_all_oauth_accounts_prompt": "Are you sure you want to unlink all OAuth accounts? This will reset the OAuth ID for each user and cannot be undone.",
|
|
||||||
"user_cleanup_job": "User cleanup",
|
"user_cleanup_job": "User cleanup",
|
||||||
"user_delete_delay": "<b>{user}</b>'s account and assets will be scheduled for permanent deletion in {delay, plural, one {# day} other {# days}}.",
|
"user_delete_delay": "<b>{user}</b>'s account and assets will be scheduled for permanent deletion in {delay, plural, one {# day} other {# days}}.",
|
||||||
"user_delete_delay_settings": "Delete delay",
|
"user_delete_delay_settings": "Delete delay",
|
||||||
@@ -500,9 +494,7 @@
|
|||||||
"assets": "Assets",
|
"assets": "Assets",
|
||||||
"assets_added_count": "Added {count, plural, one {# asset} other {# assets}}",
|
"assets_added_count": "Added {count, plural, one {# asset} other {# assets}}",
|
||||||
"assets_added_to_album_count": "Added {count, plural, one {# asset} other {# assets}} to the album",
|
"assets_added_to_album_count": "Added {count, plural, one {# asset} other {# assets}} to the album",
|
||||||
"assets_added_to_albums_count": "Added {assetTotal, plural, one {# asset} other {# assets}} to {albumTotal} albums",
|
|
||||||
"assets_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} cannot be added to the album",
|
"assets_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} cannot be added to the album",
|
||||||
"assets_cannot_be_added_to_albums": "{count, plural, one {Asset} other {Assets}} cannot be added to any of the albums",
|
|
||||||
"assets_count": "{count, plural, one {# asset} other {# assets}}",
|
"assets_count": "{count, plural, one {# asset} other {# assets}}",
|
||||||
"assets_deleted_permanently": "{count} asset(s) deleted permanently",
|
"assets_deleted_permanently": "{count} asset(s) deleted permanently",
|
||||||
"assets_deleted_permanently_from_server": "{count} asset(s) deleted permanently from the Immich server",
|
"assets_deleted_permanently_from_server": "{count} asset(s) deleted permanently from the Immich server",
|
||||||
@@ -519,7 +511,6 @@
|
|||||||
"assets_trashed_count": "Trashed {count, plural, one {# asset} other {# assets}}",
|
"assets_trashed_count": "Trashed {count, plural, one {# asset} other {# assets}}",
|
||||||
"assets_trashed_from_server": "{count} asset(s) trashed from the Immich server",
|
"assets_trashed_from_server": "{count} asset(s) trashed from the Immich server",
|
||||||
"assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} already part of the album",
|
"assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} already part of the album",
|
||||||
"assets_were_part_of_albums_count": "{count, plural, one {Asset was} other {Assets were}} already part of the albums",
|
|
||||||
"authorized_devices": "Authorized Devices",
|
"authorized_devices": "Authorized Devices",
|
||||||
"automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
|
"automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
|
||||||
"automatic_endpoint_switching_title": "Automatic URL switching",
|
"automatic_endpoint_switching_title": "Automatic URL switching",
|
||||||
@@ -921,7 +912,6 @@
|
|||||||
"failed_to_load_notifications": "Failed to load notifications",
|
"failed_to_load_notifications": "Failed to load notifications",
|
||||||
"failed_to_load_people": "Failed to load people",
|
"failed_to_load_people": "Failed to load people",
|
||||||
"failed_to_remove_product_key": "Failed to remove product key",
|
"failed_to_remove_product_key": "Failed to remove product key",
|
||||||
"failed_to_reset_pin_code": "Failed to reset PIN code",
|
|
||||||
"failed_to_stack_assets": "Failed to stack assets",
|
"failed_to_stack_assets": "Failed to stack assets",
|
||||||
"failed_to_unstack_assets": "Failed to un-stack assets",
|
"failed_to_unstack_assets": "Failed to un-stack assets",
|
||||||
"failed_to_update_notification_status": "Failed to update notification status",
|
"failed_to_update_notification_status": "Failed to update notification status",
|
||||||
@@ -930,7 +920,6 @@
|
|||||||
"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",
|
||||||
"something_went_wrong": "Something went wrong",
|
|
||||||
"unable_to_add_album_users": "Unable to add users to album",
|
"unable_to_add_album_users": "Unable to add users to album",
|
||||||
"unable_to_add_assets_to_shared_link": "Unable to add assets to shared link",
|
"unable_to_add_assets_to_shared_link": "Unable to add assets to shared link",
|
||||||
"unable_to_add_comment": "Unable to add comment",
|
"unable_to_add_comment": "Unable to add comment",
|
||||||
@@ -1062,13 +1051,11 @@
|
|||||||
"filter_people": "Filter people",
|
"filter_people": "Filter people",
|
||||||
"filter_places": "Filter places",
|
"filter_places": "Filter places",
|
||||||
"find_them_fast": "Find them fast by name with search",
|
"find_them_fast": "Find them fast by name with search",
|
||||||
"first": "First",
|
|
||||||
"fix_incorrect_match": "Fix incorrect match",
|
"fix_incorrect_match": "Fix incorrect match",
|
||||||
"folder": "Folder",
|
"folder": "Folder",
|
||||||
"folder_not_found": "Folder not found",
|
"folder_not_found": "Folder not found",
|
||||||
"folders": "Folders",
|
"folders": "Folders",
|
||||||
"folders_feature_description": "Browsing the folder view for the photos and videos on the file system",
|
"folders_feature_description": "Browsing the folder view for the photos and videos on the file system",
|
||||||
"forgot_pin_code_question": "Forgot your PIN?",
|
|
||||||
"forward": "Forward",
|
"forward": "Forward",
|
||||||
"gcast_enabled": "Google Cast",
|
"gcast_enabled": "Google Cast",
|
||||||
"gcast_enabled_description": "This feature loads external resources from Google in order to work.",
|
"gcast_enabled_description": "This feature loads external resources from Google in order to work.",
|
||||||
@@ -1184,7 +1171,6 @@
|
|||||||
"language_search_hint": "Search languages...",
|
"language_search_hint": "Search languages...",
|
||||||
"language_setting_description": "Select your preferred language",
|
"language_setting_description": "Select your preferred language",
|
||||||
"large_files": "Large Files",
|
"large_files": "Large Files",
|
||||||
"last": "Last",
|
|
||||||
"last_seen": "Last seen",
|
"last_seen": "Last seen",
|
||||||
"latest_version": "Latest Version",
|
"latest_version": "Latest Version",
|
||||||
"latitude": "Latitude",
|
"latitude": "Latitude",
|
||||||
@@ -1203,7 +1189,6 @@
|
|||||||
"library_page_sort_title": "Album title",
|
"library_page_sort_title": "Album title",
|
||||||
"licenses": "Licenses",
|
"licenses": "Licenses",
|
||||||
"light": "Light",
|
"light": "Light",
|
||||||
"like": "Like",
|
|
||||||
"like_deleted": "Like deleted",
|
"like_deleted": "Like deleted",
|
||||||
"link_motion_video": "Link motion video",
|
"link_motion_video": "Link motion video",
|
||||||
"link_to_oauth": "Link to OAuth",
|
"link_to_oauth": "Link to OAuth",
|
||||||
@@ -1466,9 +1451,9 @@
|
|||||||
"permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.",
|
"permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.",
|
||||||
"permission_onboarding_request": "Immich requires permission to view your photos and videos.",
|
"permission_onboarding_request": "Immich requires permission to view your photos and videos.",
|
||||||
"person": "Person",
|
"person": "Person",
|
||||||
"person_age_months": "{months, plural, one {# month} other {# months}} old",
|
"person_age_months": "{months} months old",
|
||||||
"person_age_year_months": "1 year, {months, plural, one {# month} other {# months}} old",
|
"person_age_year_months": "1 year, {months} months old",
|
||||||
"person_age_years": "{years, plural, other {# years}} old",
|
"person_age_years": "{years} years old",
|
||||||
"person_birthdate": "Born on {date}",
|
"person_birthdate": "Born on {date}",
|
||||||
"person_hidden": "{name}{hidden, select, true { (hidden)} other {}}",
|
"person_hidden": "{name}{hidden, select, true { (hidden)} other {}}",
|
||||||
"photo_shared_all_users": "Looks like you shared your photos with all users or you don't have any user to share with.",
|
"photo_shared_all_users": "Looks like you shared your photos with all users or you don't have any user to share with.",
|
||||||
@@ -1614,9 +1599,6 @@
|
|||||||
"reset_password": "Reset password",
|
"reset_password": "Reset password",
|
||||||
"reset_people_visibility": "Reset people visibility",
|
"reset_people_visibility": "Reset people visibility",
|
||||||
"reset_pin_code": "Reset PIN code",
|
"reset_pin_code": "Reset PIN code",
|
||||||
"reset_pin_code_description": "If you forgot your PIN code, you can contact the server administrator to reset it",
|
|
||||||
"reset_pin_code_success": "Successfully reset PIN code",
|
|
||||||
"reset_pin_code_with_password": "You can always reset your PIN code with your password",
|
|
||||||
"reset_sqlite": "Reset SQLite Database",
|
"reset_sqlite": "Reset SQLite Database",
|
||||||
"reset_sqlite_confirmation": "Are you sure you want to reset the SQLite database? You will need to log out and log in again to resync the data",
|
"reset_sqlite_confirmation": "Are you sure you want to reset the SQLite database? You will need to log out and log in again to resync the data",
|
||||||
"reset_sqlite_success": "Successfully reset the SQLite database",
|
"reset_sqlite_success": "Successfully reset the SQLite database",
|
||||||
@@ -1865,7 +1847,6 @@
|
|||||||
"sort_created": "Date created",
|
"sort_created": "Date created",
|
||||||
"sort_items": "Number of items",
|
"sort_items": "Number of items",
|
||||||
"sort_modified": "Date modified",
|
"sort_modified": "Date modified",
|
||||||
"sort_newest": "Newest photo",
|
|
||||||
"sort_oldest": "Oldest photo",
|
"sort_oldest": "Oldest photo",
|
||||||
"sort_people_by_similarity": "Sort people by similarity",
|
"sort_people_by_similarity": "Sort people by similarity",
|
||||||
"sort_recent": "Most recent photo",
|
"sort_recent": "Most recent photo",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
ARG DEVICE=cpu
|
ARG DEVICE=cpu
|
||||||
|
|
||||||
FROM python:3.11-bookworm@sha256:c642d5dfaf9115a12086785f23008558ae2e13bcd0c4794536340bcb777a4381 AS builder-cpu
|
FROM python:3.11-bookworm@sha256:ce3b954c9285a7a145cba620bae03db836ab890b6b9e0d05a3ca522ea00dfbc9 AS builder-cpu
|
||||||
|
|
||||||
FROM builder-cpu AS builder-openvino
|
FROM builder-cpu AS builder-openvino
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
|||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends g++
|
RUN apt-get update && apt-get install -y --no-install-recommends g++
|
||||||
|
|
||||||
COPY --from=ghcr.io/astral-sh/uv:latest@sha256:f64ad69940b634e75d2e4d799eb5238066c5eeda49f76e782d4873c3d014ea33 /uv /uvx /bin/
|
COPY --from=ghcr.io/astral-sh/uv:latest@sha256:9653efd4380d5a0e5511e337dcfc3b8ba5bc4e6ea7fa3be7716598261d5503fa /uv /uvx /bin/
|
||||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||||
@@ -68,11 +68,11 @@ RUN if [ "$DEVICE" = "rocm" ]; then \
|
|||||||
uv pip install /opt/onnxruntime_rocm-*.whl; \
|
uv pip install /opt/onnxruntime_rocm-*.whl; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
FROM python:3.11-slim-bookworm@sha256:838ff46ae6c481e85e369706fa3dea5166953824124735639f3c9f52af85f319 AS prod-cpu
|
FROM python:3.11-slim-bookworm@sha256:9e1912aab0a30bbd9488eb79063f68f42a68ab0946cbe98fecf197fe5b085506 AS prod-cpu
|
||||||
|
|
||||||
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2
|
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2
|
||||||
|
|
||||||
FROM python:3.11-slim-bookworm@sha256:838ff46ae6c481e85e369706fa3dea5166953824124735639f3c9f52af85f319 AS prod-openvino
|
FROM python:3.11-slim-bookworm@sha256:9e1912aab0a30bbd9488eb79063f68f42a68ab0946cbe98fecf197fe5b085506 AS prod-openvino
|
||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \
|
apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ def to_numpy(img: Image.Image) -> NDArray[np.float32]:
|
|||||||
def normalize(
|
def normalize(
|
||||||
img: NDArray[np.float32], mean: float | NDArray[np.float32], std: float | NDArray[np.float32]
|
img: NDArray[np.float32], mean: float | NDArray[np.float32], std: float | NDArray[np.float32]
|
||||||
) -> NDArray[np.float32]:
|
) -> NDArray[np.float32]:
|
||||||
return (img - mean) / std
|
return np.divide(img - mean, std, dtype=np.float32)
|
||||||
|
|
||||||
|
|
||||||
def get_pil_resampling(resample: str) -> Image.Resampling:
|
def get_pil_resampling(resample: str) -> Image.Resampling:
|
||||||
@@ -58,13 +58,11 @@ def decode_pil(image_bytes: bytes | IO[bytes] | Image.Image) -> Image.Image:
|
|||||||
|
|
||||||
|
|
||||||
def decode_cv2(image_bytes: NDArray[np.uint8] | bytes | Image.Image) -> NDArray[np.uint8]:
|
def decode_cv2(image_bytes: NDArray[np.uint8] | bytes | Image.Image) -> NDArray[np.uint8]:
|
||||||
match image_bytes:
|
if isinstance(image_bytes, bytes):
|
||||||
case bytes() | memoryview() | bytearray():
|
image_bytes = decode_pil(image_bytes) # pillow is much faster than cv2
|
||||||
return pil_to_cv2(decode_pil(image_bytes)) # pillow is much faster than cv2
|
if isinstance(image_bytes, Image.Image):
|
||||||
case Image.Image():
|
return pil_to_cv2(image_bytes)
|
||||||
return pil_to_cv2(image_bytes)
|
return image_bytes
|
||||||
case _:
|
|
||||||
return image_bytes
|
|
||||||
|
|
||||||
|
|
||||||
def clean_text(text: str, canonicalize: bool = False) -> str:
|
def clean_text(text: str, canonicalize: bool = False) -> str:
|
||||||
|
|||||||
@@ -112,4 +112,8 @@ def has_profiling(obj: Any) -> TypeGuard[HasProfiling]:
|
|||||||
return hasattr(obj, "profiling") and isinstance(obj.profiling, dict)
|
return hasattr(obj, "profiling") and isinstance(obj.profiling, dict)
|
||||||
|
|
||||||
|
|
||||||
|
def is_ndarray(obj: Any, dtype: "type[np._DTypeScalar_co]") -> "TypeGuard[npt.NDArray[np._DTypeScalar_co]]":
|
||||||
|
return isinstance(obj, np.ndarray) and obj.dtype == dtype
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ dependencies = [
|
|||||||
"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",
|
||||||
"numpy<2",
|
|
||||||
"opencv-python-headless>=4.7.0.72,<5.0",
|
"opencv-python-headless>=4.7.0.72,<5.0",
|
||||||
"orjson>=3.9.5",
|
"orjson>=3.9.5",
|
||||||
"pillow>=9.5.0,<11.0",
|
"pillow>=9.5.0,<11.0",
|
||||||
|
|||||||
Generated
+202
-571
File diff suppressed because it is too large
Load Diff
@@ -1,10 +0,0 @@
|
|||||||
cmake_minimum_required(VERSION 3.12)
|
|
||||||
|
|
||||||
set(CMAKE_C_STANDARD 17)
|
|
||||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
|
||||||
|
|
||||||
project(native_buffer LANGUAGES C)
|
|
||||||
|
|
||||||
add_library(native_buffer SHARED
|
|
||||||
src/main/cpp/native_buffer.c
|
|
||||||
)
|
|
||||||
@@ -83,12 +83,6 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
namespace 'app.alextran.immich'
|
namespace 'app.alextran.immich'
|
||||||
|
|
||||||
externalNativeBuild {
|
|
||||||
cmake {
|
|
||||||
path "CMakeLists.txt"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
#include <jni.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
|
|
||||||
JNIEXPORT jlong JNICALL
|
|
||||||
Java_app_alextran_immich_images_ThumbnailsImpl_00024Companion_allocateNative(
|
|
||||||
JNIEnv *env, jclass clazz, jint size) {
|
|
||||||
void *ptr = malloc(size);
|
|
||||||
return (jlong) ptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT jlong JNICALL
|
|
||||||
Java_app_alextran_immich_images_ThumbnailsImpl_allocateNative(
|
|
||||||
JNIEnv *env, jclass clazz, jint size) {
|
|
||||||
void *ptr = malloc(size);
|
|
||||||
return (jlong) ptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT void JNICALL
|
|
||||||
Java_app_alextran_immich_images_ThumbnailsImpl_00024Companion_freeNative(
|
|
||||||
JNIEnv *env, jclass clazz, jlong address) {
|
|
||||||
free((void *) address);
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT void JNICALL
|
|
||||||
Java_app_alextran_immich_images_ThumbnailsImpl_freeNative(
|
|
||||||
JNIEnv *env, jclass clazz, jlong address) {
|
|
||||||
free((void *) address);
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT jobject JNICALL
|
|
||||||
Java_app_alextran_immich_images_ThumbnailsImpl_00024Companion_wrapAsBuffer(
|
|
||||||
JNIEnv *env, jclass clazz, jlong address, jint capacity) {
|
|
||||||
return (*env)->NewDirectByteBuffer(env, (void *) address, capacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT jobject JNICALL
|
|
||||||
Java_app_alextran_immich_images_ThumbnailsImpl_wrapAsBuffer(
|
|
||||||
JNIEnv *env, jclass clazz, jlong address, jint capacity) {
|
|
||||||
return (*env)->NewDirectByteBuffer(env, (void *) address, capacity);
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,7 @@
|
|||||||
package app.alextran.immich
|
package app.alextran.immich
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.bumptech.glide.GlideBuilder
|
|
||||||
import com.bumptech.glide.annotation.GlideModule
|
import com.bumptech.glide.annotation.GlideModule
|
||||||
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPoolAdapter
|
|
||||||
import com.bumptech.glide.load.engine.cache.DiskCacheAdapter
|
|
||||||
import com.bumptech.glide.load.engine.cache.MemoryCacheAdapter
|
|
||||||
import com.bumptech.glide.module.AppGlideModule
|
import com.bumptech.glide.module.AppGlideModule
|
||||||
|
|
||||||
@GlideModule
|
@GlideModule
|
||||||
class AppGlideModule : AppGlideModule() {
|
class AppGlideModule : AppGlideModule()
|
||||||
override fun applyOptions(context: Context, builder: GlideBuilder) {
|
|
||||||
super.applyOptions(context, builder)
|
|
||||||
// disable caching as this is already done on the Flutter side
|
|
||||||
builder.setMemoryCache(MemoryCacheAdapter())
|
|
||||||
builder.setDiskCache(DiskCacheAdapter.Factory())
|
|
||||||
builder.setBitmapPool(BitmapPoolAdapter())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,8 +3,6 @@ package app.alextran.immich
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.ext.SdkExtensions
|
import android.os.ext.SdkExtensions
|
||||||
import androidx.annotation.NonNull
|
import androidx.annotation.NonNull
|
||||||
import app.alextran.immich.images.ThumbnailApi
|
|
||||||
import app.alextran.immich.images.ThumbnailsImpl
|
|
||||||
import app.alextran.immich.sync.NativeSyncApi
|
import app.alextran.immich.sync.NativeSyncApi
|
||||||
import app.alextran.immich.sync.NativeSyncApiImpl26
|
import app.alextran.immich.sync.NativeSyncApiImpl26
|
||||||
import app.alextran.immich.sync.NativeSyncApiImpl30
|
import app.alextran.immich.sync.NativeSyncApiImpl30
|
||||||
@@ -24,7 +22,6 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
} else {
|
} else {
|
||||||
NativeSyncApiImpl30(this)
|
NativeSyncApiImpl30(this)
|
||||||
}
|
}
|
||||||
NativeSyncApi.setUp(flutterEngine.dartExecutor.binaryMessenger, nativeSyncApiImpl)
|
NativeSyncApi.setUp(flutterEngine.dartExecutor.binaryMessenger, nativeSyncApiImpl)
|
||||||
ThumbnailApi.setUp(flutterEngine.dartExecutor.binaryMessenger, ThumbnailsImpl(this))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,164 +0,0 @@
|
|||||||
package app.alextran.immich.images;
|
|
||||||
|
|
||||||
// Copyright (c) 2023 Evan Wallace
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
||||||
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
|
|
||||||
// modified to use native allocations
|
|
||||||
public final class ThumbHash {
|
|
||||||
/**
|
|
||||||
* Decodes a ThumbHash to an RGBA image. RGB is not be premultiplied by A.
|
|
||||||
*
|
|
||||||
* @param hash The bytes of the ThumbHash.
|
|
||||||
* @return The width, height, and pixels of the rendered placeholder image.
|
|
||||||
*/
|
|
||||||
public static Image thumbHashToRGBA(byte[] hash) {
|
|
||||||
// Read the constants
|
|
||||||
int header24 = (hash[0] & 255) | ((hash[1] & 255) << 8) | ((hash[2] & 255) << 16);
|
|
||||||
int header16 = (hash[3] & 255) | ((hash[4] & 255) << 8);
|
|
||||||
float l_dc = (float) (header24 & 63) / 63.0f;
|
|
||||||
float p_dc = (float) ((header24 >> 6) & 63) / 31.5f - 1.0f;
|
|
||||||
float q_dc = (float) ((header24 >> 12) & 63) / 31.5f - 1.0f;
|
|
||||||
float l_scale = (float) ((header24 >> 18) & 31) / 31.0f;
|
|
||||||
boolean hasAlpha = (header24 >> 23) != 0;
|
|
||||||
float p_scale = (float) ((header16 >> 3) & 63) / 63.0f;
|
|
||||||
float q_scale = (float) ((header16 >> 9) & 63) / 63.0f;
|
|
||||||
boolean isLandscape = (header16 >> 15) != 0;
|
|
||||||
int lx = Math.max(3, isLandscape ? hasAlpha ? 5 : 7 : header16 & 7);
|
|
||||||
int ly = Math.max(3, isLandscape ? header16 & 7 : hasAlpha ? 5 : 7);
|
|
||||||
float a_dc = hasAlpha ? (float) (hash[5] & 15) / 15.0f : 1.0f;
|
|
||||||
float a_scale = (float) ((hash[5] >> 4) & 15) / 15.0f;
|
|
||||||
|
|
||||||
// Read the varying factors (boost saturation by 1.25x to compensate for quantization)
|
|
||||||
int ac_start = hasAlpha ? 6 : 5;
|
|
||||||
int ac_index = 0;
|
|
||||||
Channel l_channel = new Channel(lx, ly);
|
|
||||||
Channel p_channel = new Channel(3, 3);
|
|
||||||
Channel q_channel = new Channel(3, 3);
|
|
||||||
Channel a_channel = null;
|
|
||||||
ac_index = l_channel.decode(hash, ac_start, ac_index, l_scale);
|
|
||||||
ac_index = p_channel.decode(hash, ac_start, ac_index, p_scale * 1.25f);
|
|
||||||
ac_index = q_channel.decode(hash, ac_start, ac_index, q_scale * 1.25f);
|
|
||||||
if (hasAlpha) {
|
|
||||||
a_channel = new Channel(5, 5);
|
|
||||||
a_channel.decode(hash, ac_start, ac_index, a_scale);
|
|
||||||
}
|
|
||||||
float[] l_ac = l_channel.ac;
|
|
||||||
float[] p_ac = p_channel.ac;
|
|
||||||
float[] q_ac = q_channel.ac;
|
|
||||||
float[] a_ac = hasAlpha ? a_channel.ac : null;
|
|
||||||
|
|
||||||
// Decode using the DCT into RGB
|
|
||||||
float ratio = thumbHashToApproximateAspectRatio(hash);
|
|
||||||
int w = Math.round(ratio > 1.0f ? 32.0f : 32.0f * ratio);
|
|
||||||
int h = Math.round(ratio > 1.0f ? 32.0f / ratio : 32.0f);
|
|
||||||
int size = w * h * 4;
|
|
||||||
long pointer = ThumbnailsImpl.allocateNative(size);
|
|
||||||
ByteBuffer rgba = ThumbnailsImpl.wrapAsBuffer(pointer, size);
|
|
||||||
int cx_stop = Math.max(lx, hasAlpha ? 5 : 3);
|
|
||||||
int cy_stop = Math.max(ly, hasAlpha ? 5 : 3);
|
|
||||||
float[] fx = new float[cx_stop];
|
|
||||||
float[] fy = new float[cy_stop];
|
|
||||||
for (int y = 0, i = 0; y < h; y++) {
|
|
||||||
for (int x = 0; x < w; x++, i += 4) {
|
|
||||||
float l = l_dc, p = p_dc, q = q_dc, a = a_dc;
|
|
||||||
|
|
||||||
// Precompute the coefficients
|
|
||||||
for (int cx = 0; cx < cx_stop; cx++)
|
|
||||||
fx[cx] = (float) Math.cos(Math.PI / w * (x + 0.5f) * cx);
|
|
||||||
for (int cy = 0; cy < cy_stop; cy++)
|
|
||||||
fy[cy] = (float) Math.cos(Math.PI / h * (y + 0.5f) * cy);
|
|
||||||
|
|
||||||
// Decode L
|
|
||||||
for (int cy = 0, j = 0; cy < ly; cy++) {
|
|
||||||
float fy2 = fy[cy] * 2.0f;
|
|
||||||
for (int cx = cy > 0 ? 0 : 1; cx * ly < lx * (ly - cy); cx++, j++)
|
|
||||||
l += l_ac[j] * fx[cx] * fy2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode P and Q
|
|
||||||
for (int cy = 0, j = 0; cy < 3; cy++) {
|
|
||||||
float fy2 = fy[cy] * 2.0f;
|
|
||||||
for (int cx = cy > 0 ? 0 : 1; cx < 3 - cy; cx++, j++) {
|
|
||||||
float f = fx[cx] * fy2;
|
|
||||||
p += p_ac[j] * f;
|
|
||||||
q += q_ac[j] * f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode A
|
|
||||||
if (hasAlpha)
|
|
||||||
for (int cy = 0, j = 0; cy < 5; cy++) {
|
|
||||||
float fy2 = fy[cy] * 2.0f;
|
|
||||||
for (int cx = cy > 0 ? 0 : 1; cx < 5 - cy; cx++, j++)
|
|
||||||
a += a_ac[j] * fx[cx] * fy2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to RGB
|
|
||||||
float b = l - 2.0f / 3.0f * p;
|
|
||||||
float r = (3.0f * l - b + q) / 2.0f;
|
|
||||||
float g = r - q;
|
|
||||||
rgba.put(i, (byte) Math.max(0, Math.round(255.0f * Math.min(1, r))));
|
|
||||||
rgba.put(i + 1, (byte) Math.max(0, Math.round(255.0f * Math.min(1, g))));
|
|
||||||
rgba.put(i + 2, (byte) Math.max(0, Math.round(255.0f * Math.min(1, b))));
|
|
||||||
rgba.put(i + 3, (byte) Math.max(0, Math.round(255.0f * Math.min(1, a))));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new Image(w, h, pointer);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts the approximate aspect ratio of the original image.
|
|
||||||
*
|
|
||||||
* @param hash The bytes of the ThumbHash.
|
|
||||||
* @return The approximate aspect ratio (i.e. width / height).
|
|
||||||
*/
|
|
||||||
public static float thumbHashToApproximateAspectRatio(byte[] hash) {
|
|
||||||
byte header = hash[3];
|
|
||||||
boolean hasAlpha = (hash[2] & 0x80) != 0;
|
|
||||||
boolean isLandscape = (hash[4] & 0x80) != 0;
|
|
||||||
int lx = isLandscape ? hasAlpha ? 5 : 7 : header & 7;
|
|
||||||
int ly = isLandscape ? header & 7 : hasAlpha ? 5 : 7;
|
|
||||||
return (float) lx / (float) ly;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final class Image {
|
|
||||||
public int width;
|
|
||||||
public int height;
|
|
||||||
public long pointer;
|
|
||||||
|
|
||||||
public Image(int width, int height, long pointer) {
|
|
||||||
this.width = width;
|
|
||||||
this.height = height;
|
|
||||||
this.pointer = pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class Channel {
|
|
||||||
int nx;
|
|
||||||
int ny;
|
|
||||||
float[] ac;
|
|
||||||
|
|
||||||
Channel(int nx, int ny) {
|
|
||||||
this.nx = nx;
|
|
||||||
this.ny = ny;
|
|
||||||
int n = 0;
|
|
||||||
for (int cy = 0; cy < ny; cy++)
|
|
||||||
for (int cx = cy > 0 ? 0 : 1; cx * ny < nx * (ny - cy); cx++)
|
|
||||||
n++;
|
|
||||||
ac = new float[n];
|
|
||||||
}
|
|
||||||
|
|
||||||
int decode(byte[] hash, int start, int index, float scale) {
|
|
||||||
for (int i = 0; i < ac.length; i++) {
|
|
||||||
int data = hash[start + (index >> 1)] >> ((index & 1) << 2);
|
|
||||||
ac[i] = ((float) (data & 15) / 7.5f - 1.0f) * scale;
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
|
||||||
// See also: https://pub.dev/packages/pigeon
|
|
||||||
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
|
||||||
|
|
||||||
package app.alextran.immich.images
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import io.flutter.plugin.common.BasicMessageChannel
|
|
||||||
import io.flutter.plugin.common.BinaryMessenger
|
|
||||||
import io.flutter.plugin.common.EventChannel
|
|
||||||
import io.flutter.plugin.common.MessageCodec
|
|
||||||
import io.flutter.plugin.common.StandardMethodCodec
|
|
||||||
import io.flutter.plugin.common.StandardMessageCodec
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
private object ThumbnailsPigeonUtils {
|
|
||||||
|
|
||||||
fun wrapResult(result: Any?): List<Any?> {
|
|
||||||
return listOf(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun wrapError(exception: Throwable): List<Any?> {
|
|
||||||
return if (exception is FlutterError) {
|
|
||||||
listOf(
|
|
||||||
exception.code,
|
|
||||||
exception.message,
|
|
||||||
exception.details
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
listOf(
|
|
||||||
exception.javaClass.simpleName,
|
|
||||||
exception.toString(),
|
|
||||||
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error class for passing custom error details to Flutter via a thrown PlatformException.
|
|
||||||
* @property code The error code.
|
|
||||||
* @property message The error message.
|
|
||||||
* @property details The error details. Must be a datatype supported by the api codec.
|
|
||||||
*/
|
|
||||||
class FlutterError (
|
|
||||||
val code: String,
|
|
||||||
override val message: String? = null,
|
|
||||||
val details: Any? = null
|
|
||||||
) : Throwable()
|
|
||||||
private open class ThumbnailsPigeonCodec : StandardMessageCodec() {
|
|
||||||
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
|
||||||
return super.readValueOfType(type, buffer)
|
|
||||||
}
|
|
||||||
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
|
||||||
super.writeValue(stream, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
|
||||||
interface ThumbnailApi {
|
|
||||||
fun requestImage(assetId: String, requestId: Long, width: Long, height: Long, isVideo: Boolean, callback: (Result<Map<String, Long>>) -> Unit)
|
|
||||||
fun cancelImageRequest(requestId: Long)
|
|
||||||
fun getThumbhash(thumbhash: String, callback: (Result<Map<String, Long>>) -> Unit)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
/** The codec used by ThumbnailApi. */
|
|
||||||
val codec: MessageCodec<Any?> by lazy {
|
|
||||||
ThumbnailsPigeonCodec()
|
|
||||||
}
|
|
||||||
/** Sets up an instance of `ThumbnailApi` to handle messages through the `binaryMessenger`. */
|
|
||||||
@JvmOverloads
|
|
||||||
fun setUp(binaryMessenger: BinaryMessenger, api: ThumbnailApi?, messageChannelSuffix: String = "") {
|
|
||||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
|
||||||
run {
|
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ThumbnailApi.requestImage$separatedMessageChannelSuffix", codec)
|
|
||||||
if (api != null) {
|
|
||||||
channel.setMessageHandler { message, reply ->
|
|
||||||
val args = message as List<Any?>
|
|
||||||
val assetIdArg = args[0] as String
|
|
||||||
val requestIdArg = args[1] as Long
|
|
||||||
val widthArg = args[2] as Long
|
|
||||||
val heightArg = args[3] as Long
|
|
||||||
val isVideoArg = args[4] as Boolean
|
|
||||||
api.requestImage(assetIdArg, requestIdArg, widthArg, heightArg, isVideoArg) { result: Result<Map<String, Long>> ->
|
|
||||||
val error = result.exceptionOrNull()
|
|
||||||
if (error != null) {
|
|
||||||
reply.reply(ThumbnailsPigeonUtils.wrapError(error))
|
|
||||||
} else {
|
|
||||||
val data = result.getOrNull()
|
|
||||||
reply.reply(ThumbnailsPigeonUtils.wrapResult(data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
channel.setMessageHandler(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
run {
|
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ThumbnailApi.cancelImageRequest$separatedMessageChannelSuffix", codec)
|
|
||||||
if (api != null) {
|
|
||||||
channel.setMessageHandler { message, reply ->
|
|
||||||
val args = message as List<Any?>
|
|
||||||
val requestIdArg = args[0] as Long
|
|
||||||
val wrapped: List<Any?> = try {
|
|
||||||
api.cancelImageRequest(requestIdArg)
|
|
||||||
listOf(null)
|
|
||||||
} catch (exception: Throwable) {
|
|
||||||
ThumbnailsPigeonUtils.wrapError(exception)
|
|
||||||
}
|
|
||||||
reply.reply(wrapped)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
channel.setMessageHandler(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
run {
|
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ThumbnailApi.getThumbhash$separatedMessageChannelSuffix", codec)
|
|
||||||
if (api != null) {
|
|
||||||
channel.setMessageHandler { message, reply ->
|
|
||||||
val args = message as List<Any?>
|
|
||||||
val thumbhashArg = args[0] as String
|
|
||||||
api.getThumbhash(thumbhashArg) { result: Result<Map<String, Long>> ->
|
|
||||||
val error = result.exceptionOrNull()
|
|
||||||
if (error != null) {
|
|
||||||
reply.reply(ThumbnailsPigeonUtils.wrapError(error))
|
|
||||||
} else {
|
|
||||||
val data = result.getOrNull()
|
|
||||||
reply.reply(ThumbnailsPigeonUtils.wrapResult(data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
channel.setMessageHandler(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,230 +0,0 @@
|
|||||||
package app.alextran.immich.images
|
|
||||||
|
|
||||||
import android.content.ContentResolver
|
|
||||||
import android.content.ContentUris
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.*
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.CancellationSignal
|
|
||||||
import android.os.OperationCanceledException
|
|
||||||
import android.provider.MediaStore
|
|
||||||
import android.provider.MediaStore.Images
|
|
||||||
import android.provider.MediaStore.Video
|
|
||||||
import android.util.Size
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import kotlin.math.*
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.bumptech.glide.Priority
|
|
||||||
import com.bumptech.glide.load.DecodeFormat
|
|
||||||
import java.util.Base64
|
|
||||||
import java.util.HashMap
|
|
||||||
import java.util.concurrent.CancellationException
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
import java.util.concurrent.Future
|
|
||||||
|
|
||||||
data class Request(
|
|
||||||
val taskFuture: Future<*>,
|
|
||||||
val cancellationSignal: CancellationSignal,
|
|
||||||
val callback: (Result<Map<String, Long>>) -> Unit
|
|
||||||
)
|
|
||||||
|
|
||||||
class ThumbnailsImpl(context: Context) : ThumbnailApi {
|
|
||||||
private val ctx: Context = context.applicationContext
|
|
||||||
private val resolver: ContentResolver = ctx.contentResolver
|
|
||||||
private val requestThread = Executors.newSingleThreadExecutor()
|
|
||||||
private val threadPool =
|
|
||||||
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() / 2 + 1)
|
|
||||||
private val requestMap = ConcurrentHashMap<Long, Request>()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val CANCELLED = Result.success<Map<String, Long>>(mapOf())
|
|
||||||
val OPTIONS = BitmapFactory.Options().apply { inPreferredConfig = Bitmap.Config.ARGB_8888 }
|
|
||||||
|
|
||||||
init {
|
|
||||||
System.loadLibrary("native_buffer")
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
external fun allocateNative(size: Int): Long
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
external fun freeNative(pointer: Long)
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
external fun wrapAsBuffer(address: Long, capacity: Int): ByteBuffer
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getThumbhash(thumbhash: String, callback: (Result<Map<String, Long>>) -> Unit) {
|
|
||||||
threadPool.execute {
|
|
||||||
try {
|
|
||||||
val bytes = Base64.getDecoder().decode(thumbhash)
|
|
||||||
val image = ThumbHash.thumbHashToRGBA(bytes)
|
|
||||||
val res = mapOf(
|
|
||||||
"pointer" to image.pointer,
|
|
||||||
"width" to image.width.toLong(),
|
|
||||||
"height" to image.height.toLong()
|
|
||||||
)
|
|
||||||
callback(Result.success(res))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
callback(Result.failure(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun requestImage(
|
|
||||||
assetId: String,
|
|
||||||
requestId: Long,
|
|
||||||
width: Long,
|
|
||||||
height: Long,
|
|
||||||
isVideo: Boolean,
|
|
||||||
callback: (Result<Map<String, Long>>) -> Unit
|
|
||||||
) {
|
|
||||||
val signal = CancellationSignal()
|
|
||||||
val task = threadPool.submit {
|
|
||||||
try {
|
|
||||||
getThumbnailBufferInternal(assetId, width, height, isVideo, callback, signal)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
when (e) {
|
|
||||||
is OperationCanceledException -> callback(CANCELLED)
|
|
||||||
is CancellationException -> callback(CANCELLED)
|
|
||||||
else -> callback(Result.failure(e))
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
requestMap.remove(requestId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val request = Request(task, signal, callback)
|
|
||||||
requestMap[requestId] = request
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cancelImageRequest(requestId: Long) {
|
|
||||||
val request = requestMap.remove(requestId) ?: return
|
|
||||||
request.taskFuture.cancel(false)
|
|
||||||
request.cancellationSignal.cancel()
|
|
||||||
if (request.taskFuture.isCancelled) {
|
|
||||||
requestThread.execute {
|
|
||||||
try {
|
|
||||||
request.callback(CANCELLED)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getThumbnailBufferInternal(
|
|
||||||
assetId: String,
|
|
||||||
width: Long,
|
|
||||||
height: Long,
|
|
||||||
isVideo: Boolean,
|
|
||||||
callback: (Result<Map<String, Long>>) -> Unit,
|
|
||||||
signal: CancellationSignal
|
|
||||||
) {
|
|
||||||
signal.throwIfCanceled()
|
|
||||||
val targetWidth = width.toInt()
|
|
||||||
val targetHeight = height.toInt()
|
|
||||||
val id = assetId.toLong()
|
|
||||||
|
|
||||||
signal.throwIfCanceled()
|
|
||||||
val bitmap = if (isVideo) {
|
|
||||||
decodeVideoThumbnail(id, targetWidth, targetHeight, signal)
|
|
||||||
} else {
|
|
||||||
decodeImage(id, targetWidth, targetHeight, signal)
|
|
||||||
}
|
|
||||||
|
|
||||||
processBitmap(bitmap, callback, signal)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun processBitmap(
|
|
||||||
bitmap: Bitmap, callback: (Result<Map<String, Long>>) -> Unit, signal: CancellationSignal
|
|
||||||
) {
|
|
||||||
signal.throwIfCanceled()
|
|
||||||
val actualWidth = bitmap.width
|
|
||||||
val actualHeight = bitmap.height
|
|
||||||
|
|
||||||
val size = actualWidth * actualHeight * 4
|
|
||||||
val pointer = allocateNative(size)
|
|
||||||
|
|
||||||
try {
|
|
||||||
signal.throwIfCanceled()
|
|
||||||
val buffer = wrapAsBuffer(pointer, size)
|
|
||||||
bitmap.copyPixelsToBuffer(buffer)
|
|
||||||
bitmap.recycle()
|
|
||||||
signal.throwIfCanceled()
|
|
||||||
val res = mapOf(
|
|
||||||
"pointer" to pointer,
|
|
||||||
"width" to actualWidth.toLong(),
|
|
||||||
"height" to actualHeight.toLong()
|
|
||||||
)
|
|
||||||
callback(Result.success(res))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
freeNative(pointer)
|
|
||||||
callback(if (e is OperationCanceledException) CANCELLED else Result.failure(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun decodeImage(
|
|
||||||
id: Long, targetWidth: Int, targetHeight: Int, signal: CancellationSignal
|
|
||||||
): Bitmap {
|
|
||||||
signal.throwIfCanceled()
|
|
||||||
val uri = ContentUris.withAppendedId(Images.Media.EXTERNAL_CONTENT_URI, id)
|
|
||||||
if (targetHeight > 768 || targetWidth > 768) {
|
|
||||||
return decodeSource(uri, targetWidth, targetHeight, signal)
|
|
||||||
}
|
|
||||||
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
resolver.loadThumbnail(uri, Size(targetWidth, targetHeight), signal)
|
|
||||||
} else {
|
|
||||||
signal.setOnCancelListener { Images.Thumbnails.cancelThumbnailRequest(resolver, id) }
|
|
||||||
Images.Thumbnails.getThumbnail(resolver, id, Images.Thumbnails.MINI_KIND, OPTIONS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun decodeVideoThumbnail(
|
|
||||||
id: Long, targetWidth: Int, targetHeight: Int, signal: CancellationSignal
|
|
||||||
): Bitmap {
|
|
||||||
signal.throwIfCanceled()
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
val uri = ContentUris.withAppendedId(Video.Media.EXTERNAL_CONTENT_URI, id)
|
|
||||||
resolver.loadThumbnail(uri, Size(targetWidth, targetHeight), signal)
|
|
||||||
} else {
|
|
||||||
signal.setOnCancelListener { Video.Thumbnails.cancelThumbnailRequest(resolver, id) }
|
|
||||||
Video.Thumbnails.getThumbnail(resolver, id, Video.Thumbnails.MINI_KIND, OPTIONS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun decodeSource(
|
|
||||||
uri: Uri, targetWidth: Int, targetHeight: Int, signal: CancellationSignal
|
|
||||||
): Bitmap {
|
|
||||||
signal.throwIfCanceled()
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
val source = ImageDecoder.createSource(resolver, uri)
|
|
||||||
signal.throwIfCanceled()
|
|
||||||
ImageDecoder.decodeBitmap(source) { decoder, info, _ ->
|
|
||||||
val sampleSize =
|
|
||||||
getSampleSize(info.size.width, info.size.height, targetWidth, targetHeight)
|
|
||||||
decoder.setTargetSampleSize(sampleSize)
|
|
||||||
decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
|
|
||||||
decoder.setTargetColorSpace(ColorSpace.get(ColorSpace.Named.SRGB))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val ref = Glide.with(ctx).asBitmap().priority(Priority.IMMEDIATE).load(uri)
|
|
||||||
.disallowHardwareConfig().format(DecodeFormat.PREFER_ARGB_8888)
|
|
||||||
.submit(targetWidth, targetHeight)
|
|
||||||
signal.setOnCancelListener { Glide.with(ctx).clear(ref) }
|
|
||||||
ref.get()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getSampleSize(fullWidth: Int, fullHeight: Int, reqWidth: Int, reqHeight: Int): Int {
|
|
||||||
return 1 shl max(
|
|
||||||
0, floor(
|
|
||||||
min(
|
|
||||||
log2(fullWidth / reqWidth.toDouble()),
|
|
||||||
log2(fullHeight / reqHeight.toDouble()),
|
|
||||||
)
|
|
||||||
).toInt()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -35,8 +35,8 @@ platform :android do
|
|||||||
task: 'bundle',
|
task: 'bundle',
|
||||||
build_type: 'Release',
|
build_type: 'Release',
|
||||||
properties: {
|
properties: {
|
||||||
"android.injected.version.code" => 3005,
|
"android.injected.version.code" => 3002,
|
||||||
"android.injected.version.name" => "1.139.0",
|
"android.injected.version.name" => "1.137.3",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||||
|
|||||||
-1
File diff suppressed because one or more lines are too long
@@ -4,6 +4,7 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
||||||
import 'package:immich_mobile/main.dart' as app;
|
import 'package:immich_mobile/main.dart' as app;
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
@@ -39,18 +40,15 @@ class ImmichTestHelper {
|
|||||||
static Future<void> loadApp(WidgetTester tester) async {
|
static Future<void> loadApp(WidgetTester tester) async {
|
||||||
await EasyLocalization.ensureInitialized();
|
await EasyLocalization.ensureInitialized();
|
||||||
// Clear all data from Isar (reuse existing instance if available)
|
// Clear all data from Isar (reuse existing instance if available)
|
||||||
final (isar, drift, logDb) = await Bootstrap.initDB();
|
final db = await Bootstrap.initIsar();
|
||||||
await Bootstrap.initDomain(isar, drift, logDb);
|
final logDb = DriftLogger();
|
||||||
|
await Bootstrap.initDomain(db, logDb);
|
||||||
await Store.clear();
|
await Store.clear();
|
||||||
await isar.writeTxn(() => isar.clear());
|
await db.writeTxn(() => db.clear());
|
||||||
// Load main Widget
|
// Load main Widget
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
ProviderScope(
|
ProviderScope(
|
||||||
overrides: [
|
overrides: [dbProvider.overrideWithValue(db), isarProvider.overrideWithValue(db)],
|
||||||
dbProvider.overrideWithValue(isar),
|
|
||||||
isarProvider.overrideWithValue(isar),
|
|
||||||
driftProvider.overrideWith(driftOverride(drift)),
|
|
||||||
],
|
|
||||||
child: const app.MainWidget(),
|
child: const app.MainWidget(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,9 +24,6 @@
|
|||||||
FAC6F89B2D287C890078CB2F /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = FAC6F8902D287C890078CB2F /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
FAC6F89B2D287C890078CB2F /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = FAC6F8902D287C890078CB2F /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
FAC6F8B72D287F120078CB2F /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6F8B52D287F120078CB2F /* ShareViewController.swift */; };
|
FAC6F8B72D287F120078CB2F /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6F8B52D287F120078CB2F /* ShareViewController.swift */; };
|
||||||
FAC6F8B92D287F120078CB2F /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FAC6F8B32D287F120078CB2F /* MainInterface.storyboard */; };
|
FAC6F8B92D287F120078CB2F /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FAC6F8B32D287F120078CB2F /* MainInterface.storyboard */; };
|
||||||
FEAFA8732E4D42F4001E47FE /* Thumbhash.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEAFA8722E4D42F4001E47FE /* Thumbhash.swift */; };
|
|
||||||
FED3B1962E253E9B0030FD97 /* ThumbnailsImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED3B1942E253E9B0030FD97 /* ThumbnailsImpl.swift */; };
|
|
||||||
FED3B1972E253E9B0030FD97 /* Thumbnails.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED3B1932E253E9B0030FD97 /* Thumbnails.g.swift */; };
|
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -105,9 +102,6 @@
|
|||||||
FAC6F8B42D287F120078CB2F /* ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = "<group>"; };
|
FAC6F8B42D287F120078CB2F /* ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = "<group>"; };
|
||||||
FAC6F8B52D287F120078CB2F /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
FAC6F8B52D287F120078CB2F /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
||||||
FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = "<group>"; };
|
FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = "<group>"; };
|
||||||
FEAFA8722E4D42F4001E47FE /* Thumbhash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbhash.swift; sourceTree = "<group>"; };
|
|
||||||
FED3B1932E253E9B0030FD97 /* Thumbnails.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbnails.g.swift; sourceTree = "<group>"; };
|
|
||||||
FED3B1942E253E9B0030FD97 /* ThumbnailsImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailsImpl.swift; sourceTree = "<group>"; };
|
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
@@ -123,6 +117,8 @@
|
|||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
|
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
exceptions = (
|
||||||
|
);
|
||||||
path = Sync;
|
path = Sync;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -247,7 +243,6 @@
|
|||||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||||
FED3B1952E253E9B0030FD97 /* Images */,
|
|
||||||
);
|
);
|
||||||
path = Runner;
|
path = Runner;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -263,16 +258,6 @@
|
|||||||
path = ShareExtension;
|
path = ShareExtension;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
FED3B1952E253E9B0030FD97 /* Images */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
FEAFA8722E4D42F4001E47FE /* Thumbhash.swift */,
|
|
||||||
FED3B1932E253E9B0030FD97 /* Thumbnails.g.swift */,
|
|
||||||
FED3B1942E253E9B0030FD97 /* ThumbnailsImpl.swift */,
|
|
||||||
);
|
|
||||||
path = Images;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
@@ -538,9 +523,6 @@
|
|||||||
files = (
|
files = (
|
||||||
65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */,
|
65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */,
|
||||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||||
FEAFA8732E4D42F4001E47FE /* Thumbhash.swift in Sources */,
|
|
||||||
FED3B1962E253E9B0030FD97 /* ThumbnailsImpl.swift in Sources */,
|
|
||||||
FED3B1972E253E9B0030FD97 /* Thumbnails.g.swift in Sources */,
|
|
||||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||||
65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */,
|
65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */,
|
||||||
);
|
);
|
||||||
@@ -667,7 +649,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 215;
|
CURRENT_PROJECT_VERSION = 213;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -811,7 +793,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 215;
|
CURRENT_PROJECT_VERSION = 213;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -841,7 +823,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 215;
|
CURRENT_PROJECT_VERSION = 213;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -875,7 +857,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 215;
|
CURRENT_PROJECT_VERSION = 213;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
@@ -918,7 +900,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 215;
|
CURRENT_PROJECT_VERSION = 213;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
@@ -958,7 +940,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 215;
|
CURRENT_PROJECT_VERSION = 213;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
@@ -997,7 +979,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 215;
|
CURRENT_PROJECT_VERSION = 213;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
@@ -1041,7 +1023,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 215;
|
CURRENT_PROJECT_VERSION = 213;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
@@ -1082,7 +1064,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 215;
|
CURRENT_PROJECT_VERSION = 213;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import UIKit
|
|||||||
|
|
||||||
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
|
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
|
||||||
NativeSyncApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: NativeSyncApiImpl())
|
NativeSyncApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: NativeSyncApiImpl())
|
||||||
ThumbnailApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: ThumbnailApiImpl())
|
|
||||||
|
|
||||||
BackgroundServicePlugin.setPluginRegistrantCallback { registry in
|
BackgroundServicePlugin.setPluginRegistrantCallback { registry in
|
||||||
if !registry.hasPlugin("org.cocoapods.path-provider-foundation") {
|
if !registry.hasPlugin("org.cocoapods.path-provider-foundation") {
|
||||||
|
|||||||
@@ -1,225 +0,0 @@
|
|||||||
// Copyright (c) 2023 Evan Wallace
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
||||||
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
// NOTE: Swift has an exponential-time type checker and compiling very simple
|
|
||||||
// expressions can easily take many seconds, especially when expressions involve
|
|
||||||
// numeric type constructors.
|
|
||||||
//
|
|
||||||
// This file deliberately breaks compound expressions up into separate variables
|
|
||||||
// to improve compile time even though this comes at the expense of readability.
|
|
||||||
// This is a known workaround for this deficiency in the Swift compiler.
|
|
||||||
//
|
|
||||||
// The following command is helpful when debugging Swift compile time issues:
|
|
||||||
//
|
|
||||||
// swiftc ThumbHash.swift -Xfrontend -debug-time-function-bodies
|
|
||||||
//
|
|
||||||
// These optimizations brought the compile time for this file from around 2.5
|
|
||||||
// seconds to around 250ms (10x faster).
|
|
||||||
|
|
||||||
// NOTE: Swift's debug-build performance of for-in loops over numeric ranges is
|
|
||||||
// really awful. Debug builds compile a very generic indexing iterator thing
|
|
||||||
// that makes many nested calls for every iteration, which makes debug-build
|
|
||||||
// performance crawl.
|
|
||||||
//
|
|
||||||
// This file deliberately avoids for-in loops that loop for more than a few
|
|
||||||
// times to improve debug-build run time even though this comes at the expense
|
|
||||||
// of readability. Similarly unsafe pointers are used instead of array getters
|
|
||||||
// to avoid unnecessary bounds checks, which have extra overhead in debug builds.
|
|
||||||
//
|
|
||||||
// These optimizations brought the run time to encode and decode 10 ThumbHashes
|
|
||||||
// in debug mode from 700ms to 70ms (10x faster).
|
|
||||||
|
|
||||||
// changed signature and allocation method to avoid automatic GC
|
|
||||||
func thumbHashToRGBA(hash: Data) -> (Int, Int, UnsafeMutableRawBufferPointer) {
|
|
||||||
// Read the constants
|
|
||||||
let h0 = UInt32(hash[0])
|
|
||||||
let h1 = UInt32(hash[1])
|
|
||||||
let h2 = UInt32(hash[2])
|
|
||||||
let h3 = UInt16(hash[3])
|
|
||||||
let h4 = UInt16(hash[4])
|
|
||||||
let header24 = h0 | (h1 << 8) | (h2 << 16)
|
|
||||||
let header16 = h3 | (h4 << 8)
|
|
||||||
let il_dc = header24 & 63
|
|
||||||
let ip_dc = (header24 >> 6) & 63
|
|
||||||
let iq_dc = (header24 >> 12) & 63
|
|
||||||
var l_dc = Float32(il_dc)
|
|
||||||
var p_dc = Float32(ip_dc)
|
|
||||||
var q_dc = Float32(iq_dc)
|
|
||||||
l_dc = l_dc / 63
|
|
||||||
p_dc = p_dc / 31.5 - 1
|
|
||||||
q_dc = q_dc / 31.5 - 1
|
|
||||||
let il_scale = (header24 >> 18) & 31
|
|
||||||
var l_scale = Float32(il_scale)
|
|
||||||
l_scale = l_scale / 31
|
|
||||||
let hasAlpha = (header24 >> 23) != 0
|
|
||||||
let ip_scale = (header16 >> 3) & 63
|
|
||||||
let iq_scale = (header16 >> 9) & 63
|
|
||||||
var p_scale = Float32(ip_scale)
|
|
||||||
var q_scale = Float32(iq_scale)
|
|
||||||
p_scale = p_scale / 63
|
|
||||||
q_scale = q_scale / 63
|
|
||||||
let isLandscape = (header16 >> 15) != 0
|
|
||||||
let lx16 = max(3, isLandscape ? hasAlpha ? 5 : 7 : header16 & 7)
|
|
||||||
let ly16 = max(3, isLandscape ? header16 & 7 : hasAlpha ? 5 : 7)
|
|
||||||
let lx = Int(lx16)
|
|
||||||
let ly = Int(ly16)
|
|
||||||
var a_dc = Float32(1)
|
|
||||||
var a_scale = Float32(1)
|
|
||||||
if hasAlpha {
|
|
||||||
let ia_dc = hash[5] & 15
|
|
||||||
let ia_scale = hash[5] >> 4
|
|
||||||
a_dc = Float32(ia_dc)
|
|
||||||
a_scale = Float32(ia_scale)
|
|
||||||
a_dc /= 15
|
|
||||||
a_scale /= 15
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the varying factors (boost saturation by 1.25x to compensate for quantization)
|
|
||||||
let ac_start = hasAlpha ? 6 : 5
|
|
||||||
var ac_index = 0
|
|
||||||
let decodeChannel = { (nx: Int, ny: Int, scale: Float32) -> [Float32] in
|
|
||||||
var ac: [Float32] = []
|
|
||||||
for cy in 0 ..< ny {
|
|
||||||
var cx = cy > 0 ? 0 : 1
|
|
||||||
while cx * ny < nx * (ny - cy) {
|
|
||||||
let iac = (hash[ac_start + (ac_index >> 1)] >> ((ac_index & 1) << 2)) & 15;
|
|
||||||
var fac = Float32(iac)
|
|
||||||
fac = (fac / 7.5 - 1) * scale
|
|
||||||
ac.append(fac)
|
|
||||||
ac_index += 1
|
|
||||||
cx += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ac
|
|
||||||
}
|
|
||||||
let l_ac = decodeChannel(lx, ly, l_scale)
|
|
||||||
let p_ac = decodeChannel(3, 3, p_scale * 1.25)
|
|
||||||
let q_ac = decodeChannel(3, 3, q_scale * 1.25)
|
|
||||||
let a_ac = hasAlpha ? decodeChannel(5, 5, a_scale) : []
|
|
||||||
|
|
||||||
// Decode using the DCT into RGB
|
|
||||||
let ratio = thumbHashToApproximateAspectRatio(hash: hash)
|
|
||||||
let fw = round(ratio > 1 ? 32 : 32 * ratio)
|
|
||||||
let fh = round(ratio > 1 ? 32 / ratio : 32)
|
|
||||||
let w = Int(fw)
|
|
||||||
let h = Int(fh)
|
|
||||||
let pointer = UnsafeMutableRawBufferPointer.allocate(
|
|
||||||
byteCount: w * h * 4,
|
|
||||||
alignment: MemoryLayout<UInt8>.alignment
|
|
||||||
)
|
|
||||||
var rgba = pointer.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
|
||||||
let cx_stop = max(lx, hasAlpha ? 5 : 3)
|
|
||||||
let cy_stop = max(ly, hasAlpha ? 5 : 3)
|
|
||||||
var fx = [Float32](repeating: 0, count: cx_stop)
|
|
||||||
var fy = [Float32](repeating: 0, count: cy_stop)
|
|
||||||
fx.withUnsafeMutableBytes { fx in
|
|
||||||
let fx = fx.baseAddress!.bindMemory(to: Float32.self, capacity: fx.count)
|
|
||||||
fy.withUnsafeMutableBytes { fy in
|
|
||||||
let fy = fy.baseAddress!.bindMemory(to: Float32.self, capacity: fy.count)
|
|
||||||
var y = 0
|
|
||||||
while y < h {
|
|
||||||
var x = 0
|
|
||||||
while x < w {
|
|
||||||
var l = l_dc
|
|
||||||
var p = p_dc
|
|
||||||
var q = q_dc
|
|
||||||
var a = a_dc
|
|
||||||
|
|
||||||
// Precompute the coefficients
|
|
||||||
var cx = 0
|
|
||||||
while cx < cx_stop {
|
|
||||||
let fw = Float32(w)
|
|
||||||
let fxx = Float32(x)
|
|
||||||
let fcx = Float32(cx)
|
|
||||||
fx[cx] = cos(Float32.pi / fw * (fxx + 0.5) * fcx)
|
|
||||||
cx += 1
|
|
||||||
}
|
|
||||||
var cy = 0
|
|
||||||
while cy < cy_stop {
|
|
||||||
let fh = Float32(h)
|
|
||||||
let fyy = Float32(y)
|
|
||||||
let fcy = Float32(cy)
|
|
||||||
fy[cy] = cos(Float32.pi / fh * (fyy + 0.5) * fcy)
|
|
||||||
cy += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode L
|
|
||||||
var j = 0
|
|
||||||
cy = 0
|
|
||||||
while cy < ly {
|
|
||||||
var cx = cy > 0 ? 0 : 1
|
|
||||||
let fy2 = fy[cy] * 2
|
|
||||||
while cx * ly < lx * (ly - cy) {
|
|
||||||
l += l_ac[j] * fx[cx] * fy2
|
|
||||||
j += 1
|
|
||||||
cx += 1
|
|
||||||
}
|
|
||||||
cy += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode P and Q
|
|
||||||
j = 0
|
|
||||||
cy = 0
|
|
||||||
while cy < 3 {
|
|
||||||
var cx = cy > 0 ? 0 : 1
|
|
||||||
let fy2 = fy[cy] * 2
|
|
||||||
while cx < 3 - cy {
|
|
||||||
let f = fx[cx] * fy2
|
|
||||||
p += p_ac[j] * f
|
|
||||||
q += q_ac[j] * f
|
|
||||||
j += 1
|
|
||||||
cx += 1
|
|
||||||
}
|
|
||||||
cy += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode A
|
|
||||||
if hasAlpha {
|
|
||||||
j = 0
|
|
||||||
cy = 0
|
|
||||||
while cy < 5 {
|
|
||||||
var cx = cy > 0 ? 0 : 1
|
|
||||||
let fy2 = fy[cy] * 2
|
|
||||||
while cx < 5 - cy {
|
|
||||||
a += a_ac[j] * fx[cx] * fy2
|
|
||||||
j += 1
|
|
||||||
cx += 1
|
|
||||||
}
|
|
||||||
cy += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to RGB
|
|
||||||
var b = l - 2 / 3 * p
|
|
||||||
var r = (3 * l - b + q) / 2
|
|
||||||
var g = r - q
|
|
||||||
r = max(0, 255 * min(1, r))
|
|
||||||
g = max(0, 255 * min(1, g))
|
|
||||||
b = max(0, 255 * min(1, b))
|
|
||||||
a = max(0, 255 * min(1, a))
|
|
||||||
rgba[0] = UInt8(r)
|
|
||||||
rgba[1] = UInt8(g)
|
|
||||||
rgba[2] = UInt8(b)
|
|
||||||
rgba[3] = UInt8(a)
|
|
||||||
rgba = rgba.advanced(by: 4)
|
|
||||||
x += 1
|
|
||||||
}
|
|
||||||
y += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (w, h, pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func thumbHashToApproximateAspectRatio(hash: Data) -> Float32 {
|
|
||||||
let header = hash[3]
|
|
||||||
let hasAlpha = (hash[2] & 0x80) != 0
|
|
||||||
let isLandscape = (hash[4] & 0x80) != 0
|
|
||||||
let lx = isLandscape ? hasAlpha ? 5 : 7 : header & 7
|
|
||||||
let ly = isLandscape ? header & 7 : hasAlpha ? 5 : 7
|
|
||||||
return Float32(lx) / Float32(ly)
|
|
||||||
}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
|
||||||
// See also: https://pub.dev/packages/pigeon
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
#if os(iOS)
|
|
||||||
import Flutter
|
|
||||||
#elseif os(macOS)
|
|
||||||
import FlutterMacOS
|
|
||||||
#else
|
|
||||||
#error("Unsupported platform.")
|
|
||||||
#endif
|
|
||||||
|
|
||||||
private func wrapResult(_ result: Any?) -> [Any?] {
|
|
||||||
return [result]
|
|
||||||
}
|
|
||||||
|
|
||||||
private func wrapError(_ error: Any) -> [Any?] {
|
|
||||||
if let pigeonError = error as? PigeonError {
|
|
||||||
return [
|
|
||||||
pigeonError.code,
|
|
||||||
pigeonError.message,
|
|
||||||
pigeonError.details,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
if let flutterError = error as? FlutterError {
|
|
||||||
return [
|
|
||||||
flutterError.code,
|
|
||||||
flutterError.message,
|
|
||||||
flutterError.details,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
"\(error)",
|
|
||||||
"\(type(of: error))",
|
|
||||||
"Stacktrace: \(Thread.callStackSymbols)",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
private func isNullish(_ value: Any?) -> Bool {
|
|
||||||
return value is NSNull || value == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private func nilOrValue<T>(_ value: Any?) -> T? {
|
|
||||||
if value is NSNull { return nil }
|
|
||||||
return value as! T?
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private class ThumbnailsPigeonCodecReader: FlutterStandardReader {
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ThumbnailsPigeonCodecWriter: FlutterStandardWriter {
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ThumbnailsPigeonCodecReaderWriter: FlutterStandardReaderWriter {
|
|
||||||
override func reader(with data: Data) -> FlutterStandardReader {
|
|
||||||
return ThumbnailsPigeonCodecReader(data: data)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func writer(with data: NSMutableData) -> FlutterStandardWriter {
|
|
||||||
return ThumbnailsPigeonCodecWriter(data: data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ThumbnailsPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
|
|
||||||
static let shared = ThumbnailsPigeonCodec(readerWriter: ThumbnailsPigeonCodecReaderWriter())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
|
||||||
protocol ThumbnailApi {
|
|
||||||
func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, completion: @escaping (Result<[String: Int64], Error>) -> Void)
|
|
||||||
func cancelImageRequest(requestId: Int64) throws
|
|
||||||
func getThumbhash(thumbhash: String, completion: @escaping (Result<[String: Int64], Error>) -> Void)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
|
||||||
class ThumbnailApiSetup {
|
|
||||||
static var codec: FlutterStandardMessageCodec { ThumbnailsPigeonCodec.shared }
|
|
||||||
/// Sets up an instance of `ThumbnailApi` to handle messages through the `binaryMessenger`.
|
|
||||||
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: ThumbnailApi?, messageChannelSuffix: String = "") {
|
|
||||||
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
|
|
||||||
let requestImageChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ThumbnailApi.requestImage\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
|
||||||
if let api = api {
|
|
||||||
requestImageChannel.setMessageHandler { message, reply in
|
|
||||||
let args = message as! [Any?]
|
|
||||||
let assetIdArg = args[0] as! String
|
|
||||||
let requestIdArg = args[1] as! Int64
|
|
||||||
let widthArg = args[2] as! Int64
|
|
||||||
let heightArg = args[3] as! Int64
|
|
||||||
let isVideoArg = args[4] as! Bool
|
|
||||||
api.requestImage(assetId: assetIdArg, requestId: requestIdArg, width: widthArg, height: heightArg, isVideo: isVideoArg) { result in
|
|
||||||
switch result {
|
|
||||||
case .success(let res):
|
|
||||||
reply(wrapResult(res))
|
|
||||||
case .failure(let error):
|
|
||||||
reply(wrapError(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
requestImageChannel.setMessageHandler(nil)
|
|
||||||
}
|
|
||||||
let cancelImageRequestChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ThumbnailApi.cancelImageRequest\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
|
||||||
if let api = api {
|
|
||||||
cancelImageRequestChannel.setMessageHandler { message, reply in
|
|
||||||
let args = message as! [Any?]
|
|
||||||
let requestIdArg = args[0] as! Int64
|
|
||||||
do {
|
|
||||||
try api.cancelImageRequest(requestId: requestIdArg)
|
|
||||||
reply(wrapResult(nil))
|
|
||||||
} catch {
|
|
||||||
reply(wrapError(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cancelImageRequestChannel.setMessageHandler(nil)
|
|
||||||
}
|
|
||||||
let getThumbhashChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ThumbnailApi.getThumbhash\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
|
||||||
if let api = api {
|
|
||||||
getThumbhashChannel.setMessageHandler { message, reply in
|
|
||||||
let args = message as! [Any?]
|
|
||||||
let thumbhashArg = args[0] as! String
|
|
||||||
api.getThumbhash(thumbhash: thumbhashArg) { result in
|
|
||||||
switch result {
|
|
||||||
case .success(let res):
|
|
||||||
reply(wrapResult(res))
|
|
||||||
case .failure(let error):
|
|
||||||
reply(wrapError(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
getThumbhashChannel.setMessageHandler(nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
import CryptoKit
|
|
||||||
import Flutter
|
|
||||||
import MobileCoreServices
|
|
||||||
import Photos
|
|
||||||
|
|
||||||
class Request {
|
|
||||||
weak var workItem: DispatchWorkItem?
|
|
||||||
var isCancelled = false
|
|
||||||
let callback: (Result<[String: Int64], any Error>) -> Void
|
|
||||||
|
|
||||||
init(callback: @escaping (Result<[String: Int64], any Error>) -> Void) {
|
|
||||||
self.callback = callback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ThumbnailApiImpl: ThumbnailApi {
|
|
||||||
private static let imageManager = PHImageManager.default()
|
|
||||||
private static let fetchOptions = {
|
|
||||||
let fetchOptions = PHFetchOptions()
|
|
||||||
fetchOptions.fetchLimit = 1
|
|
||||||
fetchOptions.wantsIncrementalChangeDetails = false
|
|
||||||
return fetchOptions
|
|
||||||
}()
|
|
||||||
private static let requestOptions = {
|
|
||||||
let requestOptions = PHImageRequestOptions()
|
|
||||||
requestOptions.isNetworkAccessAllowed = true
|
|
||||||
requestOptions.deliveryMode = .highQualityFormat
|
|
||||||
requestOptions.resizeMode = .fast
|
|
||||||
requestOptions.isSynchronous = true
|
|
||||||
requestOptions.version = .current
|
|
||||||
return requestOptions
|
|
||||||
}()
|
|
||||||
|
|
||||||
private static let assetQueue = DispatchQueue(label: "thumbnail.assets", qos: .userInitiated)
|
|
||||||
private static let requestQueue = DispatchQueue(label: "thumbnail.requests", qos: .userInitiated)
|
|
||||||
private static let cancelQueue = DispatchQueue(label: "thumbnail.cancellation", qos: .default)
|
|
||||||
private static let processingQueue = DispatchQueue(label: "thumbnail.processing", qos: .userInteractive, attributes: .concurrent)
|
|
||||||
|
|
||||||
private static let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
|
|
||||||
private static let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).rawValue
|
|
||||||
private static var requests = [Int64: Request]()
|
|
||||||
private static let cancelledResult = Result<[String: Int64], any Error>.success([:])
|
|
||||||
private static let concurrencySemaphore = DispatchSemaphore(value: ProcessInfo.processInfo.activeProcessorCount * 2)
|
|
||||||
private static let assetCache = {
|
|
||||||
let assetCache = NSCache<NSString, PHAsset>()
|
|
||||||
assetCache.countLimit = 10000
|
|
||||||
return assetCache
|
|
||||||
}()
|
|
||||||
|
|
||||||
func getThumbhash(thumbhash: String, completion: @escaping (Result<[String : Int64], any Error>) -> Void) {
|
|
||||||
Self.processingQueue.async {
|
|
||||||
guard let data = Data(base64Encoded: thumbhash)
|
|
||||||
else { return completion(.failure(PigeonError(code: "", message: "Invalid base64 string: \(thumbhash)", details: nil)))}
|
|
||||||
|
|
||||||
let (width, height, pointer) = thumbHashToRGBA(hash: data)
|
|
||||||
completion(.success(["pointer": Int64(Int(bitPattern: pointer.baseAddress)), "width": Int64(width), "height": Int64(height)]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, completion: @escaping (Result<[String: Int64], any Error>) -> Void) {
|
|
||||||
let request = Request(callback: completion)
|
|
||||||
let item = DispatchWorkItem {
|
|
||||||
if request.isCancelled {
|
|
||||||
return completion(Self.cancelledResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
Self.concurrencySemaphore.wait()
|
|
||||||
defer {
|
|
||||||
Self.concurrencySemaphore.signal()
|
|
||||||
}
|
|
||||||
|
|
||||||
if request.isCancelled {
|
|
||||||
return completion(Self.cancelledResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let asset = Self.requestAsset(assetId: assetId)
|
|
||||||
else {
|
|
||||||
Self.removeRequest(requestId: requestId)
|
|
||||||
completion(.failure(PigeonError(code: "", message: "Could not get asset data for \(assetId)", details: nil)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if request.isCancelled {
|
|
||||||
return completion(Self.cancelledResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
var image: UIImage?
|
|
||||||
Self.imageManager.requestImage(
|
|
||||||
for: asset,
|
|
||||||
targetSize: CGSize(width: Double(width), height: Double(height)),
|
|
||||||
contentMode: .aspectFill,
|
|
||||||
options: Self.requestOptions,
|
|
||||||
resultHandler: { (_image, info) -> Void in
|
|
||||||
image = _image
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if request.isCancelled {
|
|
||||||
return completion(Self.cancelledResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let image = image,
|
|
||||||
let cgImage = image.cgImage else {
|
|
||||||
Self.removeRequest(requestId: requestId)
|
|
||||||
return completion(.failure(PigeonError(code: "", message: "Could not get pixel data for \(assetId)", details: nil)))
|
|
||||||
}
|
|
||||||
|
|
||||||
let pointer = UnsafeMutableRawPointer.allocate(
|
|
||||||
byteCount: Int(cgImage.width) * Int(cgImage.height) * 4,
|
|
||||||
alignment: MemoryLayout<UInt8>.alignment
|
|
||||||
)
|
|
||||||
|
|
||||||
if request.isCancelled {
|
|
||||||
pointer.deallocate()
|
|
||||||
return completion(Self.cancelledResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let context = CGContext(
|
|
||||||
data: pointer,
|
|
||||||
width: cgImage.width,
|
|
||||||
height: cgImage.height,
|
|
||||||
bitsPerComponent: 8,
|
|
||||||
bytesPerRow: cgImage.width * 4,
|
|
||||||
space: Self.rgbColorSpace,
|
|
||||||
bitmapInfo: Self.bitmapInfo
|
|
||||||
) else {
|
|
||||||
pointer.deallocate()
|
|
||||||
Self.removeRequest(requestId: requestId)
|
|
||||||
return completion(.failure(PigeonError(code: "", message: "Could not create context for \(assetId)", details: nil)))
|
|
||||||
}
|
|
||||||
|
|
||||||
if request.isCancelled {
|
|
||||||
pointer.deallocate()
|
|
||||||
return completion(Self.cancelledResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
context.interpolationQuality = .none
|
|
||||||
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: cgImage.width, height: cgImage.height))
|
|
||||||
|
|
||||||
if request.isCancelled {
|
|
||||||
pointer.deallocate()
|
|
||||||
return completion(Self.cancelledResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
completion(.success(["pointer": Int64(Int(bitPattern: pointer)), "width": Int64(cgImage.width), "height": Int64(cgImage.height)]))
|
|
||||||
Self.removeRequest(requestId: requestId)
|
|
||||||
}
|
|
||||||
|
|
||||||
request.workItem = item
|
|
||||||
Self.addRequest(requestId: requestId, request: request)
|
|
||||||
Self.processingQueue.async(execute: item)
|
|
||||||
}
|
|
||||||
|
|
||||||
func cancelImageRequest(requestId: Int64) {
|
|
||||||
Self.cancelRequest(requestId: requestId)
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func addRequest(requestId: Int64, request: Request) -> Void {
|
|
||||||
requestQueue.sync { requests[requestId] = request }
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func removeRequest(requestId: Int64) -> Void {
|
|
||||||
requestQueue.sync { requests[requestId] = nil }
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func cancelRequest(requestId: Int64) -> Void {
|
|
||||||
requestQueue.async {
|
|
||||||
guard let request = requests.removeValue(forKey: requestId) else { return }
|
|
||||||
request.isCancelled = true
|
|
||||||
guard let item = request.workItem else { return }
|
|
||||||
if item.isCancelled {
|
|
||||||
cancelQueue.async { request.callback(Self.cancelledResult) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func requestAsset(assetId: String) -> PHAsset? {
|
|
||||||
var asset: PHAsset?
|
|
||||||
assetQueue.sync { asset = assetCache.object(forKey: assetId as NSString) }
|
|
||||||
if asset != nil { return asset }
|
|
||||||
|
|
||||||
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: Self.fetchOptions).firstObject
|
|
||||||
else { return nil }
|
|
||||||
assetQueue.async { assetCache.setObject(asset, forKey: assetId as NSString) }
|
|
||||||
return asset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.138.1</string>
|
<string>1.137.2</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>215</string>
|
<string>213</string>
|
||||||
<key>FLTEnableImpeller</key>
|
<key>FLTEnableImpeller</key>
|
||||||
<true />
|
<true />
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
@@ -184,4 +184,4 @@
|
|||||||
<string>We need local network permission to connect to the local server using IP address and
|
<string>We need local network permission to connect to the local server using IP address and
|
||||||
allow the casting feature to work</string>
|
allow the casting feature to work</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -22,7 +22,7 @@ platform :ios do
|
|||||||
path: "./Runner.xcodeproj",
|
path: "./Runner.xcodeproj",
|
||||||
)
|
)
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: "1.139.0"
|
version_number: "1.137.3"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const String kDownloadGroupLivePhoto = 'group_livephoto';
|
|||||||
|
|
||||||
// Timeline constants
|
// Timeline constants
|
||||||
const int kTimelineNoneSegmentSize = 120;
|
const int kTimelineNoneSegmentSize = 120;
|
||||||
const int kTimelineAssetLoadBatchSize = 1024;
|
const int kTimelineAssetLoadBatchSize = 256;
|
||||||
const int kTimelineAssetLoadOppositeSize = 64;
|
const int kTimelineAssetLoadOppositeSize = 64;
|
||||||
|
|
||||||
// Widget keys
|
// Widget keys
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ class RemoteAlbum {
|
|||||||
final AlbumAssetOrder order;
|
final AlbumAssetOrder order;
|
||||||
final int assetCount;
|
final int assetCount;
|
||||||
final String ownerName;
|
final String ownerName;
|
||||||
final bool isShared;
|
|
||||||
|
|
||||||
const RemoteAlbum({
|
const RemoteAlbum({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -37,7 +36,6 @@ class RemoteAlbum {
|
|||||||
required this.order,
|
required this.order,
|
||||||
required this.assetCount,
|
required this.assetCount,
|
||||||
required this.ownerName,
|
required this.ownerName,
|
||||||
required this.isShared,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -54,7 +52,6 @@ class RemoteAlbum {
|
|||||||
thumbnailAssetId: ${thumbnailAssetId ?? "<NA>"}
|
thumbnailAssetId: ${thumbnailAssetId ?? "<NA>"}
|
||||||
assetCount: $assetCount
|
assetCount: $assetCount
|
||||||
ownerName: $ownerName
|
ownerName: $ownerName
|
||||||
isShared: $isShared
|
|
||||||
}''';
|
}''';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,8 +69,7 @@ class RemoteAlbum {
|
|||||||
isActivityEnabled == other.isActivityEnabled &&
|
isActivityEnabled == other.isActivityEnabled &&
|
||||||
order == other.order &&
|
order == other.order &&
|
||||||
assetCount == other.assetCount &&
|
assetCount == other.assetCount &&
|
||||||
ownerName == other.ownerName &&
|
ownerName == other.ownerName;
|
||||||
isShared == other.isShared;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -88,8 +84,7 @@ class RemoteAlbum {
|
|||||||
isActivityEnabled.hashCode ^
|
isActivityEnabled.hashCode ^
|
||||||
order.hashCode ^
|
order.hashCode ^
|
||||||
assetCount.hashCode ^
|
assetCount.hashCode ^
|
||||||
ownerName.hashCode ^
|
ownerName.hashCode;
|
||||||
isShared.hashCode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoteAlbum copyWith({
|
RemoteAlbum copyWith({
|
||||||
@@ -104,7 +99,6 @@ class RemoteAlbum {
|
|||||||
AlbumAssetOrder? order,
|
AlbumAssetOrder? order,
|
||||||
int? assetCount,
|
int? assetCount,
|
||||||
String? ownerName,
|
String? ownerName,
|
||||||
bool? isShared,
|
|
||||||
}) {
|
}) {
|
||||||
return RemoteAlbum(
|
return RemoteAlbum(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -118,7 +112,6 @@ class RemoteAlbum {
|
|||||||
order: order ?? this.order,
|
order: order ?? this.order,
|
||||||
assetCount: assetCount ?? this.assetCount,
|
assetCount: assetCount ?? this.assetCount,
|
||||||
ownerName: ownerName ?? this.ownerName,
|
ownerName: ownerName ?? this.ownerName,
|
||||||
isShared: isShared ?? this.isShared,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import 'package:logging/logging.dart';
|
|||||||
/// via [IStoreRepository]
|
/// via [IStoreRepository]
|
||||||
class LogService {
|
class LogService {
|
||||||
final LogRepository _logRepository;
|
final LogRepository _logRepository;
|
||||||
final IStoreRepository _storeRepository;
|
final IsarStoreRepository _storeRepository;
|
||||||
|
|
||||||
final List<LogMessage> _msgBuffer = [];
|
final List<LogMessage> _msgBuffer = [];
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ class LogService {
|
|||||||
|
|
||||||
static Future<LogService> init({
|
static Future<LogService> init({
|
||||||
required LogRepository logRepository,
|
required LogRepository logRepository,
|
||||||
required IStoreRepository storeRepository,
|
required IsarStoreRepository storeRepository,
|
||||||
bool shouldBuffer = true,
|
bool shouldBuffer = true,
|
||||||
}) async {
|
}) async {
|
||||||
_instance ??= await create(
|
_instance ??= await create(
|
||||||
@@ -51,7 +51,7 @@ class LogService {
|
|||||||
|
|
||||||
static Future<LogService> create({
|
static Future<LogService> create({
|
||||||
required LogRepository logRepository,
|
required LogRepository logRepository,
|
||||||
required IStoreRepository storeRepository,
|
required IsarStoreRepository storeRepository,
|
||||||
bool shouldBuffer = true,
|
bool shouldBuffer = true,
|
||||||
}) async {
|
}) async {
|
||||||
final instance = LogService._(logRepository, storeRepository, shouldBuffer);
|
final instance = LogService._(logRepository, storeRepository, shouldBuffer);
|
||||||
@@ -92,7 +92,7 @@ class LogService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setLogLevel(LogLevel level) async {
|
Future<void> setLogLevel(LogLevel level) async {
|
||||||
await _storeRepository.upsert(StoreKey.logLevel, level.index);
|
await _storeRepository.insert(StoreKey.logLevel, level.index);
|
||||||
Logger.root.level = level.toLevel();
|
Logger.root.level = level.toLevel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:collection/collection.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';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
||||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||||
|
import 'package:immich_mobile/utils/remote_album.utils.dart';
|
||||||
|
|
||||||
class RemoteAlbumService {
|
class RemoteAlbumService {
|
||||||
final DriftRemoteAlbumRepository _repository;
|
final DriftRemoteAlbumRepository _repository;
|
||||||
@@ -26,21 +26,8 @@ class RemoteAlbumService {
|
|||||||
return _repository.get(albumId);
|
return _repository.get(albumId);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<RemoteAlbum>> sortAlbums(
|
List<RemoteAlbum> sortAlbums(List<RemoteAlbum> albums, RemoteAlbumSortMode sortMode, {bool isReverse = false}) {
|
||||||
List<RemoteAlbum> albums,
|
return sortMode.sortFn(albums, isReverse);
|
||||||
RemoteAlbumSortMode sortMode, {
|
|
||||||
bool isReverse = false,
|
|
||||||
}) async {
|
|
||||||
final List<RemoteAlbum> sorted = switch (sortMode) {
|
|
||||||
RemoteAlbumSortMode.created => albums.sortedBy((album) => album.createdAt),
|
|
||||||
RemoteAlbumSortMode.title => albums.sortedBy((album) => album.name),
|
|
||||||
RemoteAlbumSortMode.lastModified => albums.sortedBy((album) => album.updatedAt),
|
|
||||||
RemoteAlbumSortMode.assetCount => albums.sortedBy((album) => album.assetCount),
|
|
||||||
RemoteAlbumSortMode.mostRecent => await _sortByNewestAsset(albums),
|
|
||||||
RemoteAlbumSortMode.mostOldest => await _sortByOldestAsset(albums),
|
|
||||||
};
|
|
||||||
|
|
||||||
return (isReverse ? sorted.reversed : sorted).toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<RemoteAlbum> searchAlbums(
|
List<RemoteAlbum> searchAlbums(
|
||||||
@@ -156,60 +143,4 @@ class RemoteAlbumService {
|
|||||||
Future<int> getCount() {
|
Future<int> getCount() {
|
||||||
return _repository.getCount();
|
return _repository.getCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<RemoteAlbum>> _sortByNewestAsset(List<RemoteAlbum> albums) async {
|
|
||||||
// map album IDs to their newest asset dates
|
|
||||||
final Map<String, Future<DateTime?>> assetTimestampFutures = {};
|
|
||||||
for (final album in albums) {
|
|
||||||
assetTimestampFutures[album.id] = _repository.getNewestAssetTimestamp(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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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.reversed.toList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum RemoteAlbumSortMode {
|
|
||||||
title("library_page_sort_title"),
|
|
||||||
assetCount("library_page_sort_asset_count"),
|
|
||||||
lastModified("library_page_sort_last_modified"),
|
|
||||||
created("library_page_sort_created"),
|
|
||||||
mostRecent("sort_newest"),
|
|
||||||
mostOldest("sort_oldest");
|
|
||||||
|
|
||||||
final String key;
|
|
||||||
|
|
||||||
const RemoteAlbumSortMode(this.key);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'
|
|||||||
/// Provides access to a persistent key-value store with an in-memory cache.
|
/// Provides access to a persistent key-value store with an in-memory cache.
|
||||||
/// Listens for repository changes to keep the cache updated.
|
/// Listens for repository changes to keep the cache updated.
|
||||||
class StoreService {
|
class StoreService {
|
||||||
final IStoreRepository _storeRepository;
|
final IsarStoreRepository _storeRepository;
|
||||||
|
|
||||||
/// In-memory cache. Keys are [StoreKey.id]
|
/// In-memory cache. Keys are [StoreKey.id]
|
||||||
final Map<int, Object?> _cache = {};
|
final Map<int, Object?> _cache = {};
|
||||||
late final StreamSubscription<List<StoreDto>> _storeUpdateSubscription;
|
late final StreamSubscription<StoreDto> _storeUpdateSubscription;
|
||||||
|
|
||||||
StoreService._({required IStoreRepository isarStoreRepository}) : _storeRepository = isarStoreRepository;
|
StoreService._({required IsarStoreRepository storeRepository}) : _storeRepository = storeRepository;
|
||||||
|
|
||||||
// TODO: Temporary typedef to make minimal changes. Remove this and make the presentation layer access store through a provider
|
// TODO: Temporary typedef to make minimal changes. Remove this and make the presentation layer access store through a provider
|
||||||
static StoreService? _instance;
|
static StoreService? _instance;
|
||||||
@@ -24,29 +24,27 @@ class StoreService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Replace the implementation with the one from create after removing the typedef
|
// TODO: Replace the implementation with the one from create after removing the typedef
|
||||||
static Future<StoreService> init({required IStoreRepository storeRepository}) async {
|
static Future<StoreService> init({required IsarStoreRepository storeRepository}) async {
|
||||||
_instance ??= await create(storeRepository: storeRepository);
|
_instance ??= await create(storeRepository: storeRepository);
|
||||||
return _instance!;
|
return _instance!;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<StoreService> create({required IStoreRepository storeRepository}) async {
|
static Future<StoreService> create({required IsarStoreRepository storeRepository}) async {
|
||||||
final instance = StoreService._(isarStoreRepository: storeRepository);
|
final instance = StoreService._(storeRepository: storeRepository);
|
||||||
await instance.populateCache();
|
await instance._populateCache();
|
||||||
instance._storeUpdateSubscription = instance._listenForChange();
|
instance._storeUpdateSubscription = instance._listenForChange();
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> populateCache() async {
|
Future<void> _populateCache() async {
|
||||||
final storeValues = await _storeRepository.getAll();
|
final storeValues = await _storeRepository.getAll();
|
||||||
for (StoreDto storeValue in storeValues) {
|
for (StoreDto storeValue in storeValues) {
|
||||||
_cache[storeValue.key.id] = storeValue.value;
|
_cache[storeValue.key.id] = storeValue.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamSubscription<List<StoreDto>> _listenForChange() => _storeRepository.watchAll().listen((events) {
|
StreamSubscription<StoreDto> _listenForChange() => _storeRepository.watchAll().listen((event) {
|
||||||
for (final event in events) {
|
_cache[event.key.id] = event.value;
|
||||||
_cache[event.key.id] = event.value;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Disposes the store and cancels the subscription. To reuse the store call init() again
|
/// Disposes the store and cancels the subscription. To reuse the store call init() again
|
||||||
@@ -71,7 +69,7 @@ class StoreService {
|
|||||||
/// Stores the [value] for the [key]. Skips write if value hasn't changed.
|
/// Stores the [value] for the [key]. Skips write if value hasn't changed.
|
||||||
Future<void> put<U extends StoreKey<T>, T>(U key, T value) async {
|
Future<void> put<U extends StoreKey<T>, T>(U key, T value) async {
|
||||||
if (_cache[key.id] == value) return;
|
if (_cache[key.id] == value) return;
|
||||||
await _storeRepository.upsert(key, value);
|
await _storeRepository.insert(key, value);
|
||||||
_cache[key.id] = value;
|
_cache[key.id] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -169,36 +169,6 @@ class TimelineService {
|
|||||||
return _buffer.elementAt(index - _bufferOffset);
|
return _buffer.elementAt(index - _bufferOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets an asset at the given index, automatically loading the buffer if needed.
|
|
||||||
/// This is an async version that can handle out-of-range indices by loading the appropriate buffer.
|
|
||||||
Future<BaseAsset?> getAssetAsync(int index) async {
|
|
||||||
if (index < 0 || index >= _totalAssets) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasRange(index, 1)) {
|
|
||||||
return _buffer.elementAt(index - _bufferOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the buffer containing the requested index
|
|
||||||
try {
|
|
||||||
final assets = await loadAssets(index, 1);
|
|
||||||
return assets.isNotEmpty ? assets.first : null;
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Safely gets an asset at the given index without throwing a RangeError.
|
|
||||||
/// Returns null if the index is out of bounds or not currently in the buffer.
|
|
||||||
/// For automatic buffer loading, use getAssetAsync instead.
|
|
||||||
BaseAsset? getAssetSafe(int index) {
|
|
||||||
if (index < 0 || index >= _totalAssets || !hasRange(index, 1)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return _buffer.elementAt(index - _bufferOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
await _bucketSubscription?.cancel();
|
await _bucketSubscription?.cancel();
|
||||||
_bucketSubscription = null;
|
_bucketSubscription = null;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'package:drift/drift.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
part 'store.entity.g.dart';
|
part 'store.entity.g.dart';
|
||||||
@@ -13,13 +11,3 @@ class StoreValue {
|
|||||||
|
|
||||||
const StoreValue(this.id, {this.intValue, this.strValue});
|
const StoreValue(this.id, {this.intValue, this.strValue});
|
||||||
}
|
}
|
||||||
|
|
||||||
class StoreEntity extends Table with DriftDefaultsMixin {
|
|
||||||
IntColumn get id => integer()();
|
|
||||||
|
|
||||||
TextColumn get stringValue => text().nullable()();
|
|
||||||
IntColumn get intValue => integer().nullable()();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Set<Column> get primaryKey => {id};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,426 +0,0 @@
|
|||||||
// dart format width=80
|
|
||||||
// ignore_for_file: type=lint
|
|
||||||
import 'package:drift/drift.dart' as i0;
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'
|
|
||||||
as i1;
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/store.entity.dart' as i2;
|
|
||||||
|
|
||||||
typedef $$StoreEntityTableCreateCompanionBuilder =
|
|
||||||
i1.StoreEntityCompanion Function({
|
|
||||||
required int id,
|
|
||||||
i0.Value<String?> stringValue,
|
|
||||||
i0.Value<int?> intValue,
|
|
||||||
});
|
|
||||||
typedef $$StoreEntityTableUpdateCompanionBuilder =
|
|
||||||
i1.StoreEntityCompanion Function({
|
|
||||||
i0.Value<int> id,
|
|
||||||
i0.Value<String?> stringValue,
|
|
||||||
i0.Value<int?> intValue,
|
|
||||||
});
|
|
||||||
|
|
||||||
class $$StoreEntityTableFilterComposer
|
|
||||||
extends i0.Composer<i0.GeneratedDatabase, i1.$StoreEntityTable> {
|
|
||||||
$$StoreEntityTableFilterComposer({
|
|
||||||
required super.$db,
|
|
||||||
required super.$table,
|
|
||||||
super.joinBuilder,
|
|
||||||
super.$addJoinBuilderToRootComposer,
|
|
||||||
super.$removeJoinBuilderFromRootComposer,
|
|
||||||
});
|
|
||||||
i0.ColumnFilters<int> get id => $composableBuilder(
|
|
||||||
column: $table.id,
|
|
||||||
builder: (column) => i0.ColumnFilters(column),
|
|
||||||
);
|
|
||||||
|
|
||||||
i0.ColumnFilters<String> get stringValue => $composableBuilder(
|
|
||||||
column: $table.stringValue,
|
|
||||||
builder: (column) => i0.ColumnFilters(column),
|
|
||||||
);
|
|
||||||
|
|
||||||
i0.ColumnFilters<int> get intValue => $composableBuilder(
|
|
||||||
column: $table.intValue,
|
|
||||||
builder: (column) => i0.ColumnFilters(column),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class $$StoreEntityTableOrderingComposer
|
|
||||||
extends i0.Composer<i0.GeneratedDatabase, i1.$StoreEntityTable> {
|
|
||||||
$$StoreEntityTableOrderingComposer({
|
|
||||||
required super.$db,
|
|
||||||
required super.$table,
|
|
||||||
super.joinBuilder,
|
|
||||||
super.$addJoinBuilderToRootComposer,
|
|
||||||
super.$removeJoinBuilderFromRootComposer,
|
|
||||||
});
|
|
||||||
i0.ColumnOrderings<int> get id => $composableBuilder(
|
|
||||||
column: $table.id,
|
|
||||||
builder: (column) => i0.ColumnOrderings(column),
|
|
||||||
);
|
|
||||||
|
|
||||||
i0.ColumnOrderings<String> get stringValue => $composableBuilder(
|
|
||||||
column: $table.stringValue,
|
|
||||||
builder: (column) => i0.ColumnOrderings(column),
|
|
||||||
);
|
|
||||||
|
|
||||||
i0.ColumnOrderings<int> get intValue => $composableBuilder(
|
|
||||||
column: $table.intValue,
|
|
||||||
builder: (column) => i0.ColumnOrderings(column),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class $$StoreEntityTableAnnotationComposer
|
|
||||||
extends i0.Composer<i0.GeneratedDatabase, i1.$StoreEntityTable> {
|
|
||||||
$$StoreEntityTableAnnotationComposer({
|
|
||||||
required super.$db,
|
|
||||||
required super.$table,
|
|
||||||
super.joinBuilder,
|
|
||||||
super.$addJoinBuilderToRootComposer,
|
|
||||||
super.$removeJoinBuilderFromRootComposer,
|
|
||||||
});
|
|
||||||
i0.GeneratedColumn<int> get id =>
|
|
||||||
$composableBuilder(column: $table.id, builder: (column) => column);
|
|
||||||
|
|
||||||
i0.GeneratedColumn<String> get stringValue => $composableBuilder(
|
|
||||||
column: $table.stringValue,
|
|
||||||
builder: (column) => column,
|
|
||||||
);
|
|
||||||
|
|
||||||
i0.GeneratedColumn<int> get intValue =>
|
|
||||||
$composableBuilder(column: $table.intValue, builder: (column) => column);
|
|
||||||
}
|
|
||||||
|
|
||||||
class $$StoreEntityTableTableManager
|
|
||||||
extends
|
|
||||||
i0.RootTableManager<
|
|
||||||
i0.GeneratedDatabase,
|
|
||||||
i1.$StoreEntityTable,
|
|
||||||
i1.StoreEntityData,
|
|
||||||
i1.$$StoreEntityTableFilterComposer,
|
|
||||||
i1.$$StoreEntityTableOrderingComposer,
|
|
||||||
i1.$$StoreEntityTableAnnotationComposer,
|
|
||||||
$$StoreEntityTableCreateCompanionBuilder,
|
|
||||||
$$StoreEntityTableUpdateCompanionBuilder,
|
|
||||||
(
|
|
||||||
i1.StoreEntityData,
|
|
||||||
i0.BaseReferences<
|
|
||||||
i0.GeneratedDatabase,
|
|
||||||
i1.$StoreEntityTable,
|
|
||||||
i1.StoreEntityData
|
|
||||||
>,
|
|
||||||
),
|
|
||||||
i1.StoreEntityData,
|
|
||||||
i0.PrefetchHooks Function()
|
|
||||||
> {
|
|
||||||
$$StoreEntityTableTableManager(
|
|
||||||
i0.GeneratedDatabase db,
|
|
||||||
i1.$StoreEntityTable table,
|
|
||||||
) : super(
|
|
||||||
i0.TableManagerState(
|
|
||||||
db: db,
|
|
||||||
table: table,
|
|
||||||
createFilteringComposer: () =>
|
|
||||||
i1.$$StoreEntityTableFilterComposer($db: db, $table: table),
|
|
||||||
createOrderingComposer: () =>
|
|
||||||
i1.$$StoreEntityTableOrderingComposer($db: db, $table: table),
|
|
||||||
createComputedFieldComposer: () =>
|
|
||||||
i1.$$StoreEntityTableAnnotationComposer($db: db, $table: table),
|
|
||||||
updateCompanionCallback:
|
|
||||||
({
|
|
||||||
i0.Value<int> id = const i0.Value.absent(),
|
|
||||||
i0.Value<String?> stringValue = const i0.Value.absent(),
|
|
||||||
i0.Value<int?> intValue = const i0.Value.absent(),
|
|
||||||
}) => i1.StoreEntityCompanion(
|
|
||||||
id: id,
|
|
||||||
stringValue: stringValue,
|
|
||||||
intValue: intValue,
|
|
||||||
),
|
|
||||||
createCompanionCallback:
|
|
||||||
({
|
|
||||||
required int id,
|
|
||||||
i0.Value<String?> stringValue = const i0.Value.absent(),
|
|
||||||
i0.Value<int?> intValue = const i0.Value.absent(),
|
|
||||||
}) => i1.StoreEntityCompanion.insert(
|
|
||||||
id: id,
|
|
||||||
stringValue: stringValue,
|
|
||||||
intValue: intValue,
|
|
||||||
),
|
|
||||||
withReferenceMapper: (p0) => p0
|
|
||||||
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
|
|
||||||
.toList(),
|
|
||||||
prefetchHooksCallback: null,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
typedef $$StoreEntityTableProcessedTableManager =
|
|
||||||
i0.ProcessedTableManager<
|
|
||||||
i0.GeneratedDatabase,
|
|
||||||
i1.$StoreEntityTable,
|
|
||||||
i1.StoreEntityData,
|
|
||||||
i1.$$StoreEntityTableFilterComposer,
|
|
||||||
i1.$$StoreEntityTableOrderingComposer,
|
|
||||||
i1.$$StoreEntityTableAnnotationComposer,
|
|
||||||
$$StoreEntityTableCreateCompanionBuilder,
|
|
||||||
$$StoreEntityTableUpdateCompanionBuilder,
|
|
||||||
(
|
|
||||||
i1.StoreEntityData,
|
|
||||||
i0.BaseReferences<
|
|
||||||
i0.GeneratedDatabase,
|
|
||||||
i1.$StoreEntityTable,
|
|
||||||
i1.StoreEntityData
|
|
||||||
>,
|
|
||||||
),
|
|
||||||
i1.StoreEntityData,
|
|
||||||
i0.PrefetchHooks Function()
|
|
||||||
>;
|
|
||||||
|
|
||||||
class $StoreEntityTable extends i2.StoreEntity
|
|
||||||
with i0.TableInfo<$StoreEntityTable, i1.StoreEntityData> {
|
|
||||||
@override
|
|
||||||
final i0.GeneratedDatabase attachedDatabase;
|
|
||||||
final String? _alias;
|
|
||||||
$StoreEntityTable(this.attachedDatabase, [this._alias]);
|
|
||||||
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
|
|
||||||
@override
|
|
||||||
late final i0.GeneratedColumn<int> id = i0.GeneratedColumn<int>(
|
|
||||||
'id',
|
|
||||||
aliasedName,
|
|
||||||
false,
|
|
||||||
type: i0.DriftSqlType.int,
|
|
||||||
requiredDuringInsert: true,
|
|
||||||
);
|
|
||||||
static const i0.VerificationMeta _stringValueMeta = const i0.VerificationMeta(
|
|
||||||
'stringValue',
|
|
||||||
);
|
|
||||||
@override
|
|
||||||
late final i0.GeneratedColumn<String> stringValue =
|
|
||||||
i0.GeneratedColumn<String>(
|
|
||||||
'string_value',
|
|
||||||
aliasedName,
|
|
||||||
true,
|
|
||||||
type: i0.DriftSqlType.string,
|
|
||||||
requiredDuringInsert: false,
|
|
||||||
);
|
|
||||||
static const i0.VerificationMeta _intValueMeta = const i0.VerificationMeta(
|
|
||||||
'intValue',
|
|
||||||
);
|
|
||||||
@override
|
|
||||||
late final i0.GeneratedColumn<int> intValue = i0.GeneratedColumn<int>(
|
|
||||||
'int_value',
|
|
||||||
aliasedName,
|
|
||||||
true,
|
|
||||||
type: i0.DriftSqlType.int,
|
|
||||||
requiredDuringInsert: false,
|
|
||||||
);
|
|
||||||
@override
|
|
||||||
List<i0.GeneratedColumn> get $columns => [id, stringValue, intValue];
|
|
||||||
@override
|
|
||||||
String get aliasedName => _alias ?? actualTableName;
|
|
||||||
@override
|
|
||||||
String get actualTableName => $name;
|
|
||||||
static const String $name = 'store_entity';
|
|
||||||
@override
|
|
||||||
i0.VerificationContext validateIntegrity(
|
|
||||||
i0.Insertable<i1.StoreEntityData> instance, {
|
|
||||||
bool isInserting = false,
|
|
||||||
}) {
|
|
||||||
final context = i0.VerificationContext();
|
|
||||||
final data = instance.toColumns(true);
|
|
||||||
if (data.containsKey('id')) {
|
|
||||||
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
|
||||||
} else if (isInserting) {
|
|
||||||
context.missing(_idMeta);
|
|
||||||
}
|
|
||||||
if (data.containsKey('string_value')) {
|
|
||||||
context.handle(
|
|
||||||
_stringValueMeta,
|
|
||||||
stringValue.isAcceptableOrUnknown(
|
|
||||||
data['string_value']!,
|
|
||||||
_stringValueMeta,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (data.containsKey('int_value')) {
|
|
||||||
context.handle(
|
|
||||||
_intValueMeta,
|
|
||||||
intValue.isAcceptableOrUnknown(data['int_value']!, _intValueMeta),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Set<i0.GeneratedColumn> get $primaryKey => {id};
|
|
||||||
@override
|
|
||||||
i1.StoreEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
|
||||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
|
||||||
return i1.StoreEntityData(
|
|
||||||
id: attachedDatabase.typeMapping.read(
|
|
||||||
i0.DriftSqlType.int,
|
|
||||||
data['${effectivePrefix}id'],
|
|
||||||
)!,
|
|
||||||
stringValue: attachedDatabase.typeMapping.read(
|
|
||||||
i0.DriftSqlType.string,
|
|
||||||
data['${effectivePrefix}string_value'],
|
|
||||||
),
|
|
||||||
intValue: attachedDatabase.typeMapping.read(
|
|
||||||
i0.DriftSqlType.int,
|
|
||||||
data['${effectivePrefix}int_value'],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
$StoreEntityTable createAlias(String alias) {
|
|
||||||
return $StoreEntityTable(attachedDatabase, alias);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool get withoutRowId => true;
|
|
||||||
@override
|
|
||||||
bool get isStrict => true;
|
|
||||||
}
|
|
||||||
|
|
||||||
class StoreEntityData extends i0.DataClass
|
|
||||||
implements i0.Insertable<i1.StoreEntityData> {
|
|
||||||
final int id;
|
|
||||||
final String? stringValue;
|
|
||||||
final int? intValue;
|
|
||||||
const StoreEntityData({required this.id, this.stringValue, this.intValue});
|
|
||||||
@override
|
|
||||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
|
||||||
final map = <String, i0.Expression>{};
|
|
||||||
map['id'] = i0.Variable<int>(id);
|
|
||||||
if (!nullToAbsent || stringValue != null) {
|
|
||||||
map['string_value'] = i0.Variable<String>(stringValue);
|
|
||||||
}
|
|
||||||
if (!nullToAbsent || intValue != null) {
|
|
||||||
map['int_value'] = i0.Variable<int>(intValue);
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
factory StoreEntityData.fromJson(
|
|
||||||
Map<String, dynamic> json, {
|
|
||||||
i0.ValueSerializer? serializer,
|
|
||||||
}) {
|
|
||||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
|
||||||
return StoreEntityData(
|
|
||||||
id: serializer.fromJson<int>(json['id']),
|
|
||||||
stringValue: serializer.fromJson<String?>(json['stringValue']),
|
|
||||||
intValue: serializer.fromJson<int?>(json['intValue']),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@override
|
|
||||||
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
|
|
||||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
|
||||||
return <String, dynamic>{
|
|
||||||
'id': serializer.toJson<int>(id),
|
|
||||||
'stringValue': serializer.toJson<String?>(stringValue),
|
|
||||||
'intValue': serializer.toJson<int?>(intValue),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
i1.StoreEntityData copyWith({
|
|
||||||
int? id,
|
|
||||||
i0.Value<String?> stringValue = const i0.Value.absent(),
|
|
||||||
i0.Value<int?> intValue = const i0.Value.absent(),
|
|
||||||
}) => i1.StoreEntityData(
|
|
||||||
id: id ?? this.id,
|
|
||||||
stringValue: stringValue.present ? stringValue.value : this.stringValue,
|
|
||||||
intValue: intValue.present ? intValue.value : this.intValue,
|
|
||||||
);
|
|
||||||
StoreEntityData copyWithCompanion(i1.StoreEntityCompanion data) {
|
|
||||||
return StoreEntityData(
|
|
||||||
id: data.id.present ? data.id.value : this.id,
|
|
||||||
stringValue: data.stringValue.present
|
|
||||||
? data.stringValue.value
|
|
||||||
: this.stringValue,
|
|
||||||
intValue: data.intValue.present ? data.intValue.value : this.intValue,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return (StringBuffer('StoreEntityData(')
|
|
||||||
..write('id: $id, ')
|
|
||||||
..write('stringValue: $stringValue, ')
|
|
||||||
..write('intValue: $intValue')
|
|
||||||
..write(')'))
|
|
||||||
.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => Object.hash(id, stringValue, intValue);
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
(other is i1.StoreEntityData &&
|
|
||||||
other.id == this.id &&
|
|
||||||
other.stringValue == this.stringValue &&
|
|
||||||
other.intValue == this.intValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
class StoreEntityCompanion extends i0.UpdateCompanion<i1.StoreEntityData> {
|
|
||||||
final i0.Value<int> id;
|
|
||||||
final i0.Value<String?> stringValue;
|
|
||||||
final i0.Value<int?> intValue;
|
|
||||||
const StoreEntityCompanion({
|
|
||||||
this.id = const i0.Value.absent(),
|
|
||||||
this.stringValue = const i0.Value.absent(),
|
|
||||||
this.intValue = const i0.Value.absent(),
|
|
||||||
});
|
|
||||||
StoreEntityCompanion.insert({
|
|
||||||
required int id,
|
|
||||||
this.stringValue = const i0.Value.absent(),
|
|
||||||
this.intValue = const i0.Value.absent(),
|
|
||||||
}) : id = i0.Value(id);
|
|
||||||
static i0.Insertable<i1.StoreEntityData> custom({
|
|
||||||
i0.Expression<int>? id,
|
|
||||||
i0.Expression<String>? stringValue,
|
|
||||||
i0.Expression<int>? intValue,
|
|
||||||
}) {
|
|
||||||
return i0.RawValuesInsertable({
|
|
||||||
if (id != null) 'id': id,
|
|
||||||
if (stringValue != null) 'string_value': stringValue,
|
|
||||||
if (intValue != null) 'int_value': intValue,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
i1.StoreEntityCompanion copyWith({
|
|
||||||
i0.Value<int>? id,
|
|
||||||
i0.Value<String?>? stringValue,
|
|
||||||
i0.Value<int?>? intValue,
|
|
||||||
}) {
|
|
||||||
return i1.StoreEntityCompanion(
|
|
||||||
id: id ?? this.id,
|
|
||||||
stringValue: stringValue ?? this.stringValue,
|
|
||||||
intValue: intValue ?? this.intValue,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
|
||||||
final map = <String, i0.Expression>{};
|
|
||||||
if (id.present) {
|
|
||||||
map['id'] = i0.Variable<int>(id.value);
|
|
||||||
}
|
|
||||||
if (stringValue.present) {
|
|
||||||
map['string_value'] = i0.Variable<String>(stringValue.value);
|
|
||||||
}
|
|
||||||
if (intValue.present) {
|
|
||||||
map['int_value'] = i0.Variable<int>(intValue.value);
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return (StringBuffer('StoreEntityCompanion(')
|
|
||||||
..write('id: $id, ')
|
|
||||||
..write('stringValue: $stringValue, ')
|
|
||||||
..write('intValue: $intValue')
|
|
||||||
..write(')'))
|
|
||||||
.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:ffi';
|
|
||||||
import 'dart:io';
|
|
||||||
import 'dart:ui' as ui;
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:ffi/ffi.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
|
||||||
import 'package:immich_mobile/providers/image/cache/remote_image_cache_manager.dart';
|
|
||||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
|
|
||||||
part 'local_image_request.dart';
|
|
||||||
part 'thumbhash_image_request.dart';
|
|
||||||
part 'remote_image_request.dart';
|
|
||||||
|
|
||||||
abstract class ImageRequest {
|
|
||||||
static int _nextRequestId = 0;
|
|
||||||
|
|
||||||
final int requestId = _nextRequestId++;
|
|
||||||
bool _isCancelled = false;
|
|
||||||
|
|
||||||
get isCancelled => _isCancelled;
|
|
||||||
|
|
||||||
ImageRequest();
|
|
||||||
|
|
||||||
Future<ImageInfo?> load(ImageDecoderCallback decode, {double scale = 1.0});
|
|
||||||
|
|
||||||
void cancel() {
|
|
||||||
if (_isCancelled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_isCancelled = true;
|
|
||||||
return _onCancelled();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onCancelled();
|
|
||||||
|
|
||||||
Future<ui.FrameInfo?> _fromPlatformImage(Map<String, int> info) async {
|
|
||||||
final address = info['pointer'];
|
|
||||||
if (address == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final pointer = Pointer<Uint8>.fromAddress(address);
|
|
||||||
if (_isCancelled) {
|
|
||||||
malloc.free(pointer);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final int actualWidth;
|
|
||||||
final int actualHeight;
|
|
||||||
final int actualSize;
|
|
||||||
final ui.ImmutableBuffer buffer;
|
|
||||||
try {
|
|
||||||
actualWidth = info['width']!;
|
|
||||||
actualHeight = info['height']!;
|
|
||||||
actualSize = actualWidth * actualHeight * 4;
|
|
||||||
buffer = await ImmutableBuffer.fromUint8List(pointer.asTypedList(actualSize));
|
|
||||||
} finally {
|
|
||||||
malloc.free(pointer);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_isCancelled) {
|
|
||||||
buffer.dispose();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final descriptor = ui.ImageDescriptor.raw(
|
|
||||||
buffer,
|
|
||||||
width: actualWidth,
|
|
||||||
height: actualHeight,
|
|
||||||
pixelFormat: ui.PixelFormat.rgba8888,
|
|
||||||
);
|
|
||||||
final codec = await descriptor.instantiateCodec();
|
|
||||||
if (_isCancelled) {
|
|
||||||
buffer.dispose();
|
|
||||||
descriptor.dispose();
|
|
||||||
codec.dispose();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await codec.getNextFrame();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
part of 'image_request.dart';
|
|
||||||
|
|
||||||
class LocalImageRequest extends ImageRequest {
|
|
||||||
final String localId;
|
|
||||||
final int width;
|
|
||||||
final int height;
|
|
||||||
final AssetType assetType;
|
|
||||||
|
|
||||||
LocalImageRequest({required this.localId, required ui.Size size, required this.assetType})
|
|
||||||
: width = size.width.toInt(),
|
|
||||||
height = size.height.toInt();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<ImageInfo?> load(ImageDecoderCallback decode, {double scale = 1.0}) async {
|
|
||||||
if (_isCancelled) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Map<String, int> info = await thumbnailApi.requestImage(
|
|
||||||
localId,
|
|
||||||
requestId: requestId,
|
|
||||||
width: width,
|
|
||||||
height: height,
|
|
||||||
isVideo: assetType == AssetType.video,
|
|
||||||
);
|
|
||||||
|
|
||||||
final frame = await _fromPlatformImage(info);
|
|
||||||
return frame == null ? null : ImageInfo(image: frame.image, scale: scale);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> _onCancelled() {
|
|
||||||
return thumbnailApi.cancelImageRequest(requestId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
part of 'image_request.dart';
|
|
||||||
|
|
||||||
class RemoteImageRequest extends ImageRequest {
|
|
||||||
static final log = Logger('RemoteImageRequest');
|
|
||||||
static final client = HttpClient()..maxConnectionsPerHost = 16;
|
|
||||||
final RemoteCacheManager? cacheManager;
|
|
||||||
final String uri;
|
|
||||||
final Map<String, String> headers;
|
|
||||||
HttpClientRequest? _request;
|
|
||||||
|
|
||||||
RemoteImageRequest({required this.uri, required this.headers, this.cacheManager});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<ImageInfo?> load(ImageDecoderCallback decode, {double scale = 1.0}) async {
|
|
||||||
if (_isCancelled) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: the cache manager makes everything sequential with its DB calls and its operations cannot be cancelled,
|
|
||||||
// so it ends up being a bottleneck. We only prefer fetching from it when it can skip the DB call.
|
|
||||||
final cachedFileImage = await _loadCachedFile(uri, decode, scale, inMemoryOnly: true);
|
|
||||||
if (cachedFileImage != null) {
|
|
||||||
return cachedFileImage;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final buffer = await _downloadImage(uri);
|
|
||||||
if (buffer == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await _decodeBuffer(buffer, decode, scale);
|
|
||||||
} catch (e) {
|
|
||||||
if (_isCancelled) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final cachedFileImage = await _loadCachedFile(uri, decode, scale, inMemoryOnly: false);
|
|
||||||
if (cachedFileImage != null) {
|
|
||||||
return cachedFileImage;
|
|
||||||
}
|
|
||||||
|
|
||||||
rethrow;
|
|
||||||
} finally {
|
|
||||||
_request = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<ImmutableBuffer?> _downloadImage(String url) async {
|
|
||||||
if (_isCancelled) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final request = _request = await client.getUrl(Uri.parse(url));
|
|
||||||
if (_isCancelled) {
|
|
||||||
request.abort();
|
|
||||||
return _request = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final entry in headers.entries) {
|
|
||||||
request.headers.set(entry.key, entry.value);
|
|
||||||
}
|
|
||||||
final response = await request.close();
|
|
||||||
if (_isCancelled) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final bytes = Uint8List(response.contentLength);
|
|
||||||
int offset = 0;
|
|
||||||
final subscription = response.listen((List<int> chunk) {
|
|
||||||
// this is important to break the response stream if the request is cancelled
|
|
||||||
if (_isCancelled) {
|
|
||||||
throw StateError('Cancelled request');
|
|
||||||
}
|
|
||||||
bytes.setAll(offset, chunk);
|
|
||||||
offset += chunk.length;
|
|
||||||
}, cancelOnError: true);
|
|
||||||
cacheManager?.putStreamedFile(url, response);
|
|
||||||
await subscription.asFuture();
|
|
||||||
return await ImmutableBuffer.fromUint8List(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<ImageInfo?> _loadCachedFile(
|
|
||||||
String url,
|
|
||||||
ImageDecoderCallback decode,
|
|
||||||
double scale, {
|
|
||||||
required bool inMemoryOnly,
|
|
||||||
}) async {
|
|
||||||
final cacheManager = this.cacheManager;
|
|
||||||
if (_isCancelled || cacheManager == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final file = await (inMemoryOnly ? cacheManager.getFileFromMemory(url) : cacheManager.getFileFromCache(url));
|
|
||||||
if (_isCancelled || file == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final buffer = await ImmutableBuffer.fromFilePath(file.file.path);
|
|
||||||
return await _decodeBuffer(buffer, decode, scale);
|
|
||||||
} catch (e) {
|
|
||||||
log.severe('Failed to decode cached image', e);
|
|
||||||
_evictFile(url);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _evictFile(String url) async {
|
|
||||||
try {
|
|
||||||
await cacheManager?.removeFile(url);
|
|
||||||
} catch (e) {
|
|
||||||
log.severe('Failed to remove cached image', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<ImageInfo?> _decodeBuffer(ImmutableBuffer buffer, ImageDecoderCallback decode, scale) async {
|
|
||||||
if (_isCancelled) {
|
|
||||||
buffer.dispose();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final codec = await decode(buffer);
|
|
||||||
if (_isCancelled) {
|
|
||||||
buffer.dispose();
|
|
||||||
codec.dispose();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final frame = await codec.getNextFrame();
|
|
||||||
return ImageInfo(image: frame.image, scale: scale);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void _onCancelled() {
|
|
||||||
_request?.abort();
|
|
||||||
_request = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
part of 'image_request.dart';
|
|
||||||
|
|
||||||
class ThumbhashImageRequest extends ImageRequest {
|
|
||||||
final String thumbhash;
|
|
||||||
|
|
||||||
ThumbhashImageRequest({required this.thumbhash});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<ImageInfo?> load(ImageDecoderCallback decode, {double scale = 1.0}) async {
|
|
||||||
if (_isCancelled) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Map<String, int> info = await thumbnailApi.getThumbhash(thumbhash);
|
|
||||||
final frame = await _fromPlatformImage(info);
|
|
||||||
return frame == null ? null : ImageInfo(image: frame.image, scale: scale);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void _onCancelled() {}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
|
class AssetMediaRepository {
|
||||||
|
const AssetMediaRepository();
|
||||||
|
|
||||||
|
Future<Uint8List?> getThumbnail(String id, {int quality = 80, Size size = const Size.square(256)}) => AssetEntity(
|
||||||
|
id: id,
|
||||||
|
// The below fields are not used in thumbnailDataWithSize but are required
|
||||||
|
// to create an AssetEntity instance. It is faster to create a dummy AssetEntity
|
||||||
|
// instance than to fetch the asset from the device first.
|
||||||
|
typeInt: AssetType.image.index,
|
||||||
|
width: size.width.toInt(),
|
||||||
|
height: size.height.toInt(),
|
||||||
|
).thumbnailDataWithSize(ThumbnailSize(size.width.toInt(), size.height.toInt()), quality: quality);
|
||||||
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
@@ -137,22 +135,4 @@ class DriftBackupRepository extends DriftDatabaseRepository {
|
|||||||
|
|
||||||
return query.map((localAsset) => localAsset.toDto()).get();
|
return query.map((localAsset) => localAsset.toDto()).get();
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureOr<List<LocalAlbum>> getSourceAlbums(String localAssetId) {
|
|
||||||
final query = _db.localAlbumEntity.select()
|
|
||||||
..where(
|
|
||||||
(lae) =>
|
|
||||||
existsQuery(
|
|
||||||
_db.localAlbumAssetEntity.selectOnly()
|
|
||||||
..addColumns([_db.localAlbumAssetEntity.albumId])
|
|
||||||
..where(
|
|
||||||
_db.localAlbumAssetEntity.albumId.equalsExp(lae.id) &
|
|
||||||
_db.localAlbumAssetEntity.assetId.equals(localAssetId),
|
|
||||||
),
|
|
||||||
) &
|
|
||||||
lae.backupSelection.equalsValue(BackupSelection.selected),
|
|
||||||
)
|
|
||||||
..orderBy([(lae) => OrderingTerm.asc(lae.name)]);
|
|
||||||
return query.map((localAlbum) => localAlbum.toDto()).get();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.
|
|||||||
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/stack.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/stack.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.steps.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.steps.dart';
|
||||||
@@ -59,7 +58,6 @@ class IsarDatabaseRepository implements IDatabaseRepository {
|
|||||||
StackEntity,
|
StackEntity,
|
||||||
PersonEntity,
|
PersonEntity,
|
||||||
AssetFaceEntity,
|
AssetFaceEntity,
|
||||||
StoreEntity,
|
|
||||||
],
|
],
|
||||||
include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'},
|
include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'},
|
||||||
)
|
)
|
||||||
@@ -68,7 +66,7 @@ class Drift extends $Drift implements IDatabaseRepository {
|
|||||||
: super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true)));
|
: super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true)));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 8;
|
int get schemaVersion => 7;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
@@ -120,9 +118,6 @@ class Drift extends $Drift implements IDatabaseRepository {
|
|||||||
from6To7: (m, v7) async {
|
from6To7: (m, v7) async {
|
||||||
await m.createIndex(v7.idxLatLng);
|
await m.createIndex(v7.idxLatLng);
|
||||||
},
|
},
|
||||||
from7To8: (m, v8) async {
|
|
||||||
await m.create(v8.storeEntity);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+4
-10
@@ -33,11 +33,9 @@ import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'
|
|||||||
as i15;
|
as i15;
|
||||||
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart'
|
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart'
|
||||||
as i16;
|
as i16;
|
||||||
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'
|
|
||||||
as i17;
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
|
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
|
||||||
as i18;
|
as i17;
|
||||||
import 'package:drift/internal/modular.dart' as i19;
|
import 'package:drift/internal/modular.dart' as i18;
|
||||||
|
|
||||||
abstract class $Drift extends i0.GeneratedDatabase {
|
abstract class $Drift extends i0.GeneratedDatabase {
|
||||||
$Drift(i0.QueryExecutor e) : super(e);
|
$Drift(i0.QueryExecutor e) : super(e);
|
||||||
@@ -71,10 +69,9 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
|||||||
late final i15.$PersonEntityTable personEntity = i15.$PersonEntityTable(this);
|
late final i15.$PersonEntityTable personEntity = i15.$PersonEntityTable(this);
|
||||||
late final i16.$AssetFaceEntityTable assetFaceEntity = i16
|
late final i16.$AssetFaceEntityTable assetFaceEntity = i16
|
||||||
.$AssetFaceEntityTable(this);
|
.$AssetFaceEntityTable(this);
|
||||||
late final i17.$StoreEntityTable storeEntity = i17.$StoreEntityTable(this);
|
i17.MergedAssetDrift get mergedAssetDrift => i18.ReadDatabaseContainer(
|
||||||
i18.MergedAssetDrift get mergedAssetDrift => i19.ReadDatabaseContainer(
|
|
||||||
this,
|
this,
|
||||||
).accessor<i18.MergedAssetDrift>(i18.MergedAssetDrift.new);
|
).accessor<i17.MergedAssetDrift>(i17.MergedAssetDrift.new);
|
||||||
@override
|
@override
|
||||||
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
|
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
|
||||||
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
|
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
|
||||||
@@ -101,7 +98,6 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
|||||||
memoryAssetEntity,
|
memoryAssetEntity,
|
||||||
personEntity,
|
personEntity,
|
||||||
assetFaceEntity,
|
assetFaceEntity,
|
||||||
storeEntity,
|
|
||||||
i9.idxLatLng,
|
i9.idxLatLng,
|
||||||
];
|
];
|
||||||
@override
|
@override
|
||||||
@@ -317,6 +313,4 @@ class $DriftManager {
|
|||||||
i15.$$PersonEntityTableTableManager(_db, _db.personEntity);
|
i15.$$PersonEntityTableTableManager(_db, _db.personEntity);
|
||||||
i16.$$AssetFaceEntityTableTableManager get assetFaceEntity =>
|
i16.$$AssetFaceEntityTableTableManager get assetFaceEntity =>
|
||||||
i16.$$AssetFaceEntityTableTableManager(_db, _db.assetFaceEntity);
|
i16.$$AssetFaceEntityTableTableManager(_db, _db.assetFaceEntity);
|
||||||
i17.$$StoreEntityTableTableManager get storeEntity =>
|
|
||||||
i17.$$StoreEntityTableTableManager(_db, _db.storeEntity);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3049,392 +3049,6 @@ final class Schema7 extends i0.VersionedSchema {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final class Schema8 extends i0.VersionedSchema {
|
|
||||||
Schema8({required super.database}) : super(version: 8);
|
|
||||||
@override
|
|
||||||
late final List<i1.DatabaseSchemaEntity> entities = [
|
|
||||||
userEntity,
|
|
||||||
remoteAssetEntity,
|
|
||||||
stackEntity,
|
|
||||||
localAssetEntity,
|
|
||||||
localAlbumEntity,
|
|
||||||
localAlbumAssetEntity,
|
|
||||||
idxLocalAssetChecksum,
|
|
||||||
idxRemoteAssetOwnerChecksum,
|
|
||||||
uQRemoteAssetsOwnerChecksum,
|
|
||||||
uQRemoteAssetsOwnerLibraryChecksum,
|
|
||||||
idxRemoteAssetChecksum,
|
|
||||||
userMetadataEntity,
|
|
||||||
partnerEntity,
|
|
||||||
remoteExifEntity,
|
|
||||||
remoteAlbumEntity,
|
|
||||||
remoteAlbumAssetEntity,
|
|
||||||
remoteAlbumUserEntity,
|
|
||||||
memoryEntity,
|
|
||||||
memoryAssetEntity,
|
|
||||||
personEntity,
|
|
||||||
assetFaceEntity,
|
|
||||||
storeEntity,
|
|
||||||
idxLatLng,
|
|
||||||
];
|
|
||||||
late final Shape16 userEntity = Shape16(
|
|
||||||
source: i0.VersionedTable(
|
|
||||||
entityName: 'user_entity',
|
|
||||||
withoutRowId: true,
|
|
||||||
isStrict: true,
|
|
||||||
tableConstraints: ['PRIMARY KEY(id)'],
|
|
||||||
columns: [
|
|
||||||
_column_0,
|
|
||||||
_column_1,
|
|
||||||
_column_2,
|
|
||||||
_column_3,
|
|
||||||
_column_84,
|
|
||||||
_column_85,
|
|
||||||
_column_5,
|
|
||||||
],
|
|
||||||
attachedDatabase: database,
|
|
||||||
),
|
|
||||||
alias: null,
|
|
||||||
);
|
|
||||||
late final Shape17 remoteAssetEntity = Shape17(
|
|
||||||
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,
|
|
||||||
],
|
|
||||||
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 Shape2 localAssetEntity = Shape2(
|
|
||||||
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,
|
|
||||||
],
|
|
||||||
attachedDatabase: database,
|
|
||||||
),
|
|
||||||
alias: null,
|
|
||||||
);
|
|
||||||
late final Shape6 localAlbumEntity = Shape6(
|
|
||||||
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_33,
|
|
||||||
],
|
|
||||||
attachedDatabase: database,
|
|
||||||
),
|
|
||||||
alias: null,
|
|
||||||
);
|
|
||||||
late final Shape7 localAlbumAssetEntity = Shape7(
|
|
||||||
source: i0.VersionedTable(
|
|
||||||
entityName: 'local_album_asset_entity',
|
|
||||||
withoutRowId: true,
|
|
||||||
isStrict: true,
|
|
||||||
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
|
||||||
columns: [_column_34, _column_35],
|
|
||||||
attachedDatabase: database,
|
|
||||||
),
|
|
||||||
alias: null,
|
|
||||||
);
|
|
||||||
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 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)',
|
|
||||||
);
|
|
||||||
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 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 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 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 Shape15 assetFaceEntity = Shape15(
|
|
||||||
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,
|
|
||||||
],
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
final i1.Index idxLatLng = i1.Index(
|
|
||||||
'idx_lat_lng',
|
|
||||||
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class Shape18 extends i0.VersionedTable {
|
|
||||||
Shape18({required super.source, required super.alias}) : super.aliased();
|
|
||||||
i1.GeneratedColumn<int> get id =>
|
|
||||||
columnsByName['id']! as i1.GeneratedColumn<int>;
|
|
||||||
i1.GeneratedColumn<String> get stringValue =>
|
|
||||||
columnsByName['string_value']! as i1.GeneratedColumn<String>;
|
|
||||||
i1.GeneratedColumn<int> get intValue =>
|
|
||||||
columnsByName['int_value']! as i1.GeneratedColumn<int>;
|
|
||||||
}
|
|
||||||
|
|
||||||
i1.GeneratedColumn<int> _column_87(String aliasedName) =>
|
|
||||||
i1.GeneratedColumn<int>(
|
|
||||||
'id',
|
|
||||||
aliasedName,
|
|
||||||
false,
|
|
||||||
type: i1.DriftSqlType.int,
|
|
||||||
);
|
|
||||||
i1.GeneratedColumn<String> _column_88(String aliasedName) =>
|
|
||||||
i1.GeneratedColumn<String>(
|
|
||||||
'string_value',
|
|
||||||
aliasedName,
|
|
||||||
true,
|
|
||||||
type: i1.DriftSqlType.string,
|
|
||||||
);
|
|
||||||
i1.GeneratedColumn<int> _column_89(String aliasedName) =>
|
|
||||||
i1.GeneratedColumn<int>(
|
|
||||||
'int_value',
|
|
||||||
aliasedName,
|
|
||||||
true,
|
|
||||||
type: i1.DriftSqlType.int,
|
|
||||||
);
|
|
||||||
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,
|
||||||
@@ -3442,7 +3056,6 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
|
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
|
||||||
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
|
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
|
||||||
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
|
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
|
||||||
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
|
|
||||||
}) {
|
}) {
|
||||||
return (currentVersion, database) async {
|
return (currentVersion, database) async {
|
||||||
switch (currentVersion) {
|
switch (currentVersion) {
|
||||||
@@ -3476,11 +3089,6 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
final migrator = i1.Migrator(database, schema);
|
final migrator = i1.Migrator(database, schema);
|
||||||
await from6To7(migrator, schema);
|
await from6To7(migrator, schema);
|
||||||
return 7;
|
return 7;
|
||||||
case 7:
|
|
||||||
final schema = Schema8(database: database);
|
|
||||||
final migrator = i1.Migrator(database, schema);
|
|
||||||
await from7To8(migrator, schema);
|
|
||||||
return 8;
|
|
||||||
default:
|
default:
|
||||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||||
}
|
}
|
||||||
@@ -3494,7 +3102,6 @@ i1.OnUpgrade stepByStep({
|
|||||||
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
|
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
|
||||||
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
|
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
|
||||||
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
|
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
|
||||||
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
|
|
||||||
}) => i0.VersionedSchema.stepByStepHelper(
|
}) => i0.VersionedSchema.stepByStepHelper(
|
||||||
step: migrationSteps(
|
step: migrationSteps(
|
||||||
from1To2: from1To2,
|
from1To2: from1To2,
|
||||||
@@ -3503,6 +3110,5 @@ i1.OnUpgrade stepByStep({
|
|||||||
from4To5: from4To5,
|
from4To5: from4To5,
|
||||||
from5To6: from5To6,
|
from5To6: from5To6,
|
||||||
from6To7: from6To7,
|
from6To7: from6To7,
|
||||||
from7To8: from7To8,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
|||||||
import 'package:immich_mobile/utils/database.utils.dart';
|
import 'package:immich_mobile/utils/database.utils.dart';
|
||||||
import 'package:platform/platform.dart';
|
import 'package:platform/platform.dart';
|
||||||
|
|
||||||
enum SortLocalAlbumsBy { id, backupSelection, isIosSharedAlbum, name, assetCount, newestAsset }
|
enum SortLocalAlbumsBy { id, backupSelection, isIosSharedAlbum, name, assetCount }
|
||||||
|
|
||||||
class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
||||||
final Drift _db;
|
final Drift _db;
|
||||||
@@ -40,7 +40,6 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
|||||||
SortLocalAlbumsBy.isIosSharedAlbum => OrderingTerm.asc(_db.localAlbumEntity.isIosSharedAlbum),
|
SortLocalAlbumsBy.isIosSharedAlbum => OrderingTerm.asc(_db.localAlbumEntity.isIosSharedAlbum),
|
||||||
SortLocalAlbumsBy.name => OrderingTerm.asc(_db.localAlbumEntity.name),
|
SortLocalAlbumsBy.name => OrderingTerm.asc(_db.localAlbumEntity.name),
|
||||||
SortLocalAlbumsBy.assetCount => OrderingTerm.desc(assetCount),
|
SortLocalAlbumsBy.assetCount => OrderingTerm.desc(assetCount),
|
||||||
SortLocalAlbumsBy.newestAsset => OrderingTerm.desc(_db.localAlbumEntity.updatedAt),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
query.orderBy(orderings);
|
query.orderBy(orderings);
|
||||||
@@ -56,9 +55,8 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
|||||||
final assetsToDelete = _platform.isIOS ? await _getUniqueAssetsInAlbum(albumId) : await getAssetIds(albumId);
|
final assetsToDelete = _platform.isIOS ? await _getUniqueAssetsInAlbum(albumId) : await getAssetIds(albumId);
|
||||||
await _deleteAssets(assetsToDelete);
|
await _deleteAssets(assetsToDelete);
|
||||||
|
|
||||||
await _db.managers.localAlbumEntity
|
// All the other assets that are still associated will be unlinked automatically on-cascade
|
||||||
.filter((a) => a.id.equals(albumId) & a.backupSelection.equals(BackupSelection.none))
|
await _db.managers.localAlbumEntity.filter((a) => a.id.equals(albumId)).delete();
|
||||||
.delete();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<void> syncDeletes(String albumId, Iterable<String> assetIdsToKeep) async {
|
Future<void> syncDeletes(String albumId, Iterable<String> assetIdsToKeep) async {
|
||||||
@@ -153,10 +151,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
|||||||
await deleteSmt.go();
|
await deleteSmt.go();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only remove albums that are not explicitly selected or excluded from backups
|
await _db.localAlbumEntity.deleteWhere((f) => f.marker_.isNotNull());
|
||||||
await _db.localAlbumEntity.deleteWhere(
|
|
||||||
(f) => f.marker_.isNotNull() & f.backupSelection.equalsValue(BackupSelection.none),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,7 +319,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
|||||||
innerJoin(_db.localAssetEntity, _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id)),
|
innerJoin(_db.localAssetEntity, _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id)),
|
||||||
])
|
])
|
||||||
..where(_db.localAlbumAssetEntity.albumId.equals(albumId))
|
..where(_db.localAlbumAssetEntity.albumId.equals(albumId))
|
||||||
..orderBy([OrderingTerm.desc(_db.localAssetEntity.createdAt)])
|
..orderBy([OrderingTerm.asc(_db.localAssetEntity.id)])
|
||||||
..limit(1);
|
..limit(1);
|
||||||
|
|
||||||
final results = await query.map((row) => row.readTable(_db.localAssetEntity).toDto()).get();
|
final results = await query.map((row) => row.readTable(_db.localAssetEntity).toDto()).get();
|
||||||
|
|||||||
@@ -31,17 +31,11 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
useColumns: false,
|
useColumns: false,
|
||||||
),
|
),
|
||||||
leftOuterJoin(_db.userEntity, _db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId), useColumns: false),
|
leftOuterJoin(_db.userEntity, _db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId), useColumns: false),
|
||||||
leftOuterJoin(
|
|
||||||
_db.remoteAlbumUserEntity,
|
|
||||||
_db.remoteAlbumUserEntity.albumId.equalsExp(_db.remoteAlbumEntity.id),
|
|
||||||
useColumns: false,
|
|
||||||
),
|
|
||||||
]);
|
]);
|
||||||
query
|
query
|
||||||
..where(_db.remoteAssetEntity.deletedAt.isNull())
|
..where(_db.remoteAssetEntity.deletedAt.isNull())
|
||||||
..addColumns([assetCount])
|
..addColumns([assetCount])
|
||||||
..addColumns([_db.userEntity.name])
|
..addColumns([_db.userEntity.name])
|
||||||
..addColumns([_db.remoteAlbumUserEntity.userId.count()])
|
|
||||||
..groupBy([_db.remoteAlbumEntity.id]);
|
..groupBy([_db.remoteAlbumEntity.id]);
|
||||||
|
|
||||||
if (sortBy.isNotEmpty) {
|
if (sortBy.isNotEmpty) {
|
||||||
@@ -59,11 +53,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
.map(
|
.map(
|
||||||
(row) => row
|
(row) => row
|
||||||
.readTable(_db.remoteAlbumEntity)
|
.readTable(_db.remoteAlbumEntity)
|
||||||
.toDto(
|
.toDto(assetCount: row.read(assetCount) ?? 0, ownerName: row.read(_db.userEntity.name)!),
|
||||||
assetCount: row.read(assetCount) ?? 0,
|
|
||||||
ownerName: row.read(_db.userEntity.name)!,
|
|
||||||
isShared: row.read(_db.remoteAlbumUserEntity.userId.count())! > 2,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
@@ -88,27 +78,17 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
_db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId),
|
_db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId),
|
||||||
useColumns: false,
|
useColumns: false,
|
||||||
),
|
),
|
||||||
leftOuterJoin(
|
|
||||||
_db.remoteAlbumUserEntity,
|
|
||||||
_db.remoteAlbumUserEntity.albumId.equalsExp(_db.remoteAlbumEntity.id),
|
|
||||||
useColumns: false,
|
|
||||||
),
|
|
||||||
])
|
])
|
||||||
..where(_db.remoteAlbumEntity.id.equals(albumId) & _db.remoteAssetEntity.deletedAt.isNull())
|
..where(_db.remoteAlbumEntity.id.equals(albumId) & _db.remoteAssetEntity.deletedAt.isNull())
|
||||||
..addColumns([assetCount])
|
..addColumns([assetCount])
|
||||||
..addColumns([_db.userEntity.name])
|
..addColumns([_db.userEntity.name])
|
||||||
..addColumns([_db.remoteAlbumUserEntity.userId.count()])
|
|
||||||
..groupBy([_db.remoteAlbumEntity.id]);
|
..groupBy([_db.remoteAlbumEntity.id]);
|
||||||
|
|
||||||
return query
|
return query
|
||||||
.map(
|
.map(
|
||||||
(row) => row
|
(row) => row
|
||||||
.readTable(_db.remoteAlbumEntity)
|
.readTable(_db.remoteAlbumEntity)
|
||||||
.toDto(
|
.toDto(assetCount: row.read(assetCount) ?? 0, ownerName: row.read(_db.userEntity.name)!),
|
||||||
assetCount: row.read(assetCount) ?? 0,
|
|
||||||
ownerName: row.read(_db.userEntity.name)!,
|
|
||||||
isShared: row.read(_db.remoteAlbumUserEntity.userId.count())! > 2,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
}
|
}
|
||||||
@@ -274,57 +254,24 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
_db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId),
|
_db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId),
|
||||||
useColumns: false,
|
useColumns: false,
|
||||||
),
|
),
|
||||||
leftOuterJoin(
|
|
||||||
_db.remoteAlbumUserEntity,
|
|
||||||
_db.remoteAlbumUserEntity.albumId.equalsExp(_db.remoteAlbumEntity.id),
|
|
||||||
useColumns: false,
|
|
||||||
),
|
|
||||||
])
|
])
|
||||||
..where(_db.remoteAlbumEntity.id.equals(albumId))
|
..where(_db.remoteAlbumEntity.id.equals(albumId))
|
||||||
..addColumns([_db.userEntity.name])
|
..addColumns([_db.userEntity.name])
|
||||||
..addColumns([_db.remoteAlbumUserEntity.userId.count()])
|
|
||||||
..groupBy([_db.remoteAlbumEntity.id]);
|
..groupBy([_db.remoteAlbumEntity.id]);
|
||||||
|
|
||||||
return query.map((row) {
|
return query.map((row) {
|
||||||
final album = row
|
final album = row.readTable(_db.remoteAlbumEntity).toDto(ownerName: row.read(_db.userEntity.name)!);
|
||||||
.readTable(_db.remoteAlbumEntity)
|
|
||||||
.toDto(
|
|
||||||
ownerName: row.read(_db.userEntity.name)!,
|
|
||||||
isShared: row.read(_db.remoteAlbumUserEntity.userId.count())! > 2,
|
|
||||||
);
|
|
||||||
return album;
|
return album;
|
||||||
}).watchSingleOrNull();
|
}).watchSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<DateTime?> getNewestAssetTimestamp(String albumId) {
|
|
||||||
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)),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return query.map((row) => row.read(_db.remoteAssetEntity.localDateTime.max())).getSingleOrNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<DateTime?> getOldestAssetTimestamp(String albumId) {
|
|
||||||
final query = _db.remoteAlbumAssetEntity.selectOnly()
|
|
||||||
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId))
|
|
||||||
..addColumns([_db.remoteAssetEntity.localDateTime.min()])
|
|
||||||
..join([
|
|
||||||
innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return query.map((row) => row.read(_db.remoteAssetEntity.localDateTime.min())).getSingleOrNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<int> getCount() {
|
Future<int> getCount() {
|
||||||
return _db.managers.remoteAlbumEntity.count();
|
return _db.managers.remoteAlbumEntity.count();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on RemoteAlbumEntityData {
|
extension on RemoteAlbumEntityData {
|
||||||
RemoteAlbum toDto({int assetCount = 0, required String ownerName, required bool isShared}) {
|
RemoteAlbum toDto({int assetCount = 0, required String ownerName}) {
|
||||||
return RemoteAlbum(
|
return RemoteAlbum(
|
||||||
id: id,
|
id: id,
|
||||||
name: name,
|
name: name,
|
||||||
@@ -337,7 +284,6 @@ extension on RemoteAlbumEntityData {
|
|||||||
order: order,
|
order: order,
|
||||||
assetCount: assetCount,
|
assetCount: assetCount,
|
||||||
ownerName: ownerName,
|
ownerName: ownerName,
|
||||||
isShared: isShared,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,16 @@
|
|||||||
import 'package:drift/drift.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
// Temporary interface until Isar is removed to make the service work with both Isar and Sqlite
|
class IsarStoreRepository extends IsarDatabaseRepository {
|
||||||
abstract class IStoreRepository {
|
|
||||||
Future<bool> deleteAll();
|
|
||||||
Stream<List<StoreDto<Object>>> watchAll();
|
|
||||||
Future<void> delete<T>(StoreKey<T> key);
|
|
||||||
Future<bool> upsert<T>(StoreKey<T> key, T value);
|
|
||||||
Future<T?> tryGet<T>(StoreKey<T> key);
|
|
||||||
Stream<T?> watch<T>(StoreKey<T> key);
|
|
||||||
Future<List<StoreDto<Object>>> getAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
class IsarStoreRepository extends IsarDatabaseRepository implements IStoreRepository {
|
|
||||||
final Isar _db;
|
final Isar _db;
|
||||||
final validStoreKeys = StoreKey.values.map((e) => e.id).toSet();
|
final validStoreKeys = StoreKey.values.map((e) => e.id).toSet();
|
||||||
|
|
||||||
IsarStoreRepository(super.db) : _db = db;
|
IsarStoreRepository(super.db) : _db = db;
|
||||||
|
|
||||||
@override
|
|
||||||
Future<bool> deleteAll() async {
|
Future<bool> deleteAll() async {
|
||||||
return await transaction(() async {
|
return await transaction(() async {
|
||||||
await _db.storeValues.clear();
|
await _db.storeValues.clear();
|
||||||
@@ -32,29 +18,25 @@ class IsarStoreRepository extends IsarDatabaseRepository implements IStoreReposi
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Stream<StoreDto<Object>> watchAll() {
|
||||||
Stream<List<StoreDto<Object>>> watchAll() {
|
|
||||||
return _db.storeValues
|
return _db.storeValues
|
||||||
.filter()
|
.filter()
|
||||||
.anyOf(validStoreKeys, (query, id) => query.idEqualTo(id))
|
.anyOf(validStoreKeys, (query, id) => query.idEqualTo(id))
|
||||||
.watch(fireImmediately: true)
|
.watch(fireImmediately: true)
|
||||||
.asyncMap((entities) => Future.wait(entities.map((entity) => _toUpdateEvent(entity))));
|
.asyncExpand((entities) => Stream.fromFutures(entities.map((e) async => _toUpdateEvent(e))));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> delete<T>(StoreKey<T> key) async {
|
Future<void> delete<T>(StoreKey<T> key) async {
|
||||||
return await transaction(() async => await _db.storeValues.delete(key.id));
|
return await transaction(() async => await _db.storeValues.delete(key.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Future<bool> insert<T>(StoreKey<T> key, T value) async {
|
||||||
Future<bool> upsert<T>(StoreKey<T> key, T value) async {
|
|
||||||
return await transaction(() async {
|
return await transaction(() async {
|
||||||
await _db.storeValues.put(await _fromValue(key, value));
|
await _db.storeValues.put(await _fromValue(key, value));
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Future<T?> tryGet<T>(StoreKey<T> key) async {
|
Future<T?> tryGet<T>(StoreKey<T> key) async {
|
||||||
final entity = (await _db.storeValues.get(key.id));
|
final entity = (await _db.storeValues.get(key.id));
|
||||||
if (entity == null) {
|
if (entity == null) {
|
||||||
@@ -63,7 +45,13 @@ class IsarStoreRepository extends IsarDatabaseRepository implements IStoreReposi
|
|||||||
return await _toValue(key, entity);
|
return await _toValue(key, entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Future<bool> update<T>(StoreKey<T> key, T value) async {
|
||||||
|
return await transaction(() async {
|
||||||
|
await _db.storeValues.put(await _fromValue(key, value));
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Stream<T?> watch<T>(StoreKey<T> key) async* {
|
Stream<T?> watch<T>(StoreKey<T> key) async* {
|
||||||
yield* _db.storeValues
|
yield* _db.storeValues
|
||||||
.watchObject(key.id, fireImmediately: true)
|
.watchObject(key.id, fireImmediately: true)
|
||||||
@@ -100,93 +88,8 @@ class IsarStoreRepository extends IsarDatabaseRepository implements IStoreReposi
|
|||||||
return StoreValue(key.id, intValue: intValue, strValue: strValue);
|
return StoreValue(key.id, intValue: intValue, strValue: strValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Future<List<StoreDto<Object>>> getAll() async {
|
Future<List<StoreDto<Object>>> getAll() async {
|
||||||
final entities = await _db.storeValues.filter().anyOf(validStoreKeys, (query, id) => query.idEqualTo(id)).findAll();
|
final entities = await _db.storeValues.filter().anyOf(validStoreKeys, (query, id) => query.idEqualTo(id)).findAll();
|
||||||
return Future.wait(entities.map((e) => _toUpdateEvent(e)).toList());
|
return Future.wait(entities.map((e) => _toUpdateEvent(e)).toList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DriftStoreRepository extends DriftDatabaseRepository implements IStoreRepository {
|
|
||||||
final Drift _db;
|
|
||||||
final validStoreKeys = StoreKey.values.map((e) => e.id).toSet();
|
|
||||||
|
|
||||||
DriftStoreRepository(super.db) : _db = db;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<bool> deleteAll() async {
|
|
||||||
await _db.storeEntity.deleteAll();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<List<StoreDto<Object>>> getAll() async {
|
|
||||||
final query = _db.storeEntity.select()..where((entity) => entity.id.isIn(validStoreKeys));
|
|
||||||
return query.asyncMap((entity) => _toUpdateEvent(entity)).get();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Stream<List<StoreDto<Object>>> watchAll() {
|
|
||||||
final query = _db.storeEntity.select()..where((entity) => entity.id.isIn(validStoreKeys));
|
|
||||||
|
|
||||||
return query.asyncMap((entity) => _toUpdateEvent(entity)).watch();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> delete<T>(StoreKey<T> key) async {
|
|
||||||
await _db.storeEntity.deleteWhere((entity) => entity.id.equals(key.id));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<bool> upsert<T>(StoreKey<T> key, T value) async {
|
|
||||||
await _db.storeEntity.insertOnConflictUpdate(await _fromValue(key, value));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<T?> tryGet<T>(StoreKey<T> key) async {
|
|
||||||
final entity = await _db.managers.storeEntity.filter((entity) => entity.id.equals(key.id)).getSingleOrNull();
|
|
||||||
if (entity == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return await _toValue(key, entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Stream<T?> watch<T>(StoreKey<T> key) async* {
|
|
||||||
final query = _db.storeEntity.select()..where((entity) => entity.id.equals(key.id));
|
|
||||||
|
|
||||||
yield* query.watchSingleOrNull().asyncMap((e) async => e == null ? null : await _toValue(key, e));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<StoreDto<Object>> _toUpdateEvent(StoreEntityData entity) async {
|
|
||||||
final key = StoreKey.values.firstWhere((e) => e.id == entity.id) as StoreKey<Object>;
|
|
||||||
final value = await _toValue(key, entity);
|
|
||||||
return StoreDto(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<T?> _toValue<T>(StoreKey<T> key, StoreEntityData entity) async =>
|
|
||||||
switch (key.type) {
|
|
||||||
const (int) => entity.intValue,
|
|
||||||
const (String) => entity.stringValue,
|
|
||||||
const (bool) => entity.intValue == 1,
|
|
||||||
const (DateTime) => entity.intValue == null ? null : DateTime.fromMillisecondsSinceEpoch(entity.intValue!),
|
|
||||||
const (UserDto) =>
|
|
||||||
entity.stringValue == null ? null : await DriftUserRepository(_db).get(entity.stringValue!),
|
|
||||||
_ => null,
|
|
||||||
}
|
|
||||||
as T?;
|
|
||||||
|
|
||||||
Future<StoreEntityCompanion> _fromValue<T>(StoreKey<T> key, T value) async {
|
|
||||||
final (int? intValue, String? strValue) = switch (key.type) {
|
|
||||||
const (int) => (value as int, null),
|
|
||||||
const (String) => (null, value as String),
|
|
||||||
const (bool) => ((value as bool) ? 1 : 0, null),
|
|
||||||
const (DateTime) => ((value as DateTime).millisecondsSinceEpoch, null),
|
|
||||||
const (UserDto) => (null, (await DriftUserRepository(_db).upsert(value as UserDto)).id),
|
|
||||||
_ => throw UnsupportedError("Unsupported primitive type: ${key.type} for key: ${key.name}"),
|
|
||||||
};
|
|
||||||
return StoreEntityCompanion(id: Value(key.id), intValue: Value(intValue), stringValue: Value(strValue));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -258,11 +258,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
|
|
||||||
TimelineQuery favorite(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder(
|
TimelineQuery favorite(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder(
|
||||||
filter: (row) =>
|
filter: (row) => row.deletedAt.isNull() & row.isFavorite.equals(true) & row.ownerId.equals(userId),
|
||||||
row.deletedAt.isNull() &
|
|
||||||
row.isFavorite.equals(true) &
|
|
||||||
row.ownerId.equals(userId) &
|
|
||||||
row.visibility.equalsValue(AssetVisibility.timeline),
|
|
||||||
groupBy: groupBy,
|
groupBy: groupBy,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import 'package:drift/drift.dart';
|
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity;
|
import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity;
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
@@ -65,40 +63,3 @@ class IsarUserRepository extends IsarDatabaseRepository {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DriftUserRepository extends DriftDatabaseRepository {
|
|
||||||
final Drift _db;
|
|
||||||
const DriftUserRepository(super.db) : _db = db;
|
|
||||||
|
|
||||||
Future<UserDto?> get(String id) =>
|
|
||||||
_db.managers.userEntity.filter((user) => user.id.equals(id)).getSingleOrNull().then((user) => user?.toDto());
|
|
||||||
|
|
||||||
Future<UserDto> upsert(UserDto user) async {
|
|
||||||
await _db.userEntity.insertOnConflictUpdate(
|
|
||||||
UserEntityCompanion(
|
|
||||||
id: Value(user.id),
|
|
||||||
isAdmin: Value(user.isAdmin),
|
|
||||||
updatedAt: Value(user.updatedAt),
|
|
||||||
name: Value(user.name),
|
|
||||||
email: Value(user.email),
|
|
||||||
hasProfileImage: Value(user.hasProfileImage),
|
|
||||||
profileChangedAt: Value(user.profileChangedAt),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension on UserEntityData {
|
|
||||||
UserDto toDto() {
|
|
||||||
return UserDto(
|
|
||||||
id: id,
|
|
||||||
email: email,
|
|
||||||
name: name,
|
|
||||||
isAdmin: isAdmin,
|
|
||||||
updatedAt: updatedAt,
|
|
||||||
profileChangedAt: profileChangedAt,
|
|
||||||
hasProfileImage: hasProfileImage,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+7
-18
@@ -14,6 +14,7 @@ import 'package:immich_mobile/constants/constants.dart';
|
|||||||
import 'package:immich_mobile/constants/locales.dart';
|
import 'package:immich_mobile/constants/locales.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
||||||
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
@@ -40,21 +41,18 @@ import 'package:worker_manager/worker_manager.dart';
|
|||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
ImmichWidgetsBinding();
|
ImmichWidgetsBinding();
|
||||||
final (isar, drift, logDb) = await Bootstrap.initDB();
|
final db = await Bootstrap.initIsar();
|
||||||
await Bootstrap.initDomain(isar, drift, logDb);
|
final logDb = DriftLogger();
|
||||||
|
await Bootstrap.initDomain(db, logDb);
|
||||||
await initApp();
|
await initApp();
|
||||||
// Warm-up isolate pool for worker manager
|
// Warm-up isolate pool for worker manager
|
||||||
await workerManager.init(dynamicSpawning: true);
|
await workerManager.init(dynamicSpawning: true);
|
||||||
await migrateDatabaseIfNeeded(isar, drift);
|
await migrateDatabaseIfNeeded(db);
|
||||||
HttpSSLOptions.apply();
|
HttpSSLOptions.apply();
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
ProviderScope(
|
ProviderScope(
|
||||||
overrides: [
|
overrides: [dbProvider.overrideWithValue(db), isarProvider.overrideWithValue(db)],
|
||||||
dbProvider.overrideWithValue(isar),
|
|
||||||
isarProvider.overrideWithValue(isar),
|
|
||||||
driftProvider.overrideWith(driftOverride(drift)),
|
|
||||||
],
|
|
||||||
child: const MainWidget(),
|
child: const MainWidget(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -96,9 +94,7 @@ Future<void> initApp() async {
|
|||||||
// Initialize the file downloader
|
// Initialize the file downloader
|
||||||
await FileDownloader().configure(
|
await FileDownloader().configure(
|
||||||
// maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3
|
// maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3
|
||||||
|
globalConfig: (Config.holdingQueue, (6, 6, 3)),
|
||||||
// On Android, if files are larger than 256MB, run in foreground service
|
|
||||||
globalConfig: [(Config.holdingQueue, (6, 6, 3)), (Config.runInForegroundIfFileLargerThan, 256)],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false);
|
await FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false);
|
||||||
@@ -186,13 +182,6 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
|
|||||||
complete: TaskNotification('upload_finished'.tr(), '${'file_name'.tr()}: {displayName}'),
|
complete: TaskNotification('upload_finished'.tr(), '${'file_name'.tr()}: {displayName}'),
|
||||||
progressBar: true,
|
progressBar: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
FileDownloader().configureNotificationForGroup(
|
|
||||||
kBackupGroup,
|
|
||||||
running: TaskNotification('uploading_media'.tr(), '${'file_name'.tr()}: {displayName}'),
|
|
||||||
complete: TaskNotification('upload_finished'.tr(), '${'file_name'.tr()}: {displayName}'),
|
|
||||||
progressBar: true,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<DeepLink> _deepLinkBuilder(PlatformDeepLink deepLink) async {
|
Future<DeepLink> _deepLinkBuilder(PlatformDeepLink deepLink) async {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.widget.dart';
|
||||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
@@ -40,7 +39,6 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ref.read(backgroundSyncProvider).syncRemote();
|
|
||||||
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
|
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
|
||||||
await ref.read(driftBackupProvider.notifier).startBackup(currentUser.id);
|
await ref.read(driftBackupProvider.notifier).startBackup(currentUser.id);
|
||||||
}
|
}
|
||||||
@@ -249,7 +247,6 @@ class _RemainderCard extends ConsumerWidget {
|
|||||||
title: "backup_controller_page_remainder".tr(),
|
title: "backup_controller_page_remainder".tr(),
|
||||||
subtitle: "backup_controller_page_remainder_sub".tr(),
|
subtitle: "backup_controller_page_remainder_sub".tr(),
|
||||||
info: remainderCount.toString(),
|
info: remainderCount.toString(),
|
||||||
onTap: () => context.pushRoute(const DriftBackupAssetDetailRoute()),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
|
||||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
|
||||||
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
|
||||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
|
||||||
|
|
||||||
@RoutePage()
|
|
||||||
class DriftBackupAssetDetailPage extends ConsumerWidget {
|
|
||||||
const DriftBackupAssetDetailPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
AsyncValue<List<LocalAsset>> result = ref.watch(driftBackupCandidateProvider);
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(title: Text('backup_controller_page_remainder'.t(context: context))),
|
|
||||||
body: result.when(
|
|
||||||
data: (List<LocalAsset> candidates) {
|
|
||||||
return ListView.separated(
|
|
||||||
padding: const EdgeInsets.only(top: 16.0),
|
|
||||||
separatorBuilder: (context, index) => Divider(color: context.colorScheme.outlineVariant),
|
|
||||||
itemCount: candidates.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final asset = candidates[index];
|
|
||||||
final albumsAsyncValue = ref.watch(driftCandidateBackupAlbumInfoProvider(asset.id));
|
|
||||||
return LargeLeadingTile(
|
|
||||||
title: Text(
|
|
||||||
asset.name,
|
|
||||||
style: context.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w500, fontSize: 16),
|
|
||||||
),
|
|
||||||
subtitle: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
asset.createdAt.toString(),
|
|
||||||
style: TextStyle(fontSize: 13.0, color: context.colorScheme.onSurfaceSecondary),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
asset.checksum ?? "N/A",
|
|
||||||
style: TextStyle(fontSize: 13.0, color: context.colorScheme.onSurfaceSecondary),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
albumsAsyncValue.when(
|
|
||||||
data: (albums) {
|
|
||||||
if (albums.isEmpty) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
return Text(
|
|
||||||
albums.map((a) => a.name).join(', '),
|
|
||||||
style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
error: (error, stackTrace) =>
|
|
||||||
Text('Error: $error', style: TextStyle(color: context.colorScheme.error)),
|
|
||||||
loading: () => const SizedBox(height: 16, width: 16, child: CircularProgressIndicator.adaptive()),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
leading: ClipRRect(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
|
||||||
child: SizedBox(
|
|
||||||
width: 64,
|
|
||||||
height: 64,
|
|
||||||
child: Thumbnail.fromAsset(asset: asset, size: const Size(64, 64), fit: BoxFit.cover),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
trailing: const Padding(padding: EdgeInsets.only(right: 24, left: 8), child: Icon(Icons.image_search)),
|
|
||||||
onTap: () async {
|
|
||||||
await context.maybePop();
|
|
||||||
await context.navigateTo(const TabShellRoute(children: [MainTimelineRoute()]));
|
|
||||||
EventStream.shared.emit(ScrollToDateEvent(asset.createdAt));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
error: (Object error, StackTrace stackTrace) {
|
|
||||||
return Center(child: Text('Error: $error'));
|
|
||||||
},
|
|
||||||
loading: () {
|
|
||||||
return const SizedBox(height: 48, width: 48, child: Center(child: CircularProgressIndicator.adaptive()));
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -224,7 +224,7 @@ class FileDetailDialog extends ConsumerWidget {
|
|||||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||||
),
|
),
|
||||||
child: asset != null
|
child: asset != null
|
||||||
? Thumbnail.fromAsset(asset: asset, size: const Size(128, 128), fit: BoxFit.cover)
|
? Thumbnail(asset: asset, size: const Size(512, 512), fit: BoxFit.cover)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user