mirror of
https://github.com/immich-app/immich.git
synced 2026-06-21 22:32:10 -07:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4423a8f8a4 | |||
| 77fd2ba919 | |||
| 1318dafdc4 |
@@ -15,7 +15,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data
|
- ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
- build_cache:/buildcache
|
- pnpm_store_server:/buildcache/pnpm-store
|
||||||
- ../packages/plugin-core:/build/plugins/immich-plugin-core
|
- ../packages/plugin-core:/build/plugins/immich-plugin-core
|
||||||
immich-web:
|
immich-web:
|
||||||
env_file: !reset []
|
env_file: !reset []
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
custom: ['https://buy.immich.app', 'https://immich.store']
|
||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
should_run: ${{ steps.check.outputs.should_run }}
|
should_run: ${{ steps.check.outputs.should_run }}
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -79,7 +79,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -94,7 +94,6 @@ jobs:
|
|||||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
working_directory: ./mobile
|
|
||||||
|
|
||||||
- name: Create the Keystore
|
- name: Create the Keystore
|
||||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||||
@@ -202,7 +201,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -220,7 +219,6 @@ jobs:
|
|||||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
working_directory: ./mobile
|
|
||||||
|
|
||||||
- name: Install Flutter dependencies
|
- name: Install Flutter dependencies
|
||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
actions: write
|
actions: write
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- 'open-api/**'
|
- 'open-api/**'
|
||||||
- 'mobile/lib/utils/openapi_patching.dart'
|
|
||||||
- '.github/workflows/check-openapi.yml'
|
- '.github/workflows/check-openapi.yml'
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
@@ -25,41 +24,8 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Check for breaking API changes
|
- name: Check for breaking API changes
|
||||||
uses: oasdiff/oasdiff-action/breaking@50e6a3413e5aa9c3ae4d8393c34745be44288b46 # v0.0.48
|
uses: oasdiff/oasdiff-action/breaking@6147a58e5d1249a12f42fc864ab791d571a30015 # v0.0.47
|
||||||
with:
|
with:
|
||||||
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
|
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
|
||||||
revision: open-api/immich-openapi-specs.json
|
revision: open-api/immich-openapi-specs.json
|
||||||
fail-on: ERR
|
fail-on: ERR
|
||||||
|
|
||||||
check-mobile-patches:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Mise
|
|
||||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
|
||||||
with:
|
|
||||||
github_token: ${{ github.token }}
|
|
||||||
working_directory: ./mobile
|
|
||||||
|
|
||||||
- name: Get packages
|
|
||||||
working-directory: ./mobile
|
|
||||||
run: flutter pub get
|
|
||||||
|
|
||||||
- name: Fetch base spec from main
|
|
||||||
run: |
|
|
||||||
curl -fsSL \
|
|
||||||
"https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json" \
|
|
||||||
-o /tmp/base-spec.json
|
|
||||||
|
|
||||||
- name: Check newly-required fields have a backward-compat patch
|
|
||||||
working-directory: ./mobile
|
|
||||||
env:
|
|
||||||
OPENAPI_BASE_SPEC: /tmp/base-spec.json
|
|
||||||
OPENAPI_REVISION_SPEC: ../open-api/immich-openapi-specs.json
|
|
||||||
run: flutter test test/openapi_patches_coverage.dart
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
working-directory: ./packages/cli
|
working-directory: ./packages/cli
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -49,9 +49,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish
|
- name: Publish
|
||||||
if: ${{ github.event_name == 'release' }}
|
if: ${{ github.event_name == 'release' }}
|
||||||
env:
|
run: mise run ci-publish
|
||||||
NPM_TAG: ${{ github.event.release.prerelease && 'rc' || 'latest' }}
|
|
||||||
run: mise run ci-publish -- --tag "$NPM_TAG"
|
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
name: Docker
|
name: Docker
|
||||||
@@ -63,7 +61,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -75,13 +73,13 @@ jobs:
|
|||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
|
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.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
|
||||||
@@ -96,7 +94,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate docker image tags
|
- name: Generate docker image tags
|
||||||
id: metadata
|
id: metadata
|
||||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||||
with:
|
with:
|
||||||
flavor: |
|
flavor: |
|
||||||
latest=false
|
latest=false
|
||||||
@@ -104,10 +102,10 @@ jobs:
|
|||||||
name=ghcr.io/${{ github.repository_owner }}/immich-cli
|
name=ghcr.io/${{ github.repository_owner }}/immich-cli
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=${{ steps.package-version.outputs.version }},enable=${{ github.event_name == 'release' }}
|
type=raw,value=${{ steps.package-version.outputs.version }},enable=${{ github.event_name == 'release' }}
|
||||||
type=raw,value=latest,enable=${{ github.event_name == 'release' && !github.event.release.prerelease }}
|
type=raw,value=latest,enable=${{ github.event_name == 'release' }}
|
||||||
|
|
||||||
- name: Build and push image
|
- name: Build and push image
|
||||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||||
with:
|
with:
|
||||||
file: packages/cli/Dockerfile
|
file: packages/cli/Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|||||||
@@ -14,11 +14,7 @@ jobs:
|
|||||||
should_run: ${{ steps.should_run.outputs.run }}
|
should_run: ${{ steps.should_run.outputs.run }}
|
||||||
steps:
|
steps:
|
||||||
- id: should_run
|
- id: should_run
|
||||||
run: |
|
run: echo "run=${{ github.event_name == 'issues' || github.event.discussion.category.name == 'Feature Request' }}" >> $GITHUB_OUTPUT
|
||||||
echo "run=${{
|
|
||||||
(github.event_name == 'issues' || github.event.discussion.category.name == 'Feature Request')
|
|
||||||
&& !contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.issue.author_association || github.event.discussion.author_association)
|
|
||||||
}}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
get_body:
|
get_body:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -39,7 +35,7 @@ jobs:
|
|||||||
needs: [get_body, should_run]
|
needs: [get_body, should_run]
|
||||||
if: ${{ needs.should_run.outputs.should_run == 'true' }}
|
if: ${{ needs.should_run.outputs.should_run == 'true' }}
|
||||||
container:
|
container:
|
||||||
image: ghcr.io/immich-app/mdq:main@sha256:e73f60195b39748c4876f23e3e6cd22a68a9754acec8aef1fd6979fd52cd2c9f
|
image: ghcr.io/immich-app/mdq:main@sha256:0a8b8867773a0f8368061f47578603f438349f8f1f28b0e16105f481e5c794e0
|
||||||
outputs:
|
outputs:
|
||||||
checked: ${{ steps.get_checkbox.outputs.checked }}
|
checked: ${{ steps.get_checkbox.outputs.checked }}
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.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.
|
||||||
@@ -70,7 +70,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.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
|
||||||
@@ -83,6 +83,6 @@ jobs:
|
|||||||
# ./location_of_script_within_repo/buildscript.sh
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||||
with:
|
with:
|
||||||
category: '/language:${{matrix.language}}'
|
category: '/language:${{matrix.language}}'
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
should_run: ${{ steps.check.outputs.should_run }}
|
should_run: ${{ steps.check.outputs.should_run }}
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -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@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -90,7 +90,7 @@ jobs:
|
|||||||
suffix: ['']
|
suffix: ['']
|
||||||
steps:
|
steps:
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -132,7 +132,7 @@ jobs:
|
|||||||
suffixes: '-rocm'
|
suffixes: '-rocm'
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
runner-mapping: '{"linux/amd64": "pokedex-large"}'
|
runner-mapping: '{"linux/amd64": "pokedex-large"}'
|
||||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@db54dcf16fbb12c43479a23749ceea0ad1b4a704 # multi-runner-build-workflow-v3.0.0
|
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@5813c7c4f7016c748ae7ac5d5f684846649d4d20 # multi-runner-build-workflow-v2.4.0
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
actions: read
|
actions: read
|
||||||
@@ -147,7 +147,7 @@ jobs:
|
|||||||
platforms: ${{ matrix.platforms }}
|
platforms: ${{ matrix.platforms }}
|
||||||
runner-mapping: ${{ matrix.runner-mapping }}
|
runner-mapping: ${{ matrix.runner-mapping }}
|
||||||
suffixes: ${{ matrix.suffixes }}
|
suffixes: ${{ matrix.suffixes }}
|
||||||
dockerhub-push: ${{ github.event_name == 'release' && !github.event.release.prerelease }}
|
dockerhub-push: ${{ github.event_name == 'release' }}
|
||||||
build-args: |
|
build-args: |
|
||||||
DEVICE=${{ matrix.device }}
|
DEVICE=${{ matrix.device }}
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ jobs:
|
|||||||
name: Build and Push Server
|
name: Build and Push Server
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }}
|
if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }}
|
||||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@db54dcf16fbb12c43479a23749ceea0ad1b4a704 # multi-runner-build-workflow-v3.0.0
|
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@5813c7c4f7016c748ae7ac5d5f684846649d4d20 # multi-runner-build-workflow-v2.4.0
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
actions: read
|
actions: read
|
||||||
@@ -167,7 +167,7 @@ jobs:
|
|||||||
image: immich-server
|
image: immich-server
|
||||||
context: .
|
context: .
|
||||||
dockerfile: server/Dockerfile
|
dockerfile: server/Dockerfile
|
||||||
dockerhub-push: ${{ github.event_name == 'release' && !github.event.release.prerelease }}
|
dockerhub-push: ${{ github.event_name == 'release' }}
|
||||||
build-args: |
|
build-args: |
|
||||||
DEVICE=cpu
|
DEVICE=cpu
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
should_run: ${{ steps.check.outputs.should_run }}
|
should_run: ${{ steps.check.outputs.should_run }}
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
artifact: ${{ steps.get-artifact.outputs.result }}
|
artifact: ${{ steps.get-artifact.outputs.result }}
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -98,16 +98,9 @@ jobs:
|
|||||||
shouldDeploy: true
|
shouldDeploy: true
|
||||||
};
|
};
|
||||||
} else if (eventType == "release") {
|
} else if (eventType == "release") {
|
||||||
const tag = context.payload.workflow_run.head_branch;
|
|
||||||
const { data: release } = await github.rest.repos.getReleaseByTag({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
tag,
|
|
||||||
});
|
|
||||||
parameters = {
|
parameters = {
|
||||||
event: "release",
|
event: "release",
|
||||||
name: tag,
|
name: context.payload.workflow_run.head_branch,
|
||||||
prerelease: release.prerelease,
|
|
||||||
shouldDeploy: !isFork
|
shouldDeploy: !isFork
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -126,7 +119,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:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -153,7 +146,6 @@ jobs:
|
|||||||
const parameters = JSON.parse(process.env.PARAM_JSON);
|
const parameters = JSON.parse(process.env.PARAM_JSON);
|
||||||
core.setOutput("event", parameters.event);
|
core.setOutput("event", parameters.event);
|
||||||
core.setOutput("name", parameters.name);
|
core.setOutput("name", parameters.name);
|
||||||
core.setOutput("prerelease", parameters.prerelease);
|
|
||||||
core.setOutput("shouldDeploy", parameters.shouldDeploy);
|
core.setOutput("shouldDeploy", parameters.shouldDeploy);
|
||||||
|
|
||||||
- name: Download artifact
|
- name: Download artifact
|
||||||
@@ -211,7 +203,7 @@ jobs:
|
|||||||
run: mise run //docs:deploy
|
run: mise run //docs:deploy
|
||||||
|
|
||||||
- name: Deploy Docs Release Domain
|
- name: Deploy Docs Release Domain
|
||||||
if: ${{ steps.parameters.outputs.event == 'release' && steps.parameters.outputs.prerelease != 'true' }}
|
if: ${{ steps.parameters.outputs.event == 'release' }}
|
||||||
env:
|
env:
|
||||||
TF_VAR_prefix_name: ${{ steps.parameters.outputs.name}}
|
TF_VAR_prefix_name: ${{ steps.parameters.outputs.name}}
|
||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.token.outputs.token }}
|
github-token: ${{ steps.generate-token.outputs.token }}
|
||||||
script: |
|
script: |
|
||||||
github.rest.issues.removeLabel({
|
github.rest.issues.removeLabel({
|
||||||
issue_number: context.payload.pull_request.number,
|
issue_number: context.payload.pull_request.number,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|||||||
@@ -10,13 +10,9 @@ on:
|
|||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- 'false'
|
- 'false'
|
||||||
|
- major
|
||||||
- minor
|
- minor
|
||||||
- patch
|
- patch
|
||||||
- premajor
|
|
||||||
- preminor
|
|
||||||
- prepatch
|
|
||||||
- prerelease
|
|
||||||
- release
|
|
||||||
mobileBump:
|
mobileBump:
|
||||||
description: 'Bump mobile build number'
|
description: 'Bump mobile build number'
|
||||||
required: false
|
required: false
|
||||||
@@ -53,7 +49,7 @@ jobs:
|
|||||||
permissions: {} # No job-level permissions are needed because it uses the app-token
|
permissions: {} # No job-level permissions are needed because it uses the app-token
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -78,7 +74,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
SERVER_BUMP: ${{ inputs.serverBump }}
|
SERVER_BUMP: ${{ inputs.serverBump }}
|
||||||
MOBILE_BUMP: ${{ inputs.mobileBump }}
|
MOBILE_BUMP: ${{ inputs.mobileBump }}
|
||||||
run: pnpm --silent release -s "${SERVER_BUMP}" -m "${MOBILE_BUMP}"
|
run: misc/release/pump-version.sh -s "${SERVER_BUMP}" -m "${MOBILE_BUMP}"
|
||||||
|
|
||||||
- id: output
|
- id: output
|
||||||
run: echo "version=$IMMICH_VERSION" >> $GITHUB_OUTPUT
|
run: echo "version=$IMMICH_VERSION" >> $GITHUB_OUTPUT
|
||||||
@@ -141,7 +137,7 @@ jobs:
|
|||||||
github-token: ${{ steps.generate-token.outputs.token }}
|
github-token: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|
||||||
- name: Create draft release
|
- name: Create draft release
|
||||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
|
||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
tag_name: ${{ needs.bump_version.outputs.version }}
|
tag_name: ${{ needs.bump_version.outputs.version }}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -39,6 +39,4 @@ jobs:
|
|||||||
run: pnpm --filter @immich/sdk build
|
run: pnpm --filter @immich/sdk build
|
||||||
|
|
||||||
- name: Publish
|
- name: Publish
|
||||||
env:
|
run: pnpm --filter @immich/sdk publish --provenance --no-git-checks
|
||||||
NPM_TAG: ${{ github.event.release.prerelease && 'rc' || 'latest' }}
|
|
||||||
run: pnpm --filter @immich/sdk publish --provenance --no-git-checks --tag "$NPM_TAG"
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
should_run: ${{ steps.check.outputs.should_run }}
|
should_run: ${{ steps.check.outputs.should_run }}
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -64,7 +64,6 @@ jobs:
|
|||||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
working_directory: ./mobile
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: flutter pub get
|
run: flutter pub get
|
||||||
@@ -73,6 +72,10 @@ jobs:
|
|||||||
run: flutter pub get
|
run: flutter pub get
|
||||||
working-directory: ./mobile/packages/ui
|
working-directory: ./mobile/packages/ui
|
||||||
|
|
||||||
|
- name: Install dependencies for UI Showcase
|
||||||
|
run: flutter pub get
|
||||||
|
working-directory: ./mobile/packages/ui/showcase
|
||||||
|
|
||||||
- name: Generate translation files
|
- name: Generate translation files
|
||||||
run: mise //mobile:codegen:translation
|
run: mise //mobile:codegen:translation
|
||||||
|
|
||||||
@@ -90,8 +93,6 @@ jobs:
|
|||||||
mobile/**/*.g.dart
|
mobile/**/*.g.dart
|
||||||
mobile/**/*.gr.dart
|
mobile/**/*.gr.dart
|
||||||
mobile/**/*.drift.dart
|
mobile/**/*.drift.dart
|
||||||
mobile/**/*.g.swift
|
|
||||||
mobile/**/*.g.kt
|
|
||||||
|
|
||||||
- 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'
|
||||||
|
|||||||
+19
-52
@@ -17,7 +17,7 @@ jobs:
|
|||||||
should_run: ${{ steps.check.outputs.should_run }}
|
should_run: ${{ steps.check.outputs.should_run }}
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -28,10 +28,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
github-token: ${{ steps.token.outputs.token }}
|
github-token: ${{ steps.token.outputs.token }}
|
||||||
filters: |
|
filters: |
|
||||||
root:
|
|
||||||
- 'misc/**'
|
|
||||||
- 'pnpm-lock.yaml'
|
|
||||||
- 'mise.toml'
|
|
||||||
i18n:
|
i18n:
|
||||||
- 'i18n/**'
|
- 'i18n/**'
|
||||||
- 'mise.toml'
|
- 'mise.toml'
|
||||||
@@ -66,34 +62,6 @@ jobs:
|
|||||||
- '.github/workflows/test.yml'
|
- '.github/workflows/test.yml'
|
||||||
force-events: 'workflow_dispatch'
|
force-events: 'workflow_dispatch'
|
||||||
|
|
||||||
root-unit-tests:
|
|
||||||
name: Test the root workspace
|
|
||||||
needs: pre-job
|
|
||||||
if: ${{ fromJSON(needs.pre-job.outputs.should_run).root == true }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
steps:
|
|
||||||
- id: token
|
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
|
||||||
with:
|
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
|
||||||
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
token: ${{ steps.token.outputs.token }}
|
|
||||||
|
|
||||||
- name: Setup Mise
|
|
||||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
|
||||||
with:
|
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
|
||||||
|
|
||||||
- name: Run unit tests
|
|
||||||
run: pnpm test
|
|
||||||
|
|
||||||
server-unit-tests:
|
server-unit-tests:
|
||||||
name: Test & Lint Server
|
name: Test & Lint Server
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
@@ -103,7 +71,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -134,7 +102,7 @@ jobs:
|
|||||||
working-directory: ./packages/cli
|
working-directory: ./packages/cli
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -165,7 +133,7 @@ jobs:
|
|||||||
working-directory: ./packages/cli
|
working-directory: ./packages/cli
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -209,7 +177,7 @@ jobs:
|
|||||||
working-directory: ./web
|
working-directory: ./web
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -247,7 +215,7 @@ jobs:
|
|||||||
working-directory: ./web
|
working-directory: ./web
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -275,7 +243,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -325,7 +293,7 @@ jobs:
|
|||||||
working-directory: ./e2e
|
working-directory: ./e2e
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -357,7 +325,7 @@ jobs:
|
|||||||
working-directory: ./server
|
working-directory: ./server
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -393,7 +361,7 @@ jobs:
|
|||||||
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -406,7 +374,7 @@ jobs:
|
|||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
|
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
@@ -470,7 +438,7 @@ jobs:
|
|||||||
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -483,7 +451,7 @@ jobs:
|
|||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
|
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
@@ -578,7 +546,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -592,7 +560,6 @@ jobs:
|
|||||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
working_directory: ./mobile
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: flutter pub get
|
run: flutter pub get
|
||||||
@@ -616,7 +583,7 @@ jobs:
|
|||||||
working-directory: ./machine-learning
|
working-directory: ./machine-learning
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -646,7 +613,7 @@ jobs:
|
|||||||
working-directory: ./.github
|
working-directory: ./.github
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -676,7 +643,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -697,7 +664,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -755,7 +722,7 @@ jobs:
|
|||||||
- 5432:5432
|
- 5432:5432
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
should_run: ${{ steps.check.outputs.should_run }}
|
should_run: ${{ steps.check.outputs.should_run }}
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
if: ${{ fromJSON(needs.pre-job.outputs.should_run).i18n == true }}
|
if: ${{ fromJSON(needs.pre-job.outputs.should_run).i18n == true }}
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|||||||
Vendored
-1
@@ -60,7 +60,6 @@
|
|||||||
"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",
|
||||||
"*.js": "${capture}.spec.js,${capture}.mock.js",
|
|
||||||
"package.json": "package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lockb, bun.lock, pnpm-workspace.yaml, .pnpmfile.cjs"
|
"package.json": "package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lockb, bun.lock, pnpm-workspace.yaml, .pnpmfile.cjs"
|
||||||
},
|
},
|
||||||
"search.exclude": {
|
"search.exclude": {
|
||||||
|
|||||||
+1
-1
@@ -4,4 +4,4 @@
|
|||||||
/web/ @danieldietzler
|
/web/ @danieldietzler
|
||||||
/machine-learning/ @mertalev
|
/machine-learning/ @mertalev
|
||||||
/e2e/ @danieldietzler
|
/e2e/ @danieldietzler
|
||||||
/mobile/ @shenlong-tanwen @santoshakil
|
/mobile/ @shenlong-tanwen
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation
|
||||||
|
in our community a harassment-free experience for everyone, regardless
|
||||||
|
of age, body size, visible or invisible disability, ethnicity, sex
|
||||||
|
characteristics, gender identity and expression, level of experience,
|
||||||
|
education, socio-economic status, nationality, personal appearance,
|
||||||
|
race, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open,
|
||||||
|
welcoming, diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for
|
||||||
|
our community include:
|
||||||
|
|
||||||
|
- Demonstrating empathy and kindness toward other people
|
||||||
|
- Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
- Giving and gracefully accepting constructive feedback
|
||||||
|
- Accepting responsibility and apologizing to those affected by our
|
||||||
|
mistakes, and learning from the experience
|
||||||
|
- Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
- The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
- Trolling, insulting or derogatory comments, and personal or
|
||||||
|
political attacks
|
||||||
|
- Public or private harassment
|
||||||
|
- Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
- Other conduct which could reasonably be considered inappropriate in
|
||||||
|
a professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our
|
||||||
|
standards of acceptable behavior and will take appropriate and fair
|
||||||
|
corrective action in response to any behavior that they deem
|
||||||
|
inappropriate, threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit,
|
||||||
|
or reject comments, commits, code, wiki edits, issues, and other
|
||||||
|
contributions that are not aligned to this Code of Conduct, and will
|
||||||
|
communicate reasons for moderation decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also
|
||||||
|
applies when an individual is officially representing the community in
|
||||||
|
public spaces. Examples of representing our community include using an
|
||||||
|
official e-mail address, posting via an official social media account,
|
||||||
|
or acting as an appointed representative at an online or offline
|
||||||
|
event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior
|
||||||
|
may be reported to the community leaders responsible for enforcement
|
||||||
|
at our Discord channel. All complaints
|
||||||
|
will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and
|
||||||
|
security of the reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in
|
||||||
|
determining the consequences for any action they deem in violation of
|
||||||
|
this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior
|
||||||
|
deemed unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders,
|
||||||
|
providing clarity around the nature of the violation and an
|
||||||
|
explanation of why the behavior was inappropriate. A public apology
|
||||||
|
may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued
|
||||||
|
behavior. No interaction with the people involved, including
|
||||||
|
unsolicited interaction with those enforcing the Code of Conduct, for
|
||||||
|
a specified period of time. This includes avoiding interactions in
|
||||||
|
community spaces as well as external channels like social
|
||||||
|
media. Violating these terms may lead to a temporary or permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards,
|
||||||
|
including sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or
|
||||||
|
public communication with the community for a specified period of
|
||||||
|
time. No public or private interaction with the people involved,
|
||||||
|
including unsolicited interaction with those enforcing the Code of
|
||||||
|
Conduct, is allowed during this period. Violating these terms may lead
|
||||||
|
to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of
|
||||||
|
community standards, including sustained inappropriate behavior,
|
||||||
|
harassment of an individual, or aggression toward or disparagement of
|
||||||
|
classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction
|
||||||
|
within the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor
|
||||||
|
Covenant][homepage], version 2.0, available at
|
||||||
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of
|
||||||
|
conduct enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the
|
||||||
|
FAQ at https://www.contributor-covenant.org/faq. Translations are
|
||||||
|
available at https://www.contributor-covenant.org/translations.
|
||||||
@@ -1,46 +1,46 @@
|
|||||||
dev:
|
dev:
|
||||||
@printf "This command has been removed. Please use:\n\n mise dev # or mise //:dev from another directory\n\n" >&2 && exit 1
|
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans
|
||||||
|
|
||||||
dev-down:
|
dev-down:
|
||||||
@printf "This command has been removed. Please use:\n\n mise dev-down # or mise //:dev-down from another directory\n\n" >&2 && exit 1
|
docker compose -f ./docker/docker-compose.dev.yml down --remove-orphans
|
||||||
|
|
||||||
dev-update:
|
dev-update:
|
||||||
@printf "This command has been removed. Please use:\n\n mise dev-update # or mise //:dev-update from another directory\n\n" >&2 && exit 1
|
@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:
|
||||||
@printf "This command has been removed. Please use:\n\n mise dev-scale # or mise //:dev-scale from another directory\n\n" >&2 && exit 1
|
@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
|
||||||
|
|
||||||
.PHONY: e2e
|
.PHONY: e2e
|
||||||
e2e:
|
e2e:
|
||||||
@printf "This command has been removed. Please use:\n\n mise e2e # or mise //:e2e from another directory\n\n" >&2 && exit 1
|
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --remove-orphans
|
||||||
|
|
||||||
e2e-dev:
|
e2e-dev:
|
||||||
@printf "This command has been removed. Please use:\n\n mise e2e-dev # or mise //:e2e-dev from another directory\n\n" >&2 && exit 1
|
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.dev.yml up --remove-orphans
|
||||||
|
|
||||||
e2e-update:
|
e2e-update:
|
||||||
@printf "This command has been removed. Please use:\n\n mise e2e-update # or mise //:e2e-update from another directory\n\n" >&2 && exit 1
|
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
e2e-down:
|
e2e-down:
|
||||||
@printf "This command has been removed. Please use:\n\n mise e2e-down # or mise //:e2e-down from another directory\n\n" >&2 && exit 1
|
docker compose -f ./e2e/docker-compose.yml down --remove-orphans
|
||||||
|
|
||||||
prod:
|
prod:
|
||||||
@printf "This command has been removed. Please use:\n\n mise prod # or mise //:prod from another directory\n\n" >&2 && exit 1
|
@trap 'make prod-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
prod-down:
|
prod-down:
|
||||||
@printf "This command has been removed. Please use:\n\n mise prod-down # or mise //:prod-down from another directory\n\n" >&2 && exit 1
|
docker compose -f ./docker/docker-compose.prod.yml down --remove-orphans
|
||||||
|
|
||||||
prod-scale:
|
prod-scale:
|
||||||
@printf "This command has been removed. Please use:\n\n mise prod-scale # or mise //:prod-scale from another directory\n\n" >&2 && exit 1
|
@trap 'make prod-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
|
||||||
|
|
||||||
.PHONY: open-api
|
.PHONY: open-api
|
||||||
open-api:
|
open-api:
|
||||||
@printf "This command has been removed. Please use:\n\n mise open-api # or mise //:open-api from another directory\n\n" >&2 && exit 1
|
@printf "This command has been removed. Please use:\n\n mise open-api # or mise //:open-api from another directory\n\n"\n\n >&2 && exit 1
|
||||||
|
|
||||||
sql:
|
sql:
|
||||||
@printf "This command has been removed. Please use:\n\n mise sql # or mise //:sql from another directory\n\n" >&2 && exit 1
|
@printf "This command has been removed. Please use:\n\n mise sql # or mise //:sql from another directory\n\n"\n\n >&2 && exit 1
|
||||||
|
|
||||||
|
|
||||||
renovate:
|
renovate:
|
||||||
@@ -52,7 +52,16 @@ renovate:
|
|||||||
MODULES = e2e server web cli sdk docs .github
|
MODULES = e2e server web cli sdk docs .github
|
||||||
|
|
||||||
test-e2e:
|
test-e2e:
|
||||||
@printf "This command has been removed. Please use:\n\n mise //e2e:test # or mise //e2e:test-web for web tests, respectively\n\n" >&2 && exit 1
|
docker compose -f ./e2e/docker-compose.yml build
|
||||||
|
pnpm --filter immich-e2e run test
|
||||||
|
pnpm --filter immich-e2e run test:web
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
@printf "This command has been removed. Please use:\n\n mise clean # or mise //:clean from another directory\n\n" >&2 && exit 1
|
find . -name "node_modules" -type d -prune -exec rm -rf {} +
|
||||||
|
find . -name "dist" -type d -prune -exec rm -rf '{}' +
|
||||||
|
find . -name "build" -type d -prune -exec rm -rf '{}' +
|
||||||
|
find . -name ".svelte-kit" -type d -prune -exec rm -rf '{}' +
|
||||||
|
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 down -v --remove-orphans || true
|
||||||
|
command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml down -v --remove-orphans || true
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Please report security issues to `security@immich.app`
|
||||||
@@ -154,7 +154,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
|
image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
|
image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
restart: always
|
restart: always
|
||||||
@@ -85,7 +85,7 @@ services:
|
|||||||
container_name: immich_prometheus
|
container_name: immich_prometheus
|
||||||
ports:
|
ports:
|
||||||
- 9090:9090
|
- 9090:9090
|
||||||
image: prom/prometheus@sha256:69f5241418838263316593f7274a304b095c40bcf22e57272865da91bd60a8ac
|
image: prom/prometheus@sha256:e4254400b85610324913f0dc4acf92603d9984e7519414c5a12811aa6146acc3
|
||||||
volumes:
|
volumes:
|
||||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||||
- prometheus-data:/prometheus
|
- prometheus-data:/prometheus
|
||||||
@@ -97,7 +97,7 @@ services:
|
|||||||
command: ['./run.sh', '-disable-reporting']
|
command: ['./run.sh', '-disable-reporting']
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
image: grafana/grafana:12.4.4-ubuntu@sha256:df2e7ef5f32f771794cf76bad5f2bceac227036460a2cc269a9045e5662abc58
|
image: grafana/grafana:12.4.3-ubuntu@sha256:ca3f764fdc48cebdf22dd206f33ecb0795a9a7210eacd1b5c02204aebd78b223
|
||||||
volumes:
|
volumes:
|
||||||
- grafana-data:/var/lib/grafana
|
- grafana-data:/var/lib/grafana
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
|
image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193
|
||||||
user: '1000:1000'
|
user: '1000:1000'
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
|
image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
restart: always
|
restart: always
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ services:
|
|||||||
traefik.enable: true
|
traefik.enable: true
|
||||||
# increase readingTimeouts for the entrypoint used here
|
# increase readingTimeouts for the entrypoint used here
|
||||||
traefik.http.routers.immich.entrypoints: websecure
|
traefik.http.routers.immich.entrypoints: websecure
|
||||||
traefik.http.routers.immich.rule: Host(`immich.example.com`)
|
traefik.http.routers.immich.rule: Host(`immich.your-domain.com`)
|
||||||
traefik.http.services.immich.loadbalancer.server.port: 2283
|
traefik.http.services.immich.loadbalancer.server.port: 2283
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ immich-admin list-users
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
id: 'e65e6f88-2a30-4dbe-8dd9-1885f4889b53',
|
id: 'e65e6f88-2a30-4dbe-8dd9-1885f4889b53',
|
||||||
email: 'immich@example.com',
|
email: 'immich@example.com.com',
|
||||||
name: 'Immich Admin',
|
name: 'Immich Admin',
|
||||||
storageLabel: 'admin',
|
storageLabel: 'admin',
|
||||||
externalPath: null,
|
externalPath: null,
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ Immich uses the [OpenAPI](https://swagger.io/specification/) standard to generat
|
|||||||
OpenAPI is used to generate the client (Typescript, Dart) SDK. `openapi-generator-cli` can be installed [here](https://openapi-generator.tech/docs/installation/). The generated SDK is based on the `immich-openapi-specs.json` file, which is autogenerated by the server **when running in development mode**. The `immich-openapi-specs.json` file can be modified with `@nestjs/swagger` decorators used or referenced by controller endpoints. See the [NestJS OpenAPI docs](https://docs.nestjs.com/openapi/types-and-parameters) for more info. When you add a new endpoint or modify an existing one, you must run the server in development mode and run the command below to update the client SDK.
|
OpenAPI is used to generate the client (Typescript, Dart) SDK. `openapi-generator-cli` can be installed [here](https://openapi-generator.tech/docs/installation/). The generated SDK is based on the `immich-openapi-specs.json` file, which is autogenerated by the server **when running in development mode**. The `immich-openapi-specs.json` file can be modified with `@nestjs/swagger` decorators used or referenced by controller endpoints. See the [NestJS OpenAPI docs](https://docs.nestjs.com/openapi/types-and-parameters) for more info. When you add a new endpoint or modify an existing one, you must run the server in development mode and run the command below to update the client SDK.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mise open-api
|
make open-api
|
||||||
```
|
```
|
||||||
|
|
||||||
You can find the generated client SDK in the `packages/sdk/client` for Typescript SDK and `mobile/openapi` for Dart SDK.
|
You can find the generated client SDK in the `packages/sdk/client` for Typescript SDK and `mobile/openapi` for Dart SDK.
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ When the Dev Container starts, it automatically:
|
|||||||
- Debug ports: 9230 (workers), 9231 (API)
|
- Debug ports: 9230 (workers), 9231 (API)
|
||||||
|
|
||||||
:::info
|
:::info
|
||||||
The Dev Container setup replaces the `mise dev` command from the traditional setup. All services start automatically when you open the container.
|
The Dev Container setup replaces the `make dev` command from the traditional setup. All services start automatically when you open the container.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### Accessing Services
|
### Accessing Services
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
A minimal devcontainer is supplied with this repository. All commands can be executed directly inside this container to avoid tedious installation of the environment.
|
A minimal devcontainer is supplied with this repository. All commands can be executed directly inside this container to avoid tedious installation of the environment.
|
||||||
:::warning
|
:::warning
|
||||||
The provided devcontainer isn't complete at the moment. At least all dockerized steps in the Makefile won't work (`mise dev`, ....). Feel free to contribute!
|
The provided devcontainer isn't complete at the moment. At least all dockerized steps in the Makefile won't work (`make dev`, ....). Feel free to contribute!
|
||||||
:::
|
:::
|
||||||
When contributing code through a pull request, please check the following:
|
When contributing code through a pull request, please check the following:
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ All the services are packaged to run as with single Docker Compose command.
|
|||||||
5. From the root directory, run:
|
5. From the root directory, run:
|
||||||
|
|
||||||
```bash title="Start development server"
|
```bash title="Start development server"
|
||||||
mise dev
|
make dev # required Makefile installed on the system.
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Access the dev instance in your browser at http://localhost:3000, or connect via the mobile app.
|
5. Access the dev instance in your browser at http://localhost:3000, or connect via the mobile app.
|
||||||
@@ -88,7 +88,7 @@ To see local changes to `@immich/ui` in Immich, do the following:
|
|||||||
3. Uncomment the corresponding volume in web service of the `docker/docker-compose.dev.yml` file (`../../ui:/usr/src/ui`)
|
3. Uncomment the corresponding volume in web service of the `docker/docker-compose.dev.yml` file (`../../ui:/usr/src/ui`)
|
||||||
4. Uncomment the corresponding alias in the `web/vite.config.ts` file (`'@immich/ui': path.resolve(\_\_dirname, '../../ui/packages/ui')`)
|
4. Uncomment the corresponding alias in the `web/vite.config.ts` file (`'@immich/ui': path.resolve(\_\_dirname, '../../ui/packages/ui')`)
|
||||||
5. Uncomment the import statement in `web/src/app.css` file `@import '../../../ui/packages/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 '../../../ui/packages/ui/dist/theme/default.css';` and comment out `@import '@immich/ui/theme/default.css';`
|
||||||
6. Start up the stack via `mise 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 (`pnpm run build`)
|
||||||
|
|
||||||
### Mobile app
|
### Mobile app
|
||||||
@@ -109,24 +109,6 @@ mise //mobile:translation
|
|||||||
|
|
||||||
The mobile app asks you what backend to connect to. You can utilize the demo backend (https://demo.immich.app/) if you don't need to change server code or upload photos. Alternatively, you can run the server yourself per the instructions above.
|
The mobile app asks you what backend to connect to. You can utilize the demo backend (https://demo.immich.app/) if you don't need to change server code or upload photos. Alternatively, you can run the server yourself per the instructions above.
|
||||||
|
|
||||||
#### UI components and widget previews
|
|
||||||
|
|
||||||
Shared design-system widgets (buttons, inputs, forms) live in the
|
|
||||||
[`immich_ui` package](https://github.com/immich-app/immich/tree/main/mobile/packages/ui/)
|
|
||||||
under `mobile/packages/ui/`. Components are defined in `lib/src/components/`
|
|
||||||
and have matching previews in `lib/src/previews/`.
|
|
||||||
|
|
||||||
To inspect a component in isolation with a light/dark toggle and hot reload,
|
|
||||||
launch [Flutter's Widget Previewer](https://docs.flutter.dev/tools/widget-previewer):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd mobile/packages/ui
|
|
||||||
flutter widget-preview start
|
|
||||||
```
|
|
||||||
|
|
||||||
In VS Code or Android Studio with the Flutter plugin, the previewer
|
|
||||||
auto-starts when you open the **Flutter Widget Preview** tab in the sidebar.
|
|
||||||
|
|
||||||
## IDE setup
|
## IDE setup
|
||||||
|
|
||||||
### Lint / format extensions
|
### Lint / format extensions
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ You need to run `mise //server:install` before _once_.
|
|||||||
The e2e tests can be run by first starting up a test production environment via:
|
The e2e tests can be run by first starting up a test production environment via:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mise e2e
|
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_:
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "8888:80"
|
- "8888:80"
|
||||||
environment:
|
environment:
|
||||||
PGADMIN_DEFAULT_EMAIL: admin@example.com
|
PGADMIN_DEFAULT_EMAIL: user-name@domain-name.com
|
||||||
PGADMIN_DEFAULT_PASSWORD: strong-password
|
PGADMIN_DEFAULT_PASSWORD: strong-password
|
||||||
volumes:
|
volumes:
|
||||||
- pgadmin-data:/var/lib/pgadmin
|
- pgadmin-data:/var/lib/pgadmin
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ services:
|
|||||||
e2e-auth-server:
|
e2e-auth-server:
|
||||||
container_name: immich-e2e-auth-server
|
container_name: immich-e2e-auth-server
|
||||||
build:
|
build:
|
||||||
context: ../
|
context: ../packages/e2e-auth-server
|
||||||
dockerfile: packages/e2e-auth-server/Dockerfile
|
|
||||||
ports:
|
ports:
|
||||||
- 2286:2286
|
- 2286:2286
|
||||||
|
|
||||||
@@ -45,7 +44,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich-e2e-redis
|
container_name: immich-e2e-redis
|
||||||
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
|
image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
|
|
||||||
|
|||||||
+1
-16
@@ -1,21 +1,11 @@
|
|||||||
[tasks.install]
|
[tasks.install]
|
||||||
run = "pnpm install --filter immich-e2e --frozen-lockfile"
|
run = "pnpm install --filter immich-e2e --frozen-lockfile"
|
||||||
|
|
||||||
[tasks.build]
|
|
||||||
dir = "{{ config_root }}"
|
|
||||||
run = "docker compose build"
|
|
||||||
|
|
||||||
[tasks.test]
|
[tasks.test]
|
||||||
depends = ["//e2e:build", "//e2e:ci-setup"]
|
|
||||||
env._.path = "./node_modules/.bin"
|
env._.path = "./node_modules/.bin"
|
||||||
run = "vitest --run"
|
run = "vitest --run"
|
||||||
|
|
||||||
[tasks.playwright-install]
|
|
||||||
env._.path = "./node_modules/.bin"
|
|
||||||
run = "playwright install"
|
|
||||||
|
|
||||||
[tasks."test-web"]
|
[tasks."test-web"]
|
||||||
depends = ["//e2e:build", "//e2e:ci-setup", "//e2e:playwright-install"]
|
|
||||||
env._.path = "./node_modules/.bin"
|
env._.path = "./node_modules/.bin"
|
||||||
run = "playwright test"
|
run = "playwright test"
|
||||||
|
|
||||||
@@ -40,12 +30,7 @@ run = "tsc --noEmit"
|
|||||||
|
|
||||||
|
|
||||||
[tasks.ci-setup]
|
[tasks.ci-setup]
|
||||||
depends = [
|
depends = ["//:sdk:install", "//:sdk:build", "//cli:install", "//cli:build"]
|
||||||
"//:sdk:install",
|
|
||||||
"//:sdk:build",
|
|
||||||
"//packages/cli:install",
|
|
||||||
"//packages/cli:build",
|
|
||||||
]
|
|
||||||
run = { task = ":install" }
|
run = { task = ":install" }
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -504,14 +504,13 @@ describe('/albums', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should deduplicate owner from albumUsers on create', async () => {
|
it('should not be able to share album with owner', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.post('/albums')
|
.post('/albums')
|
||||||
.send({ albumName: 'New album', albumUsers: [{ role: AlbumUserRole.Editor, userId: user1.userId }] })
|
.send({ albumName: 'New album', albumUsers: [{ role: AlbumUserRole.Editor, userId: user1.userId }] })
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
expect(status).toBe(201);
|
expect(status).toBe(400);
|
||||||
expect(body.albumUsers).toHaveLength(1);
|
expect(body).toEqual(errorDto.badRequest('Cannot share album with owner'));
|
||||||
expect(body.albumUsers[0]).toMatchObject({ role: AlbumUserRole.Owner, user: { id: user1.userId } });
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -259,6 +259,17 @@ describe('/search', () => {
|
|||||||
assets: [assetHeic],
|
assets: [assetHeic],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
should: "should search city ('')",
|
||||||
|
deferred: () => ({
|
||||||
|
dto: {
|
||||||
|
city: '',
|
||||||
|
visibility: AssetVisibility.Timeline,
|
||||||
|
includeNull: true,
|
||||||
|
},
|
||||||
|
assets: [assetLast],
|
||||||
|
}),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
should: 'should search city (null)',
|
should: 'should search city (null)',
|
||||||
deferred: () => ({
|
deferred: () => ({
|
||||||
@@ -280,6 +291,18 @@ describe('/search', () => {
|
|||||||
assets: [assetDensity],
|
assets: [assetDensity],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
should: "should search state ('')",
|
||||||
|
deferred: () => ({
|
||||||
|
dto: {
|
||||||
|
state: '',
|
||||||
|
visibility: AssetVisibility.Timeline,
|
||||||
|
withExif: true,
|
||||||
|
includeNull: true,
|
||||||
|
},
|
||||||
|
assets: [assetLast, assetNotocactus],
|
||||||
|
}),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
should: 'should search state (null)',
|
should: 'should search state (null)',
|
||||||
deferred: () => ({
|
deferred: () => ({
|
||||||
@@ -301,6 +324,17 @@ describe('/search', () => {
|
|||||||
assets: [assetFalcon],
|
assets: [assetFalcon],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
should: "should search country ('')",
|
||||||
|
deferred: () => ({
|
||||||
|
dto: {
|
||||||
|
country: '',
|
||||||
|
visibility: AssetVisibility.Timeline,
|
||||||
|
includeNull: true,
|
||||||
|
},
|
||||||
|
assets: [assetLast],
|
||||||
|
}),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
should: 'should search country (null)',
|
should: 'should search country (null)',
|
||||||
deferred: () => ({
|
deferred: () => ({
|
||||||
|
|||||||
@@ -95,7 +95,6 @@ describe('/server', () => {
|
|||||||
major: expect.any(Number),
|
major: expect.any(Number),
|
||||||
minor: expect.any(Number),
|
minor: expect.any(Number),
|
||||||
patch: expect.any(Number),
|
patch: expect.any(Number),
|
||||||
prerelease: null,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -116,7 +115,6 @@ describe('/server', () => {
|
|||||||
oauthAutoLaunch: false,
|
oauthAutoLaunch: false,
|
||||||
ocr: false,
|
ocr: false,
|
||||||
passwordLogin: true,
|
passwordLogin: true,
|
||||||
realtimeTranscoding: false,
|
|
||||||
search: true,
|
search: true,
|
||||||
sidecar: true,
|
sidecar: true,
|
||||||
trash: true,
|
trash: true,
|
||||||
@@ -141,7 +139,6 @@ describe('/server', () => {
|
|||||||
maintenanceMode: false,
|
maintenanceMode: false,
|
||||||
mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
|
mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
|
||||||
mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json',
|
mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json',
|
||||||
minFaces: 3,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,18 +21,18 @@ describe('/system-config', () => {
|
|||||||
const response1 = await request(app)
|
const response1 = await request(app)
|
||||||
.put('/system-config')
|
.put('/system-config')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ ...config, newVersionCheck: { enabled: false, channel: 'stable' } });
|
.send({ ...config, newVersionCheck: { enabled: false } });
|
||||||
|
|
||||||
expect(response1.status).toBe(200);
|
expect(response1.status).toBe(200);
|
||||||
expect(response1.body).toEqual({ ...config, newVersionCheck: { enabled: false, channel: 'stable' } });
|
expect(response1.body).toEqual({ ...config, newVersionCheck: { enabled: false } });
|
||||||
|
|
||||||
const response2 = await request(app)
|
const response2 = await request(app)
|
||||||
.put('/system-config')
|
.put('/system-config')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ ...config, newVersionCheck: { enabled: true, channel: 'stable' } });
|
.send({ ...config, newVersionCheck: { enabled: true } });
|
||||||
|
|
||||||
expect(response2.status).toBe(200);
|
expect(response2.status).toBe(200);
|
||||||
expect(response2.body).toEqual({ ...config, newVersionCheck: { enabled: true, channel: 'stable' } });
|
expect(response2.body).toEqual({ ...config, newVersionCheck: { enabled: true } });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject an invalid config entry', async () => {
|
it('should reject an invalid config entry', async () => {
|
||||||
|
|||||||
@@ -230,21 +230,6 @@ describe('/users', () => {
|
|||||||
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
|
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
|
||||||
expect(after).toMatchObject({ download: { includeEmbeddedVideos: true } });
|
expect(after).toMatchObject({ download: { includeEmbeddedVideos: true } });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update minimum face count to display people', async () => {
|
|
||||||
const before = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
|
|
||||||
expect(before).toMatchObject({ people: { minimumFaces: 3 } });
|
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.put('/users/me/preferences')
|
|
||||||
.send({ people: { minimumFaces: 2 } })
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toMatchObject({ people: { minimumFaces: 2 } });
|
|
||||||
|
|
||||||
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
|
|
||||||
expect(after).toMatchObject({ people: { minimumFaces: 2 } });
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /users/:id', () => {
|
describe('GET /users/:id', () => {
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ export function toColumnarFormat(assets: MockTimelineAsset[]): TimeBucketAssetRe
|
|||||||
result.duration.push(asset.duration);
|
result.duration.push(asset.duration);
|
||||||
result.projectionType.push(asset.projectionType);
|
result.projectionType.push(asset.projectionType);
|
||||||
result.livePhotoVideoId.push(asset.livePhotoVideoId);
|
result.livePhotoVideoId.push(asset.livePhotoVideoId);
|
||||||
result.city?.push(asset.city);
|
result.city.push(asset.city);
|
||||||
result.country?.push(asset.country);
|
result.country.push(asset.country);
|
||||||
result.visibility.push(asset.visibility);
|
result.visibility.push(asset.visibility);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -305,8 +305,6 @@
|
|||||||
"refreshing_all_libraries": "Refreshing all libraries",
|
"refreshing_all_libraries": "Refreshing all libraries",
|
||||||
"registration": "Admin Registration",
|
"registration": "Admin Registration",
|
||||||
"registration_description": "Since you are the first user on the system, you will be assigned as the Admin and are responsible for administrative tasks, and additional users will be created by you.",
|
"registration_description": "Since you are the first user on the system, you will be assigned as the Admin and are responsible for administrative tasks, and additional users will be created by you.",
|
||||||
"release_channel_release_candidate": "Release candidate",
|
|
||||||
"release_channel_stable": "Stable",
|
|
||||||
"remove_failed_jobs": "Remove failed jobs",
|
"remove_failed_jobs": "Remove failed jobs",
|
||||||
"require_password_change_on_login": "Require user to change password on first login",
|
"require_password_change_on_login": "Require user to change password on first login",
|
||||||
"reset_settings_to_default": "Reset settings to default",
|
"reset_settings_to_default": "Reset settings to default",
|
||||||
@@ -401,10 +399,6 @@
|
|||||||
"transcoding_preferred_hardware_device_description": "Applies only to VAAPI and QSV. Sets the dri node used for hardware transcoding.",
|
"transcoding_preferred_hardware_device_description": "Applies only to VAAPI and QSV. Sets the dri node used for hardware transcoding.",
|
||||||
"transcoding_preset_preset": "Preset (-preset)",
|
"transcoding_preset_preset": "Preset (-preset)",
|
||||||
"transcoding_preset_preset_description": "Compression speed. Slower presets produce smaller files, and increase quality when targeting a certain bitrate. VP9 ignores speeds above 'faster'.",
|
"transcoding_preset_preset_description": "Compression speed. Slower presets produce smaller files, and increase quality when targeting a certain bitrate. VP9 ignores speeds above 'faster'.",
|
||||||
"transcoding_realtime": "Real-time Transcoding [EXPERIMENTAL]",
|
|
||||||
"transcoding_realtime_description": "Allows transcoding to be performed in real-time as the video is being streamed. Enables quality switching, but may cause higher playback latency and stuttering depending on server capabilities.",
|
|
||||||
"transcoding_realtime_enabled": "Enable real-time transcoding",
|
|
||||||
"transcoding_realtime_enabled_description": "If disabled, the server will refuse to start new real-time transcoding sessions.",
|
|
||||||
"transcoding_reference_frames": "Reference frames",
|
"transcoding_reference_frames": "Reference frames",
|
||||||
"transcoding_reference_frames_description": "The number of frames to reference when compressing a given frame. Higher values improve compression efficiency, but slow down encoding. 0 sets this value automatically.",
|
"transcoding_reference_frames_description": "The number of frames to reference when compressing a given frame. Higher values improve compression efficiency, but slow down encoding. 0 sets this value automatically.",
|
||||||
"transcoding_required_description": "Only videos not in an accepted format",
|
"transcoding_required_description": "Only videos not in an accepted format",
|
||||||
@@ -448,8 +442,6 @@
|
|||||||
"user_settings_description": "Manage user settings",
|
"user_settings_description": "Manage user settings",
|
||||||
"user_successfully_removed": "User {email} has been successfully removed.",
|
"user_successfully_removed": "User {email} has been successfully removed.",
|
||||||
"users_page_description": "Admin users page",
|
"users_page_description": "Admin users page",
|
||||||
"version_check_channel": "Release channel",
|
|
||||||
"version_check_channel_description": "Pick the release channel you want to get version announcements for",
|
|
||||||
"version_check_enabled_description": "Enable version check",
|
"version_check_enabled_description": "Enable version check",
|
||||||
"version_check_implications": "The version check feature relies on periodic communication with {server}",
|
"version_check_implications": "The version check feature relies on periodic communication with {server}",
|
||||||
"version_check_settings": "Version Check",
|
"version_check_settings": "Version Check",
|
||||||
@@ -570,7 +562,6 @@
|
|||||||
"asset_added_to_album": "Added to album",
|
"asset_added_to_album": "Added to album",
|
||||||
"asset_adding_to_album": "Adding to album…",
|
"asset_adding_to_album": "Adding to album…",
|
||||||
"asset_created": "Asset created",
|
"asset_created": "Asset created",
|
||||||
"asset_day_count": "{date}: {count, plural, one {# asset} other {# assets}}",
|
|
||||||
"asset_description_updated": "Asset description has been updated",
|
"asset_description_updated": "Asset description has been updated",
|
||||||
"asset_filename_is_offline": "Asset {filename} is offline",
|
"asset_filename_is_offline": "Asset {filename} is offline",
|
||||||
"asset_has_unassigned_faces": "Asset has unassigned faces",
|
"asset_has_unassigned_faces": "Asset has unassigned faces",
|
||||||
@@ -700,7 +691,6 @@
|
|||||||
"backup_settings_subtitle": "Manage upload settings",
|
"backup_settings_subtitle": "Manage upload settings",
|
||||||
"backup_upload_details_page_more_details": "Tap for more details",
|
"backup_upload_details_page_more_details": "Tap for more details",
|
||||||
"backward": "Backward",
|
"backward": "Backward",
|
||||||
"battery_optimization_backup_reliability": "Disabling battery optimizations can improve the reliability of background backup",
|
|
||||||
"biometric_auth_enabled": "Biometric authentication enabled",
|
"biometric_auth_enabled": "Biometric authentication enabled",
|
||||||
"biometric_locked_out": "You are locked out of biometric authentication",
|
"biometric_locked_out": "You are locked out of biometric authentication",
|
||||||
"biometric_no_options": "No biometric options available",
|
"biometric_no_options": "No biometric options available",
|
||||||
@@ -1401,7 +1391,6 @@
|
|||||||
"leave": "Leave",
|
"leave": "Leave",
|
||||||
"leave_album": "Leave album",
|
"leave_album": "Leave album",
|
||||||
"lens_model": "Lens model",
|
"lens_model": "Lens model",
|
||||||
"less": "Less",
|
|
||||||
"let_others_respond": "Let others respond",
|
"let_others_respond": "Let others respond",
|
||||||
"level": "Level",
|
"level": "Level",
|
||||||
"library": "Library",
|
"library": "Library",
|
||||||
@@ -1595,8 +1584,6 @@
|
|||||||
"merge_people_prompt": "Do you want to merge these people? This action is irreversible.",
|
"merge_people_prompt": "Do you want to merge these people? This action is irreversible.",
|
||||||
"merge_people_successfully": "Merge people successfully",
|
"merge_people_successfully": "Merge people successfully",
|
||||||
"merged_people_count": "Merged {count, plural, one {# person} other {# people}}",
|
"merged_people_count": "Merged {count, plural, one {# person} other {# people}}",
|
||||||
"minFaces": "Minimum faces",
|
|
||||||
"minFaces_description": "The minimum number of recognized faces for a person to be displayed",
|
|
||||||
"minimize": "Minimize",
|
"minimize": "Minimize",
|
||||||
"minute": "Minute",
|
"minute": "Minute",
|
||||||
"minutes": "Minutes",
|
"minutes": "Minutes",
|
||||||
@@ -1692,7 +1679,6 @@
|
|||||||
"not_selected": "Not selected",
|
"not_selected": "Not selected",
|
||||||
"notes": "Notes",
|
"notes": "Notes",
|
||||||
"nothing_here_yet": "Nothing here yet",
|
"nothing_here_yet": "Nothing here yet",
|
||||||
"notification_backup_reliability": "Enable notifications to improve background backup reliability",
|
|
||||||
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
|
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
|
||||||
"notification_permission_list_tile_content": "Grant permission to enable notifications.",
|
"notification_permission_list_tile_content": "Grant permission to enable notifications.",
|
||||||
"notification_permission_list_tile_enable_button": "Enable Notifications",
|
"notification_permission_list_tile_enable_button": "Enable Notifications",
|
||||||
@@ -2247,8 +2233,6 @@
|
|||||||
"slideshow_repeat": "Repeat slideshow",
|
"slideshow_repeat": "Repeat slideshow",
|
||||||
"slideshow_repeat_description": "Loop back to beginning when slideshow ends",
|
"slideshow_repeat_description": "Loop back to beginning when slideshow ends",
|
||||||
"slideshow_settings": "Slideshow settings",
|
"slideshow_settings": "Slideshow settings",
|
||||||
"smart_album": "Smart album",
|
|
||||||
"some_assets_already_have_a_location_warning": "Some of the selected assets already have a location",
|
|
||||||
"sort_albums_by": "Sort albums by...",
|
"sort_albums_by": "Sort albums by...",
|
||||||
"sort_created": "Date created",
|
"sort_created": "Date created",
|
||||||
"sort_items": "Number of items",
|
"sort_items": "Number of items",
|
||||||
@@ -2415,7 +2399,6 @@
|
|||||||
"updated_password": "Updated password",
|
"updated_password": "Updated password",
|
||||||
"upload": "Upload",
|
"upload": "Upload",
|
||||||
"upload_concurrency": "Upload concurrency",
|
"upload_concurrency": "Upload concurrency",
|
||||||
"upload_day_count": "{date}: {count, plural, one {# upload} other {# uploads}}",
|
|
||||||
"upload_details": "Upload Details",
|
"upload_details": "Upload Details",
|
||||||
"upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
|
"upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
|
||||||
"upload_dialog_title": "Upload Asset",
|
"upload_dialog_title": "Upload Asset",
|
||||||
@@ -2431,8 +2414,6 @@
|
|||||||
"upload_to_immich": "Upload to Immich ({count})",
|
"upload_to_immich": "Upload to Immich ({count})",
|
||||||
"uploading": "Uploading",
|
"uploading": "Uploading",
|
||||||
"uploading_media": "Uploading media",
|
"uploading_media": "Uploading media",
|
||||||
"uploads": "Uploads",
|
|
||||||
"uploads_count": "{count, plural, one {# upload} other {# uploads}}",
|
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
"usage": "Usage",
|
"usage": "Usage",
|
||||||
"use_biometric": "Use biometric",
|
"use_biometric": "Use biometric",
|
||||||
@@ -2470,7 +2451,6 @@
|
|||||||
"video": "Video",
|
"video": "Video",
|
||||||
"video_hover_setting": "Play video thumbnail on hover",
|
"video_hover_setting": "Play video thumbnail on hover",
|
||||||
"video_hover_setting_description": "Play video thumbnail when mouse is hovering over item. Even when disabled, playback can be started by hovering over the play icon.",
|
"video_hover_setting_description": "Play video thumbnail when mouse is hovering over item. Even when disabled, playback can be started by hovering over the play icon.",
|
||||||
"video_quality": "Video quality",
|
|
||||||
"videos": "Videos",
|
"videos": "Videos",
|
||||||
"videos_count": "{count, plural, one {# Video} other {# Videos}}",
|
"videos_count": "{count, plural, one {# Video} other {# Videos}}",
|
||||||
"videos_only": "Videos only",
|
"videos_only": "Videos only",
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
ARG DEVICE=cpu
|
ARG DEVICE=cpu
|
||||||
|
|
||||||
FROM python:3.11-bookworm@sha256:121d86b6d08752968a7dddbc708849e5f3a839bbff47f32212b46d2a1d842bab AS builder-cpu
|
FROM python:3.11-bookworm@sha256:970c99f886b839fc8829289040c1845dadaf2cae46b37acc7710333158ec29b4 AS builder-cpu
|
||||||
|
|
||||||
FROM python:3.13-slim-trixie@sha256:b04b5d7233d2ad9c379e22ea8927cd1378cd15c60d4ef876c065b25ea8fb3bf3 AS builder-openvino
|
FROM python:3.13-slim-trixie@sha256:d168b8d9eb761f4d3fe305ebd04aeb7e7f2de0297cec5fb2f8f6403244621664 AS builder-openvino
|
||||||
|
|
||||||
FROM builder-cpu AS builder-cuda
|
FROM builder-cpu AS builder-cuda
|
||||||
|
|
||||||
@@ -39,23 +39,23 @@ RUN --mount=type=cache,target=/root/.cache/uv \
|
|||||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||||
uv sync --frozen --extra ${DEVICE} --no-dev --no-editable --no-install-project --compile-bytecode --no-progress --active --link-mode copy
|
uv sync --frozen --extra ${DEVICE} --no-dev --no-editable --no-install-project --compile-bytecode --no-progress --active --link-mode copy
|
||||||
|
|
||||||
FROM python:3.11-slim-bookworm@sha256:8dca233de9f3d9bb410665f00a4da6dd06f331083137e0e98ccf227236fcc438 AS prod-cpu
|
FROM python:3.11-slim-bookworm@sha256:9c6f90801e6b68e772b7c0ca74260cbf7af9f320acec894e26fccdaccfbe3b47 AS prod-cpu
|
||||||
|
|
||||||
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 \
|
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 \
|
||||||
MACHINE_LEARNING_MODEL_ARENA=false
|
MACHINE_LEARNING_MODEL_ARENA=false
|
||||||
|
|
||||||
FROM python:3.13-slim-trixie@sha256:b04b5d7233d2ad9c379e22ea8927cd1378cd15c60d4ef876c065b25ea8fb3bf3 AS prod-openvino
|
FROM python:3.13-slim-trixie@sha256:d168b8d9eb761f4d3fe305ebd04aeb7e7f2de0297cec5fb2f8f6403244621664 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 && \
|
||||||
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.34.4/intel-igc-core-2_2.34.4+21428_amd64.deb && \
|
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.32.7/intel-igc-core-2_2.32.7+21184_amd64.deb && \
|
||||||
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.34.4/intel-igc-opencl-2_2.34.4+21428_amd64.deb && \
|
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.32.7/intel-igc-opencl-2_2.32.7+21184_amd64.deb && \
|
||||||
wget -nv https://github.com/intel/compute-runtime/releases/download/26.18.38308.1/intel-opencl-icd_26.18.38308.1-0_amd64.deb && \
|
wget -nv https://github.com/intel/compute-runtime/releases/download/26.14.37833.4/intel-opencl-icd_26.14.37833.4-0_amd64.deb && \
|
||||||
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-core_1.0.17537.24_amd64.deb && \
|
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-core_1.0.17537.24_amd64.deb && \
|
||||||
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-opencl_1.0.17537.24_amd64.deb && \
|
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-opencl_1.0.17537.24_amd64.deb && \
|
||||||
wget -nv https://github.com/intel/compute-runtime/releases/download/24.35.30872.36/intel-opencl-icd-legacy1_24.35.30872.36_amd64.deb && \
|
wget -nv https://github.com/intel/compute-runtime/releases/download/24.35.30872.36/intel-opencl-icd-legacy1_24.35.30872.36_amd64.deb && \
|
||||||
# TODO: Figure out how to get renovate to manage this differently versioned libigdgmm file
|
# TODO: Figure out how to get renovate to manage this differently versioned libigdgmm file
|
||||||
wget -nv https://github.com/intel/compute-runtime/releases/download/26.18.38308.1/libigdgmm12_22.10.0_amd64.deb && \
|
wget -nv https://github.com/intel/compute-runtime/releases/download/26.14.37833.4/libigdgmm12_22.9.0_amd64.deb && \
|
||||||
dpkg -i *.deb && \
|
dpkg -i *.deb && \
|
||||||
rm *.deb && \
|
rm *.deb && \
|
||||||
apt-get remove wget -yqq && \
|
apt-get remove wget -yqq && \
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ try:
|
|||||||
str(settings.http_keepalive_timeout_s),
|
str(settings.http_keepalive_timeout_s),
|
||||||
"--graceful-timeout",
|
"--graceful-timeout",
|
||||||
"10",
|
"10",
|
||||||
"--no-control-socket",
|
|
||||||
],
|
],
|
||||||
) as cmd:
|
) as cmd:
|
||||||
cmd.wait()
|
cmd.wait()
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from zipfile import BadZipFile
|
|||||||
|
|
||||||
import orjson
|
import orjson
|
||||||
from fastapi import Depends, FastAPI, File, Form, HTTPException
|
from fastapi import Depends, FastAPI, File, Form, HTTPException
|
||||||
from fastapi.responses import PlainTextResponse
|
from fastapi.responses import ORJSONResponse, PlainTextResponse
|
||||||
from onnxruntime.capi.onnxruntime_pybind11_state import InvalidProtobuf, NoSuchFile
|
from onnxruntime.capi.onnxruntime_pybind11_state import InvalidProtobuf, NoSuchFile
|
||||||
from PIL.Image import Image
|
from PIL.Image import Image
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
@@ -32,7 +32,6 @@ from .schemas import (
|
|||||||
ModelIdentity,
|
ModelIdentity,
|
||||||
ModelTask,
|
ModelTask,
|
||||||
ModelType,
|
ModelType,
|
||||||
ORJSONResponse,
|
|
||||||
PipelineRequest,
|
PipelineRequest,
|
||||||
T,
|
T,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -89,9 +89,7 @@ class OpenClipTextualEncoder(BaseCLIPTextualEncoder):
|
|||||||
|
|
||||||
tokenizer: Tokenizer = Tokenizer.from_file(self.tokenizer_file_path.as_posix())
|
tokenizer: Tokenizer = Tokenizer.from_file(self.tokenizer_file_path.as_posix())
|
||||||
|
|
||||||
pad_id = tokenizer.token_to_id(pad_token)
|
pad_id: int = tokenizer.token_to_id(pad_token)
|
||||||
if pad_id is None:
|
|
||||||
raise ValueError(f"Pad token '{pad_token}' not found in tokenizer vocab")
|
|
||||||
tokenizer.enable_padding(length=context_length, pad_token=pad_token, pad_id=pad_id)
|
tokenizer.enable_padding(length=context_length, pad_token=pad_token, pad_id=pad_id)
|
||||||
tokenizer.enable_truncation(max_length=context_length)
|
tokenizer.enable_truncation(max_length=context_length)
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ class TextRecognizer(InferenceModel):
|
|||||||
rec_batch_num=max_batch_size if max_batch_size else 6,
|
rec_batch_num=max_batch_size if max_batch_size else 6,
|
||||||
rec_img_shape=(3, 48, 320),
|
rec_img_shape=(3, 48, 320),
|
||||||
lang_type=self.language,
|
lang_type=self.language,
|
||||||
model_root_dir=self.cache_dir,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return session
|
return session
|
||||||
|
|||||||
@@ -3,16 +3,9 @@ from typing import Any, Literal, Protocol, TypeGuard, TypeVar
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import numpy.typing as npt
|
import numpy.typing as npt
|
||||||
import orjson
|
|
||||||
from fastapi.responses import JSONResponse
|
|
||||||
from typing_extensions import TypedDict
|
from typing_extensions import TypedDict
|
||||||
|
|
||||||
|
|
||||||
class ORJSONResponse(JSONResponse):
|
|
||||||
def render(self, content: Any) -> bytes:
|
|
||||||
return orjson.dumps(content, option=orjson.OPT_SERIALIZE_NUMPY)
|
|
||||||
|
|
||||||
|
|
||||||
class StrEnum(str, Enum):
|
class StrEnum(str, Enum):
|
||||||
value: str
|
value: str
|
||||||
|
|
||||||
|
|||||||
@@ -816,10 +816,6 @@ class TestFaceRecognition:
|
|||||||
|
|
||||||
def test_recognition(self, cv_image: cv2.Mat, mocker: MockerFixture) -> None:
|
def test_recognition(self, cv_image: cv2.Mat, mocker: MockerFixture) -> None:
|
||||||
mocker.patch.object(FaceRecognizer, "load")
|
mocker.patch.object(FaceRecognizer, "load")
|
||||||
mocker.patch(
|
|
||||||
"immich_ml.models.facial_recognition.recognition.ort.get_available_providers",
|
|
||||||
return_value=["CPUExecutionProvider"],
|
|
||||||
)
|
|
||||||
face_recognizer = FaceRecognizer("buffalo_s", min_score=0.0, cache_dir="test_cache")
|
face_recognizer = FaceRecognizer("buffalo_s", min_score=0.0, cache_dir="test_cache")
|
||||||
|
|
||||||
num_faces = 2
|
num_faces = 2
|
||||||
@@ -864,10 +860,6 @@ class TestFaceRecognition:
|
|||||||
)
|
)
|
||||||
mocker.patch("immich_ml.models.base.InferenceModel.download")
|
mocker.patch("immich_ml.models.base.InferenceModel.download")
|
||||||
mocker.patch("immich_ml.models.facial_recognition.recognition.ArcFaceONNX")
|
mocker.patch("immich_ml.models.facial_recognition.recognition.ArcFaceONNX")
|
||||||
mocker.patch(
|
|
||||||
"immich_ml.models.facial_recognition.recognition.ort.get_available_providers",
|
|
||||||
return_value=["CPUExecutionProvider"],
|
|
||||||
)
|
|
||||||
ort_session.return_value.get_inputs.return_value = [SimpleNamespace(name="input.1", shape=(1, 3, 224, 224))]
|
ort_session.return_value.get_inputs.return_value = [SimpleNamespace(name="input.1", shape=(1, 3, 224, 224))]
|
||||||
ort_session.return_value.get_outputs.return_value = [SimpleNamespace(name="output.1", shape=(1, 800))]
|
ort_session.return_value.get_outputs.return_value = [SimpleNamespace(name="output.1", shape=(1, 800))]
|
||||||
path.return_value.__truediv__.return_value.__truediv__.return_value.suffix = ".onnx"
|
path.return_value.__truediv__.return_value.__truediv__.return_value.suffix = ".onnx"
|
||||||
@@ -902,10 +894,6 @@ class TestFaceRecognition:
|
|||||||
)
|
)
|
||||||
mocker.patch("immich_ml.models.base.InferenceModel.download")
|
mocker.patch("immich_ml.models.base.InferenceModel.download")
|
||||||
mocker.patch("immich_ml.models.facial_recognition.recognition.ArcFaceONNX")
|
mocker.patch("immich_ml.models.facial_recognition.recognition.ArcFaceONNX")
|
||||||
mocker.patch(
|
|
||||||
"immich_ml.models.facial_recognition.recognition.ort.get_available_providers",
|
|
||||||
return_value=["CPUExecutionProvider"],
|
|
||||||
)
|
|
||||||
path.return_value.__truediv__.return_value.__truediv__.return_value.suffix = ".onnx"
|
path.return_value.__truediv__.return_value.__truediv__.return_value.suffix = ".onnx"
|
||||||
|
|
||||||
inputs = [SimpleNamespace(name="input.1", shape=("batch", 3, 224, 224))]
|
inputs = [SimpleNamespace(name="input.1", shape=("batch", 3, 224, 224))]
|
||||||
@@ -1008,10 +996,6 @@ class TestFaceRecognition:
|
|||||||
|
|
||||||
def test_ignore_other_custom_max_batch_size(self, mocker: MockerFixture) -> None:
|
def test_ignore_other_custom_max_batch_size(self, mocker: MockerFixture) -> None:
|
||||||
mocker.patch.object(settings, "max_batch_size", MaxBatchSize(ocr=2))
|
mocker.patch.object(settings, "max_batch_size", MaxBatchSize(ocr=2))
|
||||||
mocker.patch(
|
|
||||||
"immich_ml.models.facial_recognition.recognition.ort.get_available_providers",
|
|
||||||
return_value=["CPUExecutionProvider"],
|
|
||||||
)
|
|
||||||
|
|
||||||
recognizer = FaceRecognizer("buffalo_l", cache_dir="test_cache")
|
recognizer = FaceRecognizer("buffalo_l", cache_dir="test_cache")
|
||||||
|
|
||||||
@@ -1044,12 +1028,7 @@ class TestOcr:
|
|||||||
text_recognizer.load()
|
text_recognizer.load()
|
||||||
|
|
||||||
rapid_recognizer.assert_called_once_with(
|
rapid_recognizer.assert_called_once_with(
|
||||||
OcrOptions(
|
OcrOptions(session=ort_session.return_value, rec_batch_num=6, rec_img_shape=(3, 48, 320))
|
||||||
session=ort_session.return_value,
|
|
||||||
rec_batch_num=6,
|
|
||||||
rec_img_shape=(3, 48, 320),
|
|
||||||
model_root_dir=text_recognizer.cache_dir,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_set_custom_max_batch_size(self, ort_session: mock.Mock, path: mock.Mock, mocker: MockerFixture) -> None:
|
def test_set_custom_max_batch_size(self, ort_session: mock.Mock, path: mock.Mock, mocker: MockerFixture) -> None:
|
||||||
@@ -1062,12 +1041,7 @@ class TestOcr:
|
|||||||
text_recognizer.load()
|
text_recognizer.load()
|
||||||
|
|
||||||
rapid_recognizer.assert_called_once_with(
|
rapid_recognizer.assert_called_once_with(
|
||||||
OcrOptions(
|
OcrOptions(session=ort_session.return_value, rec_batch_num=4, rec_img_shape=(3, 48, 320))
|
||||||
session=ort_session.return_value,
|
|
||||||
rec_batch_num=4,
|
|
||||||
rec_img_shape=(3, 48, 320),
|
|
||||||
model_root_dir=text_recognizer.cache_dir,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_ignore_other_custom_max_batch_size(
|
def test_ignore_other_custom_max_batch_size(
|
||||||
@@ -1082,12 +1056,7 @@ class TestOcr:
|
|||||||
text_recognizer.load()
|
text_recognizer.load()
|
||||||
|
|
||||||
rapid_recognizer.assert_called_once_with(
|
rapid_recognizer.assert_called_once_with(
|
||||||
OcrOptions(
|
OcrOptions(session=ort_session.return_value, rec_batch_num=6, rec_img_shape=(3, 48, 320))
|
||||||
session=ort_session.return_value,
|
|
||||||
rec_batch_num=6,
|
|
||||||
rec_img_shape=(3, 48, 320),
|
|
||||||
model_root_dir=text_recognizer.cache_dir,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Generated
+450
-498
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,9 @@
|
|||||||
#! /usr/bin/env node
|
#! /usr/bin/env node
|
||||||
import { readFileSync, writeFileSync } from 'node:fs';
|
const { readFileSync, writeFileSync } = require('node:fs');
|
||||||
|
|
||||||
const asVersion = (item) => {
|
const asVersion = (item) => {
|
||||||
const { label, url } = item;
|
const { label, url } = item;
|
||||||
const [version] = label.substring(1).split('-');
|
const [major, minor, patch] = label.substring(1).split('.').map(Number);
|
||||||
const [major, minor, patch] = version.split('.').map(Number);
|
|
||||||
return { major, minor, patch, label, url };
|
return { major, minor, patch, label, url };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,7 +31,7 @@ for (const item of versions) {
|
|||||||
) {
|
) {
|
||||||
versions = versions.filter((item) => item.label !== version.label);
|
versions = versions.filter((item) => item.label !== version.label);
|
||||||
console.log(
|
console.log(
|
||||||
`Removed ${version.label} (replaced with ${lastVersion.label})`,
|
`Removed ${version.label} (replaced with ${lastVersion.label})`
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -42,5 +41,5 @@ for (const item of versions) {
|
|||||||
|
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
filename,
|
filename,
|
||||||
JSON.stringify([newVersion, ...versions], null, 2) + '\n',
|
JSON.stringify([newVersion, ...versions], null, 2) + '\n'
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,14 +3,12 @@
|
|||||||
#
|
#
|
||||||
# Pump one or both of the server/mobile versions in appropriate files
|
# Pump one or both of the server/mobile versions in appropriate files
|
||||||
#
|
#
|
||||||
# usage: './scripts/pump-version.sh -s <minor|patch|premajor|preminor|prepatch|prerelease> <-m> <true|false>
|
# usage: './scripts/pump-version.sh -s <major|minor|patch> <-m> <true|false>
|
||||||
#
|
#
|
||||||
# examples:
|
# examples:
|
||||||
# ./scripts/pump-version.sh -s major # 1.0.0+50 => 2.0.0+50
|
# ./scripts/pump-version.sh -s major # 1.0.0+50 => 2.0.0+50
|
||||||
# ./scripts/pump-version.sh -s minor -m true # 1.0.0+50 => 1.1.0+51
|
# ./scripts/pump-version.sh -s minor -m true # 1.0.0+50 => 1.1.0+51
|
||||||
# ./scripts/pump-version.sh -s premajor # 1.0.0+50 => 2.0.0-rc.0+50
|
# ./scripts/pump-version.sh -m true # 1.0.0+50 => 1.0.0+51
|
||||||
# ./scripts/pump-version.sh -s prerelease # 2.0.0-rc.0+50 => 2.0.0-rc.1+50
|
|
||||||
# ./scripts/pump-version.sh -m true # 1.0.0+50 => 1.0.0+51
|
|
||||||
#
|
#
|
||||||
|
|
||||||
SERVER_PUMP="false"
|
SERVER_PUMP="false"
|
||||||
@@ -27,15 +25,31 @@ while getopts 's:m:' flag; do
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
CURRENT_SERVER=$(jq -r '.version' package.json)
|
CURRENT_SERVER=$(jq -r '.version' server/package.json)
|
||||||
if ! NEXT_SERVER=$(pnpm --silent pump "$CURRENT_SERVER" "$SERVER_PUMP"); then
|
MAJOR=$(echo "$CURRENT_SERVER" | cut -d '.' -f1)
|
||||||
echo "Fatal: failed to pump server version: $NEXT_SERVER" >&2
|
MINOR=$(echo "$CURRENT_SERVER" | cut -d '.' -f2)
|
||||||
|
PATCH=$(echo "$CURRENT_SERVER" | cut -d '.' -f3)
|
||||||
|
|
||||||
|
if [[ $SERVER_PUMP == "major" ]]; then
|
||||||
|
MAJOR=$((MAJOR + 1))
|
||||||
|
MINOR=0
|
||||||
|
PATCH=0
|
||||||
|
elif [[ $SERVER_PUMP == "minor" ]]; then
|
||||||
|
MINOR=$((MINOR + 1))
|
||||||
|
PATCH=0
|
||||||
|
elif [[ $SERVER_PUMP == "patch" ]]; then
|
||||||
|
PATCH=$((PATCH + 1))
|
||||||
|
elif [[ $SERVER_PUMP == "false" ]]; then
|
||||||
|
echo 'Skipping Server Pump'
|
||||||
|
else
|
||||||
|
echo 'Expected <major|minor|patch|false> for the server argument'
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
NEXT_SERVER=$MAJOR.$MINOR.$PATCH
|
||||||
|
|
||||||
CURRENT_MOBILE=$(grep "^version: .*+[0-9]\+$" mobile/pubspec.yaml | cut -d "+" -f2)
|
CURRENT_MOBILE=$(grep "^version: .*+[0-9]\+$" mobile/pubspec.yaml | cut -d "+" -f2)
|
||||||
NEXT_MOBILE=$CURRENT_MOBILE
|
NEXT_MOBILE=$CURRENT_MOBILE
|
||||||
|
|
||||||
if [[ $MOBILE_PUMP == "true" ]]; then
|
if [[ $MOBILE_PUMP == "true" ]]; then
|
||||||
set $((NEXT_MOBILE++))
|
set $((NEXT_MOBILE++))
|
||||||
elif [[ $MOBILE_PUMP == "false" ]]; then
|
elif [[ $MOBILE_PUMP == "false" ]]; then
|
||||||
@@ -45,17 +59,15 @@ else
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then
|
if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then
|
||||||
echo "Pumping Server: $CURRENT_SERVER => $NEXT_SERVER"
|
echo "Pumping Server: $CURRENT_SERVER => $NEXT_SERVER"
|
||||||
|
|
||||||
pnpm version "$NEXT_SERVER" --no-git-tag-version --no-git-checks
|
pnpm version "$NEXT_SERVER" --no-git-tag-version
|
||||||
pnpm version "$NEXT_SERVER" --no-git-tag-version --no-git-checks --prefix server
|
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix server
|
||||||
pnpm version "$NEXT_SERVER" --no-git-tag-version --no-git-checks --prefix packages/cli
|
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix packages/cli
|
||||||
pnpm version "$NEXT_SERVER" --no-git-tag-version --no-git-checks --prefix web
|
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix web
|
||||||
pnpm version "$NEXT_SERVER" --no-git-tag-version --no-git-checks --prefix e2e
|
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix e2e
|
||||||
pnpm version "$NEXT_SERVER" --no-git-tag-version --no-git-checks --prefix packages/sdk
|
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix packages/sdk
|
||||||
|
|
||||||
# copy version to open-api spec
|
# copy version to open-api spec
|
||||||
mise run //:open-api
|
mise run //:open-api
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { pump } from './pump.js';
|
|
||||||
|
|
||||||
const [versionRaw, type] = process.argv.slice(2);
|
|
||||||
const { message, exitCode } = pump(versionRaw, type);
|
|
||||||
|
|
||||||
console.log(message);
|
|
||||||
process.exit(exitCode);
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import semver, { SemVer } from 'semver';
|
|
||||||
|
|
||||||
const printUsage = () => {
|
|
||||||
return {
|
|
||||||
message:
|
|
||||||
'Usage: ./pump_cli.js <semver> <minor|patch|premajor|preminor|prepatch|prerelease|release>',
|
|
||||||
exitCode: 1,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const isPrerelease = (version) => version.prerelease.length > 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {SemVer} version
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
const inc = (version, type) => `v${semver.inc(version, type, {}, 'rc')}`;
|
|
||||||
|
|
||||||
/** @param {string} version */
|
|
||||||
const normalize = (version) => {
|
|
||||||
if (version.startsWith('v')) {
|
|
||||||
version = version.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return version;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} versionRaw
|
|
||||||
* @param {string} type
|
|
||||||
*/
|
|
||||||
export const pump = (versionRaw, type) => {
|
|
||||||
if (!versionRaw) {
|
|
||||||
return printUsage();
|
|
||||||
}
|
|
||||||
|
|
||||||
versionRaw = normalize(versionRaw);
|
|
||||||
|
|
||||||
const version = semver.parse(versionRaw);
|
|
||||||
if (!version) {
|
|
||||||
return printUsage();
|
|
||||||
}
|
|
||||||
|
|
||||||
let newVersionRaw;
|
|
||||||
let valid = true;
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case 'patch':
|
|
||||||
case 'prepatch':
|
|
||||||
case 'minor':
|
|
||||||
case 'preminor':
|
|
||||||
case 'premajor': {
|
|
||||||
newVersionRaw = inc(version, type);
|
|
||||||
// can only use while not in a prerelease
|
|
||||||
valid = !isPrerelease(version);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'prerelease': {
|
|
||||||
newVersionRaw = inc(version, type);
|
|
||||||
// can only use while in a prerelease
|
|
||||||
valid = isPrerelease(version);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'release': {
|
|
||||||
// drop prerelease part
|
|
||||||
newVersionRaw = `${version.major}.${version.minor}.${version.patch}`;
|
|
||||||
// can only use to promote a prerelease to a release (no version change)
|
|
||||||
valid = isPrerelease(version);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
return printUsage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!newVersionRaw) {
|
|
||||||
return printUsage();
|
|
||||||
}
|
|
||||||
|
|
||||||
newVersionRaw = normalize(newVersionRaw);
|
|
||||||
|
|
||||||
const newVersion = semver.parse(newVersionRaw);
|
|
||||||
if (!newVersion) {
|
|
||||||
return printUsage();
|
|
||||||
}
|
|
||||||
|
|
||||||
const invalidUpgrade =
|
|
||||||
isPrerelease(version) &&
|
|
||||||
!isPrerelease(newVersion) &&
|
|
||||||
(version.major !== newVersion.major ||
|
|
||||||
version.minor !== newVersion.minor ||
|
|
||||||
version.patch !== newVersion.patch);
|
|
||||||
|
|
||||||
if (!valid || invalidUpgrade) {
|
|
||||||
return {
|
|
||||||
message: `Invalid pump: ${type}. Pumping from ${versionRaw} to ${newVersionRaw} is not allowed.`,
|
|
||||||
exitCode: 1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { message: newVersionRaw, exitCode: 0 };
|
|
||||||
};
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
import { pump } from './pump';
|
|
||||||
|
|
||||||
describe(pump.name, () => {
|
|
||||||
describe('usage', () => {
|
|
||||||
it.each([
|
|
||||||
[],
|
|
||||||
['2.7.5'],
|
|
||||||
['2.7.5', 'invalid'],
|
|
||||||
['invalid', 'patch'],
|
|
||||||
['2.7.5', 'major'],
|
|
||||||
])('should not accept $0, $1 as inputs', (version, type) => {
|
|
||||||
expect(pump(version, type)).toEqual({
|
|
||||||
message: expect.stringContaining('Usage: '),
|
|
||||||
exitCode: 1,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('transitions', () => {
|
|
||||||
const valid = [
|
|
||||||
{
|
|
||||||
name: 'patch',
|
|
||||||
items: [['patch', '2.7.5', '2.7.6']],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'prepatch',
|
|
||||||
items: [
|
|
||||||
['prepatch', '2.7.5', '2.7.6-rc.0'],
|
|
||||||
['prerelease', '2.7.6-rc.0', '2.7.6-rc.1'],
|
|
||||||
['release', '2.7.6-rc.1', '2.7.6'],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'minor',
|
|
||||||
items: [['minor', '2.7.5', '2.8.0']],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'preminor',
|
|
||||||
items: [
|
|
||||||
['preminor', '2.7.5', '2.8.0-rc.0'],
|
|
||||||
['prerelease', '2.8.0-rc.0', '2.8.0-rc.1'],
|
|
||||||
['release', '2.8.0-rc.1', '2.8.0'],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'premajor',
|
|
||||||
items: [
|
|
||||||
['premajor', '2.7.5', '3.0.0-rc.0'],
|
|
||||||
['prerelease', '3.0.0-rc.0', '3.0.0-rc.1'],
|
|
||||||
['release', '3.0.0-rc.1', '3.0.0'],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const group of valid) {
|
|
||||||
describe(group.name, () => {
|
|
||||||
it.each(group.items)(
|
|
||||||
'should allow a $0 from $1 to $2',
|
|
||||||
(type, version, next) => {
|
|
||||||
expect(pump(version, type)).toEqual({
|
|
||||||
message: next,
|
|
||||||
exitCode: 0,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('invalid', () => {
|
|
||||||
it.each([
|
|
||||||
['patch', 'v3.0.0-rc.0'],
|
|
||||||
['prepatch', 'v3.0.0-rc.0'],
|
|
||||||
['minor', 'v3.0.0-rc.0'],
|
|
||||||
['preminor', 'v3.0.0-rc.0'],
|
|
||||||
['premajor', 'v3.0.0-rc.0'],
|
|
||||||
['prerelease', 'v3.0.0'],
|
|
||||||
['release', 'v3.0.0'],
|
|
||||||
])('should not allow a $0 on $1', (type, version) => {
|
|
||||||
expect(pump(version, type)).toEqual({
|
|
||||||
message: expect.stringContaining('Invalid pump'),
|
|
||||||
exitCode: 1,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,5 +1,74 @@
|
|||||||
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
||||||
|
|
||||||
|
[[tools."aqua:flutter/flutter"]]
|
||||||
|
version = "3.44.0"
|
||||||
|
backend = "aqua:flutter/flutter"
|
||||||
|
|
||||||
|
[tools."aqua:flutter/flutter"."platforms.linux-arm64"]
|
||||||
|
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.0-stable.tar.xz"
|
||||||
|
|
||||||
|
[tools."aqua:flutter/flutter"."platforms.linux-arm64-musl"]
|
||||||
|
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.0-stable.tar.xz"
|
||||||
|
|
||||||
|
[tools."aqua:flutter/flutter"."platforms.linux-x64"]
|
||||||
|
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.0-stable.tar.xz"
|
||||||
|
|
||||||
|
[tools."aqua:flutter/flutter"."platforms.linux-x64-musl"]
|
||||||
|
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.0-stable.tar.xz"
|
||||||
|
|
||||||
|
[tools."aqua:flutter/flutter"."platforms.macos-arm64"]
|
||||||
|
checksum = "blake3:fb03aa5d9790205c948922ec3f0751c16e4575b09d6ae9dd4fbeb664a69f0e00"
|
||||||
|
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_arm64_3.44.0-stable.zip"
|
||||||
|
|
||||||
|
[tools."aqua:flutter/flutter"."platforms.macos-x64"]
|
||||||
|
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_3.44.0-stable.zip"
|
||||||
|
|
||||||
|
[tools."aqua:flutter/flutter"."platforms.windows-x64"]
|
||||||
|
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_3.44.0-stable.zip"
|
||||||
|
|
||||||
|
[[tools.flutter]]
|
||||||
|
version = "3.41.9-stable"
|
||||||
|
backend = "asdf:flutter"
|
||||||
|
|
||||||
|
[[tools."github:CQLabs/homebrew-dcm"]]
|
||||||
|
version = "1.37.0"
|
||||||
|
backend = "github:CQLabs/homebrew-dcm"
|
||||||
|
|
||||||
|
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-arm64"]
|
||||||
|
checksum = "sha256:253da2512b149913dfe345bf9a62a79acb2d730f66e71162ba4a92dfc4224b82"
|
||||||
|
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-arm-release.zip"
|
||||||
|
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543838"
|
||||||
|
|
||||||
|
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-arm64-musl"]
|
||||||
|
checksum = "sha256:253da2512b149913dfe345bf9a62a79acb2d730f66e71162ba4a92dfc4224b82"
|
||||||
|
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-arm-release.zip"
|
||||||
|
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543838"
|
||||||
|
|
||||||
|
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-x64"]
|
||||||
|
checksum = "sha256:477e086d4099c12f21e5ccd83b005d5fb945dd4cac4fd127fd9a08d7649af1cf"
|
||||||
|
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-x64-release.zip"
|
||||||
|
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543797"
|
||||||
|
|
||||||
|
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-x64-musl"]
|
||||||
|
checksum = "sha256:477e086d4099c12f21e5ccd83b005d5fb945dd4cac4fd127fd9a08d7649af1cf"
|
||||||
|
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-x64-release.zip"
|
||||||
|
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543797"
|
||||||
|
|
||||||
|
[tools."github:CQLabs/homebrew-dcm"."platforms.macos-arm64"]
|
||||||
|
checksum = "sha256:30bede64367d09067093cc57af6ec9496d7717898138ded5cb98a16ac8dd9d93"
|
||||||
|
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-macos-arm-release.zip"
|
||||||
|
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543757"
|
||||||
|
|
||||||
|
[tools."github:CQLabs/homebrew-dcm"."platforms.macos-x64"]
|
||||||
|
checksum = "sha256:e56cb99872be7445a4de1d37e5438ca70e3bcd83be7a2b9b385e3538881f8068"
|
||||||
|
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-macos-x64-release.zip"
|
||||||
|
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543727"
|
||||||
|
|
||||||
|
[tools."github:CQLabs/homebrew-dcm"."platforms.windows-x64"]
|
||||||
|
checksum = "sha256:f133470daa3fb0427f039b424392af7e917d7e7db6b556aa2a968ab0e31587da"
|
||||||
|
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-windows-release.zip"
|
||||||
|
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543660"
|
||||||
|
|
||||||
[[tools."github:extism/cli"]]
|
[[tools."github:extism/cli"]]
|
||||||
version = "1.6.3"
|
version = "1.6.3"
|
||||||
backend = "github:extism/cli"
|
backend = "github:extism/cli"
|
||||||
@@ -82,8 +151,40 @@ url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353224133"
|
|||||||
version = "7.1.3-6"
|
version = "7.1.3-6"
|
||||||
backend = "github:jellyfin/jellyfin-ffmpeg"
|
backend = "github:jellyfin/jellyfin-ffmpeg"
|
||||||
|
|
||||||
[tools."github:jellyfin/jellyfin-ffmpeg".options]
|
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64"]
|
||||||
asset_pattern = "jellyfin-ffmpeg_*_portable_linuxarm64-gpl.tar.xz"
|
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
|
||||||
|
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
|
||||||
|
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
|
||||||
|
|
||||||
|
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64-musl"]
|
||||||
|
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
|
||||||
|
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
|
||||||
|
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
|
||||||
|
|
||||||
|
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64"]
|
||||||
|
checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2"
|
||||||
|
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz"
|
||||||
|
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879"
|
||||||
|
|
||||||
|
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64-musl"]
|
||||||
|
checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2"
|
||||||
|
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz"
|
||||||
|
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879"
|
||||||
|
|
||||||
|
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.macos-arm64"]
|
||||||
|
checksum = "sha256:e024d5e78d5414e75f0181036cd21373fafb9270c72894dfd7dbda2572439820"
|
||||||
|
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_macarm64-gpl.tar.xz"
|
||||||
|
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/408995838"
|
||||||
|
|
||||||
|
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.macos-x64"]
|
||||||
|
checksum = "sha256:066ede9774aaae97a18098aaeea8b7e0d286653eb8618f640476e99c59a536c2"
|
||||||
|
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_mac64-gpl.tar.xz"
|
||||||
|
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/408995889"
|
||||||
|
|
||||||
|
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.windows-x64"]
|
||||||
|
checksum = "sha256:7b7168149689610296f3a187c717056ce0786cc125a31caf28056737e9ba1cc1"
|
||||||
|
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_win64-clang-gpl.zip"
|
||||||
|
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409036094"
|
||||||
|
|
||||||
[[tools."github:webassembly/binaryen"]]
|
[[tools."github:webassembly/binaryen"]]
|
||||||
version = "version_124"
|
version = "version_124"
|
||||||
@@ -217,39 +318,9 @@ checksum = "sha256:27323f70c875b8251bfd7e61a4cffc3ebff4e56ed1e611b955016f0c70773
|
|||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_windows_amd64.tar.gz"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_windows_amd64.tar.gz"
|
||||||
|
|
||||||
[[tools.pnpm]]
|
[[tools.pnpm]]
|
||||||
version = "11.4.0"
|
version = "10.33.4"
|
||||||
backend = "aqua:pnpm/pnpm"
|
backend = "aqua:pnpm/pnpm"
|
||||||
|
|
||||||
[tools.pnpm."platforms.linux-arm64"]
|
|
||||||
checksum = "sha256:cc38ebd5b2610a5744f84576b963c49e6609a8df5aed714ae3de749998d4478c"
|
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.4.0/pnpm-linux-arm64.tar.gz"
|
|
||||||
provenance = "github-attestations"
|
|
||||||
|
|
||||||
[tools.pnpm."platforms.linux-arm64-musl"]
|
|
||||||
checksum = "sha256:a1e2ec9123c709fd04b704227cfcf3b50cd2bbbc1bd39d2df414530b5697eb75"
|
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.4.0/pnpm-linux-arm64-musl.tar.gz"
|
|
||||||
provenance = "github-attestations"
|
|
||||||
|
|
||||||
[tools.pnpm."platforms.linux-x64"]
|
|
||||||
checksum = "sha256:f3f8d1217eef013bbc71a24d52efb1f1041e4aff55edd80e0b08e25f409305a4"
|
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.4.0/pnpm-linux-x64.tar.gz"
|
|
||||||
provenance = "github-attestations"
|
|
||||||
|
|
||||||
[tools.pnpm."platforms.linux-x64-musl"]
|
|
||||||
checksum = "sha256:60010ad00a96b71e20d1618acaca7a71395e710cbd5e88946c030a1d07c56916"
|
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.4.0/pnpm-linux-x64-musl.tar.gz"
|
|
||||||
provenance = "github-attestations"
|
|
||||||
|
|
||||||
[tools.pnpm."platforms.macos-arm64"]
|
|
||||||
checksum = "sha256:ba59014c2c1ce8b76af9f559385206a2623de4ff2b694b5c91598a8f44abb4e2"
|
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.4.0/pnpm-darwin-arm64.tar.gz"
|
|
||||||
provenance = "github-attestations"
|
|
||||||
|
|
||||||
[tools.pnpm."platforms.windows-x64"]
|
|
||||||
checksum = "sha256:84ce90e38bc0b1164173eb853a0fbffc7edcb050cb0d5c8ce4ca609f5c808e0a"
|
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.4.0/pnpm-win32-x64.zip"
|
|
||||||
provenance = "github-attestations"
|
|
||||||
|
|
||||||
[[tools.terragrunt]]
|
[[tools.terragrunt]]
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
backend = "aqua:gruntwork-io/terragrunt"
|
backend = "aqua:gruntwork-io/terragrunt"
|
||||||
|
|||||||
@@ -16,14 +16,27 @@ config_roots = [
|
|||||||
|
|
||||||
[tools]
|
[tools]
|
||||||
node = "24.15.0"
|
node = "24.15.0"
|
||||||
pnpm = "11.4.0"
|
"aqua:flutter/flutter" = "3.44.0"
|
||||||
|
pnpm = "10.33.4"
|
||||||
terragrunt = "1.0.3"
|
terragrunt = "1.0.3"
|
||||||
opentofu = "1.11.6"
|
opentofu = "1.11.6"
|
||||||
|
java = "21.0.2"
|
||||||
"npm:oazapfts" = "7.5.0"
|
"npm:oazapfts" = "7.5.0"
|
||||||
"github:extism/cli" = "1.6.3"
|
"github:extism/cli" = "1.6.3"
|
||||||
"github:webassembly/binaryen" = "version_124"
|
"github:webassembly/binaryen" = "version_124"
|
||||||
"github:extism/js-pdk" = "1.6.0"
|
"github:extism/js-pdk" = "1.6.0"
|
||||||
java = "21.0.2"
|
|
||||||
|
[tools."github:CQLabs/homebrew-dcm"]
|
||||||
|
version = "1.37.0"
|
||||||
|
bin = "dcm"
|
||||||
|
postinstall = "chmod +x \"$MISE_TOOL_INSTALL_PATH/dcm\" || true"
|
||||||
|
|
||||||
|
[tools."github:CQLabs/homebrew-dcm".platforms]
|
||||||
|
linux-x64 = { asset_pattern = "dcm-linux-x64-release.zip" }
|
||||||
|
linux-arm64 = { asset_pattern = "dcm-linux-arm-release.zip" }
|
||||||
|
macos-x64 = { asset_pattern = "dcm-macos-x64-release.zip" }
|
||||||
|
macos-arm64 = { asset_pattern = "dcm-macos-arm-release.zip" }
|
||||||
|
windows-x64 = { asset_pattern = "dcm-windows-release.zip" }
|
||||||
|
|
||||||
[tools."github:jellyfin/jellyfin-ffmpeg"]
|
[tools."github:jellyfin/jellyfin-ffmpeg"]
|
||||||
version = "7.1.3-6"
|
version = "7.1.3-6"
|
||||||
@@ -41,8 +54,8 @@ lockfile = true
|
|||||||
|
|
||||||
[tasks.plugins]
|
[tasks.plugins]
|
||||||
run = [
|
run = [
|
||||||
"pnpm --filter @immich/sdk --filter @immich/plugin-sdk --filter @immich/plugin-core install --frozen-lockfile",
|
"pnpm --filter @immich/plugin-sdk --filter @immich/plugin-core install --frozen-lockfile",
|
||||||
"pnpm --filter @immich/sdk --filter @immich/plugin-sdk --filter @immich/plugin-core build",
|
"pnpm --filter @immich/plugin-sdk --filter @immich/plugin-core build",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tasks.open-api-typescript]
|
[tasks.open-api-typescript]
|
||||||
@@ -71,72 +84,6 @@ run = [
|
|||||||
dir = "server"
|
dir = "server"
|
||||||
run = "node ./dist/bin/sync-sql.js"
|
run = "node ./dist/bin/sync-sql.js"
|
||||||
|
|
||||||
# TODO dev, prod, and e2e should be de-duplicated by using env but for some reason I ran into issues
|
|
||||||
[tasks.dev]
|
|
||||||
depends = "//:plugins"
|
|
||||||
dir = "docker"
|
|
||||||
interactive = true
|
|
||||||
env = { COMPOSE_BAKE = true }
|
|
||||||
run = "docker compose -f ./docker-compose.dev.yml up --remove-orphans"
|
|
||||||
depends_post = "//:dev-down"
|
|
||||||
|
|
||||||
[tasks.dev-update]
|
|
||||||
run = { task = "//:dev", args = ["--build", "-V"] }
|
|
||||||
|
|
||||||
[tasks.dev-scale]
|
|
||||||
run = { task = "//:dev", args = ["--build", "-V", "--scale immich-server=3"] }
|
|
||||||
|
|
||||||
[tasks.dev-down]
|
|
||||||
dir = "docker"
|
|
||||||
run = "docker compose -f ./docker-compose.dev.yml down --remove-orphans"
|
|
||||||
|
|
||||||
[tasks.prod]
|
|
||||||
depends = "//:plugins"
|
|
||||||
dir = "docker"
|
|
||||||
interactive = true
|
|
||||||
env = { COMPOSE_BAKE = true }
|
|
||||||
run = "docker compose -f ./docker-compose.prod.yml up --build --remove-orphans"
|
|
||||||
depends_post = "//:prod-down"
|
|
||||||
|
|
||||||
[tasks.prod-scale]
|
|
||||||
run = { task = "//:prod", args = [
|
|
||||||
"--build",
|
|
||||||
"-V",
|
|
||||||
"--scale immich-server=3",
|
|
||||||
"--scale immich-microservices",
|
|
||||||
] }
|
|
||||||
|
|
||||||
[tasks.prod-down]
|
|
||||||
dir = "docker"
|
|
||||||
run = "docker compose -f ./docker-compose.prod.yml down --remove-orphans"
|
|
||||||
|
|
||||||
[tasks.e2e]
|
|
||||||
depends = "//:plugins"
|
|
||||||
dir = "e2e"
|
|
||||||
interactive = true
|
|
||||||
env = { COMPOSE_BAKE = true }
|
|
||||||
run = "docker compose -f ./docker-compose.yml up --remove-orphans"
|
|
||||||
depends_post = "//:e2e-down"
|
|
||||||
|
|
||||||
[tasks.e2e-dev]
|
|
||||||
depends = "//:plugins"
|
|
||||||
dir = "e2e"
|
|
||||||
interactive = true
|
|
||||||
env = { COMPOSE_BAKE = true }
|
|
||||||
run = "docker compose -f ./docker-compose.dev.yml up --remove-orphans"
|
|
||||||
depends_post = "//:e2e-dev-down"
|
|
||||||
|
|
||||||
[tasks.e2e-update]
|
|
||||||
run = { task = "//:e2e", args = ["--build", '-V'] }
|
|
||||||
|
|
||||||
[tasks.e2e-down]
|
|
||||||
dir = "e2e"
|
|
||||||
run = "docker compose -f ./docker-compose.yml down --remove-orphans"
|
|
||||||
|
|
||||||
[tasks.e2e-dev-down]
|
|
||||||
dir = "e2e"
|
|
||||||
run = "docker compose -f ./docker-compose.dev.yml down --remove-orphans"
|
|
||||||
|
|
||||||
# SDK tasks
|
# SDK tasks
|
||||||
[tasks."sdk:install"]
|
[tasks."sdk:install"]
|
||||||
dir = "packages/sdk"
|
dir = "packages/sdk"
|
||||||
@@ -152,14 +99,3 @@ run = "pnpm format"
|
|||||||
|
|
||||||
[tasks."i18n:format-fix"]
|
[tasks."i18n:format-fix"]
|
||||||
run = "pnpm format:fix"
|
run = "pnpm format:fix"
|
||||||
|
|
||||||
[tasks.clean]
|
|
||||||
run = [
|
|
||||||
"find . -name 'node_modules' -type d -prune -exec rm -rf '{}' +",
|
|
||||||
"find . -name 'dist' -type d -prune -exec rm -rf '{}' +",
|
|
||||||
"find . -name 'build' -type d -prune -exec rm -rf '{}' +",
|
|
||||||
"find . -name '.svelte-kit' -type d -prune -exec rm -rf '{}' +",
|
|
||||||
"find . -name 'coverage' -type d -prune -exec rm -rf '{}' +",
|
|
||||||
"find . -name '.pnpm-store' -type d -prune -exec rm -rf '{}' +",
|
|
||||||
{ task = "//:*-down" },
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -89,20 +89,6 @@
|
|||||||
<data android:mimeType="video/*" />
|
<data android:mimeType="video/*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<!-- Allow Immich to act as an image viewer -->
|
|
||||||
<intent-filter android:label="View in Immich">
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<data android:scheme="content" android:mimeType="image/*" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<!-- Allow Immich to act as a video viewer -->
|
|
||||||
<intent-filter android:label="View in Immich">
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<data android:scheme="content" android:mimeType="video/*" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<!-- immich:// URL scheme handling -->
|
<!-- immich:// URL scheme handling -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package app.alextran.immich
|
package app.alextran.immich
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.ext.SdkExtensions
|
import android.os.ext.SdkExtensions
|
||||||
import app.alextran.immich.background.BackgroundEngineLock
|
import app.alextran.immich.background.BackgroundEngineLock
|
||||||
@@ -23,7 +22,6 @@ import app.alextran.immich.permission.PermissionApiImpl
|
|||||||
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
|
||||||
import app.alextran.immich.viewintent.ViewIntentPlugin
|
|
||||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
|
||||||
@@ -33,11 +31,6 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
registerPlugins(this, flutterEngine)
|
registerPlugins(this, flutterEngine)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
|
||||||
super.onNewIntent(intent)
|
|
||||||
setIntent(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun registerPlugins(ctx: Context, flutterEngine: FlutterEngine) {
|
fun registerPlugins(ctx: Context, flutterEngine: FlutterEngine) {
|
||||||
HttpClientManager.initialize(ctx)
|
HttpClientManager.initialize(ctx)
|
||||||
@@ -62,7 +55,6 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
BackgroundWorkerFgHostApi.setUp(messenger, BackgroundWorkerApiImpl(ctx))
|
BackgroundWorkerFgHostApi.setUp(messenger, BackgroundWorkerApiImpl(ctx))
|
||||||
ConnectivityApi.setUp(messenger, ConnectivityApiImpl(ctx))
|
ConnectivityApi.setUp(messenger, ConnectivityApiImpl(ctx))
|
||||||
|
|
||||||
flutterEngine.plugins.add(ViewIntentPlugin())
|
|
||||||
flutterEngine.plugins.add(backgroundEngineLockImpl)
|
flutterEngine.plugins.add(backgroundEngineLockImpl)
|
||||||
flutterEngine.plugins.add(nativeSyncApiImpl)
|
flutterEngine.plugins.add(nativeSyncApiImpl)
|
||||||
flutterEngine.plugins.add(permissionApiImpl)
|
flutterEngine.plugins.add(permissionApiImpl)
|
||||||
|
|||||||
+2
-43
@@ -47,44 +47,18 @@ class FlutterError (
|
|||||||
override val message: String? = null,
|
override val message: String? = null,
|
||||||
val details: Any? = null
|
val details: Any? = null
|
||||||
) : RuntimeException()
|
) : RuntimeException()
|
||||||
|
|
||||||
enum class PermissionStatus(val raw: Int) {
|
|
||||||
GRANTED(0),
|
|
||||||
DENIED(1),
|
|
||||||
PERMANENTLY_DENIED(2);
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun ofRaw(raw: Int): PermissionStatus? {
|
|
||||||
return values().firstOrNull { it.raw == raw }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private open class PermissionApiPigeonCodec : StandardMessageCodec() {
|
private open class PermissionApiPigeonCodec : StandardMessageCodec() {
|
||||||
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||||
return when (type) {
|
return super.readValueOfType(type, buffer)
|
||||||
129.toByte() -> {
|
|
||||||
return (readValue(buffer) as Long?)?.let {
|
|
||||||
PermissionStatus.ofRaw(it.toInt())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> super.readValueOfType(type, buffer)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
||||||
when (value) {
|
super.writeValue(stream, value)
|
||||||
is PermissionStatus -> {
|
|
||||||
stream.write(129)
|
|
||||||
writeValue(stream, value.raw.toLong())
|
|
||||||
}
|
|
||||||
else -> super.writeValue(stream, value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||||
interface PermissionApi {
|
interface PermissionApi {
|
||||||
fun isIgnoringBatteryOptimizations(): PermissionStatus
|
|
||||||
fun hasManageMediaPermission(): Boolean
|
fun hasManageMediaPermission(): Boolean
|
||||||
fun requestManageMediaPermission(callback: (Result<Boolean>) -> Unit)
|
fun requestManageMediaPermission(callback: (Result<Boolean>) -> Unit)
|
||||||
fun manageMediaPermission(callback: (Result<Boolean>) -> Unit)
|
fun manageMediaPermission(callback: (Result<Boolean>) -> Unit)
|
||||||
@@ -98,21 +72,6 @@ interface PermissionApi {
|
|||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun setUp(binaryMessenger: BinaryMessenger, api: PermissionApi?, messageChannelSuffix: String = "") {
|
fun setUp(binaryMessenger: BinaryMessenger, api: PermissionApi?, messageChannelSuffix: String = "") {
|
||||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||||
run {
|
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.PermissionApi.isIgnoringBatteryOptimizations$separatedMessageChannelSuffix", codec)
|
|
||||||
if (api != null) {
|
|
||||||
channel.setMessageHandler { _, reply ->
|
|
||||||
val wrapped: List<Any?> = try {
|
|
||||||
listOf(api.isIgnoringBatteryOptimizations())
|
|
||||||
} catch (exception: Throwable) {
|
|
||||||
PermissionApiPigeonUtils.wrapError(exception)
|
|
||||||
}
|
|
||||||
reply.reply(wrapped)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
channel.setMessageHandler(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
run {
|
run {
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.PermissionApi.hasManageMediaPermission$separatedMessageChannelSuffix", codec)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.PermissionApi.hasManageMediaPermission$separatedMessageChannelSuffix", codec)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
|
|||||||
-13
@@ -1,26 +1,13 @@
|
|||||||
package app.alextran.immich.permission
|
package app.alextran.immich.permission
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.PowerManager
|
|
||||||
import app.alextran.immich.core.ImmichPlugin
|
import app.alextran.immich.core.ImmichPlugin
|
||||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||||
|
|
||||||
class PermissionApiImpl(context: Context) : ImmichPlugin(), PermissionApi, ActivityAware {
|
class PermissionApiImpl(context: Context) : ImmichPlugin(), PermissionApi, ActivityAware {
|
||||||
private val ctx: Context = context.applicationContext
|
|
||||||
private val manageMediaPermissionDelegate = ManageMediaPermissionDelegate(context)
|
private val manageMediaPermissionDelegate = ManageMediaPermissionDelegate(context)
|
||||||
|
|
||||||
private val powerManager =
|
|
||||||
ctx.getSystemService(Context.POWER_SERVICE) as PowerManager
|
|
||||||
|
|
||||||
|
|
||||||
override fun isIgnoringBatteryOptimizations(): PermissionStatus {
|
|
||||||
if (powerManager.isIgnoringBatteryOptimizations(ctx.packageName)) {
|
|
||||||
return PermissionStatus.GRANTED
|
|
||||||
}
|
|
||||||
return PermissionStatus.DENIED
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hasManageMediaPermission(): Boolean =
|
override fun hasManageMediaPermission(): Boolean =
|
||||||
manageMediaPermissionDelegate.hasManageMediaPermission()
|
manageMediaPermissionDelegate.hasManageMediaPermission()
|
||||||
|
|
||||||
|
|||||||
+34
-66
@@ -542,17 +542,16 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
|
|||||||
|
|
||||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||||
interface NativeSyncApi {
|
interface NativeSyncApi {
|
||||||
fun shouldFullSync(callback: (Result<Boolean>) -> Unit)
|
fun shouldFullSync(): Boolean
|
||||||
fun getMediaChanges(callback: (Result<SyncDelta>) -> Unit)
|
fun getMediaChanges(): SyncDelta
|
||||||
fun checkpointSync()
|
fun checkpointSync()
|
||||||
fun clearSyncCheckpoint()
|
fun clearSyncCheckpoint()
|
||||||
fun getAssetIdsForAlbum(albumId: String, callback: (Result<List<String>>) -> Unit)
|
fun getAssetIdsForAlbum(albumId: String): List<String>
|
||||||
fun getAlbums(callback: (Result<List<PlatformAlbum>>) -> Unit)
|
fun getAlbums(): List<PlatformAlbum>
|
||||||
fun getAssetsCountSince(albumId: String, timestamp: Long): Long
|
fun getAssetsCountSince(albumId: String, timestamp: Long): Long
|
||||||
fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?, callback: (Result<List<PlatformAsset>>) -> Unit)
|
fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List<PlatformAsset>
|
||||||
fun hashAssets(assetIds: List<String>, allowNetworkAccess: Boolean, callback: (Result<List<HashResult>>) -> Unit)
|
fun hashAssets(assetIds: List<String>, allowNetworkAccess: Boolean, callback: (Result<List<HashResult>>) -> Unit)
|
||||||
fun cancelHashing()
|
fun cancelHashing()
|
||||||
fun cancelSync()
|
|
||||||
fun getTrashedAssets(): Map<String, List<PlatformAsset>>
|
fun getTrashedAssets(): Map<String, List<PlatformAsset>>
|
||||||
fun restoreFromTrashById(mediaId: String, type: Long, callback: (Result<Boolean>) -> Unit)
|
fun restoreFromTrashById(mediaId: String, type: Long, callback: (Result<Boolean>) -> Unit)
|
||||||
fun getCloudIdForAssetIds(assetIds: List<String>): List<CloudIdResult>
|
fun getCloudIdForAssetIds(assetIds: List<String>): List<CloudIdResult>
|
||||||
@@ -571,33 +570,27 @@ interface NativeSyncApi {
|
|||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync$separatedMessageChannelSuffix", codec)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync$separatedMessageChannelSuffix", codec)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
channel.setMessageHandler { _, reply ->
|
channel.setMessageHandler { _, reply ->
|
||||||
api.shouldFullSync{ result: Result<Boolean> ->
|
val wrapped: List<Any?> = try {
|
||||||
val error = result.exceptionOrNull()
|
listOf(api.shouldFullSync())
|
||||||
if (error != null) {
|
} catch (exception: Throwable) {
|
||||||
reply.reply(MessagesPigeonUtils.wrapError(error))
|
MessagesPigeonUtils.wrapError(exception)
|
||||||
} else {
|
|
||||||
val data = result.getOrNull()
|
|
||||||
reply.reply(MessagesPigeonUtils.wrapResult(data))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
run {
|
run {
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges$separatedMessageChannelSuffix", codec)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges$separatedMessageChannelSuffix", codec, taskQueue)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
channel.setMessageHandler { _, reply ->
|
channel.setMessageHandler { _, reply ->
|
||||||
api.getMediaChanges{ result: Result<SyncDelta> ->
|
val wrapped: List<Any?> = try {
|
||||||
val error = result.exceptionOrNull()
|
listOf(api.getMediaChanges())
|
||||||
if (error != null) {
|
} catch (exception: Throwable) {
|
||||||
reply.reply(MessagesPigeonUtils.wrapError(error))
|
MessagesPigeonUtils.wrapError(exception)
|
||||||
} else {
|
|
||||||
val data = result.getOrNull()
|
|
||||||
reply.reply(MessagesPigeonUtils.wrapResult(data))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
@@ -636,38 +629,32 @@ interface NativeSyncApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
run {
|
run {
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum$separatedMessageChannelSuffix", codec)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum$separatedMessageChannelSuffix", codec, taskQueue)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
channel.setMessageHandler { message, reply ->
|
channel.setMessageHandler { message, reply ->
|
||||||
val args = message as List<Any?>
|
val args = message as List<Any?>
|
||||||
val albumIdArg = args[0] as String
|
val albumIdArg = args[0] as String
|
||||||
api.getAssetIdsForAlbum(albumIdArg) { result: Result<List<String>> ->
|
val wrapped: List<Any?> = try {
|
||||||
val error = result.exceptionOrNull()
|
listOf(api.getAssetIdsForAlbum(albumIdArg))
|
||||||
if (error != null) {
|
} catch (exception: Throwable) {
|
||||||
reply.reply(MessagesPigeonUtils.wrapError(error))
|
MessagesPigeonUtils.wrapError(exception)
|
||||||
} else {
|
|
||||||
val data = result.getOrNull()
|
|
||||||
reply.reply(MessagesPigeonUtils.wrapResult(data))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
run {
|
run {
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums$separatedMessageChannelSuffix", codec)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums$separatedMessageChannelSuffix", codec, taskQueue)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
channel.setMessageHandler { _, reply ->
|
channel.setMessageHandler { _, reply ->
|
||||||
api.getAlbums{ result: Result<List<PlatformAlbum>> ->
|
val wrapped: List<Any?> = try {
|
||||||
val error = result.exceptionOrNull()
|
listOf(api.getAlbums())
|
||||||
if (error != null) {
|
} catch (exception: Throwable) {
|
||||||
reply.reply(MessagesPigeonUtils.wrapError(error))
|
MessagesPigeonUtils.wrapError(exception)
|
||||||
} else {
|
|
||||||
val data = result.getOrNull()
|
|
||||||
reply.reply(MessagesPigeonUtils.wrapResult(data))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
@@ -692,21 +679,18 @@ interface NativeSyncApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
run {
|
run {
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum$separatedMessageChannelSuffix", codec)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum$separatedMessageChannelSuffix", codec, taskQueue)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
channel.setMessageHandler { message, reply ->
|
channel.setMessageHandler { message, reply ->
|
||||||
val args = message as List<Any?>
|
val args = message as List<Any?>
|
||||||
val albumIdArg = args[0] as String
|
val albumIdArg = args[0] as String
|
||||||
val updatedTimeCondArg = args[1] as Long?
|
val updatedTimeCondArg = args[1] as Long?
|
||||||
api.getAssetsForAlbum(albumIdArg, updatedTimeCondArg) { result: Result<List<PlatformAsset>> ->
|
val wrapped: List<Any?> = try {
|
||||||
val error = result.exceptionOrNull()
|
listOf(api.getAssetsForAlbum(albumIdArg, updatedTimeCondArg))
|
||||||
if (error != null) {
|
} catch (exception: Throwable) {
|
||||||
reply.reply(MessagesPigeonUtils.wrapError(error))
|
MessagesPigeonUtils.wrapError(exception)
|
||||||
} else {
|
|
||||||
val data = result.getOrNull()
|
|
||||||
reply.reply(MessagesPigeonUtils.wrapResult(data))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
@@ -749,22 +733,6 @@ interface NativeSyncApi {
|
|||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
run {
|
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.cancelSync$separatedMessageChannelSuffix", codec)
|
|
||||||
if (api != null) {
|
|
||||||
channel.setMessageHandler { _, reply ->
|
|
||||||
val wrapped: List<Any?> = try {
|
|
||||||
api.cancelSync()
|
|
||||||
listOf(null)
|
|
||||||
} catch (exception: Throwable) {
|
|
||||||
MessagesPigeonUtils.wrapError(exception)
|
|
||||||
}
|
|
||||||
reply.reply(wrapped)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
channel.setMessageHandler(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
run {
|
run {
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets$separatedMessageChannelSuffix", codec, taskQueue)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets$separatedMessageChannelSuffix", codec, taskQueue)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
|
|||||||
@@ -4,11 +4,7 @@ import android.content.Context
|
|||||||
|
|
||||||
|
|
||||||
class NativeSyncApiImpl26(context: Context) : NativeSyncApiImplBase(context), NativeSyncApi {
|
class NativeSyncApiImpl26(context: Context) : NativeSyncApiImplBase(context), NativeSyncApi {
|
||||||
override fun shouldFullSync(callback: (Result<Boolean>) -> Unit) {
|
override fun shouldFullSync(): Boolean {
|
||||||
runSync(callback) { shouldFullSync() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun shouldFullSync(): Boolean {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,11 +18,7 @@ class NativeSyncApiImpl26(context: Context) : NativeSyncApiImplBase(context), Na
|
|||||||
// No-op for Android 10 and below
|
// No-op for Android 10 and below
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMediaChanges(callback: (Result<SyncDelta>) -> Unit) {
|
override fun getMediaChanges(): SyncDelta {
|
||||||
runSync(callback) { getMediaChanges() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getMediaChanges(): SyncDelta {
|
|
||||||
throw IllegalStateException("Method not supported on this Android version.")
|
throw IllegalStateException("Method not supported on this Android version.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import android.os.Bundle
|
|||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.annotation.RequiresExtension
|
import androidx.annotation.RequiresExtension
|
||||||
import kotlinx.coroutines.currentCoroutineContext
|
|
||||||
import kotlinx.coroutines.ensureActive
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
@@ -37,11 +35,7 @@ class NativeSyncApiImpl30(context: Context) : NativeSyncApiImplBase(context), Na
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun shouldFullSync(callback: (Result<Boolean>) -> Unit) {
|
override fun shouldFullSync(): Boolean =
|
||||||
runSync(callback) { shouldFullSync() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun shouldFullSync(): Boolean =
|
|
||||||
MediaStore.getVersion(ctx) != prefs.getString(SHARED_PREF_MEDIA_STORE_VERSION_KEY, null)
|
MediaStore.getVersion(ctx) != prefs.getString(SHARED_PREF_MEDIA_STORE_VERSION_KEY, null)
|
||||||
|
|
||||||
override fun checkpointSync() {
|
override fun checkpointSync() {
|
||||||
@@ -55,11 +49,7 @@ class NativeSyncApiImpl30(context: Context) : NativeSyncApiImplBase(context), Na
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMediaChanges(callback: (Result<SyncDelta>) -> Unit) {
|
override fun getMediaChanges(): SyncDelta {
|
||||||
runSync(callback) { getMediaChanges() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getMediaChanges(): SyncDelta {
|
|
||||||
val genMap = getSavedGenerationMap()
|
val genMap = getSavedGenerationMap()
|
||||||
val currentVolumes = MediaStore.getExternalVolumeNames(ctx)
|
val currentVolumes = MediaStore.getExternalVolumeNames(ctx)
|
||||||
val changed = mutableListOf<PlatformAsset>()
|
val changed = mutableListOf<PlatformAsset>()
|
||||||
@@ -68,7 +58,6 @@ class NativeSyncApiImpl30(context: Context) : NativeSyncApiImplBase(context), Na
|
|||||||
var hasChanges = genMap.keys != currentVolumes
|
var hasChanges = genMap.keys != currentVolumes
|
||||||
|
|
||||||
for (volume in currentVolumes) {
|
for (volume in currentVolumes) {
|
||||||
currentCoroutineContext().ensureActive()
|
|
||||||
val currentGen = MediaStore.getGeneration(ctx, volume)
|
val currentGen = MediaStore.getGeneration(ctx, volume)
|
||||||
val storedGen = genMap[volume] ?: 0
|
val storedGen = genMap[volume] ?: 0
|
||||||
if (currentGen <= storedGen) {
|
if (currentGen <= storedGen) {
|
||||||
|
|||||||
@@ -45,14 +45,12 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa
|
|||||||
private val ctx: Context = context.applicationContext
|
private val ctx: Context = context.applicationContext
|
||||||
|
|
||||||
private var hashTask: Job? = null
|
private var hashTask: Job? = null
|
||||||
private var syncJob: Job? = null
|
|
||||||
private val mediaTrashDelegate = MediaTrashDelegate(ctx)
|
private val mediaTrashDelegate = MediaTrashDelegate(ctx)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val MAX_CONCURRENT_HASH_OPERATIONS = 16
|
private const val MAX_CONCURRENT_HASH_OPERATIONS = 16
|
||||||
private val hashSemaphore = Semaphore(MAX_CONCURRENT_HASH_OPERATIONS)
|
private val hashSemaphore = Semaphore(MAX_CONCURRENT_HASH_OPERATIONS)
|
||||||
private const val HASHING_CANCELLED_CODE = "HASH_CANCELLED"
|
private const val HASHING_CANCELLED_CODE = "HASH_CANCELLED"
|
||||||
private const val SYNC_CANCELLED_CODE = "SYNC_CANCELLED"
|
|
||||||
|
|
||||||
// MediaStore.Files.FileColumns.SPECIAL_FORMAT — S Extensions 21+
|
// MediaStore.Files.FileColumns.SPECIAL_FORMAT — S Extensions 21+
|
||||||
// https://developer.android.com/reference/android/provider/MediaStore.Files.FileColumns#SPECIAL_FORMAT
|
// https://developer.android.com/reference/android/provider/MediaStore.Files.FileColumns#SPECIAL_FORMAT
|
||||||
@@ -297,11 +295,7 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa
|
|||||||
return PlatformAssetPlaybackStyle.IMAGE
|
return PlatformAssetPlaybackStyle.IMAGE
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAlbums(callback: (Result<List<PlatformAlbum>>) -> Unit) {
|
fun getAlbums(): List<PlatformAlbum> {
|
||||||
runSync(callback) { getAlbums() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getAlbums(): List<PlatformAlbum> {
|
|
||||||
val albums = mutableListOf<PlatformAlbum>()
|
val albums = mutableListOf<PlatformAlbum>()
|
||||||
val albumsCount = mutableMapOf<String, Int>()
|
val albumsCount = mutableMapOf<String, Int>()
|
||||||
|
|
||||||
@@ -328,7 +322,6 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa
|
|||||||
cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATE_MODIFIED)
|
cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATE_MODIFIED)
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
currentCoroutineContext().ensureActive()
|
|
||||||
val id = cursor.getString(bucketIdColumn)
|
val id = cursor.getString(bucketIdColumn)
|
||||||
|
|
||||||
val count = albumsCount.getOrDefault(id, 0)
|
val count = albumsCount.getOrDefault(id, 0)
|
||||||
@@ -349,11 +342,7 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa
|
|||||||
.sortedBy { it.id }
|
.sortedBy { it.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAssetIdsForAlbum(albumId: String, callback: (Result<List<String>>) -> Unit) {
|
fun getAssetIdsForAlbum(albumId: String): List<String> {
|
||||||
runSync(callback) { getAssetIdsForAlbum(albumId) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getAssetIdsForAlbum(albumId: String): List<String> {
|
|
||||||
val projection = arrayOf(MediaStore.MediaColumns._ID)
|
val projection = arrayOf(MediaStore.MediaColumns._ID)
|
||||||
|
|
||||||
return getCursor(
|
return getCursor(
|
||||||
@@ -377,11 +366,7 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa
|
|||||||
)?.use { cursor -> cursor.count.toLong() } ?: 0L
|
)?.use { cursor -> cursor.count.toLong() } ?: 0L
|
||||||
|
|
||||||
|
|
||||||
fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?, callback: (Result<List<PlatformAsset>>) -> Unit) {
|
fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List<PlatformAsset> {
|
||||||
runSync(callback) { getAssetsForAlbum(albumId, updatedTimeCond) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List<PlatformAsset> {
|
|
||||||
var selection = "$BUCKET_SELECTION AND $MEDIA_SELECTION"
|
var selection = "$BUCKET_SELECTION AND $MEDIA_SELECTION"
|
||||||
val selectionArgs = mutableListOf(albumId, *MEDIA_SELECTION_ARGS)
|
val selectionArgs = mutableListOf(albumId, *MEDIA_SELECTION_ARGS)
|
||||||
|
|
||||||
@@ -466,24 +451,6 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa
|
|||||||
hashTask = null
|
hashTask = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancelSync() {
|
|
||||||
syncJob?.cancel()
|
|
||||||
syncJob = null
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun <T> runSync(callback: (Result<T>) -> Unit, work: suspend () -> T) {
|
|
||||||
syncJob?.cancel()
|
|
||||||
syncJob = CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
try {
|
|
||||||
completeWhenActive(callback, Result.success(work()))
|
|
||||||
} catch (e: CancellationException) {
|
|
||||||
completeWhenActive(callback, Result.failure(FlutterError(SYNC_CANCELLED_CODE, "Sync cancelled", null)))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
completeWhenActive(callback, Result.failure(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun restoreFromTrashById(mediaId: String, type: Long, callback: (Result<Boolean>) -> Unit) {
|
fun restoreFromTrashById(mediaId: String, type: Long, callback: (Result<Boolean>) -> Unit) {
|
||||||
mediaTrashDelegate.restoreFromTrashById(mediaId, type) { completeWhenActive(callback, it) }
|
mediaTrashDelegate.restoreFromTrashById(mediaId, type) { completeWhenActive(callback, it) }
|
||||||
}
|
}
|
||||||
|
|||||||
-292
@@ -1,292 +0,0 @@
|
|||||||
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
|
||||||
// See also: https://pub.dev/packages/pigeon
|
|
||||||
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
|
||||||
|
|
||||||
package app.alextran.immich.viewintent
|
|
||||||
|
|
||||||
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 ViewIntentPigeonUtils {
|
|
||||||
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fun doubleEquals(a: Double, b: Double): Boolean {
|
|
||||||
// Normalize -0.0 to 0.0 and handle NaN equality.
|
|
||||||
return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun floatEquals(a: Float, b: Float): Boolean {
|
|
||||||
// Normalize -0.0 to 0.0 and handle NaN equality.
|
|
||||||
return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun doubleHash(d: Double): Int {
|
|
||||||
// Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes.
|
|
||||||
val normalized = if (d == 0.0) 0.0 else d
|
|
||||||
val bits = java.lang.Double.doubleToLongBits(normalized)
|
|
||||||
return (bits xor (bits ushr 32)).toInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun floatHash(f: Float): Int {
|
|
||||||
// Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes.
|
|
||||||
val normalized = if (f == 0.0f) 0.0f else f
|
|
||||||
return java.lang.Float.floatToIntBits(normalized)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deepEquals(a: Any?, b: Any?): Boolean {
|
|
||||||
if (a === b) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (a == null || b == null) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (a is ByteArray && b is ByteArray) {
|
|
||||||
return a.contentEquals(b)
|
|
||||||
}
|
|
||||||
if (a is IntArray && b is IntArray) {
|
|
||||||
return a.contentEquals(b)
|
|
||||||
}
|
|
||||||
if (a is LongArray && b is LongArray) {
|
|
||||||
return a.contentEquals(b)
|
|
||||||
}
|
|
||||||
if (a is DoubleArray && b is DoubleArray) {
|
|
||||||
if (a.size != b.size) return false
|
|
||||||
for (i in a.indices) {
|
|
||||||
if (!doubleEquals(a[i], b[i])) return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (a is FloatArray && b is FloatArray) {
|
|
||||||
if (a.size != b.size) return false
|
|
||||||
for (i in a.indices) {
|
|
||||||
if (!floatEquals(a[i], b[i])) return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (a is Array<*> && b is Array<*>) {
|
|
||||||
if (a.size != b.size) return false
|
|
||||||
for (i in a.indices) {
|
|
||||||
if (!deepEquals(a[i], b[i])) return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (a is List<*> && b is List<*>) {
|
|
||||||
if (a.size != b.size) return false
|
|
||||||
val iterA = a.iterator()
|
|
||||||
val iterB = b.iterator()
|
|
||||||
while (iterA.hasNext() && iterB.hasNext()) {
|
|
||||||
if (!deepEquals(iterA.next(), iterB.next())) return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (a is Map<*, *> && b is Map<*, *>) {
|
|
||||||
if (a.size != b.size) return false
|
|
||||||
for (entry in a) {
|
|
||||||
val key = entry.key
|
|
||||||
var found = false
|
|
||||||
for (bEntry in b) {
|
|
||||||
if (deepEquals(key, bEntry.key)) {
|
|
||||||
if (deepEquals(entry.value, bEntry.value)) {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found) return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (a is Double && b is Double) {
|
|
||||||
return doubleEquals(a, b)
|
|
||||||
}
|
|
||||||
if (a is Float && b is Float) {
|
|
||||||
return floatEquals(a, b)
|
|
||||||
}
|
|
||||||
return a == b
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deepHash(value: Any?): Int {
|
|
||||||
return when (value) {
|
|
||||||
null -> 0
|
|
||||||
is ByteArray -> value.contentHashCode()
|
|
||||||
is IntArray -> value.contentHashCode()
|
|
||||||
is LongArray -> value.contentHashCode()
|
|
||||||
is DoubleArray -> {
|
|
||||||
var result = 1
|
|
||||||
for (item in value) {
|
|
||||||
result = 31 * result + doubleHash(item)
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
is FloatArray -> {
|
|
||||||
var result = 1
|
|
||||||
for (item in value) {
|
|
||||||
result = 31 * result + floatHash(item)
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
is Array<*> -> {
|
|
||||||
var result = 1
|
|
||||||
for (item in value) {
|
|
||||||
result = 31 * result + deepHash(item)
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
is List<*> -> {
|
|
||||||
var result = 1
|
|
||||||
for (item in value) {
|
|
||||||
result = 31 * result + deepHash(item)
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
is Map<*, *> -> {
|
|
||||||
var result = 0
|
|
||||||
for (entry in value) {
|
|
||||||
result += ((deepHash(entry.key) * 31) xor deepHash(entry.value))
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
is Double -> doubleHash(value)
|
|
||||||
is Float -> floatHash(value)
|
|
||||||
else -> value.hashCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
) : RuntimeException()
|
|
||||||
|
|
||||||
/** Generated class from Pigeon that represents data sent in messages. */
|
|
||||||
data class ViewIntentPayload (
|
|
||||||
val path: String? = null,
|
|
||||||
val mimeType: String,
|
|
||||||
val localAssetId: String? = null
|
|
||||||
)
|
|
||||||
{
|
|
||||||
companion object {
|
|
||||||
fun fromList(pigeonVar_list: List<Any?>): ViewIntentPayload {
|
|
||||||
val path = pigeonVar_list[0] as String?
|
|
||||||
val mimeType = pigeonVar_list[1] as String
|
|
||||||
val localAssetId = pigeonVar_list[2] as String?
|
|
||||||
return ViewIntentPayload(path, mimeType, localAssetId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fun toList(): List<Any?> {
|
|
||||||
return listOf(
|
|
||||||
path,
|
|
||||||
mimeType,
|
|
||||||
localAssetId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (other == null || other.javaClass != javaClass) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (this === other) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
val other = other as ViewIntentPayload
|
|
||||||
return ViewIntentPigeonUtils.deepEquals(this.path, other.path) && ViewIntentPigeonUtils.deepEquals(this.mimeType, other.mimeType) && ViewIntentPigeonUtils.deepEquals(this.localAssetId, other.localAssetId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = javaClass.hashCode()
|
|
||||||
result = 31 * result + ViewIntentPigeonUtils.deepHash(this.path)
|
|
||||||
result = 31 * result + ViewIntentPigeonUtils.deepHash(this.mimeType)
|
|
||||||
result = 31 * result + ViewIntentPigeonUtils.deepHash(this.localAssetId)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private open class ViewIntentPigeonCodec : StandardMessageCodec() {
|
|
||||||
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
|
||||||
return when (type) {
|
|
||||||
129.toByte() -> {
|
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
|
||||||
ViewIntentPayload.fromList(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> super.readValueOfType(type, buffer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
|
||||||
when (value) {
|
|
||||||
is ViewIntentPayload -> {
|
|
||||||
stream.write(129)
|
|
||||||
writeValue(stream, value.toList())
|
|
||||||
}
|
|
||||||
else -> super.writeValue(stream, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
|
||||||
interface ViewIntentHostApi {
|
|
||||||
fun consumeViewIntent(callback: (Result<ViewIntentPayload?>) -> Unit)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
/** The codec used by ViewIntentHostApi. */
|
|
||||||
val codec: MessageCodec<Any?> by lazy {
|
|
||||||
ViewIntentPigeonCodec()
|
|
||||||
}
|
|
||||||
/** Sets up an instance of `ViewIntentHostApi` to handle messages through the `binaryMessenger`. */
|
|
||||||
@JvmOverloads
|
|
||||||
fun setUp(binaryMessenger: BinaryMessenger, api: ViewIntentHostApi?, messageChannelSuffix: String = "") {
|
|
||||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
|
||||||
run {
|
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ViewIntentHostApi.consumeViewIntent$separatedMessageChannelSuffix", codec)
|
|
||||||
if (api != null) {
|
|
||||||
channel.setMessageHandler { _, reply ->
|
|
||||||
api.consumeViewIntent{ result: Result<ViewIntentPayload?> ->
|
|
||||||
val error = result.exceptionOrNull()
|
|
||||||
if (error != null) {
|
|
||||||
reply.reply(ViewIntentPigeonUtils.wrapError(error))
|
|
||||||
} else {
|
|
||||||
val data = result.getOrNull()
|
|
||||||
reply.reply(ViewIntentPigeonUtils.wrapResult(data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
channel.setMessageHandler(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-201
@@ -1,201 +0,0 @@
|
|||||||
package app.alextran.immich.viewintent
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.provider.DocumentsContract
|
|
||||||
import android.provider.MediaStore
|
|
||||||
import android.provider.OpenableColumns
|
|
||||||
import android.util.Log
|
|
||||||
import android.webkit.MimeTypeMap
|
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
|
||||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
|
||||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
|
||||||
import io.flutter.plugin.common.PluginRegistry
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
private const val TAG = "ViewIntentPlugin"
|
|
||||||
|
|
||||||
class ViewIntentPlugin : FlutterPlugin, ActivityAware, PluginRegistry.NewIntentListener, ViewIntentHostApi {
|
|
||||||
private var context: Context? = null
|
|
||||||
private var activity: Activity? = null
|
|
||||||
private var unconsumedIntent: Intent? = null
|
|
||||||
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
||||||
|
|
||||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
|
||||||
context = binding.applicationContext
|
|
||||||
ViewIntentHostApi.setUp(binding.binaryMessenger, this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
|
||||||
ViewIntentHostApi.setUp(binding.binaryMessenger, null)
|
|
||||||
ioScope.cancel()
|
|
||||||
context = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
|
||||||
activity = binding.activity
|
|
||||||
unconsumedIntent = binding.activity.intent
|
|
||||||
binding.addOnNewIntentListener(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetachedFromActivityForConfigChanges() {
|
|
||||||
activity = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
|
||||||
onAttachedToActivity(binding)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetachedFromActivity() {
|
|
||||||
activity = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent): Boolean {
|
|
||||||
unconsumedIntent = intent
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun consumeViewIntent(callback: (Result<ViewIntentPayload?>) -> Unit) {
|
|
||||||
val context = context ?: run {
|
|
||||||
callback(Result.success(null))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val intent = unconsumedIntent ?: activity?.intent
|
|
||||||
|
|
||||||
if (intent?.action != Intent.ACTION_VIEW) {
|
|
||||||
callback(Result.success(null))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val uri = intent.data
|
|
||||||
if (uri == null) {
|
|
||||||
callback(Result.success(null))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ioScope.launch {
|
|
||||||
try {
|
|
||||||
val mimeType = context.contentResolver.getType(uri) ?: intent.type
|
|
||||||
if (mimeType == null || (!mimeType.startsWith("image/") && !mimeType.startsWith("video/"))) {
|
|
||||||
callback(Result.success(null))
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
val localAssetId = extractLocalAssetId(context, uri, mimeType)
|
|
||||||
val tempFilePath = if (localAssetId == null) {
|
|
||||||
copyUriToTempFile(context, uri, mimeType)?.absolutePath ?: run {
|
|
||||||
callback(Result.success(null))
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
val payload = ViewIntentPayload(
|
|
||||||
path = tempFilePath,
|
|
||||||
mimeType = mimeType,
|
|
||||||
localAssetId = localAssetId,
|
|
||||||
)
|
|
||||||
consumeViewIntent(intent)
|
|
||||||
callback(Result.success(payload))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
callback(Result.failure(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun consumeViewIntent(currentIntent: Intent) {
|
|
||||||
unconsumedIntent = Intent(currentIntent).apply {
|
|
||||||
action = null
|
|
||||||
data = null
|
|
||||||
type = null
|
|
||||||
}
|
|
||||||
activity?.intent = unconsumedIntent
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun extractLocalAssetId(context: Context, uri: Uri, mimeType: String): String? {
|
|
||||||
return tryExtractDocumentLocalAssetId(context, uri)
|
|
||||||
?: tryParseContentUriId(uri)
|
|
||||||
?: resolveLocalIdByNameAndSize(context, uri, mimeType)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun tryExtractDocumentLocalAssetId(context: Context, uri: Uri): String? {
|
|
||||||
return try {
|
|
||||||
if (!DocumentsContract.isDocumentUri(context, uri)) return null
|
|
||||||
val docId = DocumentsContract.getDocumentId(uri)
|
|
||||||
if (docId.isBlank() || docId.startsWith("raw:")) return null
|
|
||||||
docId.substringAfter(':', docId).toLongOrNull()?.toString()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(TAG, "Failed to resolve local asset id from document URI: $uri", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun tryParseContentUriId(uri: Uri): String? {
|
|
||||||
val id = uri.lastPathSegment?.toLongOrNull() ?: return null
|
|
||||||
return if (id >= 0) id.toString() else null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyUriToTempFile(context: Context, uri: Uri, mimeType: String): File? {
|
|
||||||
return try {
|
|
||||||
val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)?.let { ".$it" }
|
|
||||||
val tempFile = File.createTempFile("view_intent_", extension, context.cacheDir)
|
|
||||||
context.contentResolver.openInputStream(uri)?.use { inputStream ->
|
|
||||||
FileOutputStream(tempFile).use { outputStream ->
|
|
||||||
inputStream.copyTo(outputStream)
|
|
||||||
}
|
|
||||||
} ?: return null
|
|
||||||
tempFile
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resolveLocalIdByNameAndSize(context: Context, uri: Uri, mimeType: String): String? {
|
|
||||||
val metaProjection = arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)
|
|
||||||
val (displayName, size) =
|
|
||||||
try {
|
|
||||||
context.contentResolver.query(uri, metaProjection, null, null, null)?.use { cursor ->
|
|
||||||
if (!cursor.moveToFirst()) return null
|
|
||||||
val nameIdx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
|
||||||
val sizeIdx = cursor.getColumnIndex(OpenableColumns.SIZE)
|
|
||||||
val name = if (nameIdx >= 0) cursor.getString(nameIdx) else null
|
|
||||||
val bytes = if (sizeIdx >= 0) cursor.getLong(sizeIdx) else -1L
|
|
||||||
if (name.isNullOrBlank() || bytes < 0) return null
|
|
||||||
name to bytes
|
|
||||||
} ?: return null
|
|
||||||
} catch (_: Exception) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val tableUri = when {
|
|
||||||
mimeType.startsWith("image/") -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
|
||||||
mimeType.startsWith("video/") -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
|
||||||
else -> return null
|
|
||||||
}
|
|
||||||
return try {
|
|
||||||
context.contentResolver
|
|
||||||
.query(
|
|
||||||
tableUri,
|
|
||||||
arrayOf(MediaStore.MediaColumns._ID),
|
|
||||||
"${MediaStore.MediaColumns.DISPLAY_NAME}=? AND ${MediaStore.MediaColumns.SIZE}=?",
|
|
||||||
arrayOf(displayName, size.toString()),
|
|
||||||
"${MediaStore.MediaColumns.DATE_MODIFIED} DESC",
|
|
||||||
)?.use { cursor ->
|
|
||||||
if (!cursor.moveToFirst()) return null
|
|
||||||
val idIndex = cursor.getColumnIndex(MediaStore.MediaColumns._ID)
|
|
||||||
if (idIndex < 0) return null
|
|
||||||
cursor.getLong(idIndex).toString()
|
|
||||||
}
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-3368
File diff suppressed because it is too large
Load Diff
-3391
File diff suppressed because it is too large
Load Diff
-3603
File diff suppressed because it is too large
Load Diff
@@ -1,154 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:drift/drift.dart' show Value;
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
|
||||||
import 'package:immich_mobile/domain/utils/background_sync.dart';
|
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
|
||||||
import 'package:immich_mobile/main.dart' as app;
|
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
|
||||||
import 'package:immich_mobile/utils/bootstrap.dart';
|
|
||||||
import 'package:immich_mobile/wm_executor.dart';
|
|
||||||
import 'package:integration_test/integration_test.dart';
|
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
import 'test_utils/fake_immich_server.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
|
||||||
// These tests do real I/O without pumping a widget tree, so disable the fake async clock
|
|
||||||
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;
|
|
||||||
|
|
||||||
late Drift drift;
|
|
||||||
late FakeImmichServer server;
|
|
||||||
|
|
||||||
setUpAll(() async {
|
|
||||||
await app.initApp();
|
|
||||||
(drift, _) = await Bootstrap.initDomain();
|
|
||||||
});
|
|
||||||
|
|
||||||
setUp(() async {
|
|
||||||
await workerManagerPatch.init(dynamicSpawning: true);
|
|
||||||
server = await FakeImmichServer.start();
|
|
||||||
await ApiService().resolveAndSetEndpoint(server.endpoint);
|
|
||||||
await drift.delete(drift.userEntity).go();
|
|
||||||
await Store.delete(StoreKey.syncMigrationStatus);
|
|
||||||
});
|
|
||||||
|
|
||||||
tearDown(() async {
|
|
||||||
await workerManagerPatch.dispose();
|
|
||||||
await server.close();
|
|
||||||
await Store.delete(StoreKey.serverEndpoint);
|
|
||||||
await Store.delete(StoreKey.syncMigrationStatus);
|
|
||||||
});
|
|
||||||
|
|
||||||
void sendUser(SyncStream stream, String id, String name) {
|
|
||||||
stream.send(
|
|
||||||
type: SyncEntityType.userV1.value,
|
|
||||||
data: SyncUserV1(
|
|
||||||
id: id,
|
|
||||||
name: name,
|
|
||||||
email: '$id@test.com',
|
|
||||||
hasProfileImage: false,
|
|
||||||
deletedAt: null,
|
|
||||||
profileChangedAt: DateTime.utc(2025),
|
|
||||||
).toJson(),
|
|
||||||
ack: id,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> dbReadable() async {
|
|
||||||
try {
|
|
||||||
await drift.customSelect('SELECT 1').get().timeout(const Duration(seconds: 5));
|
|
||||||
return true;
|
|
||||||
} catch (_) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<int> userCount() async => (await drift.select(drift.userEntity).get()).length;
|
|
||||||
|
|
||||||
// Starts a remote sync and resolves once its /sync/stream request is open.
|
|
||||||
Future<(Future<bool>, SyncStream)> startSync() async {
|
|
||||||
final sync = BackgroundSyncManager().syncRemote();
|
|
||||||
final stream = await server.streamOpened.timeout(
|
|
||||||
const Duration(seconds: 30),
|
|
||||||
onTimeout: () => fail('sync isolate never opened /sync/stream'),
|
|
||||||
);
|
|
||||||
return (sync, stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
testWidgets('a full sync ingests streamed events into the shared DB', (tester) async {
|
|
||||||
expect(await userCount(), 0);
|
|
||||||
|
|
||||||
final (sync, stream) = await startSync();
|
|
||||||
|
|
||||||
sendUser(stream, 'u1', 'Alice');
|
|
||||||
sendUser(stream, 'u2', 'Bob');
|
|
||||||
await stream.close();
|
|
||||||
|
|
||||||
final result = await sync.timeout(
|
|
||||||
const Duration(seconds: 30),
|
|
||||||
onTimeout: () => fail('sync did not complete after the stream ended'),
|
|
||||||
);
|
|
||||||
expect(result, isTrue);
|
|
||||||
expect(await userCount(), 2);
|
|
||||||
expect(server.ackRequests, greaterThan(0));
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('disposing the pool during an in-flight sync drains promptly', (tester) async {
|
|
||||||
final (sync, _) = await startSync();
|
|
||||||
|
|
||||||
final sw = Stopwatch()..start();
|
|
||||||
await workerManagerPatch.dispose().timeout(
|
|
||||||
const Duration(seconds: 15),
|
|
||||||
onTimeout: () => fail('dispose() hung — worker did not drain and exit'),
|
|
||||||
);
|
|
||||||
expect(sw.elapsed, lessThan(const Duration(seconds: 10)), reason: 'abort-driven, not socket-timeout bound');
|
|
||||||
|
|
||||||
expect(await sync.timeout(const Duration(seconds: 5), onTimeout: () => false), isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('tearing down a worker blocked mid-write leaves the DB usable', (tester) async {
|
|
||||||
final (sync, stream) = await startSync();
|
|
||||||
|
|
||||||
// Hold an exclusive write transaction so the worker's write is blocked. The lock is taken only
|
|
||||||
// after the stream opens to avoid blocking the worker's own startup DB reads.
|
|
||||||
final releaseTxn = Completer<void>();
|
|
||||||
final txnHeld = Completer<void>();
|
|
||||||
final txn = drift.transaction(() async {
|
|
||||||
await drift.into(drift.userEntity).insert(
|
|
||||||
UserEntityCompanion.insert(
|
|
||||||
id: 'holder',
|
|
||||||
name: 'holder',
|
|
||||||
email: 'holder@test.com',
|
|
||||||
hasProfileImage: const Value(false),
|
|
||||||
profileChangedAt: Value(DateTime.utc(2025)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
txnHeld.complete();
|
|
||||||
await releaseTxn.future;
|
|
||||||
});
|
|
||||||
await txnHeld.future;
|
|
||||||
|
|
||||||
sendUser(stream, 'u1', 'Alice');
|
|
||||||
await stream.close();
|
|
||||||
|
|
||||||
// dispose() can only finish once the worker unwinds, which is blocked on the
|
|
||||||
// lock — so start it, release the lock, then await completion.
|
|
||||||
final disposed = workerManagerPatch.dispose();
|
|
||||||
releaseTxn.complete();
|
|
||||||
await txn;
|
|
||||||
await disposed.timeout(
|
|
||||||
const Duration(seconds: 15),
|
|
||||||
onTimeout: () => fail('dispose() hung after releasing the write lock'),
|
|
||||||
);
|
|
||||||
await sync.timeout(const Duration(seconds: 5), onTimeout: () => false);
|
|
||||||
|
|
||||||
expect(await dbReadable(), isTrue);
|
|
||||||
final users = await drift.select(drift.userEntity).get();
|
|
||||||
expect(users.map((u) => u.id), contains('holder'));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
/// A dummy localhost server that implements only the endpoints that remote-sync touches.
|
|
||||||
class FakeImmichServer {
|
|
||||||
FakeImmichServer._(this._server, this.version);
|
|
||||||
|
|
||||||
final HttpServer _server;
|
|
||||||
final (int, int, int) version;
|
|
||||||
|
|
||||||
final Completer<SyncStream> _streamOpened = Completer<SyncStream>();
|
|
||||||
|
|
||||||
int ackRequests = 0;
|
|
||||||
|
|
||||||
String get endpoint => 'http://${_server.address.host}:${_server.port}/api';
|
|
||||||
|
|
||||||
/// Resolves when the sync isolate opens `POST /sync/stream`.
|
|
||||||
Future<SyncStream> get streamOpened => _streamOpened.future;
|
|
||||||
|
|
||||||
static Future<FakeImmichServer> start({(int, int, int) version = (3, 0, 0)}) async {
|
|
||||||
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
|
||||||
final fake = FakeImmichServer._(server, version);
|
|
||||||
fake._listen();
|
|
||||||
return fake;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _listen() {
|
|
||||||
// A connection torn down mid-write during teardown is expected
|
|
||||||
_server.listen((request) => unawaited(_route(request).catchError((_) {})));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _route(HttpRequest request) async {
|
|
||||||
final method = request.method;
|
|
||||||
final path = request.uri.path;
|
|
||||||
|
|
||||||
if (method == 'GET' && path == '/api/server/ping') {
|
|
||||||
return _respondJson(request, {'res': 'pong'});
|
|
||||||
}
|
|
||||||
if (method == 'GET' && path == '/api/server/version') {
|
|
||||||
final (major, minor, patch) = version;
|
|
||||||
return _respondJson(request, {'major': major, 'minor': minor, 'patch': patch});
|
|
||||||
}
|
|
||||||
if (path == '/api/sync/ack') {
|
|
||||||
if (method != 'DELETE') {
|
|
||||||
ackRequests++;
|
|
||||||
}
|
|
||||||
return _respondEmpty(request);
|
|
||||||
}
|
|
||||||
if (method == 'POST' && path == '/api/sync/stream') {
|
|
||||||
return _openSyncStream(request);
|
|
||||||
}
|
|
||||||
return _respondEmpty(request, status: HttpStatus.notFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _openSyncStream(HttpRequest request) async {
|
|
||||||
await request.drain<void>();
|
|
||||||
request.response
|
|
||||||
..statusCode = HttpStatus.ok
|
|
||||||
..headers.contentType = ContentType('application', 'jsonlines+json')
|
|
||||||
..contentLength = -1 // chunked: stays open to stream incrementally
|
|
||||||
..bufferOutput = false;
|
|
||||||
// Flush headers so the client's send() resolves and enters its read loop.
|
|
||||||
await request.response.flush();
|
|
||||||
if (!_streamOpened.isCompleted) {
|
|
||||||
_streamOpened.complete(SyncStream._(request.response));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _respondJson(HttpRequest request, Object body) async {
|
|
||||||
await request.drain<void>();
|
|
||||||
request.response
|
|
||||||
..statusCode = HttpStatus.ok
|
|
||||||
..headers.contentType = ContentType.json
|
|
||||||
..write(jsonEncode(body));
|
|
||||||
await request.response.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _respondEmpty(HttpRequest request, {int status = HttpStatus.ok}) async {
|
|
||||||
await request.drain<void>();
|
|
||||||
request.response.statusCode = status;
|
|
||||||
await request.response.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> close() async {
|
|
||||||
if (_streamOpened.isCompleted) {
|
|
||||||
await (await _streamOpened.future).close();
|
|
||||||
}
|
|
||||||
await _server.close(force: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle to the open `/sync/stream` response: push jsonlines events, then end.
|
|
||||||
class SyncStream {
|
|
||||||
SyncStream._(this._response);
|
|
||||||
|
|
||||||
final HttpResponse _response;
|
|
||||||
bool _closed = false;
|
|
||||||
|
|
||||||
/// [data] should be a Sync*V1 DTO's `toJson()` so the parser's `fromJson` round-trips it.
|
|
||||||
void send({required String type, required Object data, required String ack}) {
|
|
||||||
if (_closed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_response.write('${jsonEncode({'type': type, 'data': data, 'ack': ack})}\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> close() async {
|
|
||||||
if (_closed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_closed = true;
|
|
||||||
await _response.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-2
@@ -2,5 +2,4 @@ source "https://rubygems.org"
|
|||||||
|
|
||||||
gem "fastlane"
|
gem "fastlane"
|
||||||
gem "cocoapods"
|
gem "cocoapods"
|
||||||
gem "abbrev" # Required for Ruby 3.4+
|
gem "abbrev" # Required for Ruby 3.4+
|
||||||
gem "multi_json"
|
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||||
467DA6EAF83F3481F8BD94AB /* Pods_ShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8AB817AA297EDEC88B23F3F6 /* Pods_ShareExtension.framework */; };
|
3B6A31FED0FC846D6BD69BBC /* Pods_ShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */; };
|
||||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
B2EE00022E72CA15008B6CA7 /* PermissionApi.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2EE00012E72CA15008B6CA7 /* PermissionApi.g.swift */; };
|
B2EE00022E72CA15008B6CA7 /* PermissionApi.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2EE00012E72CA15008B6CA7 /* PermissionApi.g.swift */; };
|
||||||
B2EE00042E72CA15008B6CA7 /* PermissionApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2EE00032E72CA15008B6CA7 /* PermissionApiImpl.swift */; };
|
B2EE00042E72CA15008B6CA7 /* PermissionApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2EE00032E72CA15008B6CA7 /* PermissionApiImpl.swift */; };
|
||||||
B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */; };
|
B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */; };
|
||||||
D3BED739C0BC29BB32E18EB2 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CC499FBCE6B29B2DAFED7130 /* Pods_Runner.framework */; };
|
D218389C4A4C4693F141F7D1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */; };
|
||||||
F02538E92DFBCBDD008C3FA3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
F02538E92DFBCBDD008C3FA3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
F0B57D3A2DF764BD00DC5BCC /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F0B57D392DF764BD00DC5BCC /* WidgetKit.framework */; };
|
F0B57D3A2DF764BD00DC5BCC /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F0B57D392DF764BD00DC5BCC /* WidgetKit.framework */; };
|
||||||
F0B57D3C2DF764BD00DC5BCC /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F0B57D3B2DF764BD00DC5BCC /* SwiftUI.framework */; };
|
F0B57D3C2DF764BD00DC5BCC /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F0B57D3B2DF764BD00DC5BCC /* SwiftUI.framework */; };
|
||||||
@@ -85,18 +85,16 @@
|
|||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
10B378D23F917891A0F23E33 /* Pods-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.release.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.release.xcconfig"; sourceTree = "<group>"; };
|
|
||||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||||
|
2E3441B73560D0F6FD25E04F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||||
614A7F5DC5DB09E89E4FCBE8 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
571EAA93D77181C7C98C2EA6 /* Pods-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.release.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
681FBA560D5D2ADDE4F0B59E /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
|
||||||
6D160F04A389B9FFBC557803 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
|
||||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
8AB817AA297EDEC88B23F3F6 /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
937632897A02DE9C249F20A6 /* Pods-ShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.debug.xcconfig"; sourceTree = "<group>"; };
|
|
||||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||||
97C146EE1CF9000F007C117D /* Immich-Debug.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Immich-Debug.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
97C146EE1CF9000F007C117D /* Immich-Debug.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Immich-Debug.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
@@ -105,6 +103,7 @@
|
|||||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
A01DD6982F7F43B40049AB63 /* ImageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRequest.swift; sourceTree = "<group>"; };
|
A01DD6982F7F43B40049AB63 /* ImageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRequest.swift; sourceTree = "<group>"; };
|
||||||
|
B1FBA9EE014DE20271B0FE77 /* Pods-ShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorkerApiImpl.swift; sourceTree = "<group>"; };
|
B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorkerApiImpl.swift; sourceTree = "<group>"; };
|
||||||
B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.swift; sourceTree = "<group>"; };
|
B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.swift; sourceTree = "<group>"; };
|
||||||
B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connectivity.g.swift; sourceTree = "<group>"; };
|
B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connectivity.g.swift; sourceTree = "<group>"; };
|
||||||
@@ -112,11 +111,12 @@
|
|||||||
B2EE00012E72CA15008B6CA7 /* PermissionApi.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionApi.g.swift; sourceTree = "<group>"; };
|
B2EE00012E72CA15008B6CA7 /* PermissionApi.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionApi.g.swift; sourceTree = "<group>"; };
|
||||||
B2EE00032E72CA15008B6CA7 /* PermissionApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionApiImpl.swift; sourceTree = "<group>"; };
|
B2EE00032E72CA15008B6CA7 /* PermissionApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionApiImpl.swift; sourceTree = "<group>"; };
|
||||||
B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.g.swift; sourceTree = "<group>"; };
|
B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.g.swift; sourceTree = "<group>"; };
|
||||||
C4A6A71F33CE37B3C913115C /* Pods-ShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.profile.xcconfig"; sourceTree = "<group>"; };
|
E0E99CDC17B3EB7FA8BA2332 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
CC499FBCE6B29B2DAFED7130 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
F0B57D382DF764BD00DC5BCC /* WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
F0B57D382DF764BD00DC5BCC /* WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
F0B57D392DF764BD00DC5BCC /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
F0B57D392DF764BD00DC5BCC /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
||||||
F0B57D3B2DF764BD00DC5BCC /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
|
F0B57D3B2DF764BD00DC5BCC /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
|
||||||
|
F7101BB0391A314774615E89 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
F8A35EA3C3E01BD66AFDE0E5 /* Pods-ShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
FA9973382CF6DF4B000EF859 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
|
FA9973382CF6DF4B000EF859 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
|
||||||
FAC6F8902D287C890078CB2F /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
FAC6F8902D287C890078CB2F /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
FAC6F8B12D287F120078CB2F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
FAC6F8B12D287F120078CB2F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
@@ -199,7 +199,7 @@
|
|||||||
FEE084F82EC172460045228E /* SQLiteData in Frameworks */,
|
FEE084F82EC172460045228E /* SQLiteData in Frameworks */,
|
||||||
FEE084FB2EC1725A0045228E /* RawStructuredFieldValues in Frameworks */,
|
FEE084FB2EC1725A0045228E /* RawStructuredFieldValues in Frameworks */,
|
||||||
FEE084FD2EC1725A0045228E /* StructuredFieldValues in Frameworks */,
|
FEE084FD2EC1725A0045228E /* StructuredFieldValues in Frameworks */,
|
||||||
D3BED739C0BC29BB32E18EB2 /* Pods_Runner.framework in Frameworks */,
|
D218389C4A4C4693F141F7D1 /* Pods_Runner.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -216,7 +216,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
467DA6EAF83F3481F8BD94AB /* Pods_ShareExtension.framework in Frameworks */,
|
3B6A31FED0FC846D6BD69BBC /* Pods_ShareExtension.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -226,12 +226,12 @@
|
|||||||
0FB772A5B9601143383626CA /* Pods */ = {
|
0FB772A5B9601143383626CA /* Pods */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
614A7F5DC5DB09E89E4FCBE8 /* Pods-Runner.debug.xcconfig */,
|
2E3441B73560D0F6FD25E04F /* Pods-Runner.debug.xcconfig */,
|
||||||
6D160F04A389B9FFBC557803 /* Pods-Runner.release.xcconfig */,
|
E0E99CDC17B3EB7FA8BA2332 /* Pods-Runner.release.xcconfig */,
|
||||||
681FBA560D5D2ADDE4F0B59E /* Pods-Runner.profile.xcconfig */,
|
F7101BB0391A314774615E89 /* Pods-Runner.profile.xcconfig */,
|
||||||
937632897A02DE9C249F20A6 /* Pods-ShareExtension.debug.xcconfig */,
|
F8A35EA3C3E01BD66AFDE0E5 /* Pods-ShareExtension.debug.xcconfig */,
|
||||||
10B378D23F917891A0F23E33 /* Pods-ShareExtension.release.xcconfig */,
|
571EAA93D77181C7C98C2EA6 /* Pods-ShareExtension.release.xcconfig */,
|
||||||
C4A6A71F33CE37B3C913115C /* Pods-ShareExtension.profile.xcconfig */,
|
B1FBA9EE014DE20271B0FE77 /* Pods-ShareExtension.profile.xcconfig */,
|
||||||
);
|
);
|
||||||
path = Pods;
|
path = Pods;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -239,10 +239,10 @@
|
|||||||
1754452DD81DA6620E279E51 /* Frameworks */ = {
|
1754452DD81DA6620E279E51 /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */,
|
||||||
|
357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */,
|
||||||
F0B57D392DF764BD00DC5BCC /* WidgetKit.framework */,
|
F0B57D392DF764BD00DC5BCC /* WidgetKit.framework */,
|
||||||
F0B57D3B2DF764BD00DC5BCC /* SwiftUI.framework */,
|
F0B57D3B2DF764BD00DC5BCC /* SwiftUI.framework */,
|
||||||
CC499FBCE6B29B2DAFED7130 /* Pods_Runner.framework */,
|
|
||||||
8AB817AA297EDEC88B23F3F6 /* Pods_ShareExtension.framework */,
|
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -370,7 +370,7 @@
|
|||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
BAEA01ACA3F5C9CD3D732370 /* [CP] Check Pods Manifest.lock */,
|
4044AF030EF7D8721844FFBA /* [CP] Check Pods Manifest.lock */,
|
||||||
9740EEB61CF901F6004384FC /* Run Script */,
|
9740EEB61CF901F6004384FC /* Run Script */,
|
||||||
97C146EA1CF9000F007C117D /* Sources */,
|
97C146EA1CF9000F007C117D /* Sources */,
|
||||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||||
@@ -378,8 +378,8 @@
|
|||||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
FAC6F89A2D287C890078CB2F /* Embed Foundation Extensions */,
|
FAC6F89A2D287C890078CB2F /* Embed Foundation Extensions */,
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
513DA7292DED6106813332F4 /* [CP] Embed Pods Frameworks */,
|
D218A34AEE62BC1EF119F5B0 /* [CP] Embed Pods Frameworks */,
|
||||||
2FA39DEC809D6D7C4A01EFCB /* [CP] Copy Pods Resources */,
|
6724EEB7D74949FA08581154 /* [CP] Copy Pods Resources */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@@ -393,9 +393,6 @@
|
|||||||
FEE084F22EC172080045228E /* Schemas */,
|
FEE084F22EC172080045228E /* Schemas */,
|
||||||
);
|
);
|
||||||
name = Runner;
|
name = Runner;
|
||||||
packageProductDependencies = (
|
|
||||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
|
||||||
);
|
|
||||||
productName = Runner;
|
productName = Runner;
|
||||||
productReference = 97C146EE1CF9000F007C117D /* Immich-Debug.app */;
|
productReference = 97C146EE1CF9000F007C117D /* Immich-Debug.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
@@ -424,7 +421,7 @@
|
|||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = FAC6F8A02D287C890078CB2F /* Build configuration list for PBXNativeTarget "ShareExtension" */;
|
buildConfigurationList = FAC6F8A02D287C890078CB2F /* Build configuration list for PBXNativeTarget "ShareExtension" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
8EC9CF3E20AF32BF24D4F3E1 /* [CP] Check Pods Manifest.lock */,
|
3BEF3D71D97E337D921C0EB5 /* [CP] Check Pods Manifest.lock */,
|
||||||
FAC6F88C2D287C890078CB2F /* Sources */,
|
FAC6F88C2D287C890078CB2F /* Sources */,
|
||||||
FAC6F88D2D287C890078CB2F /* Frameworks */,
|
FAC6F88D2D287C890078CB2F /* Frameworks */,
|
||||||
FAC6F88E2D287C890078CB2F /* Resources */,
|
FAC6F88E2D287C890078CB2F /* Resources */,
|
||||||
@@ -473,7 +470,7 @@
|
|||||||
);
|
);
|
||||||
mainGroup = 97C146E51CF9000F007C117D;
|
mainGroup = 97C146E51CF9000F007C117D;
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
|
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
|
||||||
FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */,
|
FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */,
|
||||||
FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */,
|
FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */,
|
||||||
);
|
);
|
||||||
@@ -520,23 +517,6 @@
|
|||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXShellScriptBuildPhase section */
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
2FA39DEC809D6D7C4A01EFCB /* [CP] Copy Pods Resources */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
inputFileListPaths = (
|
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
|
||||||
);
|
|
||||||
name = "[CP] Copy Pods Resources";
|
|
||||||
outputFileListPaths = (
|
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
|
||||||
showEnvVarsInLog = 0;
|
|
||||||
};
|
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
alwaysOutOfDate = 1;
|
alwaysOutOfDate = 1;
|
||||||
@@ -553,24 +533,7 @@
|
|||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||||
};
|
};
|
||||||
513DA7292DED6106813332F4 /* [CP] Embed Pods Frameworks */ = {
|
3BEF3D71D97E337D921C0EB5 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
inputFileListPaths = (
|
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
|
||||||
);
|
|
||||||
name = "[CP] Embed Pods Frameworks";
|
|
||||||
outputFileListPaths = (
|
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
|
||||||
showEnvVarsInLog = 0;
|
|
||||||
};
|
|
||||||
8EC9CF3E20AF32BF24D4F3E1 /* [CP] Check Pods Manifest.lock */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
@@ -592,22 +555,7 @@
|
|||||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
showEnvVarsInLog = 0;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
4044AF030EF7D8721844FFBA /* [CP] Check Pods Manifest.lock */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
alwaysOutOfDate = 1;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
inputPaths = (
|
|
||||||
);
|
|
||||||
name = "Run Script";
|
|
||||||
outputPaths = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n";
|
|
||||||
};
|
|
||||||
BAEA01ACA3F5C9CD3D732370 /* [CP] Check Pods Manifest.lock */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
@@ -629,6 +577,55 @@
|
|||||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
showEnvVarsInLog = 0;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
|
6724EEB7D74949FA08581154 /* [CP] Copy Pods Resources */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
|
);
|
||||||
|
name = "[CP] Copy Pods Resources";
|
||||||
|
outputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
name = "Run Script";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n";
|
||||||
|
};
|
||||||
|
D218A34AEE62BC1EF119F5B0 /* [CP] Embed Pods Frameworks */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
|
);
|
||||||
|
name = "[CP] Embed Pods Frameworks";
|
||||||
|
outputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
@@ -1095,7 +1092,7 @@
|
|||||||
};
|
};
|
||||||
FAC6F89C2D287C890078CB2F /* Debug */ = {
|
FAC6F89C2D287C890078CB2F /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 937632897A02DE9C249F20A6 /* Pods-ShareExtension.debug.xcconfig */;
|
baseConfigurationReference = F8A35EA3C3E01BD66AFDE0E5 /* Pods-ShareExtension.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
@@ -1138,7 +1135,7 @@
|
|||||||
};
|
};
|
||||||
FAC6F89D2D287C890078CB2F /* Release */ = {
|
FAC6F89D2D287C890078CB2F /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 10B378D23F917891A0F23E33 /* Pods-ShareExtension.release.xcconfig */;
|
baseConfigurationReference = 571EAA93D77181C7C98C2EA6 /* Pods-ShareExtension.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
@@ -1178,7 +1175,7 @@
|
|||||||
};
|
};
|
||||||
FAC6F89E2D287C890078CB2F /* Profile */ = {
|
FAC6F89E2D287C890078CB2F /* Profile */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = C4A6A71F33CE37B3C913115C /* Pods-ShareExtension.profile.xcconfig */;
|
baseConfigurationReference = B1FBA9EE014DE20271B0FE77 /* Pods-ShareExtension.profile.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
@@ -1261,13 +1258,6 @@
|
|||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCLocalSwiftPackageReference section */
|
|
||||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
|
|
||||||
isa = XCLocalSwiftPackageReference;
|
|
||||||
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
|
|
||||||
};
|
|
||||||
/* End XCLocalSwiftPackageReference section */
|
|
||||||
|
|
||||||
/* Begin XCRemoteSwiftPackageReference section */
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */ = {
|
FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
@@ -1288,10 +1278,6 @@
|
|||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
productName = FlutterGeneratedPluginSwiftPackage;
|
|
||||||
};
|
|
||||||
FEE084F72EC172460045228E /* SQLiteData */ = {
|
FEE084F72EC172460045228E /* SQLiteData */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */;
|
package = FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */;
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/pointfreeco/combine-schedulers",
|
"location" : "https://github.com/pointfreeco/combine-schedulers",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "fd16d76fd8b9a976d88bfb6cacc05ca8d19c91b6",
|
"revision" : "5928286acce13def418ec36d05a001a9641086f2",
|
||||||
"version" : "1.1.0"
|
"version" : "1.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -121,8 +121,8 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancels the currently running background task, either due to timeout or external request.
|
* Cancels the currently running background task, either due to timeout or external request.
|
||||||
* Only tears down the engine after Dart confirms it's drained. If Dart overruns iOS's grace window,
|
* Sends a cancel signal to the Flutter side and sets up a fallback timer to ensure
|
||||||
* the expiration handler still calls setTaskCompleted and iOS suspends us.
|
* the completion handler is eventually called even if Flutter doesn't respond.
|
||||||
*/
|
*/
|
||||||
func close() {
|
func close() {
|
||||||
if isComplete {
|
if isComplete {
|
||||||
@@ -132,6 +132,12 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
|||||||
flutterApi?.cancel { result in
|
flutterApi?.cancel { result in
|
||||||
self.complete(success: false)
|
self.complete(success: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback safety mechanism: ensure completion is called within 2 seconds
|
||||||
|
// This prevents the background task from hanging indefinitely if Flutter doesn't respond
|
||||||
|
Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in
|
||||||
|
self.complete(success: false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-63
@@ -46,57 +46,8 @@ private func nilOrValue<T>(_ value: Any?) -> T? {
|
|||||||
return value as! T?
|
return value as! T?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
enum PermissionStatus: Int {
|
|
||||||
case granted = 0
|
|
||||||
case denied = 1
|
|
||||||
case permanentlyDenied = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
private class PermissionApiPigeonCodecReader: FlutterStandardReader {
|
|
||||||
override func readValue(ofType type: UInt8) -> Any? {
|
|
||||||
switch type {
|
|
||||||
case 129:
|
|
||||||
let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?)
|
|
||||||
if let enumResultAsInt = enumResultAsInt {
|
|
||||||
return PermissionStatus(rawValue: enumResultAsInt)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return super.readValue(ofType: type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class PermissionApiPigeonCodecWriter: FlutterStandardWriter {
|
|
||||||
override func writeValue(_ value: Any) {
|
|
||||||
if let value = value as? PermissionStatus {
|
|
||||||
super.writeByte(129)
|
|
||||||
super.writeValue(value.rawValue)
|
|
||||||
} else {
|
|
||||||
super.writeValue(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class PermissionApiPigeonCodecReaderWriter: FlutterStandardReaderWriter {
|
|
||||||
override func reader(with data: Data) -> FlutterStandardReader {
|
|
||||||
return PermissionApiPigeonCodecReader(data: data)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func writer(with data: NSMutableData) -> FlutterStandardWriter {
|
|
||||||
return PermissionApiPigeonCodecWriter(data: data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PermissionApiPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
|
|
||||||
static let shared = PermissionApiPigeonCodec(readerWriter: PermissionApiPigeonCodecReaderWriter())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
||||||
protocol PermissionApi {
|
protocol PermissionApi {
|
||||||
func isIgnoringBatteryOptimizations() throws -> PermissionStatus
|
|
||||||
func hasManageMediaPermission() throws -> Bool
|
func hasManageMediaPermission() throws -> Bool
|
||||||
func requestManageMediaPermission(completion: @escaping (Result<Bool, Error>) -> Void)
|
func requestManageMediaPermission(completion: @escaping (Result<Bool, Error>) -> Void)
|
||||||
func manageMediaPermission(completion: @escaping (Result<Bool, Error>) -> Void)
|
func manageMediaPermission(completion: @escaping (Result<Bool, Error>) -> Void)
|
||||||
@@ -104,23 +55,10 @@ protocol PermissionApi {
|
|||||||
|
|
||||||
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||||
class PermissionApiSetup {
|
class PermissionApiSetup {
|
||||||
static var codec: FlutterStandardMessageCodec { PermissionApiPigeonCodec.shared }
|
static var codec: FlutterStandardMessageCodec { FlutterStandardMessageCodec.sharedInstance() }
|
||||||
/// Sets up an instance of `PermissionApi` to handle messages through the `binaryMessenger`.
|
/// Sets up an instance of `PermissionApi` to handle messages through the `binaryMessenger`.
|
||||||
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: PermissionApi?, messageChannelSuffix: String = "") {
|
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: PermissionApi?, messageChannelSuffix: String = "") {
|
||||||
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
|
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
|
||||||
let isIgnoringBatteryOptimizationsChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.PermissionApi.isIgnoringBatteryOptimizations\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
|
||||||
if let api = api {
|
|
||||||
isIgnoringBatteryOptimizationsChannel.setMessageHandler { _, reply in
|
|
||||||
do {
|
|
||||||
let result = try api.isIgnoringBatteryOptimizations()
|
|
||||||
reply(wrapResult(result))
|
|
||||||
} catch {
|
|
||||||
reply(wrapError(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
isIgnoringBatteryOptimizationsChannel.setMessageHandler(nil)
|
|
||||||
}
|
|
||||||
let hasManageMediaPermissionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.PermissionApi.hasManageMediaPermission\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
let hasManageMediaPermissionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.PermissionApi.hasManageMediaPermission\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
if let api = api {
|
if let api = api {
|
||||||
hasManageMediaPermissionChannel.setMessageHandler { _, reply in
|
hasManageMediaPermissionChannel.setMessageHandler { _, reply in
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class PermissionApiImpl: PermissionApi {
|
class PermissionApiImpl: PermissionApi {
|
||||||
func isIgnoringBatteryOptimizations() throws -> PermissionStatus {
|
|
||||||
return PermissionStatus.granted;
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasManageMediaPermission() throws -> Bool {
|
func hasManageMediaPermission() throws -> Bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+42
-58
@@ -526,17 +526,16 @@ class MessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
|
|||||||
|
|
||||||
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
||||||
protocol NativeSyncApi {
|
protocol NativeSyncApi {
|
||||||
func shouldFullSync(completion: @escaping (Result<Bool, Error>) -> Void)
|
func shouldFullSync() throws -> Bool
|
||||||
func getMediaChanges(completion: @escaping (Result<SyncDelta, Error>) -> Void)
|
func getMediaChanges() throws -> SyncDelta
|
||||||
func checkpointSync() throws
|
func checkpointSync() throws
|
||||||
func clearSyncCheckpoint() throws
|
func clearSyncCheckpoint() throws
|
||||||
func getAssetIdsForAlbum(albumId: String, completion: @escaping (Result<[String], Error>) -> Void)
|
func getAssetIdsForAlbum(albumId: String) throws -> [String]
|
||||||
func getAlbums(completion: @escaping (Result<[PlatformAlbum], Error>) -> Void)
|
func getAlbums() throws -> [PlatformAlbum]
|
||||||
func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64
|
func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64
|
||||||
func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?, completion: @escaping (Result<[PlatformAsset], Error>) -> Void)
|
func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset]
|
||||||
func hashAssets(assetIds: [String], allowNetworkAccess: Bool, completion: @escaping (Result<[HashResult], Error>) -> Void)
|
func hashAssets(assetIds: [String], allowNetworkAccess: Bool, completion: @escaping (Result<[HashResult], Error>) -> Void)
|
||||||
func cancelHashing() throws
|
func cancelHashing() throws
|
||||||
func cancelSync() throws
|
|
||||||
func getTrashedAssets() throws -> [String: [PlatformAsset]]
|
func getTrashedAssets() throws -> [String: [PlatformAsset]]
|
||||||
func restoreFromTrashById(mediaId: String, type: Int64, completion: @escaping (Result<Bool, Error>) -> Void)
|
func restoreFromTrashById(mediaId: String, type: Int64, completion: @escaping (Result<Bool, Error>) -> Void)
|
||||||
func getCloudIdForAssetIds(assetIds: [String]) throws -> [CloudIdResult]
|
func getCloudIdForAssetIds(assetIds: [String]) throws -> [CloudIdResult]
|
||||||
@@ -556,28 +555,26 @@ class NativeSyncApiSetup {
|
|||||||
let shouldFullSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
let shouldFullSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
if let api = api {
|
if let api = api {
|
||||||
shouldFullSyncChannel.setMessageHandler { _, reply in
|
shouldFullSyncChannel.setMessageHandler { _, reply in
|
||||||
api.shouldFullSync { result in
|
do {
|
||||||
switch result {
|
let result = try api.shouldFullSync()
|
||||||
case .success(let res):
|
reply(wrapResult(result))
|
||||||
reply(wrapResult(res))
|
} catch {
|
||||||
case .failure(let error):
|
reply(wrapError(error))
|
||||||
reply(wrapError(error))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
shouldFullSyncChannel.setMessageHandler(nil)
|
shouldFullSyncChannel.setMessageHandler(nil)
|
||||||
}
|
}
|
||||||
let getMediaChangesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
let getMediaChangesChannel = taskQueue == nil
|
||||||
|
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
|
||||||
if let api = api {
|
if let api = api {
|
||||||
getMediaChangesChannel.setMessageHandler { _, reply in
|
getMediaChangesChannel.setMessageHandler { _, reply in
|
||||||
api.getMediaChanges { result in
|
do {
|
||||||
switch result {
|
let result = try api.getMediaChanges()
|
||||||
case .success(let res):
|
reply(wrapResult(result))
|
||||||
reply(wrapResult(res))
|
} catch {
|
||||||
case .failure(let error):
|
reply(wrapError(error))
|
||||||
reply(wrapError(error))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -609,33 +606,33 @@ class NativeSyncApiSetup {
|
|||||||
} else {
|
} else {
|
||||||
clearSyncCheckpointChannel.setMessageHandler(nil)
|
clearSyncCheckpointChannel.setMessageHandler(nil)
|
||||||
}
|
}
|
||||||
let getAssetIdsForAlbumChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
let getAssetIdsForAlbumChannel = taskQueue == nil
|
||||||
|
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
|
||||||
if let api = api {
|
if let api = api {
|
||||||
getAssetIdsForAlbumChannel.setMessageHandler { message, reply in
|
getAssetIdsForAlbumChannel.setMessageHandler { message, reply in
|
||||||
let args = message as! [Any?]
|
let args = message as! [Any?]
|
||||||
let albumIdArg = args[0] as! String
|
let albumIdArg = args[0] as! String
|
||||||
api.getAssetIdsForAlbum(albumId: albumIdArg) { result in
|
do {
|
||||||
switch result {
|
let result = try api.getAssetIdsForAlbum(albumId: albumIdArg)
|
||||||
case .success(let res):
|
reply(wrapResult(result))
|
||||||
reply(wrapResult(res))
|
} catch {
|
||||||
case .failure(let error):
|
reply(wrapError(error))
|
||||||
reply(wrapError(error))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
getAssetIdsForAlbumChannel.setMessageHandler(nil)
|
getAssetIdsForAlbumChannel.setMessageHandler(nil)
|
||||||
}
|
}
|
||||||
let getAlbumsChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
let getAlbumsChannel = taskQueue == nil
|
||||||
|
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
|
||||||
if let api = api {
|
if let api = api {
|
||||||
getAlbumsChannel.setMessageHandler { _, reply in
|
getAlbumsChannel.setMessageHandler { _, reply in
|
||||||
api.getAlbums { result in
|
do {
|
||||||
switch result {
|
let result = try api.getAlbums()
|
||||||
case .success(let res):
|
reply(wrapResult(result))
|
||||||
reply(wrapResult(res))
|
} catch {
|
||||||
case .failure(let error):
|
reply(wrapError(error))
|
||||||
reply(wrapError(error))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -659,19 +656,19 @@ class NativeSyncApiSetup {
|
|||||||
} else {
|
} else {
|
||||||
getAssetsCountSinceChannel.setMessageHandler(nil)
|
getAssetsCountSinceChannel.setMessageHandler(nil)
|
||||||
}
|
}
|
||||||
let getAssetsForAlbumChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
let getAssetsForAlbumChannel = taskQueue == nil
|
||||||
|
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
|
||||||
if let api = api {
|
if let api = api {
|
||||||
getAssetsForAlbumChannel.setMessageHandler { message, reply in
|
getAssetsForAlbumChannel.setMessageHandler { message, reply in
|
||||||
let args = message as! [Any?]
|
let args = message as! [Any?]
|
||||||
let albumIdArg = args[0] as! String
|
let albumIdArg = args[0] as! String
|
||||||
let updatedTimeCondArg: Int64? = nilOrValue(args[1])
|
let updatedTimeCondArg: Int64? = nilOrValue(args[1])
|
||||||
api.getAssetsForAlbum(albumId: albumIdArg, updatedTimeCond: updatedTimeCondArg) { result in
|
do {
|
||||||
switch result {
|
let result = try api.getAssetsForAlbum(albumId: albumIdArg, updatedTimeCond: updatedTimeCondArg)
|
||||||
case .success(let res):
|
reply(wrapResult(result))
|
||||||
reply(wrapResult(res))
|
} catch {
|
||||||
case .failure(let error):
|
reply(wrapError(error))
|
||||||
reply(wrapError(error))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -710,19 +707,6 @@ class NativeSyncApiSetup {
|
|||||||
} else {
|
} else {
|
||||||
cancelHashingChannel.setMessageHandler(nil)
|
cancelHashingChannel.setMessageHandler(nil)
|
||||||
}
|
}
|
||||||
let cancelSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.cancelSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
|
||||||
if let api = api {
|
|
||||||
cancelSyncChannel.setMessageHandler { _, reply in
|
|
||||||
do {
|
|
||||||
try api.cancelSync()
|
|
||||||
reply(wrapResult(nil))
|
|
||||||
} catch {
|
|
||||||
reply(wrapError(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cancelSyncChannel.setMessageHandler(nil)
|
|
||||||
}
|
|
||||||
let getTrashedAssetsChannel = taskQueue == nil
|
let getTrashedAssetsChannel = taskQueue == nil
|
||||||
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
|
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
|
||||||
|
|||||||
@@ -39,9 +39,6 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||||||
private static let hashCancelledCode = "HASH_CANCELLED"
|
private static let hashCancelledCode = "HASH_CANCELLED"
|
||||||
private static let hashCancelled = Result<[HashResult], Error>.failure(PigeonError(code: hashCancelledCode, message: "Hashing cancelled", details: nil))
|
private static let hashCancelled = Result<[HashResult], Error>.failure(PigeonError(code: hashCancelledCode, message: "Hashing cancelled", details: nil))
|
||||||
|
|
||||||
private var syncTask: Task<Void?, Error>?
|
|
||||||
private static let syncCancelledCode = "SYNC_CANCELLED"
|
|
||||||
private static let syncCancelled = PigeonError(code: syncCancelledCode, message: "Sync cancelled", details: nil)
|
|
||||||
|
|
||||||
init(with defaults: UserDefaults = .standard) {
|
init(with defaults: UserDefaults = .standard) {
|
||||||
self.defaults = defaults
|
self.defaults = defaults
|
||||||
@@ -74,11 +71,7 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||||||
saveChangeToken(token: PHPhotoLibrary.shared().currentChangeToken)
|
saveChangeToken(token: PHPhotoLibrary.shared().currentChangeToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
func shouldFullSync(completion: @escaping (Result<Bool, Error>) -> Void) {
|
func shouldFullSync() -> Bool {
|
||||||
runSync(completion) { $0.shouldFullSync() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private func shouldFullSync() -> Bool {
|
|
||||||
guard #available(iOS 16, *),
|
guard #available(iOS 16, *),
|
||||||
PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized,
|
PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized,
|
||||||
let storedToken = getChangeToken() else {
|
let storedToken = getChangeToken() else {
|
||||||
@@ -94,17 +87,12 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAlbums(completion: @escaping (Result<[PlatformAlbum], Error>) -> Void) {
|
func getAlbums() throws -> [PlatformAlbum] {
|
||||||
runSync(completion) { try $0.getAlbums() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getAlbums() throws -> [PlatformAlbum] {
|
|
||||||
var albums: [PlatformAlbum] = []
|
var albums: [PlatformAlbum] = []
|
||||||
|
|
||||||
for type in albumTypes {
|
albumTypes.forEach { type in
|
||||||
let collections = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil)
|
let collections = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil)
|
||||||
for i in 0..<collections.count {
|
for i in 0..<collections.count {
|
||||||
try Task.checkCancellation()
|
|
||||||
let album = collections.object(at: i)
|
let album = collections.object(at: i)
|
||||||
|
|
||||||
// Ignore recovered album
|
// Ignore recovered album
|
||||||
@@ -138,11 +126,7 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||||||
return albums.sorted { $0.id < $1.id }
|
return albums.sorted { $0.id < $1.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMediaChanges(completion: @escaping (Result<SyncDelta, Error>) -> Void) {
|
func getMediaChanges() throws -> SyncDelta {
|
||||||
runSync(completion) { try $0.getMediaChanges() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getMediaChanges() throws -> SyncDelta {
|
|
||||||
guard #available(iOS 16, *) else {
|
guard #available(iOS 16, *) else {
|
||||||
throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature requires iOS 16 or later.", details: nil)
|
throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature requires iOS 16 or later.", details: nil)
|
||||||
}
|
}
|
||||||
@@ -162,49 +146,51 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||||||
return SyncDelta(hasChanges: false, updates: [], deletes: [], assetAlbums: [:])
|
return SyncDelta(hasChanges: false, updates: [], deletes: [], assetAlbums: [:])
|
||||||
}
|
}
|
||||||
|
|
||||||
let changes = try PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken)
|
do {
|
||||||
|
let changes = try PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken)
|
||||||
var updatedAssets: Set<AssetWrapper> = []
|
|
||||||
var deletedAssets: Set<String> = []
|
|
||||||
|
|
||||||
for change in changes {
|
|
||||||
try Task.checkCancellation()
|
|
||||||
guard let details = try? change.changeDetails(for: PHObjectType.asset) else { continue }
|
|
||||||
|
|
||||||
let updated = details.updatedLocalIdentifiers.union(details.insertedLocalIdentifiers)
|
var updatedAssets: Set<AssetWrapper> = []
|
||||||
deletedAssets.formUnion(details.deletedLocalIdentifiers)
|
var deletedAssets: Set<String> = []
|
||||||
|
|
||||||
if (updated.isEmpty) { continue }
|
for change in changes {
|
||||||
|
guard let details = try? change.changeDetails(for: PHObjectType.asset) else { continue }
|
||||||
let options = PHFetchOptions()
|
|
||||||
options.includeHiddenAssets = false
|
|
||||||
let result = PHAsset.fetchAssets(withLocalIdentifiers: Array(updated), options: options)
|
|
||||||
for i in 0..<result.count {
|
|
||||||
let asset = result.object(at: i)
|
|
||||||
|
|
||||||
// Asset wrapper only uses the id for comparison. Multiple change can contain the same asset, skip duplicate changes
|
let updated = details.updatedLocalIdentifiers.union(details.insertedLocalIdentifiers)
|
||||||
let predicate = PlatformAsset(
|
deletedAssets.formUnion(details.deletedLocalIdentifiers)
|
||||||
id: asset.localIdentifier,
|
|
||||||
name: "",
|
if (updated.isEmpty) { continue }
|
||||||
type: 0,
|
|
||||||
durationMs: 0,
|
let options = PHFetchOptions()
|
||||||
orientation: 0,
|
options.includeHiddenAssets = false
|
||||||
isFavorite: false,
|
let result = PHAsset.fetchAssets(withLocalIdentifiers: Array(updated), options: options)
|
||||||
playbackStyle: .unknown
|
for i in 0..<result.count {
|
||||||
)
|
let asset = result.object(at: i)
|
||||||
if (updatedAssets.contains(AssetWrapper(with: predicate))) {
|
|
||||||
continue
|
// Asset wrapper only uses the id for comparison. Multiple change can contain the same asset, skip duplicate changes
|
||||||
|
let predicate = PlatformAsset(
|
||||||
|
id: asset.localIdentifier,
|
||||||
|
name: "",
|
||||||
|
type: 0,
|
||||||
|
durationMs: 0,
|
||||||
|
orientation: 0,
|
||||||
|
isFavorite: false,
|
||||||
|
playbackStyle: .unknown
|
||||||
|
)
|
||||||
|
if (updatedAssets.contains(AssetWrapper(with: predicate))) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let domainAsset = AssetWrapper(with: asset.toPlatformAsset())
|
||||||
|
updatedAssets.insert(domainAsset)
|
||||||
}
|
}
|
||||||
|
|
||||||
let domainAsset = AssetWrapper(with: asset.toPlatformAsset())
|
|
||||||
updatedAssets.insert(domainAsset)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let updates = Array(updatedAssets.map { $0.asset })
|
||||||
|
return SyncDelta(hasChanges: true, updates: updates, deletes: Array(deletedAssets), assetAlbums: buildAssetAlbumsMap(assets: updates))
|
||||||
}
|
}
|
||||||
|
|
||||||
let updates = Array(updatedAssets.map { $0.asset })
|
|
||||||
return SyncDelta(hasChanges: true, updates: updates, deletes: Array(deletedAssets), assetAlbums: buildAssetAlbumsMap(assets: updates))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func buildAssetAlbumsMap(assets: Array<PlatformAsset>) -> [String: [String]] {
|
private func buildAssetAlbumsMap(assets: Array<PlatformAsset>) -> [String: [String]] {
|
||||||
guard !assets.isEmpty else {
|
guard !assets.isEmpty else {
|
||||||
return [:]
|
return [:]
|
||||||
@@ -227,11 +213,7 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||||||
return albumAssets
|
return albumAssets
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAssetIdsForAlbum(albumId: String, completion: @escaping (Result<[String], Error>) -> Void) {
|
func getAssetIdsForAlbum(albumId: String) throws -> [String] {
|
||||||
runSync(completion) { try $0.getAssetIdsForAlbum(albumId: albumId) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getAssetIdsForAlbum(albumId: String) throws -> [String] {
|
|
||||||
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
|
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
|
||||||
guard let album = collections.firstObject else {
|
guard let album = collections.firstObject else {
|
||||||
return []
|
return []
|
||||||
@@ -241,14 +223,9 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||||||
let options = PHFetchOptions()
|
let options = PHFetchOptions()
|
||||||
options.includeHiddenAssets = false
|
options.includeHiddenAssets = false
|
||||||
let assets = getAssetsFromAlbum(in: album, options: options)
|
let assets = getAssetsFromAlbum(in: album, options: options)
|
||||||
assets.enumerateObjects { (asset, _, stop) in
|
assets.enumerateObjects { (asset, _, _) in
|
||||||
if Task.isCancelled {
|
|
||||||
stop.pointee = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ids.append(asset.localIdentifier)
|
ids.append(asset.localIdentifier)
|
||||||
}
|
}
|
||||||
try Task.checkCancellation()
|
|
||||||
return ids
|
return ids
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,11 +243,7 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||||||
return Int64(assets.count)
|
return Int64(assets.count)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?, completion: @escaping (Result<[PlatformAsset], Error>) -> Void) {
|
func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset] {
|
||||||
runSync(completion) { try $0.getAssetsForAlbum(albumId: albumId, updatedTimeCond: updatedTimeCond) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset] {
|
|
||||||
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
|
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
|
||||||
guard let album = collections.firstObject else {
|
guard let album = collections.firstObject else {
|
||||||
return []
|
return []
|
||||||
@@ -289,14 +262,9 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var assets: [PlatformAsset] = []
|
var assets: [PlatformAsset] = []
|
||||||
result.enumerateObjects { (asset, _, stop) in
|
result.enumerateObjects { (asset, _, _) in
|
||||||
if Task.isCancelled {
|
|
||||||
stop.pointee = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
assets.append(asset.toPlatformAsset())
|
assets.append(asset.toPlatformAsset())
|
||||||
}
|
}
|
||||||
try Task.checkCancellation()
|
|
||||||
return assets
|
return assets
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,31 +324,6 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||||||
hashTask = nil
|
hashTask = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func cancelSync() {
|
|
||||||
syncTask?.cancel()
|
|
||||||
syncTask = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private func runSync<T>(
|
|
||||||
_ completion: @escaping (Result<T, Error>) -> Void,
|
|
||||||
_ work: @escaping (NativeSyncApiImpl) throws -> T
|
|
||||||
) {
|
|
||||||
syncTask?.cancel()
|
|
||||||
syncTask = Task { [weak self] in
|
|
||||||
guard let self else { return nil }
|
|
||||||
let result: Result<T, Error>
|
|
||||||
do {
|
|
||||||
result = .success(try work(self))
|
|
||||||
} catch is CancellationError {
|
|
||||||
result = .failure(Self.syncCancelled)
|
|
||||||
} catch {
|
|
||||||
result = .failure(error)
|
|
||||||
}
|
|
||||||
self.completeWhenActive(for: completion, with: result)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func hashAsset(_ asset: PHAsset, allowNetworkAccess: Bool) async -> HashResult? {
|
private func hashAsset(_ asset: PHAsset, allowNetworkAccess: Bool) async -> HashResult? {
|
||||||
class RequestRef {
|
class RequestRef {
|
||||||
var id: PHAssetResourceDataRequestID?
|
var id: PHAssetResourceDataRequestID?
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ def get_version_from_pubspec
|
|||||||
pubspec = YAML.load_file(pubspec_path)
|
pubspec = YAML.load_file(pubspec_path)
|
||||||
|
|
||||||
version_string = pubspec['version']
|
version_string = pubspec['version']
|
||||||
version_string ? version_string.split('+').first.split('-').first : nil
|
version_string ? version_string.split('+').first : nil
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helper method to configure code signing for all targets
|
# Helper method to configure code signing for all targets
|
||||||
|
|||||||
@@ -22,5 +22,3 @@ enum AssetDateAggregation { start, end }
|
|||||||
enum SlideshowLook { contain, cover, blurredBackground }
|
enum SlideshowLook { contain, cover, blurredBackground }
|
||||||
|
|
||||||
enum SlideshowDirection { forward, backward, shuffle }
|
enum SlideshowDirection { forward, backward, shuffle }
|
||||||
|
|
||||||
enum PartnerDirection { sharedBy, sharedWith }
|
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ const Map<String, Locale> locales = {
|
|||||||
'English (en)': Locale('en'),
|
'English (en)': Locale('en'),
|
||||||
// Additional locales
|
// Additional locales
|
||||||
'Arabic (ar)': Locale('ar'),
|
'Arabic (ar)': Locale('ar'),
|
||||||
'Bosnian (bl)': Locale('bn'),
|
|
||||||
'Brazilian Portuguese (pt_BR)': Locale('pt', 'BR'),
|
|
||||||
'Bulgarian (bg)': Locale('bg'),
|
'Bulgarian (bg)': Locale('bg'),
|
||||||
'Catalan (ca)': Locale('ca'),
|
'Catalan (ca)': Locale('ca'),
|
||||||
'Chinese Simplified (zh_CN)': Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans'),
|
'Chinese Simplified (zh_CN)': Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans'),
|
||||||
@@ -16,7 +14,6 @@ const Map<String, Locale> locales = {
|
|||||||
'Danish (da)': Locale('da'),
|
'Danish (da)': Locale('da'),
|
||||||
'Dutch (nl)': Locale('nl'),
|
'Dutch (nl)': Locale('nl'),
|
||||||
'Estonian (et)': Locale('et'),
|
'Estonian (et)': Locale('et'),
|
||||||
'Filipino (tl)': Locale('tl'),
|
|
||||||
'Finnish (fi)': Locale('fi'),
|
'Finnish (fi)': Locale('fi'),
|
||||||
'French (fr)': Locale('fr'),
|
'French (fr)': Locale('fr'),
|
||||||
'Galician (gl)': Locale('gl'),
|
'Galician (gl)': Locale('gl'),
|
||||||
@@ -28,17 +25,13 @@ const Map<String, Locale> locales = {
|
|||||||
'Indonesian (id)': Locale('id'),
|
'Indonesian (id)': Locale('id'),
|
||||||
'Italian (it)': Locale('it'),
|
'Italian (it)': Locale('it'),
|
||||||
'Japanese (ja)': Locale('ja'),
|
'Japanese (ja)': Locale('ja'),
|
||||||
'Kabyle (kab)': Locale('kab'),
|
|
||||||
'Khmer (Northern) (kxm)': Locale('kxm'),
|
|
||||||
'Korean (ko)': Locale('ko'),
|
'Korean (ko)': Locale('ko'),
|
||||||
'Latvian (lv)': Locale('lv'),
|
'Latvian (lv)': Locale('lv'),
|
||||||
'Lithuanian (lt)': Locale('lt'),
|
'Lithuanian (lt)': Locale('lt'),
|
||||||
'Lombard (lmo)': Locale('lmo'),
|
|
||||||
'Mongolian (mn)': Locale('mn'),
|
'Mongolian (mn)': Locale('mn'),
|
||||||
'Māori (mi)': Locale('mi'),
|
|
||||||
'Nepali (ne)': Locale('ne'),
|
|
||||||
'Norwegian Bokmål (nb_NO)': Locale('nb', 'NO'),
|
'Norwegian Bokmål (nb_NO)': Locale('nb', 'NO'),
|
||||||
'Polish (pl)': Locale('pl'),
|
'Polish (pl)': Locale('pl'),
|
||||||
|
'Brazilian Portuguese (pt_BR)': Locale('pt', 'BR'),
|
||||||
'Portuguese (pt)': Locale('pt'),
|
'Portuguese (pt)': Locale('pt'),
|
||||||
'Romanian (ro)': Locale('ro'),
|
'Romanian (ro)': Locale('ro'),
|
||||||
'Russian (ru)': Locale('ru'),
|
'Russian (ru)': Locale('ru'),
|
||||||
@@ -47,8 +40,6 @@ const Map<String, Locale> locales = {
|
|||||||
'Slovak (sk)': Locale('sk'),
|
'Slovak (sk)': Locale('sk'),
|
||||||
'Slovenian (sl)': Locale('sl'),
|
'Slovenian (sl)': Locale('sl'),
|
||||||
'Spanish (es)': Locale('es'),
|
'Spanish (es)': Locale('es'),
|
||||||
'Swabian (swg)': Locale('swg'),
|
|
||||||
'Swahili (sw)': Locale('sw'),
|
|
||||||
'Swedish (sv)': Locale('sv'),
|
'Swedish (sv)': Locale('sv'),
|
||||||
'Tamil (ta)': Locale('ta'),
|
'Tamil (ta)': Locale('ta'),
|
||||||
'Telugu (te)': Locale('te'),
|
'Telugu (te)': Locale('te'),
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import 'package:immich_mobile/domain/models/config/theme_config.dart';
|
|||||||
import 'package:immich_mobile/domain/models/config/timeline_config.dart';
|
import 'package:immich_mobile/domain/models/config/timeline_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/config/viewer_config.dart';
|
import 'package:immich_mobile/domain/models/config/viewer_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/settings_key.dart';
|
import 'package:immich_mobile/domain/models/metadata_key.dart';
|
||||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ class AppConfig {
|
|||||||
String toString() =>
|
String toString() =>
|
||||||
'AppConfig(logLevel: $logLevel, theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow, album: $album, backup: $backup, network: $network)';
|
'AppConfig(logLevel: $logLevel, theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow, album: $album, backup: $backup, network: $network)';
|
||||||
|
|
||||||
T read<T extends Object>(SettingsKey<T> key) =>
|
T read<T extends Object>(MetadataKey<T> key) =>
|
||||||
(switch (key) {
|
(switch (key) {
|
||||||
.logLevel => logLevel,
|
.logLevel => logLevel,
|
||||||
.themePrimaryColor => theme.primaryColor,
|
.themePrimaryColor => theme.primaryColor,
|
||||||
@@ -143,10 +143,15 @@ class AppConfig {
|
|||||||
})
|
})
|
||||||
as T;
|
as T;
|
||||||
|
|
||||||
factory AppConfig.fromEntries(Map<SettingsKey<Object>, Object> overrides) =>
|
factory AppConfig.fromEntries(Map<MetadataKey<Object>, Object> entries) {
|
||||||
overrides.entries.fold(const AppConfig(), (config, entry) => config.write(entry.key, entry.value));
|
var config = const AppConfig();
|
||||||
|
for (final MapEntry(key: key, value: value) in entries.entries) {
|
||||||
|
config = config.write(key, value);
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
AppConfig write<T extends Object>(SettingsKey<T> key, T value) {
|
AppConfig write<T extends Object>(MetadataKey<T> key, T value) {
|
||||||
return switch (key) {
|
return switch (key) {
|
||||||
.logLevel => copyWith(logLevel: value as LogLevel),
|
.logLevel => copyWith(logLevel: value as LogLevel),
|
||||||
.themePrimaryColor => copyWith(theme: theme.copyWith(primaryColor: value as ImmichColorPreset)),
|
.themePrimaryColor => copyWith(theme: theme.copyWith(primaryColor: value as ImmichColorPreset)),
|
||||||
|
|||||||
+35
-24
@@ -7,7 +7,14 @@ import 'package:immich_mobile/domain/models/log.model.dart';
|
|||||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||||
|
|
||||||
enum SettingsKey<T extends Object> {
|
enum MetadataScope {
|
||||||
|
user, // keys with this scope are deleted on logout
|
||||||
|
system;
|
||||||
|
|
||||||
|
const MetadataScope();
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MetadataKey<T extends Object> {
|
||||||
// Theme
|
// Theme
|
||||||
themePrimaryColor<ImmichColorPreset>(codec: _EnumCodec(ImmichColorPreset.values)),
|
themePrimaryColor<ImmichColorPreset>(codec: _EnumCodec(ImmichColorPreset.values)),
|
||||||
themeMode<ThemeMode>(codec: _EnumCodec(ThemeMode.values)),
|
themeMode<ThemeMode>(codec: _EnumCodec(ThemeMode.values)),
|
||||||
@@ -25,11 +32,14 @@ enum SettingsKey<T extends Object> {
|
|||||||
viewerTapToNavigate<bool>(),
|
viewerTapToNavigate<bool>(),
|
||||||
|
|
||||||
// Network
|
// Network
|
||||||
networkAutoEndpointSwitching<bool>(),
|
networkAutoEndpointSwitching<bool>(scope: .system),
|
||||||
networkPreferredWifiName<String>(),
|
networkPreferredWifiName<String>(scope: .system),
|
||||||
networkLocalEndpoint<String>(),
|
networkLocalEndpoint<String>(scope: .system),
|
||||||
networkExternalEndpointList<List<String>>(codec: _ListCodec(_PrimitiveCodec.string)),
|
networkExternalEndpointList<List<String>>(scope: .system, codec: _ListCodec(_PrimitiveCodec.string)),
|
||||||
networkCustomHeaders<Map<String, String>>(codec: _MapCodec(_PrimitiveCodec.string, _PrimitiveCodec.string)),
|
networkCustomHeaders<Map<String, String>>(
|
||||||
|
scope: .system,
|
||||||
|
codec: _MapCodec(_PrimitiveCodec.string, _PrimitiveCodec.string),
|
||||||
|
),
|
||||||
|
|
||||||
// Album
|
// Album
|
||||||
albumSortMode<AlbumSortMode>(codec: _EnumCodec(AlbumSortMode.values)),
|
albumSortMode<AlbumSortMode>(codec: _EnumCodec(AlbumSortMode.values)),
|
||||||
@@ -50,7 +60,7 @@ enum SettingsKey<T extends Object> {
|
|||||||
timelineStorageIndicator<bool>(),
|
timelineStorageIndicator<bool>(),
|
||||||
|
|
||||||
// Log
|
// Log
|
||||||
logLevel<LogLevel>(codec: _EnumCodec(LogLevel.values)),
|
logLevel<LogLevel>(scope: .system, codec: _EnumCodec(LogLevel.values)),
|
||||||
|
|
||||||
// Map
|
// Map
|
||||||
mapShowFavoriteOnly<bool>(),
|
mapShowFavoriteOnly<bool>(),
|
||||||
@@ -73,24 +83,25 @@ enum SettingsKey<T extends Object> {
|
|||||||
slideshowLook<SlideshowLook>(codec: _EnumCodec(SlideshowLook.values)),
|
slideshowLook<SlideshowLook>(codec: _EnumCodec(SlideshowLook.values)),
|
||||||
slideshowDirection<SlideshowDirection>(codec: _EnumCodec(SlideshowDirection.values));
|
slideshowDirection<SlideshowDirection>(codec: _EnumCodec(SlideshowDirection.values));
|
||||||
|
|
||||||
final _SettingsCodec<T>? _codecOverride;
|
final MetadataScope scope;
|
||||||
|
final _MetadataCodec<T>? _codecOverride;
|
||||||
|
|
||||||
const SettingsKey({_SettingsCodec<T>? codec}) : _codecOverride = codec;
|
const MetadataKey({this.scope = .user, _MetadataCodec<T>? codec}) : _codecOverride = codec;
|
||||||
|
|
||||||
_SettingsCodec<T> get _codec => _codecOverride ?? _SettingsCodec.forType(T);
|
_MetadataCodec<T> get _codec => _codecOverride ?? _MetadataCodec.forType(T);
|
||||||
|
|
||||||
String encode(T value) => _codec.encode(value);
|
String encode(T value) => _codec.encode(value);
|
||||||
|
|
||||||
T decode(String raw) => _codec.decode(raw);
|
T decode(String raw) => _codec.decode(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class _SettingsCodec<T extends Object> {
|
sealed class _MetadataCodec<T extends Object> {
|
||||||
const _SettingsCodec();
|
const _MetadataCodec();
|
||||||
|
|
||||||
String encode(T value);
|
String encode(T value);
|
||||||
T decode(String raw);
|
T decode(String raw);
|
||||||
|
|
||||||
static const Map<Type, _SettingsCodec<Object>> _primitives = {
|
static const Map<Type, _MetadataCodec<Object>> _primitives = {
|
||||||
int: _PrimitiveCodec.integer,
|
int: _PrimitiveCodec.integer,
|
||||||
double: _PrimitiveCodec.real,
|
double: _PrimitiveCodec.real,
|
||||||
bool: _PrimitiveCodec.boolean,
|
bool: _PrimitiveCodec.boolean,
|
||||||
@@ -98,16 +109,16 @@ sealed class _SettingsCodec<T extends Object> {
|
|||||||
DateTime: _DateTimeCodec(),
|
DateTime: _DateTimeCodec(),
|
||||||
};
|
};
|
||||||
|
|
||||||
static _SettingsCodec<T> forType<T extends Object>(Type runtimeType) {
|
static _MetadataCodec<T> forType<T extends Object>(Type runtimeType) {
|
||||||
final codec = _primitives[runtimeType];
|
final codec = _primitives[runtimeType];
|
||||||
if (codec == null) {
|
if (codec == null) {
|
||||||
throw StateError('No primitive codec for $runtimeType. Provide an explicit codec when defining the SettingsKey.');
|
throw StateError('No primitive codec for $runtimeType. Provide an explicit codec when defining the MetadataKey.');
|
||||||
}
|
}
|
||||||
return codec as _SettingsCodec<T>;
|
return codec as _MetadataCodec<T>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class _EnumCodec<T extends Enum> extends _SettingsCodec<T> {
|
final class _EnumCodec<T extends Enum> extends _MetadataCodec<T> {
|
||||||
final List<T> values;
|
final List<T> values;
|
||||||
|
|
||||||
const _EnumCodec(this.values);
|
const _EnumCodec(this.values);
|
||||||
@@ -119,7 +130,7 @@ final class _EnumCodec<T extends Enum> extends _SettingsCodec<T> {
|
|||||||
T decode(String raw) => values.firstWhere((v) => v.name == raw);
|
T decode(String raw) => values.firstWhere((v) => v.name == raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
final class _DateTimeCodec extends _SettingsCodec<DateTime> {
|
final class _DateTimeCodec extends _MetadataCodec<DateTime> {
|
||||||
const _DateTimeCodec();
|
const _DateTimeCodec();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -129,9 +140,9 @@ final class _DateTimeCodec extends _SettingsCodec<DateTime> {
|
|||||||
DateTime decode(String raw) => DateTime.parse(raw);
|
DateTime decode(String raw) => DateTime.parse(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
final class _MapCodec<K extends Object, V extends Object> extends _SettingsCodec<Map<K, V>> {
|
final class _MapCodec<K extends Object, V extends Object> extends _MetadataCodec<Map<K, V>> {
|
||||||
final _SettingsCodec<K> _keyCodec;
|
final _MetadataCodec<K> _keyCodec;
|
||||||
final _SettingsCodec<V> _valueCodec;
|
final _MetadataCodec<V> _valueCodec;
|
||||||
|
|
||||||
const _MapCodec(this._keyCodec, this._valueCodec);
|
const _MapCodec(this._keyCodec, this._valueCodec);
|
||||||
|
|
||||||
@@ -167,8 +178,8 @@ final class _MapCodec<K extends Object, V extends Object> extends _SettingsCodec
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class _ListCodec<T extends Object> extends _SettingsCodec<List<T>> {
|
final class _ListCodec<T extends Object> extends _MetadataCodec<List<T>> {
|
||||||
final _SettingsCodec<T> _elementCodec;
|
final _MetadataCodec<T> _elementCodec;
|
||||||
|
|
||||||
const _ListCodec(this._elementCodec);
|
const _ListCodec(this._elementCodec);
|
||||||
|
|
||||||
@@ -197,7 +208,7 @@ final class _ListCodec<T extends Object> extends _SettingsCodec<List<T>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class _PrimitiveCodec<T extends Object> extends _SettingsCodec<T> {
|
final class _PrimitiveCodec<T extends Object> extends _MetadataCodec<T> {
|
||||||
final T Function(String) _parse;
|
final T Function(String) _parse;
|
||||||
|
|
||||||
const _PrimitiveCodec._(this._parse);
|
const _PrimitiveCodec._(this._parse);
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
class Ocr {
|
|
||||||
final String id;
|
|
||||||
final String assetId;
|
|
||||||
final double x1;
|
|
||||||
final double y1;
|
|
||||||
final double x2;
|
|
||||||
final double y2;
|
|
||||||
final double x3;
|
|
||||||
final double y3;
|
|
||||||
final double x4;
|
|
||||||
final double y4;
|
|
||||||
final double boxScore;
|
|
||||||
final double textScore;
|
|
||||||
final String text;
|
|
||||||
final bool isVisible;
|
|
||||||
|
|
||||||
const Ocr({
|
|
||||||
required this.id,
|
|
||||||
required this.assetId,
|
|
||||||
required this.x1,
|
|
||||||
required this.y1,
|
|
||||||
required this.x2,
|
|
||||||
required this.y2,
|
|
||||||
required this.x3,
|
|
||||||
required this.y3,
|
|
||||||
required this.x4,
|
|
||||||
required this.y4,
|
|
||||||
required this.boxScore,
|
|
||||||
required this.textScore,
|
|
||||||
required this.text,
|
|
||||||
required this.isVisible,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ocr copyWith({
|
|
||||||
String? id,
|
|
||||||
String? assetId,
|
|
||||||
double? x1,
|
|
||||||
double? y1,
|
|
||||||
double? x2,
|
|
||||||
double? y2,
|
|
||||||
double? x3,
|
|
||||||
double? y3,
|
|
||||||
double? x4,
|
|
||||||
double? y4,
|
|
||||||
double? boxScore,
|
|
||||||
double? textScore,
|
|
||||||
String? text,
|
|
||||||
bool? isVisible,
|
|
||||||
}) {
|
|
||||||
return Ocr(
|
|
||||||
id: id ?? this.id,
|
|
||||||
assetId: assetId ?? this.assetId,
|
|
||||||
x1: x1 ?? this.x1,
|
|
||||||
y1: y1 ?? this.y1,
|
|
||||||
x2: x2 ?? this.x2,
|
|
||||||
y2: y2 ?? this.y2,
|
|
||||||
x3: x3 ?? this.x3,
|
|
||||||
y3: y3 ?? this.y3,
|
|
||||||
x4: x4 ?? this.x4,
|
|
||||||
y4: y4 ?? this.y4,
|
|
||||||
boxScore: boxScore ?? this.boxScore,
|
|
||||||
textScore: textScore ?? this.textScore,
|
|
||||||
text: text ?? this.text,
|
|
||||||
isVisible: isVisible ?? this.isVisible,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return '''Ocr {
|
|
||||||
id: $id,
|
|
||||||
assetId: $assetId,
|
|
||||||
x1: $x1,
|
|
||||||
y1: $y1,
|
|
||||||
x2: $x2,
|
|
||||||
y2: $y2,
|
|
||||||
x3: $x3,
|
|
||||||
y3: $y3,
|
|
||||||
x4: $x4,
|
|
||||||
y4: $y4,
|
|
||||||
boxScore: $boxScore,
|
|
||||||
textScore: $textScore,
|
|
||||||
text: $text,
|
|
||||||
isVisible: $isVisible
|
|
||||||
}''';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return other is Ocr &&
|
|
||||||
other.id == id &&
|
|
||||||
other.assetId == assetId &&
|
|
||||||
other.x1 == x1 &&
|
|
||||||
other.y1 == y1 &&
|
|
||||||
other.x2 == x2 &&
|
|
||||||
other.y2 == y2 &&
|
|
||||||
other.x3 == x3 &&
|
|
||||||
other.y3 == y3 &&
|
|
||||||
other.x4 == x4 &&
|
|
||||||
other.y4 == y4 &&
|
|
||||||
other.boxScore == boxScore &&
|
|
||||||
other.textScore == textScore &&
|
|
||||||
other.text == text &&
|
|
||||||
other.isVisible == isVisible;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
return id.hashCode ^
|
|
||||||
assetId.hashCode ^
|
|
||||||
x1.hashCode ^
|
|
||||||
y1.hashCode ^
|
|
||||||
x2.hashCode ^
|
|
||||||
y2.hashCode ^
|
|
||||||
x3.hashCode ^
|
|
||||||
y3.hashCode ^
|
|
||||||
x4.hashCode ^
|
|
||||||
y4.hashCode ^
|
|
||||||
boxScore.hashCode ^
|
|
||||||
textScore.hashCode ^
|
|
||||||
text.hashCode ^
|
|
||||||
isVisible.hashCode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user