Compare commits

...

31 Commits

Author SHA1 Message Date
bo0tzz
f163cc07f9 fix: force docs-build on deployment changes 2025-10-13 17:44:17 +02:00
luzpaz
46869f664d fix: various typos (#22867)
Found via `codespell -q 3 -S "*.svg,./i18n,./docs/package-lock.json,./readme_i18n,./mobile/assets/i18n" -L afterall,devlop,finaly,inout,nd,optin,renderd,sade`
2025-10-13 15:25:23 +05:30
renovate[bot]
f2b553182a chore(deps): update github-actions (#22793)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-12 12:59:36 +02:00
renovate[bot]
8fe54a4de1 fix(deps): update dependency happy-dom to v20 [security] (#22846)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-11 13:17:25 +02:00
Min Idzelis
ce4e8fa6ba feat: (perf) remove scroll compensation (#22837) 2025-10-10 15:48:29 -04:00
Brandon Wees
efa21af6f9 fix: only cast to device if the asset is a RemoteAsset (#22805) 2025-10-10 11:13:31 -05:00
renovate[bot]
ea610760ee fix(deps): update typescript-projects (#22809)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2025-10-10 00:50:48 +02:00
renovate[bot]
a5e0d83d9f chore(deps): update base-image to v202510092146 (major) (#22818)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-09 22:40:23 +00:00
Daniel Dietzler
9793828dc7 chore: don't enforce runes (#22813) 2025-10-09 19:17:42 +00:00
bo0tzz
aed7bb53aa fix: revert terragrunt-action bump (#22812) 2025-10-09 21:11:39 +02:00
renovate[bot]
1fdbe2c6b8 chore(deps): update github-actions (major) (#22810)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-09 20:51:51 +02:00
bo0tzz
84302dc14c fix: remove postgres exclude datasource match (#22811) 2025-10-09 18:44:20 +00:00
bo0tzz
f7250f24fe chore: ignore renovate major updates for postgres image (#22764) 2025-10-09 18:34:54 +00:00
Peter Dave Hello
53680d9643 feat(docs): add zh_TW Traditional Chinese version README (#22703)
docs: add zh_TW Traditional Chinese version README
2025-10-08 15:10:58 -04:00
Tushar Harsora
b2d00405f1 feat(server): add immich.users.total metric (#21780)
* Add immich.users.total metric

* Fix tests & one lint error

* Lint

* Fix SQL Schema checks

* Fix nit

* Use workers argument in OnEvent hook and remove condition from method body
2025-10-08 13:24:11 -04:00
Sebastian Schneider
cf60f4cdcd feat(web): Add upload to stack action (#19842)
* feat(web): Add upload to stack action

* Event handling and translation

* Update asset viewer instead

* lint, improve upload return type

* Add suggestions from code review

* Resolve merge conflicts

* Apply suggestions from code review
2025-10-08 13:22:33 -04:00
Qhilm
d764a59011 docs: add Immich-Stack to community-projects (#21563)
docs: add Immich Stack community project

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2025-10-08 17:21:44 +00:00
Jorge Montejo
7ee1b977c1 feat(cli): add debug development config (#22712)
* add debug and change ts-node with tsx

* update pr changes

* update pnpm-lock

* remove ts-node from readme

* typo

* resolve conflicts

* remove tsx

* launch from dist

* add preLaunchTask

* update readme

* undo main in package.json

* remove typo

* Apply suggestion from @bwees

Co-authored-by: Brandon Wees <brandonwees@gmail.com>

* revert pnpm-lock changes

* @jrasm91 suggestions

* chore: run node with source maps

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
Co-authored-by: Brandon Wees <brandonwees@gmail.com>
2025-10-08 17:08:33 +00:00
kaziu687
9838634067 feat(web): seconds and milliseconds in timestamps (#20337)
* fix(web): seconds in timestamps

* changed date-input step to provide millisecond precision
2025-10-08 16:30:54 +00:00
Jason Rasmussen
eee793bfe4 docs: add some external library notes (#22776) 2025-10-08 12:13:41 -04:00
shenlong
b3342323de fix: persist search page scroll offset between rebuilds (#22733)
fix: persist search scroll between rebuilds

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-10-08 10:00:51 -05:00
Pascal Sommer
6f3cb4f1bb fix(web): Uniform random distribution during shuffle (#19902)
feat: better random distribution
2025-10-08 10:19:33 -04:00
Saschl
54ed78d0bf fix: brief flashing when swiping from video (#22187) 2025-10-08 09:31:15 -04:00
shenlong
265ed0b38f fix: skip local only assets in move to lock action (#22728)
* fix:prefer trashing to deletions

* skip local only assets in move to lock action

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-10-08 04:21:34 -05:00
shenlong
63c2f4415b chore: use hosted isar flutter libs (#22757)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-10-08 04:19:46 -05:00
renovate[bot]
a7cfd7f183 fix(deps): update dependency connectivity_plus to v7 (#22723)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-10-07 21:21:43 -05:00
renovate[bot]
ee4c45d5d3 chore(deps): update dependency nodemailer to v7.0.7 [security] (#22740)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-07 21:24:33 +02:00
renovate[bot]
24334aa3df chore(deps): update dependency @types/node to ^22.18.8 (#22719)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-07 14:42:00 -04:00
Brandon Wees
882baecf21 fix: bottom sheet blank with local assets that have remote counterparts (#22743) 2025-10-07 18:04:23 +00:00
shenlong
f16327d0ab chore: use isar immich fork (#22738)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-10-07 13:13:35 -04:00
Mert
8353db6a50 chore(deps): cache pnpm dependencies in prod build (#22555)
* cache pnpm dependencies

use different ids to be safe

unnecessary lines

* use buildcache folder
2025-10-07 13:10:54 -04:00
113 changed files with 3392 additions and 2799 deletions

View File

@@ -55,7 +55,7 @@ jobs:
runs-on: mich
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: ${{ inputs.ref || github.sha }}
persist-credentials: false
@@ -66,7 +66,7 @@ jobs:
working-directory: ./mobile
run: printf "%s" $KEY_JKS | base64 -d > android/key.jks
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
- uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
distribution: 'zulu'
java-version: '17'

View File

@@ -19,7 +19,7 @@ jobs:
actions: write
steps:
- name: Check out code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false

View File

@@ -29,7 +29,7 @@ jobs:
working-directory: ./cli
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
@@ -37,7 +37,7 @@ jobs:
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version-file: './cli/.nvmrc'
registry-url: 'https://registry.npmjs.org'
@@ -65,7 +65,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false

View File

@@ -44,13 +44,13 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
uses: github/codeql-action/init@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -63,7 +63,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
uses: github/codeql-action/autobuild@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6
# 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
@@ -76,6 +76,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
uses: github/codeql-action/analyze@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6
with:
category: '/language:${{matrix.language}}'

View File

@@ -124,7 +124,7 @@ jobs:
tag-suffix: '-rocm'
platforms: linux/amd64
runner-mapping: '{"linux/amd64": "mich"}'
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@129aeda75a450666ce96e8bc8126652e717917a7 # multi-runner-build-workflow-0.1.1
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@946acac326940f8badf09ccf591d9cb345d6a689 # multi-runner-build-workflow-v0.2.1
permissions:
contents: read
actions: read
@@ -147,7 +147,7 @@ jobs:
name: Build and Push Server
needs: pre-job
if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }}
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@129aeda75a450666ce96e8bc8126652e717917a7 # multi-runner-build-workflow-0.1.1
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@946acac326940f8badf09ccf591d9cb345d6a689 # multi-runner-build-workflow-v0.2.1
permissions:
contents: read
actions: read

View File

@@ -31,6 +31,8 @@ jobs:
- 'open-api/immich-openapi-specs.json'
force-filters: |
- '.github/workflows/docs-build.yml'
- '.github/workflows/docs-deploy.yml'
- 'deployment/**'
force-events: 'release'
force-branches: 'main'
@@ -47,7 +49,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
@@ -55,7 +57,7 @@ jobs:
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version-file: './docs/.nvmrc'
cache: 'pnpm'

View File

@@ -20,7 +20,7 @@ jobs:
run: echo 'The triggering workflow did not succeed' && exit 1
- name: Get artifact
id: get-artifact
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
@@ -38,7 +38,7 @@ jobs:
return { found: true, id: matchArtifact.id };
- name: Determine deploy parameters
id: parameters
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
with:
@@ -108,13 +108,13 @@ jobs:
if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }}
steps:
- name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Load parameters
id: parameters
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
PARAM_JSON: ${{ needs.checks.outputs.parameters }}
with:
@@ -125,7 +125,7 @@ jobs:
core.setOutput("shouldDeploy", parameters.shouldDeploy);
- name: Download artifact
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
ARTIFACT_JSON: ${{ needs.checks.outputs.artifact }}
with:

View File

@@ -14,7 +14,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false

View File

@@ -22,7 +22,7 @@ jobs:
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: 'Checkout'
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: ${{ github.event.pull_request.head.ref }}
token: ${{ steps.generate-token.outputs.token }}
@@ -32,7 +32,7 @@ jobs:
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version-file: './server/.nvmrc'
cache: 'pnpm'
@@ -48,7 +48,7 @@ jobs:
message: 'chore: fix formatting'
- name: Remove label
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
if: always()
with:
script: |

View File

@@ -11,4 +11,4 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1

View File

@@ -55,20 +55,20 @@ jobs:
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
token: ${{ steps.generate-token.outputs.token }}
persist-credentials: true
ref: main
- name: Install uv
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version-file: './server/.nvmrc'
cache: 'pnpm'
@@ -117,18 +117,18 @@ jobs:
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
token: ${{ steps.generate-token.outputs.token }}
persist-credentials: false
- name: Download APK
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: release-apk-signed
- name: Create draft release
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
uses: softprops/action-gh-release@aec2ec56f94eb8180ceec724245f64ef008b89f5 # v2.4.0
with:
draft: true
tag_name: ${{ env.IMMICH_VERSION }}

View File

@@ -24,7 +24,7 @@ jobs:
permissions:
pull-requests: write
steps:
- uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
github.rest.issues.removeLabel({

View File

@@ -16,7 +16,7 @@ jobs:
run:
working-directory: ./open-api/typescript-sdk
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
@@ -24,7 +24,7 @@ jobs:
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version-file: './open-api/typescript-sdk/.nvmrc'
registry-url: 'https://registry.npmjs.org'

View File

@@ -42,7 +42,7 @@ jobs:
working-directory: ./mobile
steps:
- name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false

View File

@@ -56,13 +56,13 @@ jobs:
working-directory: ./server
steps:
- name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version-file: './server/.nvmrc'
cache: 'pnpm'
@@ -93,13 +93,13 @@ jobs:
working-directory: ./cli
steps:
- name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version-file: './cli/.nvmrc'
cache: 'pnpm'
@@ -133,13 +133,13 @@ jobs:
working-directory: ./cli
steps:
- name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version-file: './cli/.nvmrc'
cache: 'pnpm'
@@ -168,13 +168,13 @@ jobs:
working-directory: ./web
steps:
- name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version-file: './web/.nvmrc'
cache: 'pnpm'
@@ -205,13 +205,13 @@ jobs:
working-directory: ./web
steps:
- name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version-file: './web/.nvmrc'
cache: 'pnpm'
@@ -236,13 +236,13 @@ jobs:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version-file: './web/.nvmrc'
cache: 'pnpm'
@@ -277,13 +277,13 @@ jobs:
working-directory: ./e2e
steps:
- name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version-file: './e2e/.nvmrc'
cache: 'pnpm'
@@ -316,13 +316,13 @@ jobs:
working-directory: ./server
steps:
- name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version-file: './server/.nvmrc'
cache: 'pnpm'
@@ -347,14 +347,14 @@ jobs:
runner: [ubuntu-latest, ubuntu-24.04-arm]
steps:
- name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
submodules: 'recursive'
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version-file: './e2e/.nvmrc'
cache: 'pnpm'
@@ -395,14 +395,14 @@ jobs:
runner: [ubuntu-latest, ubuntu-24.04-arm]
steps:
- name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
submodules: 'recursive'
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version-file: './e2e/.nvmrc'
cache: 'pnpm'
@@ -441,7 +441,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Setup Flutter SDK
@@ -466,12 +466,12 @@ jobs:
run:
working-directory: ./machine-learning
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
# TODO: add caching when supported (https://github.com/actions/setup-python/pull/818)
# with:
# python-version: 3.11
@@ -503,13 +503,13 @@ jobs:
working-directory: ./.github
steps:
- name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version-file: './.github/.nvmrc'
cache: 'pnpm'
@@ -525,7 +525,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Run ShellCheck
@@ -540,13 +540,13 @@ jobs:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version-file: './server/.nvmrc'
cache: 'pnpm'
@@ -581,7 +581,7 @@ jobs:
contents: read
services:
postgres:
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3@sha256:da52bbead5d818adaa8077c8dcdaad0aaf93038c31ad8348b51f9f0ec1310a4d
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3@sha256:dbf18b3ffea4a81434c65b71e20d27203baf903a0275f4341e4c16dfd901fd67
env:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
@@ -595,13 +595,13 @@ jobs:
working-directory: ./server
steps:
- name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version-file: './server/.nvmrc'
cache: 'pnpm'

14
.vscode/launch.json vendored
View File

@@ -18,6 +18,20 @@
"name": "Immich Workers",
"remoteRoot": "/usr/src/app/server",
"localRoot": "${workspaceFolder}/server"
},
{
"type": "node",
"request": "launch",
"name": "Immich CLI",
"program": "${workspaceFolder}/cli/dist/index.js",
"args": ["upload", "--help"],
"runtimeArgs": ["--enable-source-maps"],
"console": "integratedTerminal",
"resolveSourceMapLocations": ["${workspaceFolder}/cli/dist/**/*.js.map"],
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/cli/dist/**/*.js"],
"skipFiles": ["<node_internals>/**"],
"preLaunchTask": "Build Immich CLI"
}
]
}

5
.vscode/tasks.json vendored
View File

@@ -70,6 +70,11 @@
"runOn": "folderOpen"
},
"problemMatcher": []
},
{
"label": "Build Immich CLI",
"type": "shell",
"command": "pnpm --filter cli build:dev"
}
]
}

View File

@@ -28,7 +28,8 @@
<a href="readme_i18n/README_de_DE.md">Deutsch</a>
<a href="readme_i18n/README_nl_NL.md">Nederlands</a>
<a href="readme_i18n/README_tr_TR.md">Türkçe</a>
<a href="readme_i18n/README_zh_CN.md">中文</a>
<a href="readme_i18n/README_zh_CN.md">简体中文</a>
<a href="readme_i18n/README_zh_TW.md">正體中文</a>
<a href="readme_i18n/README_uk_UA.md">Українська</a>
<a href="readme_i18n/README_ru_RU.md">Русский</a>
<a href="readme_i18n/README_pt_BR.md">Português Brasileiro</a>

View File

@@ -13,15 +13,23 @@ Then, to build the open-api client run the following in the open-api folder:
$ ./bin/generate-open-api.sh
To run the Immich CLI from source, run the following in the cli folder:
## Run from build
Go to the cli folder and build it:
$ pnpm install
$ pnpm run build
$ ts-node .
$ node dist/index.js
You'll need ts-node, the easiest way to install it is to use pnpm:
## Run and Debug from source (VSCode)
$ pnpm i -g ts-node
With VScode you can run and debug the Immich CLI. Go to the launch.json file, find the Immich CLI config and change this with the command you need to debug
`"args": ["upload", "--help"],`
replace that for the command of your choice.
## Install from build
You can also build and install the CLI using

View File

@@ -20,7 +20,7 @@
"@types/lodash-es": "^4.17.12",
"@types/micromatch": "^4.0.9",
"@types/mock-fs": "^4.13.1",
"@types/node": "^22.18.1",
"@types/node": "^22.18.8",
"@vitest/coverage-v8": "^3.0.0",
"byte-size": "^9.0.0",
"cli-progress": "^3.12.0",
@@ -43,6 +43,7 @@
},
"scripts": {
"build": "vite build",
"build:dev": "vite build --sourcemap true",
"lint": "eslint \"src/**/*.ts\" --max-warnings 0",
"lint:fix": "npm run lint -- --fix",
"prepack": "npm run build",

View File

@@ -22,7 +22,7 @@ For organizations seeking to resell Immich, we have established the following gu
- Do not misrepresent your reseller site or services as being officially affiliated with or endorsed by Immich or our development team.
- For small resellers who wish to contribute financially to Immich's development, we recommend directing your customers to purchase licenses directy from us rather than attempting to broker revenue-sharing arrangements. We ask that you refrain from misrepresenting reseller activities as directly supporting our development work.
- For small resellers who wish to contribute financially to Immich's development, we recommend directing your customers to purchase licenses directly from us rather than attempting to broker revenue-sharing arrangements. We ask that you refrain from misrepresenting reseller activities as directly supporting our development work.
When in doubt or if you have an edge case scenario, we encourage you to contact us directly via email to discuss the use of our trademark. We can provide clear guidance on what is acceptable and what is not. You can reach out at: questions@immich.app

View File

@@ -4,7 +4,7 @@ Immich supports the Google's Cast protocol so that photos and videos can be cast
## Enable Google Cast Support
Google Cast support is disabled by default. The web UI uses Google-provided scripts and must retreive them from Google servers when the page loads. This is a privacy concern for some and is thus opt-in.
Google Cast support is disabled by default. The web UI uses Google-provided scripts and must retrieve them from Google servers when the page loads. This is a privacy concern for some and is thus opt-in.
You can enable Google Cast support through `Account Settings > Features > Cast > Google Cast`

View File

@@ -1,5 +1,9 @@
# External Libraries
:::info
Currently an external library can only belong to a single user which is selected when the library is initially created.
:::
External libraries track assets stored in the filesystem outside of Immich. When the external library is scanned, Immich will load videos and photos from disk and create the corresponding assets. These assets will then be shown in the main timeline, and they will look and behave like any other asset, including viewing on the map, adding to albums, etc. Later, if a file is modified outside of Immich, you need to scan the library for the changes to show up.
If an external asset is deleted from disk, Immich will move it to trash on rescan. To restore the asset, you need to restore the original file. After 30 days the file will be removed from trash, and any changes to metadata within Immich will be lost.

View File

@@ -21,6 +21,10 @@ Restart Immich by running `docker compose up -d`.
# Create the library
:::info
External library management requires administrator access and the steps below assume you are using an admin account.
:::
In the Immich web UI:
- click the **Administration** link in the upper right corner.

View File

@@ -17,9 +17,9 @@
"write-heading-ids": "docusaurus write-heading-ids"
},
"dependencies": {
"@docusaurus/core": "~3.8.0",
"@docusaurus/preset-classic": "~3.8.0",
"@docusaurus/theme-common": "~3.8.0",
"@docusaurus/core": "~3.9.0",
"@docusaurus/preset-classic": "~3.9.0",
"@docusaurus/theme-common": "~3.9.0",
"@mdi/js": "^7.3.67",
"@mdi/react": "^1.6.1",
"@mdx-js/react": "^3.0.0",
@@ -35,7 +35,7 @@
"url": "^0.11.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "~3.8.0",
"@docusaurus/module-type-aliases": "~3.9.0",
"@docusaurus/tsconfig": "^3.7.0",
"@docusaurus/types": "^3.7.0",
"prettier": "^3.2.4",

View File

@@ -115,6 +115,11 @@ const projects: CommunityProjectProps[] = [
description: 'Auto-stack photos with identical filenames and differing extensions (i.e. JPG+RAW)',
url: 'https://github.com/sid3windr/immich-stack',
},
{
title: 'Immich Stack',
description: 'Automatically groups similar photos into stacks within the Immich photo management system.',
url: 'https://github.com/Majorfi/immich-stack/',
},
];
function CommunityProject({ title, description, url }: CommunityProjectProps): JSX.Element {

View File

@@ -25,7 +25,7 @@
"@playwright/test": "^1.44.1",
"@socket.io/component-emitter": "^3.1.2",
"@types/luxon": "^3.4.2",
"@types/node": "^22.18.1",
"@types/node": "^22.18.8",
"@types/oidc-provider": "^9.0.0",
"@types/pg": "^8.15.1",
"@types/pngjs": "^6.0.4",
@@ -43,7 +43,7 @@
"pngjs": "^7.0.0",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^4.0.0",
"sharp": "^0.34.3",
"sharp": "^0.34.4",
"socket.io-client": "^4.7.4",
"supertest": "^7.0.0",
"typescript": "^5.3.3",

View File

@@ -33,6 +33,7 @@
"add_to_albums": "Add to albums",
"add_to_albums_count": "Add to albums ({count})",
"add_to_shared_album": "Add to shared album",
"add_upload_to_stack": "Add upload to stack",
"add_url": "Add URL",
"added_to_archive": "Added to archive",
"added_to_favorites": "Added to favorites",

View File

@@ -1,7 +1,7 @@
[tools]
node = "22.20.0"
flutter = "3.35.5"
pnpm = "10.15.1"
pnpm = "10.18.0"
[tools."github:CQLabs/homebrew-dcm"]
version = "1.30.0"

View File

@@ -64,7 +64,7 @@ PODS:
- Flutter
- integration_test (0.0.1):
- Flutter
- isar_flutter_libs (1.0.0):
- isar_community_flutter_libs (1.0.0):
- Flutter
- local_auth_darwin (0.0.1):
- Flutter
@@ -149,7 +149,7 @@ DEPENDENCIES:
- home_widget (from `.symlinks/plugins/home_widget/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`)
- isar_community_flutter_libs (from `.symlinks/plugins/isar_community_flutter_libs/ios`)
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
- maplibre_gl (from `.symlinks/plugins/maplibre_gl/ios`)
- native_video_player (from `.symlinks/plugins/native_video_player/ios`)
@@ -210,8 +210,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/image_picker_ios/ios"
integration_test:
:path: ".symlinks/plugins/integration_test/ios"
isar_flutter_libs:
:path: ".symlinks/plugins/isar_flutter_libs/ios"
isar_community_flutter_libs:
:path: ".symlinks/plugins/isar_community_flutter_libs/ios"
local_auth_darwin:
:path: ".symlinks/plugins/local_auth_darwin/darwin"
maplibre_gl:
@@ -264,7 +264,7 @@ SPEC CHECKSUMS:
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
isar_flutter_libs: bc909e72c3d756c2759f14c8776c13b5b0556e26
isar_community_flutter_libs: bede843185a61a05ff364a05c9b23209523f7e0d
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
MapLibre: 69e572367f4ef6287e18246cfafc39c80cdcabcd
maplibre_gl: 3c924e44725147b03dda33430ad216005b40555f

View File

@@ -114,7 +114,7 @@ struct ImmichMemoryProvider: TimelineProvider {
}
}
// If we didnt add any memory images (some failure occured or no images in memory),
// If we didn't add any memory images (some failure occurred or no images in memory),
// default to 12 hours of random photos
if entries.count == 0 {
// this must be a do/catch since we need to

View File

@@ -3,27 +3,30 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
class SearchResult {
final List<BaseAsset> assets;
final double scrollOffset;
final int? nextPage;
const SearchResult({required this.assets, this.nextPage});
const SearchResult({required this.assets, this.scrollOffset = 0.0, this.nextPage});
int get totalAssets => assets.length;
SearchResult copyWith({List<BaseAsset>? assets, int? nextPage}) {
return SearchResult(assets: assets ?? this.assets, nextPage: nextPage ?? this.nextPage);
SearchResult copyWith({List<BaseAsset>? assets, int? nextPage, double? scrollOffset}) {
return SearchResult(
assets: assets ?? this.assets,
nextPage: nextPage ?? this.nextPage,
scrollOffset: scrollOffset ?? this.scrollOffset,
);
}
@override
String toString() => 'SearchResult(assets: $assets, nextPage: $nextPage)';
String toString() => 'SearchResult(assets: ${assets.length}, nextPage: $nextPage, scrollOffset: $scrollOffset)';
@override
bool operator ==(covariant SearchResult other) {
if (identical(this, other)) return true;
final listEquals = const DeepCollectionEquality().equals;
return listEquals(other.assets, assets) && other.nextPage == nextPage;
return listEquals(other.assets, assets) && other.nextPage == nextPage && other.scrollOffset == scrollOffset;
}
@override
int get hashCode => assets.hashCode ^ nextPage.hashCode;
int get hashCode => assets.hashCode ^ nextPage.hashCode ^ scrollOffset.hashCode;
}

View File

@@ -86,7 +86,7 @@ class StoreService {
_cache.remove(key.id);
}
/// Clears all values from thw store (cache and DB)
/// Clears all values from the store (cache and DB)
Future<void> clear() async {
await _storeRepository.deleteAll();
_cache.clear();

View File

@@ -203,7 +203,7 @@ class TimelineService {
Future<void> dispose() async {
await _bucketSubscription?.cancel();
_bucketSubscription = null;
_buffer.clear();
_buffer = [];
_bufferOffset = 0;
}
}

View File

@@ -132,7 +132,7 @@ const AlbumSchema = CollectionSchema(
getId: _albumGetId,
getLinks: _albumGetLinks,
attach: _albumAttach,
version: '3.1.8',
version: '3.3.0-dev.3',
);
int _albumEstimateSize(

View File

@@ -47,7 +47,7 @@ const AndroidDeviceAssetSchema = CollectionSchema(
getId: _androidDeviceAssetGetId,
getLinks: _androidDeviceAssetGetLinks,
attach: _androidDeviceAssetAttach,
version: '3.1.8',
version: '3.3.0-dev.3',
);
int _androidDeviceAssetEstimateSize(

View File

@@ -168,7 +168,7 @@ const AssetSchema = CollectionSchema(
getId: _assetGetId,
getLinks: _assetGetLinks,
attach: _assetAttach,
version: '3.1.8',
version: '3.3.0-dev.3',
);
int _assetEstimateSize(

View File

@@ -43,7 +43,7 @@ const BackupAlbumSchema = CollectionSchema(
getId: _backupAlbumGetId,
getLinks: _backupAlbumGetLinks,
attach: _backupAlbumAttach,
version: '3.1.8',
version: '3.3.0-dev.3',
);
int _backupAlbumEstimateSize(

View File

@@ -32,7 +32,7 @@ const DuplicatedAssetSchema = CollectionSchema(
getId: _duplicatedAssetGetId,
getLinks: _duplicatedAssetGetLinks,
attach: _duplicatedAssetAttach,
version: '3.1.8',
version: '3.3.0-dev.3',
);
int _duplicatedAssetEstimateSize(

View File

@@ -52,7 +52,7 @@ const ETagSchema = CollectionSchema(
getId: _eTagGetId,
getLinks: _eTagGetLinks,
attach: _eTagAttach,
version: '3.1.8',
version: '3.3.0-dev.3',
);
int _eTagEstimateSize(

View File

@@ -60,7 +60,7 @@ const IOSDeviceAssetSchema = CollectionSchema(
getId: _iOSDeviceAssetGetId,
getLinks: _iOSDeviceAssetGetLinks,
attach: _iOSDeviceAssetAttach,
version: '3.1.8',
version: '3.3.0-dev.3',
);
int _iOSDeviceAssetEstimateSize(

View File

@@ -65,7 +65,7 @@ const DeviceAssetEntitySchema = CollectionSchema(
getId: _deviceAssetEntityGetId,
getLinks: _deviceAssetEntityGetLinks,
attach: _deviceAssetEntityAttach,
version: '3.1.8',
version: '3.3.0-dev.3',
);
int _deviceAssetEntityEstimateSize(

View File

@@ -68,7 +68,7 @@ const ExifInfoSchema = CollectionSchema(
getId: _exifInfoGetId,
getLinks: _exifInfoGetLinks,
attach: _exifInfoAttach,
version: '3.1.8',
version: '3.3.0-dev.3',
);
int _exifInfoEstimateSize(

View File

@@ -37,7 +37,7 @@ const StoreValueSchema = CollectionSchema(
getId: _storeValueGetId,
getLinks: _storeValueGetLinks,
attach: _storeValueAttach,
version: '3.1.8',
version: '3.3.0-dev.3',
);
int _storeValueEstimateSize(

View File

@@ -95,7 +95,7 @@ const UserSchema = CollectionSchema(
getId: _userGetId,
getLinks: _userGetLinks,
attach: _userAttach,
version: '3.1.8',
version: '3.3.0-dev.3',
);
int _userEstimateSize(

View File

@@ -599,9 +599,9 @@ class _SearchResultGrid extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final searchResult = ref.watch(paginatedSearchProvider);
final assets = ref.watch(paginatedSearchProvider.select((s) => s.assets));
if (searchResult.totalAssets == 0) {
if (assets.isEmpty) {
return const _SearchEmptyContent();
}
@@ -615,6 +615,7 @@ class _SearchResultGrid extends ConsumerWidget {
if (metrics.pixels >= metrics.maxScrollExtent && isVerticalScroll && !isBottomSheetNotification) {
onScrollEnd();
ref.read(paginatedSearchProvider.notifier).setScrollOffset(metrics.maxScrollExtent);
}
return true;
@@ -623,17 +624,18 @@ class _SearchResultGrid extends ConsumerWidget {
child: ProviderScope(
overrides: [
timelineServiceProvider.overrideWith((ref) {
final timelineService = ref.watch(timelineFactoryProvider).fromAssets(searchResult.assets);
final timelineService = ref.watch(timelineFactoryProvider).fromAssets(assets);
ref.onDispose(timelineService.dispose);
return timelineService;
}),
],
child: Timeline(
key: ValueKey(searchResult.totalAssets),
key: ValueKey(assets.length),
groupBy: GroupAssetsBy.none,
appBar: null,
bottomSheet: const GeneralBottomSheet(minChildSize: 0.20),
snapToMonth: false,
initialScrollOffset: ref.read(paginatedSearchProvider.select((s) => s.scrollOffset)),
),
),
),

View File

@@ -24,12 +24,20 @@ class PaginatedSearchNotifier extends StateNotifier<SearchResult> {
return false;
}
state = SearchResult(assets: [...state.assets, ...result.assets], nextPage: result.nextPage);
state = SearchResult(
assets: [...state.assets, ...result.assets],
nextPage: result.nextPage,
scrollOffset: state.scrollOffset,
);
return true;
}
void setScrollOffset(double offset) {
state = state.copyWith(scrollOffset: offset);
}
clear() {
state = const SearchResult(assets: [], nextPage: 1);
state = const SearchResult(assets: [], nextPage: 1, scrollOffset: 0.0);
}
}

View File

@@ -221,10 +221,8 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
context.scaffoldMessenger.hideCurrentSnackBar();
// send image to casting if the server has it
if (asset.hasRemote) {
final remoteAsset = asset as RemoteAsset;
ref.read(castProvider.notifier).loadMedia(remoteAsset, false);
if (asset is RemoteAsset) {
ref.read(castProvider.notifier).loadMedia(asset, false);
} else {
// casting cannot show local assets
context.scaffoldMessenger.clearSnackBars();

View File

@@ -51,7 +51,7 @@ class AssetDetailBottomSheet extends ConsumerWidget {
isArchived: isArchived,
isTrashEnabled: isTrashEnable,
isInLockedView: isInLockedView,
isStacked: asset.hasRemote && (asset as RemoteAsset).stackId != null,
isStacked: asset is RemoteAsset && asset.stackId != null,
currentAlbum: currentAlbum,
advancedTroubleshooting: advancedTroubleshooting,
source: ActionSource.viewer,

View File

@@ -322,6 +322,9 @@ class NativeVideoViewer extends HookConsumerWidget {
removeListeners(playerController);
}
if (value != null) {
isVisible.value = _isCurrentAsset(value, asset);
}
final curAsset = currentAsset.value;
if (curAsset == asset) {
return;

View File

@@ -40,6 +40,7 @@ class Timeline extends StatelessWidget {
this.groupBy,
this.withScrubber = true,
this.snapToMonth = true,
this.initialScrollOffset,
});
final Widget? topSliverWidget;
@@ -51,6 +52,7 @@ class Timeline extends StatelessWidget {
final GroupAssetsBy? groupBy;
final bool withScrubber;
final bool snapToMonth;
final double? initialScrollOffset;
@override
Widget build(BuildContext context) {
@@ -78,6 +80,7 @@ class Timeline extends StatelessWidget {
bottomSheet: bottomSheet,
withScrubber: withScrubber,
snapToMonth: snapToMonth,
initialScrollOffset: initialScrollOffset,
),
),
),
@@ -93,6 +96,7 @@ class _SliverTimeline extends ConsumerStatefulWidget {
this.bottomSheet,
this.withScrubber = true,
this.snapToMonth = true,
this.initialScrollOffset,
});
final Widget? topSliverWidget;
@@ -101,6 +105,7 @@ class _SliverTimeline extends ConsumerStatefulWidget {
final Widget? bottomSheet;
final bool withScrubber;
final bool snapToMonth;
final double? initialScrollOffset;
@override
ConsumerState createState() => _SliverTimelineState();
@@ -124,7 +129,10 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
@override
void initState() {
super.initState();
_scrollController = ScrollController(onAttach: _restoreScalePosition);
_scrollController = ScrollController(
initialScrollOffset: widget.initialScrollOffset ?? 0.0,
onAttach: _restoreScalePosition,
);
_eventSubscription = EventStream.shared.listen(_onEvent);
final currentTilesPerRow = ref.read(settingsProvider).get(Setting.tilesPerRow);

View File

@@ -77,11 +77,14 @@ class ActionNotifier extends Notifier<void> {
return _getAssets(source).whereType<RemoteAsset>().toIds().toList(growable: false);
}
List<String> _getLocalIdsForSource(ActionSource source) {
List<String> _getLocalIdsForSource(ActionSource source, {bool ignoreLocalOnly = false}) {
final Set<BaseAsset> assets = _getAssets(source);
final List<String> localIds = [];
for (final asset in assets) {
if (ignoreLocalOnly && asset.storage != AssetState.merged) {
continue;
}
if (asset is LocalAsset) {
localIds.add(asset.id);
} else if (asset is RemoteAsset && asset.localId != null) {
@@ -189,7 +192,7 @@ class ActionNotifier extends Notifier<void> {
Future<ActionResult> moveToLockFolder(ActionSource source) async {
final ids = _getOwnedRemoteIdsForSource(source);
final localIds = _getLocalIdsForSource(source);
final localIds = _getLocalIdsForSource(source, ignoreLocalOnly: true);
try {
await _service.moveToLockFolder(ids, localIds);
return ActionResult(count: ids.length, success: true);

View File

@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
@@ -25,7 +26,28 @@ class AssetMediaRepository {
const AssetMediaRepository(this._assetApiRepository);
Future<List<String>> deleteAll(List<String> ids) => PhotoManager.editor.deleteWithIds(ids);
Future<bool> _androidSupportsTrash() async {
if (Platform.isAndroid) {
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
int sdkVersion = androidInfo.version.sdkInt;
return sdkVersion >= 31;
}
return false;
}
Future<List<String>> deleteAll(List<String> ids) async {
if (CurrentPlatform.isAndroid) {
if (await _androidSupportsTrash()) {
return PhotoManager.editor.android.moveToTrash(
ids.map((e) => AssetEntity(id: e, width: 1, height: 1, typeInt: 0)).toList(),
);
} else {
return PhotoManager.editor.deleteWithIds(ids);
}
}
return PhotoManager.editor.deleteWithIds(ids);
}
Future<asset_entity.Asset?> get(String id) async {
final entity = await AssetEntity.fromId(id);

View File

@@ -317,10 +317,10 @@ packages:
dependency: "direct main"
description:
name: connectivity_plus
sha256: "04bf81bb0b77de31557b58d052b24b3eee33f09a6e7a8c68a3e247c7df19ec27"
sha256: b5e72753cf63becce2c61fd04dfe0f1c430cc5278b53a1342dc5ad839eab29ec
url: "https://pub.dev"
source: hosted
version: "6.1.3"
version: "6.1.5"
connectivity_plus_platform_interface:
dependency: transitive
description:
@@ -1022,25 +1022,34 @@ packages:
isar:
dependency: "direct main"
description:
name: isar
sha256: e17a9555bc7f22ff26568b8c64d019b4ffa2dc6bd4cb1c8d9b269aefd32e53ad
url: "https://pub.isar-community.dev"
source: hosted
path: "packages/isar"
ref: bb1dca40fe87a001122e5d43bc6254718cb49f3a
resolved-ref: bb1dca40fe87a001122e5d43bc6254718cb49f3a
url: "https://github.com/immich-app/isar"
source: git
version: "3.1.8"
isar_flutter_libs:
isar_community:
dependency: transitive
description:
name: isar_community
sha256: "28f59e54636c45ba0bb1b3b7f2656f1c50325f740cea6efcd101900be3fba546"
url: "https://pub.dev"
source: hosted
version: "3.3.0-dev.3"
isar_community_flutter_libs:
dependency: "direct main"
description:
name: isar_flutter_libs
sha256: "78710781e658ce4bff59b3f38c5b2735e899e627f4e926e1221934e77b95231a"
url: "https://pub.isar-community.dev"
name: isar_community_flutter_libs
sha256: c2934fe755bb3181cb67602fd5df0d080b3d3eb52799f98623aa4fc5acbea010
url: "https://pub.dev"
source: hosted
version: "3.1.8"
version: "3.3.0-dev.3"
isar_generator:
dependency: "direct dev"
description:
path: "packages/isar_generator"
ref: v3
resolved-ref: ad574f60ed6f39d2995cd16fc7dc3de9a646ef30
ref: bb1dca40fe87a001122e5d43bc6254718cb49f3a
resolved-ref: bb1dca40fe87a001122e5d43bc6254718cb49f3a
url: "https://github.com/immich-app/isar"
source: git
version: "3.1.8"

View File

@@ -8,8 +8,6 @@ environment:
sdk: '>=3.8.0 <4.0.0'
flutter: 3.35.4
isar_version: &isar_version 3.1.8
dependencies:
flutter:
sdk: flutter
@@ -81,11 +79,11 @@ dependencies:
openapi:
path: openapi
isar:
version: *isar_version
hosted: https://pub.isar-community.dev/
isar_flutter_libs: # contains Isar Core
version: *isar_version
hosted: https://pub.isar-community.dev/
git:
url: https://github.com/immich-app/isar
ref: 'bb1dca40fe87a001122e5d43bc6254718cb49f3a'
path: packages/isar/
isar_community_flutter_libs: 3.3.0-dev.3
# DB
drift: ^2.23.1
drift_flutter: ^0.2.4
@@ -101,7 +99,7 @@ dev_dependencies:
isar_generator:
git:
url: https://github.com/immich-app/isar
ref: v3
ref: 'bb1dca40fe87a001122e5d43bc6254718cb49f3a'
path: packages/isar_generator/
integration_test:
sdk: flutter

View File

@@ -19,7 +19,7 @@
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^22.18.1",
"@types/node": "^22.18.8",
"typescript": "^5.3.3"
},
"repository": {

View File

@@ -3,7 +3,7 @@
"version": "0.0.1",
"description": "Monorepo for Immich",
"private": true,
"packageManager": "pnpm@10.15.1+sha512.34e538c329b5553014ca8e8f4535997f96180a1d0f614339357449935350d924e22f8614682191264ec33d1462ac21561aff97f6bb18065351c162c7e8f6de67",
"packageManager": "pnpm@10.18.0+sha512.e804f889f1cecc40d572db084eec3e4881739f8dec69c0ff10d2d1beff9a4e309383ba27b5b750059d7f4c149535b6cd0d2cb1ed3aeb739239a4284a68f40cfa",
"engines": {
"pnpm": ">=10.0.0"
}

5171
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,7 @@ onlyBuiltDependencies:
- '@tailwindcss/oxide'
overrides:
canvas: 2.11.2
sharp: ^0.34.3
sharp: ^0.34.4
packageExtensions:
nestjs-kysely:
dependencies:

View File

@@ -28,7 +28,8 @@
<a href="README_de_DE.md">Deutsch</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_zh_CN.md">中文</a>
<a href="README_zh_CN.md">简体中文</a>
<a href="README_zh_TW.md">正體中文</a>
<a href="README_uk_UA.md">Українська</a>
<a href="README_ru_RU.md">Русский</a>
<a href="README_pt_BR.md">Português Brasileiro</a>

View File

@@ -27,7 +27,8 @@
<a href="README_de_DE.md">Deutsch</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_zh_CN.md">中文</a>
<a href="README_zh_CN.md">简体中文</a>
<a href="README_zh_TW.md">正體中文</a>
<a href="README_uk_UA.md">Українська</a>
<a href="README_ru_RU.md">Русский</a>
<a href="README_pt_BR.md">Português Brasileiro</a>

View File

@@ -27,7 +27,8 @@
<a href="README_ko_KR.md">한국어</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_zh_CN.md">中文</a>
<a href="README_zh_CN.md">简体中文</a>
<a href="README_zh_TW.md">正體中文</a>
<a href="README_uk_UA.md">Українська</a>
<a href="README_ru_RU.md">Русский</a>
<a href="README_pt_BR.md">Português Brasileiro</a>

View File

@@ -27,7 +27,8 @@
<a href="README_de_DE.md">Deutsch</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_zh_CN.md">中文</a>
<a href="README_zh_CN.md">简体中文</a>
<a href="README_zh_TW.md">正體中文</a>
<a href="README_uk_UA.md">Українська</a>
<a href="README_ru_RU.md">Русский</a>
<a href="README_pt_BR.md">Português Brasileiro</a>

View File

@@ -27,7 +27,8 @@
<a href="README_de_DE.md">Deutsch</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_zh_CN.md">中文</a>
<a href="README_zh_CN.md">简体中文</a>
<a href="README_zh_TW.md">正體中文</a>
<a href="README_uk_UA.md">Українська</a>
<a href="README_ru_RU.md">Русский</a>
<a href="README_pt_BR.md">Português Brasileiro</a>

View File

@@ -28,7 +28,8 @@
<a href="README_de_DE.md">Deutsch</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_zh_CN.md">中文</a>
<a href="README_zh_CN.md">简体中文</a>
<a href="README_zh_TW.md">正體中文</a>
<a href="README_uk_UA.md">Українська</a>
<a href="README_ru_RU.md">Русский</a>
<a href="README_pt_BR.md">Português Brasileiro</a>

View File

@@ -27,7 +27,8 @@
<a href="README_de_DE.md">Deutsch</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_zh_CN.md">中文</a>
<a href="README_zh_CN.md">简体中文</a>
<a href="README_zh_TW.md">正體中文</a>
<a href="README_ru_RU.md">Русский</a>
<a href="README_pt_BR.md">Português Brasileiro</a>
<a href="README_sv_SE.md">Svenska</a>

View File

@@ -28,7 +28,8 @@
<a href="README_de_DE.md">Deutsch</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_zh_CN.md">中文</a>
<a href="README_zh_CN.md">简体中文</a>
<a href="README_zh_TW.md">正體中文</a>
<a href="README_uk_UA.md">Українська</a>
<a href="README_ru_RU.md">Русский</a>
<a href="README_pt_BR.md">Português Brasileiro</a>

View File

@@ -27,7 +27,8 @@
<a href="README_ko_KR.md">한국어</a>
<a href="README_de_DE.md">Deutsch</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_zh_CN.md">中文</a>
<a href="README_zh_CN.md">简体中文</a>
<a href="README_zh_TW.md">正體中文</a>
<a href="README_uk_UA.md">Українська</a>
<a href="README_ru_RU.md">Русский</a>
<a href="README_pt_BR.md">Português Brasileiro</a>

View File

@@ -29,7 +29,8 @@
<a href="README_de_DE.md">Deutsch</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_zh_CN.md">中文</a>
<a href="README_zh_CN.md">简体中文</a>
<a href="README_zh_TW.md">正體中文</a>
<a href="README_uk_UA.md">Українська</a>
<a href="README_ru_RU.md">Русский</a>
<a href="README_sv_SE.md">Svenska</a>

View File

@@ -28,7 +28,8 @@
<a href="README_de_DE.md">Deutsch</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_zh_CN.md">中文</a>
<a href="README_zh_CN.md">简体中文</a>
<a href="README_zh_TW.md">正體中文</a>
<a href="README_uk_UA.md">Українська</a>
<a href="README_pt_BR.md">Português Brasileiro</a>
<a href="README_sv_SE.md">Svenska</a>

View File

@@ -29,7 +29,8 @@
<a href="README_de_DE.md">Deutsch</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_zh_CN.md">中文</a>
<a href="README_zh_CN.md">简体中文</a>
<a href="README_zh_TW.md">正體中文</a>
<a href="README_uk_UA.md">Українська</a>
<a href="README_ru_RU.md">Русский</a>
<a href="README_pt_BR.md">Português Brasileiro</a>

View File

@@ -31,7 +31,8 @@
<a href="README_de_DE.md">Deutsch</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_zh_CN.md">中文</a>
<a href="README_zh_CN.md">简体中文</a>
<a href="README_zh_TW.md">正體中文</a>
<a href="README_uk_UA.md">Українська</a>
<a href="README_ru_RU.md">Русский</a>
<a href="README_pt_BR.md">Português Brasileiro</a>

View File

@@ -27,7 +27,8 @@
<a href="README_ko_KR.md">한국어</a>
<a href="README_de_DE.md">Deutsch</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_zh_CN.md">中文</a>
<a href="README_zh_CN.md">简体中文</a>
<a href="README_zh_TW.md">正體中文</a>
<a href="README_uk_UA.md">Українська</a>
<a href="README_ru_RU.md">Русский</a>
<a href="README_pt_BR.md">Português Brasileiro</a>

View File

@@ -29,7 +29,8 @@
<a href="README_de_DE.md">Deutsch</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_zh_CN.md">中文</a>
<a href="README_zh_CN.md">简体中文</a>
<a href="README_zh_TW.md">正體中文</a>
<a href="README_ru_RU.md">Русский</a>
<a href="README_pt_BR.md">Português Brasileiro</a>
<a href="README_sv_SE.md">Svenska</a>

View File

@@ -29,7 +29,8 @@
<a href="README_de_DE.md">Deutsch</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_zh_CN.md">中文</a>
<a href="README_zh_CN.md">简体中文</a>
<a href="README_zh_TW.md">正體中文</a>
<a href="README_uk_UA.md">Українська</a>
<a href="README_ru_RU.md">Русский</a>
<a href="README_pt_BR.md">Português Brasileiro</a>

View File

@@ -32,6 +32,7 @@
<a href="README_de_DE.md">Deutsch</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_zh_TW.md">正體中文</a>
<a href="README_uk_UA.md">Українська</a>
<a href="README_ru_RU.md">Русский</a>
<a href="README_pt_BR.md">Português Brasileiro</a>

132
readme_i18n/README_zh_TW.md Normal file
View File

@@ -0,0 +1,132 @@
<p align="center">
<br/>
<a href="https://opensource.org/license/agpl-v3"><img src="https://img.shields.io/badge/License-AGPL_v3-blue.svg?color=3F51B5&style=for-the-badge&label=授權條款&logoColor=000000&labelColor=ececec" alt="授權條款AGPLv3"></a>
<a href="https://discord.immich.app">
<img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" alt="Discord"/>
</a>
<br/>
<br/>
</p>
<p align="center">
<img src="../design/immich-logo-stacked-light.svg" width="300" title="以自訂 URL 登入">
</p>
<h3 align="center">高效能的自架照片和影片管理解決方案</h3>
<br/>
<a href="https://immich.app">
<img src="../design/immich-screenshots.png" title="主要螢幕截圖">
</a>
<br/>
<p align="center">
<a href="../README.md">English</a>
<a href="README_ca_ES.md">Català</a>
<a href="README_es_ES.md">Español</a>
<a href="README_fr_FR.md">Français</a>
<a href="README_it_IT.md">Italiano</a>
<a href="README_ja_JP.md">日本語</a>
<a href="README_ko_KR.md">한국어</a>
<a href="README_de_DE.md">Deutsch</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_zh_CN.md">简体中文</a>
<a href="README_zh_TW.md">正體中文</a>
<a href="README_uk_UA.md">Українська</a>
<a href="README_ru_RU.md">Русский</a>
<a href="README_pt_BR.md">Português Brasileiro</a>
<a href="README_sv_SE.md">Svenska</a>
<a href="README_ar_JO.md">العربية</a>
<a href="README_vi_VN.md">Tiếng Việt</a>
<a href="README_th_TH.md">ภาษาไทย</a>
</p>
> [!WARNING]
> ⚠️ 請務必遵循 [3-2-1 備份原則](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/),守護珍貴的照片與影片!
>
> [!NOTE]
> 主要說明文件(包含安裝指南)可於 https://immich.app/ 取得。
## 連結
- [說明文件](https://docs.immich.app/)
- [關於](https://docs.immich.app/overview/introduction)
- [安裝](https://docs.immich.app/install/requirements)
- [發展藍圖](https://immich.app/roadmap)
- [線上體驗](#線上體驗)
- [功能](#功能)
- [翻譯](https://docs.immich.app/developer/translations)
- [貢獻指南](https://docs.immich.app/overview/support-the-project)
## 線上體驗
請前往 [Demoo 網站](https://demo.immich.app) 立即體驗 Immich。若要在手機 App 試用,請在 `伺服器端點 URL` 欄位輸入 `https://demo.immich.app`
### 登入資訊
| 電子郵件 | 密碼 |
| --------------- | ------ |
| demo@immich.app | demo |
## 功能
| 功能 | 手機版 | 網頁版 |
| :----------------------------------------- | ------ | ------ |
| 上傳與檢視照片與影片 | 是 | 是 |
| 開啟 App 時自動備份 | 是 | 不適用 |
| 避免重複媒體 | 是 | 是 |
| 選擇要備份的相簿 | 是 | 不適用 |
| 下載照片與影片到本機裝置 | 是 | 是 |
| 多使用者支援 | 是 | 是 |
| 相簿與共享相簿 | 是 | 是 |
| 可拖曳的捲軸 | 是 | 是 |
| 支援 RAW 格式 | 是 | 是 |
| 中繼資料檢視EXIF、地圖 | 是 | 是 |
| 依中繼資料、物件、臉孔與 CLIP 搜尋 | 是 | 是 |
| 管理功能(使用者管理) | 否 | 是 |
| 背景備份 | 是 | 不適用 |
| 虛擬滾動 | 是 | 是 |
| 支援 OAuth | 是 | 是 |
| API 金鑰 | 不適用 | 是 |
| Live Photo/動態照片備份與播放 | 是 | 是 |
| 支援 360 度全景照片顯示 | 否 | 是 |
| 使用者自訂儲存結構 | 是 | 是 |
| 公開分享 | 是 | 是 |
| 封存與收藏 | 是 | 是 |
| 世界地圖 | 是 | 是 |
| 親朋好友分享 | 是 | 是 |
| 臉部辨識與分群 | 是 | 是 |
| 回憶x 年前) | 是 | 是 |
| 離線支援 | 是 | 否 |
| 唯讀媒體庫 | 是 | 是 |
| 照片堆疊 | 是 | 是 |
| 標籤 | 否 | 是 |
| 資料夾檢視 | 是 | 是 |
## 翻譯
更多翻譯相關資訊請見 [此處](https://docs.immich.app/developer/translations)。
<a href="https://hosted.weblate.org/engage/immich/">
<img src="https://hosted.weblate.org/widget/immich/immich/multi-auto.svg" alt="翻譯狀態" />
</a>
## 專案活躍度
![專案活躍度](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Repobeats 分析圖片")
## Star 數量歷史紀錄
<a href="https://star-history.com/#immich-app/immich&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=immich-app/immich&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" />
<img alt="Star 歷史紀錄圖表" src="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" width="100%" />
</picture>
</a>
## 貢獻者
<a href="https://github.com/alextran1502/immich/graphs/contributors">
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
</a>

View File

@@ -21,6 +21,11 @@
"addLabels": [
"📱mobile"
]
},
{
"matchPackageNames": ["ghcr.io/immich-app/postgres"],
"matchUpdateTypes": ["major"],
"enabled": false
}
],
"ignorePaths": [

View File

@@ -1,39 +1,54 @@
FROM ghcr.io/immich-app/base-server-dev:202509210934@sha256:b5ce2d7eaf379d4cf15efd4bab180d8afc8a80d20b36c9800f4091aca6ae267e AS builder
FROM ghcr.io/immich-app/base-server-dev:202510092146@sha256:124ec9659cba4a206924de5e3691f84acde16d75fa2b10b7007542424b696b96 AS builder
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
CI=1 \
COREPACK_HOME=/tmp
COREPACK_HOME=/tmp \
PNPM_HOME=/buildcache/pnpm-store \
PATH="/buildcache/pnpm-store:$PATH"
RUN npm install --global corepack@latest && \
corepack enable pnpm
corepack enable pnpm && \
pnpm config set store-dir "$PNPM_HOME"
FROM builder AS server
WORKDIR /usr/src/app
COPY ./package* ./pnpm* .pnpmfile.cjs ./
COPY ./server ./server/
RUN SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter immich --frozen-lockfile build && \
RUN --mount=type=cache,id=pnpm-server,target=/buildcache/pnpm-store \
--mount=type=bind,source=package.json,target=package.json \
--mount=type=bind,source=.pnpmfile.cjs,target=.pnpmfile.cjs \
--mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \
--mount=type=bind,source=pnpm-workspace.yaml,target=pnpm-workspace.yaml \
SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter immich --frozen-lockfile build && \
SHARP_FORCE_GLOBAL_LIBVIPS=true pnpm --filter immich --frozen-lockfile --prod --no-optional deploy /output/server-pruned
FROM builder AS web
WORKDIR /usr/src/app
COPY ./package* ./pnpm* .pnpmfile.cjs ./
COPY ./web ./web/
COPY ./i18n ./i18n/
COPY ./open-api ./open-api/
RUN SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter @immich/sdk --filter immich-web --frozen-lockfile --force install && \
RUN --mount=type=cache,id=pnpm-web,target=/buildcache/pnpm-store \
--mount=type=bind,source=package.json,target=package.json \
--mount=type=bind,source=.pnpmfile.cjs,target=.pnpmfile.cjs \
--mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \
--mount=type=bind,source=pnpm-workspace.yaml,target=pnpm-workspace.yaml \
SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter @immich/sdk --filter immich-web --frozen-lockfile --force install && \
pnpm --filter @immich/sdk --filter immich-web build
FROM builder AS cli
COPY ./package* ./pnpm* .pnpmfile.cjs ./
COPY ./cli ./cli/
COPY ./open-api ./open-api/
RUN pnpm --filter @immich/sdk --filter @immich/cli --frozen-lockfile install && \
RUN --mount=type=cache,id=pnpm-cli,target=/buildcache/pnpm-store \
--mount=type=bind,source=package.json,target=package.json \
--mount=type=bind,source=.pnpmfile.cjs,target=.pnpmfile.cjs \
--mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \
--mount=type=bind,source=pnpm-workspace.yaml,target=pnpm-workspace.yaml \
pnpm --filter @immich/sdk --filter @immich/cli --frozen-lockfile install && \
pnpm --filter @immich/sdk --filter @immich/cli build && \
pnpm --filter @immich/cli --prod --no-optional deploy /output/cli-pruned
FROM ghcr.io/immich-app/base-server-prod:202509210934@sha256:0c7eacf0ba88ca52e1a267cfc62d20d07792ea2c604818c2cbd37dc7dcefdac9
FROM ghcr.io/immich-app/base-server-prod:202510092146@sha256:c39b9ad949e7777bce415e6931334aeff7331e04cb7f9df93f9ae44f6ff36b9e
WORKDIR /usr/src/app
ENV NODE_ENV=production \

View File

@@ -1,5 +1,5 @@
# dev build
FROM ghcr.io/immich-app/base-server-dev:202509210934@sha256:b5ce2d7eaf379d4cf15efd4bab180d8afc8a80d20b36c9800f4091aca6ae267e AS dev
FROM ghcr.io/immich-app/base-server-dev:202510092146@sha256:124ec9659cba4a206924de5e3691f84acde16d75fa2b10b7007542424b696b96 AS dev
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
CI=1 \

View File

@@ -47,7 +47,7 @@
"@opentelemetry/exporter-prometheus": "^0.205.0",
"@opentelemetry/instrumentation-http": "^0.205.0",
"@opentelemetry/instrumentation-ioredis": "^0.53.0",
"@opentelemetry/instrumentation-nestjs-core": "^0.51.0",
"@opentelemetry/instrumentation-nestjs-core": "^0.52.0",
"@opentelemetry/instrumentation-pg": "^0.58.0",
"@opentelemetry/resources": "^2.0.1",
"@opentelemetry/sdk-metrics": "^2.0.1",
@@ -67,7 +67,7 @@
"compression": "^1.8.0",
"cookie": "^1.0.2",
"cookie-parser": "^1.4.7",
"cron": "4.3.0",
"cron": "4.3.3",
"exiftool-vendored": "^28.8.0",
"express": "^5.1.0",
"fast-glob": "^3.3.2",
@@ -85,7 +85,7 @@
"multer": "^2.0.2",
"nest-commander": "^3.16.0",
"nestjs-cls": "^5.0.0",
"nestjs-kysely": "^3.0.0",
"nestjs-kysely": "3.0.0",
"nestjs-otel": "^7.0.0",
"nodemailer": "^7.0.0",
"openid-client": "^6.3.3",
@@ -101,7 +101,7 @@
"sanitize-filename": "^1.6.3",
"sanitize-html": "^2.14.0",
"semver": "^7.6.2",
"sharp": "^0.34.3",
"sharp": "^0.34.4",
"sirv": "^3.0.0",
"socket.io": "^4.8.1",
"tailwindcss-preset-email": "^1.4.0",
@@ -129,7 +129,7 @@
"@types/luxon": "^3.6.2",
"@types/mock-fs": "^4.13.1",
"@types/multer": "^2.0.0",
"@types/node": "^22.18.1",
"@types/node": "^22.18.8",
"@types/nodemailer": "^7.0.0",
"@types/picomatch": "^4.0.0",
"@types/pngjs": "^6.0.5",
@@ -164,6 +164,6 @@
"node": "22.20.0"
},
"overrides": {
"sharp": "^0.34.3"
"sharp": "^0.34.4"
}
}

View File

@@ -19,6 +19,7 @@ import { ConfigRepository } from 'src/repositories/config.repository';
import { EventRepository } from 'src/repositories/event.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { teardownTelemetry, TelemetryRepository } from 'src/repositories/telemetry.repository';
import { UserRepository } from 'src/repositories/user.repository';
import { services } from 'src/services';
import { AuthService } from 'src/services/auth.service';
import { CliService } from 'src/services/cli.service';
@@ -55,6 +56,7 @@ class BaseModule implements OnModuleInit, OnModuleDestroy {
private jobService: JobService,
private telemetryRepository: TelemetryRepository,
private authService: AuthService,
private userRepository: UserRepository,
) {
logger.setAppName(this.worker);
}

View File

@@ -363,6 +363,14 @@ group by
order by
"user"."createdAt" asc
-- UserRepository.getCount
select
count(*) as "count"
from
"user"
where
"user"."deletedAt" is null
-- UserRepository.updateUsage
update "user"
set

View File

@@ -286,6 +286,16 @@ export class UserRepository {
.execute();
}
@GenerateSql()
async getCount(): Promise<number> {
const result = await this.db
.selectFrom('user')
.select((eb) => eb.fn.countAll().as('count'))
.where('user.deletedAt', 'is', null)
.executeTakeFirstOrThrow();
return Number(result.count);
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.NUMBER] })
async updateUsage(id: string, delta: number): Promise<void> {
await this.db

View File

@@ -215,6 +215,7 @@ export class BaseService {
payload.storageLabel = sanitize(payload.storageLabel.replaceAll('.', ''));
}
this.telemetryRepository.api.addToGauge(`immich.users.total`, 1);
return this.userRepository.create(payload);
}
}

View File

@@ -102,6 +102,7 @@ export class UserAdminService extends BaseService {
const status = force ? UserStatus.Removing : UserStatus.Deleted;
const user = await this.userRepository.update(id, { status, deletedAt: new Date() });
this.telemetryRepository.api.addToGauge(`immich.users.total`, -1);
if (force) {
await this.jobRepository.queue({ name: JobName.UserDelete, data: { id: user.id, force } });
@@ -114,6 +115,7 @@ export class UserAdminService extends BaseService {
await this.findOrFail(id, { withDeleted: true });
await this.albumRepository.restoreAll(id);
const user = await this.userRepository.restore(id);
this.telemetryRepository.api.addToGauge('immich.users.total', 1);
return mapUserAdmin(user);
}

View File

@@ -3,14 +3,14 @@ import { Updateable } from 'kysely';
import { DateTime } from 'luxon';
import { SALT_ROUNDS } from 'src/constants';
import { StorageCore } from 'src/cores/storage.core';
import { OnJob } from 'src/decorators';
import { OnEvent, OnJob } from 'src/decorators';
import { AuthDto } from 'src/dtos/auth.dto';
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
import { OnboardingDto, OnboardingResponseDto } from 'src/dtos/onboarding.dto';
import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto';
import { CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto';
import { CacheControl, JobName, JobStatus, QueueName, StorageFolder, UserMetadataKey } from 'src/enum';
import { CacheControl, ImmichWorker, JobName, JobStatus, QueueName, StorageFolder, UserMetadataKey } from 'src/enum';
import { UserFindOptions } from 'src/repositories/user.repository';
import { UserTable } from 'src/schema/tables/user.table';
import { BaseService } from 'src/services/base.service';
@@ -213,6 +213,12 @@ export class UserService extends BaseService {
};
}
@OnEvent({ name: 'AppBootstrap', workers: [ImmichWorker.Api] })
async onBootstrap(): Promise<void> {
const userCount = await this.userRepository.getCount();
this.telemetryRepository.api.addToGauge('immich.users.total', userCount);
}
@OnJob({ name: JobName.UserSyncUsage, queue: QueueName.BackgroundTask })
async handleUserSyncUsage(): Promise<JobStatus> {
await this.userRepository.syncUsage();

View File

@@ -39,6 +39,7 @@ import { StorageRepository } from 'src/repositories/storage.repository';
import { SyncCheckpointRepository } from 'src/repositories/sync-checkpoint.repository';
import { SyncRepository } from 'src/repositories/sync.repository';
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
import { TelemetryRepository } from 'src/repositories/telemetry.repository';
import { UserRepository } from 'src/repositories/user.repository';
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
import { DB } from 'src/schema';
@@ -54,6 +55,7 @@ import { StackTable } from 'src/schema/tables/stack.table';
import { UserTable } from 'src/schema/tables/user.table';
import { BASE_SERVICE_DEPENDENCIES, BaseService } from 'src/services/base.service';
import { SyncService } from 'src/services/sync.service';
import { newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock';
import { factory, newDate, newEmbedding, newUuid } from 'test/small.factory';
import { automock, wait } from 'test/utils';
import { Mocked } from 'vitest';
@@ -347,6 +349,10 @@ const newMockRepository = <T>(key: ClassConstructor<T>) => {
return automock(key);
}
case TelemetryRepository: {
return newTelemetryRepositoryMock();
}
case DatabaseRepository: {
return automock(DatabaseRepository, {
args: [undefined, { setContext: () => {} }, { getEnv: () => ({ database: { vectorExtension: '' } }) }],

View File

@@ -11,6 +11,7 @@ import { LoggingRepository } from 'src/repositories/logging.repository';
import { SessionRepository } from 'src/repositories/session.repository';
import { StorageRepository } from 'src/repositories/storage.repository';
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
import { TelemetryRepository } from 'src/repositories/telemetry.repository';
import { UserRepository } from 'src/repositories/user.repository';
import { DB } from 'src/schema';
import { AuthService } from 'src/services/auth.service';
@@ -32,7 +33,7 @@ const setup = (db?: Kysely<DB>) => {
SystemMetadataRepository,
UserRepository,
],
mock: [LoggingRepository, StorageRepository, EventRepository],
mock: [LoggingRepository, StorageRepository, EventRepository, TelemetryRepository],
});
};

View File

@@ -6,6 +6,7 @@ import { CryptoRepository } from 'src/repositories/crypto.repository';
import { JobRepository } from 'src/repositories/job.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
import { TelemetryRepository } from 'src/repositories/telemetry.repository';
import { UserRepository } from 'src/repositories/user.repository';
import { DB } from 'src/schema';
import { UserService } from 'src/services/user.service';
@@ -21,7 +22,7 @@ const setup = (db?: Kysely<DB>) => {
return newMediumService(UserService, {
database: db || defaultDatabase,
real: [CryptoRepository, ConfigRepository, SystemMetadataRepository, UserRepository],
mock: [LoggingRepository, JobRepository],
mock: [LoggingRepository, JobRepository, TelemetryRepository],
});
};

View File

@@ -27,7 +27,7 @@
"dependencies": {
"@formatjs/icu-messageformat-parser": "^2.9.8",
"@immich/sdk": "file:../open-api/typescript-sdk",
"@immich/ui": "^0.29.0",
"@immich/ui": "^0.34.0",
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
"@mdi/js": "^7.4.47",
"@photo-sphere-viewer/core": "^5.11.5",
@@ -44,7 +44,7 @@
"geo-coordinates-parser": "^1.7.4",
"geojson": "^0.5.0",
"handlebars": "^4.7.8",
"happy-dom": "^18.0.1",
"happy-dom": "^20.0.0",
"intl-messageformat": "^10.7.11",
"justified-layout": "^4.1.0",
"lodash-es": "^4.17.21",
@@ -69,7 +69,7 @@
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/enhanced-img": "^0.8.0",
"@sveltejs/kit": "^2.27.1",
"@sveltejs/vite-plugin-svelte": "6.2.0",
"@sveltejs/vite-plugin-svelte": "6.2.1",
"@tailwindcss/vite": "^4.1.7",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/svelte": "^5.2.8",
@@ -89,13 +89,13 @@
"eslint-plugin-unicorn": "^61.0.2",
"factory.ts": "^1.4.1",
"globals": "^16.0.0",
"happy-dom": "^18.0.1",
"happy-dom": "^20.0.0",
"prettier": "^3.4.2",
"prettier-plugin-organize-imports": "^4.0.0",
"prettier-plugin-sort-json": "^4.1.1",
"prettier-plugin-svelte": "^3.3.3",
"rollup-plugin-visualizer": "^6.0.0",
"svelte": "5.38.10",
"svelte": "5.39.11",
"svelte-check": "^4.1.5",
"svelte-eslint-parser": "^1.3.3",
"tailwindcss": "^4.1.7",

View File

@@ -16,7 +16,7 @@
<Label size="small" color="primary" for={id}>{title}</Label>
<Text size="small" color="muted" {id}>
{#if versionHref}
<Link external href={versionHref}>{version}</Link>
<Link href={versionHref}>{version}</Link>
{:else}
{version}
{/if}

View File

@@ -12,6 +12,7 @@ type ActionMap = {
[AssetAction.RESTORE]: { asset: TimelineAsset };
[AssetAction.ADD]: { asset: TimelineAsset };
[AssetAction.ADD_TO_ALBUM]: { asset: TimelineAsset; album: AlbumResponseDto };
[AssetAction.STACK]: { stack: StackResponseDto };
[AssetAction.UNSTACK]: { assets: TimelineAsset[] };
[AssetAction.KEEP_THIS_DELETE_OTHERS]: { asset: TimelineAsset };
[AssetAction.SET_STACK_PRIMARY_ASSET]: { stack: StackResponseDto };

View File

@@ -0,0 +1,37 @@
<script lang="ts">
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import { AssetAction } from '$lib/constants';
import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { createStack, type AssetResponseDto, type StackResponseDto } from '@immich/sdk';
import { mdiUploadMultiple } from '@mdi/js';
import { t } from 'svelte-i18n';
import type { OnAction } from './action';
interface Props {
asset: AssetResponseDto;
stack: StackResponseDto | null;
onAction: OnAction;
}
let { asset, stack, onAction }: Props = $props();
const handleAddUploadToStack = async () => {
const newAssetIds = await openFileUploadDialog({ multiple: true });
// Including the old stacks primary asset ID ensures that all assets of the
// old stack are automatically included in the new stack.
const primaryAssetId = stack?.primaryAssetId ?? asset.id;
// First asset in the list will become the new primary asset.
const assetIds = [primaryAssetId, ...newAssetIds];
const newStack = await createStack({
stackCreateDto: {
assetIds,
},
});
onAction({ type: AssetAction.STACK, stack: newStack });
};
</script>
<MenuOption icon={mdiUploadMultiple} onClick={handleAddUploadToStack} text={$t('add_upload_to_stack')} />

View File

@@ -4,6 +4,7 @@
import CastButton from '$lib/cast/cast-button.svelte';
import type { OnAction, PreAction } from '$lib/components/asset-viewer/actions/action';
import AddToAlbumAction from '$lib/components/asset-viewer/actions/add-to-album-action.svelte';
import AddToStackAction from '$lib/components/asset-viewer/actions/add-to-stack-action.svelte';
import ArchiveAction from '$lib/components/asset-viewer/actions/archive-action.svelte';
import CloseAction from '$lib/components/asset-viewer/actions/close-action.svelte';
import DeleteAction from '$lib/components/asset-viewer/actions/delete-action.svelte';
@@ -196,6 +197,7 @@
{/if}
{#if isOwner}
<AddToStackAction {asset} {stack} {onAction} />
{#if stack}
<UnstackAction {stack} {onAction} />
<KeepThisDeleteOthersAction {stack} {asset} {onAction} />

View File

@@ -22,8 +22,8 @@
import {
AssetJobName,
AssetTypeEnum,
getAssetInfo,
getAllAlbums,
getAssetInfo,
getStack,
runAssetJobs,
type AlbumResponseDto,
@@ -303,10 +303,8 @@
const handleStopSlideshow = async () => {
try {
// eslint-disable-next-line tscompat/tscompat
if (document.fullscreenElement) {
document.body.style.cursor = '';
// eslint-disable-next-line tscompat/tscompat
await document.exitFullscreen();
}
} catch (error) {
@@ -336,6 +334,7 @@
}
break;
}
case AssetAction.STACK:
case AssetAction.SET_STACK_PRIMARY_ASSET: {
stack = action.stack;
break;

View File

@@ -307,6 +307,7 @@
weekday: 'short',
hour: 'numeric',
minute: '2-digit',
second: '2-digit',
timeZoneName: timeZone ? 'longOffset' : undefined,
},
{ locale: $locale },

View File

@@ -100,9 +100,7 @@
};
const onShowSettings = async () => {
// eslint-disable-next-line tscompat/tscompat
if (document.fullscreenElement) {
// eslint-disable-next-line tscompat/tscompat
await document.exitFullscreen();
}
await modalManager.show(SlideshowSettingsModal);
@@ -114,11 +112,7 @@
webkitIsFullScreen?: boolean;
};
if (
// eslint-disable-next-line tscompat/tscompat
!document.fullscreenElement &&
!doc.webkitIsFullScreen
) {
if (!document.fullscreenElement && !doc.webkitIsFullScreen) {
onClose();
}
}

View File

@@ -88,7 +88,7 @@
const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
let selectedDate = $state(initialDate.toFormat("yyyy-MM-dd'T'HH:mm"));
let selectedDate = $state(initialDate.toFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"));
// Use a fixed modern date to calculate stable timezone offsets for the list
// This ensures that the offsets shown in the combobox are always current,
// regardless of the historical date selected by the user.
@@ -106,7 +106,7 @@
const { offsetMinutes, offsetFormat: zoneOffsetAtDate } = getModernOffsetForZoneAndDate(zone, date);
// For validity, we still need to check if the exact date/time exists in the *original* timezone (for gaps/overlaps).
const dateForValidity = DateTime.fromISO(date, { zone });
const valid = dateForValidity.isValid && date === dateForValidity.toFormat("yyyy-MM-dd'T'HH:mm");
const valid = dateForValidity.isValid && date === dateForValidity.toFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
return {
value: zone,
offsetMinutes,

View File

@@ -341,7 +341,6 @@
scrubberMonthScrollPercent: monthGroupPercentY,
});
};
/* eslint-disable tscompat/tscompat */
const getTouch = (event: TouchEvent) => {
if (event.touches.length === 1) {
return event.touches[0];
@@ -386,7 +385,6 @@
isHover = false;
}
};
/* eslint-enable tscompat/tscompat */
onMount(() => {
document.addEventListener('touchmove', onTouchMove, { capture: true, passive: true });
return () => {

Some files were not shown because too many files have changed in this diff Show More