Compare commits
114
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8119a5ee22 | ||
|
|
54881128d6 | ||
|
|
d15be0dd83 | ||
|
|
47cc38061e | ||
|
|
db411a4497 | ||
|
|
f1a09c1389 | ||
|
|
c8222bba94 | ||
|
|
af66681b99 | ||
|
|
a93bbdf3e7 | ||
|
|
1707635743 | ||
|
|
7a6123f8e6 | ||
|
|
4960eddaf3 | ||
|
|
d35a6613e6 | ||
|
|
f96816b55f | ||
|
|
eca952720a | ||
|
|
c482c1e53e | ||
|
|
75a1d7c5c8 | ||
|
|
5630e541e3 | ||
|
|
27ccbee82d | ||
|
|
3458767d66 | ||
|
|
cd28300f94 | ||
|
|
d8030f3d14 | ||
|
|
0ff6470791 | ||
|
|
717152b191 | ||
|
|
06dbcf7c84 | ||
|
|
9767ea9c02 | ||
|
|
071b8db397 | ||
|
|
1c9813e130 | ||
|
|
6569aad605 | ||
|
|
a2794f0986 | ||
|
|
6ffc365b75 | ||
|
|
dc19d3a259 | ||
|
|
f2a502448d | ||
|
|
a3ffe3b3c3 | ||
|
|
8125af4859 | ||
|
|
e5b4dd1041 | ||
|
|
621504fcda | ||
|
|
919a4fd9ef | ||
|
|
2ed9207c3f | ||
|
|
c48394aa6e | ||
|
|
86c8b9e04d | ||
|
|
1190d0cc80 | ||
|
|
9669f7cc6e | ||
|
|
a5b2dcb3c2 | ||
|
|
f94fbfe14e | ||
|
|
70ef2884f0 | ||
|
|
918d66fbdb | ||
|
|
5d1167a36c | ||
|
|
e3bf54e2ba | ||
|
|
d17640a283 | ||
|
|
9e889bd4dc | ||
|
|
ca4d8d5a7a | ||
|
|
5f54700344 | ||
|
|
0991f506bb | ||
|
|
fa77f168a0 | ||
|
|
3aa65809b1 | ||
|
|
9a66f16102 | ||
|
|
493870ad7b | ||
|
|
1e20bdd74e | ||
|
|
99a7830f6c | ||
|
|
e04e70db2e | ||
|
|
75517057d0 | ||
|
|
3efc91ff9a | ||
|
|
7c07c001cd | ||
|
|
f1f7f82e91 | ||
|
|
b3bd5d1fd1 | ||
|
|
ad9b368cb8 | ||
|
|
7c78b00df6 | ||
|
|
4dfb6e88af | ||
|
|
744797e59d | ||
|
|
689105c949 | ||
|
|
1332a30630 | ||
|
|
648e5608b7 | ||
|
|
42dffaa0c8 | ||
|
|
ec3359f1cb | ||
|
|
235e57bf77 | ||
|
|
630240971f | ||
|
|
44e312cfba | ||
|
|
67c158a0ab | ||
|
|
1554f0d439 | ||
|
|
3e250d7878 | ||
|
|
a6d63285b7 | ||
|
|
c1552140b0 | ||
|
|
238617e566 | ||
|
|
3f3671fbe5 | ||
|
|
01ba579766 | ||
|
|
e2864b6664 | ||
|
|
1756d70fda | ||
|
|
b12be9119f | ||
|
|
e66d5a70e7 | ||
|
|
1a81902a36 | ||
|
|
ec5b4eb939 | ||
|
|
3457823b5b | ||
|
|
ffbe3d1681 | ||
|
|
5c0f0b498a | ||
|
|
c46f748412 | ||
|
|
59be6583b8 | ||
|
|
aeefb962d4 | ||
|
|
f80be1030b | ||
|
|
7df744b684 | ||
|
|
c66634fcfd | ||
|
|
d92248c664 | ||
|
|
67152772d2 | ||
|
|
564b720bce | ||
|
|
6aaebd167d | ||
|
|
2d236024bc | ||
|
|
e09b352067 | ||
|
|
4c89b0f7df | ||
|
|
b020f12f41 | ||
|
|
43848a7d68 | ||
|
|
9e96fdc627 | ||
|
|
434b7efd66 | ||
|
|
814cc14f88 | ||
|
|
59bf69f1d5 |
@@ -9,6 +9,8 @@ body:
|
|||||||
Stick to only a single feature per request. If you list multiple different features at once,
|
Stick to only a single feature per request. If you list multiple different features at once,
|
||||||
your request will be closed.
|
your request will be closed.
|
||||||
|
|
||||||
|
Please read our [contributing guidelines](https://github.com/immich-app/immich/blob/main/CONTRIBUTING.md) before opening a feature request.
|
||||||
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: I have searched the existing feature requests, both open and closed, to make sure this is not a duplicate request.
|
label: I have searched the existing feature requests, both open and closed, to make sure this is not a duplicate request.
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
name: Report an issue with Immich
|
name: Report an issue with Immich
|
||||||
description: Report an issue with Immich
|
description: Report an issue with Immich
|
||||||
body:
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Please read our [contributing guidelines](https://github.com/immich-app/immich/blob/main/CONTRIBUTING.md) before opening an issue.
|
||||||
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: I have searched the existing issues, both open and closed, to make sure this is not a duplicate report.
|
label: I have searched the existing issues, both open and closed, to make sure this is not a duplicate report.
|
||||||
|
|||||||
@@ -26,10 +26,17 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
BODY: ${{ github.event.pull_request.body }}
|
BODY: ${{ github.event.pull_request.body }}
|
||||||
run: |
|
run: |
|
||||||
|
BODY=$(printf '%s\n' "$BODY" | tr -d '\r' | perl -0pe 's/<!--.*?-->//gs')
|
||||||
|
section() { printf '%s\n' "$BODY" | awk -v h="$1" 'index($0, h) == 1 { f = 1; next } /^## / { f = 0 } f'; }
|
||||||
|
|
||||||
OK=true
|
OK=true
|
||||||
while IFS= read -r header; do
|
while IFS= read -r header; do
|
||||||
printf '%s\n' "$BODY" | grep -qF "$header" || OK=false
|
printf '%s\n' "$BODY" | grep -qF "$header" || OK=false
|
||||||
done < <(sed '/<!--/,/-->/d' .github/pull_request_template.md | grep "^## ")
|
done < <(perl -0pe 's/<!--.*?-->//gs' .github/pull_request_template.md | grep "^## ")
|
||||||
|
|
||||||
|
section "## Checklist" | grep -qE '^\s*- \[[xX]\]' || OK=false
|
||||||
|
[[ "$(section "## Please describe to which degree" | tr -d '[:space:]')" =~ ^(\.\.\.)?$ ]] && OK=false
|
||||||
|
|
||||||
echo "uses_template=$OK" | tee --append "$GITHUB_OUTPUT"
|
echo "uses_template=$OK" | tee --append "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
close_template:
|
close_template:
|
||||||
@@ -51,7 +58,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
gh api graphql \
|
gh api graphql \
|
||||||
-f prId="$NODE_ID" \
|
-f prId="$NODE_ID" \
|
||||||
-f body="This PR has been automatically closed as the description doesn't follow [our template](https://github.com/immich-app/immich/blob/main/.github/pull_request_template.md). After you edit it to match the template, the PR will automatically be reopened." \
|
-f body="This PR has been automatically closed as the description doesn't follow [our template](https://github.com/immich-app/immich/blob/main/.github/pull_request_template.md). Please make sure every section is present and filled in. After you edit it to match the template, the PR will automatically be reopened." \
|
||||||
-f query='
|
-f query='
|
||||||
mutation CommentAndClosePR($prId: ID!, $body: String!) {
|
mutation CommentAndClosePR($prId: ID!, $body: String!) {
|
||||||
addComment(input: {
|
addComment(input: {
|
||||||
|
|||||||
@@ -204,7 +204,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-26
|
runs-on: xcode-27
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
@@ -214,8 +214,8 @@ jobs:
|
|||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
permission-contents: read
|
permission-contents: read
|
||||||
|
|
||||||
- name: Select Xcode 26
|
- name: Select Xcode 27.1
|
||||||
run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer
|
run: sudo xcode-select -s /Applications/Xcode_27.1.app/Contents/Developer
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
@@ -230,7 +230,7 @@ jobs:
|
|||||||
working_directory: ./mobile
|
working_directory: ./mobile
|
||||||
|
|
||||||
- name: Setup Ruby
|
- name: Setup Ruby
|
||||||
uses: ruby/setup-ruby@984c0c890880bbf811283d6f09c4607c62d210a4 # v1.323.0
|
uses: ruby/setup-ruby@e1bd6146574da5e0795075b5319b300edb30c5b0 # v1.333.0
|
||||||
with:
|
with:
|
||||||
ruby-version: '3.4'
|
ruby-version: '3.4'
|
||||||
bundler-cache: true # zizmor: ignore[cache-poisoning] release branches cannot read caches written by pull requests
|
bundler-cache: true # zizmor: ignore[cache-poisoning] release branches cannot read caches written by pull requests
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Check for breaking API changes
|
- name: Check for breaking API changes
|
||||||
uses: oasdiff/oasdiff-action/breaking@5e81b5c380accc6b523f9d32a637ca630e33620b # v0.1.17
|
uses: oasdiff/oasdiff-action/breaking@9ce3a29eb0e6478dd0140932a678c7fb59d08f22 # v0.1.20
|
||||||
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
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.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,10 +71,10 @@ 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@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
uses: github/codeql-action/autobuild@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.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
|
||||||
|
|
||||||
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||||
@@ -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@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||||
with:
|
with:
|
||||||
category: '/language:${{matrix.language}}'
|
category: '/language:${{matrix.language}}'
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Generate a token
|
- name: Generate a token
|
||||||
id: generate-token
|
id: generate-token
|
||||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
- name: Generate a token
|
- name: Generate a token
|
||||||
id: generate_token
|
id: generate_token
|
||||||
if: ${{ inputs.skip != true }}
|
if: ${{ inputs.skip != true }}
|
||||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|||||||
@@ -22,3 +22,4 @@ jobs:
|
|||||||
- uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0
|
- uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ steps.token.outputs.token }}
|
repo-token: ${{ steps.token.outputs.token }}
|
||||||
|
sync-labels: true
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
<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_bg_BG.md">Български</a>
|
||||||
|
<a href="readme_i18n/README_pl_PL.md">Polski</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>
|
||||||
|
|||||||
+42
-52
@@ -1,79 +1,69 @@
|
|||||||
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
||||||
|
|
||||||
[[tools.opentofu]]
|
[[tools.opentofu]]
|
||||||
version = "1.12.6"
|
version = "1.13.2"
|
||||||
backend = "aqua:opentofu/opentofu"
|
backend = "aqua:opentofu/opentofu"
|
||||||
|
|
||||||
[tools.opentofu."platforms.linux-arm64"]
|
[tools.opentofu."platforms.linux-arm64"]
|
||||||
checksum = "sha256:9bd0228a81bcd0c88f7045c74378f45a815779f19897191dff7d9efba9976b9e"
|
checksum = "sha256:88e2d9e3f94f97b76ab2faf55ade87578f56aed19c5e45fa139d70e994366d1f"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_linux_arm64.tar.gz"
|
checksum_source = "mise-versions"
|
||||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762688"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.2/tofu_1.13.2_linux_arm64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/662742799"
|
||||||
[tools.opentofu."platforms.linux-arm64-musl"]
|
|
||||||
checksum = "sha256:9bd0228a81bcd0c88f7045c74378f45a815779f19897191dff7d9efba9976b9e"
|
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_linux_arm64.tar.gz"
|
|
||||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762688"
|
|
||||||
|
|
||||||
[tools.opentofu."platforms.linux-x64"]
|
[tools.opentofu."platforms.linux-x64"]
|
||||||
checksum = "sha256:50a6106fa4de523d09c87af85f3db1dd47535fc005727fdca6852146476b88ec"
|
checksum = "sha256:ac05d9952662c7db28dd57e93f0a68018837bfdf9342384ec0987977c16a9645"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_linux_amd64.tar.gz"
|
checksum_source = "mise-versions"
|
||||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762723"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.2/tofu_1.13.2_linux_amd64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/662742636"
|
||||||
[tools.opentofu."platforms.linux-x64-musl"]
|
|
||||||
checksum = "sha256:50a6106fa4de523d09c87af85f3db1dd47535fc005727fdca6852146476b88ec"
|
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_linux_amd64.tar.gz"
|
|
||||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762723"
|
|
||||||
|
|
||||||
[tools.opentofu."platforms.macos-arm64"]
|
[tools.opentofu."platforms.macos-arm64"]
|
||||||
checksum = "sha256:f958ec5e511063be9feb180ca015a4cb7977566a9cf6a8550bba8c2a9b5aba74"
|
checksum = "sha256:0d1cdc85f38992055b48e7fb161304a2ddfe2e5ee0ff68ffec9d0349c8bd1b59"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_darwin_arm64.tar.gz"
|
checksum_source = "mise-versions"
|
||||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762710"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.2/tofu_1.13.2_darwin_arm64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/662742637"
|
||||||
|
|
||||||
[tools.opentofu."platforms.macos-x64"]
|
[tools.opentofu."platforms.macos-x64"]
|
||||||
checksum = "sha256:44bb1855f372f17f365fb94517906e78da5001da10f4c98de57a39bf982f3a92"
|
checksum = "sha256:dc6e4eb1d3ed6550c2ef052596f933dbcd8c806957665ea83ae7537147cb3074"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_darwin_amd64.tar.gz"
|
checksum_source = "mise-versions"
|
||||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762655"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.2/tofu_1.13.2_darwin_amd64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/662742982"
|
||||||
|
|
||||||
[tools.opentofu."platforms.windows-x64"]
|
[tools.opentofu."platforms.windows-x64"]
|
||||||
checksum = "sha256:bcbcd9ca744af6c4053e2d5e75dae79ff33ad7dc38d45e1233e693d096284538"
|
checksum = "sha256:e74fc2bda0c81ce696b6b7016a4d5c1f557638161239e4101e9ee7d8e0faba59"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_windows_amd64.tar.gz"
|
checksum_source = "mise-versions"
|
||||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762708"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.2/tofu_1.13.2_windows_amd64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/662742635"
|
||||||
|
|
||||||
[[tools.terragrunt]]
|
[[tools.terragrunt]]
|
||||||
version = "1.0.3"
|
version = "1.2.0"
|
||||||
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:1e13b7f0646fa7962bc6c5bfba14a30d6a589325b82561e21b66be5981d86395"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
|
checksum_source = "mise-versions"
|
||||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654253"
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.2.0/terragrunt_linux_arm64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/662437526"
|
||||||
[tools.terragrunt."platforms.linux-arm64-musl"]
|
|
||||||
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
|
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
|
|
||||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654253"
|
|
||||||
|
|
||||||
[tools.terragrunt."platforms.linux-x64"]
|
[tools.terragrunt."platforms.linux-x64"]
|
||||||
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
|
checksum = "sha256:23b5f2f9203d34919f7fd270a4b635b721b4903beca9762a1e4853b159bc9be8"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
|
checksum_source = "mise-versions"
|
||||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654196"
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.2.0/terragrunt_linux_amd64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/662437263"
|
||||||
[tools.terragrunt."platforms.linux-x64-musl"]
|
|
||||||
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
|
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
|
|
||||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654196"
|
|
||||||
|
|
||||||
[tools.terragrunt."platforms.macos-arm64"]
|
[tools.terragrunt."platforms.macos-arm64"]
|
||||||
checksum = "sha256:aacb5be2ca5475300cbce246dfbd8a45eb47510fbaa70fab8561c49ef5db03aa"
|
checksum = "sha256:79790d841ce71fe94c689fb6f5034afd31ad53d5904937c7a10934cca9d9734b"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_arm64.tar.gz"
|
checksum_source = "mise-versions"
|
||||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654084"
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.2.0/terragrunt_darwin_arm64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/662436516"
|
||||||
|
|
||||||
[tools.terragrunt."platforms.macos-x64"]
|
[tools.terragrunt."platforms.macos-x64"]
|
||||||
checksum = "sha256:3133c2251e191aede8e3dd2a5b3aee2e91c5f08f88f117aee40eed9a24c8ef6b"
|
checksum = "sha256:0b0e63e80509eb1ffb4f89b892bc6fbf7728f27f0a73974400f1bd82a80b8627"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_amd64.tar.gz"
|
checksum_source = "mise-versions"
|
||||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406653970"
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.2.0/terragrunt_darwin_amd64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/662436142"
|
||||||
|
|
||||||
[tools.terragrunt."platforms.windows-x64"]
|
[tools.terragrunt."platforms.windows-x64"]
|
||||||
checksum = "sha256:183b2745b4e04980a6bfa4450ff81956a12596ca22d70f7aaa793980f5b036db"
|
checksum = "sha256:c9a146b9aad456a289614023ad5778123d819af3cf9efd1c6130f196cfcb1999"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_windows_amd64.exe.tar.gz"
|
checksum_source = "mise-versions"
|
||||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654412"
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.2.0/terragrunt_windows_amd64.exe.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/662438255"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tools]
|
[tools]
|
||||||
terragrunt = "1.1.1"
|
terragrunt = "1.2.0"
|
||||||
opentofu = "1.12.6"
|
opentofu = "1.13.2"
|
||||||
|
|
||||||
[tasks."tg:fmt"]
|
[tasks."tg:fmt"]
|
||||||
run = "terragrunt hclfmt"
|
run = "terragrunt hclfmt"
|
||||||
|
|||||||
+28
-28
@@ -2,34 +2,34 @@
|
|||||||
# 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.9"
|
version = "4.52.10"
|
||||||
constraints = "4.52.9"
|
constraints = "4.52.10"
|
||||||
hashes = [
|
hashes = [
|
||||||
"h1:LRBYMKjZyuld8kgVhzoceRtdtkht+w8LunrdpgqrL28=",
|
"h1:57oeYqz+pYEPAw2GvHARjcER0QYU0teS6DJzS26kCAY=",
|
||||||
"h1:Q2NeLtrjSTZwHHsMZXKpqOAbxF8DWgqpmH5j+1pUZGw=",
|
"h1:6bnAgXSboppQbj79XmRtQLj/58d+b4EozydI8XrqwRE=",
|
||||||
"h1:Q4wiux11bDVWRmJScdjp8jLbxMRsj8k3xGtPtY4BPDo=",
|
"h1:6eP/jGOz1ioH17d3pygKhvsRa4AEKvedKqd+wgyiHYk=",
|
||||||
"h1:VVW111jEbRMTu7hXeGQMmZ/oSTB8LIYhjM5ztyymqAI=",
|
"h1:84gJm57BBSvtfwt7t9ZiU7EKKDQARjsztoDuqB5DfqU=",
|
||||||
"h1:Z7sTk8/B9XYFAEpTq1ib/WsB2QvpegGwef395tHCUSo=",
|
"h1:99tME9L/Ug/dXElX3Ac7Kx+KD7nhWEEwOTqaQubW18w=",
|
||||||
"h1:eakzS6UsflV4gN/oQWdRPYvvmySKkhgKPyykCfPSVH0=",
|
"h1:TnMGVaPpIH5NZX324v+y6j1iW3ym0MqqNekxHlzXT3A=",
|
||||||
"h1:eesNMA1JUNBKCnuEvRLW2CT3tNrIejbXJG4A176nA9Q=",
|
"h1:Z1L5jI4GiJxAAsfNyZHU4MGTluWEjjKtlB8Xn6cA0Dg=",
|
||||||
"h1:o8txgjqnDDODxQQ6ITwHsvPqHllCbMGnJkjCWNJK5ME=",
|
"h1:dB/WsY27MLla/cwjYggolJhVeHUeS59rlOObAWK2e24=",
|
||||||
"h1:rDTPIzab3tbGdtU3NmES08+tRlKG8jygNAFDFbheiuM=",
|
"h1:gpffNocq9fHK+uQehZQK6YmL1aHeCGrVM1p53dcFP/0=",
|
||||||
"h1:sIoAt5vaxgFBrst1Esfm7ixQQ/T7/JNPZu0m4bw+Bc4=",
|
"h1:jvfw1A2Uck4AjNmmWXwGH6P0sGQrnNY/KUVlqgyqYUM=",
|
||||||
"h1:sPCs3KnD/db5TMz2CNgiYix7sRS8+FP92T4ipSYT9vw=",
|
"h1:ojvrl9uI1EPtAwKRHim9eSc4Lc88thU/2BFGdJLEwrc=",
|
||||||
"h1:uXPLsMQUYVJjRGUfmfDQXsKzYr1scvF6PYKj+3f0Nr4=",
|
"h1:rJZJK06NYCcSZhY5fwsRUSznSmAmRWVg4sO0auvbURo=",
|
||||||
"h1:vQibC9lgBRQjRQ5KxFH61wd3/q2I40groLPjZ/jiikQ=",
|
"h1:y2m6i2SnQ3ZxiJHlMWNLHJrEbNqQT6OpDGyhFP+Nmds=",
|
||||||
"zh:0826cad9b57368f06466ec3e1f766086f44a373288753e144f681ddc0e61ab49",
|
"zh:06c09cc50d6c61fb5f1ca79e4b22d5f383c6a848f885ed542dadef44ab4bfd55",
|
||||||
"zh:15bf8d8abdcd202652466f3ef91110f143a7fad13c6173c7180e3dc0dbe9254b",
|
"zh:205bdfdac49d6b25ec2ca7ecffd39bf47126340e1943066bdbbd64d6d72577d2",
|
||||||
"zh:1a60d1d2e55a17942902b1ccc91e8f6cb150c035843d3eefc074f1291a0e7d5f",
|
"zh:341ed688772d8e2bd9a148c57d56d5dfbe3d625bc50296a505cbae3d29694562",
|
||||||
"zh:23373a63fe06bbfa90c6c105c08429031719bedafa2458973cbdad32a384b5a7",
|
"zh:3790619bae55558ab4d93f53686f7cf55f35138f800ea07b0b9c20cce75b9c0d",
|
||||||
"zh:256a97b79cedee2575002f77ac3bf18888ce0fc97170ae8a357db406bb0a88e3",
|
"zh:44284a5b67e34f1381bbc47b6e765cee581037e25cd3b459caec069493b63898",
|
||||||
"zh:3dd9f93c167cd59491ae77781032491d36a80843a1e44f9ee68b41b3f8e3ab8e",
|
"zh:493a173f3d5245db82782e8c1aba63e34c6fcf6b952b9288926b519bfe9d9725",
|
||||||
"zh:5182d2d61930dd83584d8feb56102614f80ddeec532b939b58085ad74b796a6e",
|
"zh:4eb7846381fee6eefdb7e0aaf80153ed71d35547752f96987218a9429645844f",
|
||||||
"zh:5e3ca1888ba3b3bd8abd0f7dfcdd82fb5a8819affee899ed7fd891a33c17e0cb",
|
"zh:850b1d1da770f48def40223d754ff65c31fabb1a7f334ed6ebb40eba09c41572",
|
||||||
"zh:7a3ce2119e39fb3a90a6a459a26863851309dbc92e0735336cbc98692539cb4d",
|
"zh:9a29a08a9fd40d7c1beefe8597617c2fc475d62ec6d5ca8fcc2cd1a056a67a2e",
|
||||||
"zh:c095dc680a646cc078c10bb0551e2ea8f08e7abdbcbc58cfd332c56b58788631",
|
"zh:9e57472f0a56863204f5b2eef125d4fb1222eca793c6a5ac9f4df6c1776edc48",
|
||||||
"zh:d2400e010e069c19916c99136e200cf873eba6db2b18d3eb9541b38eb1f565fa",
|
"zh:a9eff98931da596c2d335359841bfd668f4b6cde257e15546a558c7f5d72773b",
|
||||||
"zh:dbc2f19f09aaefbd69334005bca2bcb0c9b690de1ed76f7ba97ce8e59a6e85df",
|
"zh:c9293fb3390c79e4f5072e019c0a2c4c7408a55a55e78ae37f739a1da75886ef",
|
||||||
"zh:e685225f432da3e23b8717848de8c0ad0e22363450b2ac92cd24a56739ab5e9b",
|
"zh:c9e98abec7f7a1e2669dd9c065896217125d774640e4673c1303a942736b3eb4",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ terraform {
|
|||||||
required_providers {
|
required_providers {
|
||||||
cloudflare = {
|
cloudflare = {
|
||||||
source = "cloudflare/cloudflare"
|
source = "cloudflare/cloudflare"
|
||||||
version = "4.52.9"
|
version = "4.52.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-28
@@ -2,34 +2,34 @@
|
|||||||
# 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.9"
|
version = "4.52.10"
|
||||||
constraints = "4.52.9"
|
constraints = "4.52.10"
|
||||||
hashes = [
|
hashes = [
|
||||||
"h1:LRBYMKjZyuld8kgVhzoceRtdtkht+w8LunrdpgqrL28=",
|
"h1:57oeYqz+pYEPAw2GvHARjcER0QYU0teS6DJzS26kCAY=",
|
||||||
"h1:Q2NeLtrjSTZwHHsMZXKpqOAbxF8DWgqpmH5j+1pUZGw=",
|
"h1:6bnAgXSboppQbj79XmRtQLj/58d+b4EozydI8XrqwRE=",
|
||||||
"h1:Q4wiux11bDVWRmJScdjp8jLbxMRsj8k3xGtPtY4BPDo=",
|
"h1:6eP/jGOz1ioH17d3pygKhvsRa4AEKvedKqd+wgyiHYk=",
|
||||||
"h1:VVW111jEbRMTu7hXeGQMmZ/oSTB8LIYhjM5ztyymqAI=",
|
"h1:84gJm57BBSvtfwt7t9ZiU7EKKDQARjsztoDuqB5DfqU=",
|
||||||
"h1:Z7sTk8/B9XYFAEpTq1ib/WsB2QvpegGwef395tHCUSo=",
|
"h1:99tME9L/Ug/dXElX3Ac7Kx+KD7nhWEEwOTqaQubW18w=",
|
||||||
"h1:eakzS6UsflV4gN/oQWdRPYvvmySKkhgKPyykCfPSVH0=",
|
"h1:TnMGVaPpIH5NZX324v+y6j1iW3ym0MqqNekxHlzXT3A=",
|
||||||
"h1:eesNMA1JUNBKCnuEvRLW2CT3tNrIejbXJG4A176nA9Q=",
|
"h1:Z1L5jI4GiJxAAsfNyZHU4MGTluWEjjKtlB8Xn6cA0Dg=",
|
||||||
"h1:o8txgjqnDDODxQQ6ITwHsvPqHllCbMGnJkjCWNJK5ME=",
|
"h1:dB/WsY27MLla/cwjYggolJhVeHUeS59rlOObAWK2e24=",
|
||||||
"h1:rDTPIzab3tbGdtU3NmES08+tRlKG8jygNAFDFbheiuM=",
|
"h1:gpffNocq9fHK+uQehZQK6YmL1aHeCGrVM1p53dcFP/0=",
|
||||||
"h1:sIoAt5vaxgFBrst1Esfm7ixQQ/T7/JNPZu0m4bw+Bc4=",
|
"h1:jvfw1A2Uck4AjNmmWXwGH6P0sGQrnNY/KUVlqgyqYUM=",
|
||||||
"h1:sPCs3KnD/db5TMz2CNgiYix7sRS8+FP92T4ipSYT9vw=",
|
"h1:ojvrl9uI1EPtAwKRHim9eSc4Lc88thU/2BFGdJLEwrc=",
|
||||||
"h1:uXPLsMQUYVJjRGUfmfDQXsKzYr1scvF6PYKj+3f0Nr4=",
|
"h1:rJZJK06NYCcSZhY5fwsRUSznSmAmRWVg4sO0auvbURo=",
|
||||||
"h1:vQibC9lgBRQjRQ5KxFH61wd3/q2I40groLPjZ/jiikQ=",
|
"h1:y2m6i2SnQ3ZxiJHlMWNLHJrEbNqQT6OpDGyhFP+Nmds=",
|
||||||
"zh:0826cad9b57368f06466ec3e1f766086f44a373288753e144f681ddc0e61ab49",
|
"zh:06c09cc50d6c61fb5f1ca79e4b22d5f383c6a848f885ed542dadef44ab4bfd55",
|
||||||
"zh:15bf8d8abdcd202652466f3ef91110f143a7fad13c6173c7180e3dc0dbe9254b",
|
"zh:205bdfdac49d6b25ec2ca7ecffd39bf47126340e1943066bdbbd64d6d72577d2",
|
||||||
"zh:1a60d1d2e55a17942902b1ccc91e8f6cb150c035843d3eefc074f1291a0e7d5f",
|
"zh:341ed688772d8e2bd9a148c57d56d5dfbe3d625bc50296a505cbae3d29694562",
|
||||||
"zh:23373a63fe06bbfa90c6c105c08429031719bedafa2458973cbdad32a384b5a7",
|
"zh:3790619bae55558ab4d93f53686f7cf55f35138f800ea07b0b9c20cce75b9c0d",
|
||||||
"zh:256a97b79cedee2575002f77ac3bf18888ce0fc97170ae8a357db406bb0a88e3",
|
"zh:44284a5b67e34f1381bbc47b6e765cee581037e25cd3b459caec069493b63898",
|
||||||
"zh:3dd9f93c167cd59491ae77781032491d36a80843a1e44f9ee68b41b3f8e3ab8e",
|
"zh:493a173f3d5245db82782e8c1aba63e34c6fcf6b952b9288926b519bfe9d9725",
|
||||||
"zh:5182d2d61930dd83584d8feb56102614f80ddeec532b939b58085ad74b796a6e",
|
"zh:4eb7846381fee6eefdb7e0aaf80153ed71d35547752f96987218a9429645844f",
|
||||||
"zh:5e3ca1888ba3b3bd8abd0f7dfcdd82fb5a8819affee899ed7fd891a33c17e0cb",
|
"zh:850b1d1da770f48def40223d754ff65c31fabb1a7f334ed6ebb40eba09c41572",
|
||||||
"zh:7a3ce2119e39fb3a90a6a459a26863851309dbc92e0735336cbc98692539cb4d",
|
"zh:9a29a08a9fd40d7c1beefe8597617c2fc475d62ec6d5ca8fcc2cd1a056a67a2e",
|
||||||
"zh:c095dc680a646cc078c10bb0551e2ea8f08e7abdbcbc58cfd332c56b58788631",
|
"zh:9e57472f0a56863204f5b2eef125d4fb1222eca793c6a5ac9f4df6c1776edc48",
|
||||||
"zh:d2400e010e069c19916c99136e200cf873eba6db2b18d3eb9541b38eb1f565fa",
|
"zh:a9eff98931da596c2d335359841bfd668f4b6cde257e15546a558c7f5d72773b",
|
||||||
"zh:dbc2f19f09aaefbd69334005bca2bcb0c9b690de1ed76f7ba97ce8e59a6e85df",
|
"zh:c9293fb3390c79e4f5072e019c0a2c4c7408a55a55e78ae37f739a1da75886ef",
|
||||||
"zh:e685225f432da3e23b8717848de8c0ad0e22363450b2ac92cd24a56739ab5e9b",
|
"zh:c9e98abec7f7a1e2669dd9c065896217125d774640e4673c1303a942736b3eb4",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ terraform {
|
|||||||
required_providers {
|
required_providers {
|
||||||
cloudflare = {
|
cloudflare = {
|
||||||
source = "cloudflare/cloudflare"
|
source = "cloudflare/cloudflare"
|
||||||
version = "4.52.9"
|
version = "4.52.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ services:
|
|||||||
container_name: immich_prometheus
|
container_name: immich_prometheus
|
||||||
ports:
|
ports:
|
||||||
- 9090:9090
|
- 9090:9090
|
||||||
image: prom/prometheus@sha256:5ce7540c3c00ef4ab0c9d2c995c6a5b9c421f44b4a115d97a2c7af3b1c21cbb0
|
image: prom/prometheus@sha256:fa3302279710be2f21a8301ec17a8b4f2301fd829bd113839b3b6b02b6f191a2
|
||||||
volumes:
|
volumes:
|
||||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||||
- prometheus-data:/prometheus
|
- prometheus-data:/prometheus
|
||||||
@@ -97,7 +97,7 @@ services:
|
|||||||
command: ['./run.sh', '-disable-reporting']
|
command: ['./run.sh', '-disable-reporting']
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
image: grafana/grafana:12.4.11-ubuntu@sha256:23f2328b859c7677c7065c81b479600706ef9cc9004977af5bbd68b159b8ea60
|
image: grafana/grafana:13.2.4-ubuntu@sha256:d838696907b4350d9232eb8da1503dc1d1010a0359d3ce4b3f7d2ff7db28eb75
|
||||||
volumes:
|
volumes:
|
||||||
- grafana-data:/var/lib/grafana
|
- grafana-data:/var/lib/grafana
|
||||||
|
|
||||||
|
|||||||
@@ -209,6 +209,11 @@ The default configuration looks like this:
|
|||||||
"timeout": 30000,
|
"timeout": 30000,
|
||||||
"tokenEndpointAuthMethod": "client_secret_post"
|
"tokenEndpointAuthMethod": "client_secret_post"
|
||||||
},
|
},
|
||||||
|
"passkey": {
|
||||||
|
"enabled": true,
|
||||||
|
"domain": null,
|
||||||
|
"additionalDomains": []
|
||||||
|
},
|
||||||
"passwordLogin": {
|
"passwordLogin": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
||||||
|
|
||||||
[[tools.wrangler]]
|
[[tools.wrangler]]
|
||||||
version = "4.137.0"
|
version = "4.152.0"
|
||||||
backend = "npm:wrangler"
|
backend = "npm:wrangler"
|
||||||
|
|
||||||
[tools.wrangler.options]
|
[tools.wrangler.options]
|
||||||
|
|||||||
+1
-1
@@ -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.137.0"
|
wrangler = "4.152.0"
|
||||||
|
|||||||
+1
-1
@@ -41,7 +41,7 @@
|
|||||||
"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": "^75.0.0",
|
"eslint-plugin-unicorn": "^75.0.0",
|
||||||
"exiftool-vendored": "^35.0.0",
|
"exiftool-vendored": "^39.0.0",
|
||||||
"globals": "^17.0.0",
|
"globals": "^17.0.0",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
|
|||||||
@@ -355,7 +355,7 @@ describe('/asset', () => {
|
|||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
.send({ livePhotoVideoId: asset.id });
|
.send({ livePhotoVideoId: asset.id });
|
||||||
|
|
||||||
expect(body).toEqual(errorDto.badRequest('Live photo video does not belong to the user'));
|
expect(body).toEqual(errorDto.badRequest('Not found or no asset.update access'));
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -567,7 +567,7 @@ describe('/search', () => {
|
|||||||
'New York',
|
'New York',
|
||||||
'Shanghai',
|
'Shanghai',
|
||||||
'State of Berlin',
|
'State of Berlin',
|
||||||
'St.-Petersburg',
|
'St. Petersburg',
|
||||||
'Tbilisi',
|
'Tbilisi',
|
||||||
'Tokyo',
|
'Tokyo',
|
||||||
'Virginia',
|
'Virginia',
|
||||||
@@ -591,7 +591,7 @@ describe('/search', () => {
|
|||||||
'New York',
|
'New York',
|
||||||
'Shanghai',
|
'Shanghai',
|
||||||
'State of Berlin',
|
'State of Berlin',
|
||||||
'St.-Petersburg',
|
'St. Petersburg',
|
||||||
'Tbilisi',
|
'Tbilisi',
|
||||||
'Tokyo',
|
'Tokyo',
|
||||||
'Virginia',
|
'Virginia',
|
||||||
|
|||||||
@@ -206,6 +206,13 @@ describe('/shared-links', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not return album users', async () => {
|
||||||
|
const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithAlbum.key });
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body.album.albumUsers).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
it('should return unauthorized for incorrect shared link', async () => {
|
it('should return unauthorized for incorrect shared link', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/shared-links/me')
|
.get('/shared-links/me')
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
import { LoginResponseDto, getMyUser, login } from '@immich/sdk';
|
|
||||||
import { createUserDto } from 'src/fixtures.js';
|
|
||||||
import { app, asBearerAuth, utils } from 'src/utils.js';
|
|
||||||
import request from 'supertest';
|
|
||||||
import { beforeAll, describe, expect, it } from 'vitest';
|
|
||||||
|
|
||||||
describe('/users', () => {
|
|
||||||
let admin: LoginResponseDto;
|
|
||||||
let nonAdmin: LoginResponseDto;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await utils.resetDatabase();
|
|
||||||
admin = await utils.adminSetup({ onboarding: false });
|
|
||||||
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user2);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('PUT /users/me', () => {
|
|
||||||
/** @deprecated */
|
|
||||||
it('should allow a user to change their password (deprecated)', async () => {
|
|
||||||
const user = await getMyUser({ headers: asBearerAuth(nonAdmin.accessToken) });
|
|
||||||
|
|
||||||
expect(user.shouldChangePassword).toBe(true);
|
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.put(`/users/me`)
|
|
||||||
.send({ password: 'super-secret' })
|
|
||||||
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toMatchObject({
|
|
||||||
email: nonAdmin.userEmail,
|
|
||||||
shouldChangePassword: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const token = await login({ loginCredentialDto: { email: nonAdmin.userEmail, password: 'super-secret' } });
|
|
||||||
|
|
||||||
expect(token.accessToken).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -28,7 +28,7 @@ test.describe('Registration', () => {
|
|||||||
await page.goto('/auth/login?autoLaunch=0');
|
await page.goto('/auth/login?autoLaunch=0');
|
||||||
await page.getByLabel('Email').fill('admin@immich.app');
|
await page.getByLabel('Email').fill('admin@immich.app');
|
||||||
await page.getByLabel('Password', { exact: true }).fill('password');
|
await page.getByLabel('Password', { exact: true }).fill('password');
|
||||||
await page.getByRole('button', { name: 'Login' }).click();
|
await page.getByRole('button', { name: 'Login', exact: true }).click();
|
||||||
|
|
||||||
// onboarding
|
// onboarding
|
||||||
await expect(page).toHaveURL('/auth/onboarding');
|
await expect(page).toHaveURL('/auth/onboarding');
|
||||||
@@ -66,11 +66,12 @@ test.describe('Registration', () => {
|
|||||||
await page.goto('/auth/login?autoLaunch=0');
|
await page.goto('/auth/login?autoLaunch=0');
|
||||||
await page.getByLabel('Email').fill('user@immich.cloud');
|
await page.getByLabel('Email').fill('user@immich.cloud');
|
||||||
await page.getByLabel('Password').fill('password');
|
await page.getByLabel('Password').fill('password');
|
||||||
await page.getByRole('button', { name: 'Login' }).click();
|
await page.getByRole('button', { name: 'Login', exact: true }).click();
|
||||||
|
|
||||||
// change password
|
// change password
|
||||||
await expect(page.getByRole('heading')).toHaveText('Change Password');
|
await expect(page.getByRole('heading')).toHaveText('Change Password');
|
||||||
await expect(page).toHaveURL('/auth/change-password');
|
await expect(page).toHaveURL('/auth/change-password');
|
||||||
|
await page.getByLabel('Password', { exact: true }).fill('password');
|
||||||
await page.getByLabel('New Password').fill('new-password');
|
await page.getByLabel('New Password').fill('new-password');
|
||||||
await page.getByLabel('Confirm Password').fill('new-password');
|
await page.getByLabel('Confirm Password').fill('new-password');
|
||||||
await page.getByRole('button', { name: 'Change password' }).click();
|
await page.getByRole('button', { name: 'Change password' }).click();
|
||||||
@@ -79,7 +80,7 @@ test.describe('Registration', () => {
|
|||||||
await expect(page).toHaveURL('/auth/login?autoLaunch=0');
|
await expect(page).toHaveURL('/auth/login?autoLaunch=0');
|
||||||
await page.getByLabel('Email').fill('user@immich.cloud');
|
await page.getByLabel('Email').fill('user@immich.cloud');
|
||||||
await page.getByLabel('Password').fill('new-password');
|
await page.getByLabel('Password').fill('new-password');
|
||||||
await page.getByRole('button', { name: 'Login' }).click();
|
await page.getByRole('button', { name: 'Login', exact: true }).click();
|
||||||
|
|
||||||
// onboarding
|
// onboarding
|
||||||
await expect(page).toHaveURL('/auth/onboarding');
|
await expect(page).toHaveURL('/auth/onboarding');
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ test.describe('Shared Links', () => {
|
|||||||
|
|
||||||
test('show error for invalid shared link', async ({ page }) => {
|
test('show error for invalid shared link', async ({ page }) => {
|
||||||
await page.goto('/share/invalid');
|
await page.goto('/share/invalid');
|
||||||
await page.getByRole('heading', { name: 'Invalid share key' }).waitFor();
|
await page.getByRole('heading', { name: 'Page not found' }).waitFor();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('auth on navigation from shared link to timeline', async ({ context, page }) => {
|
test('auth on navigation from shared link to timeline', async ({ context, page }) => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { BrowserContext } from '@playwright/test';
|
import { BrowserContext } from '@playwright/test';
|
||||||
import { playwrightHost } from 'src/../playwright.config.js';
|
import { playwrightHost } from 'src/../playwright.config.js';
|
||||||
|
import { adminConfig, publicConfig, userConfig } from 'src/ui/mock-network/config-fixtures.js';
|
||||||
|
|
||||||
export const setupBaseMockApiRoutes = async (context: BrowserContext, adminUserId: string) => {
|
export const setupBaseMockApiRoutes = async (context: BrowserContext, adminUserId: string) => {
|
||||||
await context.addCookies([
|
await context.addCookies([
|
||||||
@@ -113,6 +114,27 @@ export const setupBaseMockApiRoutes = async (context: BrowserContext, adminUserI
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
await context.route('**/api/public/config', async (route) => {
|
||||||
|
return route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
json: publicConfig,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await context.route('**/api/config', async (route) => {
|
||||||
|
return route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
json: userConfig,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await context.route('**/api/admin/config', async (route) => {
|
||||||
|
return route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
json: adminConfig,
|
||||||
|
});
|
||||||
|
});
|
||||||
await context.route('**/api/server/features', async (route) => {
|
await context.route('**/api/server/features', async (route) => {
|
||||||
return route.fulfill({
|
return route.fulfill({
|
||||||
status: 200,
|
status: 200,
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
export const publicConfig = {
|
||||||
|
ffmpeg: { realtime: { enabled: false } },
|
||||||
|
map: {
|
||||||
|
enabled: true,
|
||||||
|
lightStyle: 'https://tiles.immich.cloud/v1/style/light.json',
|
||||||
|
darkStyle: 'https://tiles.immich.cloud/v1/style/dark.json',
|
||||||
|
},
|
||||||
|
oauth: { autoLaunch: false, buttonText: 'Login with OAuth', enabled: false },
|
||||||
|
passkey: { enabled: true },
|
||||||
|
passwordLogin: { enabled: true },
|
||||||
|
server: { loginPageMessage: '' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const userConfig = {
|
||||||
|
ffmpeg: { realtime: { enabled: false, videoCodecs: ['h264', 'hevc'], resolutions: [480, 720, 1080] } },
|
||||||
|
machineLearning: {
|
||||||
|
enabled: false,
|
||||||
|
clip: { enabled: true },
|
||||||
|
duplicateDetection: { enabled: true },
|
||||||
|
facialRecognition: { enabled: true, minFaces: 3 },
|
||||||
|
ocr: { enabled: true },
|
||||||
|
},
|
||||||
|
map: {
|
||||||
|
enabled: true,
|
||||||
|
lightStyle: 'https://tiles.immich.cloud/v1/style/light.json',
|
||||||
|
darkStyle: 'https://tiles.immich.cloud/v1/style/dark.json',
|
||||||
|
},
|
||||||
|
reverseGeocoding: { enabled: true },
|
||||||
|
oauth: { autoLaunch: false, buttonText: 'Login with OAuth', enabled: false, accountManagementUrl: '' },
|
||||||
|
passkey: { enabled: true },
|
||||||
|
passwordLogin: { enabled: true },
|
||||||
|
image: { thumbnail: { size: 250 }, preview: { size: 1440 }, fullsize: { enabled: false } },
|
||||||
|
trash: { enabled: true, days: 30 },
|
||||||
|
notifications: { smtp: { enabled: false } },
|
||||||
|
server: { externalDomain: '', loginPageMessage: '', publicUsers: true },
|
||||||
|
user: { deleteDelay: 7 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const adminConfig = {
|
||||||
|
backup: { database: { enabled: true, cronExpression: '0 02 * * *', keepLastAmount: 14 } },
|
||||||
|
ffmpeg: {
|
||||||
|
crf: 23,
|
||||||
|
threads: 0,
|
||||||
|
preset: 'ultrafast',
|
||||||
|
targetVideoCodec: 'h264',
|
||||||
|
acceptedVideoCodecs: ['h264'],
|
||||||
|
targetAudioCodec: 'aac',
|
||||||
|
acceptedAudioCodecs: ['aac', 'mp3', 'opus'],
|
||||||
|
acceptedContainers: ['mov', 'ogg', 'webm'],
|
||||||
|
targetResolution: '720',
|
||||||
|
maxBitrate: '0',
|
||||||
|
bframes: -1,
|
||||||
|
refs: 0,
|
||||||
|
gopSize: 0,
|
||||||
|
temporalAQ: false,
|
||||||
|
cqMode: 'auto',
|
||||||
|
twoPass: false,
|
||||||
|
preferredHwDevice: 'auto',
|
||||||
|
transcode: 'required',
|
||||||
|
tonemap: 'hable',
|
||||||
|
accel: 'disabled',
|
||||||
|
accelDecode: true,
|
||||||
|
realtime: { enabled: false, videoCodecs: ['h264', 'hevc'], resolutions: [480, 720, 1080] },
|
||||||
|
},
|
||||||
|
integrityChecks: {
|
||||||
|
missingFiles: { enabled: true, cronExpression: '0 03 * * *' },
|
||||||
|
untrackedFiles: { enabled: true, cronExpression: '0 03 * * *' },
|
||||||
|
checksumFiles: { enabled: true, cronExpression: '0 03 * * *', timeLimit: 3_600_000, percentageLimit: 1 },
|
||||||
|
},
|
||||||
|
job: {
|
||||||
|
thumbnailGeneration: { concurrency: 3 },
|
||||||
|
metadataExtraction: { concurrency: 5 },
|
||||||
|
videoConversion: { concurrency: 1 },
|
||||||
|
faceDetection: { concurrency: 2 },
|
||||||
|
smartSearch: { concurrency: 2 },
|
||||||
|
backgroundTask: { concurrency: 5 },
|
||||||
|
migration: { concurrency: 5 },
|
||||||
|
search: { concurrency: 5 },
|
||||||
|
sidecar: { concurrency: 5 },
|
||||||
|
library: { concurrency: 5 },
|
||||||
|
notifications: { concurrency: 5 },
|
||||||
|
ocr: { concurrency: 1 },
|
||||||
|
workflow: { concurrency: 5 },
|
||||||
|
editor: { concurrency: 2 },
|
||||||
|
integrityCheck: { concurrency: 1 },
|
||||||
|
},
|
||||||
|
logging: { enabled: true, level: 'log' },
|
||||||
|
machineLearning: {
|
||||||
|
enabled: false,
|
||||||
|
urls: ['http://immich-machine-learning:3003'],
|
||||||
|
availabilityChecks: { enabled: true, timeout: 2000, interval: 30_000 },
|
||||||
|
clip: { enabled: true, modelName: 'ViT-B-32__openai' },
|
||||||
|
duplicateDetection: { enabled: true, maxDistance: 0.01 },
|
||||||
|
facialRecognition: { enabled: true, modelName: 'buffalo_l', minScore: 0.7, maxDistance: 0.5, minFaces: 3 },
|
||||||
|
ocr: {
|
||||||
|
enabled: true,
|
||||||
|
modelName: 'PP-OCRv5_mobile',
|
||||||
|
minDetectionScore: 0.5,
|
||||||
|
minRecognitionScore: 0.8,
|
||||||
|
maxResolution: 736,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
map: {
|
||||||
|
enabled: true,
|
||||||
|
lightStyle: 'https://tiles.immich.cloud/v1/style/light.json',
|
||||||
|
darkStyle: 'https://tiles.immich.cloud/v1/style/dark.json',
|
||||||
|
},
|
||||||
|
reverseGeocoding: { enabled: true },
|
||||||
|
metadata: { faces: { import: false } },
|
||||||
|
oauth: {
|
||||||
|
autoLaunch: false,
|
||||||
|
autoRegister: true,
|
||||||
|
buttonText: 'Login with OAuth',
|
||||||
|
clientId: '',
|
||||||
|
clientSecret: '',
|
||||||
|
defaultStorageQuota: null,
|
||||||
|
enabled: false,
|
||||||
|
issuerUrl: '',
|
||||||
|
accountManagementUrl: '',
|
||||||
|
endSessionEndpoint: '',
|
||||||
|
mobileOverrideEnabled: false,
|
||||||
|
mobileRedirectUri: '',
|
||||||
|
prompt: '',
|
||||||
|
scope: 'openid email profile',
|
||||||
|
signingAlgorithm: 'RS256',
|
||||||
|
profileSigningAlgorithm: 'none',
|
||||||
|
storageLabelClaim: 'preferred_username',
|
||||||
|
storageQuotaClaim: 'immich_quota',
|
||||||
|
roleClaim: 'immich_role',
|
||||||
|
tokenEndpointAuthMethod: 'client_secret_post',
|
||||||
|
timeout: 30_000,
|
||||||
|
allowInsecureRequests: false,
|
||||||
|
},
|
||||||
|
passkey: { enabled: true, domain: null, additionalDomains: [] },
|
||||||
|
passwordLogin: { enabled: true },
|
||||||
|
storageTemplate: {
|
||||||
|
enabled: false,
|
||||||
|
hashVerificationEnabled: true,
|
||||||
|
template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
thumbnail: { format: 'webp', size: 250, quality: 80, progressive: false },
|
||||||
|
preview: { format: 'jpeg', size: 1440, quality: 80, progressive: false },
|
||||||
|
colorspace: 'p3',
|
||||||
|
extractEmbedded: false,
|
||||||
|
fullsize: { enabled: false, format: 'jpeg', quality: 80, progressive: false },
|
||||||
|
},
|
||||||
|
newVersionCheck: { enabled: true, channel: 'stable' },
|
||||||
|
nightlyTasks: {
|
||||||
|
startTime: '00:00',
|
||||||
|
databaseCleanup: true,
|
||||||
|
generateMemories: true,
|
||||||
|
syncQuotaUsage: true,
|
||||||
|
missingThumbnails: true,
|
||||||
|
clusterNewFaces: true,
|
||||||
|
},
|
||||||
|
trash: { enabled: true, days: 30 },
|
||||||
|
theme: { customCss: '' },
|
||||||
|
library: { scan: { enabled: true, cronExpression: '0 0 * * *' }, watch: { enabled: false } },
|
||||||
|
server: { externalDomain: '', loginPageMessage: '', publicUsers: true },
|
||||||
|
notifications: {
|
||||||
|
smtp: {
|
||||||
|
enabled: false,
|
||||||
|
from: '',
|
||||||
|
replyTo: '',
|
||||||
|
transport: { ignoreCert: false, host: '', port: 587, secure: false, username: '', password: '' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
templates: { email: { welcomeTemplate: '', albumInviteTemplate: '', albumUpdateTemplate: '' } },
|
||||||
|
user: { deleteDelay: 7 },
|
||||||
|
};
|
||||||
@@ -100,7 +100,7 @@ test.describe('OCR with stacked assets', () => {
|
|||||||
|
|
||||||
const stackThumbnails = page.locator('#stack-slideshow [data-asset]');
|
const stackThumbnails = page.locator('#stack-slideshow [data-asset]');
|
||||||
await expect(stackThumbnails).toHaveCount(2);
|
await expect(stackThumbnails).toHaveCount(2);
|
||||||
await stackThumbnails.nth(1).click();
|
await page.locator(`#stack-slideshow [data-asset="${secondAssetDto.id}"]`).click();
|
||||||
|
|
||||||
// refreshOcr() clears showOverlay when switching assets, so re-enable it
|
// refreshOcr() clears showOverlay when switching assets, so re-enable it
|
||||||
await expect(ocrBoxes).toHaveCount(0);
|
await expect(ocrBoxes).toHaveCount(0);
|
||||||
|
|||||||
@@ -75,8 +75,8 @@ test.describe('asset-viewer stack', () => {
|
|||||||
await page.goto(`/photos/${fixture.primaryAsset.id}`);
|
await page.goto(`/photos/${fixture.primaryAsset.id}`);
|
||||||
await ensureDetailPanelVisible(page);
|
await ensureDetailPanelVisible(page);
|
||||||
|
|
||||||
const stackAssets = page.locator('#stack-slideshow [data-asset]');
|
const secondAsset = page.locator(`#stack-slideshow [data-asset="${secondAssetDto.id}"]`);
|
||||||
await stackAssets.nth(1).click();
|
await secondAsset.click();
|
||||||
|
|
||||||
const tags = page.getByTestId('detail-panel-tags').getByRole('link');
|
const tags = page.getByTestId('detail-panel-tags').getByRole('link');
|
||||||
await expect(tags.first()).toHaveText('test/2');
|
await expect(tags.first()).toHaveText('test/2');
|
||||||
|
|||||||
+23
-3
@@ -87,7 +87,7 @@
|
|||||||
"failed_job_command": "Command {command} failed for job: {job}",
|
"failed_job_command": "Command {command} failed for job: {job}",
|
||||||
"force_delete_user_warning": "WARNING: This will immediately remove the user and all assets. This cannot be undone and the files cannot be recovered.",
|
"force_delete_user_warning": "WARNING: This will immediately remove the user and all assets. This cannot be undone and the files cannot be recovered.",
|
||||||
"image_format": "Format",
|
"image_format": "Format",
|
||||||
"image_format_description": "WebP produces smaller files than JPEG, but is slower to encode.",
|
"image_format_description": "WebP produces smaller files than JPEG, but is slower to encode. JPEG XL is the best option for high-quality compression, but not all browsers and apps can display it.",
|
||||||
"image_fullsize_description": "Full-size image with stripped metadata, used when zoomed in",
|
"image_fullsize_description": "Full-size image with stripped metadata, used when zoomed in",
|
||||||
"image_fullsize_enabled": "Enable full-size image generation",
|
"image_fullsize_enabled": "Enable full-size image generation",
|
||||||
"image_fullsize_enabled_description": "Generate full-size image for non-web-friendly formats. When \"Prefer embedded preview\" is enabled, embedded previews are used directly without conversion. Does not affect web-friendly formats like JPEG.",
|
"image_fullsize_enabled_description": "Generate full-size image for non-web-friendly formats. When \"Prefer embedded preview\" is enabled, embedded previews are used directly without conversion. Does not affect web-friendly formats like JPEG.",
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
"image_preview_quality_description": "Preview quality from 1-100. Higher is better, but produces larger files and can reduce app responsiveness. Setting a low value may affect machine learning quality.",
|
"image_preview_quality_description": "Preview quality from 1-100. Higher is better, but produces larger files and can reduce app responsiveness. Setting a low value may affect machine learning quality.",
|
||||||
"image_preview_title": "Preview Settings",
|
"image_preview_title": "Preview Settings",
|
||||||
"image_progressive": "Progressive",
|
"image_progressive": "Progressive",
|
||||||
"image_progressive_description": "Encode JPEG images progressively for gradual loading display. This has no effect on WebP images.",
|
"image_progressive_description": "Encode JPEG images progressively for gradual loading display. This has no effect on other formats.",
|
||||||
"image_quality": "Quality",
|
"image_quality": "Quality",
|
||||||
"image_resolution": "Resolution",
|
"image_resolution": "Resolution",
|
||||||
"image_resolution_description": "Higher resolutions can preserve more detail but take longer to encode, have larger file sizes and can reduce app responsiveness.",
|
"image_resolution_description": "Higher resolutions can preserve more detail but take longer to encode, have larger file sizes and can reduce app responsiveness.",
|
||||||
@@ -324,6 +324,13 @@
|
|||||||
"oauth_timeout": "Request Timeout",
|
"oauth_timeout": "Request Timeout",
|
||||||
"oauth_timeout_description": "Timeout for requests in milliseconds",
|
"oauth_timeout_description": "Timeout for requests in milliseconds",
|
||||||
"ocr_job_description": "Use machine learning to recognize text in images",
|
"ocr_job_description": "Use machine learning to recognize text in images",
|
||||||
|
"passkey_additional_domains": "Additional domains",
|
||||||
|
"passkey_additional_domains_description": "Additional, trusted domains (related origin requests). These will only work if the passkey domain is also accessible.",
|
||||||
|
"passkey_domain": "Passkey domain",
|
||||||
|
"passkey_domain_description": "Domain used for passkeys (relying party). Leave blank to infer from the origin header. Changing this value will result in existing passkeys no longer working.",
|
||||||
|
"passkey_enable_description": "Login with a passkey",
|
||||||
|
"passkey_settings": "Passkey Login",
|
||||||
|
"passkey_settings_description": "Manage passkey login settings",
|
||||||
"password_enable_description": "Login with email and password",
|
"password_enable_description": "Login with email and password",
|
||||||
"password_settings": "Password Login",
|
"password_settings": "Password Login",
|
||||||
"password_settings_description": "Manage password login settings",
|
"password_settings_description": "Manage password login settings",
|
||||||
@@ -874,6 +881,8 @@
|
|||||||
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
|
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
|
||||||
"delete_local_dialog_ok_force": "Delete Anyway",
|
"delete_local_dialog_ok_force": "Delete Anyway",
|
||||||
"delete_others": "Delete others",
|
"delete_others": "Delete others",
|
||||||
|
"delete_passkey": "Delete passkey",
|
||||||
|
"delete_passkey_prompt": "Are you sure you want to delete this passkey?",
|
||||||
"delete_permanently": "Delete permanently",
|
"delete_permanently": "Delete permanently",
|
||||||
"delete_permanently_action_prompt": "{count} deleted permanently",
|
"delete_permanently_action_prompt": "{count} deleted permanently",
|
||||||
"delete_shared_link": "Delete shared link",
|
"delete_shared_link": "Delete shared link",
|
||||||
@@ -943,6 +952,7 @@
|
|||||||
"edit_location_action_prompt": "{count} location edited",
|
"edit_location_action_prompt": "{count} location edited",
|
||||||
"edit_location_dialog_title": "Location",
|
"edit_location_dialog_title": "Location",
|
||||||
"edit_name": "Edit name",
|
"edit_name": "Edit name",
|
||||||
|
"edit_passkey": "Edit passkey",
|
||||||
"edit_people": "Edit people",
|
"edit_people": "Edit people",
|
||||||
"edit_person": "Edit person",
|
"edit_person": "Edit person",
|
||||||
"edit_tag": "Edit tag",
|
"edit_tag": "Edit tag",
|
||||||
@@ -1287,6 +1297,7 @@
|
|||||||
"last": "Last",
|
"last": "Last",
|
||||||
"last_months": "{count, plural, one {Last month} other {Last # months}}",
|
"last_months": "{count, plural, one {Last month} other {Last # months}}",
|
||||||
"last_seen": "Last seen",
|
"last_seen": "Last seen",
|
||||||
|
"last_used": "Last used",
|
||||||
"latest_version": "Latest Version",
|
"latest_version": "Latest Version",
|
||||||
"latitude": "Latitude",
|
"latitude": "Latitude",
|
||||||
"leave": "Leave",
|
"leave": "Leave",
|
||||||
@@ -1379,6 +1390,7 @@
|
|||||||
"login_has_been_disabled": "Login has been disabled",
|
"login_has_been_disabled": "Login has been disabled",
|
||||||
"login_password_changed_error": "There was an error updating your password",
|
"login_password_changed_error": "There was an error updating your password",
|
||||||
"login_password_changed_success": "Password updated successfully",
|
"login_password_changed_success": "Password updated successfully",
|
||||||
|
"login_with_passkey": "Login with passkey",
|
||||||
"logout_all_device_confirmation": "Are you sure you want to log out all devices?",
|
"logout_all_device_confirmation": "Are you sure you want to log out all devices?",
|
||||||
"logout_this_device_confirmation": "Are you sure you want to log out this device?",
|
"logout_this_device_confirmation": "Are you sure you want to log out this device?",
|
||||||
"logs": "Logs",
|
"logs": "Logs",
|
||||||
@@ -1427,6 +1439,7 @@
|
|||||||
"manage_your_api_keys": "Manage your API keys",
|
"manage_your_api_keys": "Manage your API keys",
|
||||||
"manage_your_devices": "Manage your logged-in devices",
|
"manage_your_devices": "Manage your logged-in devices",
|
||||||
"manage_your_oauth_connection": "Manage your OAuth connection",
|
"manage_your_oauth_connection": "Manage your OAuth connection",
|
||||||
|
"manage_your_passkeys": "Manage your passkeys",
|
||||||
"map": "Map",
|
"map": "Map",
|
||||||
"map_assets_in_bounds": "{count, plural, =0 {No photos in this area} one {# photo} other {# photos}}",
|
"map_assets_in_bounds": "{count, plural, =0 {No photos in this area} one {# photo} other {# photos}}",
|
||||||
"map_cannot_get_user_location": "Cannot get user's location",
|
"map_cannot_get_user_location": "Cannot get user's location",
|
||||||
@@ -1546,6 +1559,7 @@
|
|||||||
"new_album": "New Album",
|
"new_album": "New Album",
|
||||||
"new_api_key": "New API Key",
|
"new_api_key": "New API Key",
|
||||||
"new_feature": "New Feature",
|
"new_feature": "New Feature",
|
||||||
|
"new_passkey": "New passkey",
|
||||||
"new_password": "New password",
|
"new_password": "New password",
|
||||||
"new_person": "New person",
|
"new_person": "New person",
|
||||||
"new_pin_code": "New PIN code",
|
"new_pin_code": "New PIN code",
|
||||||
@@ -1658,6 +1672,8 @@
|
|||||||
"partner_sharing": "Partner Sharing",
|
"partner_sharing": "Partner Sharing",
|
||||||
"partners": "Partners",
|
"partners": "Partners",
|
||||||
"partners_description": "Share your photos and videos with other users",
|
"partners_description": "Share your photos and videos with other users",
|
||||||
|
"passkeys": "{count, plural, one {Passkey} other {Passkeys}}",
|
||||||
|
"passkeys_not_supported": "Passkeys are not supported by this browser",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"password_does_not_match": "Password does not match",
|
"password_does_not_match": "Password does not match",
|
||||||
"password_required": "Password Required",
|
"password_required": "Password Required",
|
||||||
@@ -1842,6 +1858,7 @@
|
|||||||
"removed_from_favorites_count": "{count, plural, other {Removed #}} from favorites",
|
"removed_from_favorites_count": "{count, plural, other {Removed #}} from favorites",
|
||||||
"removed_from_stack": "Removed asset from stack",
|
"removed_from_stack": "Removed asset from stack",
|
||||||
"removed_memory": "Removed memory",
|
"removed_memory": "Removed memory",
|
||||||
|
"removed_passkey": "Removed passkey: {name}",
|
||||||
"removed_tagged_assets": "Removed tag from {count, plural, one {# asset} other {# assets}}",
|
"removed_tagged_assets": "Removed tag from {count, plural, one {# asset} other {# assets}}",
|
||||||
"rename": "Rename",
|
"rename": "Rename",
|
||||||
"repository": "Repository",
|
"repository": "Repository",
|
||||||
@@ -2025,6 +2042,7 @@
|
|||||||
"share_everyone": "Share everyone",
|
"share_everyone": "Share everyone",
|
||||||
"share_everyone_description": "Give {user} access to all of your people.",
|
"share_everyone_description": "Give {user} access to all of your people.",
|
||||||
"share_link": "Share Link",
|
"share_link": "Share Link",
|
||||||
|
"share_logs": "Share Logs",
|
||||||
"share_original": "Use original (large)",
|
"share_original": "Use original (large)",
|
||||||
"share_preview": "Use preview (small)",
|
"share_preview": "Use preview (small)",
|
||||||
"share_quality_body": "Press and hold the share button to choose the image quality before you share.",
|
"share_quality_body": "Press and hold the share button to choose the image quality before you share.",
|
||||||
@@ -2203,6 +2221,8 @@
|
|||||||
"theme": "Theme",
|
"theme": "Theme",
|
||||||
"theme_selection": "Theme selection",
|
"theme_selection": "Theme selection",
|
||||||
"theme_selection_description": "Automatically set the theme to light or dark based on your browser's system preference",
|
"theme_selection_description": "Automatically set the theme to light or dark based on your browser's system preference",
|
||||||
|
"theme_setting_asset_list_smooth_scrubbing_subtitle": "Scroll continuously when dragging the scrollbar instead of snapping to months",
|
||||||
|
"theme_setting_asset_list_smooth_scrubbing_title": "Smooth timeline scrubbing",
|
||||||
"theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles",
|
"theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles",
|
||||||
"theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({count})",
|
"theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({count})",
|
||||||
"theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.",
|
"theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.",
|
||||||
@@ -2318,7 +2338,7 @@
|
|||||||
"user": "User",
|
"user": "User",
|
||||||
"user_has_been_deleted": "This user has been deleted.",
|
"user_has_been_deleted": "This user has been deleted.",
|
||||||
"user_id": "User ID",
|
"user_id": "User ID",
|
||||||
"user_liked": "{user} liked {type, select, photo {this photo} video {this video} asset {this asset} other {it}}",
|
"user_liked": "{user} liked {type, select, image {this photo} video {this video} other {this}}",
|
||||||
"user_pin_code_settings": "PIN Code",
|
"user_pin_code_settings": "PIN Code",
|
||||||
"user_pin_code_settings_description": "Manage your PIN code",
|
"user_pin_code_settings_description": "Manage your PIN code",
|
||||||
"user_privacy": "User Privacy",
|
"user_privacy": "User Privacy",
|
||||||
|
|||||||
@@ -323,6 +323,13 @@
|
|||||||
"oauth_timeout": "Таймаут для запросов",
|
"oauth_timeout": "Таймаут для запросов",
|
||||||
"oauth_timeout_description": "Максимальное время, в течение которого ожидать ответа, в миллисекундах",
|
"oauth_timeout_description": "Максимальное время, в течение которого ожидать ответа, в миллисекундах",
|
||||||
"ocr_job_description": "Использование машинного обучения для распознавания текста на изображениях",
|
"ocr_job_description": "Использование машинного обучения для распознавания текста на изображениях",
|
||||||
|
"passkey_additional_domains": "Дополнительные домены",
|
||||||
|
"passkey_additional_domains_description": "Дополнительные доверенные домены. Для их работы основной домен ключей доступа также должен быть доступен.",
|
||||||
|
"passkey_domain": "Домен ключей доступа",
|
||||||
|
"passkey_domain_description": "Домен для ключей доступа. Оставьте пустым для определения по адресу сайта. При изменении домена существующие ключи перестанут работать.",
|
||||||
|
"passkey_enable_description": "Вход с помощью ключа доступа",
|
||||||
|
"passkey_settings": "Вход по ключу доступа",
|
||||||
|
"passkey_settings_description": "Настройки входа по ключу доступа",
|
||||||
"password_enable_description": "Вход по электронной почте и паролю",
|
"password_enable_description": "Вход по электронной почте и паролю",
|
||||||
"password_settings": "Настройки входа с паролем",
|
"password_settings": "Настройки входа с паролем",
|
||||||
"password_settings_description": "Управление настройками входа по паролю",
|
"password_settings_description": "Управление настройками входа по паролю",
|
||||||
@@ -873,6 +880,8 @@
|
|||||||
"delete_local_dialog_ok_backed_up_only": "Удалить только резервные копии",
|
"delete_local_dialog_ok_backed_up_only": "Удалить только резервные копии",
|
||||||
"delete_local_dialog_ok_force": "Все равно удалить",
|
"delete_local_dialog_ok_force": "Все равно удалить",
|
||||||
"delete_others": "Удалить остальные",
|
"delete_others": "Удалить остальные",
|
||||||
|
"delete_passkey": "Удалить ключ доступа",
|
||||||
|
"delete_passkey_prompt": "Удалить этот ключ доступа?",
|
||||||
"delete_permanently": "Удалить навсегда",
|
"delete_permanently": "Удалить навсегда",
|
||||||
"delete_permanently_action_prompt": "Объекты удалены безвозвратно ({count} шт.)",
|
"delete_permanently_action_prompt": "Объекты удалены безвозвратно ({count} шт.)",
|
||||||
"delete_shared_link": "Удалить публичную ссылку",
|
"delete_shared_link": "Удалить публичную ссылку",
|
||||||
@@ -942,6 +951,7 @@
|
|||||||
"edit_location_action_prompt": "Места изменены ({count} шт.)",
|
"edit_location_action_prompt": "Места изменены ({count} шт.)",
|
||||||
"edit_location_dialog_title": "Местоположение",
|
"edit_location_dialog_title": "Местоположение",
|
||||||
"edit_name": "Изменить имя",
|
"edit_name": "Изменить имя",
|
||||||
|
"edit_passkey": "Изменить ключ доступа",
|
||||||
"edit_people": "Изменить людей",
|
"edit_people": "Изменить людей",
|
||||||
"edit_person": "Изменить человека",
|
"edit_person": "Изменить человека",
|
||||||
"edit_tag": "Изменить тег",
|
"edit_tag": "Изменить тег",
|
||||||
@@ -1287,6 +1297,7 @@
|
|||||||
"last": "Последний",
|
"last": "Последний",
|
||||||
"last_months": "{count, plural, one {Последний месяц} many {# последних месяцев} other {# последних месяца}}",
|
"last_months": "{count, plural, one {Последний месяц} many {# последних месяцев} other {# последних месяца}}",
|
||||||
"last_seen": "Последний доступ",
|
"last_seen": "Последний доступ",
|
||||||
|
"last_used": "Последнее использование",
|
||||||
"latest_version": "Последняя версия",
|
"latest_version": "Последняя версия",
|
||||||
"latitude": "Широта",
|
"latitude": "Широта",
|
||||||
"leave": "Покинуть",
|
"leave": "Покинуть",
|
||||||
@@ -1379,6 +1390,7 @@
|
|||||||
"login_has_been_disabled": "Вход в систему был отключён",
|
"login_has_been_disabled": "Вход в систему был отключён",
|
||||||
"login_password_changed_error": "Произошла ошибка при обновлении пароля",
|
"login_password_changed_error": "Произошла ошибка при обновлении пароля",
|
||||||
"login_password_changed_success": "Пароль успешно обновлен",
|
"login_password_changed_success": "Пароль успешно обновлен",
|
||||||
|
"login_with_passkey": "Войти с ключом доступа",
|
||||||
"logout_all_device_confirmation": "Вы действительно хотите завершить все сеансы, кроме текущего?",
|
"logout_all_device_confirmation": "Вы действительно хотите завершить все сеансы, кроме текущего?",
|
||||||
"logout_this_device_confirmation": "Вы действительно хотите завершить сеанс на этом устройстве?",
|
"logout_this_device_confirmation": "Вы действительно хотите завершить сеанс на этом устройстве?",
|
||||||
"logs": "Журнал событий",
|
"logs": "Журнал событий",
|
||||||
@@ -1427,6 +1439,7 @@
|
|||||||
"manage_your_api_keys": "Управление API ключами для взаимодействия с другими программами",
|
"manage_your_api_keys": "Управление API ключами для взаимодействия с другими программами",
|
||||||
"manage_your_devices": "Управление устройствами, на которых вы входили в свой аккаунт",
|
"manage_your_devices": "Управление устройствами, на которых вы входили в свой аккаунт",
|
||||||
"manage_your_oauth_connection": "Настройки подключённого OAuth",
|
"manage_your_oauth_connection": "Настройки подключённого OAuth",
|
||||||
|
"manage_your_passkeys": "Управление ключами доступа",
|
||||||
"map": "Карта",
|
"map": "Карта",
|
||||||
"map_assets_in_bounds": "{count, plural, =0 {Нет фото в этой области} other {# фото}}",
|
"map_assets_in_bounds": "{count, plural, =0 {Нет фото в этой области} other {# фото}}",
|
||||||
"map_cannot_get_user_location": "Невозможно получить местоположение пользователя",
|
"map_cannot_get_user_location": "Невозможно получить местоположение пользователя",
|
||||||
@@ -1546,6 +1559,7 @@
|
|||||||
"new_album": "Новый альбом",
|
"new_album": "Новый альбом",
|
||||||
"new_api_key": "Новый API ключ",
|
"new_api_key": "Новый API ключ",
|
||||||
"new_feature": "Новая функциональность",
|
"new_feature": "Новая функциональность",
|
||||||
|
"new_passkey": "Новый ключ доступа",
|
||||||
"new_password": "Новый пароль",
|
"new_password": "Новый пароль",
|
||||||
"new_person": "Новый человек",
|
"new_person": "Новый человек",
|
||||||
"new_pin_code": "Новый PIN-код",
|
"new_pin_code": "Новый PIN-код",
|
||||||
@@ -1658,6 +1672,8 @@
|
|||||||
"partner_sharing": "Совместное использование",
|
"partner_sharing": "Совместное использование",
|
||||||
"partners": "Партнёры",
|
"partners": "Партнёры",
|
||||||
"partners_description": "Делитесь своими фото и видео с другими пользователями",
|
"partners_description": "Делитесь своими фото и видео с другими пользователями",
|
||||||
|
"passkeys": "{count, plural, one {Ключ доступа} few {Ключи доступа} many {Ключи доступа} other {Ключи доступа}}",
|
||||||
|
"passkeys_not_supported": "Этот браузер не поддерживает ключи доступа",
|
||||||
"password": "Пароль",
|
"password": "Пароль",
|
||||||
"password_does_not_match": "Пароли не совпадают",
|
"password_does_not_match": "Пароли не совпадают",
|
||||||
"password_required": "Требуется пароль",
|
"password_required": "Требуется пароль",
|
||||||
@@ -1842,6 +1858,7 @@
|
|||||||
"removed_from_favorites_count": "{count, plural, one {# объект удалён} many {# объектов удалено} other {# объекта удалено}} из избранного",
|
"removed_from_favorites_count": "{count, plural, one {# объект удалён} many {# объектов удалено} other {# объекта удалено}} из избранного",
|
||||||
"removed_from_stack": "Объект удалён из группы",
|
"removed_from_stack": "Объект удалён из группы",
|
||||||
"removed_memory": "Воспоминание удалено",
|
"removed_memory": "Воспоминание удалено",
|
||||||
|
"removed_passkey": "Удалён ключ доступа: {name}",
|
||||||
"removed_tagged_assets": "Тег удалён у {count, plural, one {# объекта} other {# объектов}}",
|
"removed_tagged_assets": "Тег удалён у {count, plural, one {# объекта} other {# объектов}}",
|
||||||
"rename": "Переименовать",
|
"rename": "Переименовать",
|
||||||
"repository": "Репозиторий",
|
"repository": "Репозиторий",
|
||||||
@@ -2027,6 +2044,7 @@
|
|||||||
"share_everyone": "Поделиться со всеми",
|
"share_everyone": "Поделиться со всеми",
|
||||||
"share_everyone_description": "Предоставить пользователю {user} доступ всем своим людям.",
|
"share_everyone_description": "Предоставить пользователю {user} доступ всем своим людям.",
|
||||||
"share_link": "Создать ссылку",
|
"share_link": "Создать ссылку",
|
||||||
|
"share_logs": "Поделиться журналами",
|
||||||
"share_original": "Оригинал (большой размер)",
|
"share_original": "Оригинал (большой размер)",
|
||||||
"share_preview": "Предпросмотр (небольшой размер)",
|
"share_preview": "Предпросмотр (небольшой размер)",
|
||||||
"share_quality_body": "Нажмите и удерживайте кнопку «Поделиться», чтобы выбрать качество изображения перед отправкой.",
|
"share_quality_body": "Нажмите и удерживайте кнопку «Поделиться», чтобы выбрать качество изображения перед отправкой.",
|
||||||
@@ -2206,6 +2224,8 @@
|
|||||||
"theme": "Тема",
|
"theme": "Тема",
|
||||||
"theme_selection": "Выбор темы",
|
"theme_selection": "Выбор темы",
|
||||||
"theme_selection_description": "Автоматически устанавливать светлую или тёмную тему в зависимости от настроек вашего браузера",
|
"theme_selection_description": "Автоматически устанавливать светлую или тёмную тему в зависимости от настроек вашего браузера",
|
||||||
|
"theme_setting_asset_list_smooth_scrubbing_subtitle": "Прокручивать ленту плавно при перетаскивании полосы прокрутки, без переходов по месяцам",
|
||||||
|
"theme_setting_asset_list_smooth_scrubbing_title": "Плавная прокрутка ленты",
|
||||||
"theme_setting_asset_list_storage_indicator_title": "Отображать индикатор хранилища на плитках объектов",
|
"theme_setting_asset_list_storage_indicator_title": "Отображать индикатор хранилища на плитках объектов",
|
||||||
"theme_setting_asset_list_tiles_per_row_title": "Количество объектов в строке ({count})",
|
"theme_setting_asset_list_tiles_per_row_title": "Количество объектов в строке ({count})",
|
||||||
"theme_setting_colorful_interface_subtitle": "Добавить оттенок к фону.",
|
"theme_setting_colorful_interface_subtitle": "Добавить оттенок к фону.",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
ARG DEVICE=cpu
|
ARG DEVICE=cpu
|
||||||
|
|
||||||
FROM python:3.13-slim-trixie@sha256:9d2e5553305c7c7b0097999bb17187c69b921ccd6bc9d40e4bb5ebe652c00285 AS builder-cpu
|
FROM python:3.13-slim-trixie@sha256:cf50a4f6ac4f026458b08d5222023d700ac806410dff5e9ca8728fa44f2b850f AS builder-cpu
|
||||||
|
|
||||||
FROM python:3.13-slim-trixie@sha256:7c61056e61ac89e852de05f3dc6fa51a6dd2181797bceed46aa725dd7cb2cd3b AS builder-openvino
|
FROM python:3.13-slim-trixie@sha256:cf50a4f6ac4f026458b08d5222023d700ac806410dff5e9ca8728fa44f2b850f AS builder-openvino
|
||||||
|
|
||||||
# renovate: datasource=github-releases depName=microsoft/onnxruntime
|
# renovate: datasource=github-releases depName=microsoft/onnxruntime
|
||||||
ARG ONNXRUNTIME_VERSION="v1.30.0"
|
ARG ONNXRUNTIME_VERSION="v1.30.0"
|
||||||
@@ -59,7 +59,7 @@ RUN mkdir /opt/armnn && \
|
|||||||
cd /opt/ann && \
|
cd /opt/ann && \
|
||||||
sh build.sh
|
sh build.sh
|
||||||
|
|
||||||
FROM python:3.12-slim-trixie@sha256:78387bc3881b8273120a12ebe6c1ab22b018ccc2c9adf565ae1ac9b536e184ea AS builder-rknn
|
FROM python:3.12-slim-trixie@sha256:cb33f7f771ecec2ef1225cf39b159591a040391bd73e4c01f00fdfc4c47611d2 AS builder-rknn
|
||||||
|
|
||||||
# Warning: 25GiB+ disk space required to pull this image
|
# Warning: 25GiB+ disk space required to pull this image
|
||||||
# TODO: find a way to reduce the image size
|
# TODO: find a way to reduce the image size
|
||||||
@@ -81,11 +81,11 @@ RUN --mount=type=cache,target=/root/.cache/uv \
|
|||||||
uv sync --frozen --extra ${DEVICE} --no-dev --no-editable --no-install-project --compile-bytecode --no-progress --active --link-mode copy && \
|
uv sync --frozen --extra ${DEVICE} --no-dev --no-editable --no-install-project --compile-bytecode --no-progress --active --link-mode copy && \
|
||||||
if [ "$DEVICE" = "openvino" ]; then uv pip install --no-deps /opt/onnxruntime_openvino-*.whl; fi
|
if [ "$DEVICE" = "openvino" ]; then uv pip install --no-deps /opt/onnxruntime_openvino-*.whl; fi
|
||||||
|
|
||||||
FROM python:3.13-slim-trixie@sha256:9d2e5553305c7c7b0097999bb17187c69b921ccd6bc9d40e4bb5ebe652c00285 AS prod-cpu
|
FROM python:3.13-slim-trixie@sha256:cf50a4f6ac4f026458b08d5222023d700ac806410dff5e9ca8728fa44f2b850f AS prod-cpu
|
||||||
|
|
||||||
ENV MACHINE_LEARNING_MODEL_ARENA=false
|
ENV MACHINE_LEARNING_MODEL_ARENA=false
|
||||||
|
|
||||||
FROM python:3.13-slim-trixie@sha256:9d2e5553305c7c7b0097999bb17187c69b921ccd6bc9d40e4bb5ebe652c00285 AS prod-openvino
|
FROM python:3.13-slim-trixie@sha256:cf50a4f6ac4f026458b08d5222023d700ac806410dff5e9ca8728fa44f2b850f AS prod-openvino
|
||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \
|
apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \
|
||||||
@@ -141,7 +141,7 @@ COPY --from=builder-armnn \
|
|||||||
/opt/ann/build.sh \
|
/opt/ann/build.sh \
|
||||||
/opt/armnn/
|
/opt/armnn/
|
||||||
|
|
||||||
FROM python:3.12-slim-trixie@sha256:78387bc3881b8273120a12ebe6c1ab22b018ccc2c9adf565ae1ac9b536e184ea AS prod-rknn
|
FROM python:3.12-slim-trixie@sha256:cb33f7f771ecec2ef1225cf39b159591a040391bd73e4c01f00fdfc4c47611d2 AS prod-rknn
|
||||||
|
|
||||||
# renovate: datasource=github-tags depName=airockchip/rknn-toolkit2
|
# renovate: datasource=github-tags depName=airockchip/rknn-toolkit2
|
||||||
ARG RKNN_TOOLKIT_VERSION="v2.3.0"
|
ARG RKNN_TOOLKIT_VERSION="v2.3.0"
|
||||||
@@ -150,7 +150,7 @@ ENV MACHINE_LEARNING_MODEL_ARENA=false
|
|||||||
|
|
||||||
ADD --chmod=644 --checksum=sha256:73993ed4b440460825f21611731564503cc1d5a0c123746477da6cd574f34885 "https://github.com/airockchip/rknn-toolkit2/raw/refs/tags/${RKNN_TOOLKIT_VERSION}/rknpu2/runtime/Linux/librknn_api/aarch64/librknnrt.so" /usr/lib/
|
ADD --chmod=644 --checksum=sha256:73993ed4b440460825f21611731564503cc1d5a0c123746477da6cd574f34885 "https://github.com/airockchip/rknn-toolkit2/raw/refs/tags/${RKNN_TOOLKIT_VERSION}/rknpu2/runtime/Linux/librknn_api/aarch64/librknnrt.so" /usr/lib/
|
||||||
|
|
||||||
FROM python:3.13-slim-trixie@sha256:9d2e5553305c7c7b0097999bb17187c69b921ccd6bc9d40e4bb5ebe652c00285 AS mimalloc
|
FROM python:3.13-slim-trixie@sha256:cf50a4f6ac4f026458b08d5222023d700ac806410dff5e9ca8728fa44f2b850f AS mimalloc
|
||||||
RUN echo "deb http://deb.debian.org/debian trixie-backports main" > /etc/apt/sources.list.d/backports.list && \
|
RUN echo "deb http://deb.debian.org/debian trixie-backports main" > /etc/apt/sources.list.d/backports.list && \
|
||||||
apt-get update && \
|
apt-get update && \
|
||||||
apt-get install -y --no-install-recommends libmimalloc3/trixie-backports && \
|
apt-get install -y --no-install-recommends libmimalloc3/trixie-backports && \
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from collections.abc import Hashable
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from immich_ml import allocator
|
from immich_ml import allocator
|
||||||
|
from immich_ml.config import clean_name
|
||||||
from immich_ml.models.base import InferenceEntry, InferenceModel
|
from immich_ml.models.base import InferenceEntry, InferenceModel
|
||||||
from immich_ml.schemas import Options
|
from immich_ml.schemas import Options
|
||||||
|
|
||||||
@@ -15,7 +16,8 @@ class ModelCache:
|
|||||||
self._expiries: dict[Key, asyncio.TimerHandle] = {}
|
self._expiries: dict[Key, asyncio.TimerHandle] = {}
|
||||||
|
|
||||||
def get[O: Options](self, entry: InferenceEntry[O], ttl: int | None = None) -> InferenceModel[O]:
|
def get[O: Options](self, entry: InferenceEntry[O], ttl: int | None = None) -> InferenceModel[O]:
|
||||||
key = (entry.model, entry.name, entry.model.graph(entry.options))
|
# names that differ only in what cleaning drops, like an `immich-app/` prefix, are the same model
|
||||||
|
key = (entry.model, clean_name(entry.name), entry.model.graph(entry.options))
|
||||||
if key not in self._models:
|
if key not in self._models:
|
||||||
self._models[key] = entry.model.create(entry.name, entry.options)
|
self._models[key] = entry.model.create(entry.name, entry.options)
|
||||||
elif key not in self._expiries:
|
elif key not in self._expiries:
|
||||||
|
|||||||
@@ -7,10 +7,14 @@ import numpy as np
|
|||||||
import orjson
|
import orjson
|
||||||
from numpy.typing import NDArray
|
from numpy.typing import NDArray
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
from pillow_jxl import JpegXLImagePlugin
|
||||||
|
|
||||||
_PIL_RESAMPLING_METHODS = {resampling.name.lower(): resampling for resampling in Image.Resampling}
|
_PIL_RESAMPLING_METHODS = {resampling.name.lower(): resampling for resampling in Image.Resampling}
|
||||||
_PUNCTUATION_TRANS = str.maketrans("", "", string.punctuation)
|
_PUNCTUATION_TRANS = str.maketrans("", "", string.punctuation)
|
||||||
|
|
||||||
|
# Requests are already decoded in parallel, so don't start a thread per core for each one
|
||||||
|
JpegXLImagePlugin.DECODE_THREADS = 0
|
||||||
|
|
||||||
|
|
||||||
def resize_pil(img: Image.Image, size: int, resample: Image.Resampling = Image.Resampling.BICUBIC) -> Image.Image:
|
def resize_pil(img: Image.Image, size: int, resample: Image.Resampling = Image.Resampling.BICUBIC) -> Image.Image:
|
||||||
if img.width < img.height:
|
if img.width < img.height:
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ dependencies = [
|
|||||||
"onnx>=1.22.0",
|
"onnx>=1.22.0",
|
||||||
"opencv-python-headless>=4.7.0.72,<5.0",
|
"opencv-python-headless>=4.7.0.72,<5.0",
|
||||||
"orjson>=3.9.5",
|
"orjson>=3.9.5",
|
||||||
"pillow>=12.2,<13",
|
"pillow>=13,<14",
|
||||||
|
"pillow-jxl-plugin>=1.3.8,<2.0",
|
||||||
"pydantic>=2.0.0,<3",
|
"pydantic>=2.0.0,<3",
|
||||||
"pydantic-settings>=2.5.2,<3",
|
"pydantic-settings>=2.5.2,<3",
|
||||||
"python-multipart>=0.0.6,<1.0",
|
"python-multipart>=0.0.6,<1.0",
|
||||||
@@ -64,7 +65,7 @@ compile-bytecode = true
|
|||||||
override-dependencies = ["opencv-python ; sys_platform == 'never'"] # only install the headless version
|
override-dependencies = ["opencv-python ; sys_platform == 'never'"] # only install the headless version
|
||||||
|
|
||||||
[tool.uv.sources]
|
[tool.uv.sources]
|
||||||
immich-model = { git = "https://github.com/immich-app/ml-models.git", rev = "8bf3fd1b44b0c8608f52fb6e50ddff52826576d8" }
|
immich-model = { git = "https://github.com/immich-app/ml-models.git", rev = "a1eeff2c2118d76067efecf4cc4630998eac519a" }
|
||||||
onnxruntime-gpu = { index = "onnxruntime-cuda-12" }
|
onnxruntime-gpu = { index = "onnxruntime-cuda-12" }
|
||||||
openvino = { index = "openvino-nightly" }
|
openvino = { index = "openvino-nightly" }
|
||||||
|
|
||||||
|
|||||||
@@ -1856,6 +1856,14 @@ class TestCache:
|
|||||||
assert len(model_cache._models) == 1
|
assert len(model_cache._models) == 1
|
||||||
model.create.assert_called_once()
|
model.create.assert_called_once()
|
||||||
|
|
||||||
|
async def test_shares_an_instance_across_names_of_the_same_model(self) -> None:
|
||||||
|
model_cache = ModelCache()
|
||||||
|
bare = model_cache.get(InferenceEntry(OpenClipTextualEncoder, "ViT-B-32__openai", TextualOptions()))
|
||||||
|
prefixed = model_cache.get(
|
||||||
|
InferenceEntry(OpenClipTextualEncoder, "immich-app/ViT-B-32__openai", TextualOptions(language="en-US"))
|
||||||
|
)
|
||||||
|
assert bare is prefixed
|
||||||
|
|
||||||
async def test_separate_instances_per_graph_option(self) -> None:
|
async def test_separate_instances_per_graph_option(self) -> None:
|
||||||
# the real TextDetector, so that keying it on anything but its resolution fails this
|
# the real TextDetector, so that keying it on anything but its resolution fails this
|
||||||
model_cache = ModelCache()
|
model_cache = ModelCache()
|
||||||
|
|||||||
Generated
+775
-537
File diff suppressed because it is too large
Load Diff
@@ -1,29 +1,22 @@
|
|||||||
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
||||||
|
|
||||||
|
lockfile_version = 3
|
||||||
|
|
||||||
[[tools."github:extism/cli"]]
|
[[tools."github:extism/cli"]]
|
||||||
version = "1.6.3"
|
version = "1.6.3"
|
||||||
backend = "github:extism/cli"
|
backend = "github:extism/cli"
|
||||||
|
specifiers = ["1.6.3"]
|
||||||
|
|
||||||
[tools."github:extism/cli"."platforms.linux-arm64"]
|
[tools."github:extism/cli"."platforms.linux-arm64"]
|
||||||
checksum = "sha256:d92f830c9be39637569feacb04e9750c28848df6d9a219db94152a9b4eb9452b"
|
checksum = "sha256:d92f830c9be39637569feacb04e9750c28848df6d9a219db94152a9b4eb9452b"
|
||||||
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-arm64.tar.gz"
|
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-arm64.tar.gz"
|
||||||
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694030"
|
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694030"
|
||||||
|
|
||||||
[tools."github:extism/cli"."platforms.linux-arm64-musl"]
|
|
||||||
checksum = "sha256:d92f830c9be39637569feacb04e9750c28848df6d9a219db94152a9b4eb9452b"
|
|
||||||
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-arm64.tar.gz"
|
|
||||||
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694030"
|
|
||||||
|
|
||||||
[tools."github:extism/cli"."platforms.linux-x64"]
|
[tools."github:extism/cli"."platforms.linux-x64"]
|
||||||
checksum = "sha256:34e7ae9bfded6e2c32dee83f70a4e50d34f9d3e80d1762b09625fe82e214d02d"
|
checksum = "sha256:34e7ae9bfded6e2c32dee83f70a4e50d34f9d3e80d1762b09625fe82e214d02d"
|
||||||
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-amd64.tar.gz"
|
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-amd64.tar.gz"
|
||||||
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694025"
|
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694025"
|
||||||
|
|
||||||
[tools."github:extism/cli"."platforms.linux-x64-musl"]
|
|
||||||
checksum = "sha256:34e7ae9bfded6e2c32dee83f70a4e50d34f9d3e80d1762b09625fe82e214d02d"
|
|
||||||
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-amd64.tar.gz"
|
|
||||||
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694025"
|
|
||||||
|
|
||||||
[tools."github:extism/cli"."platforms.macos-arm64"]
|
[tools."github:extism/cli"."platforms.macos-arm64"]
|
||||||
checksum = "sha256:b4ddbc575b5ac000115247f781723f9b9f284ed87b29c600539d72161b5b29fc"
|
checksum = "sha256:b4ddbc575b5ac000115247f781723f9b9f284ed87b29c600539d72161b5b29fc"
|
||||||
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-darwin-arm64.tar.gz"
|
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-darwin-arm64.tar.gz"
|
||||||
@@ -42,27 +35,18 @@ url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694035"
|
|||||||
[[tools."github:extism/js-pdk"]]
|
[[tools."github:extism/js-pdk"]]
|
||||||
version = "1.7.0"
|
version = "1.7.0"
|
||||||
backend = "github:extism/js-pdk"
|
backend = "github:extism/js-pdk"
|
||||||
|
specifiers = ["v1.7.0"]
|
||||||
|
|
||||||
[tools."github:extism/js-pdk"."platforms.linux-arm64"]
|
[tools."github:extism/js-pdk"."platforms.linux-arm64"]
|
||||||
checksum = "sha256:025f4050b199d68413c159bde1187271ae270021a9f7171e7beb509922821f2a"
|
checksum = "sha256:025f4050b199d68413c159bde1187271ae270021a9f7171e7beb509922821f2a"
|
||||||
url = "https://github.com/extism/js-pdk/releases/download/v1.7.0/extism-js-aarch64-linux-v1.7.0.gz"
|
url = "https://github.com/extism/js-pdk/releases/download/v1.7.0/extism-js-aarch64-linux-v1.7.0.gz"
|
||||||
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/518632503"
|
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/518632503"
|
||||||
|
|
||||||
[tools."github:extism/js-pdk"."platforms.linux-arm64-musl"]
|
|
||||||
checksum = "sha256:025f4050b199d68413c159bde1187271ae270021a9f7171e7beb509922821f2a"
|
|
||||||
url = "https://github.com/extism/js-pdk/releases/download/v1.7.0/extism-js-aarch64-linux-v1.7.0.gz"
|
|
||||||
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/518632503"
|
|
||||||
|
|
||||||
[tools."github:extism/js-pdk"."platforms.linux-x64"]
|
[tools."github:extism/js-pdk"."platforms.linux-x64"]
|
||||||
checksum = "sha256:63b72da2f5e88655522dc21477de549f238a2f40546a69ce4e0fce7e78654035"
|
checksum = "sha256:63b72da2f5e88655522dc21477de549f238a2f40546a69ce4e0fce7e78654035"
|
||||||
url = "https://github.com/extism/js-pdk/releases/download/v1.7.0/extism-js-x86_64-linux-v1.7.0.gz"
|
url = "https://github.com/extism/js-pdk/releases/download/v1.7.0/extism-js-x86_64-linux-v1.7.0.gz"
|
||||||
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/518632991"
|
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/518632991"
|
||||||
|
|
||||||
[tools."github:extism/js-pdk"."platforms.linux-x64-musl"]
|
|
||||||
checksum = "sha256:63b72da2f5e88655522dc21477de549f238a2f40546a69ce4e0fce7e78654035"
|
|
||||||
url = "https://github.com/extism/js-pdk/releases/download/v1.7.0/extism-js-x86_64-linux-v1.7.0.gz"
|
|
||||||
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/518632991"
|
|
||||||
|
|
||||||
[tools."github:extism/js-pdk"."platforms.macos-arm64"]
|
[tools."github:extism/js-pdk"."platforms.macos-arm64"]
|
||||||
checksum = "sha256:12c01c2bb2240a6a05a4f8babe680c793c397d84e6a1497a7ca1312e78a475c3"
|
checksum = "sha256:12c01c2bb2240a6a05a4f8babe680c793c397d84e6a1497a7ca1312e78a475c3"
|
||||||
url = "https://github.com/extism/js-pdk/releases/download/v1.7.0/extism-js-aarch64-macos-v1.7.0.gz"
|
url = "https://github.com/extism/js-pdk/releases/download/v1.7.0/extism-js-aarch64-macos-v1.7.0.gz"
|
||||||
@@ -81,6 +65,7 @@ url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/518636347"
|
|||||||
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
|
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
|
||||||
version = "7.1.3-6"
|
version = "7.1.3-6"
|
||||||
backend = "github:jellyfin/jellyfin-ffmpeg"
|
backend = "github:jellyfin/jellyfin-ffmpeg"
|
||||||
|
specifiers = ["7.1.3-6"]
|
||||||
|
|
||||||
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.windows-x64"]
|
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.windows-x64"]
|
||||||
checksum = "sha256:7b7168149689610296f3a187c717056ce0786cc125a31caf28056737e9ba1cc1"
|
checksum = "sha256:7b7168149689610296f3a187c717056ce0786cc125a31caf28056737e9ba1cc1"
|
||||||
@@ -90,18 +75,33 @@ url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets
|
|||||||
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
|
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
|
||||||
version = "7.1.3-6"
|
version = "7.1.3-6"
|
||||||
backend = "github:jellyfin/jellyfin-ffmpeg"
|
backend = "github:jellyfin/jellyfin-ffmpeg"
|
||||||
|
specifiers = ["7.1.3-6"]
|
||||||
|
|
||||||
[tools."github:jellyfin/jellyfin-ffmpeg".options]
|
[tools."github:jellyfin/jellyfin-ffmpeg".options]
|
||||||
asset_pattern = "jellyfin-ffmpeg_*_portable_macarm64-gpl.tar.xz"
|
asset_pattern = "jellyfin-ffmpeg_*_portable_linux64-gpl.tar.xz"
|
||||||
|
|
||||||
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.macos-arm64"]
|
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64"]
|
||||||
checksum = "sha256:e024d5e78d5414e75f0181036cd21373fafb9270c72894dfd7dbda2572439820"
|
checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2"
|
||||||
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_macarm64-gpl.tar.xz"
|
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz"
|
||||||
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/408995838"
|
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879"
|
||||||
|
|
||||||
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
|
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
|
||||||
version = "7.1.3-6"
|
version = "7.1.3-6"
|
||||||
backend = "github:jellyfin/jellyfin-ffmpeg"
|
backend = "github:jellyfin/jellyfin-ffmpeg"
|
||||||
|
specifiers = ["7.1.3-6"]
|
||||||
|
|
||||||
|
[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"]]
|
||||||
|
version = "7.1.3-6"
|
||||||
|
backend = "github:jellyfin/jellyfin-ffmpeg"
|
||||||
|
specifiers = ["7.1.3-6"]
|
||||||
|
|
||||||
[tools."github:jellyfin/jellyfin-ffmpeg".options]
|
[tools."github:jellyfin/jellyfin-ffmpeg".options]
|
||||||
asset_pattern = "jellyfin-ffmpeg_*_portable_mac64-gpl.tar.xz"
|
asset_pattern = "jellyfin-ffmpeg_*_portable_mac64-gpl.tar.xz"
|
||||||
@@ -114,61 +114,31 @@ url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets
|
|||||||
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
|
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
|
||||||
version = "7.1.3-6"
|
version = "7.1.3-6"
|
||||||
backend = "github:jellyfin/jellyfin-ffmpeg"
|
backend = "github:jellyfin/jellyfin-ffmpeg"
|
||||||
|
specifiers = ["7.1.3-6"]
|
||||||
|
|
||||||
[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_macarm64-gpl.tar.xz"
|
||||||
|
|
||||||
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64"]
|
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.macos-arm64"]
|
||||||
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
|
checksum = "sha256:e024d5e78d5414e75f0181036cd21373fafb9270c72894dfd7dbda2572439820"
|
||||||
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_macarm64-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/408995838"
|
||||||
|
|
||||||
[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]
|
|
||||||
asset_pattern = "jellyfin-ffmpeg_*_portable_linux64-gpl.tar.xz"
|
|
||||||
|
|
||||||
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64"]
|
|
||||||
checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2"
|
|
||||||
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz"
|
|
||||||
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879"
|
|
||||||
|
|
||||||
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64-musl"]
|
|
||||||
checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2"
|
|
||||||
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz"
|
|
||||||
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879"
|
|
||||||
|
|
||||||
[[tools."github:webassembly/binaryen"]]
|
[[tools."github:webassembly/binaryen"]]
|
||||||
version = "version_124"
|
version = "version_124"
|
||||||
backend = "github:webassembly/binaryen"
|
backend = "github:webassembly/binaryen"
|
||||||
|
specifiers = ["version_124"]
|
||||||
|
|
||||||
[tools."github:webassembly/binaryen"."platforms.linux-arm64"]
|
[tools."github:webassembly/binaryen"."platforms.linux-arm64"]
|
||||||
checksum = "sha256:6291bd9a57d8e046f3bc099a4db386c147433a87f71c783a901c5b1792e38de3"
|
checksum = "sha256:6291bd9a57d8e046f3bc099a4db386c147433a87f71c783a901c5b1792e38de3"
|
||||||
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-aarch64-linux.tar.gz"
|
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-aarch64-linux.tar.gz"
|
||||||
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288927659"
|
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288927659"
|
||||||
|
|
||||||
[tools."github:webassembly/binaryen"."platforms.linux-arm64-musl"]
|
|
||||||
checksum = "sha256:6291bd9a57d8e046f3bc099a4db386c147433a87f71c783a901c5b1792e38de3"
|
|
||||||
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-aarch64-linux.tar.gz"
|
|
||||||
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288927659"
|
|
||||||
|
|
||||||
[tools."github:webassembly/binaryen"."platforms.linux-x64"]
|
[tools."github:webassembly/binaryen"."platforms.linux-x64"]
|
||||||
checksum = "sha256:0290c3779fedf592b8da0ded3032ff55c41a2b7bfa2d6bf7b7bac6f0e6e28963"
|
checksum = "sha256:0290c3779fedf592b8da0ded3032ff55c41a2b7bfa2d6bf7b7bac6f0e6e28963"
|
||||||
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-linux.tar.gz"
|
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-linux.tar.gz"
|
||||||
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288926769"
|
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288926769"
|
||||||
|
|
||||||
[tools."github:webassembly/binaryen"."platforms.linux-x64-musl"]
|
|
||||||
checksum = "sha256:0290c3779fedf592b8da0ded3032ff55c41a2b7bfa2d6bf7b7bac6f0e6e28963"
|
|
||||||
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-linux.tar.gz"
|
|
||||||
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288926769"
|
|
||||||
|
|
||||||
[tools."github:webassembly/binaryen"."platforms.macos-arm64"]
|
[tools."github:webassembly/binaryen"."platforms.macos-arm64"]
|
||||||
checksum = "sha256:86a2c960ff62c6d2ea6009d1f89745c22c70100d394a095eab45eb941bdaa24c"
|
checksum = "sha256:86a2c960ff62c6d2ea6009d1f89745c22c70100d394a095eab45eb941bdaa24c"
|
||||||
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-arm64-macos.tar.gz"
|
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-arm64-macos.tar.gz"
|
||||||
@@ -187,9 +157,10 @@ url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288
|
|||||||
[[tools.java]]
|
[[tools.java]]
|
||||||
version = "21.0.2"
|
version = "21.0.2"
|
||||||
backend = "core:java"
|
backend = "core:java"
|
||||||
|
specifiers = ["21.0.2"]
|
||||||
|
|
||||||
[tools.java.options]
|
[tools.java.options]
|
||||||
shorthand_vendor = "openjdk"
|
shorthand_vendor = "temurin"
|
||||||
|
|
||||||
[tools.java."platforms.linux-arm64"]
|
[tools.java."platforms.linux-arm64"]
|
||||||
checksum = "sha256:08db1392a48d4eb5ea5315cf8f18b89dbaf36cda663ba882cf03c704c9257ec2"
|
checksum = "sha256:08db1392a48d4eb5ea5315cf8f18b89dbaf36cda663ba882cf03c704c9257ec2"
|
||||||
@@ -212,159 +183,151 @@ checksum = "sha256:b6c17e747ae78cdd6de4d7532b3164b277daee97c007d3eaa2b39cca99882
|
|||||||
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_windows-x64_bin.zip"
|
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_windows-x64_bin.zip"
|
||||||
|
|
||||||
[[tools.node]]
|
[[tools.node]]
|
||||||
version = "24.21.0"
|
version = "24.22.1"
|
||||||
backend = "core:node"
|
backend = "core:node"
|
||||||
|
specifiers = ["24.22.1"]
|
||||||
|
|
||||||
[tools.node."platforms.linux-arm64"]
|
[tools.node."platforms.linux-arm64"]
|
||||||
checksum = "sha256:724282c3b43aec998aa9527380465b45d229e021b58035f5f4f63095eabfe5d5"
|
checksum = "sha256:33c37be234ba8bee041961047b19160f4d7d0f164e8e246f78bdd10feff148b3"
|
||||||
url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-linux-arm64.tar.gz"
|
url = "https://nodejs.org/dist/v24.22.1/node-v24.22.1-linux-arm64.tar.gz"
|
||||||
|
|
||||||
[tools.node."platforms.linux-arm64-musl"]
|
[tools.node."platforms.linux-arm64-musl"]
|
||||||
checksum = "sha256:3048b0811e158ca0d8672b59c839861763e144492980d8d29b369dfee45747e4"
|
checksum = "sha256:3048b0811e158ca0d8672b59c839861763e144492980d8d29b369dfee45747e4"
|
||||||
url = "https://unofficial-builds.nodejs.org/download/release/v24.21.0/node-v24.21.0-linux-arm64-musl.tar.gz"
|
url = "https://unofficial-builds.nodejs.org/download/release/v24.21.0/node-v24.21.0-linux-arm64-musl.tar.gz"
|
||||||
|
|
||||||
[tools.node."platforms.linux-x64"]
|
[tools.node."platforms.linux-x64"]
|
||||||
checksum = "sha256:6e1db87ef58b8819e5d5402eff1536491b18edd8eb7bee5ef7897876e88dc5ff"
|
checksum = "sha256:eb6728e77c576a35162c122c45ccadf224d89ff00abc8dae46cc2897c6002d7c"
|
||||||
url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-linux-x64.tar.gz"
|
url = "https://nodejs.org/dist/v24.22.1/node-v24.22.1-linux-x64.tar.gz"
|
||||||
|
|
||||||
[tools.node."platforms.linux-x64-musl"]
|
[tools.node."platforms.linux-x64-musl"]
|
||||||
checksum = "sha256:01a713e34da98e7b4d2a3191ee7ca3f93dd0384d1a2ef23c379b2161ee6e133a"
|
checksum = "sha256:01a713e34da98e7b4d2a3191ee7ca3f93dd0384d1a2ef23c379b2161ee6e133a"
|
||||||
url = "https://unofficial-builds.nodejs.org/download/release/v24.21.0/node-v24.21.0-linux-x64-musl.tar.gz"
|
url = "https://unofficial-builds.nodejs.org/download/release/v24.21.0/node-v24.21.0-linux-x64-musl.tar.gz"
|
||||||
|
|
||||||
[tools.node."platforms.macos-arm64"]
|
[tools.node."platforms.macos-arm64"]
|
||||||
checksum = "sha256:bed7eea5325e1108f32ce5228ddd6a5f0f08a499ee42aa7442aea583702f6057"
|
checksum = "sha256:89c682593d76cd44233fa53b1563264e7de9cbb189cad169135c5b4fadfbc0a7"
|
||||||
url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-darwin-arm64.tar.gz"
|
url = "https://nodejs.org/dist/v24.22.1/node-v24.22.1-darwin-arm64.tar.gz"
|
||||||
|
|
||||||
[tools.node."platforms.macos-x64"]
|
[tools.node."platforms.macos-x64"]
|
||||||
checksum = "sha256:1462cb3b3046b815cf8ea436d3da450ec1a9f11dac7e5a46b0ada5305d7e8097"
|
checksum = "sha256:d132557e7ee2b85af08b0c8c34338e65c6430f2238d01f727728ef42dc83b035"
|
||||||
url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-darwin-x64.tar.gz"
|
url = "https://nodejs.org/dist/v24.22.1/node-v24.22.1-darwin-x64.tar.gz"
|
||||||
|
|
||||||
[tools.node."platforms.windows-x64"]
|
[tools.node."platforms.windows-x64"]
|
||||||
checksum = "sha256:158f7685b44de51f6c0df1d153526cbcd3e1bc739a8dfc607721cef75de9e541"
|
checksum = "sha256:d2046d28c2c3dcd211ac5145c0bf30e2047ef9dfb306693c8c6e2eccd8b95fa5"
|
||||||
url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-win-x64.zip"
|
url = "https://nodejs.org/dist/v24.22.1/node-v24.22.1-win-x64.zip"
|
||||||
|
|
||||||
[[tools."npm:@openapitools/openapi-generator-cli"]]
|
[[tools."npm:@openapitools/openapi-generator-cli"]]
|
||||||
version = "2.41.0"
|
version = "2.42.0"
|
||||||
|
aube = { path = ".mise/locks/npm-openapitools-openapi-generator-cli/2.42.0", digest = "sha256:ef9f5cf15f958d31297bb706ee83d1347633603f3079431938665c7bc4136856" }
|
||||||
backend = "npm:@openapitools/openapi-generator-cli"
|
backend = "npm:@openapitools/openapi-generator-cli"
|
||||||
|
specifiers = ["2.42.0"]
|
||||||
|
|
||||||
[[tools."npm:oazapfts"]]
|
[[tools."npm:oazapfts"]]
|
||||||
version = "7.5.0"
|
version = "7.5.0"
|
||||||
|
aube = { path = ".mise/locks/npm-oazapfts/7.5.0", digest = "sha256:74214614b0338eaa2baee026d615afde514311c5c76e274f420f8de8b1f0e017" }
|
||||||
backend = "npm:oazapfts"
|
backend = "npm:oazapfts"
|
||||||
|
specifiers = ["7.5.0"]
|
||||||
|
|
||||||
[[tools.opentofu]]
|
[[tools.opentofu]]
|
||||||
version = "1.12.6"
|
version = "1.13.2"
|
||||||
backend = "aqua:opentofu/opentofu"
|
backend = "aqua:opentofu/opentofu"
|
||||||
|
specifiers = ["1.13.2"]
|
||||||
|
|
||||||
[tools.opentofu."platforms.linux-arm64"]
|
[tools.opentofu."platforms.linux-arm64"]
|
||||||
checksum = "sha256:9bd0228a81bcd0c88f7045c74378f45a815779f19897191dff7d9efba9976b9e"
|
checksum = "sha256:88e2d9e3f94f97b76ab2faf55ade87578f56aed19c5e45fa139d70e994366d1f"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_linux_arm64.tar.gz"
|
checksum_source = "mise-versions"
|
||||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762688"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.2/tofu_1.13.2_linux_arm64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/662742799"
|
||||||
[tools.opentofu."platforms.linux-arm64-musl"]
|
|
||||||
checksum = "sha256:9bd0228a81bcd0c88f7045c74378f45a815779f19897191dff7d9efba9976b9e"
|
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_linux_arm64.tar.gz"
|
|
||||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762688"
|
|
||||||
|
|
||||||
[tools.opentofu."platforms.linux-x64"]
|
[tools.opentofu."platforms.linux-x64"]
|
||||||
checksum = "sha256:50a6106fa4de523d09c87af85f3db1dd47535fc005727fdca6852146476b88ec"
|
checksum = "sha256:ac05d9952662c7db28dd57e93f0a68018837bfdf9342384ec0987977c16a9645"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_linux_amd64.tar.gz"
|
checksum_source = "mise-versions"
|
||||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762723"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.2/tofu_1.13.2_linux_amd64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/662742636"
|
||||||
[tools.opentofu."platforms.linux-x64-musl"]
|
|
||||||
checksum = "sha256:50a6106fa4de523d09c87af85f3db1dd47535fc005727fdca6852146476b88ec"
|
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_linux_amd64.tar.gz"
|
|
||||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762723"
|
|
||||||
|
|
||||||
[tools.opentofu."platforms.macos-arm64"]
|
[tools.opentofu."platforms.macos-arm64"]
|
||||||
checksum = "sha256:f958ec5e511063be9feb180ca015a4cb7977566a9cf6a8550bba8c2a9b5aba74"
|
checksum = "sha256:0d1cdc85f38992055b48e7fb161304a2ddfe2e5ee0ff68ffec9d0349c8bd1b59"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_darwin_arm64.tar.gz"
|
checksum_source = "mise-versions"
|
||||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762710"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.2/tofu_1.13.2_darwin_arm64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/662742637"
|
||||||
|
|
||||||
[tools.opentofu."platforms.macos-x64"]
|
[tools.opentofu."platforms.macos-x64"]
|
||||||
checksum = "sha256:44bb1855f372f17f365fb94517906e78da5001da10f4c98de57a39bf982f3a92"
|
checksum = "sha256:dc6e4eb1d3ed6550c2ef052596f933dbcd8c806957665ea83ae7537147cb3074"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_darwin_amd64.tar.gz"
|
checksum_source = "mise-versions"
|
||||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762655"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.2/tofu_1.13.2_darwin_amd64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/662742982"
|
||||||
|
|
||||||
[tools.opentofu."platforms.windows-x64"]
|
[tools.opentofu."platforms.windows-x64"]
|
||||||
checksum = "sha256:bcbcd9ca744af6c4053e2d5e75dae79ff33ad7dc38d45e1233e693d096284538"
|
checksum = "sha256:e74fc2bda0c81ce696b6b7016a4d5c1f557638161239e4101e9ee7d8e0faba59"
|
||||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_windows_amd64.tar.gz"
|
checksum_source = "mise-versions"
|
||||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762708"
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.2/tofu_1.13.2_windows_amd64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/662742635"
|
||||||
|
|
||||||
[[tools.pnpm]]
|
[[tools.pnpm]]
|
||||||
version = "11.27.1"
|
version = "12.15.0"
|
||||||
backend = "aqua:pnpm/pnpm"
|
backend = "aqua:pnpm/pnpm"
|
||||||
|
specifiers = ["12.15.0"]
|
||||||
|
|
||||||
[tools.pnpm."platforms.linux-arm64"]
|
[tools.pnpm."platforms.linux-arm64"]
|
||||||
checksum = "sha256:dbf8f84263d2bc696306d4219be68256d881d9af409345374d878676a7edcf0c"
|
checksum = "sha256:d7ce93a2c7d847d3a34bd3f559b60c14f578b1320c17cc352a2c5c5fdf956117"
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.27.1/pnpm-linux-arm64.tar.gz"
|
url = "https://github.com/pnpm/pnpm/releases/download/v12.15.0/pnpm-linux-arm64.tar.gz"
|
||||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/577602580"
|
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/662173352"
|
||||||
provenance = "github-attestations"
|
|
||||||
|
|
||||||
[tools.pnpm."platforms.linux-arm64-musl"]
|
|
||||||
checksum = "sha256:7a7854e90bdb7d339430d9943ea482b34d7bde817cfc1c749d3c04ddd464142d"
|
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.27.1/pnpm-linux-arm64-musl.tar.gz"
|
|
||||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/577602583"
|
|
||||||
provenance = "github-attestations"
|
provenance = "github-attestations"
|
||||||
|
|
||||||
[tools.pnpm."platforms.linux-x64"]
|
[tools.pnpm."platforms.linux-x64"]
|
||||||
checksum = "sha256:6be1426540406747b7731564a46c5b30d3023ea6c19e4ce99f994c4ec122f3d2"
|
checksum = "sha256:55b5ed0092bd11176c1669598e231ae52e7cedac59de6efd0d415730f37d0598"
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.27.1/pnpm-linux-x64.tar.gz"
|
url = "https://github.com/pnpm/pnpm/releases/download/v12.15.0/pnpm-linux-x64.tar.gz"
|
||||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/577602576"
|
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/662173360"
|
||||||
provenance = "github-attestations"
|
|
||||||
|
|
||||||
[tools.pnpm."platforms.linux-x64-musl"]
|
|
||||||
checksum = "sha256:cc66c1558a1a03b320ff5b4022f755386fa1de54db3f97ff624112015be1ccf1"
|
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.27.1/pnpm-linux-x64-musl.tar.gz"
|
|
||||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/577602579"
|
|
||||||
provenance = "github-attestations"
|
provenance = "github-attestations"
|
||||||
|
|
||||||
[tools.pnpm."platforms.macos-arm64"]
|
[tools.pnpm."platforms.macos-arm64"]
|
||||||
checksum = "sha256:352f67ceaaa5aba294a53f483da93c376005b4c7e785fe5411e0ce720fe74741"
|
checksum = "sha256:6a41a2877773e97f19832067187cc6fc82e186d8a2cf91fec4b4c1a5fbd6ef27"
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.27.1/pnpm-darwin-arm64.tar.gz"
|
url = "https://github.com/pnpm/pnpm/releases/download/v12.15.0/pnpm-darwin-arm64.tar.gz"
|
||||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/577602575"
|
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/662173343"
|
||||||
|
provenance = "github-attestations"
|
||||||
|
|
||||||
|
[tools.pnpm."platforms.macos-x64"]
|
||||||
|
checksum = "sha256:2d7b336884574aee8a765bee48591212e9caf4fcf985fb3279588c1fb73fb47f"
|
||||||
|
url = "https://github.com/pnpm/pnpm/releases/download/v12.15.0/pnpm-darwin-x64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/662173347"
|
||||||
provenance = "github-attestations"
|
provenance = "github-attestations"
|
||||||
|
|
||||||
[tools.pnpm."platforms.windows-x64"]
|
[tools.pnpm."platforms.windows-x64"]
|
||||||
checksum = "sha256:4b56d1184c3390b08146060fa4b414b8ceec9164aa41bf996e87a0f0bc135851"
|
checksum = "sha256:84cf69db1be19e95bb4e5bef329291a046f2821b390d87e00f5690809d8a2ed7"
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.27.1/pnpm-win32-x64.zip"
|
url = "https://github.com/pnpm/pnpm/releases/download/v12.15.0/pnpm-win32-x64.zip"
|
||||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/577602578"
|
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/662173351"
|
||||||
provenance = "github-attestations"
|
provenance = "github-attestations"
|
||||||
|
|
||||||
[[tools.terragrunt]]
|
[[tools.terragrunt]]
|
||||||
version = "1.1.1"
|
version = "1.2.0"
|
||||||
backend = "aqua:gruntwork-io/terragrunt"
|
backend = "aqua:gruntwork-io/terragrunt"
|
||||||
|
specifiers = ["1.2.0"]
|
||||||
|
|
||||||
[tools.terragrunt."platforms.linux-arm64"]
|
[tools.terragrunt."platforms.linux-arm64"]
|
||||||
checksum = "sha256:a374a7993ff3d99665a7e014007d3647ec7f0465c9d55c85e9f94c932e73cea2"
|
checksum = "sha256:1e13b7f0646fa7962bc6c5bfba14a30d6a589325b82561e21b66be5981d86395"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_linux_arm64.tar.gz"
|
checksum_source = "mise-versions"
|
||||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476786148"
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.2.0/terragrunt_linux_arm64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/662437526"
|
||||||
[tools.terragrunt."platforms.linux-arm64-musl"]
|
|
||||||
checksum = "sha256:a374a7993ff3d99665a7e014007d3647ec7f0465c9d55c85e9f94c932e73cea2"
|
|
||||||
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/476786148"
|
|
||||||
|
|
||||||
[tools.terragrunt."platforms.linux-x64"]
|
[tools.terragrunt."platforms.linux-x64"]
|
||||||
checksum = "sha256:ce90077ac31ef17a2ba10d11d45f36c6501997a8f4f79d703bb7daba37032f53"
|
checksum = "sha256:23b5f2f9203d34919f7fd270a4b635b721b4903beca9762a1e4853b159bc9be8"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_linux_amd64.tar.gz"
|
checksum_source = "mise-versions"
|
||||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476785968"
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.2.0/terragrunt_linux_amd64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/662437263"
|
||||||
[tools.terragrunt."platforms.linux-x64-musl"]
|
|
||||||
checksum = "sha256:ce90077ac31ef17a2ba10d11d45f36c6501997a8f4f79d703bb7daba37032f53"
|
|
||||||
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/476785968"
|
|
||||||
|
|
||||||
[tools.terragrunt."platforms.macos-arm64"]
|
[tools.terragrunt."platforms.macos-arm64"]
|
||||||
checksum = "sha256:9ec8f678b9ae6c81d5d9d77b94cbf6349ce639d5938694b4adc9dea73e416794"
|
checksum = "sha256:79790d841ce71fe94c689fb6f5034afd31ad53d5904937c7a10934cca9d9734b"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_darwin_arm64.tar.gz"
|
checksum_source = "mise-versions"
|
||||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476785628"
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.2.0/terragrunt_darwin_arm64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/662436516"
|
||||||
|
|
||||||
[tools.terragrunt."platforms.macos-x64"]
|
[tools.terragrunt."platforms.macos-x64"]
|
||||||
checksum = "sha256:73e768a69fa44a60f9d9174f54aaf179327e08706f633c9c79d5f5c9622c91c2"
|
checksum = "sha256:0b0e63e80509eb1ffb4f89b892bc6fbf7728f27f0a73974400f1bd82a80b8627"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_darwin_amd64.tar.gz"
|
checksum_source = "mise-versions"
|
||||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476785520"
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.2.0/terragrunt_darwin_amd64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/662436142"
|
||||||
|
|
||||||
[tools.terragrunt."platforms.windows-x64"]
|
[tools.terragrunt."platforms.windows-x64"]
|
||||||
checksum = "sha256:dd50a324691e072a3ac879e9ec43d6310e2936c929fda658eb2811fefcc75115"
|
checksum = "sha256:c9a146b9aad456a289614023ad5778123d819af3cf9efd1c6130f196cfcb1999"
|
||||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_windows_amd64.exe.tar.gz"
|
checksum_source = "mise-versions"
|
||||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476786421"
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.2.0/terragrunt_windows_amd64.exe.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/662438255"
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ config_roots = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[tools]
|
[tools]
|
||||||
node = "24.21.0"
|
node = "24.22.1"
|
||||||
pnpm = "11.27.1"
|
pnpm = "12.15.0"
|
||||||
terragrunt = "1.1.1"
|
terragrunt = "1.2.0"
|
||||||
opentofu = "1.12.6"
|
opentofu = "1.13.2"
|
||||||
"npm:@openapitools/openapi-generator-cli" = "2.41.0"
|
"npm:@openapitools/openapi-generator-cli" = "2.42.0"
|
||||||
"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"
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
cmake_minimum_required(VERSION 3.12)
|
|
||||||
|
|
||||||
set(CMAKE_C_STANDARD 17)
|
|
||||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
|
||||||
|
|
||||||
project(native_buffer LANGUAGES C)
|
|
||||||
|
|
||||||
add_library(native_buffer SHARED
|
|
||||||
src/main/cpp/native_buffer.c
|
|
||||||
src/main/cpp/native_image.c
|
|
||||||
)
|
|
||||||
|
|
||||||
target_link_libraries(native_buffer jnigraphics)
|
|
||||||
@@ -79,11 +79,6 @@ android {
|
|||||||
}
|
}
|
||||||
namespace 'app.alextran.immich'
|
namespace 'app.alextran.immich'
|
||||||
|
|
||||||
externalNativeBuild {
|
|
||||||
cmake {
|
|
||||||
path "CMakeLists.txt"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
#include <jni.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
JNIEXPORT jlong JNICALL
|
|
||||||
Java_app_alextran_immich_NativeBuffer_allocate(
|
|
||||||
JNIEnv *env, jclass clazz, jint size) {
|
|
||||||
void *ptr = malloc(size);
|
|
||||||
return (jlong) ptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT void JNICALL
|
|
||||||
Java_app_alextran_immich_NativeBuffer_free(
|
|
||||||
JNIEnv *env, jclass clazz, jlong address) {
|
|
||||||
free((void *) address);
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT jlong JNICALL
|
|
||||||
Java_app_alextran_immich_NativeBuffer_realloc(
|
|
||||||
JNIEnv *env, jclass clazz, jlong address, jint size) {
|
|
||||||
void *ptr = realloc((void *) address, size);
|
|
||||||
return (jlong) ptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT jobject JNICALL
|
|
||||||
Java_app_alextran_immich_NativeBuffer_wrap(
|
|
||||||
JNIEnv *env, jclass clazz, jlong address, jint capacity) {
|
|
||||||
return (*env)->NewDirectByteBuffer(env, (void *) address, capacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT void JNICALL
|
|
||||||
Java_app_alextran_immich_NativeBuffer_copy(
|
|
||||||
JNIEnv *env, jclass clazz, jobject buffer, jlong destAddress, jint offset, jint length) {
|
|
||||||
void *src = (*env)->GetDirectBufferAddress(env, buffer);
|
|
||||||
if (src != NULL) {
|
|
||||||
memcpy((void *) destAddress, (char *) src + offset, length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a JNI global reference to the given object and returns its address.
|
|
||||||
* The caller is responsible for deleting the global reference when it's no longer needed.
|
|
||||||
*/
|
|
||||||
JNIEXPORT jlong JNICALL
|
|
||||||
Java_app_alextran_immich_NativeBuffer_createGlobalRef(JNIEnv *env, jobject clazz, jobject obj) {
|
|
||||||
if (obj == NULL) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
jobject globalRef = (*env)->NewGlobalRef(env, obj);
|
|
||||||
return (jlong) globalRef;
|
|
||||||
}
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
#include <jni.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <android/bitmap.h>
|
|
||||||
|
|
||||||
// Cache-friendly block size for the tiled rotation (in pixels). 32x32 uint32 = 4KB, fits L1.
|
|
||||||
#define TILE 32
|
|
||||||
|
|
||||||
// EXIF orientation values (androidx.exifinterface.media.ExifInterface.ORIENTATION_*).
|
|
||||||
enum {
|
|
||||||
ORIENTATION_FLIP_HORIZONTAL = 2,
|
|
||||||
ORIENTATION_ROTATE_180 = 3,
|
|
||||||
ORIENTATION_FLIP_VERTICAL = 4,
|
|
||||||
ORIENTATION_TRANSPOSE = 5,
|
|
||||||
ORIENTATION_ROTATE_90 = 6,
|
|
||||||
ORIENTATION_TRANSVERSE = 7,
|
|
||||||
ORIENTATION_ROTATE_270 = 8,
|
|
||||||
};
|
|
||||||
|
|
||||||
// The orientations that swap width and height. Must stay in sync with affine_for's dim usage.
|
|
||||||
static int swaps_dims(int o) {
|
|
||||||
return o == ORIENTATION_ROTATE_90 || o == ORIENTATION_ROTATE_270 ||
|
|
||||||
o == ORIENTATION_TRANSPOSE || o == ORIENTATION_TRANSVERSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A source pixel (sx, sy) maps to destination index base + sx*stepX + sy*stepY, where dw is the
|
|
||||||
// destination width. This affine form covers all 8 EXIF orientations and matches the pixel layout
|
|
||||||
// of Bitmap.createBitmap(src, matrixForExifOrientation(o)). int64_t so it stays correct on
|
|
||||||
// armeabi-v7a (32-bit long) regardless of how large MAX_RAW_DECODE_PIXELS grows.
|
|
||||||
static void affine_for(int o, int sw, int sh, int dw, int64_t *base, int64_t *stepX, int64_t *stepY) {
|
|
||||||
switch (o) {
|
|
||||||
case ORIENTATION_ROTATE_90: *base = sh - 1; *stepX = dw; *stepY = -1; break;
|
|
||||||
case ORIENTATION_ROTATE_270: *base = (int64_t) (sw - 1) * dw; *stepX = -dw; *stepY = 1; break;
|
|
||||||
case ORIENTATION_ROTATE_180: *base = (int64_t) (sh - 1) * dw + (sw - 1); *stepX = -1; *stepY = -dw; break;
|
|
||||||
case ORIENTATION_FLIP_HORIZONTAL: *base = sw - 1; *stepX = -1; *stepY = dw; break;
|
|
||||||
case ORIENTATION_FLIP_VERTICAL: *base = (int64_t) (sh - 1) * dw; *stepX = 1; *stepY = -dw; break;
|
|
||||||
case ORIENTATION_TRANSPOSE: *base = 0; *stepX = dw; *stepY = 1; break;
|
|
||||||
case ORIENTATION_TRANSVERSE: *base = (int64_t) (sw - 1) * dw + (sh - 1); *stepX = -dw; *stepY = -1; break;
|
|
||||||
default: *base = 0; *stepX = 1; *stepY = dw; break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy each source pixel (whole uint32, so channel order/premult is irrelevant) to its rotated
|
|
||||||
// destination, walking TILE x TILE blocks so the scattered writes of a 90/270 transpose stay
|
|
||||||
// cache-resident. dst is densely packed (rowBytes == dw*4, no padding), which the affine math relies on.
|
|
||||||
static void rotate_tiled(const uint8_t *src, int srcStride, uint32_t *dst,
|
|
||||||
int sw, int sh, int64_t base, int64_t stepX, int64_t stepY) {
|
|
||||||
for (int ty = 0; ty < sh; ty += TILE) {
|
|
||||||
int yEnd = ty + TILE < sh ? ty + TILE : sh;
|
|
||||||
for (int tx = 0; tx < sw; tx += TILE) {
|
|
||||||
int xEnd = tx + TILE < sw ? tx + TILE : sw;
|
|
||||||
for (int sy = ty; sy < yEnd; sy++) {
|
|
||||||
const uint32_t *srcRow = (const uint32_t *) (src + (size_t) sy * srcStride);
|
|
||||||
int64_t idx = base + (int64_t) sy * stepY + (int64_t) tx * stepX;
|
|
||||||
for (int sx = tx; sx < xEnd; sx++) {
|
|
||||||
dst[idx] = srcRow[sx];
|
|
||||||
idx += stepX;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rotates an RGBA_8888 bitmap to the given EXIF orientation into a freshly malloc'd buffer (free it
|
|
||||||
// via NativeBuffer.free). Fills outInfo with {width, height, rowBytes} and returns the buffer
|
|
||||||
// address, or 0 if the bitmap can't be handled (e.g. a non-8888 format) so the caller can fall back.
|
|
||||||
JNIEXPORT jlong JNICALL
|
|
||||||
Java_app_alextran_immich_NativeImage_rotate(
|
|
||||||
JNIEnv *env, jclass clazz, jobject bitmap, jint orientation, jintArray outInfo) {
|
|
||||||
AndroidBitmapInfo info;
|
|
||||||
if (AndroidBitmap_getInfo(env, bitmap, &info) != ANDROID_BITMAP_RESULT_SUCCESS) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (info.format != ANDROID_BITMAP_FORMAT_RGBA_8888) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int sw = (int) info.width;
|
|
||||||
int sh = (int) info.height;
|
|
||||||
int dw = swaps_dims(orientation) ? sh : sw;
|
|
||||||
int dh = swaps_dims(orientation) ? sw : sh;
|
|
||||||
|
|
||||||
uint32_t *dst = (uint32_t *) malloc((size_t) dw * dh * 4);
|
|
||||||
if (dst == NULL) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void *srcPixels = NULL;
|
|
||||||
if (AndroidBitmap_lockPixels(env, bitmap, &srcPixels) != ANDROID_BITMAP_RESULT_SUCCESS) {
|
|
||||||
free(dst);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int64_t base, stepX, stepY;
|
|
||||||
affine_for(orientation, sw, sh, dw, &base, &stepX, &stepY);
|
|
||||||
rotate_tiled((const uint8_t *) srcPixels, (int) info.stride, dst, sw, sh, base, stepX, stepY);
|
|
||||||
|
|
||||||
AndroidBitmap_unlockPixels(env, bitmap);
|
|
||||||
|
|
||||||
jint dims[3] = {dw, dh, dw * 4};
|
|
||||||
(*env)->SetIntArrayRegion(env, outInfo, 0, 3, dims);
|
|
||||||
// Keep ownership in C until the buffer is safely handed back: if outInfo was somehow too small,
|
|
||||||
// SetIntArrayRegion left a pending exception and Kotlin will never receive (or free) dst.
|
|
||||||
if ((*env)->ExceptionCheck(env)) {
|
|
||||||
free(dst);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return (jlong) dst;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert an RGBA_1010102 buffer to densely-packed RGBA_8888, matching Skia's
|
|
||||||
// Bitmap.copy(ARGB_8888) byte-for-byte so it's a drop-in for the intermediate 8888 bitmap.
|
|
||||||
// Each source pixel is a native (little-endian on every Android ABI) u32 with R in bits 0-9,
|
|
||||||
// G in 10-19, B in 20-29, A in 30-31 (standard RGB10_A2). Each 10-bit channel maps to 8-bit via
|
|
||||||
// round(v*255/1023). The 2-bit alpha maps to a*85. Both are kept as plain arithmetic as the whole
|
|
||||||
// loop auto-vectorizes to NEON and measures faster than a LUT on-device. Output is R,G,B,A bytes
|
|
||||||
// per pixel, i.e. Android ARGB_8888 memory == Dart PixelFormat.rgba8888.
|
|
||||||
static void convert_1010102(const uint8_t *src, int srcStride, uint32_t *dst, int w, int h) {
|
|
||||||
for (int y = 0; y < h; y++) {
|
|
||||||
const uint32_t *srcRow = (const uint32_t *) (src + (size_t) y * srcStride);
|
|
||||||
uint32_t *dstRow = dst + (size_t) y * w;
|
|
||||||
for (int x = 0; x < w; x++) {
|
|
||||||
uint32_t px = srcRow[x];
|
|
||||||
uint32_t r = ((px & 0x3FF) * 16336u + 32768u) >> 16;
|
|
||||||
uint32_t g = (((px >> 10) & 0x3FF) * 16336u + 32768u) >> 16;
|
|
||||||
uint32_t b = (((px >> 20) & 0x3FF) * 16336u + 32768u) >> 16;
|
|
||||||
uint32_t a = ((px >> 30) & 0x3) * 85u;
|
|
||||||
dstRow[x] = r | (g << 8) | (b << 16) | (a << 24);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Converts an RGBA_1010102 bitmap (what a 10-bit HEIC/AVIF decodes to on API 33+) into a freshly
|
|
||||||
// malloc'd RGBA_8888 buffer. Fills outInfo with {width, height, rowBytes} and returns the buffer
|
|
||||||
// address, or 0 (so the caller falls back to a Skia copy) if the bitmap isn't 1010102 or can't be
|
|
||||||
// locked. Same ownership contract as rotate: free the returned buffer via NativeBuffer.free.
|
|
||||||
JNIEXPORT jlong JNICALL
|
|
||||||
Java_app_alextran_immich_NativeImage_convert1010102(
|
|
||||||
JNIEnv *env, jclass clazz, jobject bitmap, jintArray outInfo) {
|
|
||||||
AndroidBitmapInfo info;
|
|
||||||
if (AndroidBitmap_getInfo(env, bitmap, &info) != ANDROID_BITMAP_RESULT_SUCCESS) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (info.format != ANDROID_BITMAP_FORMAT_RGBA_1010102) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int w = (int) info.width;
|
|
||||||
int h = (int) info.height;
|
|
||||||
|
|
||||||
uint32_t *dst = (uint32_t *) malloc((size_t) w * h * 4);
|
|
||||||
if (dst == NULL) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void *srcPixels = NULL;
|
|
||||||
if (AndroidBitmap_lockPixels(env, bitmap, &srcPixels) != ANDROID_BITMAP_RESULT_SUCCESS) {
|
|
||||||
free(dst);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
convert_1010102((const uint8_t *) srcPixels, (int) info.stride, dst, w, h);
|
|
||||||
|
|
||||||
AndroidBitmap_unlockPixels(env, bitmap);
|
|
||||||
|
|
||||||
jint dims[3] = {w, h, w * 4};
|
|
||||||
(*env)->SetIntArrayRegion(env, outInfo, 0, 3, dims);
|
|
||||||
if ((*env)->ExceptionCheck(env)) {
|
|
||||||
free(dst);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return (jlong) dst;
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ const val INITIAL_BUFFER_SIZE = 32 * 1024
|
|||||||
|
|
||||||
object NativeBuffer {
|
object NativeBuffer {
|
||||||
init {
|
init {
|
||||||
System.loadLibrary("native_buffer")
|
System.loadLibrary("native_core_ffi")
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package app.alextran.immich
|
package app.alextran.immich
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
object NativeImage {
|
object NativeImage {
|
||||||
init {
|
init {
|
||||||
// rotate() is compiled into the native_buffer shared lib (which already links jnigraphics).
|
System.loadLibrary("native_core_ffi")
|
||||||
System.loadLibrary("native_buffer")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,4 +26,24 @@ object NativeImage {
|
|||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
external fun convert1010102(bitmap: Bitmap, outInfo: IntArray): Long
|
external fun convert1010102(bitmap: Bitmap, outInfo: IntArray): Long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like [convert1010102], but decodes the JPEG XL image of [length] bytes at [address], stopped by
|
||||||
|
* [cancel], downscaled to cover [targetWidth] x [targetHeight] unless either is 0 and to fit
|
||||||
|
* within [maxPixelSize].
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
external fun decodeJxl(
|
||||||
|
address: Long,
|
||||||
|
length: Int,
|
||||||
|
targetWidth: Int,
|
||||||
|
targetHeight: Int,
|
||||||
|
maxPixelSize: Int,
|
||||||
|
cancelled: ByteBuffer,
|
||||||
|
outInfo: IntArray
|
||||||
|
): Long
|
||||||
|
|
||||||
|
/** Stops the [decodeJxl] given this one-byte direct buffer. */
|
||||||
|
@JvmStatic
|
||||||
|
external fun cancel(cancelled: ByteBuffer)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import androidx.exifinterface.media.ExifInterface
|
|||||||
import app.alextran.immich.INITIAL_BUFFER_SIZE
|
import app.alextran.immich.INITIAL_BUFFER_SIZE
|
||||||
import app.alextran.immich.NativeBuffer
|
import app.alextran.immich.NativeBuffer
|
||||||
import app.alextran.immich.NativeByteBuffer
|
import app.alextran.immich.NativeByteBuffer
|
||||||
|
import app.alextran.immich.NativeImage
|
||||||
import app.alextran.immich.core.HttpClientManager
|
import app.alextran.immich.core.HttpClientManager
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import okhttp3.Cache
|
import okhttp3.Cache
|
||||||
@@ -29,6 +30,8 @@ import java.util.concurrent.ConcurrentHashMap
|
|||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
private const val MAX_PREALLOC_BYTES = 128 * 1024 * 1024
|
private const val MAX_PREALLOC_BYTES = 128 * 1024 * 1024
|
||||||
|
// Match the upstream GPU texture bound without depending on a newer local image decoder.
|
||||||
|
private const val MAX_JXL_PIXEL_SIZE = 16384
|
||||||
|
|
||||||
private class RemoteRequest(val cancellationSignal: CancellationSignal)
|
private class RemoteRequest(val cancellationSignal: CancellationSignal)
|
||||||
|
|
||||||
@@ -56,10 +59,9 @@ private val RAW_MIME_TYPES = setOf(
|
|||||||
"image/x-sony-sr2", "image/x-sony-srf", "image/x3f",
|
"image/x-sony-sr2", "image/x-sony-srf", "image/x3f",
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun isRawMime(contentType: String?): Boolean {
|
private fun mimeType(contentType: String?) = contentType?.substringBefore(';')?.trim()?.lowercase()
|
||||||
val mime = contentType?.substringBefore(';')?.trim()?.lowercase() ?: return false
|
|
||||||
return mime in RAW_MIME_TYPES
|
private fun isRawMime(contentType: String?) = mimeType(contentType) in RAW_MIME_TYPES
|
||||||
}
|
|
||||||
|
|
||||||
class RemoteImagesImpl(context: Context) : RemoteImageApi {
|
class RemoteImagesImpl(context: Context) : RemoteImageApi {
|
||||||
private val requestMap = ConcurrentHashMap<Long, RemoteRequest>()
|
private val requestMap = ConcurrentHashMap<Long, RemoteRequest>()
|
||||||
@@ -97,6 +99,42 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi {
|
|||||||
return@fetch callback(CANCELLED)
|
return@fetch callback(CANCELLED)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Neither Android nor Flutter can decode JPEG XL, so this applies to encoded requests too.
|
||||||
|
if (mimeType(contentType) == "image/jxl") {
|
||||||
|
decodeExecutor.execute {
|
||||||
|
val info = IntArray(3)
|
||||||
|
// The fetch is done, so cancelling now stops the decode instead.
|
||||||
|
val cancelled = ByteBuffer.allocateDirect(1)
|
||||||
|
signal.setOnCancelListener { NativeImage.cancel(cancelled) }
|
||||||
|
val pointer = if (signal.isCanceled) 0L else NativeImage.decodeJxl(
|
||||||
|
buffer.pointer,
|
||||||
|
buffer.offset,
|
||||||
|
width?.toInt() ?: 0,
|
||||||
|
height?.toInt() ?: 0,
|
||||||
|
MAX_JXL_PIXEL_SIZE,
|
||||||
|
cancelled,
|
||||||
|
info
|
||||||
|
)
|
||||||
|
buffer.free()
|
||||||
|
requestMap.remove(requestId)
|
||||||
|
callback(
|
||||||
|
when {
|
||||||
|
pointer != 0L -> Result.success(
|
||||||
|
mapOf(
|
||||||
|
"pointer" to pointer,
|
||||||
|
"width" to info[0].toLong(),
|
||||||
|
"height" to info[1].toLong(),
|
||||||
|
"rowBytes" to info[2].toLong()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
signal.isCanceled -> CANCELLED
|
||||||
|
else -> Result.failure(IOException("Failed to decode JPEG XL image"))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return@fetch
|
||||||
|
}
|
||||||
|
|
||||||
// Decode natively when the caller wants pixels: Flutter's fallback decoder copies
|
// Decode natively when the caller wants pixels: Flutter's fallback decoder copies
|
||||||
// 10-bit bitmaps (RGBA_1010102) as if they were rgba8888, garbling colors. Decode on a
|
// 10-bit bitmaps (RGBA_1010102) as if they were rgba8888, garbling colors. Decode on a
|
||||||
// dedicated pool - the fetch callback threads are shared with video streaming. On any
|
// dedicated pool - the fetch callback threads are shared with video streaming. On any
|
||||||
@@ -272,7 +310,11 @@ private class CronetImageFetcher : ImageFetcher {
|
|||||||
requestBuilder.addHeader(key, value)
|
requestBuilder.addHeader(key, value)
|
||||||
}
|
}
|
||||||
val request = requestBuilder.build()
|
val request = requestBuilder.build()
|
||||||
signal.setOnCancelListener(request::cancel)
|
// Cronet's disk cache can still use a read buffer after onCanceled, so only cancel between reads.
|
||||||
|
signal.setOnCancelListener {
|
||||||
|
callback.requestCancel = true
|
||||||
|
if (!callback.receivingResponse) request.cancel()
|
||||||
|
}
|
||||||
request.start()
|
request.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,12 +370,16 @@ private class CronetImageFetcher : ImageFetcher {
|
|||||||
) : UrlRequest.Callback() {
|
) : UrlRequest.Callback() {
|
||||||
private var buffer: NativeByteBuffer? = null
|
private var buffer: NativeByteBuffer? = null
|
||||||
private var error: Exception? = null
|
private var error: Exception? = null
|
||||||
|
@Volatile var receivingResponse = false
|
||||||
|
@Volatile var requestCancel = false
|
||||||
|
|
||||||
override fun onRedirectReceived(request: UrlRequest, info: UrlResponseInfo, newUrl: String) {
|
override fun onRedirectReceived(request: UrlRequest, info: UrlResponseInfo, newUrl: String) {
|
||||||
request.followRedirect()
|
request.followRedirect()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResponseStarted(request: UrlRequest, info: UrlResponseInfo) {
|
override fun onResponseStarted(request: UrlRequest, info: UrlResponseInfo) {
|
||||||
|
receivingResponse = true
|
||||||
|
if (requestCancel) return request.cancel()
|
||||||
if (info.httpStatusCode !in 200..299) {
|
if (info.httpStatusCode !in 200..299) {
|
||||||
error = IOException("HTTP ${info.httpStatusCode}: ${info.httpStatusText}")
|
error = IOException("HTTP ${info.httpStatusCode}: ${info.httpStatusText}")
|
||||||
return request.cancel()
|
return request.cancel()
|
||||||
@@ -361,6 +407,7 @@ private class CronetImageFetcher : ImageFetcher {
|
|||||||
info: UrlResponseInfo,
|
info: UrlResponseInfo,
|
||||||
byteBuffer: ByteBuffer
|
byteBuffer: ByteBuffer
|
||||||
) {
|
) {
|
||||||
|
if (requestCancel) return request.cancel()
|
||||||
try {
|
try {
|
||||||
// Always pass a fresh wrap so byteBuffer.position() represents only the
|
// Always pass a fresh wrap so byteBuffer.position() represents only the
|
||||||
// bytes Cronet wrote in this iteration. Reusing the caller-supplied
|
// bytes Cronet wrote in this iteration. Reusing the caller-supplied
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
import 'package:native_core/native_core.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
test('native core loads on device', () {
|
||||||
|
expect(version(), isNotEmpty);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum NativeCore {
|
||||||
|
private static let library = dlopen("@rpath/native_core_ffi.framework/native_core_ffi", RTLD_NOW)
|
||||||
|
|
||||||
|
private static func symbol<T>(_ name: String, as type: T.Type) -> T? {
|
||||||
|
guard let library, let symbol = dlsym(library, name) else { return nil }
|
||||||
|
return unsafeBitCast(symbol, to: type)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static let coreThumbhash = symbol("native_core_thumbhash", as: NativeCoreThumbhashFn.self)
|
||||||
|
private static let coreJxlDecode = symbol("native_core_jxl_decode", as: NativeCoreJxlDecodeFn.self)
|
||||||
|
private static let coreCancel = symbol("native_core_cancel", as: NativeCoreCancelFn.self)
|
||||||
|
|
||||||
|
static func thumbhash(_ hash: Data) -> (width: Int, height: Int, pointer: UnsafeMutableRawPointer)? {
|
||||||
|
guard let coreThumbhash else { return nil }
|
||||||
|
var width: Int32 = 0
|
||||||
|
var height: Int32 = 0
|
||||||
|
return hash.withUnsafeBytes { bytes in
|
||||||
|
guard let pointer = coreThumbhash(bytes.bindMemory(to: UInt8.self).baseAddress, bytes.count, &width, &height)
|
||||||
|
else { return nil }
|
||||||
|
return (Int(width), Int(height), UnsafeMutableRawPointer(pointer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func decodeJxl(_ data: Data, targetWidth: Int64?, targetHeight: Int64?, maxPixelSize: Int, cancelled: UnsafeMutablePointer<Bool>) -> (width: Int, height: Int, pointer: UnsafeMutableRawPointer)? {
|
||||||
|
guard let coreJxlDecode else { return nil }
|
||||||
|
var info = NativeCoreImageInfo()
|
||||||
|
return data.withUnsafeBytes { bytes in
|
||||||
|
guard let pointer = coreJxlDecode(bytes.bindMemory(to: UInt8.self).baseAddress, bytes.count, false, UInt32(clamping: targetWidth ?? 0), UInt32(clamping: targetHeight ?? 0), UInt32(clamping: maxPixelSize), cancelled, &info)
|
||||||
|
else { return nil }
|
||||||
|
return (Int(info.width), Int(info.height), UnsafeMutableRawPointer(pointer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func cancel(_ flag: UnsafeMutablePointer<Bool>) {
|
||||||
|
coreCancel?(flag)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,4 +9,6 @@ enum ImageProcessing {
|
|||||||
return q
|
return q
|
||||||
}()
|
}()
|
||||||
static let cancelledResult = Result<[String: Int64]?, any Error>.success(nil)
|
static let cancelledResult = Result<[String: Int64]?, any Error>.success(nil)
|
||||||
|
// Impeller shrinks each side to the GPU texture limit on its own, which squishes long images.
|
||||||
|
static let maxPixelSize = 16384
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ class LocalImageApiImpl: LocalImageApi {
|
|||||||
requestOptions.version = .current
|
requestOptions.version = .current
|
||||||
return requestOptions
|
return requestOptions
|
||||||
}()
|
}()
|
||||||
private static let maxPixelSize: CGFloat = 16384
|
|
||||||
|
|
||||||
private static let registry = RequestRegistry<ImageRequest>()
|
private static let registry = RequestRegistry<ImageRequest>()
|
||||||
|
|
||||||
@@ -111,7 +110,7 @@ class LocalImageApiImpl: LocalImageApi {
|
|||||||
|
|
||||||
let isOriginal = !(width > 0 && height > 0)
|
let isOriginal = !(width > 0 && height > 0)
|
||||||
let targetSize = isOriginal
|
let targetSize = isOriginal
|
||||||
? CGSize(width: Self.maxPixelSize, height: Self.maxPixelSize)
|
? CGSize(width: ImageProcessing.maxPixelSize, height: ImageProcessing.maxPixelSize)
|
||||||
: CGSize(width: Double(width), height: Double(height))
|
: CGSize(width: Double(width), height: Double(height))
|
||||||
let contentMode: PHImageContentMode = isOriginal ? .aspectFit : .aspectFill
|
let contentMode: PHImageContentMode = isOriginal ? .aspectFit : .aspectFill
|
||||||
var image: UIImage?
|
var image: UIImage?
|
||||||
@@ -136,7 +135,7 @@ class LocalImageApiImpl: LocalImageApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// .fast can return larger than the target, so retry with .exact to guarantee the bound.
|
// .fast can return larger than the target, so retry with .exact to guarantee the bound.
|
||||||
if max(cgImage.width, cgImage.height) > Int(Self.maxPixelSize) {
|
if max(cgImage.width, cgImage.height) > ImageProcessing.maxPixelSize {
|
||||||
let exactOptions = Self.requestOptions.copy() as! PHImageRequestOptions
|
let exactOptions = Self.requestOptions.copy() as! PHImageRequestOptions
|
||||||
exactOptions.resizeMode = .exact
|
exactOptions.resizeMode = .exact
|
||||||
image = nil
|
image = nil
|
||||||
|
|||||||
@@ -7,15 +7,23 @@ import Photos
|
|||||||
final class RemoteImageRequest: ImageRequest {
|
final class RemoteImageRequest: ImageRequest {
|
||||||
var task: URLSessionDataTask?
|
var task: URLSessionDataTask?
|
||||||
let id: Int64
|
let id: Int64
|
||||||
|
/// Lets cancellation stop a native decode partway through.
|
||||||
|
let decodeCancelled = UnsafeMutablePointer<Bool>.allocate(capacity: 1)
|
||||||
|
|
||||||
init(id: Int64, completion: @escaping @Sendable (Result<[String: Int64]?, any Error>) -> Void) {
|
init(id: Int64, completion: @escaping @Sendable (Result<[String: Int64]?, any Error>) -> Void) {
|
||||||
self.id = id
|
self.id = id
|
||||||
super.init(completion: completion)
|
super.init(completion: completion)
|
||||||
|
decodeCancelled.initialize(to: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
decodeCancelled.deallocate()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func cancel() {
|
override func cancel() {
|
||||||
super.cancel()
|
super.cancel()
|
||||||
task?.cancel()
|
task?.cancel()
|
||||||
|
NativeCore.cancel(decodeCancelled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +73,38 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi {
|
|||||||
return request.completion(.failure(PigeonError(code: "", message: "No data received", details: nil)))
|
return request.completion(.failure(PigeonError(code: "", message: "No data received", details: nil)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flutter can't decode JPEG XL and older iOS versions can't either, so this applies to encoded requests too.
|
||||||
|
if response?.mimeType == "image/jxl" {
|
||||||
|
ImageProcessing.queue.addOperation {
|
||||||
|
if request.isCancelled {
|
||||||
|
return request.completion(ImageProcessing.cancelledResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let (width, height, pointer) = NativeCore.decodeJxl(data, targetWidth: width, targetHeight: height, maxPixelSize: ImageProcessing.maxPixelSize, cancelled: request.decodeCancelled) else {
|
||||||
|
registry.remove(requestId: request.id)
|
||||||
|
if request.isCancelled {
|
||||||
|
return request.completion(ImageProcessing.cancelledResult)
|
||||||
|
}
|
||||||
|
return request.completion(.failure(PigeonError(code: "", message: "Failed to decode JPEG XL image", details: nil)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.isCancelled {
|
||||||
|
free(pointer)
|
||||||
|
return request.completion(ImageProcessing.cancelledResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.remove(requestId: request.id)
|
||||||
|
request.completion(
|
||||||
|
.success([
|
||||||
|
"pointer": Int64(Int(bitPattern: pointer)),
|
||||||
|
"width": Int64(width),
|
||||||
|
"height": Int64(height),
|
||||||
|
"rowBytes": Int64(width * 4),
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if encoded {
|
if encoded {
|
||||||
let length = data.count
|
let length = data.count
|
||||||
let pointer = malloc(length)!
|
let pointer = malloc(length)!
|
||||||
|
|||||||
@@ -1 +1,7 @@
|
|||||||
#import "GeneratedPluginRegistrant.h"
|
#import "GeneratedPluginRegistrant.h"
|
||||||
|
#include "../../../native/crates/native_core_ffi/include/native_core.h"
|
||||||
|
|
||||||
|
// The function pointer type comes from the declaration, so nothing is retyped here.
|
||||||
|
typedef __typeof__(&native_core_thumbhash) NativeCoreThumbhashFn;
|
||||||
|
typedef __typeof__(&native_core_jxl_decode) NativeCoreJxlDecodeFn;
|
||||||
|
typedef __typeof__(&native_core_cancel) NativeCoreCancelFn;
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ class Drift extends $Drift {
|
|||||||
@override
|
@override
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
await super.close();
|
await super.close();
|
||||||
_updatePool?.close();
|
await _updatePool?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> reset() async {
|
Future<void> reset() async {
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ abstract class AppConfig with _$AppConfig {
|
|||||||
.timelineTilesPerRow => timeline.tilesPerRow,
|
.timelineTilesPerRow => timeline.tilesPerRow,
|
||||||
.timelineGroupAssetsBy => timeline.groupAssetsBy,
|
.timelineGroupAssetsBy => timeline.groupAssetsBy,
|
||||||
.timelineStorageIndicator => timeline.storageIndicator,
|
.timelineStorageIndicator => timeline.storageIndicator,
|
||||||
|
.timelineSmoothScrubbing => timeline.smoothScrubbing,
|
||||||
.mapShowFavoriteOnly => map.favoritesOnly,
|
.mapShowFavoriteOnly => map.favoritesOnly,
|
||||||
.mapRelativeDate => map.relativeDays,
|
.mapRelativeDate => map.relativeDays,
|
||||||
.mapCustomFrom => map.customFrom,
|
.mapCustomFrom => map.customFrom,
|
||||||
@@ -131,6 +132,7 @@ abstract class AppConfig with _$AppConfig {
|
|||||||
.timelineTilesPerRow => copyWith(timeline: timeline.copyWith(tilesPerRow: value as int)),
|
.timelineTilesPerRow => copyWith(timeline: timeline.copyWith(tilesPerRow: value as int)),
|
||||||
.timelineGroupAssetsBy => copyWith(timeline: timeline.copyWith(groupAssetsBy: value as GroupAssetsBy)),
|
.timelineGroupAssetsBy => copyWith(timeline: timeline.copyWith(groupAssetsBy: value as GroupAssetsBy)),
|
||||||
.timelineStorageIndicator => copyWith(timeline: timeline.copyWith(storageIndicator: value as bool)),
|
.timelineStorageIndicator => copyWith(timeline: timeline.copyWith(storageIndicator: value as bool)),
|
||||||
|
.timelineSmoothScrubbing => copyWith(timeline: timeline.copyWith(smoothScrubbing: value as bool)),
|
||||||
.mapShowFavoriteOnly => copyWith(map: map.copyWith(favoritesOnly: value as bool)),
|
.mapShowFavoriteOnly => copyWith(map: map.copyWith(favoritesOnly: value as bool)),
|
||||||
.mapRelativeDate => copyWith(map: map.copyWith(relativeDays: value as int)),
|
.mapRelativeDate => copyWith(map: map.copyWith(relativeDays: value as int)),
|
||||||
.mapCustomFrom => copyWith(map: map.copyWith(customFrom: .fromNullable(value as DateTime?))),
|
.mapCustomFrom => copyWith(map: map.copyWith(customFrom: .fromNullable(value as DateTime?))),
|
||||||
|
|||||||
@@ -10,5 +10,6 @@ abstract class TimelineConfig with _$TimelineConfig {
|
|||||||
@Default(4) int tilesPerRow,
|
@Default(4) int tilesPerRow,
|
||||||
@Default(GroupAssetsBy.day) GroupAssetsBy groupAssetsBy,
|
@Default(GroupAssetsBy.day) GroupAssetsBy groupAssetsBy,
|
||||||
@Default(true) bool storageIndicator,
|
@Default(true) bool storageIndicator,
|
||||||
|
@Default(false) bool smoothScrubbing,
|
||||||
}) = _TimelineConfig;
|
}) = _TimelineConfig;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ enum SettingsKey<T> {
|
|||||||
timelineTilesPerRow<int>(),
|
timelineTilesPerRow<int>(),
|
||||||
timelineGroupAssetsBy<GroupAssetsBy>(codec: EnumCodec(GroupAssetsBy.values)),
|
timelineGroupAssetsBy<GroupAssetsBy>(codec: EnumCodec(GroupAssetsBy.values)),
|
||||||
timelineStorageIndicator<bool>(),
|
timelineStorageIndicator<bool>(),
|
||||||
|
timelineSmoothScrubbing<bool>(),
|
||||||
|
|
||||||
// Log
|
// Log
|
||||||
logLevel<LogLevel>(codec: EnumCodec(LogLevel.values)),
|
logLevel<LogLevel>(codec: EnumCodec(LogLevel.values)),
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ class AssetService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final stack = await _remoteRepository.getStackChildren(asset);
|
final stack = await _remoteRepository.getStackChildren(asset);
|
||||||
// Include the primary asset in the stack as the first item
|
|
||||||
return [asset, ...stack];
|
return [asset, ...stack];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:http/http.dart' show ClientException;
|
||||||
import 'package:immich_mobile/constants/constants.dart';
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/settings_key.dart';
|
import 'package:immich_mobile/domain/models/settings_key.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
|
||||||
import 'package:immich_mobile/utils/debug_print.dart';
|
import 'package:immich_mobile/utils/debug_print.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:openapi/api.dart' show ApiException;
|
||||||
|
|
||||||
/// Service responsible for handling application logging.
|
/// Service responsible for handling application logging.
|
||||||
///
|
///
|
||||||
@@ -14,8 +18,15 @@ import 'package:logging/logging.dart';
|
|||||||
/// writes them to a persistent [LogRepository], and manages log levels via
|
/// writes them to a persistent [LogRepository], and manages log levels via
|
||||||
/// [SettingsRepository].
|
/// [SettingsRepository].
|
||||||
class LogService {
|
class LogService {
|
||||||
|
static final _schemeAndHost = RegExp(r'''https?://[^\s/?#'"<>(),]*''');
|
||||||
|
static final _ipAddress = RegExp(
|
||||||
|
r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b|'
|
||||||
|
r'\[?\b(?:(?:[0-9a-f]{1,4}:){7}[0-9a-f]{1,4}|(?:[0-9a-f]{1,4}:)+(?::[0-9a-f]{1,4})+)(?:%\w+)?\]?',
|
||||||
|
);
|
||||||
|
|
||||||
final LogRepository _logRepository;
|
final LogRepository _logRepository;
|
||||||
final SettingsRepository _settingsRepository;
|
final SettingsRepository _settingsRepository;
|
||||||
|
final StoreService _storeService;
|
||||||
|
|
||||||
final List<LogMessage> _msgBuffer = [];
|
final List<LogMessage> _msgBuffer = [];
|
||||||
|
|
||||||
@@ -39,11 +50,13 @@ class LogService {
|
|||||||
static Future<LogService> init({
|
static Future<LogService> init({
|
||||||
required LogRepository logRepository,
|
required LogRepository logRepository,
|
||||||
required SettingsRepository settingsRepository,
|
required SettingsRepository settingsRepository,
|
||||||
|
required StoreService storeService,
|
||||||
bool shouldBuffer = true,
|
bool shouldBuffer = true,
|
||||||
}) async {
|
}) async {
|
||||||
_instance ??= await create(
|
_instance ??= await create(
|
||||||
logRepository: logRepository,
|
logRepository: logRepository,
|
||||||
settingsRepository: settingsRepository,
|
settingsRepository: settingsRepository,
|
||||||
|
storeService: storeService,
|
||||||
shouldBuffer: shouldBuffer,
|
shouldBuffer: shouldBuffer,
|
||||||
);
|
);
|
||||||
return _instance!;
|
return _instance!;
|
||||||
@@ -52,16 +65,17 @@ class LogService {
|
|||||||
static Future<LogService> create({
|
static Future<LogService> create({
|
||||||
required LogRepository logRepository,
|
required LogRepository logRepository,
|
||||||
required SettingsRepository settingsRepository,
|
required SettingsRepository settingsRepository,
|
||||||
|
required StoreService storeService,
|
||||||
bool shouldBuffer = true,
|
bool shouldBuffer = true,
|
||||||
}) async {
|
}) async {
|
||||||
final instance = LogService._(logRepository, settingsRepository, shouldBuffer);
|
final instance = LogService._(logRepository, settingsRepository, storeService, shouldBuffer);
|
||||||
await logRepository.truncate(limit: kLogTruncateLimit);
|
await logRepository.truncate(limit: kLogTruncateLimit);
|
||||||
final level = instance._settingsRepository.appConfig.logLevel;
|
final level = instance._settingsRepository.appConfig.logLevel;
|
||||||
Logger.root.level = Level.LEVELS.elementAtOrNull(level.index) ?? Level.INFO;
|
Logger.root.level = Level.LEVELS.elementAtOrNull(level.index) ?? Level.INFO;
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
LogService._(this._logRepository, this._settingsRepository, this._shouldBuffer) {
|
LogService._(this._logRepository, this._settingsRepository, this._storeService, this._shouldBuffer) {
|
||||||
_logSubscription = Logger.root.onRecord.listen(_handleLogRecord);
|
_logSubscription = Logger.root.onRecord.listen(_handleLogRecord);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,12 +88,12 @@ class LogService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final record = LogMessage(
|
final record = LogMessage(
|
||||||
message: r.message,
|
message: _redact(r.message),
|
||||||
level: r.level.toLogLevel(),
|
level: r.level.toLogLevel(),
|
||||||
createdAt: r.time,
|
createdAt: r.time,
|
||||||
logger: r.loggerName,
|
logger: r.loggerName,
|
||||||
error: r.error?.toString(),
|
error: r.error == null ? null : _redactError(r.error!),
|
||||||
stack: r.stackTrace?.toString(),
|
stack: r.stackTrace == null ? null : _redact(r.stackTrace.toString()),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (_shouldBuffer) {
|
if (_shouldBuffer) {
|
||||||
@@ -100,6 +114,64 @@ class LogService {
|
|||||||
return [..._msgBuffer.reversed, ...logsFromDb];
|
return [..._msgBuffer.reversed, ...logsFromDb];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _redact(String text) {
|
||||||
|
var redacted = text.replaceAllMapped(_schemeAndHost, (match) => _urlLabel(Uri.tryParse(match[0]!)));
|
||||||
|
final accessToken = _storeService.tryGet(StoreKey.accessToken);
|
||||||
|
if (accessToken != null) {
|
||||||
|
redacted = redacted.replaceAll(accessToken, '[ACCESS TOKEN]');
|
||||||
|
}
|
||||||
|
// as a header map prints it, so a short value like "1" can't match other text
|
||||||
|
for (final header in _settingsRepository.appConfig.network.customHeaders.entries) {
|
||||||
|
redacted = redacted.replaceAll(
|
||||||
|
RegExp('${RegExp.escape(header.key)}:\\s+${RegExp.escape(header.value)}'),
|
||||||
|
'${header.key}: [HEADER VALUE]',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return redacted;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _redactError(Object error) {
|
||||||
|
final redacted = _redact(error.toString());
|
||||||
|
final httpError = error is ApiException ? error.innerException : error;
|
||||||
|
if (httpError is! ClientException) {
|
||||||
|
return redacted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ok_http also prints the host and its ip on their own: "Failed to connect to host/203.0.113.9:443"
|
||||||
|
final url = httpError.uri;
|
||||||
|
final withoutHost = url == null || url.host.isEmpty
|
||||||
|
? redacted
|
||||||
|
: redacted.replaceAll(RegExp('\\b${RegExp.escape(url.host)}\\b'), _urlLabel(url));
|
||||||
|
return withoutHost.replaceAll(_ipAddress, '[IP ADDRESS]');
|
||||||
|
}
|
||||||
|
|
||||||
|
String _urlLabel(Uri? url) {
|
||||||
|
if (url == null) {
|
||||||
|
return '[OTHER URL]';
|
||||||
|
}
|
||||||
|
|
||||||
|
// a server url that is also a switching entry gets the entry's label
|
||||||
|
final network = _settingsRepository.appConfig.network;
|
||||||
|
for (var i = 0; i < network.externalEndpointList.length; i++) {
|
||||||
|
if (_isSameAddress(url, network.externalEndpointList[i])) {
|
||||||
|
return '[EXTERNAL ENDPOINT ${i + 1}]';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_isSameAddress(url, network.localEndpoint)) {
|
||||||
|
return '[LOCAL ENDPOINT]';
|
||||||
|
}
|
||||||
|
if (_isSameAddress(url, _storeService.tryGet(StoreKey.serverUrl)) ||
|
||||||
|
_isSameAddress(url, _storeService.tryGet(StoreKey.serverEndpoint))) {
|
||||||
|
return '[SERVER URL]';
|
||||||
|
}
|
||||||
|
return '[OTHER URL]';
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isSameAddress(Uri url, String? savedUrl) {
|
||||||
|
final saved = savedUrl == null ? null : Uri.tryParse(savedUrl);
|
||||||
|
return saved != null && saved.scheme == url.scheme && saved.host == url.host && saved.port == url.port;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> clearLogs() async {
|
Future<void> clearLogs() async {
|
||||||
_flushTimer?.cancel();
|
_flushTimer?.cancel();
|
||||||
_flushTimer = null;
|
_flushTimer = null;
|
||||||
|
|||||||
@@ -79,8 +79,7 @@ abstract class ImageRequest {
|
|||||||
return (codec, descriptor);
|
return (codec, descriptor);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ui.FrameInfo?> _fromEncodedPlatformImage(int address, int length, {ui.Size? decodeSize}) async {
|
Future<ui.FrameInfo?> _firstFrame((ui.Codec, ui.ImageDescriptor)? result) async {
|
||||||
final result = await _codecFromEncodedPlatformImage(address, length, decodeSize: decodeSize);
|
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -116,7 +115,12 @@ abstract class ImageRequest {
|
|||||||
return ((width * scale).ceil(), (height * scale).ceil());
|
return ((width * scale).ceil(), (height * scale).ceil());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ui.FrameInfo?> _fromDecodedPlatformImage(int address, int width, int height, int rowBytes) async {
|
Future<(ui.Codec, ui.ImageDescriptor)?> _codecFromDecodedPlatformImage(
|
||||||
|
int address,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
int rowBytes,
|
||||||
|
) async {
|
||||||
final pointer = Pointer<Uint8>.fromAddress(address);
|
final pointer = Pointer<Uint8>.fromAddress(address);
|
||||||
if (_isCancelled) {
|
if (_isCancelled) {
|
||||||
malloc.free(pointer);
|
malloc.free(pointer);
|
||||||
@@ -152,14 +156,6 @@ abstract class ImageRequest {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final frame = await codec.getNextFrame();
|
return (codec, descriptor);
|
||||||
descriptor.dispose();
|
|
||||||
codec.dispose();
|
|
||||||
if (_isCancelled) {
|
|
||||||
frame.image.dispose();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return frame;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ class LocalImageRequest extends ImageRequest {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final frame = await _fromDecodedPlatformImage(info["pointer"]!, info["width"]!, info["height"]!, info["rowBytes"]!);
|
final frame = await _firstFrame(
|
||||||
|
await _codecFromDecodedPlatformImage(info["pointer"]!, info["width"]!, info["height"]!, info["rowBytes"]!),
|
||||||
|
);
|
||||||
return frame == null ? null : ImageInfo(image: frame.image, scale: scale);
|
return frame == null ? null : ImageInfo(image: frame.image, scale: scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,22 +21,7 @@ class RemoteImageRequest extends ImageRequest {
|
|||||||
width: decodeSize?.width.ceil(),
|
width: decodeSize?.width.ceil(),
|
||||||
height: decodeSize?.height.ceil(),
|
height: decodeSize?.height.ceil(),
|
||||||
);
|
);
|
||||||
// Android falls back to encoded data if native decoding fails, so check for both shapes of the response.
|
final frame = await _firstFrame(await _codecFromResponse(info, decodeSize: decodeSize));
|
||||||
final frame = switch (info) {
|
|
||||||
{'pointer': final int pointer, 'length': final int length} => await _fromEncodedPlatformImage(
|
|
||||||
pointer,
|
|
||||||
length,
|
|
||||||
decodeSize: decodeSize,
|
|
||||||
),
|
|
||||||
{
|
|
||||||
'pointer': final int pointer,
|
|
||||||
'width': final int width,
|
|
||||||
'height': final int height,
|
|
||||||
'rowBytes': final int rowBytes,
|
|
||||||
} =>
|
|
||||||
await _fromDecodedPlatformImage(pointer, width, height, rowBytes),
|
|
||||||
_ => null,
|
|
||||||
};
|
|
||||||
return frame == null ? null : ImageInfo(image: frame.image, scale: scale);
|
return frame == null ? null : ImageInfo(image: frame.image, scale: scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,14 +38,29 @@ class RemoteImageRequest extends ImageRequest {
|
|||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
);
|
);
|
||||||
if (info == null) {
|
final result = await _codecFromResponse(info);
|
||||||
return null;
|
return result?.$1;
|
||||||
}
|
|
||||||
|
|
||||||
final (codec, _) = await _codecFromEncodedPlatformImage(info['pointer']!, info['length']!) ?? (null, null);
|
|
||||||
return codec;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Android falls back to encoded data if native decoding fails, and JPEG XL arrives decoded even when the encoded image
|
||||||
|
// is requested, so check for both shapes of the response.
|
||||||
|
Future<(ui.Codec, ui.ImageDescriptor)?> _codecFromResponse(Map<String, int>? info, {ui.Size? decodeSize}) =>
|
||||||
|
switch (info) {
|
||||||
|
{'pointer': final int pointer, 'length': final int length} => _codecFromEncodedPlatformImage(
|
||||||
|
pointer,
|
||||||
|
length,
|
||||||
|
decodeSize: decodeSize,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
'pointer': final int pointer,
|
||||||
|
'width': final int width,
|
||||||
|
'height': final int height,
|
||||||
|
'rowBytes': final int rowBytes,
|
||||||
|
} =>
|
||||||
|
_codecFromDecodedPlatformImage(pointer, width, height, rowBytes),
|
||||||
|
_ => Future.value(),
|
||||||
|
};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> _onCancelled() {
|
Future<void> _onCancelled() {
|
||||||
return remoteImageApi.cancelRequest(requestId);
|
return remoteImageApi.cancelRequest(requestId);
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ class ThumbhashImageRequest extends ImageRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final Map<String, int> info = await localImageApi.getThumbhash(thumbhash);
|
final Map<String, int> info = await localImageApi.getThumbhash(thumbhash);
|
||||||
final frame = await _fromDecodedPlatformImage(info["pointer"]!, info["width"]!, info["height"]!, info["rowBytes"]!);
|
final frame = await _firstFrame(
|
||||||
|
await _codecFromDecodedPlatformImage(info["pointer"]!, info["width"]!, info["height"]!, info["rowBytes"]!),
|
||||||
|
);
|
||||||
return frame == null ? null : ImageInfo(image: frame.image, scale: scale);
|
return frame == null ? null : ImageInfo(image: frame.image, scale: scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,23 @@ class RemoteAssetRepository extends DatabaseAccessor<Drift> with $RemoteAssetRep
|
|||||||
return _assetSelectable(id).watchSingleOrNull();
|
return _assetSelectable(id).watchSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stream<Set<String>> watchDeletedAssetIds(Iterable<String> ids, AssetVisibility visibility) {
|
||||||
|
final idList = ids.toList(growable: false);
|
||||||
|
if (idList.isEmpty) {
|
||||||
|
return Stream.value(const <String>{});
|
||||||
|
}
|
||||||
|
|
||||||
|
final query = _db.remoteAssetEntity.selectOnly()
|
||||||
|
..addColumns([_db.remoteAssetEntity.id])
|
||||||
|
..where(
|
||||||
|
_db.remoteAssetEntity.id.isIn(idList) &
|
||||||
|
(_db.remoteAssetEntity.deletedAt.isNotNull() |
|
||||||
|
_db.remoteAssetEntity.visibility.equalsValue(visibility).not()),
|
||||||
|
);
|
||||||
|
|
||||||
|
return query.map((row) => row.read(_db.remoteAssetEntity.id)!).watch().map((rows) => rows.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
Future<RemoteAsset?> get(String id) {
|
Future<RemoteAsset?> get(String id) {
|
||||||
return _assetSelectable(id).getSingleOrNull();
|
return _assetSelectable(id).getSingleOrNull();
|
||||||
}
|
}
|
||||||
@@ -59,7 +76,7 @@ class RemoteAssetRepository extends DatabaseAccessor<Drift> with $RemoteAssetRep
|
|||||||
|
|
||||||
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())
|
||||||
..orderBy([(row) => OrderingTerm.desc(row.createdAt)]);
|
..orderBy([(row) => OrderingTerm.asc(row.createdAt)]);
|
||||||
|
|
||||||
return query.map((row) => row.toDto()).get();
|
return query.map((row) => row.toDto()).get();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ void main() async {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
|
Logger('main').severe('Failed to start', error, stack);
|
||||||
runApp(BootstrapErrorWidget(error: error.toString(), stack: stack.toString()));
|
runApp(BootstrapErrorWidget(error: error.toString(), stack: stack.toString()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import 'package:immich_mobile/providers/server_info.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/view_intent/view_intent_handler.provider.dart';
|
import 'package:immich_mobile/providers/view_intent/view_intent_handler.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/immich_logger.service.dart';
|
||||||
import 'package:immich_mobile/theme/color_scheme.dart';
|
import 'package:immich_mobile/theme/color_scheme.dart';
|
||||||
import 'package:immich_mobile/theme/theme_data.dart';
|
import 'package:immich_mobile/theme/theme_data.dart';
|
||||||
import 'package:immich_mobile/utils/fallback_localizations.dart';
|
import 'package:immich_mobile/utils/fallback_localizations.dart';
|
||||||
@@ -78,9 +79,12 @@ class BootstrapErrorWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
const SafeArea(
|
SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
child: Padding(padding: EdgeInsets.fromLTRB(24, 16, 24, 16), child: _BottomPanel()),
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
|
||||||
|
child: _BottomPanel(startupError: '$error\n\n$stack'),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -93,7 +97,9 @@ class BootstrapErrorWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _BottomPanel extends StatefulWidget {
|
class _BottomPanel extends StatefulWidget {
|
||||||
const _BottomPanel();
|
final String startupError;
|
||||||
|
|
||||||
|
const _BottomPanel({required this.startupError});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_BottomPanel> createState() => _BottomPanelState();
|
State<_BottomPanel> createState() => _BottomPanelState();
|
||||||
@@ -157,8 +163,8 @@ class _BottomPanelState extends State<_BottomPanel> {
|
|||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
Row(
|
Wrap(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
alignment: WrapAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
_ActionLink(
|
_ActionLink(
|
||||||
icon: Icons.chat_bubble_outline,
|
icon: Icons.chat_bubble_outline,
|
||||||
@@ -173,6 +179,11 @@ class _BottomPanelState extends State<_BottomPanel> {
|
|||||||
mode: LaunchMode.externalApplication,
|
mode: LaunchMode.externalApplication,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
_ActionLink(
|
||||||
|
icon: Icons.share_outlined,
|
||||||
|
label: context.t.share_logs,
|
||||||
|
onTap: () => ImmichLogger.shareLogs(context, startupError: widget.startupError),
|
||||||
|
),
|
||||||
if (!_cleared)
|
if (!_cleared)
|
||||||
_ActionLink(
|
_ActionLink(
|
||||||
icon: Icons.delete_outline,
|
icon: Icons.delete_outline,
|
||||||
@@ -288,21 +299,7 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
unawaited(
|
unawaited(ref.read(authProvider.notifier).setOpenApiServiceEndpoint().whenComplete(() => resumeSession()));
|
||||||
ref
|
|
||||||
.read(authProvider.notifier)
|
|
||||||
.setOpenApiServiceEndpoint()
|
|
||||||
.then(logConnectionInfo)
|
|
||||||
.whenComplete(() => resumeSession()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void logConnectionInfo(String? endpoint) {
|
|
||||||
if (endpoint == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("Resuming session at $endpoint");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> resumeSession() async {
|
Future<void> resumeSession() async {
|
||||||
@@ -356,7 +353,7 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (exception) {
|
onError: (exception) {
|
||||||
log.severe('Failed to update auth info with access token: $accessToken');
|
log.severe('Failed to update auth info', exception);
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,14 +121,14 @@ class DeleteAction extends AssetActionBuilder {
|
|||||||
required bool isLocalOnly,
|
required bool isLocalOnly,
|
||||||
}) async {
|
}) async {
|
||||||
final assetService = ref.read(assetServiceProvider);
|
final assetService = ref.read(assetServiceProvider);
|
||||||
|
final message = context.t.trash_action_prompt(count: remoteIds.length);
|
||||||
if (localIds.isNotEmpty) {
|
if (localIds.isNotEmpty) {
|
||||||
await _cleanupLocalAssets(context, ref, localIds, isLocalOnly: isLocalOnly);
|
final count = await _cleanupLocalAssets(context, ref, localIds, isLocalOnly: isLocalOnly);
|
||||||
if (!context.mounted) {
|
if (count <= 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final message = context.t.trash_action_prompt(count: remoteIds.length);
|
|
||||||
await assetService.trash(remoteIds);
|
await assetService.trash(remoteIds);
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -417,21 +417,57 @@ final sharedWithPartnerProvider = StreamProvider.autoDispose<Iterable<Partner>>(
|
|||||||
return ref.watch(partnerServiceProvider).search(currentUser.id, .sharedWith);
|
return ref.watch(partnerServiceProvider).search(currentUser.id, .sharedWith);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
class _QuickAccessItem {
|
||||||
|
final Widget leading;
|
||||||
|
final String title;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final EdgeInsetsGeometry? contentPadding;
|
||||||
|
|
||||||
|
const _QuickAccessItem({required this.leading, required this.title, required this.onTap, this.contentPadding});
|
||||||
|
}
|
||||||
|
|
||||||
class _QuickAccessButtonList extends ConsumerWidget {
|
class _QuickAccessButtonList extends ConsumerWidget {
|
||||||
const _QuickAccessButtonList();
|
const _QuickAccessButtonList();
|
||||||
|
|
||||||
|
static const _tileRadius = 20.0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final partnerSharedWithAsync = ref.watch(sharedWithPartnerProvider);
|
final partnerSharedWithAsync = ref.watch(sharedWithPartnerProvider);
|
||||||
final partners = partnerSharedWithAsync.valueOrNull ?? [];
|
final partners = partnerSharedWithAsync.valueOrNull ?? [];
|
||||||
|
|
||||||
|
final items = [
|
||||||
|
_QuickAccessItem(
|
||||||
|
leading: const Icon(Icons.folder_outlined, size: 26),
|
||||||
|
title: context.t.folders,
|
||||||
|
onTap: () => context.pushRoute(FolderRoute()),
|
||||||
|
),
|
||||||
|
_QuickAccessItem(
|
||||||
|
leading: const Icon(Icons.lock_outline_rounded, size: 26),
|
||||||
|
title: context.t.locked_folder,
|
||||||
|
onTap: () => context.pushRoute(const LockedFolderRoute()),
|
||||||
|
),
|
||||||
|
_QuickAccessItem(
|
||||||
|
leading: const Icon(Icons.group_outlined, size: 26),
|
||||||
|
title: context.t.partners,
|
||||||
|
onTap: () => context.pushRoute(const PartnerRoute()),
|
||||||
|
),
|
||||||
|
for (final partner in partners)
|
||||||
|
_QuickAccessItem(
|
||||||
|
contentPadding: const EdgeInsets.only(left: 12.0, right: 18.0),
|
||||||
|
leading: PartnerUserAvatar(userId: partner.id, name: partner.name),
|
||||||
|
title: context.t.partner_list_user_photos(user: partner.name),
|
||||||
|
onTap: () => context.pushRoute(PartnerDetailRoute(partner: partner)),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
return SliverPadding(
|
return SliverPadding(
|
||||||
padding: const EdgeInsets.only(left: 16, top: 12, right: 16, bottom: 32),
|
padding: const EdgeInsets.only(left: 16, top: 12, right: 16, bottom: 32),
|
||||||
sliver: SliverToBoxAdapter(
|
sliver: SliverToBoxAdapter(
|
||||||
child: DecoratedBox(
|
child: DecoratedBox(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(color: context.colorScheme.onSurface.withAlpha(10), width: 1),
|
border: Border.all(color: context.colorScheme.onSurface.withAlpha(10), width: 1),
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
borderRadius: const BorderRadius.all(Radius.circular(_tileRadius)),
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
colors: [
|
colors: [
|
||||||
context.colorScheme.primary.withAlpha(10),
|
context.colorScheme.primary.withAlpha(10),
|
||||||
@@ -442,83 +478,33 @@ class _QuickAccessButtonList extends ConsumerWidget {
|
|||||||
end: Alignment.bottomCenter,
|
end: Alignment.bottomCenter,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: ListView(
|
child: ListView.builder(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
children: [
|
itemCount: items.length,
|
||||||
ListTile(
|
itemBuilder: (context, index) {
|
||||||
|
final item = items[index];
|
||||||
|
final isFirst = index == 0;
|
||||||
|
final isLast = index == items.length - 1;
|
||||||
|
return ListTile(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
topLeft: const Radius.circular(20),
|
topLeft: Radius.circular(isFirst ? _tileRadius : 0),
|
||||||
topRight: const Radius.circular(20),
|
topRight: Radius.circular(isFirst ? _tileRadius : 0),
|
||||||
bottomLeft: Radius.circular(partners.isEmpty ? 20 : 0),
|
bottomLeft: Radius.circular(isLast ? _tileRadius : 0),
|
||||||
bottomRight: Radius.circular(partners.isEmpty ? 20 : 0),
|
bottomRight: Radius.circular(isLast ? _tileRadius : 0),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
leading: const Icon(Icons.folder_outlined, size: 26),
|
contentPadding: item.contentPadding,
|
||||||
title: Text(
|
leading: item.leading,
|
||||||
context.t.folders,
|
title: Text(item.title, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500)),
|
||||||
style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500),
|
onTap: item.onTap,
|
||||||
),
|
);
|
||||||
onTap: () => context.pushRoute(FolderRoute()),
|
},
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.lock_outline_rounded, size: 26),
|
|
||||||
title: Text(
|
|
||||||
context.t.locked_folder,
|
|
||||||
style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500),
|
|
||||||
),
|
|
||||||
onTap: () => context.pushRoute(const LockedFolderRoute()),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.group_outlined, size: 26),
|
|
||||||
title: Text(
|
|
||||||
context.t.partners,
|
|
||||||
style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500),
|
|
||||||
),
|
|
||||||
onTap: () => context.pushRoute(const PartnerRoute()),
|
|
||||||
),
|
|
||||||
_PartnerList(partners: partners.toList()),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PartnerList extends StatelessWidget {
|
|
||||||
const _PartnerList({required this.partners});
|
|
||||||
|
|
||||||
final List<Partner> partners;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ListView.builder(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
itemCount: partners.length,
|
|
||||||
shrinkWrap: true,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final partner = partners[index];
|
|
||||||
final isLastItem = index == partners.length - 1;
|
|
||||||
return ListTile(
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.only(
|
|
||||||
bottomLeft: Radius.circular(isLastItem ? 20 : 0),
|
|
||||||
bottomRight: Radius.circular(isLastItem ? 20 : 0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
contentPadding: const EdgeInsets.only(left: 12.0, right: 18.0),
|
|
||||||
leading: PartnerUserAvatar(userId: partner.id, name: partner.name),
|
|
||||||
title: Text(
|
|
||||||
context.t.partner_list_user_photos(user: partner.name),
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
|
||||||
),
|
|
||||||
onTap: () => context.pushRoute(PartnerDetailRoute(partner: partner)),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import 'dart:async';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/search.service.dart';
|
import 'package:immich_mobile/domain/services/search.service.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
||||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/search.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/search.provider.dart';
|
||||||
|
|
||||||
final searchPreFilterProvider = NotifierProvider<SearchFilterProvider, SearchFilter?>(SearchFilterProvider.new);
|
final searchPreFilterProvider = NotifierProvider<SearchFilterProvider, SearchFilter?>(SearchFilterProvider.new);
|
||||||
@@ -32,16 +34,21 @@ class SearchState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final paginatedSearchProvider = StateNotifierProvider<PaginatedSearchNotifier, SearchState>(
|
final paginatedSearchProvider = StateNotifierProvider<PaginatedSearchNotifier, SearchState>(
|
||||||
(ref) => PaginatedSearchNotifier(ref.watch(searchServiceProvider)),
|
(ref) => PaginatedSearchNotifier(ref.watch(searchServiceProvider), ref.watch(driftProvider).remoteAssetRepository),
|
||||||
);
|
);
|
||||||
|
|
||||||
class PaginatedSearchNotifier extends StateNotifier<SearchState> {
|
class PaginatedSearchNotifier extends StateNotifier<SearchState> {
|
||||||
final SearchService _searchService;
|
final SearchService _searchService;
|
||||||
|
final RemoteAssetRepository _remoteAssetRepository;
|
||||||
final _assetCountController = StreamController<int>.broadcast();
|
final _assetCountController = StreamController<int>.broadcast();
|
||||||
|
|
||||||
PaginatedSearchNotifier(this._searchService) : super(const SearchState());
|
StreamSubscription<Set<String>>? _deletedIdsSubscription;
|
||||||
|
List<BaseAsset> _results = const [];
|
||||||
|
int? _nextPage = 1;
|
||||||
|
|
||||||
Stream<int> get assetCount => _assetCountController.stream;
|
PaginatedSearchNotifier(this._searchService, this._remoteAssetRepository) : super(const SearchState());
|
||||||
|
|
||||||
|
Stream<int> get assetCount => _assetCountController.stream.distinct();
|
||||||
|
|
||||||
Future<void> search(SearchFilter filter) async {
|
Future<void> search(SearchFilter filter) async {
|
||||||
if (state.nextPage == null || state.isLoading) {
|
if (state.nextPage == null || state.isLoading) {
|
||||||
@@ -52,24 +59,47 @@ class PaginatedSearchNotifier extends StateNotifier<SearchState> {
|
|||||||
|
|
||||||
final result = await _searchService.search(filter, state.nextPage!);
|
final result = await _searchService.search(filter, state.nextPage!);
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
state = SearchState(assets: state.assets, nextPage: state.nextPage);
|
state = SearchState(assets: state.assets, nextPage: state.nextPage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final assets = [...state.assets, ...result.assets];
|
_results = [..._results, ...result.assets];
|
||||||
state = SearchState(assets: assets, nextPage: result.nextPage);
|
_nextPage = result.nextPage;
|
||||||
|
_watchDeletedIds(filter.display.isArchive ? AssetVisibility.archive : AssetVisibility.timeline);
|
||||||
_assetCountController.add(assets.length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
state = const SearchState();
|
unawaited(_deletedIdsSubscription?.cancel());
|
||||||
_assetCountController.add(0);
|
_deletedIdsSubscription = null;
|
||||||
|
_results = const [];
|
||||||
|
_nextPage = 1;
|
||||||
|
_emit(const {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _watchDeletedIds(AssetVisibility visibility) {
|
||||||
|
unawaited(_deletedIdsSubscription?.cancel());
|
||||||
|
|
||||||
|
final ids = _results.whereType<RemoteAsset>().map((asset) => asset.id);
|
||||||
|
_deletedIdsSubscription = _remoteAssetRepository.watchDeletedAssetIds(ids, visibility).listen(_emit);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _emit(Set<String> deletedIds) {
|
||||||
|
final visible = deletedIds.isEmpty
|
||||||
|
? _results
|
||||||
|
: _results.where((asset) => asset is! RemoteAsset || !deletedIds.contains(asset.id)).toList(growable: false);
|
||||||
|
|
||||||
|
state = SearchState(assets: visible, nextPage: _nextPage);
|
||||||
|
_assetCountController.add(visible.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
unawaited(_deletedIdsSubscription?.cancel());
|
||||||
unawaited(_assetCountController.close());
|
unawaited(_assetCountController.close());
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -328,6 +328,8 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> with WidgetsBi
|
|||||||
final isMultiSelectStatusVisible = !isSelectionMode && isMultiSelectEnabled;
|
final isMultiSelectStatusVisible = !isSelectionMode && isMultiSelectEnabled;
|
||||||
final isBottomWidgetVisible =
|
final isBottomWidgetVisible =
|
||||||
widget.bottomSheet != null && (isMultiSelectStatusVisible || widget.persistentBottomBar);
|
widget.bottomSheet != null && (isMultiSelectStatusVisible || widget.persistentBottomBar);
|
||||||
|
final smoothScrubbing = ref.watch(appConfigProvider.select((config) => config.timeline.smoothScrubbing));
|
||||||
|
final snapToMonth = widget.snapToMonth && !smoothScrubbing;
|
||||||
|
|
||||||
return PopScope(
|
return PopScope(
|
||||||
canPop: !isMultiSelectEnabled,
|
canPop: !isMultiSelectEnabled,
|
||||||
@@ -409,7 +411,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> with WidgetsBi
|
|||||||
final Widget timeline;
|
final Widget timeline;
|
||||||
if (widget.withScrubber) {
|
if (widget.withScrubber) {
|
||||||
timeline = Scrubber(
|
timeline = Scrubber(
|
||||||
snapToMonth: widget.snapToMonth,
|
snapToMonth: snapToMonth,
|
||||||
layoutSegments: segments,
|
layoutSegments: segments,
|
||||||
timelineHeight: maxHeight,
|
timelineHeight: maxHeight,
|
||||||
topPadding: topPadding,
|
topPadding: topPadding,
|
||||||
|
|||||||
@@ -84,8 +84,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
|||||||
// Needs to be logged in
|
// Needs to be logged in
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
// switch endpoint if needed
|
// switch endpoint if needed
|
||||||
final endpoint = await _ref.read(authProvider.notifier).setOpenApiServiceEndpoint();
|
await _ref.read(authProvider.notifier).setOpenApiServiceEndpoint();
|
||||||
_log.info("Using server URL: $endpoint");
|
|
||||||
|
|
||||||
await _ref.read(serverInfoProvider.notifier).getServerVersion();
|
await _ref.read(serverInfoProvider.notifier).getServerVersion();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart';
|
|||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
||||||
import 'package:immich_mobile/services/share_intent_service.dart';
|
import 'package:immich_mobile/services/share_intent_service.dart';
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
final shareIntentUploadProvider = StateNotifierProvider<ShareIntentUploadStateNotifier, List<ShareIntentAttachment>>(
|
final shareIntentUploadProvider = StateNotifierProvider<ShareIntentUploadStateNotifier, List<ShareIntentAttachment>>(
|
||||||
@@ -21,7 +20,6 @@ class ShareIntentUploadStateNotifier extends StateNotifier<List<ShareIntentAttac
|
|||||||
final AppRouter router;
|
final AppRouter router;
|
||||||
final ForegroundUploadService _foregroundUploadService;
|
final ForegroundUploadService _foregroundUploadService;
|
||||||
final ShareIntentService _shareIntentService;
|
final ShareIntentService _shareIntentService;
|
||||||
final Logger _logger = Logger('ShareIntentUploadStateNotifier');
|
|
||||||
|
|
||||||
ShareIntentUploadStateNotifier(this.router, this._foregroundUploadService, this._shareIntentService) : super([]);
|
ShareIntentUploadStateNotifier(this.router, this._foregroundUploadService, this._shareIntentService) : super([]);
|
||||||
|
|
||||||
@@ -74,8 +72,7 @@ class ShareIntentUploadStateNotifier extends StateNotifier<List<ShareIntentAttac
|
|||||||
onSuccess: (fileId, _) {
|
onSuccess: (fileId, _) {
|
||||||
_updateStatus(fileId, UploadStatus.complete, progress: 1.0);
|
_updateStatus(fileId, UploadStatus.complete, progress: 1.0);
|
||||||
},
|
},
|
||||||
onError: (fileId, errorMessage) {
|
onError: (fileId, _) {
|
||||||
_logger.warning("Upload failed for file: $fileId, error: $errorMessage");
|
|
||||||
_updateStatus(fileId, UploadStatus.failed);
|
_updateStatus(fileId, UploadStatus.failed);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -221,8 +221,6 @@ class BackupNotifier extends StateNotifier<BackupState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleForegroundBackupError(String localAssetId, String errorMessage) {
|
void _handleForegroundBackupError(String localAssetId, String errorMessage) {
|
||||||
_logger.severe("Upload failed for $localAssetId: $errorMessage");
|
|
||||||
|
|
||||||
final currentItem = state.uploadItems[localAssetId];
|
final currentItem = state.uploadItems[localAssetId];
|
||||||
if (currentItem != null) {
|
if (currentItem != null) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ class UploadRepository {
|
|||||||
} on RequestAbortedException {
|
} on RequestAbortedException {
|
||||||
rethrow;
|
rethrow;
|
||||||
} on ClientException catch (error) {
|
} on ClientException catch (error) {
|
||||||
logger.warning("Upload $logContext failed before a response, resending once: $error");
|
logger.warning("Upload $logContext failed before a response, resending once", error);
|
||||||
response = await client.send(buildRequest());
|
response = await client.send(buildRequest());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,8 +124,8 @@ class UploadRepository {
|
|||||||
logger.warning("Upload $logContext was cancelled");
|
logger.warning("Upload $logContext was cancelled");
|
||||||
return UploadResult.cancelled();
|
return UploadResult.cancelled();
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
logger.warning("Error uploading $logContext: $error: $stackTrace");
|
logger.warning("Error uploading $logContext", error, stackTrace);
|
||||||
return UploadResult.error(errorMessage: error.toString());
|
return UploadResult(isSuccess: false, isCancelled: false, errorMessage: error.toString(), error: error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,6 +166,7 @@ class UploadResult {
|
|||||||
final String? remoteAssetId;
|
final String? remoteAssetId;
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
final int? statusCode;
|
final int? statusCode;
|
||||||
|
final Object? error;
|
||||||
|
|
||||||
const UploadResult({
|
const UploadResult({
|
||||||
required this.isSuccess,
|
required this.isSuccess,
|
||||||
@@ -173,6 +174,7 @@ class UploadResult {
|
|||||||
this.remoteAssetId,
|
this.remoteAssetId,
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
this.statusCode,
|
this.statusCode,
|
||||||
|
this.error,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory UploadResult.success({required String remoteAssetId}) {
|
factory UploadResult.success({required String remoteAssetId}) {
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ class AuthService {
|
|||||||
final localEndpoint = _authRepository.getLocalEndpoint();
|
final localEndpoint = _authRepository.getLocalEndpoint();
|
||||||
if (localEndpoint != null) {
|
if (localEndpoint != null) {
|
||||||
await _apiService.resolveAndSetEndpoint(localEndpoint);
|
await _apiService.resolveAndSetEndpoint(localEndpoint);
|
||||||
|
_log.info("Using local endpoint");
|
||||||
return localEndpoint;
|
return localEndpoint;
|
||||||
}
|
}
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
@@ -181,9 +182,11 @@ class AuthService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final endpoint in endpointList) {
|
for (var i = 0; i < endpointList.length; i++) {
|
||||||
try {
|
try {
|
||||||
return await _apiService.resolveAndSetEndpoint(endpoint.url);
|
final url = await _apiService.resolveAndSetEndpoint(endpointList[i].url);
|
||||||
|
_log.info("Using external endpoint ${i + 1}");
|
||||||
|
return url;
|
||||||
} on ApiException catch (error) {
|
} on ApiException catch (error) {
|
||||||
_log.severe("Cannot resolve endpoint", error);
|
_log.severe("Cannot resolve endpoint", error);
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ class ForegroundUploadService {
|
|||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
onSuccess?.call(fileId, result.remoteAssetId!);
|
onSuccess?.call(fileId, result.remoteAssetId!);
|
||||||
} else if (!result.isCancelled && result.errorMessage != null) {
|
} else if (!result.isCancelled && result.errorMessage != null) {
|
||||||
|
_logger.warning("Error(${result.statusCode}) uploading $fileId", result.error ?? result.errorMessage);
|
||||||
onError?.call(fileId, result.errorMessage!);
|
onError?.call(fileId, result.errorMessage!);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -395,7 +396,8 @@ class ForegroundUploadService {
|
|||||||
} else if (result.errorMessage != null) {
|
} else if (result.errorMessage != null) {
|
||||||
_logger.severe(
|
_logger.severe(
|
||||||
() =>
|
() =>
|
||||||
"Error(${result.statusCode}) uploading ${asset.localId} | $originalFileName | Created on ${asset.createdAt} | ${result.errorMessage}",
|
"Error(${result.statusCode}) uploading ${asset.localId} | $originalFileName | Created on ${asset.createdAt}",
|
||||||
|
result.error ?? result.errorMessage,
|
||||||
);
|
);
|
||||||
|
|
||||||
callbacks.onError?.call(asset.localId!, result.errorMessage!);
|
callbacks.onError?.call(asset.localId!, result.errorMessage!);
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import 'dart:async';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:immich_mobile/data/db/logger/database.dart';
|
||||||
|
import 'package:immich_mobile/data/db/main/database.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
@@ -15,15 +19,37 @@ import 'package:share_plus/share_plus.dart';
|
|||||||
/// Logs can be shared by calling the `shareLogs` method, which will open a share dialog
|
/// Logs can be shared by calling the `shareLogs` method, which will open a share dialog
|
||||||
/// and generate a csv file.
|
/// and generate a csv file.
|
||||||
abstract final class ImmichLogger {
|
abstract final class ImmichLogger {
|
||||||
static Future<void> shareLogs(BuildContext context) async {
|
// the startup error screen can show before the log service starts, and then the startup error was not logged
|
||||||
|
static Future<List<LogMessage>> _read(String? startupError) async {
|
||||||
|
try {
|
||||||
|
return await LogService.I.getMessages();
|
||||||
|
} on LoggerUnInitializedException {
|
||||||
|
final connection = await openSqliteConnection(name: 'immich_logs');
|
||||||
|
final logs = DriftLogger.sqlite(connection);
|
||||||
|
try {
|
||||||
|
return [
|
||||||
|
// if the log service had started, main would have logged this already
|
||||||
|
if (startupError != null)
|
||||||
|
LogMessage(message: startupError, level: LogLevel.severe, createdAt: DateTime.now()),
|
||||||
|
...await LogRepository(logs).getAll(),
|
||||||
|
];
|
||||||
|
} finally {
|
||||||
|
await logs.close();
|
||||||
|
await connection.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> shareLogs(BuildContext context, {String? startupError}) async {
|
||||||
final tempDir = await getTemporaryDirectory();
|
final tempDir = await getTemporaryDirectory();
|
||||||
final dateTime = DateTime.now().toIso8601String();
|
final dateTime = DateTime.now().toIso8601String();
|
||||||
final filePath = '${tempDir.path}/Immich_log_$dateTime.log';
|
final safeTimestamp = dateTime.replaceAll(':', '-');
|
||||||
|
final filePath = '${tempDir.path}/Immich_log_$safeTimestamp.log';
|
||||||
final logFile = await File(filePath).create();
|
final logFile = await File(filePath).create();
|
||||||
final io = logFile.openWrite();
|
final io = logFile.openWrite();
|
||||||
try {
|
try {
|
||||||
// Write messages
|
// Write messages
|
||||||
for (final m in await LogService.I.getMessages()) {
|
for (final m in await _read(startupError)) {
|
||||||
final created = m.createdAt;
|
final created = m.createdAt;
|
||||||
final level = m.level.name.padRight(8);
|
final level = m.level.name.padRight(8);
|
||||||
final logger = (m.logger ?? "<UNKNOWN_LOGGER>").padRight(20);
|
final logger = (m.logger ?? "<UNKNOWN_LOGGER>").padRight(20);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class OAuthService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final authUrl = dto?.url;
|
final authUrl = dto?.url;
|
||||||
log.info('Received Authorization URL: $authUrl');
|
log.info('Received Authorization URL');
|
||||||
|
|
||||||
return authUrl;
|
return authUrl;
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,7 @@ class OAuthService {
|
|||||||
Future<LoginResponseDto?> oAuthLogin(String oauthUrl, String state, String codeVerifier) async {
|
Future<LoginResponseDto?> oAuthLogin(String oauthUrl, String state, String codeVerifier) async {
|
||||||
String result = await FlutterWebAuth2.authenticate(url: oauthUrl, callbackUrlScheme: callbackUrlScheme);
|
String result = await FlutterWebAuth2.authenticate(url: oauthUrl, callbackUrlScheme: callbackUrlScheme);
|
||||||
|
|
||||||
log.info('Received OAuth callback: $result');
|
log.info('Received OAuth callback');
|
||||||
|
|
||||||
if (result.startsWith('app.immich:/oauth-callback')) {
|
if (result.startsWith('app.immich:/oauth-callback')) {
|
||||||
result = result.replaceAll('app.immich:/oauth-callback', 'app.immich:///oauth-callback');
|
result = result.replaceAll('app.immich:/oauth-callback', 'app.immich:///oauth-callback');
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:immich_mobile/constants/constants.dart';
|
|||||||
import 'package:immich_mobile/data/data_controller.dart';
|
import 'package:immich_mobile/data/data_controller.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/generated/translations.g.dart';
|
import 'package:immich_mobile/generated/translations.g.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
||||||
@@ -72,6 +73,7 @@ abstract final class Bootstrap {
|
|||||||
await LogService.init(
|
await LogService.init(
|
||||||
logRepository: LogRepository(dataController.logDb),
|
logRepository: LogRepository(dataController.logDb),
|
||||||
settingsRepository: settingsRepo,
|
settingsRepository: settingsRepo,
|
||||||
|
storeService: StoreService.I,
|
||||||
shouldBuffer: shouldBufferLogs,
|
shouldBuffer: shouldBufferLogs,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ final Map<String, Map<String, Object?>> openApiPatches = {
|
|||||||
'ffmpeg': {
|
'ffmpeg': {
|
||||||
'realtime': {'enabled': false},
|
'realtime': {'enabled': false},
|
||||||
},
|
},
|
||||||
|
'passkey': {'enabled': false},
|
||||||
},
|
},
|
||||||
'ServerConfigDto': {
|
'ServerConfigDto': {
|
||||||
'mapLightStyleUrl': 'https://tiles.immich.cloud/v1/style/light.json',
|
'mapLightStyleUrl': 'https://tiles.immich.cloud/v1/style/light.json',
|
||||||
@@ -55,12 +56,16 @@ final Map<String, Map<String, Object?>> openApiPatches = {
|
|||||||
'PeopleResponse': {'updateStrategy': 'everyone'},
|
'PeopleResponse': {'updateStrategy': 'everyone'},
|
||||||
'PersonResponseDto': {'otherPeople': const [], 'sharedBy': const [], 'sharedWith': const []},
|
'PersonResponseDto': {'otherPeople': const [], 'sharedBy': const [], 'sharedWith': const []},
|
||||||
'ServerVersionResponseDto': {'featureLevel': 0},
|
'ServerVersionResponseDto': {'featureLevel': 0},
|
||||||
|
'WorkflowResponseDto': {'logging': false},
|
||||||
|
'AdminConfigDto': {
|
||||||
|
'passkey': {'enabled': false, 'domain': null, 'additionalDomains': const []},
|
||||||
|
},
|
||||||
'UserConfigDto': {
|
'UserConfigDto': {
|
||||||
'notifications': {
|
'notifications': {
|
||||||
'smtp': {'enabled': false},
|
'smtp': {'enabled': false},
|
||||||
},
|
},
|
||||||
|
'passkey': {'enabled': false},
|
||||||
},
|
},
|
||||||
'WorkflowResponseDto': {'logging': false},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ignore: unused-code
|
// ignore: unused-code
|
||||||
|
|||||||
@@ -36,64 +36,63 @@ class _MesmerizingSliverAppBarState extends ConsumerState<MesmerizingSliverAppBa
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isMultiSelectEnabled = ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
final isMultiSelectEnabled = ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
||||||
|
Widget hideWhenSelecting(Widget child) => IgnorePointer(
|
||||||
|
ignoring: isMultiSelectEnabled,
|
||||||
|
child: AnimatedOpacity(duration: Durations.medium1, opacity: isMultiSelectEnabled ? 0 : 1, child: child),
|
||||||
|
);
|
||||||
|
|
||||||
return isMultiSelectEnabled
|
return SliverAppBar(
|
||||||
? SliverToBoxAdapter(
|
expandedHeight: 300.0,
|
||||||
child: switch (_scrollProgress) {
|
floating: false,
|
||||||
< 0.8 => const SizedBox(height: 120),
|
pinned: true,
|
||||||
_ => const SizedBox(height: 352),
|
snap: false,
|
||||||
},
|
elevation: 0,
|
||||||
)
|
leading: hideWhenSelecting(
|
||||||
: SliverAppBar(
|
IconButton(
|
||||||
expandedHeight: 300.0,
|
icon: Icon(
|
||||||
floating: false,
|
Platform.isIOS ? Icons.arrow_back_ios_new_rounded : Icons.arrow_back,
|
||||||
pinned: true,
|
color: Color.lerp(Colors.white, context.primaryColor, _scrollProgress),
|
||||||
snap: false,
|
shadows: [
|
||||||
elevation: 0,
|
_scrollProgress < 0.95
|
||||||
leading: IconButton(
|
? Shadow(offset: const Offset(0, 2), blurRadius: 5, color: Colors.black.withValues(alpha: 0.5))
|
||||||
icon: Icon(
|
: const Shadow(offset: Offset(0, 2), blurRadius: 0, color: Colors.transparent),
|
||||||
Platform.isIOS ? Icons.arrow_back_ios_new_rounded : Icons.arrow_back,
|
],
|
||||||
color: Color.lerp(Colors.white, context.primaryColor, _scrollProgress),
|
),
|
||||||
shadows: [
|
onPressed: () {
|
||||||
_scrollProgress < 0.95
|
context.pop();
|
||||||
? Shadow(offset: const Offset(0, 2), blurRadius: 5, color: Colors.black.withValues(alpha: 0.5))
|
},
|
||||||
: const Shadow(offset: Offset(0, 2), blurRadius: 0, color: Colors.transparent),
|
),
|
||||||
],
|
),
|
||||||
),
|
flexibleSpace: Builder(
|
||||||
onPressed: () {
|
builder: (context) {
|
||||||
context.pop();
|
final settings = context.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>();
|
||||||
},
|
final scrollProgress = _calculateScrollProgress(settings);
|
||||||
),
|
|
||||||
flexibleSpace: Builder(
|
|
||||||
builder: (context) {
|
|
||||||
final settings = context.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>();
|
|
||||||
final scrollProgress = _calculateScrollProgress(settings);
|
|
||||||
|
|
||||||
// Update scroll progress for the leading button
|
// Update scroll progress for the leading button
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (mounted && _scrollProgress != scrollProgress) {
|
if (mounted && _scrollProgress != scrollProgress) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_scrollProgress = scrollProgress;
|
_scrollProgress = scrollProgress;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return FlexibleSpaceBar(
|
return FlexibleSpaceBar(
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
title: AnimatedSwitcher(
|
title: AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
child: scrollProgress > 0.95
|
child: scrollProgress > 0.95
|
||||||
? Text(
|
? Text(
|
||||||
widget.title,
|
widget.title,
|
||||||
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.w600, fontSize: 18),
|
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.w600, fontSize: 18),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
|
||||||
background: _ExpandedBackground(scrollProgress: scrollProgress, title: widget.title),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
background: _ExpandedBackground(scrollProgress: scrollProgress, title: widget.title),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ class _MesmerizingSliverAppBarState extends ConsumerState<PersonSliverAppBar> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isMultiSelectEnabled = ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
final isMultiSelectEnabled = ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
||||||
|
Widget hideWhenSelecting(Widget child) => IgnorePointer(
|
||||||
|
ignoring: isMultiSelectEnabled,
|
||||||
|
child: AnimatedOpacity(duration: Durations.medium1, opacity: isMultiSelectEnabled ? 0 : 1, child: child),
|
||||||
|
);
|
||||||
final Color? actionIconColor = Color.lerp(Colors.white, context.primaryColor, _scrollProgress);
|
final Color? actionIconColor = Color.lerp(Colors.white, context.primaryColor, _scrollProgress);
|
||||||
final List<Shadow> actionIconShadows = [
|
final List<Shadow> actionIconShadows = [
|
||||||
if (_scrollProgress < 0.95)
|
if (_scrollProgress < 0.95)
|
||||||
@@ -62,74 +66,74 @@ class _MesmerizingSliverAppBarState extends ConsumerState<PersonSliverAppBar> {
|
|||||||
const Shadow(offset: Offset(0, 2), blurRadius: 0, color: Colors.transparent),
|
const Shadow(offset: Offset(0, 2), blurRadius: 0, color: Colors.transparent),
|
||||||
];
|
];
|
||||||
|
|
||||||
return isMultiSelectEnabled
|
return SliverAppBar(
|
||||||
? SliverToBoxAdapter(
|
expandedHeight: 300.0,
|
||||||
child: switch (_scrollProgress) {
|
floating: false,
|
||||||
< 0.8 => const SizedBox(height: 120),
|
pinned: true,
|
||||||
_ => const SizedBox(height: 352),
|
snap: false,
|
||||||
},
|
elevation: 0,
|
||||||
)
|
leading: hideWhenSelecting(
|
||||||
: SliverAppBar(
|
IconButton(
|
||||||
expandedHeight: 300.0,
|
icon: Icon(
|
||||||
floating: false,
|
Platform.isIOS ? Icons.arrow_back_ios_new_rounded : Icons.arrow_back,
|
||||||
pinned: true,
|
color: Color.lerp(Colors.white, context.primaryColor, _scrollProgress),
|
||||||
snap: false,
|
shadows: [
|
||||||
elevation: 0,
|
_scrollProgress < 0.95
|
||||||
leading: IconButton(
|
? Shadow(offset: const Offset(0, 2), blurRadius: 5, color: Colors.black.withValues(alpha: 0.5))
|
||||||
icon: Icon(
|
: const Shadow(offset: Offset(0, 2), blurRadius: 0, color: Colors.transparent),
|
||||||
Platform.isIOS ? Icons.arrow_back_ios_new_rounded : Icons.arrow_back,
|
|
||||||
color: Color.lerp(Colors.white, context.primaryColor, _scrollProgress),
|
|
||||||
shadows: [
|
|
||||||
_scrollProgress < 0.95
|
|
||||||
? Shadow(offset: const Offset(0, 2), blurRadius: 5, color: Colors.black.withValues(alpha: 0.5))
|
|
||||||
: const Shadow(offset: Offset(0, 2), blurRadius: 0, color: Colors.transparent),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
context.pop();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(Icons.more_vert, color: actionIconColor, shadows: actionIconShadows),
|
|
||||||
onPressed: widget.onShowOptions,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
flexibleSpace: Builder(
|
),
|
||||||
builder: (context) {
|
onPressed: () {
|
||||||
final settings = context.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>();
|
context.pop();
|
||||||
final scrollProgress = _calculateScrollProgress(settings);
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
hideWhenSelecting(
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.more_vert, color: actionIconColor, shadows: actionIconShadows),
|
||||||
|
onPressed: widget.onShowOptions,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
flexibleSpace: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
final settings = context.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>();
|
||||||
|
final scrollProgress = _calculateScrollProgress(settings);
|
||||||
|
|
||||||
// Update scroll progress for the leading button
|
// Update scroll progress for the leading button
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (mounted && _scrollProgress != scrollProgress) {
|
if (mounted && _scrollProgress != scrollProgress) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_scrollProgress = scrollProgress;
|
_scrollProgress = scrollProgress;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return FlexibleSpaceBar(
|
return FlexibleSpaceBar(
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
title: AnimatedSwitcher(
|
title: AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
child: scrollProgress > 0.95
|
child: scrollProgress > 0.95
|
||||||
? Text(
|
? Text(
|
||||||
widget.person.name,
|
widget.person.name,
|
||||||
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.w600, fontSize: 18),
|
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.w600, fontSize: 18),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
background: _ExpandedBackground(
|
background: IgnorePointer(
|
||||||
scrollProgress: scrollProgress,
|
ignoring: isMultiSelectEnabled,
|
||||||
person: widget.person,
|
child: _ExpandedBackground(
|
||||||
onNameTap: widget.onNameTap,
|
scrollProgress: scrollProgress,
|
||||||
onBirthdayTap: widget.onBirthdayTap,
|
person: widget.person,
|
||||||
),
|
onNameTap: widget.onNameTap,
|
||||||
);
|
onBirthdayTap: widget.onBirthdayTap,
|
||||||
},
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class AssetListSettings extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final storageIndicator = useValueNotifier(ref.watch(appConfigProvider.select((s) => s.timeline.storageIndicator)));
|
final storageIndicator = useValueNotifier(ref.watch(appConfigProvider.select((s) => s.timeline.storageIndicator)));
|
||||||
|
final smoothScrubbing = useValueNotifier(ref.watch(appConfigProvider.select((s) => s.timeline.smoothScrubbing)));
|
||||||
|
|
||||||
final assetListSetting = [
|
final assetListSetting = [
|
||||||
SettingsSwitchListTile(
|
SettingsSwitchListTile(
|
||||||
@@ -27,6 +28,16 @@ class AssetListSettings extends HookConsumerWidget {
|
|||||||
ref.invalidate(settingsProvider);
|
ref.invalidate(settingsProvider);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
SettingsSwitchListTile(
|
||||||
|
valueNotifier: smoothScrubbing,
|
||||||
|
title: context.t.theme_setting_asset_list_smooth_scrubbing_title,
|
||||||
|
subtitle: context.t.theme_setting_asset_list_smooth_scrubbing_subtitle,
|
||||||
|
onChanged: (value) {
|
||||||
|
unawaited(ref.read(settingsProvider).write(.timelineSmoothScrubbing, value));
|
||||||
|
ref.invalidate(appSettingsServiceProvider);
|
||||||
|
ref.invalidate(settingsProvider);
|
||||||
|
},
|
||||||
|
),
|
||||||
const LayoutSettings(),
|
const LayoutSettings(),
|
||||||
const GroupSettings(),
|
const GroupSettings(),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
[tools]
|
[tools]
|
||||||
java = "21.0.2"
|
java = "21.0.2"
|
||||||
|
rust = { version = "1.99.1", targets = "aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android,aarch64-apple-ios,aarch64-apple-ios-sim,x86_64-apple-ios" }
|
||||||
|
|
||||||
[tools."aqua:flutter/flutter"]
|
[tools."aqua:flutter/flutter"]
|
||||||
version = "3.47.2"
|
version = "3.47.2"
|
||||||
@@ -34,6 +35,7 @@ depends = [
|
|||||||
"codegen:dart",
|
"codegen:dart",
|
||||||
"codegen:drift:migration",
|
"codegen:drift:migration",
|
||||||
"codegen:drift:schema",
|
"codegen:drift:schema",
|
||||||
|
"codegen:native",
|
||||||
"codegen:pigeon",
|
"codegen:pigeon",
|
||||||
"codegen:translation",
|
"codegen:translation",
|
||||||
]
|
]
|
||||||
@@ -80,6 +82,11 @@ alias = "watch"
|
|||||||
description = "Watch and auto-generate dart code"
|
description = "Watch and auto-generate dart code"
|
||||||
run = "dart run build_runner watch"
|
run = "dart run build_runner watch"
|
||||||
|
|
||||||
|
[tasks."codegen:native"]
|
||||||
|
description = "Generate the native C header and Dart bindings"
|
||||||
|
dir = "../native/native_core"
|
||||||
|
run = ["cargo build -p native_core_ffi", "flutter pub get", "dart run ffigen --config ffigen.yaml"]
|
||||||
|
|
||||||
[tasks."codegen:pigeon"]
|
[tasks."codegen:pigeon"]
|
||||||
alias = "pigeon"
|
alias = "pigeon"
|
||||||
description = "Generate pigeon platform code"
|
description = "Generate pigeon platform code"
|
||||||
@@ -139,6 +146,7 @@ depends = ["//:open-api-dart"]
|
|||||||
run = [
|
run = [
|
||||||
# This receives passed `--enforce-lockfile` args
|
# This receives passed `--enforce-lockfile` args
|
||||||
"flutter pub get",
|
"flutter pub get",
|
||||||
|
{ task = "install:build-runner-patch" },
|
||||||
{ task = "install:ios" },
|
{ task = "install:ios" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -198,6 +206,11 @@ fi
|
|||||||
cd ios && pod install
|
cd ios && pod install
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
[tasks."install:build-runner-patch"]
|
||||||
|
description = "Patch build_runner to prevent crashes when sources are deleted"
|
||||||
|
hide = true
|
||||||
|
run = "node ./scripts/build_runner_patch.mjs"
|
||||||
|
|
||||||
[tasks."analyze:dart"]
|
[tasks."analyze:dart"]
|
||||||
description = "Run Dart analysis"
|
description = "Run Dart analysis"
|
||||||
hide = true
|
hide = true
|
||||||
|
|||||||
+29
-6
@@ -796,10 +796,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: hooks
|
name: hooks
|
||||||
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
|
sha256: eaac480a35ec0814146c2c48d96aaa829e0e44a7662c88ae84c9edf4bc35651f
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.2"
|
version: "2.2.0"
|
||||||
hooks_riverpod:
|
hooks_riverpod:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1132,6 +1132,13 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.5"
|
version: "1.0.5"
|
||||||
|
native_core:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: "../native/native_core"
|
||||||
|
relative: true
|
||||||
|
source: path
|
||||||
|
version: "0.0.0"
|
||||||
native_toolchain_c:
|
native_toolchain_c:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1140,6 +1147,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.19.1"
|
version: "0.19.1"
|
||||||
|
native_toolchain_rust:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: native_toolchain_rust
|
||||||
|
sha256: "8469a0e32a47a5c84b7fd300de9fd0e8082aac76c37922986324f5082a58350d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.6"
|
||||||
native_video_player:
|
native_video_player:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1458,10 +1473,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: record_use
|
name: record_use
|
||||||
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
|
sha256: "1cb8564af8d43b464294411db9217f5ec04891c6f22ee2c32d73ae05e88a6bd2"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.0"
|
version: "1.1.1"
|
||||||
riverpod:
|
riverpod:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1664,10 +1679,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: sqlite3_connection_pool
|
name: sqlite3_connection_pool
|
||||||
sha256: ad51972d37bdf82929888e4c85e722bd9a0b5ff8ad389f89b1b00d19603c0d2d
|
sha256: ad8413d9100598ee9ef1a27628f5673b7d9212fb9d15b2742f7fde690f743832
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.7"
|
version: "0.2.13"
|
||||||
sqlite3_web:
|
sqlite3_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1772,6 +1787,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.4"
|
version: "0.9.4"
|
||||||
|
toml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: toml
|
||||||
|
sha256: "35a35f782228656a2af31e8c73d1353cc4ef3d683fd68af1111b44631879c05e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.18.0"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
+4
-2
@@ -2,7 +2,7 @@ name: immich_mobile
|
|||||||
description: Immich - selfhosted backup media file on mobile phone
|
description: Immich - selfhosted backup media file on mobile phone
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 3.3.3-local.6+3030404
|
version: 3.3.5-local.12+3030504
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.12.0 <4.0.0'
|
sdk: '>=3.12.0 <4.0.0'
|
||||||
@@ -39,6 +39,8 @@ dependencies:
|
|||||||
hooks_riverpod: ^2.6.1
|
hooks_riverpod: ^2.6.1
|
||||||
http: ^1.6.0
|
http: ^1.6.0
|
||||||
image_picker: ^1.2.3
|
image_picker: ^1.2.3
|
||||||
|
native_core:
|
||||||
|
path: ../native/native_core
|
||||||
immich_ui:
|
immich_ui:
|
||||||
path: './packages/ui'
|
path: './packages/ui'
|
||||||
intl: ^0.20.2
|
intl: ^0.20.2
|
||||||
@@ -69,7 +71,7 @@ dependencies:
|
|||||||
stream_transform: ^2.1.1
|
stream_transform: ^2.1.1
|
||||||
sqlite3: ^3.4.0
|
sqlite3: ^3.4.0
|
||||||
sqlite_async: 0.14.2
|
sqlite_async: 0.14.2
|
||||||
sqlite3_connection_pool: ^0.2.7
|
sqlite3_connection_pool: ^0.2.13
|
||||||
thumbhash: 0.1.0+1
|
thumbhash: 0.1.0+1
|
||||||
timezone: ^0.9.4
|
timezone: ^0.9.4
|
||||||
url_launcher: ^6.3.2
|
url_launcher: ^6.3.2
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// Prevents build_runner crashing when a source file is deleted from a path dependency, which commonly occurs to `generated/openapi` on branch switch
|
||||||
|
//
|
||||||
|
// Fixed upstream in 2.15.3 (https://github.com/dart-lang/build/pull/5022), but we can't take that dependency due to Dart analyzer being locked
|
||||||
|
import { readFileSync, writeFileSync } from 'node:fs';
|
||||||
|
|
||||||
|
const packageConfig = new URL('../.dart_tool/package_config.json', import.meta.url);
|
||||||
|
|
||||||
|
const config = JSON.parse(readFileSync(packageConfig, 'utf8'));
|
||||||
|
const buildRunner = new URL(config.packages.find(({ name }) => name === 'build_runner').rootUri);
|
||||||
|
|
||||||
|
const file = new URL(`${buildRunner}/lib/src/build_plan/build_plan.dart`);
|
||||||
|
|
||||||
|
const buggy = 'for (final id in deletedOutputs) {';
|
||||||
|
const fixed = 'for (final id in deletedOutputs.difference(buildInputs.deletedSources.build().asSet())) {';
|
||||||
|
|
||||||
|
const source = readFileSync(file, 'utf8');
|
||||||
|
if (source.includes(buggy)) {
|
||||||
|
writeFileSync(file, source.replace(buggy, fixed));
|
||||||
|
console.log('build_runner patch: stopped deleting removed sources from path dependencies');
|
||||||
|
}
|
||||||
@@ -1,13 +1,23 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:drift/native.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:http/http.dart' show ClientException;
|
||||||
import 'package:immich_mobile/constants/constants.dart';
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
|
import 'package:immich_mobile/data/db/main/database.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/config/network_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/settings_key.dart';
|
import 'package:immich_mobile/domain/models/settings_key.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:openapi/api.dart' show ApiException;
|
||||||
|
|
||||||
import '../../infrastructure/repository.mock.dart';
|
import '../../infrastructure/repository.mock.dart';
|
||||||
import '../../test_utils.dart';
|
import '../../test_utils.dart';
|
||||||
@@ -30,6 +40,16 @@ void main() {
|
|||||||
late LogService sut;
|
late LogService sut;
|
||||||
late LogRepository mockLogRepo;
|
late LogRepository mockLogRepo;
|
||||||
late MockSettingsRepository mockSettingsRepository;
|
late MockSettingsRepository mockSettingsRepository;
|
||||||
|
late Drift db;
|
||||||
|
|
||||||
|
setUpAll(() async {
|
||||||
|
db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
|
||||||
|
await StoreService.init(storeRepository: StoreRepository(db), listenUpdates: false);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDownAll(() async {
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
mockLogRepo = MockLogRepository();
|
mockLogRepo = MockLogRepository();
|
||||||
@@ -45,11 +65,16 @@ void main() {
|
|||||||
when(() => mockLogRepo.insert(any())).thenAnswer((_) async => true);
|
when(() => mockLogRepo.insert(any())).thenAnswer((_) async => true);
|
||||||
when(() => mockLogRepo.insertAll(any())).thenAnswer((_) async => true);
|
when(() => mockLogRepo.insertAll(any())).thenAnswer((_) async => true);
|
||||||
|
|
||||||
sut = await LogService.create(logRepository: mockLogRepo, settingsRepository: mockSettingsRepository);
|
sut = await LogService.create(
|
||||||
|
logRepository: mockLogRepo,
|
||||||
|
settingsRepository: mockSettingsRepository,
|
||||||
|
storeService: StoreService.I,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() async {
|
tearDown(() async {
|
||||||
await sut.dispose();
|
await sut.dispose();
|
||||||
|
await StoreService.I.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
group("Log Service Init:", () {
|
group("Log Service Init:", () {
|
||||||
@@ -87,6 +112,7 @@ void main() {
|
|||||||
sut = await LogService.create(
|
sut = await LogService.create(
|
||||||
logRepository: mockLogRepo,
|
logRepository: mockLogRepo,
|
||||||
settingsRepository: mockSettingsRepository,
|
settingsRepository: mockSettingsRepository,
|
||||||
|
storeService: StoreService.I,
|
||||||
shouldBuffer: true,
|
shouldBuffer: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -105,6 +131,7 @@ void main() {
|
|||||||
sut = await LogService.create(
|
sut = await LogService.create(
|
||||||
logRepository: mockLogRepo,
|
logRepository: mockLogRepo,
|
||||||
settingsRepository: mockSettingsRepository,
|
settingsRepository: mockSettingsRepository,
|
||||||
|
storeService: StoreService.I,
|
||||||
shouldBuffer: true,
|
shouldBuffer: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -126,6 +153,7 @@ void main() {
|
|||||||
sut = await LogService.create(
|
sut = await LogService.create(
|
||||||
logRepository: mockLogRepo,
|
logRepository: mockLogRepo,
|
||||||
settingsRepository: mockSettingsRepository,
|
settingsRepository: mockSettingsRepository,
|
||||||
|
storeService: StoreService.I,
|
||||||
shouldBuffer: false,
|
shouldBuffer: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -160,6 +188,7 @@ void main() {
|
|||||||
sut = await LogService.create(
|
sut = await LogService.create(
|
||||||
logRepository: mockLogRepo,
|
logRepository: mockLogRepo,
|
||||||
settingsRepository: mockSettingsRepository,
|
settingsRepository: mockSettingsRepository,
|
||||||
|
storeService: StoreService.I,
|
||||||
shouldBuffer: true,
|
shouldBuffer: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -176,4 +205,144 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group("Log Service Redaction:", () {
|
||||||
|
const accessToken = 'Kq3Zr8WbN2xYp5aLm7cT9vQ4sD6fH1jE0gU2iO5wA8y';
|
||||||
|
|
||||||
|
test('Replaces urls, the access token and header values when a line is logged', () async {
|
||||||
|
await StoreService.I.put(StoreKey.serverUrl, 'https://immich.example.com');
|
||||||
|
await StoreService.I.put(StoreKey.serverEndpoint, 'https://api.example.com/api');
|
||||||
|
await StoreService.I.put(StoreKey.accessToken, accessToken);
|
||||||
|
when(() => mockSettingsRepository.appConfig).thenReturn(
|
||||||
|
const AppConfig(
|
||||||
|
network: NetworkConfig(
|
||||||
|
localEndpoint: 'http://192.168.1.5:2283',
|
||||||
|
externalEndpointList: ['https://immich.example.com', 'http://nas.lan:2283'],
|
||||||
|
customHeaders: {'X-Proxy-Key': 'cf-secret-abcdef0123', 'X-Debug': '1'},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const rows = {
|
||||||
|
'Resuming session at https://immich.example.com/api': 'Resuming session at [EXTERNAL ENDPOINT 1]/api',
|
||||||
|
'GET http://nas.lan:2283/api/server/ping': 'GET [EXTERNAL ENDPOINT 2]/api/server/ping',
|
||||||
|
'GET http://192.168.1.5:2283/api/server/ping': 'GET [LOCAL ENDPOINT]/api/server/ping',
|
||||||
|
'Using server URL: https://api.example.com/api': 'Using server URL: [SERVER URL]/api',
|
||||||
|
'Received Authorization URL: https://auth.example.org/authorize':
|
||||||
|
'Received Authorization URL: [OTHER URL]/authorize',
|
||||||
|
'Causing statement: INSERT INTO "store_entity", parameters: 11, $accessToken':
|
||||||
|
'Causing statement: INSERT INTO "store_entity", parameters: 11, [ACCESS TOKEN]',
|
||||||
|
'Could not enqueue task DownloadTask{headers: {X-Proxy-Key: cf-secret-abcdef0123, X-Debug: 1}, retries: 1}':
|
||||||
|
'Could not enqueue task DownloadTask{headers: {X-Proxy-Key: [HEADER VALUE], X-Debug: [HEADER VALUE]}, retries: 1}',
|
||||||
|
'X-Proxy-Key:\tcf-secret-abcdef0123': 'X-Proxy-Key: [HEADER VALUE]',
|
||||||
|
'Uploaded 1 assets in 1.2s': 'Uploaded 1 assets in 1.2s',
|
||||||
|
};
|
||||||
|
for (final row in rows.keys) {
|
||||||
|
Logger('Test').info(row);
|
||||||
|
}
|
||||||
|
await sut.flush();
|
||||||
|
|
||||||
|
final stored = verify(() => mockLogRepo.insertAll(captureAny())).captured.single as List<LogMessage>;
|
||||||
|
expect(stored.map((m) => m.message), rows.values);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Hides the host and ip in http client errors', () async {
|
||||||
|
when(
|
||||||
|
() => mockSettingsRepository.appConfig,
|
||||||
|
).thenReturn(const AppConfig(network: NetworkConfig(externalEndpointList: ['http://immich:2283'])));
|
||||||
|
final connectError = ClientException(
|
||||||
|
'java.net.ConnectException: Failed to connect to immich/192.168.1.5:2283',
|
||||||
|
Uri.parse('http://immich:2283/api/server/ping'),
|
||||||
|
);
|
||||||
|
final unknownHostError = ClientException(
|
||||||
|
'java.net.UnknownHostException: Unable to resolve host "typo.example.org": No address associated with hostname',
|
||||||
|
Uri.parse('https://alice:secret@typo.example.org/api/users/me'),
|
||||||
|
);
|
||||||
|
|
||||||
|
Logger('Test').severe(
|
||||||
|
'Error while checking server availability',
|
||||||
|
ApiException.withInner(
|
||||||
|
400,
|
||||||
|
'HTTP connection failed: GET /server/ping',
|
||||||
|
connectError,
|
||||||
|
StackTrace.fromString('#0 ApiService._isEndpointAvailable (package:immich_mobile/services/api.service.dart)'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Logger('Test').severe('Error validating auxiliary endpoint', unknownHostError);
|
||||||
|
await sut.flush();
|
||||||
|
|
||||||
|
final stored = verify(() => mockLogRepo.insertAll(captureAny())).captured.single as List<LogMessage>;
|
||||||
|
expect(
|
||||||
|
stored.first.error,
|
||||||
|
'ApiException 400: HTTP connection failed: GET /server/ping (Inner exception: ClientException: '
|
||||||
|
'java.net.ConnectException: Failed to connect to [EXTERNAL ENDPOINT 1]/[IP ADDRESS]:2283, '
|
||||||
|
'uri=[EXTERNAL ENDPOINT 1]/api/server/ping)\n\n'
|
||||||
|
'#0 ApiService._isEndpointAvailable (package:immich_mobile/services/api.service.dart)',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
stored.last.error,
|
||||||
|
'ClientException: java.net.UnknownHostException: Unable to resolve host "[OTHER URL]": '
|
||||||
|
'No address associated with hostname, uri=[OTHER URL]/api/users/me',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Hides ipv6 addresses in http client errors', () async {
|
||||||
|
const rows = {
|
||||||
|
'java.net.ConnectException: Failed to connect to immich.example.com/[2001:db8::5]:443':
|
||||||
|
'java.net.ConnectException: Failed to connect to [OTHER URL]/[IP ADDRESS]:443',
|
||||||
|
'java.net.ConnectException: Failed to connect to immich.example.com/2001:db8::5:443':
|
||||||
|
'java.net.ConnectException: Failed to connect to [OTHER URL]/[IP ADDRESS]',
|
||||||
|
'java.net.SocketTimeoutException: failed to connect to immich.example.com/2001:db8::5 (port 443) '
|
||||||
|
'from /2001:db8:1234:5678:9abc:def0:1234:5678 (port 41234) after 30000ms':
|
||||||
|
'java.net.SocketTimeoutException: failed to connect to [OTHER URL]/[IP ADDRESS] (port 443) '
|
||||||
|
'from /[IP ADDRESS] (port 41234) after 30000ms',
|
||||||
|
'java.net.ConnectException: Failed to connect to /[fe80::5%wlan0]:2283':
|
||||||
|
'java.net.ConnectException: Failed to connect to /[IP ADDRESS]:2283',
|
||||||
|
'at 12:34:56': 'at 12:34:56',
|
||||||
|
'0:00:01.500000': '0:00:01.500000',
|
||||||
|
'deadbeef': 'deadbeef',
|
||||||
|
'#0 ApiClient.invokeAPI (package:openapi/api_client.dart:131:7)':
|
||||||
|
'#0 ApiClient.invokeAPI (package:openapi/api_client.dart:131:7)',
|
||||||
|
};
|
||||||
|
for (final row in rows.keys) {
|
||||||
|
Logger('Test').severe('Upload error', ClientException(row, Uri.parse('https://immich.example.com/api/assets')));
|
||||||
|
}
|
||||||
|
await sut.flush();
|
||||||
|
|
||||||
|
final stored = verify(() => mockLogRepo.insertAll(captureAny())).captured.single as List<LogMessage>;
|
||||||
|
expect(stored.map((m) => m.error), [
|
||||||
|
for (final row in rows.values) 'ClientException: $row, uri=[OTHER URL]/api/assets',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Keeps the labels of a line logged before a logout', () async {
|
||||||
|
final settings = await SettingsRepository.ensureInitialized(db);
|
||||||
|
addTearDown(SettingsRepository.reset);
|
||||||
|
await settings.write(SettingsKey.networkExternalEndpointList, ['https://immich.example.com']);
|
||||||
|
await StoreService.I.put(StoreKey.accessToken, accessToken);
|
||||||
|
await sut.dispose();
|
||||||
|
sut = await LogService.create(
|
||||||
|
logRepository: mockLogRepo,
|
||||||
|
settingsRepository: settings,
|
||||||
|
storeService: StoreService.I,
|
||||||
|
);
|
||||||
|
|
||||||
|
Logger('Test').info('Resuming session at https://immich.example.com/api with $accessToken');
|
||||||
|
// the store and settings changes of AuthService.clearLocalData
|
||||||
|
await StoreService.I.delete(StoreKey.accessToken);
|
||||||
|
await settings.clear(const [
|
||||||
|
.networkAutoEndpointSwitching,
|
||||||
|
.networkPreferredWifiName,
|
||||||
|
.networkLocalEndpoint,
|
||||||
|
.networkExternalEndpointList,
|
||||||
|
]);
|
||||||
|
Logger('Test').info('Resuming session at https://immich.example.com/api');
|
||||||
|
await sut.flush();
|
||||||
|
|
||||||
|
final stored = verify(() => mockLogRepo.insertAll(captureAny())).captured.single as List<LogMessage>;
|
||||||
|
expect(stored.map((m) => m.message), [
|
||||||
|
'Resuming session at [EXTERNAL ENDPOINT 1]/api with [ACCESS TOKEN]',
|
||||||
|
'Resuming session at [OTHER URL]/api',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ Future<void> _writerTask(String path) async {
|
|||||||
} finally {
|
} finally {
|
||||||
await db.close();
|
await db.close();
|
||||||
await dbConnection.close();
|
await dbConnection.close();
|
||||||
sqlitePool.close();
|
await sqlitePool.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,24 @@ void main() {
|
|||||||
expect(args[4], isNull);
|
expect(args[4], isNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('accepts a decoded image for an animation request', () async {
|
||||||
|
final pointer = malloc<Uint8>(8)..asTypedList(8).setAll(0, [255, 0, 0, 255, 0, 0, 255, 255]);
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockDecodedMessageHandler(channel, (
|
||||||
|
message,
|
||||||
|
) async {
|
||||||
|
return <Object?>[
|
||||||
|
<Object?, Object?>{'pointer': pointer.address, 'width': 2, 'height': 1, 'rowBytes': 8},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
final codec = (await RemoteImageRequest(uri: 'https://example.test/animation').loadCodec())!;
|
||||||
|
final image = (await codec.getNextFrame()).image;
|
||||||
|
|
||||||
|
expect((image.width, image.height), (2, 1));
|
||||||
|
image.dispose();
|
||||||
|
codec.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
test('keeps portrait cover quality in a wide tile', () async {
|
test('keeps portrait cover quality in a wide tile', () async {
|
||||||
final image = await loadEncoded('assets/feature_message/ocr.webp', const ui.Size(963, 642));
|
final image = await loadEncoded('assets/feature_message/ocr.webp', const ui.Size(963, 642));
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import 'package:async/async.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
||||||
|
|
||||||
import '../repository_context.dart';
|
import '../repository_context.dart';
|
||||||
@@ -16,6 +18,37 @@ void main() {
|
|||||||
await ctx.dispose();
|
await ctx.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('getStackChildren', () {
|
||||||
|
test('returns stack children from oldest to newest', () async {
|
||||||
|
final user = await ctx.newUser();
|
||||||
|
const stackId = 'stack';
|
||||||
|
final primary = await ctx.newRemoteAsset(
|
||||||
|
id: 'primary',
|
||||||
|
ownerId: user.id,
|
||||||
|
stackId: stackId,
|
||||||
|
createdAt: DateTime(2026, 1, 2),
|
||||||
|
);
|
||||||
|
final beforePrimary = await ctx.newRemoteAsset(
|
||||||
|
id: 'before-primary',
|
||||||
|
ownerId: user.id,
|
||||||
|
stackId: stackId,
|
||||||
|
createdAt: DateTime(2026, 1, 1),
|
||||||
|
);
|
||||||
|
final afterPrimary = await ctx.newRemoteAsset(
|
||||||
|
id: 'after-primary',
|
||||||
|
ownerId: user.id,
|
||||||
|
stackId: stackId,
|
||||||
|
createdAt: DateTime(2026, 1, 3),
|
||||||
|
);
|
||||||
|
await ctx.newRemoteAsset(ownerId: user.id, stackId: 'other-stack');
|
||||||
|
|
||||||
|
final primaryAsset = await sut.get(primary.id);
|
||||||
|
final stack = await sut.getStackChildren(primaryAsset!);
|
||||||
|
|
||||||
|
expect(stack.map((asset) => asset.id), [beforePrimary.id, afterPrimary.id]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
group('getByChecksum', () {
|
group('getByChecksum', () {
|
||||||
late String userId;
|
late String userId;
|
||||||
|
|
||||||
@@ -64,4 +97,53 @@ void main() {
|
|||||||
expect(result[0].id, remote.id);
|
expect(result[0].id, remote.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('watchDeletedAssetIds', () {
|
||||||
|
late String userId;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
final user = await ctx.newUser();
|
||||||
|
userId = user.id;
|
||||||
|
await ctx.newAuthUser(id: userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hides trashed and archived assets but keeps matching and unsynced ones', () async {
|
||||||
|
final timeline = await ctx.newRemoteAsset(ownerId: userId);
|
||||||
|
final trashed = await ctx.newRemoteAsset(ownerId: userId, deletedAt: DateTime(2020));
|
||||||
|
final archived = await ctx.newRemoteAsset(ownerId: userId, visibility: AssetVisibility.archive);
|
||||||
|
|
||||||
|
final result = await sut.watchDeletedAssetIds([
|
||||||
|
timeline.id,
|
||||||
|
trashed.id,
|
||||||
|
archived.id,
|
||||||
|
'never-synced',
|
||||||
|
], AssetVisibility.timeline).first;
|
||||||
|
|
||||||
|
expect(result, {trashed.id, archived.id});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hides assets not matching the requested visibility for archive searches', () async {
|
||||||
|
final timeline = await ctx.newRemoteAsset(ownerId: userId);
|
||||||
|
final archived = await ctx.newRemoteAsset(ownerId: userId, visibility: AssetVisibility.archive);
|
||||||
|
|
||||||
|
final result = await sut.watchDeletedAssetIds([timeline.id, archived.id], AssetVisibility.archive).first;
|
||||||
|
|
||||||
|
expect(result, {timeline.id});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('emits when an asset is trashed and again when it is restored', () async {
|
||||||
|
final asset = await ctx.newRemoteAsset(ownerId: userId);
|
||||||
|
final queue = StreamQueue(sut.watchDeletedAssetIds([asset.id], AssetVisibility.timeline));
|
||||||
|
|
||||||
|
expect(await queue.next, isEmpty);
|
||||||
|
|
||||||
|
await sut.trash([asset.id]);
|
||||||
|
expect(await queue.next, {asset.id});
|
||||||
|
|
||||||
|
await sut.restoreTrash([asset.id]);
|
||||||
|
expect(await queue.next, isEmpty);
|
||||||
|
|
||||||
|
await queue.cancel();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ void main() {
|
|||||||
logService = await LogService.init(
|
logService = await LogService.init(
|
||||||
logRepository: logRepository,
|
logRepository: logRepository,
|
||||||
settingsRepository: settingsRepository,
|
settingsRepository: settingsRepository,
|
||||||
|
storeService: MockStoreService(),
|
||||||
shouldBuffer: false,
|
shouldBuffer: false,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import 'package:immich_mobile/domain/services/store.service.dart';
|
|||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/upload.repository.dart';
|
import 'package:immich_mobile/repositories/upload.repository.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
class _MockHttpClient extends Mock implements http.Client {}
|
class _MockHttpClient extends Mock implements http.Client {}
|
||||||
@@ -124,4 +125,21 @@ void main() {
|
|||||||
expect(result.errorMessage, 'boom');
|
expect(result.errorMessage, 'boom');
|
||||||
verify(() => client.send(any())).called(1);
|
verify(() => client.send(any())).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a transport failure is logged as the error, not in the message', () async {
|
||||||
|
final error = http.ClientException('Failed to connect to /203.0.113.9:2283');
|
||||||
|
stubSend((_) => throw error);
|
||||||
|
final level = Logger.root.level;
|
||||||
|
addTearDown(() => Logger.root.level = level);
|
||||||
|
Logger.root.level = Level.ALL;
|
||||||
|
final records = <LogRecord>[];
|
||||||
|
final subscription = sut.logger.onRecord.listen(records.add);
|
||||||
|
|
||||||
|
final result = await upload();
|
||||||
|
await subscription.cancel();
|
||||||
|
|
||||||
|
expect(records.map((r) => r.error), [error, error]);
|
||||||
|
expect(records.map((r) => r.message), everyElement(isNot(contains('203.0.113.9'))));
|
||||||
|
expect(result.error, error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:immich_mobile/data/db/logger/database.dart';
|
||||||
|
import 'package:immich_mobile/data/db/main/database.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
||||||
|
import 'package:immich_mobile/services/immich_logger.service.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('Shares the startup error and the stored logs before the log service starts', (tester) async {
|
||||||
|
final dir = Directory.systemTemp.createTempSync('immich-logs');
|
||||||
|
addTearDown(() => dir.delete(recursive: true));
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||||
|
const MethodChannel('plugins.flutter.io/path_provider'),
|
||||||
|
(_) async => dir.path,
|
||||||
|
);
|
||||||
|
late String shared;
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||||
|
const MethodChannel('dev.fluttercommunity.plus/share'),
|
||||||
|
(call) async {
|
||||||
|
shared = File(((call.arguments as Map)['paths'] as List).single as String).readAsStringSync();
|
||||||
|
return 'success';
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await tester.pumpWidget(const SizedBox());
|
||||||
|
|
||||||
|
await tester.runAsync(() async {
|
||||||
|
final connection = await openSqliteConnection(name: 'immich_logs');
|
||||||
|
final logs = DriftLogger.sqlite(connection);
|
||||||
|
await LogRepository(logs).insertAll([
|
||||||
|
LogMessage(
|
||||||
|
message: 'Failed to connect to old.example.net/203.0.113.9:443, uri=https://old.example.net/api',
|
||||||
|
level: LogLevel.warning,
|
||||||
|
createdAt: DateTime(2026, 10, 15),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
await logs.close();
|
||||||
|
await connection.close();
|
||||||
|
|
||||||
|
await ImmichLogger.shareLogs(
|
||||||
|
tester.element(find.byType(SizedBox)),
|
||||||
|
startupError: 'SqliteException(2067): UNIQUE constraint failed, parameters: 14, someone@example.com',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
shared.split('\n').first,
|
||||||
|
endsWith('| SqliteException(2067): UNIQUE constraint failed, parameters: 14, someone@example.com |'),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
shared,
|
||||||
|
contains('| Failed to connect to old.example.net/203.0.113.9:443, uri=https://old.example.net/api |'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
@@ -97,6 +99,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('trashes a merged asset and removes its device copy', (tester) async {
|
testWidgets('trashes a merged asset and removes its device copy', (tester) async {
|
||||||
|
when(() => cleanupService.deleteLocalAssets(['local'])).thenAnswer((_) async => 1);
|
||||||
final asset = owned(localId: 'local');
|
final asset = owned(localId: 'local');
|
||||||
|
|
||||||
await pumpDelete(tester, {asset});
|
await pumpDelete(tester, {asset});
|
||||||
@@ -106,6 +109,65 @@ void main() {
|
|||||||
verify(() => assetService.trash([asset.id])).called(1);
|
verify(() => assetService.trash([asset.id])).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('does not trash a merged asset when the MANAGE_MEDIA prompt is cancelled', (tester) async {
|
||||||
|
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||||
|
await StoreService.I.put(StoreKey.manageLocalMediaAndroid, true);
|
||||||
|
final asset = owned(localId: 'local');
|
||||||
|
|
||||||
|
await pumpDelete(tester, {asset});
|
||||||
|
await respondToDialog(tester, confirm: false);
|
||||||
|
|
||||||
|
verifyNever(() => cleanupService.deleteLocalAssets(any()));
|
||||||
|
verifyNever(() => assetService.trash(any()));
|
||||||
|
verifyNever(() => assetService.delete(any()));
|
||||||
|
debugDefaultTargetPlatformOverride = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('does not trash a merged asset when the Android system prompt is cancelled', (tester) async {
|
||||||
|
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||||
|
when(context.repository.permission.getAndroidSdkVersion).thenAnswer((_) async => 30);
|
||||||
|
when(() => cleanupService.deleteLocalAssets(['local'])).thenAnswer((_) async => 0);
|
||||||
|
final asset = owned(localId: 'local');
|
||||||
|
|
||||||
|
await pumpDelete(tester, {asset});
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
verify(() => cleanupService.deleteLocalAssets(['local'])).called(1);
|
||||||
|
verifyNever(() => assetService.trash(any()));
|
||||||
|
verifyNever(() => assetService.delete(any()));
|
||||||
|
debugDefaultTargetPlatformOverride = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('finishes trashing when the action context is disposed during local cleanup', (tester) async {
|
||||||
|
final asset = owned(localId: 'local');
|
||||||
|
final cleanupResult = Completer<int>();
|
||||||
|
when(() => cleanupService.deleteLocalAssets(['local'])).thenAnswer((_) => cleanupResult.future);
|
||||||
|
var showAction = true;
|
||||||
|
late StateSetter updateHost;
|
||||||
|
|
||||||
|
await tester.pumpTestWidget(
|
||||||
|
context,
|
||||||
|
StatefulBuilder(
|
||||||
|
builder: (_, setState) {
|
||||||
|
updateHost = setState;
|
||||||
|
return showAction
|
||||||
|
? const ActionIconButton(action: DeleteAction(source: .timeline))
|
||||||
|
: const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
overrides: context.selected({asset}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.byType(ImmichIconButton));
|
||||||
|
await tester.pump();
|
||||||
|
updateHost(() => showAction = false);
|
||||||
|
await tester.pump();
|
||||||
|
cleanupResult.complete(1);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
verify(() => assetService.trash([asset.id])).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('offers an undo that restores the trashed assets', (tester) async {
|
testWidgets('offers an undo that restores the trashed assets', (tester) async {
|
||||||
final asset = owned();
|
final asset = owned();
|
||||||
|
|
||||||
@@ -283,6 +345,7 @@ void main() {
|
|||||||
when(context.repository.permission.getAndroidSdkVersion).thenAnswer((_) async => 28);
|
when(context.repository.permission.getAndroidSdkVersion).thenAnswer((_) async => 28);
|
||||||
final backedUp = owned(localId: 'local');
|
final backedUp = owned(localId: 'local');
|
||||||
final localOnly = LocalAssetFactory.create();
|
final localOnly = LocalAssetFactory.create();
|
||||||
|
when(() => cleanupService.deleteLocalAssets(['local', localOnly.id])).thenAnswer((_) async => 1);
|
||||||
|
|
||||||
await pumpDelete(tester, {backedUp, localOnly});
|
await pumpDelete(tester, {backedUp, localOnly});
|
||||||
await tester.pump(const Duration(milliseconds: 300));
|
await tester.pump(const Duration(milliseconds: 300));
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# Link against minSdk (mobile/android/app/build.gradle) instead of the API 35 that native_toolchain_rust picks.
|
||||||
|
[target.aarch64-linux-android]
|
||||||
|
rustflags = ["-C", "link-arg=--target=aarch64-linux-android26"]
|
||||||
|
|
||||||
|
# Neon on 32 bit arm is the NDK's default for C, the rust target leaves it off.
|
||||||
|
[target.armv7-linux-androideabi]
|
||||||
|
rustflags = ["-C", "link-arg=--target=armv7a-linux-androideabi26", "-C", "target-feature=+neon"]
|
||||||
|
|
||||||
|
[target.x86_64-linux-android]
|
||||||
|
rustflags = ["-C", "link-arg=--target=x86_64-linux-android26"]
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
/target
|
||||||
|
/crates/native_core_ffi/include/native_core.h
|
||||||
|
/native_core/lib/src/bindings.g.dart
|
||||||
Generated
+659
@@ -0,0 +1,659 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "array-init"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byteorder"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytes"
|
||||||
|
version = "1.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cbindgen"
|
||||||
|
version = "0.29.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2ecb53484c9c167ba674026b656d8a27d7657a58e6066aa902bfb1a4aa00ae20"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"indexmap",
|
||||||
|
"log",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"syn 2.0.119",
|
||||||
|
"tempfile",
|
||||||
|
"toml",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "combine"
|
||||||
|
version = "4.6.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-deque"
|
||||||
|
version = "0.8.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-epoch",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-epoch"
|
||||||
|
version = "0.9.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-utils"
|
||||||
|
version = "0.8.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.3.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.17.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.14.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jni"
|
||||||
|
version = "0.22.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"combine",
|
||||||
|
"jni-macros",
|
||||||
|
"jni-sys",
|
||||||
|
"log",
|
||||||
|
"simd_cesu8",
|
||||||
|
"thiserror",
|
||||||
|
"walkdir",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jni-macros"
|
||||||
|
version = "0.22.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rustc_version",
|
||||||
|
"simd_cesu8",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jni-sys"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
|
||||||
|
dependencies = [
|
||||||
|
"jni-sys-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jni-sys-macros"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jxl"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8379fc83d79477900c9aed78cb8370a556d834081bebcb771c6010381c4e230"
|
||||||
|
dependencies = [
|
||||||
|
"array-init",
|
||||||
|
"byteorder",
|
||||||
|
"jxl_macros",
|
||||||
|
"jxl_simd",
|
||||||
|
"jxl_transforms",
|
||||||
|
"num-derive",
|
||||||
|
"num-traits",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jxl_macros"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eeb27c06a9957bd9c8e65d0aeea0db6a555a9e1c85597e284f8304015c410c87"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jxl_simd"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a645451b4d34b4017caf24110a14eb737e275b5d3d121bc754e67727ee65af0e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jxl_transforms"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "21c4b7f8957f5ba578b2e28cd5ebbc97dad58a3ad5ff26765da61e8dab8f1cad"
|
||||||
|
dependencies = [
|
||||||
|
"jxl_simd",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.191"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "moxcms"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "396077b717181ce3f9a6b66739b269b82d756be5ec0f76be0bc40a7c51775830"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
"pxfm",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "native_core"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"jxl",
|
||||||
|
"libc",
|
||||||
|
"moxcms",
|
||||||
|
"pic-scale",
|
||||||
|
"quick_cache",
|
||||||
|
"rayon",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "native_core_ffi"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"cbindgen",
|
||||||
|
"jni",
|
||||||
|
"libc",
|
||||||
|
"native_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-derive"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pic-scale"
|
||||||
|
version = "0.7.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6edb0ff645d35accaec252de081d55984a0d6c952cb1e5dc2b9afa7c78f07288"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
"pxfm",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.107"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pxfm"
|
||||||
|
version = "0.1.30"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quick_cache"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "403c1a912fec895cafb223201e368234842acb9220aaf08ab042ae89ba5f135c"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.47"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "6.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rayon"
|
||||||
|
version = "1.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
"rayon-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rayon-core"
|
||||||
|
version = "1.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-deque",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc_version"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||||
|
dependencies = [
|
||||||
|
"semver",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "1.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "same-file"
|
||||||
|
version = "1.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "semver"
|
||||||
|
version = "1.0.28"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.151"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simd_cesu8"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"
|
||||||
|
dependencies = [
|
||||||
|
"rustc_version",
|
||||||
|
"simdutf8",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simdutf8"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.119"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "3.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tempfile"
|
||||||
|
version = "3.27.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"getrandom",
|
||||||
|
"once_cell",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "2.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "2.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.9.12+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde_core",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_parser",
|
||||||
|
"toml_writer",
|
||||||
|
"winnow 0.7.15",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "0.7.5+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_parser"
|
||||||
|
version = "1.1.3+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
|
||||||
|
dependencies = [
|
||||||
|
"winnow 1.0.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_writer"
|
||||||
|
version = "1.1.2+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "walkdir"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||||
|
dependencies = [
|
||||||
|
"same-file",
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-util"
|
||||||
|
version = "0.1.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.7.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user