mirror of
https://github.com/immich-app/immich.git
synced 2026-07-23 13:54:05 +03:00
Compare commits
67 Commits
fix/29853-
...
fix/auto-h
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5302d6ea0e | ||
|
|
564cda5088 | ||
|
|
a7f1d495c6 | ||
|
|
b9f6c4aaf2 | ||
|
|
23778551f7 | ||
|
|
457da17c5d | ||
|
|
12f741d911 | ||
|
|
a0c7b81142 | ||
|
|
20997823d8 | ||
|
|
8a875f5978 | ||
|
|
8435d219e4 | ||
|
|
8bbc252703 | ||
|
|
2d3cf8ffaa | ||
|
|
33cba2f9af | ||
|
|
75053ebfba | ||
|
|
ba9ec7c80e | ||
|
|
99370b2ac4 | ||
|
|
0f5bf6071f | ||
|
|
998fd5f6a8 | ||
|
|
f413c446e3 | ||
|
|
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 |
@@ -83,7 +83,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
"ghcr.io/devcontainers/features/docker-in-docker:4": {
|
||||||
// https://github.com/devcontainers/features/issues/1466
|
// https://github.com/devcontainers/features/issues/1466
|
||||||
"moby": false
|
"moby": false
|
||||||
}
|
}
|
||||||
|
|||||||
2
.github/workflows/auto-close.yml
vendored
2
.github/workflows/auto-close.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
uses_template: ${{ steps.check.outputs.uses_template }}
|
uses_template: ${{ steps.check.outputs.uses_template }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
sparse-checkout: .github/pull_request_template.md
|
sparse-checkout: .github/pull_request_template.md
|
||||||
sparse-checkout-cone-mode: false
|
sparse-checkout-cone-mode: false
|
||||||
|
|||||||
14
.github/workflows/build-mobile.yml
vendored
14
.github/workflows/build-mobile.yml
vendored
@@ -87,7 +87,7 @@ jobs:
|
|||||||
permission-contents: read
|
permission-contents: read
|
||||||
permission-pull-requests: write
|
permission-pull-requests: write
|
||||||
|
|
||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.ref }}
|
ref: ${{ inputs.ref }}
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -106,14 +106,14 @@ jobs:
|
|||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
run: printf "%s" $KEY_JKS | base64 -d > android/key.jks
|
run: printf "%s" $KEY_JKS | base64 -d > android/key.jks
|
||||||
|
|
||||||
- uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
|
- uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
||||||
with:
|
with:
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
|
|
||||||
- name: Restore Gradle Cache
|
- name: Restore Gradle Cache
|
||||||
id: cache-gradle-restore
|
id: cache-gradle-restore
|
||||||
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
|
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.gradle/caches
|
~/.gradle/caches
|
||||||
@@ -184,7 +184,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Save Gradle Cache
|
- name: Save Gradle Cache
|
||||||
id: cache-gradle-save
|
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'
|
if: github.ref == 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
@@ -201,7 +201,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
# Run on main branch or workflow_dispatch, or on PRs/other branches (build only, no upload)
|
# 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 }}
|
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:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
@@ -215,7 +215,7 @@ jobs:
|
|||||||
run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer
|
run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.ref || github.sha }}
|
ref: ${{ inputs.ref || github.sha }}
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -241,7 +241,7 @@ jobs:
|
|||||||
run: flutter build ios --config-only --no-codesign
|
run: flutter build ios --config-only --no-codesign
|
||||||
|
|
||||||
- name: Setup Ruby
|
- name: Setup Ruby
|
||||||
uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0
|
uses: ruby/setup-ruby@003a5c4d8d6321bd302e38f6f0ec593f77f06600 # v1.319.0
|
||||||
with:
|
with:
|
||||||
ruby-version: '3.3'
|
ruby-version: '3.3'
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
|
|||||||
2
.github/workflows/cache-cleanup.yml
vendored
2
.github/workflows/cache-cleanup.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
permission-actions: write
|
permission-actions: write
|
||||||
|
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
token: ${{ steps.token.outputs.token }}
|
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
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Check for breaking API changes
|
- name: Check for breaking API changes
|
||||||
uses: oasdiff/oasdiff-action/breaking@291b6594ce617f22f49af5898ee038ed0b46e8fe # v0.1.5
|
uses: oasdiff/oasdiff-action/breaking@024f6c399f9a21ada1addb0f9a36ce1bfac995f1 # v0.1.6
|
||||||
with:
|
with:
|
||||||
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
|
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
|
||||||
revision: open-api/immich-openapi-specs.json
|
revision: open-api/immich-openapi-specs.json
|
||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/cli.yml
vendored
4
.github/workflows/cli.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
|||||||
permission-contents: read
|
permission-contents: read
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
@@ -71,7 +71,7 @@ jobs:
|
|||||||
permission-contents: read
|
permission-contents: read
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|||||||
2
.github/workflows/close-duplicates.yml
vendored
2
.github/workflows/close-duplicates.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
|||||||
needs: [get_body, should_run]
|
needs: [get_body, should_run]
|
||||||
if: ${{ needs.should_run.outputs.should_run == 'true' }}
|
if: ${{ needs.should_run.outputs.should_run == 'true' }}
|
||||||
container:
|
container:
|
||||||
image: ghcr.io/immich-app/mdq:main@sha256:e3fe456f346b02c2f479462100763aee3ba8245768dfeac3c4262acb068e5c62
|
image: ghcr.io/immich-app/mdq:main@sha256:d603f5618c1fc9f2b4d506ffed54ba6bcfec83d56b893b5fa8b3f899a402fa0a
|
||||||
outputs:
|
outputs:
|
||||||
checked: ${{ steps.get_checkbox.outputs.checked }}
|
checked: ${{ steps.get_checkbox.outputs.checked }}
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -51,14 +51,14 @@ jobs:
|
|||||||
permission-contents: read
|
permission-contents: read
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
|
uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -71,7 +71,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
|
uses: github/codeql-action/autobuild@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
@@ -84,6 +84,6 @@ jobs:
|
|||||||
# ./location_of_script_within_repo/buildscript.sh
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
|
uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
|
||||||
with:
|
with:
|
||||||
category: '/language:${{matrix.language}}'
|
category: '/language:${{matrix.language}}'
|
||||||
|
|||||||
2
.github/workflows/docs-build.yml
vendored
2
.github/workflows/docs-build.yml
vendored
@@ -62,7 +62,7 @@ jobs:
|
|||||||
permission-contents: read
|
permission-contents: read
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|||||||
2
.github/workflows/docs-deploy.yml
vendored
2
.github/workflows/docs-deploy.yml
vendored
@@ -137,7 +137,7 @@ jobs:
|
|||||||
permission-pull-requests: write
|
permission-pull-requests: write
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|||||||
2
.github/workflows/docs-destroy.yml
vendored
2
.github/workflows/docs-destroy.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
|||||||
permission-pull-requests: write
|
permission-pull-requests: write
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|||||||
2
.github/workflows/fdroid.yml
vendored
2
.github/workflows/fdroid.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout pubspec for versionCode
|
- name: Checkout pubspec for versionCode
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.release.tag_name }}
|
ref: ${{ github.event.release.tag_name }}
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|||||||
2
.github/workflows/fix-format.yml
vendored
2
.github/workflows/fix-format.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
permission-pull-requests: write
|
permission-pull-requests: write
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.ref }}
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
persist-credentials: true
|
persist-credentials: true
|
||||||
|
|||||||
6
.github/workflows/prepare-release.yml
vendored
6
.github/workflows/prepare-release.yml
vendored
@@ -61,7 +61,7 @@ jobs:
|
|||||||
permission-contents: write
|
permission-contents: write
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
persist-credentials: true
|
persist-credentials: true
|
||||||
@@ -140,7 +140,7 @@ jobs:
|
|||||||
permission-actions: read
|
permission-actions: read
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -152,7 +152,7 @@ jobs:
|
|||||||
github-token: ${{ steps.generate-token.outputs.token }}
|
github-token: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|
||||||
- name: Create draft release
|
- name: Create draft release
|
||||||
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
|
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
|
||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
prerelease: ${{ needs.bump_version.outputs.rc }}
|
prerelease: ${{ needs.bump_version.outputs.rc }}
|
||||||
|
|||||||
2
.github/workflows/sdk.yml
vendored
2
.github/workflows/sdk.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
permission-contents: read
|
permission-contents: read
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|||||||
2
.github/workflows/static_analysis.yml
vendored
2
.github/workflows/static_analysis.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
|||||||
permission-contents: read
|
permission-contents: read
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|||||||
38
.github/workflows/test.yml
vendored
38
.github/workflows/test.yml
vendored
@@ -85,7 +85,7 @@ jobs:
|
|||||||
permission-contents: read
|
permission-contents: read
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
@@ -114,7 +114,7 @@ jobs:
|
|||||||
permission-contents: read
|
permission-contents: read
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
@@ -146,7 +146,7 @@ jobs:
|
|||||||
permission-contents: read
|
permission-contents: read
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
@@ -178,7 +178,7 @@ jobs:
|
|||||||
permission-contents: read
|
permission-contents: read
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
@@ -223,7 +223,7 @@ jobs:
|
|||||||
permission-contents: read
|
permission-contents: read
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
@@ -262,7 +262,7 @@ jobs:
|
|||||||
permission-contents: read
|
permission-contents: read
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
@@ -291,7 +291,7 @@ jobs:
|
|||||||
permission-contents: read
|
permission-contents: read
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
@@ -342,7 +342,7 @@ jobs:
|
|||||||
permission-contents: read
|
permission-contents: read
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
@@ -375,7 +375,7 @@ jobs:
|
|||||||
permission-contents: read
|
permission-contents: read
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
@@ -412,7 +412,7 @@ jobs:
|
|||||||
permission-contents: read
|
permission-contents: read
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
@@ -422,7 +422,7 @@ jobs:
|
|||||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -490,7 +490,7 @@ jobs:
|
|||||||
permission-contents: read
|
permission-contents: read
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
@@ -500,7 +500,7 @@ jobs:
|
|||||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -598,7 +598,7 @@ jobs:
|
|||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
permission-contents: read
|
permission-contents: read
|
||||||
|
|
||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
@@ -637,7 +637,7 @@ jobs:
|
|||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
permission-contents: read
|
permission-contents: read
|
||||||
|
|
||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
@@ -669,7 +669,7 @@ jobs:
|
|||||||
permission-contents: read
|
permission-contents: read
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
@@ -699,7 +699,7 @@ jobs:
|
|||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
permission-contents: read
|
permission-contents: read
|
||||||
|
|
||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
@@ -722,7 +722,7 @@ jobs:
|
|||||||
permission-contents: read
|
permission-contents: read
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
@@ -781,7 +781,7 @@ jobs:
|
|||||||
permission-contents: read
|
permission-contents: read
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|||||||
38
.vscode/settings.json
vendored
38
.vscode/settings.json
vendored
@@ -29,9 +29,6 @@
|
|||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"tailwindCSS.lint.suggestCanonicalClasses": "ignore"
|
"tailwindCSS.lint.suggestCanonicalClasses": "ignore"
|
||||||
},
|
},
|
||||||
"svelte.plugin.svelte.compilerWarnings": {
|
|
||||||
"state_referenced_locally": "ignore"
|
|
||||||
},
|
|
||||||
"[typescript]": {
|
"[typescript]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.formatOnSave": true
|
"editor.formatOnSave": true
|
||||||
@@ -43,37 +40,40 @@
|
|||||||
"eslint.useFlatConfig": true,
|
"eslint.useFlatConfig": true,
|
||||||
"eslint.validate": ["javascript", "typescript", "svelte"],
|
"eslint.validate": ["javascript", "typescript", "svelte"],
|
||||||
"eslint.workingDirectories": [
|
"eslint.workingDirectories": [
|
||||||
{ "directory": "cli", "changeProcessCWD": true },
|
{ "changeProcessCWD": true, "directory": "cli" },
|
||||||
{ "directory": "e2e", "changeProcessCWD": true },
|
{ "changeProcessCWD": true, "directory": "e2e" },
|
||||||
{ "directory": "server", "changeProcessCWD": true },
|
{ "changeProcessCWD": true, "directory": "server" },
|
||||||
{ "directory": "web", "changeProcessCWD": true }
|
{ "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.enabled": true,
|
||||||
"explorer.fileNesting.patterns": {
|
"explorer.fileNesting.patterns": {
|
||||||
"*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart",
|
"*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart",
|
||||||
"*.ts": "${capture}.spec.ts,${capture}.mock.ts",
|
|
||||||
"*.js": "${capture}.spec.js,${capture}.mock.js",
|
"*.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"
|
"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": {
|
"search.exclude": {
|
||||||
"**/node_modules": true,
|
"**/.svelte-kit": true,
|
||||||
"**/build": true,
|
"**/build": true,
|
||||||
"**/dist": true,
|
"**/dist": true,
|
||||||
"**/.svelte-kit": true,
|
"**/node_modules": true,
|
||||||
"**/open-api/typescript-sdk/src": true
|
"**/open-api/typescript-sdk/src": true
|
||||||
},
|
},
|
||||||
"svelte.enable-ts-plugin": true,
|
"svelte.enable-ts-plugin": true,
|
||||||
|
"svelte.plugin.svelte.compilerWarnings": {
|
||||||
|
"state_referenced_locally": "ignore"
|
||||||
|
},
|
||||||
"tailwindCSS.experimental.configFile": {
|
"tailwindCSS.experimental.configFile": {
|
||||||
"web/src/app.css": "web/src/**"
|
"web/src/app.css": "web/src/**"
|
||||||
},
|
},
|
||||||
"js/ts.preferences.importModuleSpecifier": "non-relative",
|
|
||||||
"vitest.maximumConfigs": 10
|
"vitest.maximumConfigs": 10
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
<a href="readme_i18n/README_zh_TW.md">正體中文</a>
|
<a href="readme_i18n/README_zh_TW.md">正體中文</a>
|
||||||
<a href="readme_i18n/README_uk_UA.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_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_pt_BR.md">Português Brasileiro</a>
|
||||||
<a href="readme_i18n/README_sv_SE.md">Svenska</a>
|
<a href="readme_i18n/README_sv_SE.md">Svenska</a>
|
||||||
<a href="readme_i18n/README_ar_JO.md">العربية</a>
|
<a href="readme_i18n/README_ar_JO.md">العربية</a>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tools]
|
[tools]
|
||||||
terragrunt = "1.0.3"
|
terragrunt = "1.1.1"
|
||||||
opentofu = "1.11.6"
|
opentofu = "1.12.4"
|
||||||
|
|
||||||
[tasks."tg:fmt"]
|
[tasks."tg:fmt"]
|
||||||
run = "terragrunt hclfmt"
|
run = "terragrunt hclfmt"
|
||||||
|
|||||||
@@ -2,37 +2,35 @@
|
|||||||
# Manual edits may be lost in future updates.
|
# Manual edits may be lost in future updates.
|
||||||
|
|
||||||
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
||||||
version = "4.52.7"
|
version = "4.52.8"
|
||||||
constraints = "4.52.7"
|
constraints = "4.52.8"
|
||||||
hashes = [
|
hashes = [
|
||||||
"h1:+O72J3QYiZtYmYYZM/Eh0f4NNfl1BvjX1eju43qTQsQ=",
|
"h1:2uxVaMaGPDJljXqYzuD6xtyfOKdNudtGIgawRTkFJIs=",
|
||||||
"h1:0oqjYIPXcXh7XiDiKI085cHDYQQ5mh8kDl9dmBtvtog=",
|
"h1:34HL1GDpqYdn2iEC+85pCi+NddZf30qH3fJ8y3P6CAM=",
|
||||||
"h1:4b4ESb87MGv5bnadgYe7sK5rEkKMZhbkQcwPubQTsR4=",
|
"h1:4Z+uD+YY/7HrqdypM7DyUK+RtWepoLBc9fqtRQYZx4I=",
|
||||||
"h1:6mTr3eA1Ddb348lLmJuyvn98z4KF+ejqaUEJ76D1rzQ=",
|
"h1:8Ss6wlJL2BHvkIZU1iaHvyUntqNhPFZ0fl6mjsxhctc=",
|
||||||
"h1:9/3YH+9k9HqsvFtbmBf7SO2+xqZeZrXNKzLkjNuhUEA=",
|
"h1:AYeWWMTWaNg01N3hivxr/6YlG41j1RietMnft9vLjUI=",
|
||||||
"h1:Jcq4tBWgyH4/2JsojNBSRaN0mcItVMchO+lynonrlqc=",
|
"h1:BGRNOzo8NUgbx5RwpWkWmr38f/s3txb7mzQhqfM5blI=",
|
||||||
"h1:Y4Vv/2RdP0Q+uxqhOxzOdKxuuEMjXPDcU0vPc5bCQzI=",
|
"h1:EEXRMdC8bPlpLuOU8u+dE8mFJe3BpquFdH6vXcKseY4=",
|
||||||
"h1:a0gW8FBKsbP9Fi0HEDoy49WIbEWVHk9+BR4/iwuBdDQ=",
|
"h1:ENp29e6O6OnaHJlbtmRlyjlAmm3Wt4mGctWUpBY3d0g=",
|
||||||
"h1:gElv6iqJtg8OKN77gbw+MjrkrQmJHPkkMEi1J+0xkpU=",
|
"h1:IAtBAbGVe1npj/n1NvCIZkSYGl7qaPYT+FnVdZ2CZ00=",
|
||||||
"h1:oslXUugD/NQ+duJgT4BhKQyfGbuFOANknMvR73fiOeM=",
|
"h1:QRGGKngrxLORr4CImKXLDDJL5s67zUNBDAgk6FIIFNk=",
|
||||||
"h1:pPItIWii5oymR+geZB219ROSPuSODPLTlM4S/u8xLvM=",
|
"h1:bNql6cC+MfDbaJK3q7a8C1m3A21o7e5mrjBIGtk0RdI=",
|
||||||
"h1:u67GWw8GwD9NDlDzp9Y5VRnSQGcCrE8rSpkGPaBpDl0=",
|
"h1:gmNv+PEOkYDdu+c15bfpcoAE3EI7pgmov9RMf3GZLsI=",
|
||||||
"h1:uUUa9dY0XQOycI8pxg16PFFtL0WCTi9uEJz8trTQ7pU=",
|
"h1:yieZ7NWRYQcLYHUevzDDrrmDkoQICM+3ZeX4i/mw9ME=",
|
||||||
"h1:y3rV8KF2q6GEMANNlf5EkKJurlfbKlIKpjGcdxoy7pQ=",
|
"zh:08b305329a680a9213b2d8e642fbce7e4d97a524b1d2cef59e190ba9d678c477",
|
||||||
"zh:0c904ce31a4c6c4a5b3bf7ff1560e77c0cc7e2450c8553ded8e8c90398e1418b",
|
"zh:47975bd711ee18a46e589822171fa87474a552b332bfc8dea8fd1a64504eed8d",
|
||||||
"zh:36183d310c36373fe4cb936b83c595c6fd3b0a94bc7827f28e5789ccbf59752e",
|
"zh:5640d0d226bbafff3395542456c29feb942ca9c55ac01b4245a34c0590a33363",
|
||||||
"zh:556a568a6f0235e8f41647de9e4d3a1e7b1d6502df8b19b54ec441f1c653ea10",
|
"zh:5b0ad839fafba938c60a95d6b4a865843643591e97573ed2b9c4af3754064f74",
|
||||||
"zh:633ebbd5b0245e75e500ef9be4d9e62288f97e8da3baaa51323892a786d90285",
|
|
||||||
"zh:6acfe60cf52a65ba8f044f748548d2119e7f4fd7f8ebcb14698960d87c68f529",
|
|
||||||
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
|
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
|
||||||
"zh:904acc31ebb9d6ef68c792074b30532ee61bf515f19e0a3c75b46f126cca1f13",
|
"zh:93a9bc1139f5c02a44fdbf51fb2ce0891e2a42033b66febb928ccf72e870408f",
|
||||||
"zh:a1d0a81246afc8750286d3f6fe7a8fbe6460dd2662407b28dbfbabb612e5fa9d",
|
"zh:977f75cdf365686aa16ae02dcc0fc1769bae6f86be6e165393756c940bfb4af0",
|
||||||
"zh:a41a36fe253fc365fe2b7ffc749624688b2693b4634862fda161179ab100029f",
|
"zh:9afcda2660b3dc6ee6329acea532a719225bd1f6cf46f695feb2d77160847c1b",
|
||||||
"zh:a7ef269e77ffa8715c8945a2c14322c7ff159ea44c15f62505f3cbb2cae3b32d",
|
"zh:9e3da67b1b05b03d1f0c18b8677edf3797815b5dd49629f10eb2f457dccbed30",
|
||||||
"zh:b01aa3bed30610633b762df64332b26f8844a68c3960cebcb30f04918efc67fe",
|
"zh:b8d7da230f5266367c1b6b1cf31aa39087e231a87d55b71bb9d5c854ca1fcfe6",
|
||||||
"zh:b069cc2cd18cae10757df3ae030508eac8d55de7e49eda7a5e3e11f2f7fe6455",
|
"zh:cc96e7cd7350456b7a11a53d1c76c73a542de8359f9750f637ff865e8e77be91",
|
||||||
"zh:b2d2c6313729ebb7465dceece374049e2d08bda34473901be9ff46a8836d42b2",
|
"zh:da1f58d067def243047bb7178cb197e2b9c3a791a9eb380ad92b73290615ae29",
|
||||||
"zh:db0e114edaf4bc2f3d4769958807c83022bfbc619a00bdf4c4bd17faa4ab2d8b",
|
"zh:ed5f3e1f59a338bcdce0a2537a8a2f299cfe18c168626799b2a9c97af3d8d3cb",
|
||||||
"zh:ecc0aa8b9044f664fd2aaf8fa992d976578f78478980555b4b8f6148e8d1a5fe",
|
"zh:f16cc31f73a58a26ffe2d93223eddb7c340623a4e2ef38c6091766fd611e3e11",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ terraform {
|
|||||||
required_providers {
|
required_providers {
|
||||||
cloudflare = {
|
cloudflare = {
|
||||||
source = "cloudflare/cloudflare"
|
source = "cloudflare/cloudflare"
|
||||||
version = "4.52.7"
|
version = "4.52.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,37 +2,35 @@
|
|||||||
# Manual edits may be lost in future updates.
|
# Manual edits may be lost in future updates.
|
||||||
|
|
||||||
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
||||||
version = "4.52.7"
|
version = "4.52.8"
|
||||||
constraints = "4.52.7"
|
constraints = "4.52.8"
|
||||||
hashes = [
|
hashes = [
|
||||||
"h1:+O72J3QYiZtYmYYZM/Eh0f4NNfl1BvjX1eju43qTQsQ=",
|
"h1:2uxVaMaGPDJljXqYzuD6xtyfOKdNudtGIgawRTkFJIs=",
|
||||||
"h1:0oqjYIPXcXh7XiDiKI085cHDYQQ5mh8kDl9dmBtvtog=",
|
"h1:34HL1GDpqYdn2iEC+85pCi+NddZf30qH3fJ8y3P6CAM=",
|
||||||
"h1:4b4ESb87MGv5bnadgYe7sK5rEkKMZhbkQcwPubQTsR4=",
|
"h1:4Z+uD+YY/7HrqdypM7DyUK+RtWepoLBc9fqtRQYZx4I=",
|
||||||
"h1:6mTr3eA1Ddb348lLmJuyvn98z4KF+ejqaUEJ76D1rzQ=",
|
"h1:8Ss6wlJL2BHvkIZU1iaHvyUntqNhPFZ0fl6mjsxhctc=",
|
||||||
"h1:9/3YH+9k9HqsvFtbmBf7SO2+xqZeZrXNKzLkjNuhUEA=",
|
"h1:AYeWWMTWaNg01N3hivxr/6YlG41j1RietMnft9vLjUI=",
|
||||||
"h1:Jcq4tBWgyH4/2JsojNBSRaN0mcItVMchO+lynonrlqc=",
|
"h1:BGRNOzo8NUgbx5RwpWkWmr38f/s3txb7mzQhqfM5blI=",
|
||||||
"h1:Y4Vv/2RdP0Q+uxqhOxzOdKxuuEMjXPDcU0vPc5bCQzI=",
|
"h1:EEXRMdC8bPlpLuOU8u+dE8mFJe3BpquFdH6vXcKseY4=",
|
||||||
"h1:a0gW8FBKsbP9Fi0HEDoy49WIbEWVHk9+BR4/iwuBdDQ=",
|
"h1:ENp29e6O6OnaHJlbtmRlyjlAmm3Wt4mGctWUpBY3d0g=",
|
||||||
"h1:gElv6iqJtg8OKN77gbw+MjrkrQmJHPkkMEi1J+0xkpU=",
|
"h1:IAtBAbGVe1npj/n1NvCIZkSYGl7qaPYT+FnVdZ2CZ00=",
|
||||||
"h1:oslXUugD/NQ+duJgT4BhKQyfGbuFOANknMvR73fiOeM=",
|
"h1:QRGGKngrxLORr4CImKXLDDJL5s67zUNBDAgk6FIIFNk=",
|
||||||
"h1:pPItIWii5oymR+geZB219ROSPuSODPLTlM4S/u8xLvM=",
|
"h1:bNql6cC+MfDbaJK3q7a8C1m3A21o7e5mrjBIGtk0RdI=",
|
||||||
"h1:u67GWw8GwD9NDlDzp9Y5VRnSQGcCrE8rSpkGPaBpDl0=",
|
"h1:gmNv+PEOkYDdu+c15bfpcoAE3EI7pgmov9RMf3GZLsI=",
|
||||||
"h1:uUUa9dY0XQOycI8pxg16PFFtL0WCTi9uEJz8trTQ7pU=",
|
"h1:yieZ7NWRYQcLYHUevzDDrrmDkoQICM+3ZeX4i/mw9ME=",
|
||||||
"h1:y3rV8KF2q6GEMANNlf5EkKJurlfbKlIKpjGcdxoy7pQ=",
|
"zh:08b305329a680a9213b2d8e642fbce7e4d97a524b1d2cef59e190ba9d678c477",
|
||||||
"zh:0c904ce31a4c6c4a5b3bf7ff1560e77c0cc7e2450c8553ded8e8c90398e1418b",
|
"zh:47975bd711ee18a46e589822171fa87474a552b332bfc8dea8fd1a64504eed8d",
|
||||||
"zh:36183d310c36373fe4cb936b83c595c6fd3b0a94bc7827f28e5789ccbf59752e",
|
"zh:5640d0d226bbafff3395542456c29feb942ca9c55ac01b4245a34c0590a33363",
|
||||||
"zh:556a568a6f0235e8f41647de9e4d3a1e7b1d6502df8b19b54ec441f1c653ea10",
|
"zh:5b0ad839fafba938c60a95d6b4a865843643591e97573ed2b9c4af3754064f74",
|
||||||
"zh:633ebbd5b0245e75e500ef9be4d9e62288f97e8da3baaa51323892a786d90285",
|
|
||||||
"zh:6acfe60cf52a65ba8f044f748548d2119e7f4fd7f8ebcb14698960d87c68f529",
|
|
||||||
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
|
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
|
||||||
"zh:904acc31ebb9d6ef68c792074b30532ee61bf515f19e0a3c75b46f126cca1f13",
|
"zh:93a9bc1139f5c02a44fdbf51fb2ce0891e2a42033b66febb928ccf72e870408f",
|
||||||
"zh:a1d0a81246afc8750286d3f6fe7a8fbe6460dd2662407b28dbfbabb612e5fa9d",
|
"zh:977f75cdf365686aa16ae02dcc0fc1769bae6f86be6e165393756c940bfb4af0",
|
||||||
"zh:a41a36fe253fc365fe2b7ffc749624688b2693b4634862fda161179ab100029f",
|
"zh:9afcda2660b3dc6ee6329acea532a719225bd1f6cf46f695feb2d77160847c1b",
|
||||||
"zh:a7ef269e77ffa8715c8945a2c14322c7ff159ea44c15f62505f3cbb2cae3b32d",
|
"zh:9e3da67b1b05b03d1f0c18b8677edf3797815b5dd49629f10eb2f457dccbed30",
|
||||||
"zh:b01aa3bed30610633b762df64332b26f8844a68c3960cebcb30f04918efc67fe",
|
"zh:b8d7da230f5266367c1b6b1cf31aa39087e231a87d55b71bb9d5c854ca1fcfe6",
|
||||||
"zh:b069cc2cd18cae10757df3ae030508eac8d55de7e49eda7a5e3e11f2f7fe6455",
|
"zh:cc96e7cd7350456b7a11a53d1c76c73a542de8359f9750f637ff865e8e77be91",
|
||||||
"zh:b2d2c6313729ebb7465dceece374049e2d08bda34473901be9ff46a8836d42b2",
|
"zh:da1f58d067def243047bb7178cb197e2b9c3a791a9eb380ad92b73290615ae29",
|
||||||
"zh:db0e114edaf4bc2f3d4769958807c83022bfbc619a00bdf4c4bd17faa4ab2d8b",
|
"zh:ed5f3e1f59a338bcdce0a2537a8a2f299cfe18c168626799b2a9c97af3d8d3cb",
|
||||||
"zh:ecc0aa8b9044f664fd2aaf8fa992d976578f78478980555b4b8f6148e8d1a5fe",
|
"zh:f16cc31f73a58a26ffe2d93223eddb7c340623a4e2ef38c6091766fd611e3e11",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ terraform {
|
|||||||
required_providers {
|
required_providers {
|
||||||
cloudflare = {
|
cloudflare = {
|
||||||
source = "cloudflare/cloudflare"
|
source = "cloudflare/cloudflare"
|
||||||
version = "4.52.7"
|
version = "4.52.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
|
image: docker.io/valkey/valkey:9@sha256:8e8d64b405ce18f41b8e5ee20aa4687a8ed0022d1298f2ce31cdcf3a76e09411
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
|
image: docker.io/valkey/valkey:9@sha256:8e8d64b405ce18f41b8e5ee20aa4687a8ed0022d1298f2ce31cdcf3a76e09411
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
restart: always
|
restart: always
|
||||||
@@ -97,7 +97,7 @@ services:
|
|||||||
command: ['./run.sh', '-disable-reporting']
|
command: ['./run.sh', '-disable-reporting']
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
image: grafana/grafana:12.4.4-ubuntu@sha256:df2e7ef5f32f771794cf76bad5f2bceac227036460a2cc269a9045e5662abc58
|
image: grafana/grafana:12.4.5-ubuntu@sha256:00396460e499415c828b7c298f19287c8a0f95e72412ee37ac11723655c2d6b9
|
||||||
volumes:
|
volumes:
|
||||||
- grafana-data:/var/lib/grafana
|
- grafana-data:/var/lib/grafana
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
|
image: docker.io/valkey/valkey:9@sha256:8e8d64b405ce18f41b8e5ee20aa4687a8ed0022d1298f2ce31cdcf3a76e09411
|
||||||
user: '1000:1000'
|
user: '1000:1000'
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
|
image: docker.io/valkey/valkey:9@sha256:8e8d64b405ce18f41b8e5ee20aa4687a8ed0022d1298f2ce31cdcf3a76e09411
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
restart: always
|
restart: always
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 8.9 KiB |
@@ -35,6 +35,7 @@ Found Admin:
|
|||||||
- Email=admin@example.com
|
- Email=admin@example.com
|
||||||
- Name=Immich Admin
|
- Name=Immich Admin
|
||||||
? Please choose a new password (optional) immich-is-cool
|
? Please choose a new password (optional) immich-is-cool
|
||||||
|
? Invalidate existing sessions? Yes
|
||||||
The admin password has been updated.
|
The admin password has been updated.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ sidebar_position: 85
|
|||||||
:::note
|
:::note
|
||||||
This is a community contribution and not officially supported by the Immich team, but included here for convenience.
|
This is a community contribution and not officially supported by the Immich team, but included here for convenience.
|
||||||
|
|
||||||
Community support can be found in the dedicated channel on the [Discord Server](https://discord.immich.app/).
|
Community support 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.
|
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.
|
||||||
|
|||||||
@@ -28,4 +28,4 @@ run = "prettier --write ."
|
|||||||
run = "wrangler pages deploy build --project-name=${PROJECT_NAME} --branch=${BRANCH_NAME}"
|
run = "wrangler pages deploy build --project-name=${PROJECT_NAME} --branch=${BRANCH_NAME}"
|
||||||
|
|
||||||
[tools]
|
[tools]
|
||||||
wrangler = "4.110.0"
|
wrangler = "4.111.0"
|
||||||
|
|||||||
@@ -40,7 +40,8 @@
|
|||||||
"@docusaurus/tsconfig": "^3.10.0",
|
"@docusaurus/tsconfig": "^3.10.0",
|
||||||
"@docusaurus/types": "^3.10.0",
|
"@docusaurus/types": "^3.10.0",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.7.4",
|
||||||
"typescript": "^6.0.0"
|
"@typescript/native": "npm:typescript@^7.0.2",
|
||||||
|
"typescript": "npm:@typescript/typescript6@^6.0.2"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich-e2e-redis
|
container_name: immich-e2e-redis
|
||||||
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
|
image: docker.io/valkey/valkey:9@sha256:8e8d64b405ce18f41b8e5ee20aa4687a8ed0022d1298f2ce31cdcf3a76e09411
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
|
|
||||||
|
|||||||
@@ -41,9 +41,18 @@ export default typescriptEslint.config([
|
|||||||
'@typescript-eslint/no-floating-promises': 'error',
|
'@typescript-eslint/no-floating-promises': 'error',
|
||||||
'unicorn/prefer-module': 'off',
|
'unicorn/prefer-module': 'off',
|
||||||
'unicorn/import-style': '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,
|
curly: 2,
|
||||||
'prettier/prettier': 0,
|
'prettier/prettier': 0,
|
||||||
'unicorn/prevent-abbreviations': 'off',
|
'unicorn/name-replacements': 'off',
|
||||||
'unicorn/filename-case': 'off',
|
'unicorn/filename-case': 'off',
|
||||||
'unicorn/no-null': 'off',
|
'unicorn/no-null': 'off',
|
||||||
'unicorn/prefer-top-level-await': 'off',
|
'unicorn/prefer-top-level-await': 'off',
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
"eslint": "^10.0.0",
|
"eslint": "^10.0.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unicorn": "^64.0.0",
|
"eslint-plugin-unicorn": "^72.0.0",
|
||||||
"exiftool-vendored": "^35.0.0",
|
"exiftool-vendored": "^35.0.0",
|
||||||
"globals": "^17.0.0",
|
"globals": "^17.0.0",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
@@ -51,7 +51,8 @@
|
|||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"socket.io-client": "^4.7.4",
|
"socket.io-client": "^4.7.4",
|
||||||
"supertest": "^7.0.0",
|
"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",
|
"typescript-eslint": "^8.28.0",
|
||||||
"utimes": "^5.2.1",
|
"utimes": "^5.2.1",
|
||||||
"vitest": "^4.0.0"
|
"vitest": "^4.0.0"
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ describe('/admin/database-backups', () => {
|
|||||||
|
|
||||||
expect(status).toBe(201);
|
expect(status).toBe(201);
|
||||||
|
|
||||||
cookie = headers['set-cookie'][0].split(';')[0];
|
cookie = headers['set-cookie'][0].split(';', 1)[0];
|
||||||
|
|
||||||
await expect
|
await expect
|
||||||
.poll(
|
.poll(
|
||||||
@@ -224,7 +224,7 @@ describe('/admin/database-backups', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(status).toBe(201);
|
expect(status).toBe(201);
|
||||||
cookie = headers['set-cookie'][0].split(';')[0];
|
cookie = headers['set-cookie'][0].split(';', 1)[0];
|
||||||
|
|
||||||
await expect
|
await expect
|
||||||
.poll(
|
.poll(
|
||||||
@@ -295,7 +295,7 @@ describe('/admin/database-backups', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(status).toBe(201);
|
expect(status).toBe(201);
|
||||||
cookie = headers['set-cookie'][0].split(';')[0];
|
cookie = headers['set-cookie'][0].split(';', 1)[0];
|
||||||
|
|
||||||
await expect
|
await expect
|
||||||
.poll(
|
.poll(
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ describe('/admin/maintenance', () => {
|
|||||||
|
|
||||||
expect(status).toBe(201);
|
expect(status).toBe(201);
|
||||||
|
|
||||||
cookie = headers['set-cookie'][0].split(';')[0];
|
cookie = headers['set-cookie'][0].split(';', 1)[0];
|
||||||
expect(cookie).toEqual(
|
expect(cookie).toEqual(
|
||||||
expect.stringMatching(/^immich_maintenance_token=[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*$/),
|
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)
|
const { status, body } = await request(app)
|
||||||
.post('/admin/maintenance/login')
|
.post('/admin/maintenance/login')
|
||||||
.send({
|
.send({
|
||||||
token: cookie!.split('=')[1].trim(),
|
token: cookie!.split('=', 2)[1].trim(),
|
||||||
});
|
});
|
||||||
expect(status).toBe(201);
|
expect(status).toBe(201);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ test.describe('Maintenance', () => {
|
|||||||
test('maintenance shows no options to users until they authenticate', async ({ page }) => {
|
test('maintenance shows no options to users until they authenticate', async ({ page }) => {
|
||||||
const setCookie = await utils.enterMaintenance(admin.accessToken);
|
const setCookie = await utils.enterMaintenance(admin.accessToken);
|
||||||
const cookie = setCookie
|
const cookie = setCookie
|
||||||
?.map((cookie) => cookie.split(';')[0].split('='))
|
?.map((cookie) => cookie.split(';', 1)[0].split('='))
|
||||||
?.find(([name]) => name === 'immich_maintenance_token');
|
?.find(([name]) => name === 'immich_maintenance_token');
|
||||||
|
|
||||||
expect(cookie).toBeTruthy();
|
expect(cookie).toBeTruthy();
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ describe('/albums', () => {
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// eslint-disable-next-line unicorn/no-unreadable-array-destructuring
|
||||||
[user2Albums[0]] = await Promise.all([
|
[user2Albums[0]] = await Promise.all([
|
||||||
getAlbumInfo({ id: user2Albums[0].id }, { headers: asBearerAuth(user2.accessToken) }),
|
getAlbumInfo({ id: user2Albums[0].id }, { headers: asBearerAuth(user2.accessToken) }),
|
||||||
deleteUserAdmin({ id: user3.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) }),
|
deleteUserAdmin({ id: user3.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) }),
|
||||||
|
|||||||
@@ -781,7 +781,7 @@ describe('/asset', () => {
|
|||||||
exifImageWidth: 4032,
|
exifImageWidth: 4032,
|
||||||
exifImageHeight: 3024,
|
exifImageHeight: 3024,
|
||||||
latitude: 41.2203,
|
latitude: 41.2203,
|
||||||
longitude: -96.071_625,
|
longitude: -96.071625,
|
||||||
make: 'Apple',
|
make: 'Apple',
|
||||||
model: 'iPhone 7',
|
model: 'iPhone 7',
|
||||||
lensModel: 'iPhone 7 back camera 3.99mm f/1.8',
|
lensModel: 'iPhone 7 back camera 3.99mm f/1.8',
|
||||||
@@ -973,9 +973,9 @@ describe('/asset', () => {
|
|||||||
fileSizeInByte: 31_175_472,
|
fileSizeInByte: 31_175_472,
|
||||||
focalLength: 18.3,
|
focalLength: 18.3,
|
||||||
iso: 100,
|
iso: 100,
|
||||||
latitude: 36.613_24,
|
latitude: 36.61324,
|
||||||
lensModel: '18.3mm F2.8',
|
lensModel: '18.3mm F2.8',
|
||||||
longitude: -121.897_85,
|
longitude: -121.89785,
|
||||||
make: 'RICOH IMAGING COMPANY, LTD.',
|
make: 'RICOH IMAGING COMPANY, LTD.',
|
||||||
model: 'RICOH GR III',
|
model: 'RICOH GR III',
|
||||||
orientation: '1',
|
orientation: '1',
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ describe('/map', () => {
|
|||||||
country: 'United States of America',
|
country: 'United States of America',
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
lat: expect.closeTo(39.115),
|
lat: expect.closeTo(39.115),
|
||||||
lon: expect.closeTo(-108.400_968),
|
lon: expect.closeTo(-108.400968),
|
||||||
state: 'Colorado',
|
state: 'Colorado',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -83,7 +83,7 @@ describe('/map', () => {
|
|||||||
country: 'United States of America',
|
country: 'United States of America',
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
lat: expect.closeTo(41.2203),
|
lat: expect.closeTo(41.2203),
|
||||||
lon: expect.closeTo(-96.071_625),
|
lon: expect.closeTo(-96.071625),
|
||||||
state: 'Nebraska',
|
state: 'Nebraska',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -123,7 +123,7 @@ describe('/map', () => {
|
|||||||
country: 'United States of America',
|
country: 'United States of America',
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
lat: expect.closeTo(39.115),
|
lat: expect.closeTo(39.115),
|
||||||
lon: expect.closeTo(-108.400_968),
|
lon: expect.closeTo(-108.400968),
|
||||||
state: 'Colorado',
|
state: 'Colorado',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -131,7 +131,7 @@ describe('/map', () => {
|
|||||||
country: 'United States of America',
|
country: 'United States of America',
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
lat: expect.closeTo(41.2203),
|
lat: expect.closeTo(41.2203),
|
||||||
lon: expect.closeTo(-96.071_625),
|
lon: expect.closeTo(-96.071625),
|
||||||
state: 'Nebraska',
|
state: 'Nebraska',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -188,20 +188,20 @@ describe('/map', () => {
|
|||||||
const reverseGeocodeTestCases = [
|
const reverseGeocodeTestCases = [
|
||||||
{
|
{
|
||||||
name: 'Vaucluse',
|
name: 'Vaucluse',
|
||||||
lat: -33.858_977_058_663_13,
|
lat: -33.85897705866313,
|
||||||
lon: 151.278_490_730_270_48,
|
lon: 151.27849073027048,
|
||||||
results: [{ city: 'Vaucluse', state: 'New South Wales', country: 'Australia' }],
|
results: [{ city: 'Vaucluse', state: 'New South Wales', country: 'Australia' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Ravenhall',
|
name: 'Ravenhall',
|
||||||
lat: -37.765_732_399_174_75,
|
lat: -37.76573239917475,
|
||||||
lon: 144.752_453_164_883_3,
|
lon: 144.7524531648833,
|
||||||
results: [{ city: 'Ravenhall', state: 'Victoria', country: 'Australia' }],
|
results: [{ city: 'Ravenhall', state: 'Victoria', country: 'Australia' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Scarborough',
|
name: 'Scarborough',
|
||||||
lat: -31.894_346_156_789_997,
|
lat: -31.894346156789997,
|
||||||
lon: 115.757_617_103_904_64,
|
lon: 115.75761710390464,
|
||||||
results: [{ city: 'Scarborough', state: 'Western Australia', country: 'Australia' }],
|
results: [{ city: 'Scarborough', state: 'Western Australia', country: 'Australia' }],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const loginWithOAuth = async (sub: OAuthUser | string, redirectUri?: string) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
// login
|
// 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)
|
const response2 = await request(authServer.external + response1.location)
|
||||||
.post('')
|
.post('')
|
||||||
.set('Cookie', response1.cookies)
|
.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
|
// note: the coordinates here are not the actual coordinates of the images and are random for most of them
|
||||||
const coordinates = [
|
const coordinates = [
|
||||||
{ latitude: 48.853_41, longitude: 2.3488 }, // paris
|
{ latitude: 48.85341, longitude: 2.3488 }, // paris
|
||||||
{ latitude: 35.6895, longitude: 139.691_71 }, // tokyo
|
{ latitude: 35.6895, longitude: 139.69171 }, // tokyo
|
||||||
{ latitude: 52.524_37, longitude: 13.410_53 }, // berlin
|
{ latitude: 52.52437, longitude: 13.41053 }, // berlin
|
||||||
{ latitude: 1.314_663_1, longitude: 103.845_409_3 }, // singapore
|
{ latitude: 1.3146631, longitude: 103.8454093 }, // singapore
|
||||||
{ latitude: 41.013_84, longitude: 28.949_66 }, // istanbul
|
{ latitude: 41.01384, longitude: 28.94966 }, // istanbul
|
||||||
{ latitude: 5.556_02, longitude: -0.1969 }, // accra
|
{ latitude: 5.55602, longitude: -0.1969 }, // accra
|
||||||
{ latitude: 37.544_270_6, longitude: -4.727_752_8 }, // andalusia
|
{ latitude: 37.5442706, longitude: -4.7277528 }, // andalusia
|
||||||
{ latitude: 23.133_02, longitude: -82.383_04 }, // havana
|
{ latitude: 23.13302, longitude: -82.38304 }, // havana
|
||||||
{ latitude: 41.694_11, longitude: 44.833_68 }, // tbilisi
|
{ latitude: 41.69411, longitude: 44.83368 }, // tbilisi
|
||||||
{ latitude: 31.222_22, longitude: 121.458_06 }, // shanghai
|
{ latitude: 31.22222, longitude: 121.45806 }, // shanghai
|
||||||
{ latitude: 38.9711, longitude: -109.7137 }, // thompson springs
|
{ latitude: 38.9711, longitude: -109.7137 }, // thompson springs
|
||||||
{ latitude: 40.714_27, longitude: -74.005_97 }, // new york
|
{ latitude: 40.71427, longitude: -74.00597 }, // new york
|
||||||
{ latitude: 47.040_57, longitude: 9.068_04 }, // glarus
|
{ latitude: 47.04057, longitude: 9.06804 }, // glarus
|
||||||
{ latitude: 32.771_52, longitude: -89.116_73 }, // philadelphia
|
{ latitude: 32.77152, longitude: -89.11673 }, // philadelphia
|
||||||
{ latitude: 31.634_16, longitude: -7.999_94 }, // marrakesh
|
{ latitude: 31.63416, longitude: -7.99994 }, // marrakesh
|
||||||
{ latitude: 38.523_735_4, longitude: -78.488_619_4 }, // tanners ridge
|
{ latitude: 38.5237354, longitude: -78.4886194 }, // tanners ridge
|
||||||
{ latitude: 59.938_63, longitude: 30.314_13 }, // st. petersburg
|
{ latitude: 59.93863, longitude: 30.31413 }, // st. petersburg
|
||||||
{ latitude: 0, longitude: 0 }, // null island
|
{ latitude: 0, longitude: 0 }, // null island
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ describe('/search', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await Promise.all(updates);
|
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 });
|
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 () => {
|
it('should login and save auth.yml with 600', async () => {
|
||||||
const admin = await utils.adminSetup();
|
const admin = await utils.adminSetup();
|
||||||
const key = await utils.createApiKey(admin.accessToken, [Permission.All]);
|
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([
|
expect(stdout.split('\n')).toEqual([
|
||||||
'Logging in to http://127.0.0.1:2285/api',
|
'Logging in to http://127.0.0.1:2285/api',
|
||||||
'Logged in as admin@immich.cloud',
|
'Logged in as admin@immich.cloud',
|
||||||
@@ -48,7 +48,7 @@ describe(`immich login`, () => {
|
|||||||
it('should login without /api in the url', async () => {
|
it('should login without /api in the url', async () => {
|
||||||
const admin = await utils.adminSetup();
|
const admin = await utils.adminSetup();
|
||||||
const key = await utils.createApiKey(admin.accessToken, [Permission.All]);
|
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([
|
expect(stdout.split('\n')).toEqual([
|
||||||
'Logging in to http://127.0.0.1:2285',
|
'Logging in to http://127.0.0.1:2285',
|
||||||
'Discovered API at http://127.0.0.1:2285/api',
|
'Discovered API at http://127.0.0.1:2285/api',
|
||||||
|
|||||||
@@ -119,7 +119,9 @@ describe(`immich upload`, () => {
|
|||||||
const baseDir = `/tmp/upload/`;
|
const baseDir = `/tmp/upload/`;
|
||||||
|
|
||||||
const testPaths = Object.keys(files).map((filePath) => `${baseDir}/${filePath}`);
|
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}`);
|
const commandLine = paths.map((argument) => `${baseDir}/${argument}`);
|
||||||
|
|
||||||
@@ -135,7 +137,9 @@ describe(`immich upload`, () => {
|
|||||||
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.total).toBe(expectedCount);
|
expect(assets.total).toBe(expectedCount);
|
||||||
|
|
||||||
testPaths.map((filePath) => utils.removeImageFile(filePath));
|
for (const filePath of testPaths) {
|
||||||
|
utils.removeImageFile(filePath);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -646,7 +650,7 @@ describe(`immich upload`, () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
expect(stdout).toBe('');
|
expect(stdout).toBe('');
|
||||||
expect(stderr).toEqual(`error: option '-n, --dry-run' cannot be used with option '-h, --skip-hash'`);
|
expect(stderr).toEqual(`error: option '-n, --dry-run' cannot be used with option '--skip-hash'`);
|
||||||
expect(exitCode).not.toBe(0);
|
expect(exitCode).not.toBe(0);
|
||||||
|
|
||||||
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ describe(`immich-admin`, () => {
|
|||||||
child.stdout.on('data', (chunk) => {
|
child.stdout.on('data', (chunk) => {
|
||||||
data += chunk;
|
data += chunk;
|
||||||
if (data.includes('Please choose a new password (optional)')) {
|
if (data.includes('Please choose a new password (optional)')) {
|
||||||
|
child.stdin.write('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.includes('Invalidate existing sessions?')) {
|
||||||
child.stdin.end('\n');
|
child.stdin.end('\n');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export const randomImageFromString = async (
|
|||||||
let seedNumber = 0;
|
let seedNumber = 0;
|
||||||
for (let i = 0; i < seed.length; i++) {
|
for (let i = 0; i < seed.length; i++) {
|
||||||
seedNumber = (seedNumber << 5) - seedNumber + (seed.codePointAt(i) ?? 0);
|
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 });
|
return randomImage(new SeededRandom(Math.abs(seedNumber)), { width, height });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export function generateAsset(
|
|||||||
const asset: MockTimelineAsset = {
|
const asset: MockTimelineAsset = {
|
||||||
id: assetId,
|
id: assetId,
|
||||||
ownerId,
|
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),
|
thumbhash: generateThumbhash(rng),
|
||||||
localDateTime: date.toISOString(),
|
localDateTime: date.toISOString(),
|
||||||
fileCreatedAt: date.toISOString(),
|
fileCreatedAt: date.toISOString(),
|
||||||
@@ -214,7 +214,7 @@ export function generateTimelineData(config: TimelineConfig): MockTimelineData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a mock album from random assets
|
// 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)
|
// 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));
|
const albumSize = Math.min(allAssets.length, globalRng.nextInt(10, 31));
|
||||||
|
|||||||
@@ -172,11 +172,7 @@ function shouldIncludeAsset(
|
|||||||
if (isArchived !== undefined && actuallyArchived !== isArchived) {
|
if (isArchived !== undefined && actuallyArchived !== isArchived) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (isFavorite !== undefined && actuallyFavorited !== isFavorite) {
|
return isFavorite === undefined || actuallyFavorited === isFavorite;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Get summary for all buckets (mimics getTimeBuckets API)
|
* Get summary for all buckets (mimics getTimeBuckets API)
|
||||||
@@ -361,7 +357,7 @@ export function getAsset(
|
|||||||
owner?: UserResponseDto,
|
owner?: UserResponseDto,
|
||||||
): AssetResponseDto | undefined {
|
): AssetResponseDto | undefined {
|
||||||
// Search through all buckets for the asset
|
// Search through all buckets for the asset
|
||||||
const buckets = [...timelineData.buckets.values()];
|
const buckets = timelineData.buckets.values().toArray();
|
||||||
for (const assets of buckets) {
|
for (const assets of buckets) {
|
||||||
const asset = assets.find((a) => a.id === assetId);
|
const asset = assets.find((a) => a.id === assetId);
|
||||||
if (asset) {
|
if (asset) {
|
||||||
@@ -395,7 +391,7 @@ export function getAlbum(
|
|||||||
|
|
||||||
// Get the actual asset objects from the timeline data
|
// Get the actual asset objects from the timeline data
|
||||||
const albumAssets: AssetResponseDto[] = [];
|
const albumAssets: AssetResponseDto[] = [];
|
||||||
const allAssets = [...timelineData.buckets.values()].flat();
|
const allAssets = timelineData.buckets.values().toArray().flat();
|
||||||
|
|
||||||
for (const assetId of album.assetIds) {
|
for (const assetId of album.assetIds) {
|
||||||
const assetConfig = allAssets.find((a) => a.id === assetId);
|
const assetConfig = allAssets.find((a) => a.id === assetId);
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export function validateTimelineConfig(config: TimelineConfig): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate seed if provided
|
// 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');
|
throw new Error('Seed must be a non-negative integer');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -153,11 +153,8 @@ export function getMockAsset(
|
|||||||
const isInDifferentPeriod = (date1: DateTime, date2: DateTime): boolean => {
|
const isInDifferentPeriod = (date1: DateTime, date2: DateTime): boolean => {
|
||||||
if (unit === 'day') {
|
if (unit === 'day') {
|
||||||
return !date1.startOf('day').equals(date2.startOf('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') {
|
if (direction === 'next') {
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ export const setupTimelineMockApiRoutes = async (
|
|||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
json: getTimeBuckets(timelineRestData, isTrashed, isArchived, isFavorite, albumId, changes),
|
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');
|
const timeBucket = url.searchParams.get('timeBucket');
|
||||||
if (!timeBucket) {
|
if (!timeBucket) {
|
||||||
return route.continue();
|
return route.continue();
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ test.describe('asset-viewer', () => {
|
|||||||
assets.push(...timeBucket);
|
assets.push(...timeBucket);
|
||||||
}
|
}
|
||||||
for (const yearMonth of timelineRestData.buckets.keys()) {
|
for (const yearMonth of timelineRestData.buckets.keys()) {
|
||||||
const [year, month] = yearMonth.split('-');
|
const [year, month] = yearMonth.split('-', 2);
|
||||||
yearMonths.push(`${year}-${Number(month)}`);
|
yearMonths.push(`${year}-${Number(month)}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable unicorn/no-this-outside-of-class */
|
||||||
import type { AssetResponseDto } from '@immich/sdk';
|
import type { AssetResponseDto } from '@immich/sdk';
|
||||||
import { expect, Page } from '@playwright/test';
|
import { expect, Page } from '@playwright/test';
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ test.describe('Timeline', () => {
|
|||||||
assets.push(...timeBucket);
|
assets.push(...timeBucket);
|
||||||
}
|
}
|
||||||
for (const yearMonth of timelineRestData.buckets.keys()) {
|
for (const yearMonth of timelineRestData.buckets.keys()) {
|
||||||
const [year, month] = yearMonth.split('-');
|
const [year, month] = yearMonth.split('-', 2);
|
||||||
yearMonths.push(`${year}-${Number(month)}`);
|
yearMonths.push(`${year}-${Number(month)}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const sleep = (ms: number) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const padYearMonth = (yearMonth: string) => {
|
export const padYearMonth = (yearMonth: string) => {
|
||||||
const [year, month] = yearMonth.split('-');
|
const [year, month] = yearMonth.split('-', 2);
|
||||||
return `${year}-${month.padStart(2, '0')}`;
|
return `${year}-${month.padStart(2, '0')}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -72,6 +72,7 @@ export const thumbnailUtils = {
|
|||||||
},
|
},
|
||||||
async queryThumbnailInViewport(page: Page, collector: (assetId: string) => boolean) {
|
async queryThumbnailInViewport(page: Page, collector: (assetId: string) => boolean) {
|
||||||
const assetIds: string[] = [];
|
const assetIds: string[] = [];
|
||||||
|
// eslint-disable-next-line unicorn/no-this-outside-of-class
|
||||||
for (const thumb of await this.locator(page).all()) {
|
for (const thumb of await this.locator(page).all()) {
|
||||||
const box = await thumb.boundingBox();
|
const box = await thumb.boundingBox();
|
||||||
if (box) {
|
if (box) {
|
||||||
@@ -151,6 +152,7 @@ export const timelineUtils = {
|
|||||||
page.evaluate(() => {
|
page.evaluate(() => {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
||||||
return document.querySelector('#asset-grid').scrollTop;
|
return document.querySelector('#asset-grid').scrollTop;
|
||||||
});
|
});
|
||||||
await expect.poll(queryTop).toBeGreaterThan(0);
|
await expect.poll(queryTop).toBeGreaterThan(0);
|
||||||
@@ -177,6 +179,7 @@ export const assetViewerUtils = {
|
|||||||
page.evaluate(() => {
|
page.evaluate(() => {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
// eslint-disable-next-line unicorn/no-optional-chaining-on-undeclared-variable
|
||||||
return document.activeElement?.dataset?.asset;
|
return document.activeElement?.dataset?.asset;
|
||||||
});
|
});
|
||||||
await expect(poll(page, activeElement, (result) => result === assetId)).resolves.toBe(assetId);
|
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 {
|
import {
|
||||||
AssetMediaCreateDto,
|
AssetMediaCreateDto,
|
||||||
AssetMediaResponseDto,
|
AssetMediaResponseDto,
|
||||||
@@ -177,7 +178,7 @@ export const utils = {
|
|||||||
resetDatabase: async (tables?: string[]) => {
|
resetDatabase: async (tables?: string[]) => {
|
||||||
client = await utils.connectDatabase();
|
client = await utils.connectDatabase();
|
||||||
|
|
||||||
tables = tables || [
|
tables ||= [
|
||||||
// TODO e2e test for deleting a stack, since it is quite complex
|
// TODO e2e test for deleting a stack, since it is quite complex
|
||||||
'stack',
|
'stack',
|
||||||
'library',
|
'library',
|
||||||
@@ -304,7 +305,7 @@ export const utils = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
adminSetup: async (options?: AdminSetupOptions) => {
|
adminSetup: async (options?: AdminSetupOptions) => {
|
||||||
options = options || { onboarding: true };
|
options ||= { onboarding: true };
|
||||||
|
|
||||||
await signUpAdmin({ signUpDto: signupDto.admin });
|
await signUpAdmin({ signUpDto: signupDto.admin });
|
||||||
const response = await login({ loginCredentialDto: loginDto.admin });
|
const response = await login({ loginCredentialDto: loginDto.admin });
|
||||||
@@ -545,6 +546,7 @@ export const utils = {
|
|||||||
{
|
{
|
||||||
headers: asBearerAuth(accessToken),
|
headers: asBearerAuth(accessToken),
|
||||||
fetch: (...args: Parameters<typeof fetch>) =>
|
fetch: (...args: Parameters<typeof fetch>) =>
|
||||||
|
// eslint-disable-next-line unicorn/no-invalid-argument-count, unicorn/prefer-await
|
||||||
fetch(...args).then((response) => {
|
fetch(...args).then((response) => {
|
||||||
setCookie = response.headers.getSetCookie();
|
setCookie = response.headers.getSetCookie();
|
||||||
return response;
|
return response;
|
||||||
@@ -674,7 +676,7 @@ export const utils = {
|
|||||||
|
|
||||||
cliLogin: async (accessToken: string) => {
|
cliLogin: async (accessToken: string) => {
|
||||||
const key = await utils.createApiKey(accessToken, [Permission.All]);
|
const key = await utils.createApiKey(accessToken, [Permission.All]);
|
||||||
await immichCli(['login', app, `${key.secret}`]);
|
await immichCli(['login', app, key.secret]);
|
||||||
return key.secret;
|
return key.secret;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -706,6 +708,7 @@ export const utils = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line unicorn/no-top-level-side-effects
|
||||||
utils.initSdk();
|
utils.initSdk();
|
||||||
|
|
||||||
if (!existsSync(`${testAssetDir}/albums`)) {
|
if (!existsSync(`${testAssetDir}/albums`)) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"target": "es2023",
|
"target": "es2023",
|
||||||
|
"lib": ["dom", "ESNext"],
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
|
|||||||
170
mise.lock
170
mise.lock
@@ -94,6 +94,23 @@ url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets
|
|||||||
version = "7.1.3-6"
|
version = "7.1.3-6"
|
||||||
backend = "github:jellyfin/jellyfin-ffmpeg"
|
backend = "github:jellyfin/jellyfin-ffmpeg"
|
||||||
|
|
||||||
|
[tools."github:jellyfin/jellyfin-ffmpeg".options]
|
||||||
|
asset_pattern = "jellyfin-ffmpeg_*_portable_linuxarm64-gpl.tar.xz"
|
||||||
|
|
||||||
|
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64"]
|
||||||
|
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
|
||||||
|
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
|
||||||
|
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
|
||||||
|
|
||||||
|
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64-musl"]
|
||||||
|
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
|
||||||
|
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
|
||||||
|
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
|
||||||
|
|
||||||
|
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
|
||||||
|
version = "7.1.3-6"
|
||||||
|
backend = "github:jellyfin/jellyfin-ffmpeg"
|
||||||
|
|
||||||
[tools."github:jellyfin/jellyfin-ffmpeg".options]
|
[tools."github:jellyfin/jellyfin-ffmpeg".options]
|
||||||
asset_pattern = "jellyfin-ffmpeg_*_portable_linux64-gpl.tar.xz"
|
asset_pattern = "jellyfin-ffmpeg_*_portable_linux64-gpl.tar.xz"
|
||||||
|
|
||||||
@@ -112,17 +129,12 @@ version = "7.1.3-6"
|
|||||||
backend = "github:jellyfin/jellyfin-ffmpeg"
|
backend = "github:jellyfin/jellyfin-ffmpeg"
|
||||||
|
|
||||||
[tools."github:jellyfin/jellyfin-ffmpeg".options]
|
[tools."github:jellyfin/jellyfin-ffmpeg".options]
|
||||||
asset_pattern = "jellyfin-ffmpeg_*_portable_linuxarm64-gpl.tar.xz"
|
asset_pattern = "jellyfin-ffmpeg_*_portable_mac64-gpl.tar.xz"
|
||||||
|
|
||||||
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64"]
|
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.macos-x64"]
|
||||||
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
|
checksum = "sha256:066ede9774aaae97a18098aaeea8b7e0d286653eb8618f640476e99c59a536c2"
|
||||||
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
|
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_mac64-gpl.tar.xz"
|
||||||
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
|
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/408995889"
|
||||||
|
|
||||||
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64-musl"]
|
|
||||||
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
|
|
||||||
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
|
|
||||||
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
|
|
||||||
|
|
||||||
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
|
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
|
||||||
version = "7.1.3-6"
|
version = "7.1.3-6"
|
||||||
@@ -133,18 +145,6 @@ checksum = "sha256:7b7168149689610296f3a187c717056ce0786cc125a31caf28056737e9ba1
|
|||||||
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_win64-clang-gpl.zip"
|
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_win64-clang-gpl.zip"
|
||||||
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409036094"
|
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409036094"
|
||||||
|
|
||||||
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
|
|
||||||
version = "7.1.3-6"
|
|
||||||
backend = "github:jellyfin/jellyfin-ffmpeg"
|
|
||||||
|
|
||||||
[tools."github:jellyfin/jellyfin-ffmpeg".options]
|
|
||||||
asset_pattern = "jellyfin-ffmpeg_*_portable_mac64-gpl.tar.xz"
|
|
||||||
|
|
||||||
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.macos-x64"]
|
|
||||||
checksum = "sha256:066ede9774aaae97a18098aaeea8b7e0d286653eb8618f640476e99c59a536c2"
|
|
||||||
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_mac64-gpl.tar.xz"
|
|
||||||
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/408995889"
|
|
||||||
|
|
||||||
[[tools."github:webassembly/binaryen"]]
|
[[tools."github:webassembly/binaryen"]]
|
||||||
version = "version_124"
|
version = "version_124"
|
||||||
backend = "github:webassembly/binaryen"
|
backend = "github:webassembly/binaryen"
|
||||||
@@ -248,119 +248,119 @@ version = "7.5.0"
|
|||||||
backend = "npm:oazapfts"
|
backend = "npm:oazapfts"
|
||||||
|
|
||||||
[[tools.opentofu]]
|
[[tools.opentofu]]
|
||||||
version = "1.11.6"
|
version = "1.12.4"
|
||||||
backend = "aqua:opentofu/opentofu"
|
backend = "aqua:opentofu/opentofu"
|
||||||
|
|
||||||
[tools.opentofu."platforms.linux-arm64"]
|
[tools.opentofu."platforms.linux-arm64"]
|
||||||
checksum = "sha256:d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850"
|
checksum = "sha256:dc7bfcd93ce9795a86c58fbf71efd013c39dcd1febb13c9cd3555c43b9c2403a"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_arm64.tar.gz"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.4/tofu_1.12.4_linux_arm64.tar.gz"
|
||||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536382"
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/475646678"
|
||||||
|
|
||||||
[tools.opentofu."platforms.linux-arm64-musl"]
|
[tools.opentofu."platforms.linux-arm64-musl"]
|
||||||
checksum = "sha256:d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850"
|
checksum = "sha256:dc7bfcd93ce9795a86c58fbf71efd013c39dcd1febb13c9cd3555c43b9c2403a"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_arm64.tar.gz"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.4/tofu_1.12.4_linux_arm64.tar.gz"
|
||||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536382"
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/475646678"
|
||||||
|
|
||||||
[tools.opentofu."platforms.linux-x64"]
|
[tools.opentofu."platforms.linux-x64"]
|
||||||
checksum = "sha256:02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e"
|
checksum = "sha256:81836d0f12b4fe9013b85586349f993def9429b6383bb77cdd6c2f3a9d9aac24"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_amd64.tar.gz"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.4/tofu_1.12.4_linux_amd64.tar.gz"
|
||||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536401"
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/475646677"
|
||||||
|
|
||||||
[tools.opentofu."platforms.linux-x64-musl"]
|
[tools.opentofu."platforms.linux-x64-musl"]
|
||||||
checksum = "sha256:02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e"
|
checksum = "sha256:81836d0f12b4fe9013b85586349f993def9429b6383bb77cdd6c2f3a9d9aac24"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_amd64.tar.gz"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.4/tofu_1.12.4_linux_amd64.tar.gz"
|
||||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536401"
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/475646677"
|
||||||
|
|
||||||
[tools.opentofu."platforms.macos-arm64"]
|
[tools.opentofu."platforms.macos-arm64"]
|
||||||
checksum = "sha256:62d7fa8539e13b444827aa0a3b90c5972da5c47e8f8882d9dcf2e430e78840c1"
|
checksum = "sha256:7c06e4390d9ccd467773e37ff1c3d833c7ca0c24742cd9e9ad47284bea472247"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_darwin_arm64.tar.gz"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.4/tofu_1.12.4_darwin_arm64.tar.gz"
|
||||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536399"
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/475646544"
|
||||||
|
|
||||||
[tools.opentofu."platforms.macos-x64"]
|
[tools.opentofu."platforms.macos-x64"]
|
||||||
checksum = "sha256:1408cdef1c380f914565e6b4bb70794c6b163f195fcb233357f3d6c5745906b6"
|
checksum = "sha256:ead1d2ce643addb4ffeb93240b9377ae1c2fd793a6bd22d65922ac37adfdf546"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_darwin_amd64.tar.gz"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.4/tofu_1.12.4_darwin_amd64.tar.gz"
|
||||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536384"
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/475646688"
|
||||||
|
|
||||||
[tools.opentofu."platforms.windows-x64"]
|
[tools.opentofu."platforms.windows-x64"]
|
||||||
checksum = "sha256:27323f70c875b8251bfd7e61a4cffc3ebff4e56ed1e611b955016f0c7077367e"
|
checksum = "sha256:a4d86a07755c8d151f20f945e6cfb5b40deeed942af36a9bd385c5c2e965d5dd"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_windows_amd64.tar.gz"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.4/tofu_1.12.4_windows_amd64.tar.gz"
|
||||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536406"
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/475646543"
|
||||||
|
|
||||||
[[tools.pnpm]]
|
[[tools.pnpm]]
|
||||||
version = "11.11.0"
|
version = "11.13.1"
|
||||||
backend = "aqua:pnpm/pnpm"
|
backend = "aqua:pnpm/pnpm"
|
||||||
|
|
||||||
[tools.pnpm."platforms.linux-arm64"]
|
[tools.pnpm."platforms.linux-arm64"]
|
||||||
checksum = "sha256:4871093439f036b3082df6102255ac119f3b6f4f50c51c5711fc7c358b8b22d7"
|
checksum = "sha256:b52db99d215ed7dc9563aed815953c62a6c1ffd7cd75803d3a07ad7e4f246aed"
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.11.0/pnpm-linux-arm64.tar.gz"
|
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/471734816"
|
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563983"
|
||||||
provenance = "github-attestations"
|
provenance = "github-attestations"
|
||||||
|
|
||||||
[tools.pnpm."platforms.linux-arm64-musl"]
|
[tools.pnpm."platforms.linux-arm64-musl"]
|
||||||
checksum = "sha256:f97316cee0deb3ae3fd64e8a09e1c8c88a76da33373a2b465bd6a4c3e85e0690"
|
checksum = "sha256:2cadd4fc815c591f498a0a84c9e74a836e3e8c1236275f6e1cfd355bae6ae957"
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.11.0/pnpm-linux-arm64-musl.tar.gz"
|
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/471734819"
|
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563985"
|
||||||
provenance = "github-attestations"
|
provenance = "github-attestations"
|
||||||
|
|
||||||
[tools.pnpm."platforms.linux-x64"]
|
[tools.pnpm."platforms.linux-x64"]
|
||||||
checksum = "sha256:df4699e897012ab14df2cc6eaa942910e830eb7fcaa420a2a1421a9461fd9108"
|
checksum = "sha256:bd6d4b0e14536207ad76bc838f5980cecd968da15f69aae0b207380cca3f2e98"
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.11.0/pnpm-linux-x64.tar.gz"
|
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/471734818"
|
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563986"
|
||||||
provenance = "github-attestations"
|
provenance = "github-attestations"
|
||||||
|
|
||||||
[tools.pnpm."platforms.linux-x64-musl"]
|
[tools.pnpm."platforms.linux-x64-musl"]
|
||||||
checksum = "sha256:aad186f8a8ae72d827d5efe63e99e801456715fb7f2798949405fa33f20260db"
|
checksum = "sha256:ba19690f4ed1b64f1203ade14e9216352b46232d5582468b26a0160e0c9618c5"
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.11.0/pnpm-linux-x64-musl.tar.gz"
|
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/471734821"
|
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563980"
|
||||||
provenance = "github-attestations"
|
provenance = "github-attestations"
|
||||||
|
|
||||||
[tools.pnpm."platforms.macos-arm64"]
|
[tools.pnpm."platforms.macos-arm64"]
|
||||||
checksum = "sha256:ad46ad16c2376c2b78354ac488c1b166e076aa59bfcbcfd567d55957d755a690"
|
checksum = "sha256:765c2bf04e8129cb58c0f946e324262e418370b35a203b50b1f06a0567ef8bc1"
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.11.0/pnpm-darwin-arm64.tar.gz"
|
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/471734814"
|
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563984"
|
||||||
provenance = "github-attestations"
|
provenance = "github-attestations"
|
||||||
|
|
||||||
[tools.pnpm."platforms.windows-x64"]
|
[tools.pnpm."platforms.windows-x64"]
|
||||||
checksum = "sha256:13ad8a9b139c4c829fea33462cf04d061d72f0926b2b76a92ac03c9a36e405f9"
|
checksum = "sha256:d8bebbc71df2702961c1d34a5e61196bc0aa3bbde33c33253f6afa3dd4546a6d"
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.11.0/pnpm-win32-x64.zip"
|
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/471734820"
|
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563981"
|
||||||
provenance = "github-attestations"
|
provenance = "github-attestations"
|
||||||
|
|
||||||
[[tools.terragrunt]]
|
[[tools.terragrunt]]
|
||||||
version = "1.0.3"
|
version = "1.1.1"
|
||||||
backend = "aqua:gruntwork-io/terragrunt"
|
backend = "aqua:gruntwork-io/terragrunt"
|
||||||
|
|
||||||
[tools.terragrunt."platforms.linux-arm64"]
|
[tools.terragrunt."platforms.linux-arm64"]
|
||||||
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
|
checksum = "sha256:a374a7993ff3d99665a7e014007d3647ec7f0465c9d55c85e9f94c932e73cea2"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_linux_arm64.tar.gz"
|
||||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654253"
|
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476786148"
|
||||||
|
|
||||||
[tools.terragrunt."platforms.linux-arm64-musl"]
|
[tools.terragrunt."platforms.linux-arm64-musl"]
|
||||||
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
|
checksum = "sha256:a374a7993ff3d99665a7e014007d3647ec7f0465c9d55c85e9f94c932e73cea2"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_linux_arm64.tar.gz"
|
||||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654253"
|
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476786148"
|
||||||
|
|
||||||
[tools.terragrunt."platforms.linux-x64"]
|
[tools.terragrunt."platforms.linux-x64"]
|
||||||
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
|
checksum = "sha256:ce90077ac31ef17a2ba10d11d45f36c6501997a8f4f79d703bb7daba37032f53"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_linux_amd64.tar.gz"
|
||||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654196"
|
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476785968"
|
||||||
|
|
||||||
[tools.terragrunt."platforms.linux-x64-musl"]
|
[tools.terragrunt."platforms.linux-x64-musl"]
|
||||||
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
|
checksum = "sha256:ce90077ac31ef17a2ba10d11d45f36c6501997a8f4f79d703bb7daba37032f53"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_linux_amd64.tar.gz"
|
||||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654196"
|
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476785968"
|
||||||
|
|
||||||
[tools.terragrunt."platforms.macos-arm64"]
|
[tools.terragrunt."platforms.macos-arm64"]
|
||||||
checksum = "sha256:aacb5be2ca5475300cbce246dfbd8a45eb47510fbaa70fab8561c49ef5db03aa"
|
checksum = "sha256:9ec8f678b9ae6c81d5d9d77b94cbf6349ce639d5938694b4adc9dea73e416794"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_arm64.tar.gz"
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_darwin_arm64.tar.gz"
|
||||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654084"
|
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476785628"
|
||||||
|
|
||||||
[tools.terragrunt."platforms.macos-x64"]
|
[tools.terragrunt."platforms.macos-x64"]
|
||||||
checksum = "sha256:3133c2251e191aede8e3dd2a5b3aee2e91c5f08f88f117aee40eed9a24c8ef6b"
|
checksum = "sha256:73e768a69fa44a60f9d9174f54aaf179327e08706f633c9c79d5f5c9622c91c2"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_amd64.tar.gz"
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_darwin_amd64.tar.gz"
|
||||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406653970"
|
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476785520"
|
||||||
|
|
||||||
[tools.terragrunt."platforms.windows-x64"]
|
[tools.terragrunt."platforms.windows-x64"]
|
||||||
checksum = "sha256:183b2745b4e04980a6bfa4450ff81956a12596ca22d70f7aaa793980f5b036db"
|
checksum = "sha256:dd50a324691e072a3ac879e9ec43d6310e2936c929fda658eb2811fefcc75115"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_windows_amd64.exe.tar.gz"
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_windows_amd64.exe.tar.gz"
|
||||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654412"
|
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476786421"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
experimental_monorepo_root = true
|
monorepo_root = true
|
||||||
|
|
||||||
[monorepo]
|
[monorepo]
|
||||||
config_roots = [
|
config_roots = [
|
||||||
@@ -16,9 +16,9 @@ config_roots = [
|
|||||||
|
|
||||||
[tools]
|
[tools]
|
||||||
node = "24.15.0"
|
node = "24.15.0"
|
||||||
pnpm = "11.11.0"
|
pnpm = "11.13.1"
|
||||||
terragrunt = "1.0.3"
|
terragrunt = "1.1.1"
|
||||||
opentofu = "1.11.6"
|
opentofu = "1.12.4"
|
||||||
"npm:oazapfts" = "7.5.0"
|
"npm:oazapfts" = "7.5.0"
|
||||||
"github:extism/cli" = "1.6.3"
|
"github:extism/cli" = "1.6.3"
|
||||||
"github:webassembly/binaryen" = "version_124"
|
"github:webassembly/binaryen" = "version_124"
|
||||||
@@ -35,7 +35,6 @@ macos-x64 = { asset_pattern = "jellyfin-ffmpeg_*_portable_mac64-gpl.tar.xz" }
|
|||||||
macos-arm64 = { asset_pattern = "jellyfin-ffmpeg_*_portable_macarm64-gpl.tar.xz" }
|
macos-arm64 = { asset_pattern = "jellyfin-ffmpeg_*_portable_macarm64-gpl.tar.xz" }
|
||||||
|
|
||||||
[settings]
|
[settings]
|
||||||
experimental = true
|
|
||||||
pin = true
|
pin = true
|
||||||
lockfile = true
|
lockfile = true
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,6 @@ class ImmichApp : Application() {
|
|||||||
// as the previous start might have been killed without unlocking.
|
// as the previous start might have been killed without unlocking.
|
||||||
if (BackgroundEngineLock.connectEngines > 0) return@postDelayed
|
if (BackgroundEngineLock.connectEngines > 0) return@postDelayed
|
||||||
BackgroundWorkerApiImpl.enqueueBackgroundWorker(this)
|
BackgroundWorkerApiImpl.enqueueBackgroundWorker(this)
|
||||||
}, 5000)
|
}, 15000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import androidx.work.ListenableWorker
|
|||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import app.alextran.immich.MainActivity
|
import app.alextran.immich.MainActivity
|
||||||
import app.alextran.immich.R
|
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.ListenableFuture
|
||||||
import com.google.common.util.concurrent.SettableFuture
|
import com.google.common.util.concurrent.SettableFuture
|
||||||
import io.flutter.FlutterInjector
|
import io.flutter.FlutterInjector
|
||||||
@@ -61,6 +62,11 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun startWork(): ListenableFuture<Result> {
|
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")
|
Log.i(TAG, "Starting background upload worker")
|
||||||
|
|
||||||
if (!loader.initialized()) {
|
if (!loader.initialized()) {
|
||||||
@@ -77,6 +83,10 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
|
|||||||
showNotification(notificationConfig.first, notificationConfig.second)
|
showNotification(notificationConfig.first, notificationConfig.second)
|
||||||
|
|
||||||
loader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
|
loader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
|
||||||
|
if (isStopped || isComplete) {
|
||||||
|
return@ensureInitializationCompleteAsync
|
||||||
|
}
|
||||||
|
|
||||||
engine = FlutterEngine(ctx)
|
engine = FlutterEngine(ctx)
|
||||||
FlutterEngineCache.getInstance().put(BackgroundWorkerApiImpl.ENGINE_CACHE_KEY, engine!!)
|
FlutterEngineCache.getInstance().put(BackgroundWorkerApiImpl.ENGINE_CACHE_KEY, engine!!)
|
||||||
|
|
||||||
@@ -143,11 +153,17 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val api = flutterApi
|
||||||
|
if (api == null) {
|
||||||
|
Handler(Looper.getMainLooper()).postAtFrontOfQueue {
|
||||||
|
complete(Result.failure())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Handler(Looper.getMainLooper()).postAtFrontOfQueue {
|
Handler(Looper.getMainLooper()).postAtFrontOfQueue {
|
||||||
if (flutterApi != null) {
|
api.cancel {
|
||||||
flutterApi?.cancel {
|
complete(Result.failure())
|
||||||
complete(Result.failure())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ void main() {
|
|||||||
|
|
||||||
void sendUser(SyncStream stream, String id, String name) {
|
void sendUser(SyncStream stream, String id, String name) {
|
||||||
stream.send(
|
stream.send(
|
||||||
type: SyncEntityType.userV1.value,
|
type: SyncEntityType.userV1.toString(),
|
||||||
data: SyncUserV1(
|
data: SyncUserV1(
|
||||||
id: id,
|
id: id,
|
||||||
name: name,
|
name: name,
|
||||||
|
|||||||
@@ -586,7 +586,7 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
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 */ = {
|
BAEA01ACA3F5C9CD3D732370 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
|||||||
@@ -32,8 +32,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/maplibre/maplibre-gl-native-distribution.git",
|
"location" : "https://github.com/maplibre/maplibre-gl-native-distribution.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "60d9bb85c94ce6e7fc4406cd32529fd12bdb7809",
|
"revision" : "84a79bc375a301169390ac110c868f06c857b83f",
|
||||||
"version" : "6.14.0"
|
"version" : "6.27.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -43,15 +43,16 @@ class ConnectivityApiImpl: ConnectivityApi {
|
|||||||
capabilities.append(.vpn)
|
capabilities.append(.vpn)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if connection is unmetered:
|
// Determine if connection is unmetered from the OS metered flags rather than
|
||||||
// - Must be on WiFi (not cellular)
|
// the interface type, so wired ethernet (iPhone USB adapters, Apple Silicon
|
||||||
// - Must not be expensive (rules out personal hotspot)
|
// Macs) is treated as unmetered like Wi-Fi:
|
||||||
// - Must not be constrained (Low Data Mode)
|
// - Not on cellular
|
||||||
// Note: VPN over cellular should still be considered metered
|
// - 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 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)
|
capabilities.append(.unmetered)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -203,7 +203,10 @@ class ImmichAPI {
|
|||||||
|
|
||||||
func fetchMemory(for date: Date) async throws -> [MemoryResult] {
|
func fetchMemory(for date: Date) async throws -> [MemoryResult] {
|
||||||
// get URL
|
// get URL
|
||||||
let memoryParams = [URLQueryItem(name: "for", value: date.ISO8601Format())]
|
let localDay = date.formatted(
|
||||||
|
Date.ISO8601FormatStyle(timeZone: .current).year().month().day().dateSeparator(.dash)
|
||||||
|
)
|
||||||
|
let memoryParams = [URLQueryItem(name: "for", value: localDay)]
|
||||||
guard
|
guard
|
||||||
let searchURL = buildRequestURL(
|
let searchURL = buildRequestURL(
|
||||||
serverConfig: serverConfig,
|
serverConfig: serverConfig,
|
||||||
|
|||||||
45
mobile/ios/scripts/xcode_flutter_build.sh
Executable file
45
mobile/ios/scripts/xcode_flutter_build.sh
Executable file
@@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Makes Flutter's builds through the Xcode GUI properly display errors and warnings
|
||||||
|
# in the Issue navigator
|
||||||
|
#
|
||||||
|
# Flutter's `xcode_backend.dart` runs `flutter assemble` with `allowFail: true`,
|
||||||
|
# which intentionally does not prefix output with `error:`. Unsure why they do this,
|
||||||
|
# but this script rebuilds the expected output so Xcode can parse and display the errors
|
||||||
|
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
# The Immich mobile root (containing the Dart `lib` directory). This is used to make
|
||||||
|
# absolute paths for Xcode linking
|
||||||
|
app_root="${FLUTTER_APPLICATION_PATH:-$SRCROOT/..}"
|
||||||
|
|
||||||
|
/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build 2>&1 \
|
||||||
|
| awk -v app_root="$app_root" '
|
||||||
|
# Match Dart CFE diagnostics: <path>.dart:<line>:<col>: <Kind>: <message>
|
||||||
|
# Written for macOS/POSIX/BSD awk
|
||||||
|
{
|
||||||
|
# Always pass the original line through to preserve the original build log
|
||||||
|
print
|
||||||
|
|
||||||
|
if ($0 ~ /^.*\.dart:[0-9]+:[0-9]+: (Error|Warning|Context|Info):/) {
|
||||||
|
# Locate the ": Kind:" separator to split location from message.
|
||||||
|
rest = $0
|
||||||
|
if (match(rest, /: Error:/)) { kind = "Error"; keyword = "error" }
|
||||||
|
else if (match(rest, /: Warning:/)) { kind = "Warning"; keyword = "warning" }
|
||||||
|
else if (match(rest, /: Context:/)) { kind = "Context"; keyword = "note" }
|
||||||
|
else if (match(rest, /: Info:/)) { kind = "Info"; keyword = "note" }
|
||||||
|
|
||||||
|
# location = everything before ": Kind:" (e.g. "lib/foo.dart:12:5")
|
||||||
|
location = substr(rest, 1, RSTART - 1)
|
||||||
|
# message = everything after ": Kind:" (leading space preserved)
|
||||||
|
message = substr(rest, RSTART + length(": " kind ":"))
|
||||||
|
|
||||||
|
# Make the path absolute so Xcode links to it
|
||||||
|
if (location !~ /^\//)
|
||||||
|
location = app_root "/" location
|
||||||
|
|
||||||
|
printf "%s: %s:%s\n", location, keyword, message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'
|
||||||
|
|
||||||
|
exit "${PIPESTATUS[0]}"
|
||||||
@@ -6,6 +6,7 @@ const Map<String, Locale> locales = {
|
|||||||
// Additional locales
|
// Additional locales
|
||||||
'Arabic (ar)': Locale('ar'),
|
'Arabic (ar)': Locale('ar'),
|
||||||
'Basque (eu)': Locale('eu'),
|
'Basque (eu)': Locale('eu'),
|
||||||
|
'Belarusian (be)': Locale('be'),
|
||||||
'Bosnian (bl)': Locale('bn'),
|
'Bosnian (bl)': Locale('bn'),
|
||||||
'Brazilian Portuguese (pt_BR)': Locale('pt', 'BR'),
|
'Brazilian Portuguese (pt_BR)': Locale('pt', 'BR'),
|
||||||
'Bulgarian (bg)': Locale('bg'),
|
'Bulgarian (bg)': Locale('bg'),
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ class ViewerReloadAssetEvent extends Event {
|
|||||||
const ViewerReloadAssetEvent();
|
const ViewerReloadAssetEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ViewerStackAssetDeletedEvent extends Event {
|
||||||
|
final int stackIndex;
|
||||||
|
|
||||||
|
const ViewerStackAssetDeletedEvent({required this.stackIndex});
|
||||||
|
}
|
||||||
|
|
||||||
// Multi-Select Events
|
// Multi-Select Events
|
||||||
class MultiSelectToggleEvent extends Event {
|
class MultiSelectToggleEvent extends Event {
|
||||||
final bool isEnabled;
|
final bool isEnabled;
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/constants/colors.dart';
|
import 'package:immich_mobile/constants/colors.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/value_codec.dart';
|
||||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||||
import 'package:immich_mobile/utils/semver.dart';
|
import 'package:immich_mobile/utils/semver.dart';
|
||||||
|
|
||||||
enum SettingsKey<T> {
|
enum SettingsKey<T> {
|
||||||
// Theme
|
// Theme
|
||||||
themePrimaryColor<ImmichColorPreset>(codec: _EnumCodec(ImmichColorPreset.values)),
|
themePrimaryColor<ImmichColorPreset>(codec: EnumCodec(ImmichColorPreset.values)),
|
||||||
themeMode<ThemeMode>(codec: _EnumCodec(ThemeMode.values)),
|
themeMode<ThemeMode>(codec: EnumCodec(ThemeMode.values)),
|
||||||
themeDynamic<bool>(),
|
themeDynamic<bool>(),
|
||||||
themeColorfulInterface<bool>(),
|
themeColorfulInterface<bool>(),
|
||||||
|
|
||||||
@@ -27,13 +26,13 @@ enum SettingsKey<T> {
|
|||||||
|
|
||||||
// Network
|
// Network
|
||||||
networkAutoEndpointSwitching<bool>(),
|
networkAutoEndpointSwitching<bool>(),
|
||||||
|
networkExternalEndpointList<List<String>>(codec: ListCodec(PrimitiveCodec.string)),
|
||||||
|
networkCustomHeaders<Map<String, String>>(codec: MapCodec(PrimitiveCodec.string, PrimitiveCodec.string)),
|
||||||
networkPreferredWifiName<String?>(),
|
networkPreferredWifiName<String?>(),
|
||||||
networkLocalEndpoint<String?>(),
|
networkLocalEndpoint<String?>(),
|
||||||
networkExternalEndpointList<List<String>>(codec: _ListCodec(_PrimitiveCodec.string)),
|
|
||||||
networkCustomHeaders<Map<String, String>>(codec: _MapCodec(_PrimitiveCodec.string, _PrimitiveCodec.string)),
|
|
||||||
|
|
||||||
// Album
|
// Album
|
||||||
albumSortMode<AlbumSortMode>(codec: _EnumCodec(AlbumSortMode.values)),
|
albumSortMode<AlbumSortMode>(codec: EnumCodec(AlbumSortMode.values)),
|
||||||
albumIsReverse<bool>(),
|
albumIsReverse<bool>(),
|
||||||
albumIsGrid<bool>(),
|
albumIsGrid<bool>(),
|
||||||
|
|
||||||
@@ -47,195 +46,45 @@ enum SettingsKey<T> {
|
|||||||
|
|
||||||
// Timeline
|
// Timeline
|
||||||
timelineTilesPerRow<int>(),
|
timelineTilesPerRow<int>(),
|
||||||
timelineGroupAssetsBy<GroupAssetsBy>(codec: _EnumCodec(GroupAssetsBy.values)),
|
timelineGroupAssetsBy<GroupAssetsBy>(codec: EnumCodec(GroupAssetsBy.values)),
|
||||||
timelineStorageIndicator<bool>(),
|
timelineStorageIndicator<bool>(),
|
||||||
|
|
||||||
// Log
|
// Log
|
||||||
logLevel<LogLevel>(codec: _EnumCodec(LogLevel.values)),
|
logLevel<LogLevel>(codec: EnumCodec(LogLevel.values)),
|
||||||
|
|
||||||
// Map
|
// Map
|
||||||
mapShowFavoriteOnly<bool>(),
|
mapShowFavoriteOnly<bool>(),
|
||||||
mapRelativeDate<int>(),
|
mapRelativeDate<int>(),
|
||||||
mapIncludeArchived<bool>(),
|
mapIncludeArchived<bool>(),
|
||||||
mapThemeMode<ThemeMode>(codec: _EnumCodec(ThemeMode.values)),
|
mapThemeMode<ThemeMode>(codec: EnumCodec(ThemeMode.values)),
|
||||||
mapWithPartners<bool>(),
|
mapWithPartners<bool>(),
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
cleanupKeepFavorites<bool>(),
|
cleanupKeepFavorites<bool>(),
|
||||||
cleanupKeepMediaType<AssetKeepType>(codec: _EnumCodec(AssetKeepType.values)),
|
cleanupKeepMediaType<AssetKeepType>(codec: EnumCodec(AssetKeepType.values)),
|
||||||
cleanupKeepAlbumIds<List<String>>(codec: _ListCodec(_PrimitiveCodec.string)),
|
cleanupKeepAlbumIds<List<String>>(codec: ListCodec(PrimitiveCodec.string)),
|
||||||
cleanupCutoffDaysAgo<int>(),
|
cleanupCutoffDaysAgo<int>(),
|
||||||
cleanupDefaultsInitialized<bool>(),
|
cleanupDefaultsInitialized<bool>(),
|
||||||
|
|
||||||
// Share
|
// Share
|
||||||
shareFileType<ShareAssetType>(codec: _EnumCodec(ShareAssetType.values)),
|
shareFileType<ShareAssetType>(codec: EnumCodec(ShareAssetType.values)),
|
||||||
|
|
||||||
// Slideshow
|
// Slideshow
|
||||||
slideshowRepeat<bool>(),
|
slideshowRepeat<bool>(),
|
||||||
slideshowDuration<int>(),
|
slideshowDuration<int>(),
|
||||||
slideshowLook<SlideshowLook>(codec: _EnumCodec(SlideshowLook.values)),
|
slideshowLook<SlideshowLook>(codec: EnumCodec(SlideshowLook.values)),
|
||||||
slideshowDirection<SlideshowDirection>(codec: _EnumCodec(SlideshowDirection.values)),
|
slideshowDirection<SlideshowDirection>(codec: EnumCodec(SlideshowDirection.values)),
|
||||||
|
|
||||||
// Feature message
|
// Feature message
|
||||||
featureMessageSeenRelease<SemVer>(codec: _SemVerCodec());
|
featureMessageSeenRelease<SemVer>(codec: SemVerCodec());
|
||||||
|
|
||||||
final _SettingsCodec<T>? _codecOverride;
|
final ValueCodec<T>? _codecOverride;
|
||||||
|
|
||||||
const SettingsKey({_SettingsCodec<T>? codec}) : _codecOverride = codec;
|
const SettingsKey({ValueCodec<T>? codec}) : _codecOverride = codec;
|
||||||
|
|
||||||
_SettingsCodec<T> get _codec => _codecOverride ?? _SettingsCodec.forType(T);
|
ValueCodec<T> get _codec => _codecOverride ?? ValueCodec.forType(T);
|
||||||
|
|
||||||
String encode(T value) => _codec.encode(value);
|
String encode(T value) => _codec.encode(value);
|
||||||
|
|
||||||
T decode(String raw) => _codec.decode(raw);
|
T decode(String raw) => _codec.decode(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class _SettingsCodec<T> {
|
|
||||||
const _SettingsCodec();
|
|
||||||
|
|
||||||
String encode(T value);
|
|
||||||
T decode(String raw);
|
|
||||||
|
|
||||||
static final Map<Type, _SettingsCodec<Object>> _primitives = {
|
|
||||||
..._register<int>(_PrimitiveCodec.integer),
|
|
||||||
..._register<double>(_PrimitiveCodec.real),
|
|
||||||
..._register<bool>(_PrimitiveCodec.boolean),
|
|
||||||
..._register<String>(_PrimitiveCodec.string),
|
|
||||||
..._register<DateTime>(const _DateTimeCodec()),
|
|
||||||
};
|
|
||||||
|
|
||||||
static Map<Type, _SettingsCodec<Object>> _register<T>(_SettingsCodec<Object> codec) => {
|
|
||||||
T: codec,
|
|
||||||
// Reifies the nullable type T so it can be used as a key in the _primitives map
|
|
||||||
_typeOf<T?>(): codec,
|
|
||||||
};
|
|
||||||
|
|
||||||
static Type _typeOf<T>() => T;
|
|
||||||
|
|
||||||
static _SettingsCodec<T> forType<T>(Type runtimeType) {
|
|
||||||
final codec = _primitives[runtimeType];
|
|
||||||
if (codec == null) {
|
|
||||||
throw StateError('No primitive codec for $runtimeType. Provide an explicit codec when defining the SettingsKey.');
|
|
||||||
}
|
|
||||||
return codec as _SettingsCodec<T>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class _EnumCodec<T extends Enum> extends _SettingsCodec<T> {
|
|
||||||
final List<T> values;
|
|
||||||
|
|
||||||
const _EnumCodec(this.values);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String encode(T value) => value.name;
|
|
||||||
|
|
||||||
@override
|
|
||||||
T decode(String raw) => values.firstWhere((v) => v.name == raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
final class _DateTimeCodec extends _SettingsCodec<DateTime> {
|
|
||||||
const _DateTimeCodec();
|
|
||||||
|
|
||||||
@override
|
|
||||||
String encode(DateTime value) => value.toIso8601String();
|
|
||||||
|
|
||||||
@override
|
|
||||||
DateTime decode(String raw) => DateTime.parse(raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
final class _SemVerCodec extends _SettingsCodec<SemVer> {
|
|
||||||
const _SemVerCodec();
|
|
||||||
|
|
||||||
@override
|
|
||||||
String encode(SemVer value) => value.toString();
|
|
||||||
|
|
||||||
@override
|
|
||||||
SemVer decode(String raw) => SemVer.fromString(raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
final class _MapCodec<K extends Object, V extends Object> extends _SettingsCodec<Map<K, V>> {
|
|
||||||
final _SettingsCodec<K> _keyCodec;
|
|
||||||
final _SettingsCodec<V> _valueCodec;
|
|
||||||
|
|
||||||
const _MapCodec(this._keyCodec, this._valueCodec);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String encode(Map<K, V> value) {
|
|
||||||
final entries = <String, String>{};
|
|
||||||
value.forEach((k, v) => entries[_keyCodec.encode(k)] = _valueCodec.encode(v));
|
|
||||||
return jsonEncode(entries);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<K, V> decode(String raw) {
|
|
||||||
try {
|
|
||||||
final decoded = jsonDecode(raw);
|
|
||||||
if (decoded is! Map) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
final result = <K, V>{};
|
|
||||||
for (final entry in decoded.entries) {
|
|
||||||
final rawKey = entry.key;
|
|
||||||
final rawValue = entry.value;
|
|
||||||
if (rawKey is! String || rawValue is! String) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
final k = _keyCodec.decode(rawKey);
|
|
||||||
final v = _valueCodec.decode(rawValue);
|
|
||||||
result[k] = v;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
} on FormatException {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class _ListCodec<T extends Object> extends _SettingsCodec<List<T>> {
|
|
||||||
final _SettingsCodec<T> _elementCodec;
|
|
||||||
|
|
||||||
const _ListCodec(this._elementCodec);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String encode(List<T> value) => jsonEncode(value.map(_elementCodec.encode).toList());
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<T> decode(String raw) {
|
|
||||||
try {
|
|
||||||
final decoded = jsonDecode(raw);
|
|
||||||
if (decoded is! List) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
final result = <T>[];
|
|
||||||
for (final item in decoded) {
|
|
||||||
if (item is! String) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
final element = _elementCodec.decode(item);
|
|
||||||
result.add(element);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
} on FormatException {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class _PrimitiveCodec<T extends Object> extends _SettingsCodec<T> {
|
|
||||||
final T Function(String) _parse;
|
|
||||||
|
|
||||||
const _PrimitiveCodec._(this._parse);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String encode(T value) => value.toString();
|
|
||||||
|
|
||||||
@override
|
|
||||||
T decode(String raw) => _parse(raw);
|
|
||||||
|
|
||||||
static const integer = _PrimitiveCodec<int>._(int.parse);
|
|
||||||
static const real = _PrimitiveCodec<double>._(double.parse);
|
|
||||||
static const boolean = _PrimitiveCodec<bool>._(bool.parse);
|
|
||||||
static const string = _PrimitiveCodec<String>._(_identity);
|
|
||||||
|
|
||||||
static String _identity(String s) => s;
|
|
||||||
}
|
|
||||||
|
|||||||
153
mobile/lib/domain/models/value_codec.dart
Normal file
153
mobile/lib/domain/models/value_codec.dart
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:immich_mobile/utils/semver.dart';
|
||||||
|
|
||||||
|
sealed class ValueCodec<T> {
|
||||||
|
const ValueCodec();
|
||||||
|
|
||||||
|
String encode(T value);
|
||||||
|
T decode(String raw);
|
||||||
|
|
||||||
|
static final Map<Type, ValueCodec<Object>> _primitives = {
|
||||||
|
..._register<int>(PrimitiveCodec.integer),
|
||||||
|
..._register<double>(PrimitiveCodec.real),
|
||||||
|
..._register<bool>(PrimitiveCodec.boolean),
|
||||||
|
..._register<String>(PrimitiveCodec.string),
|
||||||
|
..._register<DateTime>(const DateTimeCodec()),
|
||||||
|
};
|
||||||
|
|
||||||
|
static Map<Type, ValueCodec<Object>> _register<T>(ValueCodec<Object> codec) => {
|
||||||
|
T: codec,
|
||||||
|
// Reifies the nullable type T so it can be used as a key in the _primitives map
|
||||||
|
_typeOf<T?>(): codec,
|
||||||
|
};
|
||||||
|
|
||||||
|
static Type _typeOf<T>() => T;
|
||||||
|
|
||||||
|
static ValueCodec<T> forType<T>(Type runtimeType) {
|
||||||
|
final codec = _primitives[runtimeType];
|
||||||
|
if (codec == null) {
|
||||||
|
throw StateError('No primitive codec for $runtimeType. Provide an explicit codec when defining the key.');
|
||||||
|
}
|
||||||
|
return codec as ValueCodec<T>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class EnumCodec<T extends Enum> extends ValueCodec<T> {
|
||||||
|
final List<T> values;
|
||||||
|
|
||||||
|
const EnumCodec(this.values);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String encode(T value) => value.name;
|
||||||
|
|
||||||
|
@override
|
||||||
|
T decode(String raw) => values.firstWhere((v) => v.name == raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
final class DateTimeCodec extends ValueCodec<DateTime> {
|
||||||
|
const DateTimeCodec();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String encode(DateTime value) => value.toIso8601String();
|
||||||
|
|
||||||
|
@override
|
||||||
|
DateTime decode(String raw) => DateTime.parse(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
final class SemVerCodec extends ValueCodec<SemVer> {
|
||||||
|
const SemVerCodec();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String encode(SemVer value) => value.toString();
|
||||||
|
|
||||||
|
@override
|
||||||
|
SemVer decode(String raw) => SemVer.fromString(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MapCodec<K extends Object, V extends Object> extends ValueCodec<Map<K, V>> {
|
||||||
|
final ValueCodec<K> _keyCodec;
|
||||||
|
final ValueCodec<V> _valueCodec;
|
||||||
|
|
||||||
|
const MapCodec(this._keyCodec, this._valueCodec);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String encode(Map<K, V> value) {
|
||||||
|
final entries = <String, String>{};
|
||||||
|
value.forEach((k, v) => entries[_keyCodec.encode(k)] = _valueCodec.encode(v));
|
||||||
|
return jsonEncode(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<K, V> decode(String raw) {
|
||||||
|
try {
|
||||||
|
final decoded = jsonDecode(raw);
|
||||||
|
if (decoded is! Map) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
final result = <K, V>{};
|
||||||
|
for (final entry in decoded.entries) {
|
||||||
|
final rawKey = entry.key;
|
||||||
|
final rawValue = entry.value;
|
||||||
|
if (rawKey is! String || rawValue is! String) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final k = _keyCodec.decode(rawKey);
|
||||||
|
final v = _valueCodec.decode(rawValue);
|
||||||
|
result[k] = v;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} on FormatException {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ListCodec<T extends Object> extends ValueCodec<List<T>> {
|
||||||
|
final ValueCodec<T> _elementCodec;
|
||||||
|
|
||||||
|
const ListCodec(this._elementCodec);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String encode(List<T> value) => jsonEncode(value.map(_elementCodec.encode).toList());
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<T> decode(String raw) {
|
||||||
|
try {
|
||||||
|
final decoded = jsonDecode(raw);
|
||||||
|
if (decoded is! List) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
final result = <T>[];
|
||||||
|
for (final item in decoded) {
|
||||||
|
if (item is! String) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
final element = _elementCodec.decode(item);
|
||||||
|
result.add(element);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} on FormatException {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class PrimitiveCodec<T extends Object> extends ValueCodec<T> {
|
||||||
|
final T Function(String) _parse;
|
||||||
|
|
||||||
|
const PrimitiveCodec._(this._parse);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String encode(T value) => value.toString();
|
||||||
|
|
||||||
|
@override
|
||||||
|
T decode(String raw) => _parse(raw);
|
||||||
|
|
||||||
|
static const integer = PrimitiveCodec<int>._(int.parse);
|
||||||
|
static const real = PrimitiveCodec<double>._(double.parse);
|
||||||
|
static const boolean = PrimitiveCodec<bool>._(bool.parse);
|
||||||
|
static const string = PrimitiveCodec<String>._(_identity);
|
||||||
|
|
||||||
|
static String _identity(String s) => s;
|
||||||
|
}
|
||||||
@@ -159,8 +159,8 @@ class RemoteAlbumService {
|
|||||||
return updatedAlbum;
|
return updatedAlbum;
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureOr<(DateTime, DateTime)> getDateRange(String albumId) {
|
Stream<(DateTime, DateTime)> watchDateRange(String albumId) {
|
||||||
return _repository.getDateRange(albumId);
|
return _repository.watchDateRange(albumId);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<UserDto>> getSharedUsers(String albumId) {
|
Future<List<UserDto>> getSharedUsers(String albumId) {
|
||||||
@@ -175,12 +175,12 @@ class RemoteAlbumService {
|
|||||||
return _repository.getAssets(albumId);
|
return _repository.getAssets(albumId);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> addAssets({required String albumId, required List<String> assetIds}) async {
|
Future<({int added, int failed})> addAssets({required String albumId, required List<String> assetIds}) async {
|
||||||
final album = await _albumApiRepository.addAssets(albumId, assetIds);
|
final album = await _albumApiRepository.addAssets(albumId, assetIds);
|
||||||
|
|
||||||
await _repository.addAssets(albumId, album.added);
|
await _repository.addAssets(albumId, album.added);
|
||||||
|
|
||||||
return album.added.length;
|
return (added: album.added.length, failed: album.failed.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// !TODO The name here is not clear as we have addAssets method above,
|
/// !TODO The name here is not clear as we have addAssets method above,
|
||||||
@@ -196,7 +196,7 @@ class RemoteAlbumService {
|
|||||||
}) async {
|
}) async {
|
||||||
int addedCount = 0;
|
int addedCount = 0;
|
||||||
if (candidates.remoteAssetIds.isNotEmpty) {
|
if (candidates.remoteAssetIds.isNotEmpty) {
|
||||||
addedCount += await addAssets(albumId: albumId, assetIds: candidates.remoteAssetIds);
|
addedCount += (await addAssets(albumId: albumId, assetIds: candidates.remoteAssetIds)).added;
|
||||||
}
|
}
|
||||||
if (candidates.localAssetsToUpload.isNotEmpty) {
|
if (candidates.localAssetsToUpload.isNotEmpty) {
|
||||||
addedCount += await _uploadAndAddLocals(
|
addedCount += await _uploadAndAddLocals(
|
||||||
|
|||||||
@@ -328,8 +328,6 @@ class SyncStreamService {
|
|||||||
return _syncStreamRepository.updateAssetOcrV1(data.cast());
|
return _syncStreamRepository.updateAssetOcrV1(data.cast());
|
||||||
case SyncEntityType.assetOcrDeleteV1:
|
case SyncEntityType.assetOcrDeleteV1:
|
||||||
return _syncStreamRepository.deleteAssetOcrV1(data.cast());
|
return _syncStreamRepository.deleteAssetOcrV1(data.cast());
|
||||||
default:
|
|
||||||
_logger.warning("Unknown sync data type: $type");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ extension on api.AssetVisibility {
|
|||||||
api.AssetVisibility.hidden => AssetVisibility.hidden,
|
api.AssetVisibility.hidden => AssetVisibility.hidden,
|
||||||
api.AssetVisibility.archive => AssetVisibility.archive,
|
api.AssetVisibility.archive => AssetVisibility.archive,
|
||||||
api.AssetVisibility.locked => AssetVisibility.locked,
|
api.AssetVisibility.locked => AssetVisibility.locked,
|
||||||
_ => AssetVisibility.timeline,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +67,5 @@ extension on api.AssetTypeEnum {
|
|||||||
api.AssetTypeEnum.VIDEO => AssetType.video,
|
api.AssetTypeEnum.VIDEO => AssetType.video,
|
||||||
api.AssetTypeEnum.AUDIO => AssetType.audio,
|
api.AssetTypeEnum.AUDIO => AssetType.audio,
|
||||||
api.AssetTypeEnum.OTHER => AssetType.other,
|
api.AssetTypeEnum.OTHER => AssetType.other,
|
||||||
_ => throw Exception('Unknown AssetType value: $this'),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
// ignore: depend_on_referenced_packages
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
abstract class CachedKeyValueRepository<K extends Enum, S> {
|
||||||
|
CachedKeyValueRepository(this._snapshot);
|
||||||
|
|
||||||
|
S _snapshot;
|
||||||
|
S get snapshot => _snapshot;
|
||||||
|
@protected
|
||||||
|
set snapshot(S value) => _snapshot = value;
|
||||||
|
|
||||||
|
List<K> get keys;
|
||||||
|
|
||||||
|
Object decodeValue(K key, String raw);
|
||||||
|
|
||||||
|
S buildSnapshot(Map<K, Object?> overrides);
|
||||||
|
|
||||||
|
Selectable<({String key, String? value})> selectable();
|
||||||
|
|
||||||
|
Future<void> refresh() async => _snapshot = _build(await selectable().get());
|
||||||
|
|
||||||
|
Stream<S> watchSnapshot() => selectable().watch().map((rows) => _snapshot = _build(rows));
|
||||||
|
|
||||||
|
S _build(List<({String key, String? value})> rows) => buildSnapshot(
|
||||||
|
rows.fold({}, (overrides, row) {
|
||||||
|
final key = keys.firstWhereOrNull((key) => key.name == row.key);
|
||||||
|
if (key == null) {
|
||||||
|
return overrides;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object? decodedValue;
|
||||||
|
if (row.value != null) {
|
||||||
|
decodedValue = decodeValue(key, row.value!);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {...overrides, key: decodedValue};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -210,17 +210,19 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
|||||||
await _deleteAssets(deletes);
|
await _deleteAssets(deletes);
|
||||||
|
|
||||||
await _upsertAssets(updates);
|
await _upsertAssets(updates);
|
||||||
|
// Drop every existing album link for each changed asset before re-adding the
|
||||||
|
// ones the native side reports. A moved asset only reports its new album here,
|
||||||
|
// so leaving the old link around makes the per-album delete sweep wipe the
|
||||||
|
// asset entirely (it is still linked to a bucket it no longer lives in).
|
||||||
|
await _db.batch((batch) async {
|
||||||
|
for (final assetId in assetAlbums.keys) {
|
||||||
|
batch.deleteWhere(_db.localAlbumAssetEntity, (f) => f.assetId.equals(assetId));
|
||||||
|
}
|
||||||
|
});
|
||||||
// The ugly casting below is required for now because the generated code
|
// The ugly casting below is required for now because the generated code
|
||||||
// casts the returned values from the platform during decoding them
|
// casts the returned values from the platform during decoding them
|
||||||
// and iterating over them causes the type to be List<Object?> instead of
|
// and iterating over them causes the type to be List<Object?> instead of
|
||||||
// List<String>
|
// List<String>
|
||||||
await _db.batch((batch) async {
|
|
||||||
assetAlbums.cast<String, List<Object?>>().forEach((assetId, albumIds) {
|
|
||||||
for (final albumId in albumIds.cast<String?>().nonNulls) {
|
|
||||||
batch.deleteWhere(_db.localAlbumAssetEntity, (f) => f.albumId.equals(albumId) & f.assetId.equals(assetId));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
await _db.batch((batch) async {
|
await _db.batch((batch) async {
|
||||||
assetAlbums.cast<String, List<Object?>>().forEach((assetId, albumIds) {
|
assetAlbums.cast<String, List<Object?>>().forEach((assetId, albumIds) {
|
||||||
batch.insertAll(
|
batch.insertAll(
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureOr<(DateTime, DateTime)> getDateRange(String albumId) {
|
Stream<(DateTime, DateTime)> watchDateRange(String albumId) {
|
||||||
final query = _db.remoteAlbumAssetEntity.selectOnly()
|
final query = _db.remoteAlbumAssetEntity.selectOnly()
|
||||||
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId))
|
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId))
|
||||||
..addColumns([_db.remoteAssetEntity.createdAt.min(), _db.remoteAssetEntity.createdAt.max()])
|
..addColumns([_db.remoteAssetEntity.createdAt.min(), _db.remoteAssetEntity.createdAt.max()])
|
||||||
@@ -229,7 +229,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
final minDate = row.read(_db.remoteAssetEntity.createdAt.min());
|
final minDate = row.read(_db.remoteAssetEntity.createdAt.min());
|
||||||
final maxDate = row.read(_db.remoteAssetEntity.createdAt.max());
|
final maxDate = row.read(_db.remoteAssetEntity.createdAt.max());
|
||||||
return (minDate ?? DateTime.now(), maxDate ?? DateTime.now());
|
return (minDate ?? DateTime.now(), maxDate ?? DateTime.now());
|
||||||
}).getSingle();
|
}).watchSingle();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<UserDto>> getSharedUsers(String albumId) async {
|
Future<List<UserDto>> getSharedUsers(String albumId) async {
|
||||||
|
|||||||
@@ -71,7 +71,13 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final query = _db.remoteAssetEntity.select()
|
final query = _db.remoteAssetEntity.select()
|
||||||
..where((row) => row.stackId.equals(stackId) & row.id.equals(asset.id).not())
|
..where(
|
||||||
|
(row) =>
|
||||||
|
row.stackId.equals(stackId) &
|
||||||
|
row.id.equals(asset.id).not() &
|
||||||
|
row.deletedAt.isNull() &
|
||||||
|
row.visibility.equalsValue(AssetVisibility.timeline),
|
||||||
|
)
|
||||||
..orderBy([(row) => OrderingTerm.desc(row.createdAt)]);
|
..orderBy([(row) => OrderingTerm.desc(row.createdAt)]);
|
||||||
|
|
||||||
return query.map((row) => row.toDto()).get();
|
return query.map((row) => row.toDto()).get();
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:immich_mobile/domain/models/config/app_config.dart';
|
import 'package:immich_mobile/domain/models/config/app_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/settings_key.dart';
|
import 'package:immich_mobile/domain/models/settings_key.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/cached_key_value_repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
|
||||||
class SettingsRepository extends DriftDatabaseRepository {
|
class SettingsRepository extends CachedKeyValueRepository<SettingsKey, AppConfig> {
|
||||||
final Drift _db;
|
final Drift _db;
|
||||||
|
|
||||||
SettingsRepository._(this._db) : super(_db);
|
SettingsRepository._(this._db) : super(const .new());
|
||||||
|
|
||||||
static SettingsRepository? _instance;
|
static SettingsRepository? _instance;
|
||||||
|
|
||||||
@@ -19,9 +20,6 @@ class SettingsRepository extends DriftDatabaseRepository {
|
|||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig _appConfig = const .new();
|
|
||||||
AppConfig get appConfig => _appConfig;
|
|
||||||
|
|
||||||
static Future<SettingsRepository> ensureInitialized(Drift db) async {
|
static Future<SettingsRepository> ensureInitialized(Drift db) async {
|
||||||
if (_instance == null) {
|
if (_instance == null) {
|
||||||
final instance = SettingsRepository._(db);
|
final instance = SettingsRepository._(db);
|
||||||
@@ -31,7 +29,20 @@ class SettingsRepository extends DriftDatabaseRepository {
|
|||||||
return _instance!;
|
return _instance!;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> refresh() async => _applyOverrides(await _db.select(_db.settingsEntity).get());
|
@override
|
||||||
|
List<SettingsKey> get keys => SettingsKey.values;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Object decodeValue(SettingsKey key, String raw) => key.decode(raw);
|
||||||
|
|
||||||
|
@override
|
||||||
|
AppConfig buildSnapshot(Map<SettingsKey, Object?> overrides) => AppConfig.fromEntries(overrides);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Selectable<({String key, String? value})> selectable() =>
|
||||||
|
_db.select(_db.settingsEntity).map((row) => (key: row.key, value: row.value));
|
||||||
|
|
||||||
|
AppConfig get appConfig => snapshot;
|
||||||
|
|
||||||
Future<void> clear(Iterable<SettingsKey> keys) async {
|
Future<void> clear(Iterable<SettingsKey> keys) async {
|
||||||
if (keys.isEmpty) {
|
if (keys.isEmpty) {
|
||||||
@@ -41,13 +52,15 @@ class SettingsRepository extends DriftDatabaseRepository {
|
|||||||
final names = keys.map((key) => key.name).toList();
|
final names = keys.map((key) => key.name).toList();
|
||||||
await (_db.delete(_db.settingsEntity)..where((row) => row.key.isIn(names))).go();
|
await (_db.delete(_db.settingsEntity)..where((row) => row.key.isIn(names))).go();
|
||||||
|
|
||||||
|
var config = snapshot;
|
||||||
for (final key in keys) {
|
for (final key in keys) {
|
||||||
_appConfig = _appConfig.write(key, defaultConfig.read(key));
|
config = config.write(key, defaultConfig.read(key));
|
||||||
}
|
}
|
||||||
|
snapshot = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> write<T, U extends T>(SettingsKey<T> key, U value) async {
|
Future<void> write<T, U extends T>(SettingsKey<T> key, U value) async {
|
||||||
if (value == _appConfig.read(key)) {
|
if (value == snapshot.read(key)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,29 +78,8 @@ class SettingsRepository extends DriftDatabaseRepository {
|
|||||||
.insertOnConflictUpdate(
|
.insertOnConflictUpdate(
|
||||||
SettingsEntityCompanion.insert(key: key.name, value: .new(resolvedValue), updatedAt: .new(DateTime.now())),
|
SettingsEntityCompanion.insert(key: key.name, value: .new(resolvedValue), updatedAt: .new(DateTime.now())),
|
||||||
);
|
);
|
||||||
_appConfig = _appConfig.write(key, value);
|
snapshot = snapshot.write(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<AppConfig> watchConfig() => _db.select(_db.settingsEntity).watch().map((rows) {
|
Stream<AppConfig> watchConfig() => watchSnapshot();
|
||||||
_applyOverrides(rows);
|
|
||||||
return _appConfig;
|
|
||||||
});
|
|
||||||
|
|
||||||
void _applyOverrides(List<SettingsEntityData> rows) {
|
|
||||||
_appConfig = AppConfig.fromEntries(
|
|
||||||
rows.fold({}, (overrides, row) {
|
|
||||||
final metadataKey = SettingsKey.values.firstWhereOrNull((key) => key.name == row.key);
|
|
||||||
if (metadataKey == null) {
|
|
||||||
return overrides;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object? decodedValue;
|
|
||||||
if (row.value != null) {
|
|
||||||
decodedValue = metadataKey.decode(row.value!);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {...overrides, metadataKey: decodedValue};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -937,7 +937,6 @@ extension on AssetTypeEnum {
|
|||||||
AssetTypeEnum.VIDEO => AssetType.video,
|
AssetTypeEnum.VIDEO => AssetType.video,
|
||||||
AssetTypeEnum.AUDIO => AssetType.audio,
|
AssetTypeEnum.AUDIO => AssetType.audio,
|
||||||
AssetTypeEnum.OTHER => AssetType.other,
|
AssetTypeEnum.OTHER => AssetType.other,
|
||||||
_ => throw Exception('Unknown AssetType value: $this'),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -945,14 +944,12 @@ extension on AssetOrder {
|
|||||||
AlbumAssetOrder toAlbumAssetOrder() => switch (this) {
|
AlbumAssetOrder toAlbumAssetOrder() => switch (this) {
|
||||||
AssetOrder.asc => AlbumAssetOrder.asc,
|
AssetOrder.asc => AlbumAssetOrder.asc,
|
||||||
AssetOrder.desc => AlbumAssetOrder.desc,
|
AssetOrder.desc => AlbumAssetOrder.desc,
|
||||||
_ => throw Exception('Unknown AssetOrder value: $this'),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on MemoryType {
|
extension on MemoryType {
|
||||||
MemoryTypeEnum toMemoryType() => switch (this) {
|
MemoryTypeEnum toMemoryType() => switch (this) {
|
||||||
MemoryType.onThisDay => MemoryTypeEnum.onThisDay,
|
MemoryType.onThisDay => MemoryTypeEnum.onThisDay,
|
||||||
_ => throw Exception('Unknown MemoryType value: $this'),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -961,7 +958,6 @@ extension on api.AlbumUserRole {
|
|||||||
api.AlbumUserRole.editor => AlbumUserRole.editor,
|
api.AlbumUserRole.editor => AlbumUserRole.editor,
|
||||||
api.AlbumUserRole.viewer => AlbumUserRole.viewer,
|
api.AlbumUserRole.viewer => AlbumUserRole.viewer,
|
||||||
api.AlbumUserRole.owner => AlbumUserRole.owner,
|
api.AlbumUserRole.owner => AlbumUserRole.owner,
|
||||||
_ => throw Exception('Unknown AlbumUserRole value: $this'),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -971,7 +967,6 @@ extension on api.AssetVisibility {
|
|||||||
api.AssetVisibility.hidden => AssetVisibility.hidden,
|
api.AssetVisibility.hidden => AssetVisibility.hidden,
|
||||||
api.AssetVisibility.archive => AssetVisibility.archive,
|
api.AssetVisibility.archive => AssetVisibility.archive,
|
||||||
api.AssetVisibility.locked => AssetVisibility.locked,
|
api.AssetVisibility.locked => AssetVisibility.locked,
|
||||||
_ => throw Exception('Unknown AssetVisibility value: $this'),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -980,12 +975,11 @@ extension on api.UserMetadataKey {
|
|||||||
api.UserMetadataKey.onboarding => UserMetadataKey.onboarding,
|
api.UserMetadataKey.onboarding => UserMetadataKey.onboarding,
|
||||||
api.UserMetadataKey.preferences => UserMetadataKey.preferences,
|
api.UserMetadataKey.preferences => UserMetadataKey.preferences,
|
||||||
api.UserMetadataKey.license => UserMetadataKey.license,
|
api.UserMetadataKey.license => UserMetadataKey.license,
|
||||||
_ => throw Exception('Unknown UserMetadataKey value: $this'),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on UserAvatarColor {
|
extension on UserAvatarColor {
|
||||||
AvatarColor? toAvatarColor() => AvatarColor.values.firstWhereOrNull((c) => c.name == value);
|
AvatarColor? toAvatarColor() => AvatarColor.values.firstWhereOrNull((c) => c.name == toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on api.AssetEditAction {
|
extension on api.AssetEditAction {
|
||||||
@@ -993,6 +987,5 @@ extension on api.AssetEditAction {
|
|||||||
api.AssetEditAction.crop => AssetEditAction.crop,
|
api.AssetEditAction.crop => AssetEditAction.crop,
|
||||||
api.AssetEditAction.rotate => AssetEditAction.rotate,
|
api.AssetEditAction.rotate => AssetEditAction.rotate,
|
||||||
api.AssetEditAction.mirror => AssetEditAction.mirror,
|
api.AssetEditAction.mirror => AssetEditAction.mirror,
|
||||||
_ => AssetEditAction.other,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
|
|||||||
FocusNode albumTitleTextFieldFocusNode = FocusNode();
|
FocusNode albumTitleTextFieldFocusNode = FocusNode();
|
||||||
FocusNode albumDescriptionTextFieldFocusNode = FocusNode();
|
FocusNode albumDescriptionTextFieldFocusNode = FocusNode();
|
||||||
bool isAlbumTitleTextFieldFocus = false;
|
bool isAlbumTitleTextFieldFocus = false;
|
||||||
|
bool isCreatingAlbum = false;
|
||||||
Set<BaseAsset> selectedAssets = {};
|
Set<BaseAsset> selectedAssets = {};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -48,7 +49,7 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get _canCreateAlbum => albumTitleController.text.trim().isNotEmpty;
|
bool get _canCreateAlbum => albumTitleController.text.trim().isNotEmpty && !isCreatingAlbum;
|
||||||
|
|
||||||
String _getEffectiveTitle() {
|
String _getEffectiveTitle() {
|
||||||
return albumTitleController.text.isNotEmpty
|
return albumTitleController.text.isNotEmpty
|
||||||
@@ -167,7 +168,12 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> createAlbum() async {
|
Future<void> createAlbum() async {
|
||||||
|
if (isCreatingAlbum) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
onBackgroundTapped();
|
onBackgroundTapped();
|
||||||
|
setState(() => isCreatingAlbum = true);
|
||||||
|
|
||||||
final title = _getEffectiveTitle().trim();
|
final title = _getEffectiveTitle().trim();
|
||||||
|
|
||||||
@@ -187,6 +193,10 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
|
|||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ImmichToast.show(context: context, toastType: ToastType.error, msg: 'errors.failed_to_create_album'.t());
|
ImmichToast.show(context: context, toastType: ToastType.error, msg: 'errors.failed_to_create_album'.t());
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => isCreatingAlbum = false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ class _DriftSlideshowPageState extends ConsumerState<DriftSlideshowPage> with Si
|
|||||||
int? _crossfadeFromIndex;
|
int? _crossfadeFromIndex;
|
||||||
int? _crossfadeToIndex;
|
int? _crossfadeToIndex;
|
||||||
int _zoomCycle = 0;
|
int _zoomCycle = 0;
|
||||||
|
bool _disableAnimations = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
initState() {
|
initState() {
|
||||||
@@ -70,6 +71,12 @@ class _DriftSlideshowPageState extends ConsumerState<DriftSlideshowPage> with Si
|
|||||||
unawaited(WakelockPlus.enable());
|
unawaited(WakelockPlus.enable());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
_disableAnimations = MediaQuery.disableAnimationsOf(context);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
dispose() {
|
dispose() {
|
||||||
_timer.cancel();
|
_timer.cancel();
|
||||||
@@ -166,6 +173,11 @@ class _DriftSlideshowPageState extends ConsumerState<DriftSlideshowPage> with Si
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _crossFadeToPage(int page) {
|
void _crossFadeToPage(int page) {
|
||||||
|
if (_disableAnimations) {
|
||||||
|
_pageController.jumpToPage(page);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final previousIndex = _index;
|
final previousIndex = _index;
|
||||||
_pageController.jumpToPage(page);
|
_pageController.jumpToPage(page);
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -273,19 +285,12 @@ class _DriftSlideshowPageState extends ConsumerState<DriftSlideshowPage> with Si
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (asset.isImage) {
|
if (asset.isImage) {
|
||||||
final elapsed = _stopwatch.elapsedMilliseconds;
|
return _SlideshowProgressBar(
|
||||||
final duration = _config.duration * 1000;
|
|
||||||
|
|
||||||
return TweenAnimationBuilder(
|
|
||||||
key: Key(_index.toString()),
|
key: Key(_index.toString()),
|
||||||
tween: Tween<double>(begin: elapsed / duration.toDouble(), end: _paused ? elapsed / duration.toDouble() : 1.0),
|
durationMs: _config.duration * 1000,
|
||||||
duration: Duration(milliseconds: _paused ? 1 : max(duration - elapsed, 1)),
|
elapsedMs: _stopwatch.elapsedMilliseconds,
|
||||||
builder: (context, value, _) => LinearProgressIndicator(
|
paused: _paused,
|
||||||
color: context.colorScheme.primary,
|
color: context.colorScheme.primary,
|
||||||
borderRadius: const BorderRadius.all(Radius.zero),
|
|
||||||
minHeight: 5,
|
|
||||||
value: value,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return LinearProgressIndicator(
|
return LinearProgressIndicator(
|
||||||
@@ -334,6 +339,21 @@ class _DriftSlideshowPageState extends ConsumerState<DriftSlideshowPage> with Si
|
|||||||
final imageProvider = getFullImageProvider(asset, size: context.sizeData);
|
final imageProvider = getFullImageProvider(asset, size: context.sizeData);
|
||||||
|
|
||||||
if (asset.isImage) {
|
if (asset.isImage) {
|
||||||
|
PhotoView buildPhotoView(PhotoViewComputedScale initialScale) => PhotoView(
|
||||||
|
imageProvider: imageProvider,
|
||||||
|
index: index,
|
||||||
|
disableScaleGestures: true,
|
||||||
|
gaplessPlayback: true,
|
||||||
|
filterQuality: FilterQuality.high,
|
||||||
|
initialScale: initialScale,
|
||||||
|
controller: PhotoViewController(),
|
||||||
|
onTapUp: (_, _, _) => _onTapUp(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (_disableAnimations) {
|
||||||
|
return buildPhotoView(scale);
|
||||||
|
}
|
||||||
|
|
||||||
final zoomOut = _zoomCycle.isOdd;
|
final zoomOut = _zoomCycle.isOdd;
|
||||||
final elapsed = _stopwatch.elapsedMilliseconds;
|
final elapsed = _stopwatch.elapsedMilliseconds;
|
||||||
final duration = _config.duration * 1000;
|
final duration = _config.duration * 1000;
|
||||||
@@ -349,16 +369,7 @@ class _DriftSlideshowPageState extends ConsumerState<DriftSlideshowPage> with Si
|
|||||||
: 1.0,
|
: 1.0,
|
||||||
),
|
),
|
||||||
duration: Duration(milliseconds: _paused ? 1 : max(duration - elapsed, 1)),
|
duration: Duration(milliseconds: _paused ? 1 : max(duration - elapsed, 1)),
|
||||||
builder: (context, value, _) => PhotoView(
|
builder: (context, value, _) => buildPhotoView(scale * (1.0 + value * _kenBurnsZoom)),
|
||||||
imageProvider: imageProvider,
|
|
||||||
index: index,
|
|
||||||
disableScaleGestures: true,
|
|
||||||
gaplessPlayback: true,
|
|
||||||
filterQuality: FilterQuality.high,
|
|
||||||
initialScale: scale * (1.0 + value * _kenBurnsZoom),
|
|
||||||
controller: PhotoViewController(),
|
|
||||||
onTapUp: (_, _, _) => _onTapUp(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final status = ref.watch(videoPlayerProvider(asset.heroTag).select((s) => s.status));
|
final status = ref.watch(videoPlayerProvider(asset.heroTag).select((s) => s.status));
|
||||||
@@ -463,3 +474,75 @@ class _DriftSlideshowPageState extends ConsumerState<DriftSlideshowPage> with Si
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Progress bar for image slides, driven by an explicit [AnimationController].
|
||||||
|
///
|
||||||
|
/// [TweenAnimationBuilder] creates its controller internally with the default
|
||||||
|
/// [AnimationBehavior.normal], which makes it run ~20x too fast while the system
|
||||||
|
/// "reduce motion" setting is on (flutter/flutter#164287). This owns its
|
||||||
|
/// controller so it can use [AnimationBehavior.preserve] and animate at the real
|
||||||
|
/// slide duration regardless of that setting.
|
||||||
|
class _SlideshowProgressBar extends StatefulWidget {
|
||||||
|
final int durationMs;
|
||||||
|
final int elapsedMs;
|
||||||
|
final bool paused;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
const _SlideshowProgressBar({
|
||||||
|
super.key,
|
||||||
|
required this.durationMs,
|
||||||
|
required this.elapsedMs,
|
||||||
|
required this.paused,
|
||||||
|
required this.color,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_SlideshowProgressBar> createState() => _SlideshowProgressBarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SlideshowProgressBarState extends State<_SlideshowProgressBar> with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: Duration(milliseconds: widget.durationMs),
|
||||||
|
animationBehavior: AnimationBehavior.preserve,
|
||||||
|
)..value = (widget.elapsedMs / widget.durationMs).clamp(0.0, 1.0);
|
||||||
|
if (!widget.paused) {
|
||||||
|
_controller.forward();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(_SlideshowProgressBar oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.durationMs != oldWidget.durationMs) {
|
||||||
|
_controller.duration = Duration(milliseconds: widget.durationMs);
|
||||||
|
}
|
||||||
|
if (widget.paused != oldWidget.paused) {
|
||||||
|
widget.paused ? _controller.stop() : _controller.forward();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: _controller,
|
||||||
|
builder: (context, _) => LinearProgressIndicator(
|
||||||
|
color: widget.color,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.zero),
|
||||||
|
minHeight: 5,
|
||||||
|
value: _controller.value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -886,7 +886,6 @@ class _QuickLinkList extends StatelessWidget {
|
|||||||
_QuickLink(
|
_QuickLink(
|
||||||
title: context.t.recently_added,
|
title: context.t.recently_added,
|
||||||
icon: Icons.upload_outlined,
|
icon: Icons.upload_outlined,
|
||||||
isTop: true,
|
|
||||||
onTap: () => context.pushRoute(const DriftRecentlyAddedRoute()),
|
onTap: () => context.pushRoute(const DriftRecentlyAddedRoute()),
|
||||||
),
|
),
|
||||||
_QuickLink(
|
_QuickLink(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||||
@@ -154,12 +155,8 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.count == 0) {
|
// Only report the failure when nothing was added; if some succeeded we show "added".
|
||||||
ImmichToast.show(
|
if (result.count > 0) {
|
||||||
context: context,
|
|
||||||
msg: 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {'album': album.name}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}),
|
msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}),
|
||||||
@@ -167,6 +164,17 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
|
|||||||
|
|
||||||
// Refresh the "Appears in" list on the asset's info panel.
|
// Refresh the "Appears in" list on the asset's info panel.
|
||||||
ref.invalidate(albumsContainingAssetProvider(latest.remoteId!));
|
ref.invalidate(albumsContainingAssetProvider(latest.remoteId!));
|
||||||
|
} else if (result.failedCount > 0) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: 'assets_cannot_be_added_to_album_count'.t(context: context, args: {'count': result.failedCount}),
|
||||||
|
toastType: ToastType.error,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {'album': album.name}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
import 'package:immich_mobile/domain/models/events.model.dart';
|
||||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
@@ -59,13 +61,19 @@ class DeleteActionButton extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (source == ActionSource.viewer) {
|
final currentAsset = ref.read(assetViewerProvider).currentAsset;
|
||||||
EventStream.shared.emit(const ViewerReloadAssetEvent());
|
final stackIndex = ref.read(assetViewerProvider).stackIndex;
|
||||||
}
|
|
||||||
|
|
||||||
final result = await ref.read(actionProvider.notifier).trashRemoteAndDeleteLocal(source);
|
final result = await ref.read(actionProvider.notifier).trashRemoteAndDeleteLocal(source);
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
|
if (source == ActionSource.viewer && result.success) {
|
||||||
|
final shouldRefreshStack = currentAsset is RemoteAsset && currentAsset.stackId != null;
|
||||||
|
EventStream.shared.emit(
|
||||||
|
shouldRefreshStack ? ViewerStackAssetDeletedEvent(stackIndex: stackIndex) : const ViewerReloadAssetEvent(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final successMessage = 'delete_action_prompt'.t(context: context, args: {'count': result.count.toString()});
|
final successMessage = 'delete_action_prompt'.t(context: context, args: {'count': result.count.toString()});
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import 'package:fluttertoast/fluttertoast.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/settings_key.dart';
|
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/generated/translations.g.dart';
|
import 'package:immich_mobile/generated/translations.g.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
@@ -129,12 +128,6 @@ class ShareActionButton extends ConsumerWidget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ref.read(settingsProvider).write(SettingsKey.shareFileType, fileType);
|
|
||||||
|
|
||||||
if (!context.mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _share(context, ref, fileType);
|
await _share(context, ref, fileType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -413,7 +413,8 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
final showAssetStack = ref.watch(timelineServiceProvider.select((s) => s.origin != TimelineOrigin.trash));
|
final showAssetStack = ref.watch(timelineServiceProvider.select((s) => s.origin != TimelineOrigin.trash));
|
||||||
final stackChildren = showAssetStack ? ref.watch(stackChildrenNotifier(asset)).valueOrNull : null;
|
final stackChildren = showAssetStack ? ref.watch(stackChildrenNotifier(asset)).valueOrNull : null;
|
||||||
if (stackChildren != null && stackChildren.isNotEmpty) {
|
if (stackChildren != null && stackChildren.isNotEmpty) {
|
||||||
displayAsset = stackChildren.elementAt(stackIndex);
|
final safeStackIndex = stackIndex.clamp(0, stackChildren.length - 1);
|
||||||
|
displayAsset = stackChildren.elementAt(safeStackIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
final isCurrent = currentAsset != null && currentAsset.refersToSameAsset(displayAsset);
|
final isCurrent = currentAsset != null && currentAsset.refersToSameAsset(displayAsset);
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ class StackChildrenNotifier extends AutoDisposeFamilyAsyncNotifier<List<RemoteAs
|
|||||||
|
|
||||||
return ref.watch(assetServiceProvider).getStack(asset);
|
return ref.watch(assetServiceProvider).getStack(asset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setStack(List<RemoteAsset> stack) {
|
||||||
|
state = AsyncData(stack);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final stackChildrenNotifier = AsyncNotifierProvider.autoDispose
|
final stackChildrenNotifier = AsyncNotifierProvider.autoDispose
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
@@ -174,6 +175,12 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The viewer is closing; don't flip the current asset now. Flipping it swaps
|
||||||
|
// the grid tile hero keys mid pop and animates the close on two tiles (#23779).
|
||||||
|
if (!mounted || !(ModalRoute.of(context)?.isActive ?? true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
AssetViewer._setAsset(ref, asset);
|
AssetViewer._setAsset(ref, asset);
|
||||||
_preloader.preload(index, context.sizeData);
|
_preloader.preload(index, context.sizeData);
|
||||||
_handleCasting();
|
_handleCasting();
|
||||||
@@ -215,6 +222,8 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
_onTimelineReloadEvent();
|
_onTimelineReloadEvent();
|
||||||
case ViewerReloadAssetEvent():
|
case ViewerReloadAssetEvent():
|
||||||
_onViewerReloadEvent();
|
_onViewerReloadEvent();
|
||||||
|
case ViewerStackAssetDeletedEvent event:
|
||||||
|
_onViewerStackAssetDeletedEvent(event);
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -230,6 +239,33 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
_onAssetChanged(target);
|
_onAssetChanged(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _onViewerStackAssetDeletedEvent(ViewerStackAssetDeletedEvent event) async {
|
||||||
|
final timelineAsset = ref.read(timelineServiceProvider).getAssetSafe(_currentPage);
|
||||||
|
if (timelineAsset == null) {
|
||||||
|
_onViewerReloadEvent();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final stackProvider = stackChildrenNotifier(timelineAsset);
|
||||||
|
|
||||||
|
ref.invalidate(stackProvider);
|
||||||
|
final stack = await ref.read(stackProvider.future);
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stack.isEmpty) {
|
||||||
|
_onViewerReloadEvent();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final targetIndex = math.min(event.stackIndex, stack.length - 1);
|
||||||
|
ref.read(assetViewerProvider.notifier)
|
||||||
|
..setAsset(stack[targetIndex])
|
||||||
|
..setStackIndex(targetIndex);
|
||||||
|
}
|
||||||
|
|
||||||
void _onTimelineReloadEvent() {
|
void _onTimelineReloadEvent() {
|
||||||
final timelineService = ref.read(timelineServiceProvider);
|
final timelineService = ref.read(timelineServiceProvider);
|
||||||
final totalAssets = timelineService.totalAssets;
|
final totalAssets = timelineService.totalAssets;
|
||||||
@@ -246,6 +282,11 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
if (index != _currentPage) {
|
if (index != _currentPage) {
|
||||||
_pageController.jumpToPage(index);
|
_pageController.jumpToPage(index);
|
||||||
_onAssetChanged(index);
|
_onAssetChanged(index);
|
||||||
|
} else if (currentAsset is RemoteAsset && currentAsset.stackId != null && assetIndex == null) {
|
||||||
|
final timelineAsset = timelineService.getAssetSafe(index);
|
||||||
|
if (timelineAsset is! RemoteAsset || currentAsset.stackId != timelineAsset.stackId) {
|
||||||
|
_onAssetChanged(index);
|
||||||
|
}
|
||||||
} else if (currentAsset != null && assetIndex == null) {
|
} else if (currentAsset != null && assetIndex == null) {
|
||||||
_onAssetChanged(index);
|
_onAssetChanged(index);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ class ViewerBottomBar extends ConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
OcrToggleButton(asset: asset),
|
if (asset.isImage) OcrToggleButton(asset: asset),
|
||||||
if (asset.isVideo) VideoControls(videoPlayerName: asset.heroTag),
|
if (asset.isVideo) VideoControls(videoPlayerName: asset.heroTag),
|
||||||
if (!isReadonlyModeEnabled)
|
if (!isReadonlyModeEnabled)
|
||||||
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions),
|
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions),
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
|
|||||||
}
|
}
|
||||||
case AppLifecycleState.paused:
|
case AppLifecycleState.paused:
|
||||||
_shouldPlayOnForeground = await _controller?.isPlaying() ?? true;
|
_shouldPlayOnForeground = await _controller?.isPlaying() ?? true;
|
||||||
if (_shouldPlayOnForeground) {
|
if (_shouldPlayOnForeground && mounted) {
|
||||||
await _notifier.pause();
|
await _notifier.pause();
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -268,10 +268,13 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _notifier.load(source);
|
// Grab refs to prevent reading after dispose
|
||||||
final loopVideo = ref.read(appConfigProvider).viewer.loopVideo;
|
final loopVideo = ref.read(appConfigProvider).viewer.loopVideo;
|
||||||
await _notifier.setLoop(!widget.asset.isMotionPhoto && loopVideo);
|
final localNotifier = _notifier;
|
||||||
await _notifier.setVolume(1);
|
|
||||||
|
await localNotifier.load(source);
|
||||||
|
await localNotifier.setLoop(!widget.asset.isMotionPhoto && loopVideo);
|
||||||
|
await localNotifier.setVolume(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initController(NativeVideoPlayerController nc) {
|
void _initController(NativeVideoPlayerController nc) {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class FavoriteBottomSheet extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final remoteAssets = selectedAssets.whereType<RemoteAsset>();
|
final remoteAssets = selectedAssets.whereType<RemoteAsset>();
|
||||||
final addedCount = await ref
|
final result = await ref
|
||||||
.read(remoteAlbumProvider.notifier)
|
.read(remoteAlbumProvider.notifier)
|
||||||
.addAssets(album.id, remoteAssets.map((e) => e.id).toList());
|
.addAssets(album.id, remoteAssets.map((e) => e.id).toList());
|
||||||
|
|
||||||
@@ -52,15 +52,22 @@ class FavoriteBottomSheet extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (addedCount != remoteAssets.length) {
|
// Only report the failure when nothing was added; if some succeeded we show "added".
|
||||||
|
if (result.added > 0) {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: 'add_to_album_bottom_sheet_already_exists'.t(args: {"album": album.name}),
|
msg: 'add_to_album_bottom_sheet_added'.t(args: {"album": album.name}),
|
||||||
|
);
|
||||||
|
} else if (result.failed > 0) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: 'assets_cannot_be_added_to_album_count'.t(context: context, args: {'count': result.failed}),
|
||||||
|
toastType: ToastType.error,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: 'add_to_album_bottom_sheet_added'.t(args: {"album": album.name}),
|
msg: 'add_to_album_bottom_sheet_already_exists'.t(args: {"album": album.name}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,76 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
class PartnerDetailBottomSheet extends ConsumerWidget {
|
class PartnerDetailBottomSheet extends ConsumerStatefulWidget {
|
||||||
const PartnerDetailBottomSheet({super.key});
|
const PartnerDetailBottomSheet({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
ConsumerState<PartnerDetailBottomSheet> createState() => _PartnerDetailBottomSheetState();
|
||||||
return const BaseBottomSheet(
|
}
|
||||||
|
|
||||||
|
class _PartnerDetailBottomSheetState extends ConsumerState<PartnerDetailBottomSheet> {
|
||||||
|
late final DraggableScrollableController sheetController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
sheetController = DraggableScrollableController();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
sheetController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Future<void> addToAlbum(RemoteAlbum album) async {
|
||||||
|
final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.timeline, album);
|
||||||
|
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
ImmichToast.show(context: context, msg: 'scaffold_body_error_occurred'.tr(), toastType: ToastType.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: result.count == 0
|
||||||
|
? 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {'album': album.name})
|
||||||
|
: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> onKeyboardExpand() {
|
||||||
|
return sheetController.animateTo(0.85, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
return BaseBottomSheet(
|
||||||
|
controller: sheetController,
|
||||||
initialChildSize: 0.25,
|
initialChildSize: 0.25,
|
||||||
maxChildSize: 0.4,
|
maxChildSize: 0.85,
|
||||||
shouldCloseOnMinExtent: false,
|
shouldCloseOnMinExtent: false,
|
||||||
actions: [
|
actions: const [
|
||||||
ShareActionButton(source: ActionSource.timeline),
|
ShareActionButton(source: ActionSource.timeline),
|
||||||
DownloadActionButton(source: ActionSource.timeline),
|
DownloadActionButton(source: ActionSource.timeline),
|
||||||
],
|
],
|
||||||
|
slivers: [
|
||||||
|
const AddToAlbumHeader(),
|
||||||
|
AlbumSelector(onAlbumSelected: addToAlbum, onKeyboardExpanded: onKeyboardExpand),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import 'package:immich_mobile/presentation/widgets/bottom_sheet/map_bottom_sheet
|
|||||||
import 'package:immich_mobile/presentation/widgets/map/map.state.dart';
|
import 'package:immich_mobile/presentation/widgets/map/map.state.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/map/map_utils.dart';
|
import 'package:immich_mobile/presentation/widgets/map/map_utils.dart';
|
||||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
|
||||||
import 'package:immich_mobile/utils/async_mutex.dart';
|
import 'package:immich_mobile/utils/async_mutex.dart';
|
||||||
import 'package:immich_mobile/utils/debounce.dart';
|
import 'package:immich_mobile/utils/debounce.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
@@ -133,8 +132,7 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
|||||||
// When the AssetViewer is open, the DriftMap route stays alive in the background.
|
// When the AssetViewer is open, the DriftMap route stays alive in the background.
|
||||||
// If we continue to update bounds, the map-scoped timeline service gets recreated and the previous one disposed,
|
// If we continue to update bounds, the map-scoped timeline service gets recreated and the previous one disposed,
|
||||||
// which can invalidate the TimelineService instance that was passed into AssetViewerRoute (causing "loading forever").
|
// which can invalidate the TimelineService instance that was passed into AssetViewerRoute (causing "loading forever").
|
||||||
final currentRoute = ref.read(currentRouteNameProvider);
|
if (ref.read(isAssetViewerOpenProvider)) {
|
||||||
if (currentRoute == AssetViewerRoute.name) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,6 +181,11 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
ref.listen<bool>(isAssetViewerOpenProvider, (previous, current) {
|
||||||
|
if (previous == true && !current) {
|
||||||
|
_debouncer.run(() => setBounds(forceReload: true));
|
||||||
|
}
|
||||||
|
});
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
_Map(initialLocation: widget.initialLocation, onMapCreated: onMapCreated, onMapReady: onMapReady),
|
_Map(initialLocation: widget.initialLocation, onMapCreated: onMapCreated, onMapReady: onMapReady),
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:background_downloader/background_downloader.dart';
|
import 'package:background_downloader/background_downloader.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/models/download/download_state.model.dart';
|
import 'package:immich_mobile/models/download/download_state.model.dart';
|
||||||
import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
|
|
||||||
import 'package:immich_mobile/services/download.service.dart';
|
import 'package:immich_mobile/services/download.service.dart';
|
||||||
|
|
||||||
class DownloadStateNotifier extends StateNotifier<DownloadState> {
|
class DownloadStateNotifier extends StateNotifier<DownloadState> {
|
||||||
@@ -17,79 +14,9 @@ class DownloadStateNotifier extends StateNotifier<DownloadState> {
|
|||||||
taskProgress: <String, DownloadInfo>{},
|
taskProgress: <String, DownloadInfo>{},
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
_downloadService.onImageDownloadStatus = _downloadImageCallback;
|
|
||||||
_downloadService.onVideoDownloadStatus = _downloadVideoCallback;
|
|
||||||
_downloadService.onLivePhotoDownloadStatus = _downloadLivePhotoCallback;
|
|
||||||
_downloadService.onTaskProgress = _taskProgressCallback;
|
_downloadService.onTaskProgress = _taskProgressCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateDownloadStatus(String taskId, TaskStatus status) {
|
|
||||||
if (status == TaskStatus.canceled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
state = state.copyWith(
|
|
||||||
taskProgress: <String, DownloadInfo>{}
|
|
||||||
..addAll(state.taskProgress)
|
|
||||||
..addAll({
|
|
||||||
taskId: DownloadInfo(
|
|
||||||
progress: state.taskProgress[taskId]?.progress ?? 0,
|
|
||||||
fileName: state.taskProgress[taskId]?.fileName ?? '',
|
|
||||||
status: status,
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Download live photo callback
|
|
||||||
void _downloadLivePhotoCallback(TaskStatusUpdate update) {
|
|
||||||
_updateDownloadStatus(update.task.taskId, update.status);
|
|
||||||
|
|
||||||
switch (update.status) {
|
|
||||||
case TaskStatus.complete:
|
|
||||||
if (update.task.metaData.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final livePhotosId = LivePhotosMetadata.fromJson(update.task.metaData).id;
|
|
||||||
_downloadService.saveLivePhotos(update.task, livePhotosId);
|
|
||||||
_onDownloadComplete(update.task.taskId);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Download image callback
|
|
||||||
void _downloadImageCallback(TaskStatusUpdate update) {
|
|
||||||
_updateDownloadStatus(update.task.taskId, update.status);
|
|
||||||
|
|
||||||
switch (update.status) {
|
|
||||||
case TaskStatus.complete:
|
|
||||||
_downloadService.saveImageWithPath(update.task);
|
|
||||||
_onDownloadComplete(update.task.taskId);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Download video callback
|
|
||||||
void _downloadVideoCallback(TaskStatusUpdate update) {
|
|
||||||
_updateDownloadStatus(update.task.taskId, update.status);
|
|
||||||
|
|
||||||
switch (update.status) {
|
|
||||||
case TaskStatus.complete:
|
|
||||||
_downloadService.saveVideo(update.task);
|
|
||||||
_onDownloadComplete(update.task.taskId);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _taskProgressCallback(TaskProgressUpdate update) {
|
void _taskProgressCallback(TaskProgressUpdate update) {
|
||||||
// Ignore if the task is canceled or completed
|
// Ignore if the task is canceled or completed
|
||||||
if (update.progress == -2 || update.progress == -1) {
|
if (update.progress == -2 || update.progress == -1) {
|
||||||
@@ -110,20 +37,6 @@ class DownloadStateNotifier extends StateNotifier<DownloadState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onDownloadComplete(String id) {
|
|
||||||
Future.delayed(const Duration(seconds: 2), () {
|
|
||||||
state = state.copyWith(
|
|
||||||
taskProgress: <String, DownloadInfo>{}
|
|
||||||
..addAll(state.taskProgress)
|
|
||||||
..remove(id),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (state.taskProgress.isEmpty) {
|
|
||||||
state = state.copyWith(showProgress: false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void cancelDownload(String id) async {
|
void cancelDownload(String id) async {
|
||||||
final isCanceled = await _downloadService.cancelDownload(id);
|
final isCanceled = await _downloadService.cancelDownload(id);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:background_downloader/background_downloader.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
@@ -10,7 +9,6 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
|||||||
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/asset.service.dart';
|
import 'package:immich_mobile/domain/services/asset.service.dart';
|
||||||
import 'package:immich_mobile/domain/services/remote_album.service.dart';
|
import 'package:immich_mobile/domain/services/remote_album.service.dart';
|
||||||
import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
|
|
||||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart';
|
import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
@@ -23,7 +21,6 @@ import 'package:immich_mobile/providers/user.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/services/action.service.dart';
|
import 'package:immich_mobile/services/action.service.dart';
|
||||||
import 'package:immich_mobile/services/download.service.dart';
|
|
||||||
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
||||||
import 'package:immich_mobile/utils/semver.dart';
|
import 'package:immich_mobile/utils/semver.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart';
|
import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart';
|
||||||
@@ -37,18 +34,25 @@ class ActionResult {
|
|||||||
final bool success;
|
final bool success;
|
||||||
final String? error;
|
final String? error;
|
||||||
final List<String> remoteAssetIds;
|
final List<String> remoteAssetIds;
|
||||||
|
final int failedCount;
|
||||||
|
|
||||||
const ActionResult({required this.count, required this.success, this.error, this.remoteAssetIds = const []});
|
const ActionResult({
|
||||||
|
required this.count,
|
||||||
|
required this.success,
|
||||||
|
this.error,
|
||||||
|
this.remoteAssetIds = const [],
|
||||||
|
this.failedCount = 0,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'ActionResult(count: $count, success: $success, error: $error, remoteAssetIds: $remoteAssetIds)';
|
String toString() =>
|
||||||
|
'ActionResult(count: $count, success: $success, error: $error, remoteAssetIds: $remoteAssetIds, failedCount: $failedCount)';
|
||||||
}
|
}
|
||||||
|
|
||||||
class ActionNotifier extends Notifier<void> {
|
class ActionNotifier extends Notifier<void> {
|
||||||
final Logger _logger = Logger('ActionNotifier');
|
final Logger _logger = Logger('ActionNotifier');
|
||||||
late ActionService _service;
|
late ActionService _service;
|
||||||
late ForegroundUploadService _foregroundUploadService;
|
late ForegroundUploadService _foregroundUploadService;
|
||||||
late DownloadService _downloadService;
|
|
||||||
late AssetService _assetService;
|
late AssetService _assetService;
|
||||||
|
|
||||||
ActionNotifier() : super();
|
ActionNotifier() : super();
|
||||||
@@ -58,29 +62,6 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
_foregroundUploadService = ref.watch(foregroundUploadServiceProvider);
|
_foregroundUploadService = ref.watch(foregroundUploadServiceProvider);
|
||||||
_service = ref.watch(actionServiceProvider);
|
_service = ref.watch(actionServiceProvider);
|
||||||
_assetService = ref.watch(assetServiceProvider);
|
_assetService = ref.watch(assetServiceProvider);
|
||||||
_downloadService = ref.watch(downloadServiceProvider);
|
|
||||||
_downloadService.onImageDownloadStatus = _downloadImageCallback;
|
|
||||||
_downloadService.onVideoDownloadStatus = _downloadVideoCallback;
|
|
||||||
_downloadService.onLivePhotoDownloadStatus = _downloadLivePhotoCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _downloadImageCallback(TaskStatusUpdate update) {
|
|
||||||
if (update.status == TaskStatus.complete) {
|
|
||||||
_downloadService.saveImageWithPath(update.task);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _downloadVideoCallback(TaskStatusUpdate update) {
|
|
||||||
if (update.status == TaskStatus.complete) {
|
|
||||||
_downloadService.saveVideo(update.task);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _downloadLivePhotoCallback(TaskStatusUpdate update) async {
|
|
||||||
if (update.status == TaskStatus.complete) {
|
|
||||||
final livePhotosId = LivePhotosMetadata.fromJson(update.task.metaData).id;
|
|
||||||
unawaited(_downloadService.saveLivePhotos(update.task, livePhotosId));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> _getRemoteIdsForSource(ActionSource source) {
|
List<String> _getRemoteIdsForSource(ActionSource source) {
|
||||||
@@ -393,9 +374,12 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
final albumNotifier = ref.read(remoteAlbumProvider.notifier);
|
final albumNotifier = ref.read(remoteAlbumProvider.notifier);
|
||||||
|
|
||||||
int addedRemote = 0;
|
int addedRemote = 0;
|
||||||
|
int failedRemote = 0;
|
||||||
if (remoteIds.isNotEmpty) {
|
if (remoteIds.isNotEmpty) {
|
||||||
try {
|
try {
|
||||||
addedRemote = await albumNotifier.addAssets(album.id, remoteIds);
|
final result = await albumNotifier.addAssets(album.id, remoteIds);
|
||||||
|
addedRemote = result.added;
|
||||||
|
failedRemote = result.failed;
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Failed to add assets to album ${album.id}', error, stack);
|
_logger.severe('Failed to add assets to album ${album.id}', error, stack);
|
||||||
return ActionResult(count: 0, success: false, error: error.toString());
|
return ActionResult(count: 0, success: false, error: error.toString());
|
||||||
@@ -409,7 +393,7 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (localAssets.isEmpty) {
|
if (localAssets.isEmpty) {
|
||||||
return ActionResult(count: addedRemote, success: true);
|
return ActionResult(count: addedRemote, success: true, failedCount: failedRemote);
|
||||||
}
|
}
|
||||||
|
|
||||||
final uploadResult = await upload(
|
final uploadResult = await upload(
|
||||||
@@ -424,6 +408,7 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
count: addedRemote + uploadResult.count,
|
count: addedRemote + uploadResult.count,
|
||||||
success: uploadResult.success,
|
success: uploadResult.success,
|
||||||
error: uploadResult.error,
|
error: uploadResult.error,
|
||||||
|
failedCount: failedRemote,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -200,12 +200,12 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
|||||||
return _remoteAlbumService.getAssets(albumId);
|
return _remoteAlbumService.getAssets(albumId);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> addAssets(String albumId, List<String> assetIds) async {
|
Future<({int added, int failed})> addAssets(String albumId, List<String> assetIds) async {
|
||||||
final added = await _remoteAlbumService.addAssets(albumId: albumId, assetIds: assetIds);
|
final result = await _remoteAlbumService.addAssets(albumId: albumId, assetIds: assetIds);
|
||||||
if (added > 0) {
|
if (result.added > 0) {
|
||||||
await _refreshAlbumInState(albumId);
|
await _refreshAlbumInState(albumId);
|
||||||
}
|
}
|
||||||
return added;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Links a freshly-uploaded local asset to an album using its new remote ID,
|
/// Links a freshly-uploaded local asset to an album using its new remote ID,
|
||||||
@@ -313,9 +313,9 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final remoteAlbumDateRangeProvider = FutureProvider.family<(DateTime, DateTime), String>((ref, albumId) async {
|
final remoteAlbumDateRangeProvider = StreamProvider.autoDispose.family<(DateTime, DateTime), String>((ref, albumId) {
|
||||||
final service = ref.watch(remoteAlbumServiceProvider);
|
final service = ref.watch(remoteAlbumServiceProvider);
|
||||||
return service.getDateRange(albumId);
|
return service.watchDateRange(albumId);
|
||||||
});
|
});
|
||||||
|
|
||||||
final remoteAlbumSharedUsersProvider = FutureProvider.autoDispose.family<List<UserDto>, String>((ref, albumId) async {
|
final remoteAlbumSharedUsersProvider = FutureProvider.autoDispose.family<List<UserDto>, String>((ref, albumId) async {
|
||||||
|
|||||||
@@ -1,7 +1,19 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
bool isRouteInStack(Ref ref, String routeName) {
|
||||||
|
final router = ref.watch(appRouterProvider);
|
||||||
|
void onChange() => ref.invalidateSelf();
|
||||||
|
router.addListener(onChange);
|
||||||
|
ref.onDispose(() => router.removeListener(onChange));
|
||||||
|
return router.stackData.any((route) => route.name == routeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
final inLockedViewProvider = Provider<bool>((ref) => isRouteInStack(ref, DriftLockedFolderRoute.name));
|
||||||
|
final isAssetViewerOpenProvider = Provider<bool>((ref) => isRouteInStack(ref, AssetViewerRoute.name));
|
||||||
|
|
||||||
final inLockedViewProvider = StateProvider<bool>((ref) => false);
|
|
||||||
final currentRouteNameProvider = StateProvider<String?>((ref) => null);
|
final currentRouteNameProvider = StateProvider<String?>((ref) => null);
|
||||||
final previousRouteNameProvider = StateProvider<String?>((ref) => null);
|
final previousRouteNameProvider = StateProvider<String?>((ref) => null);
|
||||||
final previousRouteDataProvider = StateProvider<RouteSettings?>((ref) => null);
|
final previousRouteDataProvider = StateProvider<RouteSettings?>((ref) => null);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user