mirror of
https://github.com/immich-app/immich.git
synced 2026-07-22 05:44:03 +03:00
Compare commits
104 Commits
fix-29727
...
refactor/a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa6e6f45a1 | ||
|
|
c9539a4aa8 | ||
|
|
a560e08693 | ||
|
|
49c0baf076 | ||
|
|
3485dbffda | ||
|
|
e508781125 | ||
|
|
a30ebab70c | ||
|
|
5d7283e44d | ||
|
|
73329a8ce8 | ||
|
|
d5adfb97dd | ||
|
|
b70cb58bc8 | ||
|
|
943bfafb7a | ||
|
|
ee4bd3f833 | ||
|
|
7a7303aceb | ||
|
|
0a7c067b20 | ||
|
|
c8de56c422 | ||
|
|
0e4572c37b | ||
|
|
14a2776453 | ||
|
|
59bc81423c | ||
|
|
e6fff3b15e | ||
|
|
755df23b22 | ||
|
|
899f547053 | ||
|
|
4938fd4c89 | ||
|
|
e81a79224c | ||
|
|
7b0f58b2c1 | ||
|
|
ae8398ffe4 | ||
|
|
aa08dad1f5 | ||
|
|
ce022233ae | ||
|
|
1b4d41324e | ||
|
|
8365cdd59e | ||
|
|
1cec7021e8 | ||
|
|
1b0f473457 | ||
|
|
e918658cc1 | ||
|
|
2e587fc7e8 | ||
|
|
df970da59e | ||
|
|
8061a2e5ff | ||
|
|
77091b0107 | ||
|
|
4c754f2999 | ||
|
|
6fa3f2feac | ||
|
|
3f6897ef80 | ||
|
|
89d0a9d59f | ||
|
|
45a6ea84af | ||
|
|
bf64f3867b | ||
|
|
4a4d468aa2 | ||
|
|
522def1ed6 | ||
|
|
3adc3920fb | ||
|
|
00cb50cc67 | ||
|
|
f2b0b696f6 | ||
|
|
b5401eb120 | ||
|
|
12fc8bac18 | ||
|
|
f2c00c107d | ||
|
|
f19f30ec66 | ||
|
|
297316c7b8 | ||
|
|
f18065b3c2 | ||
|
|
a3dd19cd2f | ||
|
|
9cea3f2375 | ||
|
|
507448d797 | ||
|
|
fe3eb7d865 | ||
|
|
90dc328c22 | ||
|
|
a5623c41ec | ||
|
|
3a016f4451 | ||
|
|
0af8284456 | ||
|
|
ad7a9bd189 | ||
|
|
80cdfebbc7 | ||
|
|
c50a807bc1 | ||
|
|
26e07548a7 | ||
|
|
f73c53c563 | ||
|
|
d2744f52b1 | ||
|
|
d8ed7d7bb9 | ||
|
|
834cc54abc | ||
|
|
eda605c534 | ||
|
|
b75e6520bb | ||
|
|
4c2d2284aa | ||
|
|
5d29f3edc1 | ||
|
|
e00dcf7850 | ||
|
|
e7f773197f | ||
|
|
2237078bd5 | ||
|
|
55dc579059 | ||
|
|
b4530b0671 | ||
|
|
a04e2ddeca | ||
|
|
5f2e8a4fcf | ||
|
|
dc55b234d9 | ||
|
|
f6bf22d9ca | ||
|
|
2cdbd0d00f | ||
|
|
cd308ad930 | ||
|
|
12061f3bf8 | ||
|
|
2460d431af | ||
|
|
24556cd3fc | ||
|
|
c84ab54889 | ||
|
|
19e59ebd98 | ||
|
|
89b0d906e8 | ||
|
|
3c43e8d6c1 | ||
|
|
2ed8b2bddd | ||
|
|
9c8d718ddc | ||
|
|
20e123c839 | ||
|
|
35fcca6254 | ||
|
|
ffafc144c6 | ||
|
|
89d31aaba1 | ||
|
|
f2ddace584 | ||
|
|
99883096d6 | ||
|
|
84dff19ca9 | ||
|
|
da8774801b | ||
|
|
9057ae9759 | ||
|
|
557189d7a8 |
@@ -83,7 +83,7 @@
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:4": {
|
||||
// https://github.com/devcontainers/features/issues/1466
|
||||
"moby": false
|
||||
}
|
||||
|
||||
2
.github/workflows/auto-close.yml
vendored
2
.github/workflows/auto-close.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
outputs:
|
||||
uses_template: ${{ steps.check.outputs.uses_template }}
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
sparse-checkout: .github/pull_request_template.md
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
24
.github/workflows/build-mobile.yml
vendored
24
.github/workflows/build-mobile.yml
vendored
@@ -51,10 +51,11 @@ jobs:
|
||||
should_run: ${{ steps.check.outputs.should_run }}
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- name: Check what should run
|
||||
id: check
|
||||
@@ -79,12 +80,14 @@ jobs:
|
||||
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
permission-pull-requests: write
|
||||
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: false
|
||||
@@ -103,14 +106,14 @@ jobs:
|
||||
working-directory: ./mobile
|
||||
run: printf "%s" $KEY_JKS | base64 -d > android/key.jks
|
||||
|
||||
- uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
- uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
|
||||
- name: Restore Gradle Cache
|
||||
id: cache-gradle-restore
|
||||
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -181,7 +184,7 @@ jobs:
|
||||
|
||||
- name: Save Gradle Cache
|
||||
id: cache-gradle-save
|
||||
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
if: github.ref == 'refs/heads/main'
|
||||
with:
|
||||
path: |
|
||||
@@ -198,20 +201,21 @@ jobs:
|
||||
contents: read
|
||||
# Run on main branch or workflow_dispatch, or on PRs/other branches (build only, no upload)
|
||||
if: ${{ !github.event.pull_request.head.repo.fork && fromJSON(needs.pre-job.outputs.should_run).mobile == true }}
|
||||
runs-on: macos-15
|
||||
runs-on: macos-26
|
||||
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- name: Select Xcode 26
|
||||
run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.sha }}
|
||||
persist-credentials: false
|
||||
@@ -237,7 +241,7 @@ jobs:
|
||||
run: flutter build ios --config-only --no-codesign
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0
|
||||
uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0
|
||||
with:
|
||||
ruby-version: '3.3'
|
||||
bundler-cache: true
|
||||
|
||||
6
.github/workflows/cache-cleanup.yml
vendored
6
.github/workflows/cache-cleanup.yml
vendored
@@ -19,13 +19,15 @@ jobs:
|
||||
actions: write
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
permission-actions: write
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
6
.github/workflows/check-openapi.yml
vendored
6
.github/workflows/check-openapi.yml
vendored
@@ -20,12 +20,12 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check for breaking API changes
|
||||
uses: oasdiff/oasdiff-action/breaking@ae5ef6402ebe218ac6e872c59d0fa4557694e75c # v0.1.4
|
||||
uses: oasdiff/oasdiff-action/breaking@291b6594ce617f22f49af5898ee038ed0b46e8fe # v0.1.5
|
||||
with:
|
||||
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
|
||||
revision: open-api/immich-openapi-specs.json
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
12
.github/workflows/cli.yml
vendored
12
.github/workflows/cli.yml
vendored
@@ -31,13 +31,14 @@ jobs:
|
||||
working-directory: ./packages/cli
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
@@ -63,13 +64,14 @@ jobs:
|
||||
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
@@ -81,7 +83,7 @@ jobs:
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
with:
|
||||
registry: ghcr.io
|
||||
|
||||
2
.github/workflows/close-duplicates.yml
vendored
2
.github/workflows/close-duplicates.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
needs: [get_body, should_run]
|
||||
if: ${{ needs.should_run.outputs.should_run == 'true' }}
|
||||
container:
|
||||
image: ghcr.io/immich-app/mdq:main@sha256:2a64c7f045cb7b580fbdf3614d7d1805f5775fec453e3d1023764180efa8c70b
|
||||
image: ghcr.io/immich-app/mdq:main@sha256:e3fe456f346b02c2f479462100763aee3ba8245768dfeac3c4262acb068e5c62
|
||||
outputs:
|
||||
checked: ${{ steps.get_checkbox.outputs.checked }}
|
||||
steps:
|
||||
|
||||
11
.github/workflows/codeql-analysis.yml
vendored
11
.github/workflows/codeql-analysis.yml
vendored
@@ -44,20 +44,21 @@ jobs:
|
||||
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||
uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -70,7 +71,7 @@ jobs:
|
||||
# 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)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||
uses: github/codeql-action/autobuild@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
|
||||
|
||||
# ℹ️ 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
|
||||
@@ -83,6 +84,6 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||
uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
|
||||
with:
|
||||
category: '/language:${{matrix.language}}'
|
||||
|
||||
7
.github/workflows/docker.yml
vendored
7
.github/workflows/docker.yml
vendored
@@ -23,10 +23,11 @@ jobs:
|
||||
should_run: ${{ steps.check.outputs.should_run }}
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- name: Check what should run
|
||||
id: check
|
||||
@@ -60,7 +61,7 @@ jobs:
|
||||
suffix: ['', '-cuda', '-rocm', '-openvino', '-armnn', '-rknn']
|
||||
steps:
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -90,7 +91,7 @@ jobs:
|
||||
suffix: ['']
|
||||
steps:
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
|
||||
8
.github/workflows/docs-build.yml
vendored
8
.github/workflows/docs-build.yml
vendored
@@ -21,10 +21,11 @@ jobs:
|
||||
should_run: ${{ steps.check.outputs.should_run }}
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- name: Check what should run
|
||||
id: check
|
||||
@@ -54,13 +55,14 @@ jobs:
|
||||
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
11
.github/workflows/docs-deploy.yml
vendored
11
.github/workflows/docs-deploy.yml
vendored
@@ -20,10 +20,12 @@ jobs:
|
||||
artifact: ${{ steps.get-artifact.outputs.result }}
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-actions: read
|
||||
permission-pull-requests: read
|
||||
|
||||
- if: ${{ github.event.workflow_run.conclusion != 'success' }}
|
||||
run: echo 'The triggering workflow did not succeed' && exit 1
|
||||
@@ -126,13 +128,16 @@ jobs:
|
||||
if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }}
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
permission-actions: read
|
||||
permission-pull-requests: write
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
6
.github/workflows/docs-destroy.yml
vendored
6
.github/workflows/docs-destroy.yml
vendored
@@ -17,13 +17,15 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
permission-pull-requests: write
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
56
.github/workflows/fdroid.yml
vendored
Normal file
56
.github/workflows/fdroid.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: Update F-Droid Repo
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
update-index:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !github.event.release.prerelease }}
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout pubspec for versionCode
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: ${{ github.event.release.tag_name }}
|
||||
persist-credentials: false
|
||||
sparse-checkout: mobile/pubspec.yaml
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Update F-Droid repo
|
||||
env:
|
||||
GITLAB_TOKEN: ${{ secrets.FDROID_REPO_TOKEN }}
|
||||
RELEASE: ${{ toJSON(github.event.release) }}
|
||||
TAG: ${{ github.event.release.tag_name }}
|
||||
run: |
|
||||
VERSION_CODE=$(yq '.version' mobile/pubspec.yaml | cut -d+ -f2)
|
||||
export VERSION_CODE
|
||||
|
||||
NEW_ENTRY=$(yq -P '
|
||||
[.assets[] | select(.name == "app-release.apk")] as $matches |
|
||||
with(select($matches | length == 0); error("app-release.apk not found in release assets")) |
|
||||
{
|
||||
"url": $matches[0].browser_download_url,
|
||||
"sha256sum": ($matches[0].digest | sub("^sha256:", "")),
|
||||
"date": (.published_at | split("T") | .[0]),
|
||||
"version-code": env(VERSION_CODE)
|
||||
} | .date tag= "!!timestamp"
|
||||
' <<< "$RELEASE")
|
||||
export NEW_ENTRY
|
||||
|
||||
git clone --depth 1 "https://oauth2:${GITLAB_TOKEN}@gitlab.futo.org/fdroid/repo-v2.git" fdroid-repo
|
||||
cd fdroid-repo
|
||||
|
||||
yq -i '
|
||||
with(select([.apks[] | select(."version-code" == env(VERSION_CODE))] | length > 0); error("version-code already in index")) |
|
||||
.apks = [env(NEW_ENTRY)] + .apks
|
||||
' apps/Immich/index.yml
|
||||
|
||||
git config user.name "Immich Release Bot"
|
||||
git config user.email "bot@immich.app"
|
||||
git commit -am "Immich: add version-code ${VERSION_CODE} (${TAG})"
|
||||
git push
|
||||
6
.github/workflows/fix-format.yml
vendored
6
.github/workflows/fix-format.yml
vendored
@@ -15,13 +15,15 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: write
|
||||
permission-pull-requests: write
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
persist-credentials: true
|
||||
|
||||
2
.github/workflows/merge-translations.yml
vendored
2
.github/workflows/merge-translations.yml
vendored
@@ -35,6 +35,8 @@ jobs:
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: write
|
||||
permission-pull-requests: write
|
||||
|
||||
- name: Find translation PR
|
||||
id: find_pr
|
||||
|
||||
5
.github/workflows/pr-label-validation.yml
vendored
5
.github/workflows/pr-label-validation.yml
vendored
@@ -14,13 +14,14 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-pull-requests: write
|
||||
|
||||
- name: Require PR to have a changelog label
|
||||
uses: mheap/github-action-required-labels@0ac283b4e65c1fb28ce6079dea5546ceca98ccbe # v5.5.2
|
||||
uses: mheap/github-action-required-labels@23e10fde7e062233401931a0eece796cd9bf3177 # v5.6.0
|
||||
with:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
mode: exactly
|
||||
|
||||
6
.github/workflows/pr-labeler.yml
vendored
6
.github/workflows/pr-labeler.yml
vendored
@@ -12,11 +12,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
permission-pull-requests: write
|
||||
|
||||
- uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
|
||||
- uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6.2.0
|
||||
with:
|
||||
repo-token: ${{ steps.token.outputs.token }}
|
||||
|
||||
11
.github/workflows/prepare-release.yml
vendored
11
.github/workflows/prepare-release.yml
vendored
@@ -54,13 +54,14 @@ jobs:
|
||||
permissions: {} # No job-level permissions are needed because it uses the app-token
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: write
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
persist-credentials: true
|
||||
@@ -73,7 +74,7 @@ jobs:
|
||||
|
||||
# TODO move to mise
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
|
||||
- name: Bump version
|
||||
env:
|
||||
@@ -135,9 +136,11 @@ jobs:
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: write
|
||||
permission-actions: read
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
persist-credentials: false
|
||||
|
||||
6
.github/workflows/preview-label.yaml
vendored
6
.github/workflows/preview-label.yaml
vendored
@@ -14,10 +14,11 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-pull-requests: write
|
||||
|
||||
- uses: immich-app/devtools/actions/sticky-comment@0135acd12ad9f3369b94a2aa3c0ae8c835a4e926 # sticky-comment-action-v1.0.0
|
||||
with:
|
||||
@@ -32,10 +33,11 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-pull-requests: write
|
||||
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
|
||||
5
.github/workflows/sdk.yml
vendored
5
.github/workflows/sdk.yml
vendored
@@ -16,13 +16,14 @@ jobs:
|
||||
packages: write
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
9
.github/workflows/static_analysis.yml
vendored
9
.github/workflows/static_analysis.yml
vendored
@@ -20,10 +20,11 @@ jobs:
|
||||
should_run: ${{ steps.check.outputs.should_run }}
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- name: Check what should run
|
||||
id: check
|
||||
@@ -33,6 +34,7 @@ jobs:
|
||||
filters: |
|
||||
mobile:
|
||||
- 'mobile/**'
|
||||
- 'i18n/en.json'
|
||||
force-filters: |
|
||||
- '.github/workflows/static_analysis.yml'
|
||||
force-events: 'workflow_dispatch,release'
|
||||
@@ -49,13 +51,14 @@ jobs:
|
||||
working-directory: ./mobile
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
92
.github/workflows/test.yml
vendored
92
.github/workflows/test.yml
vendored
@@ -17,10 +17,11 @@ jobs:
|
||||
should_run: ${{ steps.check.outputs.should_run }}
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- name: Check what should run
|
||||
id: check
|
||||
@@ -77,13 +78,14 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
@@ -105,13 +107,14 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
@@ -136,13 +139,14 @@ jobs:
|
||||
working-directory: ./packages/cli
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
@@ -167,13 +171,14 @@ jobs:
|
||||
working-directory: ./packages/cli
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
@@ -211,13 +216,14 @@ jobs:
|
||||
working-directory: ./web
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
@@ -249,13 +255,14 @@ jobs:
|
||||
working-directory: ./web
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
@@ -277,13 +284,14 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
@@ -327,13 +335,14 @@ jobs:
|
||||
working-directory: ./e2e
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
@@ -359,13 +368,14 @@ jobs:
|
||||
working-directory: ./server
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: 'recursive'
|
||||
@@ -395,13 +405,14 @@ jobs:
|
||||
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: 'recursive'
|
||||
@@ -411,7 +422,7 @@ jobs:
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -472,13 +483,14 @@ jobs:
|
||||
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: 'recursive'
|
||||
@@ -488,7 +500,7 @@ jobs:
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -580,12 +592,13 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
@@ -618,12 +631,13 @@ jobs:
|
||||
working-directory: ./machine-learning
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
@@ -648,13 +662,14 @@ jobs:
|
||||
working-directory: ./.github
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
@@ -678,12 +693,13 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
@@ -699,13 +715,14 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
@@ -757,13 +774,14 @@ jobs:
|
||||
- 5432:5432
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
6
.github/workflows/weblate-lock.yml
vendored
6
.github/workflows/weblate-lock.yml
vendored
@@ -24,10 +24,11 @@ jobs:
|
||||
should_run: ${{ steps.check.outputs.should_run }}
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- name: Check what should run
|
||||
id: check
|
||||
@@ -47,10 +48,11 @@ jobs:
|
||||
if: ${{ fromJSON(needs.pre-job.outputs.should_run).i18n == true }}
|
||||
steps:
|
||||
- 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-pull-requests: read
|
||||
|
||||
- name: Bot review status
|
||||
env:
|
||||
|
||||
38
.vscode/settings.json
vendored
38
.vscode/settings.json
vendored
@@ -29,9 +29,6 @@
|
||||
"editor.formatOnSave": true,
|
||||
"tailwindCSS.lint.suggestCanonicalClasses": "ignore"
|
||||
},
|
||||
"svelte.plugin.svelte.compilerWarnings": {
|
||||
"state_referenced_locally": "ignore"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true
|
||||
@@ -43,37 +40,40 @@
|
||||
"eslint.useFlatConfig": true,
|
||||
"eslint.validate": ["javascript", "typescript", "svelte"],
|
||||
"eslint.workingDirectories": [
|
||||
{ "directory": "cli", "changeProcessCWD": true },
|
||||
{ "directory": "e2e", "changeProcessCWD": true },
|
||||
{ "directory": "server", "changeProcessCWD": true },
|
||||
{ "directory": "web", "changeProcessCWD": true }
|
||||
{ "changeProcessCWD": true, "directory": "cli" },
|
||||
{ "changeProcessCWD": true, "directory": "e2e" },
|
||||
{ "changeProcessCWD": true, "directory": "server" },
|
||||
{ "changeProcessCWD": true, "directory": "web" }
|
||||
],
|
||||
"files.watcherExclude": {
|
||||
"**/.jj/**": true,
|
||||
"**/.git/**": true,
|
||||
"**/node_modules/**": true,
|
||||
"**/build/**": true,
|
||||
"**/dist/**": true,
|
||||
"**/.svelte-kit/**": true
|
||||
},
|
||||
"explorer.fileNesting.enabled": true,
|
||||
"explorer.fileNesting.patterns": {
|
||||
"*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart",
|
||||
"*.ts": "${capture}.spec.ts,${capture}.mock.ts",
|
||||
"*.js": "${capture}.spec.js,${capture}.mock.js",
|
||||
"*.ts": "${capture}.spec.ts,${capture}.mock.ts",
|
||||
"package.json": "package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lockb, bun.lock, pnpm-workspace.yaml, .pnpmfile.cjs"
|
||||
},
|
||||
"files.watcherExclude": {
|
||||
"**/.git/**": true,
|
||||
"**/.jj/**": true,
|
||||
"**/.svelte-kit/**": true,
|
||||
"**/build/**": true,
|
||||
"**/dist/**": true,
|
||||
"**/node_modules/**": true
|
||||
},
|
||||
"js/ts.preferences.importModuleSpecifier": "non-relative",
|
||||
"search.exclude": {
|
||||
"**/node_modules": true,
|
||||
"**/.svelte-kit": true,
|
||||
"**/build": true,
|
||||
"**/dist": true,
|
||||
"**/.svelte-kit": true,
|
||||
"**/node_modules": true,
|
||||
"**/open-api/typescript-sdk/src": true
|
||||
},
|
||||
"svelte.enable-ts-plugin": true,
|
||||
"svelte.plugin.svelte.compilerWarnings": {
|
||||
"state_referenced_locally": "ignore"
|
||||
},
|
||||
"tailwindCSS.experimental.configFile": {
|
||||
"web/src/app.css": "web/src/**"
|
||||
},
|
||||
"js/ts.preferences.importModuleSpecifier": "non-relative",
|
||||
"vitest.maximumConfigs": 10
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
/web/ @danieldietzler
|
||||
/machine-learning/ @mertalev
|
||||
/e2e/ @danieldietzler
|
||||
/mobile/ @shenlong-tanwen @santoshakil
|
||||
/mobile/ @shenlong-tanwen @santoshakil @agg23
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<a href="readme_i18n/README_zh_TW.md">正體中文</a>
|
||||
<a href="readme_i18n/README_uk_UA.md">Українська</a>
|
||||
<a href="readme_i18n/README_ru_RU.md">Русский</a>
|
||||
<a href="readme_i18n/README_bg_BG.md">Български</a>
|
||||
<a href="readme_i18n/README_pt_BR.md">Português Brasileiro</a>
|
||||
<a href="readme_i18n/README_sv_SE.md">Svenska</a>
|
||||
<a href="readme_i18n/README_ar_JO.md">العربية</a>
|
||||
|
||||
@@ -154,7 +154,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
|
||||
image: docker.io/valkey/valkey:9@sha256:8e8d64b405ce18f41b8e5ee20aa4687a8ed0022d1298f2ce31cdcf3a76e09411
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
|
||||
image: docker.io/valkey/valkey:9@sha256:8e8d64b405ce18f41b8e5ee20aa4687a8ed0022d1298f2ce31cdcf3a76e09411
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
restart: always
|
||||
@@ -85,7 +85,7 @@ services:
|
||||
container_name: immich_prometheus
|
||||
ports:
|
||||
- 9090:9090
|
||||
image: prom/prometheus@sha256:a75c5a35bc21d7afe69551eefa3cb1e1fb1775fe759408007a66b54ec3de1f29
|
||||
image: prom/prometheus@sha256:3c42b892cf723fa54d2f262c37a0e1f80aa8c8ddb1da7b9b0df9455a35a7f893
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
- prometheus-data:/prometheus
|
||||
|
||||
@@ -61,7 +61,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
|
||||
image: docker.io/valkey/valkey:9@sha256:8e8d64b405ce18f41b8e5ee20aa4687a8ed0022d1298f2ce31cdcf3a76e09411
|
||||
user: '1000:1000'
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
@@ -49,7 +49,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
|
||||
image: docker.io/valkey/valkey:9@sha256:8e8d64b405ce18f41b8e5ee20aa4687a8ed0022d1298f2ce31cdcf3a76e09411
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
restart: always
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
# System Integrity
|
||||
|
||||
## Integrity report
|
||||
|
||||
At a [customizable interval](https://my.immich.app/admin/system-settings?isOpen=integrity-checks), Immich runs integrity checks to ensure that your library is still intact and there are no corrupt files.
|
||||
There are three kind of issues Immich checks for:
|
||||
|
||||
- Untracked files: the path was found in Immich's directories but it is not referenced in Immich's database
|
||||
- Missing files: the path is found in the Immich internal database, but does not actually exist on disk
|
||||
- Checksum mismatches: the checksum of the file stored in Immich's database does not match the actual file's checksum anymore
|
||||
|
||||
All three run nightly at 3am by default. For the "Checksum files" check, there are additional time and progress limits, as those are the most taxing checks. With these additional limits, Immich can slowly check checksums of your files over the course of a couple of days.
|
||||
|
||||
You can see the results of these checks on the [maintenance](https://my.immich.app/admin/maintenance) page. Here, you can also trigger a full scan (a _check_) for specific jobs, or all of them. In addition, you can also _refresh_ checks. This will only look at items that have currently been reported on, and check if those have been fixed.
|
||||
|
||||
### Common causes
|
||||
|
||||
Most common are untracked files. In many cases those are corrupted thumbnails or encoded videos that have been partially generated at some point and never got cleaned up properly. These are usually fine to delete, as both can always be regenerated at a later point. Other files will need to be investigated on a case-by-case basis by checking they already exist in Immich and thinking about how they might have gotten untracked.
|
||||
|
||||
:::info
|
||||
You might want to run the _missing_ jobs for thumbnails and encoded videos (https://my.immich.app/admin/queues) to make sure all your assets have proper thumbnails and encoded videos. Watch the server logs while running the jobs, in case there are any issues with some assets.
|
||||
:::
|
||||
|
||||
Missing files are files where Immich references them internally, but they don't actually exist on disk in that location. It could be that you deleted a file on disk from the internal library (don't do that, Immich doesn't support it). It could also be that there are issues with your file storage. Please carefully investigate missing files, and never hesitate to reach out on [our Discord](https://discord.immich.app) if you have any questions!
|
||||
|
||||
Checksum mismatches are often indicative of file system corruption. It could also be that you previously edited a file from the internal library on the disk, which is also not supported and will cause a checksum mismatch. Again, the recommended action is to look at any reported item individually, check it out, try to remember if you changed it or some metadata on it at some point. If you edited the file, the supported resolution is to delete the mismatched asset from Immich and reupload it as a new asset.
|
||||
|
||||
## Folder checks
|
||||
|
||||
:::info
|
||||
@@ -40,7 +65,7 @@ The above error messages show that the server has previously (successfully) writ
|
||||
|
||||
### Ignoring the checks
|
||||
|
||||
:::warning
|
||||
:::danger
|
||||
The checks are designed to catch common problems that we have seen users have in the past, and often indicate there's something wrong that you should solve. If you know what you're doing and you want to disable them you can set the following environment variable:
|
||||
:::
|
||||
|
||||
|
||||
@@ -99,6 +99,21 @@ To see local changes to `@immich/ui` in Immich, do the following:
|
||||
2. Run `mise //mobile:translation` to generate the translation file.
|
||||
3. Change to the `mobile/` directory and run `flutter run` to start the app.
|
||||
|
||||
##### iOS Code Signing
|
||||
|
||||
The Immich Apple Team ID and bundle IDs are specified in `mobile/ios/Signing.xcconfig`. For local development, we provide an override mechanism.
|
||||
|
||||
Create `mobile/ios/Signing.local.xcconfig` and populate it with the necessary values needed to build and sign Immich yourself. This local override file is gitignored.
|
||||
|
||||
```
|
||||
IMMICH_TEAM_ID = ABCDE12345
|
||||
IMMICH_BUNDLE_ID_PROD = com.customuniqueid.immich
|
||||
IMMICH_BUNDLE_ID_DEV = com.customuniqueid.immichdev
|
||||
IMMICH_GROUP_ID = group.com.customuniqueid.immich
|
||||
```
|
||||
|
||||
The environment values are used across Immich's targets and schemes to prevent redundant edits by contributors.
|
||||
|
||||
#### Translation
|
||||
|
||||
To add a new translation text, enter the key-value pair in the `i18n/en.json` in the root of the immich project. Then run:
|
||||
|
||||
@@ -99,6 +99,7 @@ Options:
|
||||
-H, --include-hidden Include hidden folders (default: false, env: IMMICH_INCLUDE_HIDDEN)
|
||||
-a, --album Automatically create albums based on folder name (default: false, env: IMMICH_AUTO_CREATE_ALBUM)
|
||||
-A, --album-name <name> Add all assets to specified album (env: IMMICH_ALBUM_NAME)
|
||||
--visibility <visibility> Set the visibility of uploaded assets (choices: "archive", "timeline", "hidden", "locked", env: IMMICH_VISIBILITY)
|
||||
-n, --dry-run Don't perform any actions, just show what will be done (default: false, env: IMMICH_DRY_RUN)
|
||||
-c, --concurrency <number> Number of assets to upload at the same time (default: 4, env: IMMICH_UPLOAD_CONCURRENCY)
|
||||
-j, --json-output Output detailed information in json format (default: false, env: IMMICH_JSON_OUTPUT)
|
||||
@@ -176,6 +177,12 @@ By default, hidden files are skipped. If you want to include hidden files, use t
|
||||
immich upload --include-hidden --recursive directory/
|
||||
```
|
||||
|
||||
You can set the visibility of uploaded assets to `archive`, `timeline`, `hidden`, or `locked` with the `--visibility` option:
|
||||
|
||||
```bash
|
||||
immich upload --visibility archive --recursive directory/
|
||||
```
|
||||
|
||||
You can use the `--json-output` option to get a json printed which includes
|
||||
three keys: `newFiles`, `duplicates` and `newAssets`. Due to some logging
|
||||
output you will need to strip the first three lines of output to get the json.
|
||||
|
||||
@@ -7,7 +7,7 @@ sidebar_position: 85
|
||||
:::note
|
||||
This is a community contribution and not officially supported by the Immich team, but included here for convenience.
|
||||
|
||||
Community support can be found in the dedicated channel on the [Discord Server](https://discord.immich.app/).
|
||||
Community support should be directed to Synology-specific support platforms.
|
||||
:::
|
||||
|
||||
Immich can easily be installed on a Synology NAS using Container Manager within DSM. If you have not installed Container Manager already, you can install it in the Packages Center. Refer to the [Container Manager docs](https://kb.synology.com/en-us/DSM/help/ContainerManager/docker_desc?version=7) for more information on using Container Manager.
|
||||
|
||||
@@ -121,7 +121,7 @@ alt="Go to Docker Tab and visit the address listed next to immich-web"
|
||||
width="90%"
|
||||
alt="Go to Docker Tab and visit the address listed next to immich-web"
|
||||
/>
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
:::tip
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
The mobile app can be downloaded from the following places:
|
||||
|
||||
- Obtainium: You can get your Obtainium config link from the [Utilities page of your Immich server](https://my.immich.app/utilities).
|
||||
- [Google Play Store](https://play.google.com/store/apps/details?id=app.alextran.immich)
|
||||
- [Apple App Store](https://apps.apple.com/us/app/immich/id1613945652)
|
||||
- [F-Droid](https://f-droid.org/packages/app.alextran.immich)
|
||||
- [GitHub Releases (apk)](https://github.com/immich-app/immich/releases)
|
||||
- [Google Play Store](https://play.google.com/store/apps/details?id=app.alextran.immich)
|
||||
- [GitHub Releases (APK)](https://github.com/immich-app/immich/releases)
|
||||
- Obtainium: You can get your Obtainium config link from the [Utilities page of your Immich server](https://my.immich.app/utilities).
|
||||
- [F-Droid](https://app.futo.org/fdroid/repo/)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
||||
|
||||
[[tools.wrangler]]
|
||||
version = "4.100.0"
|
||||
version = "4.110.0"
|
||||
backend = "npm:wrangler"
|
||||
|
||||
[tools.wrangler.options]
|
||||
|
||||
@@ -28,4 +28,4 @@ run = "prettier --write ."
|
||||
run = "wrangler pages deploy build --project-name=${PROJECT_NAME} --branch=${BRANCH_NAME}"
|
||||
|
||||
[tools]
|
||||
wrangler = "4.100.0"
|
||||
wrangler = "4.111.0"
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
"@docusaurus/tsconfig": "^3.10.0",
|
||||
"@docusaurus/types": "^3.10.0",
|
||||
"prettier": "^3.7.4",
|
||||
"typescript": "^6.0.0"
|
||||
"@typescript/native": "npm:typescript@^7.0.2",
|
||||
"typescript": "npm:@typescript/typescript6@^6.0.2"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
4
docs/static/archived-versions.json
vendored
4
docs/static/archived-versions.json
vendored
@@ -1,7 +1,7 @@
|
||||
[
|
||||
{
|
||||
"label": "v3.0.2",
|
||||
"url": "https://docs.v3.0.2.archive.immich.app"
|
||||
"label": "v3.0.3",
|
||||
"url": "https://docs.v3.0.3.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v2.7.5",
|
||||
|
||||
@@ -45,7 +45,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich-e2e-redis
|
||||
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
|
||||
image: docker.io/valkey/valkey:9@sha256:8e8d64b405ce18f41b8e5ee20aa4687a8ed0022d1298f2ce31cdcf3a76e09411
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
|
||||
|
||||
@@ -41,9 +41,18 @@ export default typescriptEslint.config([
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'unicorn/prefer-module': 'off',
|
||||
'unicorn/import-style': 'off',
|
||||
'unicorn/consistent-boolean-name': 'off',
|
||||
'unicorn/no-non-function-verb-prefix': 'off',
|
||||
'unicorn/no-unreadable-for-of-expression': 'off',
|
||||
'unicorn/max-nested-calls': 'off',
|
||||
'unicorn/prefer-uint8array-base64': 'off',
|
||||
'unicorn/isolated-functions': 'off',
|
||||
'unicorn/prefer-promise-with-resolvers': 'off',
|
||||
'unicorn/no-declarations-before-early-exit': 'off',
|
||||
'unicorn/prefer-simple-condition-first': 'off',
|
||||
curly: 2,
|
||||
'prettier/prettier': 0,
|
||||
'unicorn/prevent-abbreviations': 'off',
|
||||
'unicorn/name-replacements': 'off',
|
||||
'unicorn/filename-case': 'off',
|
||||
'unicorn/no-null': 'off',
|
||||
'unicorn/prefer-top-level-await': 'off',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "3.0.2",
|
||||
"version": "3.0.3",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
@@ -32,7 +32,7 @@
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@socket.io/component-emitter": "^3.1.2",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^24.13.2",
|
||||
"@types/node": "^24.13.3",
|
||||
"@types/pg": "^8.15.1",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
"@types/supertest": "^7.0.0",
|
||||
@@ -40,7 +40,7 @@
|
||||
"eslint": "^10.0.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^64.0.0",
|
||||
"eslint-plugin-unicorn": "^72.0.0",
|
||||
"exiftool-vendored": "^35.0.0",
|
||||
"globals": "^17.0.0",
|
||||
"luxon": "^3.4.4",
|
||||
@@ -51,7 +51,8 @@
|
||||
"sharp": "^0.34.5",
|
||||
"socket.io-client": "^4.7.4",
|
||||
"supertest": "^7.0.0",
|
||||
"typescript": "^6.0.0",
|
||||
"@typescript/native": "npm:typescript@^7.0.2",
|
||||
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
||||
"typescript-eslint": "^8.28.0",
|
||||
"utimes": "^5.2.1",
|
||||
"vitest": "^4.0.0"
|
||||
|
||||
@@ -118,7 +118,7 @@ describe('/admin/database-backups', () => {
|
||||
|
||||
expect(status).toBe(201);
|
||||
|
||||
cookie = headers['set-cookie'][0].split(';')[0];
|
||||
cookie = headers['set-cookie'][0].split(';', 1)[0];
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
@@ -224,7 +224,7 @@ describe('/admin/database-backups', () => {
|
||||
});
|
||||
|
||||
expect(status).toBe(201);
|
||||
cookie = headers['set-cookie'][0].split(';')[0];
|
||||
cookie = headers['set-cookie'][0].split(';', 1)[0];
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
@@ -295,7 +295,7 @@ describe('/admin/database-backups', () => {
|
||||
});
|
||||
|
||||
expect(status).toBe(201);
|
||||
cookie = headers['set-cookie'][0].split(';')[0];
|
||||
cookie = headers['set-cookie'][0].split(';', 1)[0];
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
|
||||
@@ -85,7 +85,7 @@ describe('/admin/maintenance', () => {
|
||||
|
||||
expect(status).toBe(201);
|
||||
|
||||
cookie = headers['set-cookie'][0].split(';')[0];
|
||||
cookie = headers['set-cookie'][0].split(';', 1)[0];
|
||||
expect(cookie).toEqual(
|
||||
expect.stringMatching(/^immich_maintenance_token=[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*$/),
|
||||
);
|
||||
@@ -149,7 +149,7 @@ describe('/admin/maintenance', () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/admin/maintenance/login')
|
||||
.send({
|
||||
token: cookie!.split('=')[1].trim(),
|
||||
token: cookie!.split('=', 2)[1].trim(),
|
||||
});
|
||||
expect(status).toBe(201);
|
||||
expect(body).toEqual(
|
||||
|
||||
@@ -27,7 +27,7 @@ test.describe('Maintenance', () => {
|
||||
test('maintenance shows no options to users until they authenticate', async ({ page }) => {
|
||||
const setCookie = await utils.enterMaintenance(admin.accessToken);
|
||||
const cookie = setCookie
|
||||
?.map((cookie) => cookie.split(';')[0].split('='))
|
||||
?.map((cookie) => cookie.split(';', 1)[0].split('='))
|
||||
?.find(([name]) => name === 'immich_maintenance_token');
|
||||
|
||||
expect(cookie).toBeTruthy();
|
||||
|
||||
@@ -120,6 +120,7 @@ describe('/albums', () => {
|
||||
}),
|
||||
]);
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unreadable-array-destructuring
|
||||
[user2Albums[0]] = await Promise.all([
|
||||
getAlbumInfo({ id: user2Albums[0].id }, { headers: asBearerAuth(user2.accessToken) }),
|
||||
deleteUserAdmin({ id: user3.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) }),
|
||||
@@ -796,5 +797,22 @@ describe('/albums', () => {
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest('Not found or no album.share access'));
|
||||
});
|
||||
|
||||
it('should not allow an editor to change the role of an owner', async () => {
|
||||
const album = await utils.createAlbum(user1.accessToken, {
|
||||
albumName: 'testAlbum',
|
||||
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }],
|
||||
});
|
||||
|
||||
expect(album.albumUsers[1].role).toEqual(AlbumUserRole.Editor);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.put(`/albums/${album.id}/user/${user1.userId}`)
|
||||
.set('Authorization', `Bearer ${user2.accessToken}`)
|
||||
.send({ role: AlbumUserRole.Editor });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest('User is owner'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -781,7 +781,7 @@ describe('/asset', () => {
|
||||
exifImageWidth: 4032,
|
||||
exifImageHeight: 3024,
|
||||
latitude: 41.2203,
|
||||
longitude: -96.071_625,
|
||||
longitude: -96.071625,
|
||||
make: 'Apple',
|
||||
model: 'iPhone 7',
|
||||
lensModel: 'iPhone 7 back camera 3.99mm f/1.8',
|
||||
@@ -973,9 +973,9 @@ describe('/asset', () => {
|
||||
fileSizeInByte: 31_175_472,
|
||||
focalLength: 18.3,
|
||||
iso: 100,
|
||||
latitude: 36.613_24,
|
||||
latitude: 36.61324,
|
||||
lensModel: '18.3mm F2.8',
|
||||
longitude: -121.897_85,
|
||||
longitude: -121.89785,
|
||||
make: 'RICOH IMAGING COMPANY, LTD.',
|
||||
model: 'RICOH GR III',
|
||||
orientation: '1',
|
||||
|
||||
@@ -75,7 +75,7 @@ describe('/map', () => {
|
||||
country: 'United States of America',
|
||||
id: expect.any(String),
|
||||
lat: expect.closeTo(39.115),
|
||||
lon: expect.closeTo(-108.400_968),
|
||||
lon: expect.closeTo(-108.400968),
|
||||
state: 'Colorado',
|
||||
},
|
||||
{
|
||||
@@ -83,7 +83,7 @@ describe('/map', () => {
|
||||
country: 'United States of America',
|
||||
id: expect.any(String),
|
||||
lat: expect.closeTo(41.2203),
|
||||
lon: expect.closeTo(-96.071_625),
|
||||
lon: expect.closeTo(-96.071625),
|
||||
state: 'Nebraska',
|
||||
},
|
||||
]);
|
||||
@@ -123,7 +123,7 @@ describe('/map', () => {
|
||||
country: 'United States of America',
|
||||
id: expect.any(String),
|
||||
lat: expect.closeTo(39.115),
|
||||
lon: expect.closeTo(-108.400_968),
|
||||
lon: expect.closeTo(-108.400968),
|
||||
state: 'Colorado',
|
||||
},
|
||||
{
|
||||
@@ -131,7 +131,7 @@ describe('/map', () => {
|
||||
country: 'United States of America',
|
||||
id: expect.any(String),
|
||||
lat: expect.closeTo(41.2203),
|
||||
lon: expect.closeTo(-96.071_625),
|
||||
lon: expect.closeTo(-96.071625),
|
||||
state: 'Nebraska',
|
||||
},
|
||||
]);
|
||||
@@ -188,20 +188,20 @@ describe('/map', () => {
|
||||
const reverseGeocodeTestCases = [
|
||||
{
|
||||
name: 'Vaucluse',
|
||||
lat: -33.858_977_058_663_13,
|
||||
lon: 151.278_490_730_270_48,
|
||||
lat: -33.85897705866313,
|
||||
lon: 151.27849073027048,
|
||||
results: [{ city: 'Vaucluse', state: 'New South Wales', country: 'Australia' }],
|
||||
},
|
||||
{
|
||||
name: 'Ravenhall',
|
||||
lat: -37.765_732_399_174_75,
|
||||
lon: 144.752_453_164_883_3,
|
||||
lat: -37.76573239917475,
|
||||
lon: 144.7524531648833,
|
||||
results: [{ city: 'Ravenhall', state: 'Victoria', country: 'Australia' }],
|
||||
},
|
||||
{
|
||||
name: 'Scarborough',
|
||||
lat: -31.894_346_156_789_997,
|
||||
lon: 115.757_617_103_904_64,
|
||||
lat: -31.894346156789997,
|
||||
lon: 115.75761710390464,
|
||||
results: [{ city: 'Scarborough', state: 'Western Australia', country: 'Australia' }],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -44,7 +44,7 @@ const loginWithOAuth = async (sub: OAuthUser | string, redirectUri?: string) =>
|
||||
});
|
||||
|
||||
// login
|
||||
const response1 = await redirect(url.replace(authServer.internal, authServer.external));
|
||||
const response1 = await redirect(url.replace(authServer.internal, () => authServer.external));
|
||||
const response2 = await request(authServer.external + response1.location)
|
||||
.post('')
|
||||
.set('Cookie', response1.cookies)
|
||||
|
||||
@@ -87,23 +87,23 @@ describe('/search', () => {
|
||||
|
||||
// note: the coordinates here are not the actual coordinates of the images and are random for most of them
|
||||
const coordinates = [
|
||||
{ latitude: 48.853_41, longitude: 2.3488 }, // paris
|
||||
{ latitude: 35.6895, longitude: 139.691_71 }, // tokyo
|
||||
{ latitude: 52.524_37, longitude: 13.410_53 }, // berlin
|
||||
{ latitude: 1.314_663_1, longitude: 103.845_409_3 }, // singapore
|
||||
{ latitude: 41.013_84, longitude: 28.949_66 }, // istanbul
|
||||
{ latitude: 5.556_02, longitude: -0.1969 }, // accra
|
||||
{ latitude: 37.544_270_6, longitude: -4.727_752_8 }, // andalusia
|
||||
{ latitude: 23.133_02, longitude: -82.383_04 }, // havana
|
||||
{ latitude: 41.694_11, longitude: 44.833_68 }, // tbilisi
|
||||
{ latitude: 31.222_22, longitude: 121.458_06 }, // shanghai
|
||||
{ latitude: 48.85341, longitude: 2.3488 }, // paris
|
||||
{ latitude: 35.6895, longitude: 139.69171 }, // tokyo
|
||||
{ latitude: 52.52437, longitude: 13.41053 }, // berlin
|
||||
{ latitude: 1.3146631, longitude: 103.8454093 }, // singapore
|
||||
{ latitude: 41.01384, longitude: 28.94966 }, // istanbul
|
||||
{ latitude: 5.55602, longitude: -0.1969 }, // accra
|
||||
{ latitude: 37.5442706, longitude: -4.7277528 }, // andalusia
|
||||
{ latitude: 23.13302, longitude: -82.38304 }, // havana
|
||||
{ latitude: 41.69411, longitude: 44.83368 }, // tbilisi
|
||||
{ latitude: 31.22222, longitude: 121.45806 }, // shanghai
|
||||
{ latitude: 38.9711, longitude: -109.7137 }, // thompson springs
|
||||
{ latitude: 40.714_27, longitude: -74.005_97 }, // new york
|
||||
{ latitude: 47.040_57, longitude: 9.068_04 }, // glarus
|
||||
{ latitude: 32.771_52, longitude: -89.116_73 }, // philadelphia
|
||||
{ latitude: 31.634_16, longitude: -7.999_94 }, // marrakesh
|
||||
{ latitude: 38.523_735_4, longitude: -78.488_619_4 }, // tanners ridge
|
||||
{ latitude: 59.938_63, longitude: 30.314_13 }, // st. petersburg
|
||||
{ latitude: 40.71427, longitude: -74.00597 }, // new york
|
||||
{ latitude: 47.04057, longitude: 9.06804 }, // glarus
|
||||
{ latitude: 32.77152, longitude: -89.11673 }, // philadelphia
|
||||
{ latitude: 31.63416, longitude: -7.99994 }, // marrakesh
|
||||
{ latitude: 38.5237354, longitude: -78.4886194 }, // tanners ridge
|
||||
{ latitude: 59.93863, longitude: 30.31413 }, // st. petersburg
|
||||
{ latitude: 0, longitude: 0 }, // null island
|
||||
];
|
||||
|
||||
@@ -112,7 +112,7 @@ describe('/search', () => {
|
||||
);
|
||||
|
||||
await Promise.all(updates);
|
||||
for (const [i] of coordinates.entries()) {
|
||||
for (const i of coordinates.keys()) {
|
||||
await utils.waitForWebsocketEvent({ event: 'assetUpdate', id: assets[i].id });
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ describe(`immich login`, () => {
|
||||
it('should login and save auth.yml with 600', async () => {
|
||||
const admin = await utils.adminSetup();
|
||||
const key = await utils.createApiKey(admin.accessToken, [Permission.All]);
|
||||
const { stdout, stderr, exitCode } = await immichCli(['login', app, `${key.secret}`]);
|
||||
const { stdout, stderr, exitCode } = await immichCli(['login', app, key.secret]);
|
||||
expect(stdout.split('\n')).toEqual([
|
||||
'Logging in to http://127.0.0.1:2285/api',
|
||||
'Logged in as admin@immich.cloud',
|
||||
@@ -48,7 +48,7 @@ describe(`immich login`, () => {
|
||||
it('should login without /api in the url', async () => {
|
||||
const admin = await utils.adminSetup();
|
||||
const key = await utils.createApiKey(admin.accessToken, [Permission.All]);
|
||||
const { stdout, stderr, exitCode } = await immichCli(['login', app.replaceAll('/api', ''), `${key.secret}`]);
|
||||
const { stdout, stderr, exitCode } = await immichCli(['login', app.replaceAll('/api', ''), key.secret]);
|
||||
expect(stdout.split('\n')).toEqual([
|
||||
'Logging in to http://127.0.0.1:2285',
|
||||
'Discovered API at http://127.0.0.1:2285/api',
|
||||
|
||||
@@ -119,7 +119,9 @@ describe(`immich upload`, () => {
|
||||
const baseDir = `/tmp/upload/`;
|
||||
|
||||
const testPaths = Object.keys(files).map((filePath) => `${baseDir}/${filePath}`);
|
||||
testPaths.map((filePath) => utils.createImageFile(filePath));
|
||||
for (const filePath of testPaths) {
|
||||
utils.createImageFile(filePath);
|
||||
}
|
||||
|
||||
const commandLine = paths.map((argument) => `${baseDir}/${argument}`);
|
||||
|
||||
@@ -135,7 +137,9 @@ describe(`immich upload`, () => {
|
||||
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.total).toBe(expectedCount);
|
||||
|
||||
testPaths.map((filePath) => utils.removeImageFile(filePath));
|
||||
for (const filePath of testPaths) {
|
||||
utils.removeImageFile(filePath);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ export const randomImageFromString = async (
|
||||
let seedNumber = 0;
|
||||
for (let i = 0; i < seed.length; i++) {
|
||||
seedNumber = (seedNumber << 5) - seedNumber + (seed.codePointAt(i) ?? 0);
|
||||
seedNumber = seedNumber & seedNumber; // Convert to 32bit integer
|
||||
seedNumber &= seedNumber; // Convert to 32bit integer
|
||||
}
|
||||
return randomImage(new SeededRandom(Math.abs(seedNumber)), { width, height });
|
||||
};
|
||||
|
||||
@@ -64,7 +64,7 @@ export function generateAsset(
|
||||
const asset: MockTimelineAsset = {
|
||||
id: assetId,
|
||||
ownerId,
|
||||
ratio: Number.parseFloat(ratio.split(':')[0]) / Number.parseFloat(ratio.split(':')[1]),
|
||||
ratio: Number(ratio.split(':', 1)[0]) / Number(ratio.split(':', 2)[1]),
|
||||
thumbhash: generateThumbhash(rng),
|
||||
localDateTime: date.toISOString(),
|
||||
fileCreatedAt: date.toISOString(),
|
||||
@@ -214,7 +214,7 @@ export function generateTimelineData(config: TimelineConfig): MockTimelineData {
|
||||
}
|
||||
|
||||
// Create a mock album from random assets
|
||||
const allAssets = [...buckets.values()].flat();
|
||||
const allAssets = buckets.values().toArray().flat();
|
||||
|
||||
// Select 10-30 random assets for the album (or all assets if less than 10)
|
||||
const albumSize = Math.min(allAssets.length, globalRng.nextInt(10, 31));
|
||||
|
||||
@@ -172,11 +172,7 @@ function shouldIncludeAsset(
|
||||
if (isArchived !== undefined && actuallyArchived !== isArchived) {
|
||||
return false;
|
||||
}
|
||||
if (isFavorite !== undefined && actuallyFavorited !== isFavorite) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return isFavorite === undefined || actuallyFavorited === isFavorite;
|
||||
}
|
||||
/**
|
||||
* Get summary for all buckets (mimics getTimeBuckets API)
|
||||
@@ -361,7 +357,7 @@ export function getAsset(
|
||||
owner?: UserResponseDto,
|
||||
): AssetResponseDto | undefined {
|
||||
// Search through all buckets for the asset
|
||||
const buckets = [...timelineData.buckets.values()];
|
||||
const buckets = timelineData.buckets.values().toArray();
|
||||
for (const assets of buckets) {
|
||||
const asset = assets.find((a) => a.id === assetId);
|
||||
if (asset) {
|
||||
@@ -395,7 +391,7 @@ export function getAlbum(
|
||||
|
||||
// Get the actual asset objects from the timeline data
|
||||
const albumAssets: AssetResponseDto[] = [];
|
||||
const allAssets = [...timelineData.buckets.values()].flat();
|
||||
const allAssets = timelineData.buckets.values().toArray().flat();
|
||||
|
||||
for (const assetId of album.assetIds) {
|
||||
const assetConfig = allAssets.find((a) => a.id === assetId);
|
||||
|
||||
@@ -143,7 +143,7 @@ export function validateTimelineConfig(config: TimelineConfig): void {
|
||||
}
|
||||
|
||||
// Validate seed if provided
|
||||
if (config.seed !== undefined && (config.seed < 0 || !Number.isInteger(config.seed))) {
|
||||
if (config.seed !== undefined && (config.seed < 0 || !Number.isSafeInteger(config.seed))) {
|
||||
throw new Error('Seed must be a non-negative integer');
|
||||
}
|
||||
|
||||
|
||||
@@ -153,11 +153,8 @@ export function getMockAsset(
|
||||
const isInDifferentPeriod = (date1: DateTime, date2: DateTime): boolean => {
|
||||
if (unit === 'day') {
|
||||
return !date1.startOf('day').equals(date2.startOf('day'));
|
||||
} else if (unit === 'month') {
|
||||
return date1.year !== date2.year || date1.month !== date2.month;
|
||||
} else {
|
||||
return date1.year !== date2.year;
|
||||
}
|
||||
return unit === 'month' ? date1.year !== date2.year || date1.month !== date2.month : date1.year !== date2.year;
|
||||
};
|
||||
|
||||
if (direction === 'next') {
|
||||
|
||||
@@ -40,7 +40,8 @@ export const setupTimelineMockApiRoutes = async (
|
||||
contentType: 'application/json',
|
||||
json: getTimeBuckets(timelineRestData, isTrashed, isArchived, isFavorite, albumId, changes),
|
||||
});
|
||||
} else if (pathname === '/api/timeline/bucket') {
|
||||
}
|
||||
if (pathname === '/api/timeline/bucket') {
|
||||
const timeBucket = url.searchParams.get('timeBucket');
|
||||
if (!timeBucket) {
|
||||
return route.continue();
|
||||
|
||||
@@ -38,7 +38,7 @@ test.describe('asset-viewer', () => {
|
||||
assets.push(...timeBucket);
|
||||
}
|
||||
for (const yearMonth of timelineRestData.buckets.keys()) {
|
||||
const [year, month] = yearMonth.split('-');
|
||||
const [year, month] = yearMonth.split('-', 2);
|
||||
yearMonths.push(`${year}-${Number(month)}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable unicorn/no-this-outside-of-class */
|
||||
import type { AssetResponseDto } from '@immich/sdk';
|
||||
import { expect, Page } from '@playwright/test';
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ test.describe('Timeline', () => {
|
||||
assets.push(...timeBucket);
|
||||
}
|
||||
for (const yearMonth of timelineRestData.buckets.keys()) {
|
||||
const [year, month] = yearMonth.split('-');
|
||||
const [year, month] = yearMonth.split('-', 2);
|
||||
yearMonths.push(`${year}-${Number(month)}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ export const sleep = (ms: number) => {
|
||||
};
|
||||
|
||||
export const padYearMonth = (yearMonth: string) => {
|
||||
const [year, month] = yearMonth.split('-');
|
||||
const [year, month] = yearMonth.split('-', 2);
|
||||
return `${year}-${month.padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
@@ -72,6 +72,7 @@ export const thumbnailUtils = {
|
||||
},
|
||||
async queryThumbnailInViewport(page: Page, collector: (assetId: string) => boolean) {
|
||||
const assetIds: string[] = [];
|
||||
// eslint-disable-next-line unicorn/no-this-outside-of-class
|
||||
for (const thumb of await this.locator(page).all()) {
|
||||
const box = await thumb.boundingBox();
|
||||
if (box) {
|
||||
@@ -151,6 +152,7 @@ export const timelineUtils = {
|
||||
page.evaluate(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
|
||||
return document.querySelector('#asset-grid').scrollTop;
|
||||
});
|
||||
await expect.poll(queryTop).toBeGreaterThan(0);
|
||||
@@ -177,6 +179,7 @@ export const assetViewerUtils = {
|
||||
page.evaluate(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line unicorn/no-optional-chaining-on-undeclared-variable
|
||||
return document.activeElement?.dataset?.asset;
|
||||
});
|
||||
await expect(poll(page, activeElement, (result) => result === assetId)).resolves.toBe(assetId);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable unicorn/no-top-level-assignment-in-function */
|
||||
import {
|
||||
AssetMediaCreateDto,
|
||||
AssetMediaResponseDto,
|
||||
@@ -177,7 +178,7 @@ export const utils = {
|
||||
resetDatabase: async (tables?: string[]) => {
|
||||
client = await utils.connectDatabase();
|
||||
|
||||
tables = tables || [
|
||||
tables ||= [
|
||||
// TODO e2e test for deleting a stack, since it is quite complex
|
||||
'stack',
|
||||
'library',
|
||||
@@ -304,7 +305,7 @@ export const utils = {
|
||||
},
|
||||
|
||||
adminSetup: async (options?: AdminSetupOptions) => {
|
||||
options = options || { onboarding: true };
|
||||
options ||= { onboarding: true };
|
||||
|
||||
await signUpAdmin({ signUpDto: signupDto.admin });
|
||||
const response = await login({ loginCredentialDto: loginDto.admin });
|
||||
@@ -545,6 +546,7 @@ export const utils = {
|
||||
{
|
||||
headers: asBearerAuth(accessToken),
|
||||
fetch: (...args: Parameters<typeof fetch>) =>
|
||||
// eslint-disable-next-line unicorn/no-invalid-argument-count, unicorn/prefer-await
|
||||
fetch(...args).then((response) => {
|
||||
setCookie = response.headers.getSetCookie();
|
||||
return response;
|
||||
@@ -674,7 +676,7 @@ export const utils = {
|
||||
|
||||
cliLogin: async (accessToken: string) => {
|
||||
const key = await utils.createApiKey(accessToken, [Permission.All]);
|
||||
await immichCli(['login', app, `${key.secret}`]);
|
||||
await immichCli(['login', app, key.secret]);
|
||||
return key.secret;
|
||||
},
|
||||
|
||||
@@ -706,6 +708,7 @@ export const utils = {
|
||||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line unicorn/no-top-level-side-effects
|
||||
utils.initSdk();
|
||||
|
||||
if (!existsSync(`${testAssetDir}/albums`)) {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"target": "es2023",
|
||||
"lib": ["dom", "ESNext"],
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"incremental": true,
|
||||
|
||||
@@ -814,7 +814,6 @@
|
||||
"custom_date": "Custom date",
|
||||
"custom_locale": "Custom locale",
|
||||
"custom_locale_description": "Format dates, times, and numbers based on the selected language and region",
|
||||
"custom_url": "Custom URL",
|
||||
"cutoff_date_description": "Keep photos from the last…",
|
||||
"cutoff_day": "{count, plural, one {day} other {days}}",
|
||||
"cutoff_year": "{count, plural, one {year} other {years}}",
|
||||
@@ -1621,7 +1620,7 @@
|
||||
"person": "Person",
|
||||
"person_age_months": "{months, plural, one {# month} other {# months}} old",
|
||||
"person_age_year_months": "1 year, {months, plural, one {# month} other {# months}} old",
|
||||
"person_age_years": "{years, plural, other {# years}} old",
|
||||
"person_age_years": "{years, plural, one {# year} other {# years}} old",
|
||||
"person_birthdate": "Born on {date}",
|
||||
"person_hidden": "{name}{hidden, select, true { (hidden)} other {}}",
|
||||
"person_recognized": "Person recognized",
|
||||
@@ -1948,7 +1947,9 @@
|
||||
"shared_link_app_bar_title": "Shared Links",
|
||||
"shared_link_clipboard_copied_massage": "Copied to clipboard",
|
||||
"shared_link_create_error": "Error while creating shared link",
|
||||
"shared_link_custom_url_description": "Access this shared link with a custom URL",
|
||||
"shared_link_custom_url_description": "Access this shared link with a custom URL name",
|
||||
"shared_link_custom_url_title": "Custom URL",
|
||||
"shared_link_custom_url_warning": "Warning: a custom URL name with a forward slash may not behave as you expect.",
|
||||
"shared_link_edit_description_hint": "Enter the share description",
|
||||
"shared_link_edit_expire_after_option_day": "1 day",
|
||||
"shared_link_edit_expire_after_option_days": "{count} days",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "immich-ml"
|
||||
version = "3.0.2"
|
||||
version = "3.0.3"
|
||||
description = ""
|
||||
authors = [{ name = "Hau Tran", email = "alex.tran1502@gmail.com" }]
|
||||
requires-python = ">=3.11,<4.0"
|
||||
|
||||
2
machine-learning/uv.lock
generated
2
machine-learning/uv.lock
generated
@@ -974,7 +974,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "immich-ml"
|
||||
version = "3.0.2"
|
||||
version = "3.0.3"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiocache" },
|
||||
|
||||
38
mise.lock
38
mise.lock
@@ -287,43 +287,43 @@ url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536406"
|
||||
|
||||
[[tools.pnpm]]
|
||||
version = "11.6.0"
|
||||
version = "11.13.1"
|
||||
backend = "aqua:pnpm/pnpm"
|
||||
|
||||
[tools.pnpm."platforms.linux-arm64"]
|
||||
checksum = "sha256:2fec653ff6dadab340d1c3d2214688a7451cc471f39710839440b293ca7c53b0"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120174"
|
||||
checksum = "sha256:b52db99d215ed7dc9563aed815953c62a6c1ffd7cd75803d3a07ad7e4f246aed"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.13.1/pnpm-linux-arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563983"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:56a78c08cf22adf29e7dacb6f7f100139731693863d20fb94a7883463a62169e"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-arm64-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120177"
|
||||
checksum = "sha256:2cadd4fc815c591f498a0a84c9e74a836e3e8c1236275f6e1cfd355bae6ae957"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.13.1/pnpm-linux-arm64-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563985"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.linux-x64"]
|
||||
checksum = "sha256:74d64c1646385fb21691f32f0ab6aca1a9f5c829ba54d3cda3a24838a228e68c"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-x64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120176"
|
||||
checksum = "sha256:bd6d4b0e14536207ad76bc838f5980cecd968da15f69aae0b207380cca3f2e98"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.13.1/pnpm-linux-x64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563986"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:7a0c463a09d912fba6b7d9eca0a946bc228ea50f3015a05c09a29e7e85a932d7"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-x64-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120175"
|
||||
checksum = "sha256:ba19690f4ed1b64f1203ade14e9216352b46232d5582468b26a0160e0c9618c5"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.13.1/pnpm-linux-x64-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563980"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.macos-arm64"]
|
||||
checksum = "sha256:87c901635a14481fb30566a3749041134ffd4317bc6fe866c345b69fdf9b6b85"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-darwin-arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120180"
|
||||
checksum = "sha256:765c2bf04e8129cb58c0f946e324262e418370b35a203b50b1f06a0567ef8bc1"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.13.1/pnpm-darwin-arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563984"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.windows-x64"]
|
||||
checksum = "sha256:91c753435542b04859c689304fae0dd64eba6b40937cfa426a48485b712e4e9c"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-win32-x64.zip"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120173"
|
||||
checksum = "sha256:d8bebbc71df2702961c1d34a5e61196bc0aa3bbde33c33253f6afa3dd4546a6d"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.13.1/pnpm-win32-x64.zip"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563981"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[[tools.terragrunt]]
|
||||
|
||||
@@ -16,7 +16,7 @@ config_roots = [
|
||||
|
||||
[tools]
|
||||
node = "24.15.0"
|
||||
pnpm = "11.6.0"
|
||||
pnpm = "11.13.1"
|
||||
terragrunt = "1.0.3"
|
||||
opentofu = "1.11.6"
|
||||
"npm:oazapfts" = "7.5.0"
|
||||
|
||||
@@ -89,13 +89,6 @@ flutter {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
constraints {
|
||||
implementation("androidx.glance:glance-appwidget") {
|
||||
version { strictly libs.versions.glance.get() }
|
||||
because 'home_widget requests 1.+ which can resolve to pre-releases incompatible with our compileSdk/AGP'
|
||||
}
|
||||
}
|
||||
|
||||
implementation libs.okhttp
|
||||
implementation libs.cronet.embedded
|
||||
implementation libs.media3.datasource.okhttp
|
||||
|
||||
@@ -23,6 +23,6 @@ class ImmichApp : Application() {
|
||||
// as the previous start might have been killed without unlocking.
|
||||
if (BackgroundEngineLock.connectEngines > 0) return@postDelayed
|
||||
BackgroundWorkerApiImpl.enqueueBackgroundWorker(this)
|
||||
}, 5000)
|
||||
}, 15000)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import app.alextran.immich.images.LocalImageApi
|
||||
import app.alextran.immich.images.LocalImagesImpl
|
||||
import app.alextran.immich.images.RemoteImageApi
|
||||
import app.alextran.immich.images.RemoteImagesImpl
|
||||
import app.alextran.immich.media.AssetMediaApiImpl
|
||||
import app.alextran.immich.permission.PermissionApi
|
||||
import app.alextran.immich.permission.PermissionApiImpl
|
||||
import app.alextran.immich.sync.NativeSyncApi
|
||||
@@ -66,6 +67,7 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
flutterEngine.plugins.add(backgroundEngineLockImpl)
|
||||
flutterEngine.plugins.add(nativeSyncApiImpl)
|
||||
flutterEngine.plugins.add(permissionApiImpl)
|
||||
flutterEngine.plugins.add(AssetMediaApiImpl(ctx))
|
||||
}
|
||||
|
||||
fun cancelPlugins(flutterEngine: FlutterEngine) {
|
||||
@@ -75,6 +77,8 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
nativeApi?.detachFromEngine()
|
||||
val permissionApi = flutterEngine.plugins.get(PermissionApiImpl::class.java) as ImmichPlugin?
|
||||
permissionApi?.detachFromEngine()
|
||||
val assetMediaApi = flutterEngine.plugins.get(AssetMediaApiImpl::class.java) as ImmichPlugin?
|
||||
assetMediaApi?.detachFromEngine()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import androidx.work.ListenableWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import app.alextran.immich.MainActivity
|
||||
import app.alextran.immich.R
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import io.flutter.FlutterInjector
|
||||
@@ -61,6 +62,11 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
|
||||
}
|
||||
|
||||
override fun startWork(): ListenableFuture<Result> {
|
||||
if (BackgroundWorkerPreferences(ctx).isLocked() && BackgroundEngineLock.connectEngines > 0) {
|
||||
Log.i(TAG, "Foreground engine active, skipping background worker")
|
||||
return Futures.immediateFuture(Result.success())
|
||||
}
|
||||
|
||||
Log.i(TAG, "Starting background upload worker")
|
||||
|
||||
if (!loader.initialized()) {
|
||||
@@ -77,6 +83,10 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
|
||||
showNotification(notificationConfig.first, notificationConfig.second)
|
||||
|
||||
loader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
|
||||
if (isStopped || isComplete) {
|
||||
return@ensureInitializationCompleteAsync
|
||||
}
|
||||
|
||||
engine = FlutterEngine(ctx)
|
||||
FlutterEngineCache.getInstance().put(BackgroundWorkerApiImpl.ENGINE_CACHE_KEY, engine!!)
|
||||
|
||||
@@ -143,11 +153,17 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
|
||||
return
|
||||
}
|
||||
|
||||
val api = flutterApi
|
||||
if (api == null) {
|
||||
Handler(Looper.getMainLooper()).postAtFrontOfQueue {
|
||||
complete(Result.failure())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Handler(Looper.getMainLooper()).postAtFrontOfQueue {
|
||||
if (flutterApi != null) {
|
||||
flutterApi?.cancel {
|
||||
complete(Result.failure())
|
||||
}
|
||||
api.cancel {
|
||||
complete(Result.failure())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,17 +2,30 @@ package app.alextran.immich.core
|
||||
|
||||
import androidx.annotation.CallSuper
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.isActive
|
||||
|
||||
abstract class ImmichPlugin : FlutterPlugin {
|
||||
private var detached: Boolean = false;
|
||||
private var detached: Boolean = false
|
||||
|
||||
private var supervisor = SupervisorJob()
|
||||
protected var scope = CoroutineScope(supervisor + Dispatchers.Default)
|
||||
private set
|
||||
|
||||
@CallSuper
|
||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
detached = false;
|
||||
detached = false
|
||||
if (!scope.isActive) {
|
||||
supervisor = SupervisorJob()
|
||||
scope = CoroutineScope(supervisor + Dispatchers.Default)
|
||||
}
|
||||
}
|
||||
|
||||
fun detachFromEngine() {
|
||||
detached = true
|
||||
supervisor.cancel()
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@@ -22,8 +35,9 @@ abstract class ImmichPlugin : FlutterPlugin {
|
||||
|
||||
fun <T> completeWhenActive(callback: (T) -> Unit, value: T) {
|
||||
if (detached) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
callback(value);
|
||||
|
||||
callback(value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,13 +64,7 @@ fun Bitmap.toNativeBuffer(): Map<String, Long> {
|
||||
// native convert declined (OOM/lock) -> fall through to the Skia copy path below.
|
||||
}
|
||||
// Other non-8888 configs (e.g. HDR F16) still need Skia's convert; 8-bit is copied as-is.
|
||||
val bitmap = if (config != Bitmap.Config.ARGB_8888) {
|
||||
val converted = copy(Bitmap.Config.ARGB_8888, false)
|
||||
recycle()
|
||||
converted ?: throw IOException("could not convert bitmap to ARGB_8888")
|
||||
} else {
|
||||
this
|
||||
}
|
||||
val bitmap = toArgb8888()
|
||||
val size = bitmap.width * bitmap.height * 4
|
||||
val pointer = NativeBuffer.allocate(size)
|
||||
try {
|
||||
@@ -217,7 +211,7 @@ class LocalImagesImpl(context: Context) : LocalImageApi {
|
||||
if (orientation == ExifInterface.ORIENTATION_NORMAL || orientation == ExifInterface.ORIENTATION_UNDEFINED) {
|
||||
bitmap.toNativeBuffer()
|
||||
} else {
|
||||
rotateToNativeBuffer(bitmap, orientation, signal)
|
||||
rotateToNativeBuffer(bitmap, orientation)
|
||||
}
|
||||
}
|
||||
// Don't re-check cancellation here: res owns a malloc'd buffer, and bailing to CANCELLED would
|
||||
@@ -257,39 +251,7 @@ class LocalImagesImpl(context: Context) : LocalImageApi {
|
||||
}
|
||||
|
||||
private fun rawOrientation(uri: Uri): Int {
|
||||
return resolver.openInputStream(uri)?.use {
|
||||
ExifInterface(it).getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
|
||||
} ?: ExifInterface.ORIENTATION_NORMAL
|
||||
}
|
||||
|
||||
// ImageDecoder / loadThumbnail skip EXIF orientation for raw (e.g. DNG) on Q+, so the decoded
|
||||
// bitmap comes back unrotated. Rotate it into the output buffer in native code (one pass, no
|
||||
// intermediate rotated bitmap).
|
||||
private fun rotateToNativeBuffer(bitmap: Bitmap, orientation: Int, signal: CancellationSignal): Map<String, Long> {
|
||||
signal.throwIfCanceled()
|
||||
// Force ARGB_8888: the native rotate needs a lockable 8888 buffer, and toNativeBuffer() below
|
||||
// allocates width*height*4 (an F16/HDR decode would otherwise under-allocate). No-op for the
|
||||
// common already-8888 case.
|
||||
val src = if (bitmap.config != Bitmap.Config.ARGB_8888) {
|
||||
val converted = bitmap.copy(Bitmap.Config.ARGB_8888, false)
|
||||
bitmap.recycle()
|
||||
converted ?: throw IOException("could not convert bitmap to ARGB_8888")
|
||||
} else {
|
||||
bitmap
|
||||
}
|
||||
try {
|
||||
val info = IntArray(3)
|
||||
val pointer = NativeImage.rotate(src, orientation, info)
|
||||
if (pointer == 0L) throw IOException("native rotate failed for orientation $orientation")
|
||||
return mapOf(
|
||||
"pointer" to pointer,
|
||||
"width" to info[0].toLong(),
|
||||
"height" to info[1].toLong(),
|
||||
"rowBytes" to info[2].toLong()
|
||||
)
|
||||
} finally {
|
||||
if (!src.isRecycled) src.recycle()
|
||||
}
|
||||
return resolver.openInputStream(uri)?.use { readOrientation(it) } ?: ExifInterface.ORIENTATION_NORMAL
|
||||
}
|
||||
|
||||
private fun decodeVideoThumbnail(id: Long, target: Size, signal: CancellationSignal): Bitmap {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package app.alextran.immich.images
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import app.alextran.immich.NativeImage
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
fun readOrientation(input: InputStream): Int =
|
||||
ExifInterface(input).getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
|
||||
|
||||
// Reads the EXIF orientation (TIFF tag 0x0112) from a raw file's IFD0.
|
||||
fun readRawOrientation(buffer: ByteBuffer, len: Int): Int {
|
||||
try {
|
||||
if (len < 8) return ExifInterface.ORIENTATION_NORMAL
|
||||
buffer.order(
|
||||
when (buffer.get(0).toInt() and 0xFF) {
|
||||
0x49 -> ByteOrder.LITTLE_ENDIAN
|
||||
0x4D -> ByteOrder.BIG_ENDIAN
|
||||
else -> return ExifInterface.ORIENTATION_NORMAL
|
||||
}
|
||||
)
|
||||
val ifd0 = buffer.getInt(4).toLong() and 0xFFFFFFFFL
|
||||
if (ifd0 < 8 || ifd0 + 2 > len) return ExifInterface.ORIENTATION_NORMAL
|
||||
val start = ifd0.toInt()
|
||||
val entries = buffer.getShort(start).toInt() and 0xFFFF
|
||||
if (entries <= 0 || entries > 512) return ExifInterface.ORIENTATION_NORMAL
|
||||
// Each IFD entry is 12 bytes: tag(2) type(2) count(4) value(4); orientation is a SHORT in value.
|
||||
for (i in 0 until entries) {
|
||||
val p = start + 2 + i * 12
|
||||
if (p + 12 > len) return ExifInterface.ORIENTATION_NORMAL
|
||||
if ((buffer.getShort(p).toInt() and 0xFFFF) == 0x0112) {
|
||||
val value = buffer.getShort(p + 8).toInt() and 0xFFFF
|
||||
return if (value in 1..8) value else ExifInterface.ORIENTATION_NORMAL
|
||||
}
|
||||
}
|
||||
return ExifInterface.ORIENTATION_NORMAL
|
||||
} catch (_: Exception) {
|
||||
return ExifInterface.ORIENTATION_NORMAL
|
||||
}
|
||||
}
|
||||
|
||||
fun Bitmap.toArgb8888(): Bitmap {
|
||||
if (config == Bitmap.Config.ARGB_8888) return this
|
||||
val converted = copy(Bitmap.Config.ARGB_8888, false)
|
||||
recycle()
|
||||
return converted ?: throw IOException("could not convert bitmap to ARGB_8888")
|
||||
}
|
||||
|
||||
// Force ARGB_8888 first: the native rotate needs a lockable 8888 buffer and allocates w*h*4.
|
||||
fun rotateToNativeBuffer(bitmap: Bitmap, orientation: Int): Map<String, Long> {
|
||||
val src = bitmap.toArgb8888()
|
||||
try {
|
||||
val info = IntArray(3)
|
||||
val pointer = NativeImage.rotate(src, orientation, info)
|
||||
if (pointer == 0L) throw IOException("native rotate failed for orientation $orientation")
|
||||
return mapOf(
|
||||
"pointer" to pointer,
|
||||
"width" to info[0].toLong(),
|
||||
"height" to info[1].toLong(),
|
||||
"rowBytes" to info[2].toLong()
|
||||
)
|
||||
} finally {
|
||||
if (!src.isRecycled) src.recycle()
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import android.graphics.ImageDecoder
|
||||
import android.os.Build
|
||||
import android.os.CancellationSignal
|
||||
import android.os.OperationCanceledException
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import app.alextran.immich.INITIAL_BUFFER_SIZE
|
||||
import app.alextran.immich.NativeBuffer
|
||||
import app.alextran.immich.NativeByteBuffer
|
||||
@@ -30,6 +31,35 @@ private const val MAX_PREALLOC_BYTES = 128 * 1024 * 1024
|
||||
|
||||
private class RemoteRequest(val cancellationSignal: CancellationSignal)
|
||||
|
||||
// The full mimeTypes.raw set the server can serve (short + vendor forms).
|
||||
private val RAW_MIME_TYPES = setOf(
|
||||
"image/3fr", "image/ari", "image/arw",
|
||||
"image/cap", "image/cin", "image/cr2",
|
||||
"image/cr3", "image/crw", "image/dcr",
|
||||
"image/dng", "image/erf", "image/fff",
|
||||
"image/iiq", "image/k25", "image/kdc",
|
||||
"image/mrw", "image/nef", "image/nrw",
|
||||
"image/orf", "image/ori", "image/pef",
|
||||
"image/psd", "image/raf", "image/raw",
|
||||
"image/rw2", "image/rwl", "image/sr2",
|
||||
"image/srf", "image/srw", "image/vnd.adobe.photoshop",
|
||||
"image/x-adobe-dng", "image/x-arriflex-ari", "image/x-canon-cr2",
|
||||
"image/x-canon-cr3", "image/x-canon-crw", "image/x-epson-erf",
|
||||
"image/x-fuji-raf", "image/x-hasselblad-3fr", "image/x-hasselblad-fff",
|
||||
"image/x-kodak-dcr", "image/x-kodak-k25", "image/x-kodak-kdc",
|
||||
"image/x-leica-rwl", "image/x-minolta-mrw", "image/x-nikon-nef",
|
||||
"image/x-nikon-nrw", "image/x-olympus-orf", "image/x-olympus-ori",
|
||||
"image/x-panasonic-raw", "image/x-panasonic-rw2", "image/x-pentax-pef",
|
||||
"image/x-phantom-cin", "image/x-phaseone-cap", "image/x-phaseone-iiq",
|
||||
"image/x-samsung-srw", "image/x-sigma-x3f", "image/x-sony-arw",
|
||||
"image/x-sony-sr2", "image/x-sony-srf", "image/x3f",
|
||||
)
|
||||
|
||||
private fun isRawMime(contentType: String?): Boolean {
|
||||
val mime = contentType?.substringBefore(';')?.trim()?.lowercase() ?: return false
|
||||
return mime in RAW_MIME_TYPES
|
||||
}
|
||||
|
||||
class RemoteImagesImpl(context: Context) : RemoteImageApi {
|
||||
private val requestMap = ConcurrentHashMap<Long, RemoteRequest>()
|
||||
|
||||
@@ -57,7 +87,7 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi {
|
||||
ImageFetcherManager.fetch(
|
||||
url,
|
||||
signal,
|
||||
onSuccess = { buffer ->
|
||||
onSuccess = { buffer, contentType ->
|
||||
if (signal.isCanceled) {
|
||||
requestMap.remove(requestId)
|
||||
buffer.free()
|
||||
@@ -72,8 +102,18 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi {
|
||||
if (!preferEncoded && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
decodeExecutor.execute {
|
||||
val res = if (signal.isCanceled) null else try {
|
||||
val source = ImageDecoder.createSource(NativeBuffer.wrap(buffer.pointer, buffer.offset))
|
||||
source.decodeBitmap().toNativeBuffer()
|
||||
val bitmap = ImageDecoder.createSource(NativeBuffer.wrap(buffer.pointer, buffer.offset)).decodeBitmap()
|
||||
// The embedded preview a raw decodes to has no orientation, so read the container's.
|
||||
val orientation = if (isRawMime(contentType)) {
|
||||
readRawOrientation(NativeBuffer.wrap(buffer.pointer, buffer.offset), buffer.offset)
|
||||
} else {
|
||||
ExifInterface.ORIENTATION_NORMAL
|
||||
}
|
||||
if (orientation == ExifInterface.ORIENTATION_NORMAL || orientation == ExifInterface.ORIENTATION_UNDEFINED) {
|
||||
bitmap.toNativeBuffer()
|
||||
} else {
|
||||
rotateToNativeBuffer(bitmap, orientation)
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
@@ -154,7 +194,7 @@ private object ImageFetcherManager {
|
||||
fun fetch(
|
||||
url: String,
|
||||
signal: CancellationSignal,
|
||||
onSuccess: (NativeByteBuffer) -> Unit,
|
||||
onSuccess: (NativeByteBuffer, String?) -> Unit,
|
||||
onFailure: (Exception) -> Unit,
|
||||
) {
|
||||
fetcher.fetch(url, signal, onSuccess, onFailure)
|
||||
@@ -185,7 +225,7 @@ private sealed interface ImageFetcher {
|
||||
fun fetch(
|
||||
url: String,
|
||||
signal: CancellationSignal,
|
||||
onSuccess: (NativeByteBuffer) -> Unit,
|
||||
onSuccess: (NativeByteBuffer, String?) -> Unit,
|
||||
onFailure: (Exception) -> Unit,
|
||||
)
|
||||
|
||||
@@ -203,7 +243,7 @@ private class CronetImageFetcher : ImageFetcher {
|
||||
override fun fetch(
|
||||
url: String,
|
||||
signal: CancellationSignal,
|
||||
onSuccess: (NativeByteBuffer) -> Unit,
|
||||
onSuccess: (NativeByteBuffer, String?) -> Unit,
|
||||
onFailure: (Exception) -> Unit,
|
||||
) {
|
||||
synchronized(stateLock) {
|
||||
@@ -271,7 +311,7 @@ private class CronetImageFetcher : ImageFetcher {
|
||||
}
|
||||
|
||||
private class FetchCallback(
|
||||
private val onSuccess: (NativeByteBuffer) -> Unit,
|
||||
private val onSuccess: (NativeByteBuffer, String?) -> Unit,
|
||||
private val onFailure: (Exception) -> Unit,
|
||||
private val onComplete: () -> Unit,
|
||||
) : UrlRequest.Callback() {
|
||||
@@ -328,7 +368,9 @@ private class CronetImageFetcher : ImageFetcher {
|
||||
}
|
||||
|
||||
override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) {
|
||||
onSuccess(buffer!!)
|
||||
val contentType = info.allHeaders.entries
|
||||
.firstOrNull { it.key.equals("content-type", ignoreCase = true) }?.value?.firstOrNull()
|
||||
onSuccess(buffer!!, contentType)
|
||||
onComplete()
|
||||
}
|
||||
|
||||
@@ -379,7 +421,7 @@ private class OkHttpImageFetcher private constructor(
|
||||
override fun fetch(
|
||||
url: String,
|
||||
signal: CancellationSignal,
|
||||
onSuccess: (NativeByteBuffer) -> Unit,
|
||||
onSuccess: (NativeByteBuffer, String?) -> Unit,
|
||||
onFailure: (Exception) -> Unit,
|
||||
) {
|
||||
synchronized(stateLock) {
|
||||
@@ -433,7 +475,7 @@ private class OkHttpImageFetcher private constructor(
|
||||
buffer.ensureHeadroom()
|
||||
}
|
||||
}
|
||||
onSuccess(buffer)
|
||||
onSuccess(buffer, response.header("Content-Type"))
|
||||
} catch (e: Exception) {
|
||||
buffer.free()
|
||||
onFailure(e)
|
||||
|
||||
354
mobile/android/app/src/main/kotlin/app/alextran/immich/media/AssetMedia.g.kt
generated
Normal file
354
mobile/android/app/src/main/kotlin/app/alextran/immich/media/AssetMedia.g.kt
generated
Normal file
@@ -0,0 +1,354 @@
|
||||
// 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.media
|
||||
|
||||
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 AssetMediaPigeonUtils {
|
||||
|
||||
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()
|
||||
|
||||
enum class AssetMediaActionStatus(val raw: Int) {
|
||||
DONE(0),
|
||||
ALREADY_IN_STATE(1),
|
||||
NOT_FOUND(2),
|
||||
FAILED(3);
|
||||
|
||||
companion object {
|
||||
fun ofRaw(raw: Int): AssetMediaActionStatus? {
|
||||
return values().firstOrNull { it.raw == raw }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Generated class from Pigeon that represents data sent in messages. */
|
||||
data class AssetMediaActionResult (
|
||||
val id: String,
|
||||
val status: AssetMediaActionStatus
|
||||
)
|
||||
{
|
||||
companion object {
|
||||
fun fromList(pigeonVar_list: List<Any?>): AssetMediaActionResult {
|
||||
val id = pigeonVar_list[0] as String
|
||||
val status = pigeonVar_list[1] as AssetMediaActionStatus
|
||||
return AssetMediaActionResult(id, status)
|
||||
}
|
||||
}
|
||||
fun toList(): List<Any?> {
|
||||
return listOf(
|
||||
id,
|
||||
status,
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other == null || other.javaClass != javaClass) {
|
||||
return false
|
||||
}
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
val other = other as AssetMediaActionResult
|
||||
return AssetMediaPigeonUtils.deepEquals(this.id, other.id) && AssetMediaPigeonUtils.deepEquals(this.status, other.status)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = javaClass.hashCode()
|
||||
result = 31 * result + AssetMediaPigeonUtils.deepHash(this.id)
|
||||
result = 31 * result + AssetMediaPigeonUtils.deepHash(this.status)
|
||||
return result
|
||||
}
|
||||
}
|
||||
private open class AssetMediaPigeonCodec : StandardMessageCodec() {
|
||||
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||
return when (type) {
|
||||
129.toByte() -> {
|
||||
return (readValue(buffer) as Long?)?.let {
|
||||
AssetMediaActionStatus.ofRaw(it.toInt())
|
||||
}
|
||||
}
|
||||
130.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
AssetMediaActionResult.fromList(it)
|
||||
}
|
||||
}
|
||||
else -> super.readValueOfType(type, buffer)
|
||||
}
|
||||
}
|
||||
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
||||
when (value) {
|
||||
is AssetMediaActionStatus -> {
|
||||
stream.write(129)
|
||||
writeValue(stream, value.raw.toLong())
|
||||
}
|
||||
is AssetMediaActionResult -> {
|
||||
stream.write(130)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
else -> super.writeValue(stream, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||
interface AssetMediaApi {
|
||||
fun trash(ids: List<String>, callback: (Result<List<AssetMediaActionResult>>) -> Unit)
|
||||
fun restore(ids: List<String>, callback: (Result<List<AssetMediaActionResult>>) -> Unit)
|
||||
fun trashedAmong(ids: List<String>, callback: (Result<List<String>>) -> Unit)
|
||||
|
||||
companion object {
|
||||
/** The codec used by AssetMediaApi. */
|
||||
val codec: MessageCodec<Any?> by lazy {
|
||||
AssetMediaPigeonCodec()
|
||||
}
|
||||
/** Sets up an instance of `AssetMediaApi` to handle messages through the `binaryMessenger`. */
|
||||
@JvmOverloads
|
||||
fun setUp(binaryMessenger: BinaryMessenger, api: AssetMediaApi?, messageChannelSuffix: String = "") {
|
||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.AssetMediaApi.trash$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val idsArg = args[0] as List<String>
|
||||
api.trash(idsArg) { result: Result<List<AssetMediaActionResult>> ->
|
||||
val error = result.exceptionOrNull()
|
||||
if (error != null) {
|
||||
reply.reply(AssetMediaPigeonUtils.wrapError(error))
|
||||
} else {
|
||||
val data = result.getOrNull()
|
||||
reply.reply(AssetMediaPigeonUtils.wrapResult(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.AssetMediaApi.restore$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val idsArg = args[0] as List<String>
|
||||
api.restore(idsArg) { result: Result<List<AssetMediaActionResult>> ->
|
||||
val error = result.exceptionOrNull()
|
||||
if (error != null) {
|
||||
reply.reply(AssetMediaPigeonUtils.wrapError(error))
|
||||
} else {
|
||||
val data = result.getOrNull()
|
||||
reply.reply(AssetMediaPigeonUtils.wrapResult(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.AssetMediaApi.trashedAmong$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val idsArg = args[0] as List<String>
|
||||
api.trashedAmong(idsArg) { result: Result<List<String>> ->
|
||||
val error = result.exceptionOrNull()
|
||||
if (error != null) {
|
||||
reply.reply(AssetMediaPigeonUtils.wrapError(error))
|
||||
} else {
|
||||
val data = result.getOrNull()
|
||||
reply.reply(AssetMediaPigeonUtils.wrapResult(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
package app.alextran.immich.media
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.MediaStore
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.IntentSenderRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import app.alextran.immich.core.ImmichPlugin
|
||||
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.BinaryMessenger
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.UUID
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
private enum class MediaAction { TRASH, RESTORE }
|
||||
|
||||
private data class MediaItem(val uri: Uri, val isTrashed: Boolean)
|
||||
|
||||
private const val MAX_QUERY_ARGS = 900
|
||||
|
||||
private const val MAX_CONSENT_URIS = 500
|
||||
|
||||
@SuppressLint("NewApi", "InlinedApi")
|
||||
class AssetMediaApiImpl(context: Context) : ImmichPlugin(), AssetMediaApi, ActivityAware {
|
||||
private val ctx: Context = context.applicationContext
|
||||
private var binaryMessenger: BinaryMessenger? = null
|
||||
private var activityBinding: ActivityPluginBinding? = null
|
||||
|
||||
private val supportsMediaRequest: Boolean
|
||||
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
|
||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
super.onAttachedToEngine(binding)
|
||||
binaryMessenger = binding.binaryMessenger
|
||||
AssetMediaApi.setUp(binding.binaryMessenger, this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
super.onDetachedFromEngine(binding)
|
||||
binaryMessenger?.let { AssetMediaApi.setUp(it, null) }
|
||||
binaryMessenger = null
|
||||
activityBinding = null
|
||||
}
|
||||
|
||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||
activityBinding = binding
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivityForConfigChanges() {
|
||||
activityBinding = null
|
||||
}
|
||||
|
||||
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||
activityBinding = binding
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivity() {
|
||||
activityBinding = null
|
||||
}
|
||||
|
||||
override fun trash(ids: List<String>, callback: (Result<List<AssetMediaActionResult>>) -> Unit) =
|
||||
runAction(ids, MediaAction.TRASH, callback)
|
||||
|
||||
override fun restore(
|
||||
ids: List<String>,
|
||||
callback: (Result<List<AssetMediaActionResult>>) -> Unit
|
||||
) =
|
||||
runAction(ids, MediaAction.RESTORE, callback)
|
||||
|
||||
override fun trashedAmong(ids: List<String>, callback: (Result<List<String>>) -> Unit) =
|
||||
respond(callback, "TRASHED_AMONG_ERROR") {
|
||||
if (!supportsMediaRequest) return@respond emptyList()
|
||||
val items = queryMediaItems(ids)
|
||||
ids.filter { items[it]?.isTrashed == true }
|
||||
}
|
||||
|
||||
private fun runAction(
|
||||
ids: List<String>,
|
||||
action: MediaAction,
|
||||
callback: (Result<List<AssetMediaActionResult>>) -> Unit,
|
||||
) = respond(callback, "MEDIA_ACTION_ERROR") {
|
||||
if (ids.isEmpty()) return@respond emptyList()
|
||||
if (!supportsMediaRequest) {
|
||||
return@respond ids.map { AssetMediaActionResult(it, AssetMediaActionStatus.FAILED) }
|
||||
}
|
||||
|
||||
val items = queryMediaItems(ids)
|
||||
|
||||
val targets = ids.mapNotNull { id ->
|
||||
val item = items[id] ?: return@mapNotNull null
|
||||
val needsAction = when (action) {
|
||||
MediaAction.TRASH -> !item.isTrashed
|
||||
MediaAction.RESTORE -> item.isTrashed
|
||||
}
|
||||
|
||||
if (needsAction) id to item.uri else null
|
||||
}.toMap()
|
||||
|
||||
val applied = buildMap {
|
||||
for (batch in targets.entries.chunked(MAX_CONSENT_URIS)) {
|
||||
val result = if (requestConsent(action, batch.map { it.value })) {
|
||||
AssetMediaActionStatus.DONE
|
||||
} else {
|
||||
AssetMediaActionStatus.FAILED
|
||||
}
|
||||
batch.forEach { put(it.key, result) }
|
||||
}
|
||||
}
|
||||
|
||||
ids.map { id ->
|
||||
val status = when (id) {
|
||||
in applied -> applied.getValue(id)
|
||||
!in items -> AssetMediaActionStatus.NOT_FOUND
|
||||
else -> AssetMediaActionStatus.ALREADY_IN_STATE
|
||||
}
|
||||
AssetMediaActionResult(id, status)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun <T> respond(callback: (Result<T>) -> Unit, errorCode: String, work: suspend () -> T) {
|
||||
scope.launch {
|
||||
try {
|
||||
completeWhenActive(callback, Result.success(work()))
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
completeWhenActive(callback, Result.failure(FlutterError(errorCode, e.message, null)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun requestConsent(action: MediaAction, uris: List<Uri>): Boolean =
|
||||
withContext(Dispatchers.Main) {
|
||||
val activity = activityBinding?.activity as? ComponentActivity ?: return@withContext false
|
||||
val resolver = ctx.contentResolver
|
||||
val request = when (action) {
|
||||
MediaAction.TRASH -> MediaStore.createTrashRequest(resolver, uris, true)
|
||||
MediaAction.RESTORE -> MediaStore.createTrashRequest(resolver, uris, false)
|
||||
}
|
||||
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
val key = "immich_asset_media_api_${UUID.randomUUID()}"
|
||||
var launcher: ActivityResultLauncher<IntentSenderRequest>? = null
|
||||
launcher = activity.activityResultRegistry.register(
|
||||
key,
|
||||
ActivityResultContracts.StartIntentSenderForResult()
|
||||
) { result ->
|
||||
launcher?.unregister()
|
||||
if (continuation.isActive) {
|
||||
continuation.resume(result.resultCode == Activity.RESULT_OK)
|
||||
}
|
||||
}
|
||||
continuation.invokeOnCancellation { launcher.unregister() }
|
||||
try {
|
||||
launcher.launch(IntentSenderRequest.Builder(request.intentSender).build())
|
||||
} catch (_: Exception) {
|
||||
launcher.unregister()
|
||||
if (continuation.isActive) {
|
||||
continuation.resume(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun queryMediaItems(ids: List<String>): Map<String, MediaItem> =
|
||||
withContext(Dispatchers.IO) {
|
||||
val numeric = ids.filter { it.toLongOrNull() != null }
|
||||
if (numeric.isEmpty()) {
|
||||
return@withContext emptyMap()
|
||||
}
|
||||
|
||||
buildMap {
|
||||
for (chunk in numeric.chunked(MAX_QUERY_ARGS)) {
|
||||
ensureActive()
|
||||
val placeholders = chunk.joinToString(",") { "?" }
|
||||
val args = Bundle().apply {
|
||||
putString(
|
||||
ContentResolver.QUERY_ARG_SQL_SELECTION,
|
||||
"${MediaStore.Files.FileColumns._ID} IN ($placeholders)"
|
||||
)
|
||||
putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, chunk.toTypedArray())
|
||||
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE)
|
||||
}
|
||||
|
||||
ctx.contentResolver.query(
|
||||
MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL),
|
||||
arrayOf(
|
||||
MediaStore.Files.FileColumns._ID,
|
||||
MediaStore.Files.FileColumns.MEDIA_TYPE,
|
||||
MediaStore.Files.FileColumns.IS_TRASHED,
|
||||
),
|
||||
args,
|
||||
null,
|
||||
)?.use { cursor ->
|
||||
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)
|
||||
val typeColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE)
|
||||
val trashedColumn =
|
||||
cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.IS_TRASHED)
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(idColumn)
|
||||
val uri = mediaContentUri(cursor.getInt(typeColumn), id) ?: continue
|
||||
put(id.toString(), MediaItem(uri, cursor.getInt(trashedColumn) == 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun mediaContentUri(mediaType: Int, id: Long): Uri? = when (mediaType) {
|
||||
MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE ->
|
||||
ContentUris.withAppendedId(
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id
|
||||
)
|
||||
|
||||
MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO ->
|
||||
ContentUris.withAppendedId(
|
||||
MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL),
|
||||
id
|
||||
)
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
package app.alextran.immich.sync
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.MediaStore
|
||||
import androidx.annotation.RequiresApi
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||
import io.flutter.plugin.common.PluginRegistry
|
||||
|
||||
class MediaTrashDelegate(
|
||||
context: Context,
|
||||
private val trashRequestCode: Int = 1002,
|
||||
) : PluginRegistry.ActivityResultListener {
|
||||
private val ctx = context.applicationContext
|
||||
private var activityBinding: ActivityPluginBinding? = null
|
||||
private var pendingResult: ((Result<Boolean>) -> Unit)? = null
|
||||
|
||||
private fun hasManageMediaPermission(): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
MediaStore.canManageMedia(ctx)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun restoreFromTrashById(mediaId: String, type: Long, callback: (Result<Boolean>) -> Unit) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R || !hasManageMediaPermission()) {
|
||||
callback(Result.failure(FlutterError("PERMISSION_DENIED", "Media permission required", null)))
|
||||
return
|
||||
}
|
||||
|
||||
val id = mediaId.toLongOrNull()
|
||||
if (id == null) {
|
||||
callback(Result.failure(FlutterError("INVALID_ID", "The file id is not a valid number: $mediaId", null)))
|
||||
return
|
||||
}
|
||||
|
||||
if (!isInTrash(id)) {
|
||||
callback(Result.failure(FlutterError("TRASH_NOT_FOUND", "Item with id=$id not found in trash", null)))
|
||||
return
|
||||
}
|
||||
|
||||
restoreUri(ContentUris.withAppendedId(contentUriForType(type.toInt()), id), callback)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
private fun restoreUri(
|
||||
contentUri: Uri,
|
||||
callback: (Result<Boolean>) -> Unit,
|
||||
) {
|
||||
val activity = activityBinding?.activity
|
||||
if (activity == null) {
|
||||
callback(Result.failure(FlutterError("NO_ACTIVITY", "Activity not available", null)))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val pendingIntent = MediaStore.createTrashRequest(ctx.contentResolver, listOf(contentUri), false)
|
||||
pendingResult = callback
|
||||
activity.startIntentSenderForResult(
|
||||
pendingIntent.intentSender,
|
||||
trashRequestCode,
|
||||
null,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
pendingResult = null
|
||||
callback(
|
||||
Result.failure(
|
||||
FlutterError("TRASH_ERROR", "Error creating or starting trash request", e.toString())
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
private fun isInTrash(id: Long): Boolean {
|
||||
val filesUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
|
||||
val args = Bundle().apply {
|
||||
putString(ContentResolver.QUERY_ARG_SQL_SELECTION, "${MediaStore.Files.FileColumns._ID}=?")
|
||||
putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(id.toString()))
|
||||
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY)
|
||||
putInt(ContentResolver.QUERY_ARG_LIMIT, 1)
|
||||
}
|
||||
return ctx.contentResolver.query(filesUri, arrayOf(MediaStore.Files.FileColumns._ID), args, null)
|
||||
?.use { it.moveToFirst() } == true
|
||||
}
|
||||
|
||||
private fun contentUriForType(type: Int): Uri =
|
||||
when (type) {
|
||||
// Same order as AssetType from Dart.
|
||||
1 -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||
2 -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
||||
3 -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
|
||||
else -> MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
|
||||
}
|
||||
|
||||
fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||
activityBinding = binding
|
||||
binding.addActivityResultListener(this)
|
||||
}
|
||||
|
||||
fun onDetachedFromActivity() {
|
||||
failPending("ACTIVITY_DETACHED", "Activity detached before trash result")
|
||||
activityBinding?.removeActivityResultListener(this)
|
||||
activityBinding = null
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
||||
if (requestCode == trashRequestCode) {
|
||||
val callback = pendingResult
|
||||
pendingResult = null
|
||||
callback?.invoke(Result.success(resultCode == Activity.RESULT_OK))
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun failPending(code: String, message: String) {
|
||||
val callback = pendingResult ?: return
|
||||
pendingResult = null
|
||||
callback(Result.failure(FlutterError(code, message, null)))
|
||||
}
|
||||
}
|
||||
@@ -553,8 +553,6 @@ interface NativeSyncApi {
|
||||
fun hashAssets(assetIds: List<String>, allowNetworkAccess: Boolean, callback: (Result<List<HashResult>>) -> Unit)
|
||||
fun cancelHashing()
|
||||
fun cancelSync()
|
||||
fun getTrashedAssets(): Map<String, List<PlatformAsset>>
|
||||
fun restoreFromTrashById(mediaId: String, type: Long, callback: (Result<Boolean>) -> Unit)
|
||||
fun getCloudIdForAssetIds(assetIds: List<String>): List<CloudIdResult>
|
||||
|
||||
companion object {
|
||||
@@ -765,42 +763,6 @@ interface NativeSyncApi {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets$separatedMessageChannelSuffix", codec, taskQueue)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { _, reply ->
|
||||
val wrapped: List<Any?> = try {
|
||||
listOf(api.getTrashedAssets())
|
||||
} catch (exception: Throwable) {
|
||||
MessagesPigeonUtils.wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.restoreFromTrashById$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val mediaIdArg = args[0] as String
|
||||
val typeArg = args[1] as Long
|
||||
api.restoreFromTrashById(mediaIdArg, typeArg) { result: Result<Boolean> ->
|
||||
val error = result.exceptionOrNull()
|
||||
if (error != null) {
|
||||
reply.reply(MessagesPigeonUtils.wrapError(error))
|
||||
} else {
|
||||
val data = result.getOrNull()
|
||||
reply.reply(MessagesPigeonUtils.wrapResult(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds$separatedMessageChannelSuffix", codec, taskQueue)
|
||||
if (api != null) {
|
||||
|
||||
@@ -29,9 +29,4 @@ class NativeSyncApiImpl26(context: Context) : NativeSyncApiImplBase(context), Na
|
||||
private fun getMediaChanges(): SyncDelta {
|
||||
throw IllegalStateException("Method not supported on this Android version.")
|
||||
}
|
||||
|
||||
override fun getTrashedAssets(): Map<String, List<PlatformAsset>> {
|
||||
//Method not supported on this Android version.
|
||||
return emptyMap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,29 +99,4 @@ class NativeSyncApiImpl30(context: Context) : NativeSyncApiImplBase(context), Na
|
||||
// Unmounted volumes are handled in dart when the album is removed
|
||||
return SyncDelta(hasChanges, changed, deleted, assetAlbums)
|
||||
}
|
||||
|
||||
override fun getTrashedAssets(): Map<String, List<PlatformAsset>> {
|
||||
|
||||
val result = LinkedHashMap<String, MutableList<PlatformAsset>>()
|
||||
val volumes = MediaStore.getExternalVolumeNames(ctx)
|
||||
|
||||
for (volume in volumes) {
|
||||
|
||||
val queryArgs = Bundle().apply {
|
||||
putString(ContentResolver.QUERY_ARG_SQL_SELECTION, MEDIA_SELECTION)
|
||||
putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, MEDIA_SELECTION_ARGS)
|
||||
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY)
|
||||
}
|
||||
|
||||
getCursor(volume, queryArgs).use { cursor ->
|
||||
getAssets(cursor).forEach { res ->
|
||||
if (res is AssetResult.ValidAsset) {
|
||||
result.getOrPut(res.albumId) { mutableListOf() }.add(res.asset)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.mapValues { it.value.toList() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.annotation.SuppressLint
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.ext.SdkExtensions
|
||||
@@ -17,8 +16,6 @@ import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.ImageHeaderParser
|
||||
import com.bumptech.glide.load.ImageHeaderParserUtils
|
||||
import com.bumptech.glide.load.resource.bitmap.DefaultImageHeaderParser
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -41,12 +38,11 @@ sealed class AssetResult {
|
||||
private const val TAG = "NativeSyncApiImplBase"
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAware {
|
||||
open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
|
||||
private val ctx: Context = context.applicationContext
|
||||
|
||||
private var hashTask: Job? = null
|
||||
private var syncJob: Job? = null
|
||||
private val mediaTrashDelegate = MediaTrashDelegate(ctx)
|
||||
|
||||
companion object {
|
||||
private const val MAX_CONCURRENT_HASH_OPERATIONS = 16
|
||||
@@ -377,7 +373,11 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa
|
||||
)?.use { cursor -> cursor.count.toLong() } ?: 0L
|
||||
|
||||
|
||||
fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?, callback: (Result<List<PlatformAsset>>) -> Unit) {
|
||||
fun getAssetsForAlbum(
|
||||
albumId: String,
|
||||
updatedTimeCond: Long?,
|
||||
callback: (Result<List<PlatformAsset>>) -> Unit
|
||||
) {
|
||||
runSync(callback) { getAssetsForAlbum(albumId, updatedTimeCond) }
|
||||
}
|
||||
|
||||
@@ -477,33 +477,16 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa
|
||||
try {
|
||||
completeWhenActive(callback, Result.success(work()))
|
||||
} catch (e: CancellationException) {
|
||||
completeWhenActive(callback, Result.failure(FlutterError(SYNC_CANCELLED_CODE, "Sync cancelled", null)))
|
||||
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) {
|
||||
mediaTrashDelegate.restoreFromTrashById(mediaId, type) { completeWhenActive(callback, it) }
|
||||
}
|
||||
|
||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||
mediaTrashDelegate.onAttachedToActivity(binding)
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivityForConfigChanges() {
|
||||
mediaTrashDelegate.onDetachedFromActivity()
|
||||
}
|
||||
|
||||
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||
mediaTrashDelegate.onAttachedToActivity(binding)
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivity() {
|
||||
mediaTrashDelegate.onDetachedFromActivity()
|
||||
}
|
||||
|
||||
// This method is only implemented on iOS; on Android, we do not have a concept of cloud IDs
|
||||
@Suppress("unused", "UNUSED_PARAMETER")
|
||||
fun getCloudIdForAssetIds(assetIds: List<String>): List<CloudIdResult> {
|
||||
|
||||
@@ -22,8 +22,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 3055,
|
||||
"android.injected.version.name" => "3.0.2",
|
||||
"android.injected.version.code" => 3056,
|
||||
"android.injected.version.name" => "3.0.3",
|
||||
}
|
||||
)
|
||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab', track: 'beta')
|
||||
@@ -35,8 +35,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 3055,
|
||||
"android.injected.version.name" => "3.0.2",
|
||||
"android.injected.version.code" => 3056,
|
||||
"android.injected.version.name" => "3.0.3",
|
||||
}
|
||||
)
|
||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||
|
||||
3550
mobile/drift_schemas/main/drift_schema_v32.json
generated
Normal file
3550
mobile/drift_schemas/main/drift_schema_v32.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -46,7 +46,7 @@ void main() {
|
||||
|
||||
void sendUser(SyncStream stream, String id, String name) {
|
||||
stream.send(
|
||||
type: SyncEntityType.userV1.value,
|
||||
type: SyncEntityType.userV1.toString(),
|
||||
data: SyncUserV1(
|
||||
id: id,
|
||||
name: name,
|
||||
|
||||
3
mobile/ios/.gitignore
vendored
3
mobile/ios/.gitignore
vendored
@@ -26,6 +26,9 @@ Flutter/flutter_export_environment.sh
|
||||
ServiceDefinitions.json
|
||||
Runner/GeneratedPluginRegistrant.*
|
||||
|
||||
# Local signing overrides.
|
||||
Signing.local.xcconfig
|
||||
|
||||
# Exceptions to above rules.
|
||||
!default.mode1v3
|
||||
!default.mode2v3
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
platform :ios, '14.0'
|
||||
platform :ios, '15.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
@@ -45,7 +45,7 @@ post_install do |installer|
|
||||
installer.generated_projects.each do |project|
|
||||
project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0'
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,12 +12,8 @@ PODS:
|
||||
- Flutter
|
||||
- fluttertoast (0.0.2):
|
||||
- Flutter
|
||||
- home_widget (0.0.1):
|
||||
- Flutter
|
||||
- native_video_player (1.0.0):
|
||||
- Flutter
|
||||
- permission_handler_apple (9.3.0):
|
||||
- Flutter
|
||||
- share_handler_ios (0.0.14):
|
||||
- Flutter
|
||||
- share_handler_ios/share_handler_ios_models (= 0.0.14)
|
||||
@@ -34,9 +30,7 @@ DEPENDENCIES:
|
||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
||||
- home_widget (from `.symlinks/plugins/home_widget/ios`)
|
||||
- native_video_player (from `.symlinks/plugins/native_video_player/ios`)
|
||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||
- share_handler_ios (from `.symlinks/plugins/share_handler_ios/ios`)
|
||||
- share_handler_ios_models (from `.symlinks/plugins/share_handler_ios/ios/Models`)
|
||||
|
||||
@@ -53,12 +47,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||
fluttertoast:
|
||||
:path: ".symlinks/plugins/fluttertoast/ios"
|
||||
home_widget:
|
||||
:path: ".symlinks/plugins/home_widget/ios"
|
||||
native_video_player:
|
||||
:path: ".symlinks/plugins/native_video_player/ios"
|
||||
permission_handler_apple:
|
||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||
share_handler_ios:
|
||||
:path: ".symlinks/plugins/share_handler_ios/ios"
|
||||
share_handler_ios_models:
|
||||
@@ -71,12 +61,10 @@ SPEC CHECKSUMS:
|
||||
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
|
||||
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
|
||||
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
|
||||
native_video_player: b65c58951ede2f93d103a25366bdebca95081265
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb
|
||||
share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871
|
||||
|
||||
PODFILE CHECKSUM: 938abbae4114b9c2140c550a2a0d8f7c674f5dfe
|
||||
PODFILE CHECKSUM: 3c43a700a4bffb4120bf696cad263aefd4bb3c8c
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -96,6 +96,7 @@
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
51516E0F2A0000000000C0F1 /* Signing.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Signing.xcconfig; sourceTree = "<group>"; };
|
||||
8AB817AA297EDEC88B23F3F6 /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.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>"; };
|
||||
@@ -153,14 +154,23 @@
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
B231F52D2E93A44A00BC45D1 /* Core */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
path = Core;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
path = Sync;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B2D5F7B13005FCA00080CE3E /* AssetMedia */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
path = AssetMedia;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F0B57D3D2DF764BD00DC5BCC /* WidgetExtension */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
@@ -179,6 +189,8 @@
|
||||
};
|
||||
FEE084F22EC172080045228E /* Schemas */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
path = Schemas;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -256,6 +268,7 @@
|
||||
97C146E51CF9000F007C117D = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
51516E0F2A0000000000C0F1 /* Signing.xcconfig */,
|
||||
9740EEB11CF90186004384FC /* Flutter */,
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
FAC6F8B62D287F120078CB2F /* ShareExtension */,
|
||||
@@ -279,6 +292,7 @@
|
||||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B2D5F7B13005FCA00080CE3E /* AssetMedia */,
|
||||
FE1BB4562F8319560087DBF9 /* Utility */,
|
||||
FEE084F22EC172080045228E /* Schemas */,
|
||||
B231F52D2E93A44A00BC45D1 /* Core */,
|
||||
@@ -370,7 +384,6 @@
|
||||
FAC6F89A2D287C890078CB2F /* Embed Foundation Extensions */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
513DA7292DED6106813332F4 /* [CP] Embed Pods Frameworks */,
|
||||
2FA39DEC809D6D7C4A01EFCB /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -381,6 +394,7 @@
|
||||
fileSystemSynchronizedGroups = (
|
||||
B231F52D2E93A44A00BC45D1 /* Core */,
|
||||
B2CF7F8C2DDE4EBB00744BF6 /* Sync */,
|
||||
B2D5F7B13005FCA00080CE3E /* AssetMedia */,
|
||||
FEE084F22EC172080045228E /* Schemas */,
|
||||
);
|
||||
name = Runner;
|
||||
@@ -464,7 +478,7 @@
|
||||
);
|
||||
mainGroup = 97C146E51CF9000F007C117D;
|
||||
packageReferences = (
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
|
||||
FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */,
|
||||
FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */,
|
||||
);
|
||||
@@ -511,27 +525,6 @@
|
||||
/* End PBXResourcesBuildPhase 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",
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
@@ -556,14 +549,10 @@
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
@@ -604,7 +593,7 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n";
|
||||
shellScript = "/bin/bash \"$SRCROOT/scripts/xcode_flutter_build.sh\"\n";
|
||||
};
|
||||
BAEA01ACA3F5C9CD3D732370 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
@@ -714,6 +703,7 @@
|
||||
/* Begin XCBuildConfiguration section */
|
||||
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 51516E0F2A0000000000C0F1 /* Signing.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
@@ -744,7 +734,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share.profile;
|
||||
CUSTOM_GROUP_ID = "$(IMMICH_GROUP_ID).profile";
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -757,7 +747,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
NEW_SETTING = "";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -777,16 +767,16 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
DEVELOPMENT_TEAM = 2W7AC6T8T5;
|
||||
DEVELOPMENT_TEAM = "$(IMMICH_TEAM_ID)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.futo.immich.profile;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(IMMICH_BUNDLE_ID_DEV).profile";
|
||||
PRODUCT_NAME = "Immich-Profile";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
@@ -797,6 +787,7 @@
|
||||
};
|
||||
97C147031CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 51516E0F2A0000000000C0F1 /* Signing.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||
@@ -827,7 +818,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share.debug;
|
||||
CUSTOM_GROUP_ID = "$(IMMICH_GROUP_ID).debug";
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
@@ -846,7 +837,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
NEW_SETTING = "";
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
@@ -857,6 +848,7 @@
|
||||
};
|
||||
97C147041CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 51516E0F2A0000000000C0F1 /* Signing.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||
@@ -887,7 +879,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
CUSTOM_GROUP_ID = "$(IMMICH_GROUP_ID)";
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -900,7 +892,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
NEW_SETTING = "";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -922,16 +914,16 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
DEVELOPMENT_TEAM = 2W7AC6T8T5;
|
||||
DEVELOPMENT_TEAM = "$(IMMICH_TEAM_ID)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.futo.immich.debug;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(IMMICH_BUNDLE_ID_DEV).debug";
|
||||
PRODUCT_NAME = "Immich-Debug";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
@@ -951,16 +943,16 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
DEVELOPMENT_TEAM = 2W7AC6T8T5;
|
||||
DEVELOPMENT_TEAM = "$(IMMICH_TEAM_ID)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(IMMICH_BUNDLE_ID_PROD)";
|
||||
PRODUCT_NAME = Immich;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
@@ -984,7 +976,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 240;
|
||||
DEVELOPMENT_TEAM = 2W7AC6T8T5;
|
||||
DEVELOPMENT_TEAM = "$(IMMICH_TEAM_ID)";
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -1001,7 +993,7 @@
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.futo.immich.debug.Widget;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(IMMICH_BUNDLE_ID_DEV).debug.Widget";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
@@ -1027,7 +1019,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 240;
|
||||
DEVELOPMENT_TEAM = 2W7AC6T8T5;
|
||||
DEVELOPMENT_TEAM = "$(IMMICH_TEAM_ID)";
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -1043,7 +1035,7 @@
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.Widget;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(IMMICH_BUNDLE_ID_PROD).Widget";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@@ -1067,7 +1059,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 240;
|
||||
DEVELOPMENT_TEAM = 2W7AC6T8T5;
|
||||
DEVELOPMENT_TEAM = "$(IMMICH_TEAM_ID)";
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -1083,7 +1075,7 @@
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.futo.immich.profile.Widget;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(IMMICH_BUNDLE_ID_DEV).profile.Widget";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@@ -1106,7 +1098,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 240;
|
||||
DEVELOPMENT_TEAM = 2W7AC6T8T5;
|
||||
DEVELOPMENT_TEAM = "$(IMMICH_TEAM_ID)";
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -1123,7 +1115,7 @@
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.futo.immich.debug.ShareExtension;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(IMMICH_BUNDLE_ID_DEV).debug.ShareExtension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SKIP_INSTALL = YES;
|
||||
@@ -1149,7 +1141,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 240;
|
||||
DEVELOPMENT_TEAM = 2W7AC6T8T5;
|
||||
DEVELOPMENT_TEAM = "$(IMMICH_TEAM_ID)";
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -1165,7 +1157,7 @@
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.ShareExtension;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(IMMICH_BUNDLE_ID_PROD).ShareExtension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SKIP_INSTALL = YES;
|
||||
@@ -1189,7 +1181,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 240;
|
||||
DEVELOPMENT_TEAM = 2W7AC6T8T5;
|
||||
DEVELOPMENT_TEAM = "$(IMMICH_TEAM_ID)";
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -1205,7 +1197,7 @@
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.futo.immich.profile.ShareExtension;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(IMMICH_BUNDLE_ID_DEV).profile.ShareExtension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SKIP_INSTALL = YES;
|
||||
@@ -1261,7 +1253,7 @@
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCLocalSwiftPackageReference section */
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
|
||||
};
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/maplibre/maplibre-gl-native-distribution.git",
|
||||
"state" : {
|
||||
"revision" : "60d9bb85c94ce6e7fc4406cd32529fd12bdb7809",
|
||||
"version" : "6.14.0"
|
||||
"revision" : "84a79bc375a301169390ac110c868f06c857b83f",
|
||||
"version" : "6.27.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -122,8 +122,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-sharing",
|
||||
"state" : {
|
||||
"revision" : "3bfc408cc2d0bee2287c174da6b1c76768377818",
|
||||
"version" : "2.7.4"
|
||||
"revision" : "8244fe63bf43e58188ab13851ad693eecf6a9e90",
|
||||
"version" : "2.9.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -26,6 +26,7 @@ import native_video_player
|
||||
|
||||
public static func registerPlugins(with registry: FlutterPluginRegistry, messenger: FlutterBinaryMessenger) {
|
||||
NativeSyncApiImpl.register(with: registry.registrar(forPlugin: NativeSyncApiImpl.name)!)
|
||||
AssetMediaApiImpl.register(with: registry.registrar(forPlugin: AssetMediaApiImpl.name)!)
|
||||
PermissionApiSetup.setUp(binaryMessenger: messenger, api: PermissionApiImpl())
|
||||
LocalImageApiSetup.setUp(binaryMessenger: messenger, api: LocalImageApiImpl())
|
||||
RemoteImageApiSetup.setUp(binaryMessenger: messenger, api: RemoteImageApiImpl())
|
||||
@@ -36,5 +37,6 @@ import native_video_player
|
||||
|
||||
public static func cancelPlugins(with engine: FlutterEngine) {
|
||||
(engine.valuePublished(byPlugin: NativeSyncApiImpl.name) as? NativeSyncApiImpl)?.detachFromEngine()
|
||||
(engine.valuePublished(byPlugin: AssetMediaApiImpl.name) as? AssetMediaApiImpl)?.detachFromEngine()
|
||||
}
|
||||
}
|
||||
|
||||
313
mobile/ios/Runner/AssetMedia/AssetMedia.g.swift
generated
Normal file
313
mobile/ios/Runner/AssetMedia/AssetMedia.g.swift
generated
Normal file
@@ -0,0 +1,313 @@
|
||||
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
|
||||
import Foundation
|
||||
|
||||
#if os(iOS)
|
||||
import Flutter
|
||||
#elseif os(macOS)
|
||||
import FlutterMacOS
|
||||
#else
|
||||
#error("Unsupported platform.")
|
||||
#endif
|
||||
|
||||
private func wrapResult(_ result: Any?) -> [Any?] {
|
||||
return [result]
|
||||
}
|
||||
|
||||
private func wrapError(_ error: Any) -> [Any?] {
|
||||
if let pigeonError = error as? PigeonError {
|
||||
return [
|
||||
pigeonError.code,
|
||||
pigeonError.message,
|
||||
pigeonError.details,
|
||||
]
|
||||
}
|
||||
if let flutterError = error as? FlutterError {
|
||||
return [
|
||||
flutterError.code,
|
||||
flutterError.message,
|
||||
flutterError.details,
|
||||
]
|
||||
}
|
||||
return [
|
||||
"\(error)",
|
||||
"\(Swift.type(of: error))",
|
||||
"Stacktrace: \(Thread.callStackSymbols)",
|
||||
]
|
||||
}
|
||||
|
||||
private func isNullish(_ value: Any?) -> Bool {
|
||||
return value is NSNull || value == nil
|
||||
}
|
||||
|
||||
private func nilOrValue<T>(_ value: Any?) -> T? {
|
||||
if value is NSNull { return nil }
|
||||
return value as! T?
|
||||
}
|
||||
|
||||
private func doubleEqualsAssetMedia(_ lhs: Double, _ rhs: Double) -> Bool {
|
||||
return (lhs.isNaN && rhs.isNaN) || lhs == rhs
|
||||
}
|
||||
|
||||
private func doubleHashAssetMedia(_ value: Double, _ hasher: inout Hasher) {
|
||||
if value.isNaN {
|
||||
hasher.combine(0x7FF8000000000000)
|
||||
} else {
|
||||
// Normalize -0.0 to 0.0
|
||||
hasher.combine(value == 0 ? 0 : value)
|
||||
}
|
||||
}
|
||||
|
||||
func deepEqualsAssetMedia(_ lhs: Any?, _ rhs: Any?) -> Bool {
|
||||
let cleanLhs = nilOrValue(lhs) as Any?
|
||||
let cleanRhs = nilOrValue(rhs) as Any?
|
||||
switch (cleanLhs, cleanRhs) {
|
||||
case (nil, nil):
|
||||
return true
|
||||
|
||||
case (nil, _), (_, nil):
|
||||
return false
|
||||
|
||||
case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs:
|
||||
return true
|
||||
|
||||
case is (Void, Void):
|
||||
return true
|
||||
|
||||
case (let lhsArray, let rhsArray) as ([Any?], [Any?]):
|
||||
guard lhsArray.count == rhsArray.count else { return false }
|
||||
for (index, element) in lhsArray.enumerated() {
|
||||
if !deepEqualsAssetMedia(element, rhsArray[index]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
case (let lhsArray, let rhsArray) as ([Double], [Double]):
|
||||
guard lhsArray.count == rhsArray.count else { return false }
|
||||
for (index, element) in lhsArray.enumerated() {
|
||||
if !doubleEqualsAssetMedia(element, rhsArray[index]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]):
|
||||
guard lhsDictionary.count == rhsDictionary.count else { return false }
|
||||
for (lhsKey, lhsValue) in lhsDictionary {
|
||||
var found = false
|
||||
for (rhsKey, rhsValue) in rhsDictionary {
|
||||
if deepEqualsAssetMedia(lhsKey, rhsKey) {
|
||||
if deepEqualsAssetMedia(lhsValue, rhsValue) {
|
||||
found = true
|
||||
break
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found { return false }
|
||||
}
|
||||
return true
|
||||
|
||||
case (let lhs as Double, let rhs as Double):
|
||||
return doubleEqualsAssetMedia(lhs, rhs)
|
||||
|
||||
case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable):
|
||||
return lhsHashable == rhsHashable
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func deepHashAssetMedia(value: Any?, hasher: inout Hasher) {
|
||||
let cleanValue = nilOrValue(value) as Any?
|
||||
if let cleanValue = cleanValue {
|
||||
if let doubleValue = cleanValue as? Double {
|
||||
doubleHashAssetMedia(doubleValue, &hasher)
|
||||
} else if let valueList = cleanValue as? [Any?] {
|
||||
for item in valueList {
|
||||
deepHashAssetMedia(value: item, hasher: &hasher)
|
||||
}
|
||||
} else if let valueList = cleanValue as? [Double] {
|
||||
for item in valueList {
|
||||
doubleHashAssetMedia(item, &hasher)
|
||||
}
|
||||
} else if let valueDict = cleanValue as? [AnyHashable: Any?] {
|
||||
var result = 0
|
||||
for (key, value) in valueDict {
|
||||
var entryKeyHasher = Hasher()
|
||||
deepHashAssetMedia(value: key, hasher: &entryKeyHasher)
|
||||
var entryValueHasher = Hasher()
|
||||
deepHashAssetMedia(value: value, hasher: &entryValueHasher)
|
||||
result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize())
|
||||
}
|
||||
hasher.combine(result)
|
||||
} else if let hashableValue = cleanValue as? AnyHashable {
|
||||
hasher.combine(hashableValue)
|
||||
} else {
|
||||
hasher.combine(String(describing: cleanValue))
|
||||
}
|
||||
} else {
|
||||
hasher.combine(0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum AssetMediaActionStatus: Int {
|
||||
case done = 0
|
||||
case alreadyInState = 1
|
||||
case notFound = 2
|
||||
case failed = 3
|
||||
}
|
||||
|
||||
/// Generated class from Pigeon that represents data sent in messages.
|
||||
struct AssetMediaActionResult: Hashable {
|
||||
var id: String
|
||||
var status: AssetMediaActionStatus
|
||||
|
||||
|
||||
// swift-format-ignore: AlwaysUseLowerCamelCase
|
||||
static func fromList(_ pigeonVar_list: [Any?]) -> AssetMediaActionResult? {
|
||||
let id = pigeonVar_list[0] as! String
|
||||
let status = pigeonVar_list[1] as! AssetMediaActionStatus
|
||||
|
||||
return AssetMediaActionResult(
|
||||
id: id,
|
||||
status: status
|
||||
)
|
||||
}
|
||||
func toList() -> [Any?] {
|
||||
return [
|
||||
id,
|
||||
status,
|
||||
]
|
||||
}
|
||||
static func == (lhs: AssetMediaActionResult, rhs: AssetMediaActionResult) -> Bool {
|
||||
if Swift.type(of: lhs) != Swift.type(of: rhs) {
|
||||
return false
|
||||
}
|
||||
return deepEqualsAssetMedia(lhs.id, rhs.id) && deepEqualsAssetMedia(lhs.status, rhs.status)
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine("AssetMediaActionResult")
|
||||
deepHashAssetMedia(value: id, hasher: &hasher)
|
||||
deepHashAssetMedia(value: status, hasher: &hasher)
|
||||
}
|
||||
}
|
||||
|
||||
private class AssetMediaPigeonCodecReader: 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 AssetMediaActionStatus(rawValue: enumResultAsInt)
|
||||
}
|
||||
return nil
|
||||
case 130:
|
||||
return AssetMediaActionResult.fromList(self.readValue() as! [Any?])
|
||||
default:
|
||||
return super.readValue(ofType: type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class AssetMediaPigeonCodecWriter: FlutterStandardWriter {
|
||||
override func writeValue(_ value: Any) {
|
||||
if let value = value as? AssetMediaActionStatus {
|
||||
super.writeByte(129)
|
||||
super.writeValue(value.rawValue)
|
||||
} else if let value = value as? AssetMediaActionResult {
|
||||
super.writeByte(130)
|
||||
super.writeValue(value.toList())
|
||||
} else {
|
||||
super.writeValue(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class AssetMediaPigeonCodecReaderWriter: FlutterStandardReaderWriter {
|
||||
override func reader(with data: Data) -> FlutterStandardReader {
|
||||
return AssetMediaPigeonCodecReader(data: data)
|
||||
}
|
||||
|
||||
override func writer(with data: NSMutableData) -> FlutterStandardWriter {
|
||||
return AssetMediaPigeonCodecWriter(data: data)
|
||||
}
|
||||
}
|
||||
|
||||
class AssetMediaPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
|
||||
static let shared = AssetMediaPigeonCodec(readerWriter: AssetMediaPigeonCodecReaderWriter())
|
||||
}
|
||||
|
||||
|
||||
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
||||
protocol AssetMediaApi {
|
||||
func trash(ids: [String], completion: @escaping (Result<[AssetMediaActionResult], Error>) -> Void)
|
||||
func restore(ids: [String], completion: @escaping (Result<[AssetMediaActionResult], Error>) -> Void)
|
||||
func trashedAmong(ids: [String], completion: @escaping (Result<[String], Error>) -> Void)
|
||||
}
|
||||
|
||||
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||
class AssetMediaApiSetup {
|
||||
static var codec: FlutterStandardMessageCodec { AssetMediaPigeonCodec.shared }
|
||||
/// Sets up an instance of `AssetMediaApi` to handle messages through the `binaryMessenger`.
|
||||
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: AssetMediaApi?, messageChannelSuffix: String = "") {
|
||||
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
|
||||
let trashChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.AssetMediaApi.trash\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
trashChannel.setMessageHandler { message, reply in
|
||||
let args = message as! [Any?]
|
||||
let idsArg = args[0] as! [String]
|
||||
api.trash(ids: idsArg) { result in
|
||||
switch result {
|
||||
case .success(let res):
|
||||
reply(wrapResult(res))
|
||||
case .failure(let error):
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
trashChannel.setMessageHandler(nil)
|
||||
}
|
||||
let restoreChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.AssetMediaApi.restore\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
restoreChannel.setMessageHandler { message, reply in
|
||||
let args = message as! [Any?]
|
||||
let idsArg = args[0] as! [String]
|
||||
api.restore(ids: idsArg) { result in
|
||||
switch result {
|
||||
case .success(let res):
|
||||
reply(wrapResult(res))
|
||||
case .failure(let error):
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
restoreChannel.setMessageHandler(nil)
|
||||
}
|
||||
let trashedAmongChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.AssetMediaApi.trashedAmong\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
trashedAmongChannel.setMessageHandler { message, reply in
|
||||
let args = message as! [Any?]
|
||||
let idsArg = args[0] as! [String]
|
||||
api.trashedAmong(ids: idsArg) { result in
|
||||
switch result {
|
||||
case .success(let res):
|
||||
reply(wrapResult(res))
|
||||
case .failure(let error):
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
trashedAmongChannel.setMessageHandler(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
26
mobile/ios/Runner/AssetMedia/AssetMediaApiImpl.swift
Normal file
26
mobile/ios/Runner/AssetMedia/AssetMediaApiImpl.swift
Normal file
@@ -0,0 +1,26 @@
|
||||
class AssetMediaApiImpl: ImmichPlugin, AssetMediaApi, FlutterPlugin {
|
||||
static let name = "AssetMediaApi"
|
||||
|
||||
static func register(with registrar: FlutterPluginRegistrar) {
|
||||
let instance = AssetMediaApiImpl()
|
||||
AssetMediaApiSetup.setUp(binaryMessenger: registrar.messenger(), api: instance)
|
||||
registrar.publish(instance)
|
||||
}
|
||||
|
||||
func detachFromEngine(for registrar: FlutterPluginRegistrar) {
|
||||
AssetMediaApiSetup.setUp(binaryMessenger: registrar.messenger(), api: nil)
|
||||
super.detachFromEngine()
|
||||
}
|
||||
|
||||
func trash(ids: [String], completion: @escaping (Result<[AssetMediaActionResult], Error>) -> Void) {
|
||||
completeWhenActive(for: completion, with: .success(ids.map { AssetMediaActionResult(id: $0, status: .failed) }))
|
||||
}
|
||||
|
||||
func restore(ids: [String], completion: @escaping (Result<[AssetMediaActionResult], Error>) -> Void) {
|
||||
completeWhenActive(for: completion, with: .success(ids.map { AssetMediaActionResult(id: $0, status: .failed) }))
|
||||
}
|
||||
|
||||
func trashedAmong(ids: [String], completion: @escaping (Result<[String], Error>) -> Void) {
|
||||
completeWhenActive(for: completion, with: .success([]))
|
||||
}
|
||||
}
|
||||
@@ -43,15 +43,16 @@ class ConnectivityApiImpl: ConnectivityApi {
|
||||
capabilities.append(.vpn)
|
||||
}
|
||||
|
||||
// Determine if connection is unmetered:
|
||||
// - Must be on WiFi (not cellular)
|
||||
// - Must not be expensive (rules out personal hotspot)
|
||||
// - Must not be constrained (Low Data Mode)
|
||||
// Note: VPN over cellular should still be considered metered
|
||||
// Determine if connection is unmetered from the OS metered flags rather than
|
||||
// the interface type, so wired ethernet (iPhone USB adapters, Apple Silicon
|
||||
// Macs) is treated as unmetered like Wi-Fi:
|
||||
// - Not on cellular
|
||||
// - Not expensive (also rules out cellular and personal hotspot)
|
||||
// - Not constrained (Low Data Mode)
|
||||
// Note: VPN over cellular stays metered because the path is still expensive.
|
||||
let isOnCellular = path.usesInterfaceType(.cellular)
|
||||
let isOnWifi = path.usesInterfaceType(.wifi)
|
||||
|
||||
if isOnWifi && !isOnCellular && !path.isExpensive && !path.isConstrained {
|
||||
|
||||
if !isOnCellular && !path.isExpensive && !path.isConstrained {
|
||||
capabilities.append(.unmetered)
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>3.0.2</string>
|
||||
<string>3.0.3</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
35
mobile/ios/Runner/Sync/Messages.g.swift
generated
35
mobile/ios/Runner/Sync/Messages.g.swift
generated
@@ -537,8 +537,6 @@ protocol NativeSyncApi {
|
||||
func hashAssets(assetIds: [String], allowNetworkAccess: Bool, completion: @escaping (Result<[HashResult], Error>) -> Void)
|
||||
func cancelHashing() throws
|
||||
func cancelSync() throws
|
||||
func getTrashedAssets() throws -> [String: [PlatformAsset]]
|
||||
func restoreFromTrashById(mediaId: String, type: Int64, completion: @escaping (Result<Bool, Error>) -> Void)
|
||||
func getCloudIdForAssetIds(assetIds: [String]) throws -> [CloudIdResult]
|
||||
}
|
||||
|
||||
@@ -723,39 +721,6 @@ class NativeSyncApiSetup {
|
||||
} else {
|
||||
cancelSyncChannel.setMessageHandler(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, taskQueue: taskQueue)
|
||||
if let api = api {
|
||||
getTrashedAssetsChannel.setMessageHandler { _, reply in
|
||||
do {
|
||||
let result = try api.getTrashedAssets()
|
||||
reply(wrapResult(result))
|
||||
} catch {
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
getTrashedAssetsChannel.setMessageHandler(nil)
|
||||
}
|
||||
let restoreFromTrashByIdChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.restoreFromTrashById\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
restoreFromTrashByIdChannel.setMessageHandler { message, reply in
|
||||
let args = message as! [Any?]
|
||||
let mediaIdArg = args[0] as! String
|
||||
let typeArg = args[1] as! Int64
|
||||
api.restoreFromTrashById(mediaId: mediaIdArg, type: typeArg) { result in
|
||||
switch result {
|
||||
case .success(let res):
|
||||
reply(wrapResult(res))
|
||||
case .failure(let error):
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
restoreFromTrashByIdChannel.setMessageHandler(nil)
|
||||
}
|
||||
let getCloudIdForAssetIdsChannel = taskQueue == nil
|
||||
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
|
||||
|
||||
@@ -436,14 +436,6 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
||||
})
|
||||
}
|
||||
|
||||
func getTrashedAssets() throws -> [String: [PlatformAsset]] {
|
||||
throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature not supported on iOS.", details: nil)
|
||||
}
|
||||
|
||||
func restoreFromTrashById(mediaId: String, type: Int64, completion: @escaping (Result<Bool, Error>) -> Void) {
|
||||
completion(.success(false))
|
||||
}
|
||||
|
||||
private func getAssetsFromAlbum(in album: PHAssetCollection, options: PHFetchOptions) -> PHFetchResult<PHAsset> {
|
||||
// Ensure to actually getting all assets for the Recents album
|
||||
if (album.assetCollectionSubtype == .smartAlbumUserLibrary) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user