Compare commits

..

68 Commits

Author SHA1 Message Date
renovate[bot] ca877a004f fix(deps): update dependency permission_handler to v12 2026-05-11 14:28:27 +00:00
bo0tzz b4f719653f fix: indentation and typo (#28349)
* fix: indentation and typo

* neline
2026-05-11 09:17:39 -05:00
bo0tzz f370b4bac6 chore: fold apk comment qr in <details> (#28348) 2026-05-11 09:40:19 -04:00
Mert d788169bf3 chore(server)!: remove libopus enum (#28325) 2026-05-11 08:02:57 -04:00
Mert eea820fa2f chore(server): enable hw decoding by default (#28324) 2026-05-11 08:02:26 -04:00
Timon 271f1cb868 feat(web): Add metadata overlay to slideshow (#24627)
* feat: add slideshow metadata overlay and settings

* Introduced a new SlideshowMetadataOverlay component to display image information during slideshows.
* Updated slideshow settings modal to include options for showing the metadata overlay and selecting its display mode (Description Only or Full).
* Added corresponding translations and store management for the new overlay features.

* remove noisy log

* constant opacity

* 2nd pass

* more

* use text components

* lint
2026-05-11 11:49:12 +02:00
Mert 8c8dc9d32f chore(ml)!: remove deprecated envs (#28326)
remove deprecated envs
2026-05-09 22:40:05 +00:00
Alex fd18e55f7c chore: token extraction for build mobile (#28320)
Co-authored-by: bo0tzz <git@bo0tzz.me>
2026-05-09 15:08:07 +00:00
shenlong faab9e620d refactor: medium repository context helpers (#28311)
* refactor: medium repository context helpers

* test: add regress test for 26723

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-05-09 08:19:31 -05:00
Matthew Momjian 5ba3efafd8 fix(deployment): remove unneeded volume (#28307)
remove unneeded volume
2026-05-09 09:07:54 -04:00
Alex 8b3c9bf9c3 feat(ci): publish PR Android APK to comment (#28283)
* feat(ci): publish PR Android APK to R2 with installable links

Adds a universal debug APK to PR builds and uploads it to a public
R2 bucket alongside the existing GitHub Actions artifact. Posts a
sticky PR comment with tap-to-install links and a QR code so testers
can install directly on their device without unzipping artifacts.

Required setup:
- Secrets: R2_APK_ACCESS_KEY_ID, R2_APK_SECRET_ACCESS_KEY,
  R2_APK_ACCOUNT_ID, R2_APK_BUCKET
- Optional repo variable: APK_PUBLIC_HOST (defaults to apk.immich.app)
- R2 bucket configured with a public custom domain matching APK_PUBLIC_HOST

* chore(ci): drop R2 upload, link directly to GitHub artifact

Surfaces the existing release-apk-signed artifact in a sticky PR
comment with a QR code. Avoids new infra and secrets — the trade-off
is GitHub login and a zip wrapper instead of tap-to-install.

* feat(ci): build PR APK as release and publish to GitHub Release

PR builds now produce a release APK signed with the release keystore.
The universal APK is published as a GitHub Release asset under tag
'pr-<num>' (prerelease), giving testers a direct, unzipped, tap-to-
install URL plus a QR code in the PR comment. The release-apk-signed
artifact is unchanged.

* chore(ci): drop GitHub Release, publish universal APK as own artifact

Reverts the prerelease publish. Uploads the universal release APK as
a separate single-file artifact so its download URL gives a zip
containing only that APK — no extra files to dig through. The QR in
the PR comment points at this universal-only artifact.

* chore(ci): build only universal APK for PR, drop split artifact

PR builds skip the arm64-only split — release-apk-signed now contains
just the universal app-release.apk, so the download zip is a single
file. Removes the redundant separate universal artifact and points
the PR comment QR at the main artifact URL.

* feat(mobile): suffix PR APK applicationId so it installs alongside production

Each PR build now becomes app.alextran.immich.pr<num> via PR_NUMBER env
read in build.gradle, so testers can install multiple PR builds and the
Play Store version on the same device without uninstalling. Also tags
the version with -pr<num> for visibility.

* feat(ci): allow PR APK build to run on forks

Forks can now run the Android build job. Steps that need repo secrets
(create-workflow-token, Create Keystore) are skipped when the PR is
from a fork, the checkout falls back to GITHUB_TOKEN, and build.gradle
falls back to debug signing if the release keystore isn't materialised.
The PR comment still requires write access, so it's gated to non-fork
PRs — fork APKs are reachable from the workflow run's artifact tab.
2026-05-09 07:46:40 -05:00
Yaros 41f285aa3e feat(mobile): increased tap area on video player overlay (#27269)
* fix(mobile): improved tap area on video player

* fix: back button padding

* chore: use sizedbox.square & button padding

* chore: fixed padding
2026-05-09 10:47:41 +07:00
Sandro fdac6c8bc4 fix(docs): missing colon in config file doc (#28313)
Fix missing colon
2026-05-09 09:44:41 +07:00
Thorsten Winkler d7f05d2510 fix(mobile): Deduplicate assets in person view timeline (#26723)
fix(mobile): deduplicate assets in person view timeline

Previously, assets with multiple face records for the same person (e.g.,
manual Digikam imports and Immich ML detections) appeared multiple times
in the person timeline. This was caused by an inner join on the
assetFaceEntity without proper deduplication.

This commit refactors the timeline queries to use a subquery approach
instead of joins and grouping. This ensures:

- _getPersonBucketAssets: Only unique assets are fetched, even if
  multiple face records exist for a single asset.
- _watchPersonBucket: Asset counts in timeline headers are accurate
  and represent unique assets.
- Performance: Database overhead is reduced by avoiding complex joins
  and explicit groupBy operations on large result sets.

Signed-off-by: thowdev <12428285+thowdev@users.noreply.github.com>
2026-05-09 01:53:18 +00:00
stfn 3100bd5eed fix(mobile): avoid duplicate assets in album view (#28152)
fix(mobile): avoid duplicate assets in remote album timeline

Co-authored-by: Stefan Friedli <stefan@stefanfriedli.ch>
2026-05-09 08:24:54 +07:00
Jason Rasmussen 8a024e2b50 chore: faster web linting (#28303) 2026-05-08 16:55:14 -04:00
Jason Rasmussen 25a6a38b30 chore: use mise (#28298) 2026-05-08 15:21:33 -04:00
Santo Shakil 7c6750941e fix(mobile): mounted check before setState in album sync action (#28300)
_manualSyncAlbums fires a setState 1s after sync via Future.delayed
with no mounted check. if the widget is gone by then, setState throws
null check and the global error logger logs it severe.
2026-05-08 18:55:03 +00:00
renovate[bot] 832ed4d015 chore(deps): update dependency exiftool-vendored to v35.19.0 [security] (#28261)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-05-08 12:24:31 +02:00
Santo Shakil 238895cad9 fix(mobile): restore notification plugin init (#28284)
#27666 removed LocalNotificationService with the legacy stack, which
was the only place calling FlutterLocalNotificationsPlugin().initialize().
without it, ios never prompts for the notification perm on fresh
installs so background_downloader notifications get dropped silently.

restores the init in the same spot the deleted call used to live.
2026-05-08 10:45:52 +07:00
sakshamchawla e2ec04e86c feat: hide hidden person from memories (#20877)
* hide hidden person from memories

* clean up

* fix united test

* clean up

* moved sql to inline, rebased

* clean up

* clean up again

* chore: sync sql

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2026-05-07 19:54:26 +00:00
renovate[bot] 6050526360 fix(deps): update dependency connectivity_plus to v7 (#22921)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-07 15:22:26 -04:00
renovate[bot] bfd76570c5 chore(deps): update dependency python-multipart to v0.0.27 [security] (#28286)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-07 15:22:03 -04:00
renovate[bot] 37e6a49652 fix(deps): update dependency nestjs-otel to v8 (#27863)
* fix(deps): update dependency nestjs-otel to v8

* fix: apiMetrics

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jason Rasmussen <jason@rasm.me>
2026-05-07 19:14:15 +00:00
Mert 36caeb34ec chore(ml)!: require numpy 2.4 (#28158)
require numpy 2.4
2026-05-07 19:07:39 +00:00
renovate[bot] 87713c7f2f chore(deps): update dependency flutter to v3.41.9 (#28235)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-07 15:00:37 -04:00
Daniel Dietzler 2039c129f2 refactor: settings accordion reactivity (#28281) 2026-05-07 19:00:23 +00:00
Timon 52b00b0bad chore(ml): add mise checklist command (#28267)
* chore(ml): add mise checklist command

* don't depend tests on installing a cpu flavor
2026-05-07 12:28:24 -04:00
shenlong 21af184045 refactor: move cleanup config to metadata table (#28225)
* refactor: app metadata

* refactor to per row store

* cleanup

* more test

* review changes

* more refactor

* refactor

* migrate primary color

* migrate dynamic theme

* migrate colorfulInterface

* cleanup providers

* migrate cleanup

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-05-07 11:27:06 -05:00
Timon 1fcc2b704b feat(server)!: add isOwned filter to albums API (#28213)
* feat(server)!: add owned filter to albums API

BREAKING CHANGE: GET /albums with no parameters now returns all accessible albums (owned + shared-with-me) instead of only owned albums.

* document tri-state matrix

* web impl

* collapse to single method and handover branching to sql

* dedupe

* verify that owned, shared, and notShared counts are mapped independently from their respective queries

* refactor(server): add select:['id'] overload to albumRepository.getAll

Avoid fetching full album rows (with albumUsers/sharedLinks subqueries) in map.service where only album IDs are needed.

* focus relevant test filters

* fmt

* Revert "verify that owned, shared, and notShared counts are mapped independently from their respective queries"

This reverts commit 47aab458192c766de4662aada5a6841b091d2a80.

* sync sql

* Revert "document tri-state matrix"

This reverts commit a5b2355d0c.

* address review comments

* inline shared condition and return as ternary

* sync sql

* use [...albums].sort

Array.toSorted() is not supported in Chrome 109

* use isShared and isOwned nomenclature

* fix e2e tests

* add params to sql query
2026-05-07 12:13:07 -04:00
Timon 7de73dc176 fix(server): hide isFavorite from partner asset sync stream (#28035)
* fix(server): hide isFavorite from partner asset sync stream

* use new column entry instead

* sync sql

* add migration

* use sql.val

* sync sql
2026-05-07 12:00:54 -04:00
TheBestX11 fe2bf0c6dd fix(mobile): correct filter default and UI desync in similar photos search (#27516)
* fix(mobile): view similar defaults to images only

* fix(mobile): reset filter chips when pre-filter is applied

---------

Co-authored-by: shenlong <139912620+shenlong-tanwen@users.noreply.github.com>
2026-05-07 15:22:35 +00:00
shenlong d4a97f2d25 refactor: move theme config to metadata table (#28224)
* refactor: app metadata

* refactor to per row store

* cleanup

* more test

* review changes

* more refactor

* refactor

* migrate primary color

* migrate dynamic theme

* migrate colorfulInterface

* cleanup providers

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-05-07 15:12:14 +00:00
shenlong bd58db4fcc fix: periodically execute pragma optimize (#28241)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-05-07 09:54:35 -05:00
Luis Nachtigall 7f43c6a3a3 fix(mobile): prevent asset loading issues when changing page or when closing memories (#27596) 2026-05-07 09:13:22 -04:00
Mees Frensel 87175ee56c fix(docs): document upgrade-insecure-requests default (#28279) 2026-05-07 08:40:14 -04:00
Mees Frensel 13587bf13c feat(web): video player i18n (#28192) 2026-05-07 13:39:37 +02:00
Thomas f09769a2f3 chore(mobile): add box shadow to asset details (#27510)
The details widget can blend with the image when they are similar
colours.
2026-05-06 19:43:55 +00:00
bo0tzz bfdff12ee0 chore: use app token for mise in workflows (#28270) 2026-05-06 14:51:25 -04:00
bo0tzz eb6dca6a31 chore: switch push-o-matic auth from app-id to client-id (#28269) 2026-05-06 17:01:14 +00:00
Daniel Dietzler c2e3739a58 chore: run relevant tests for pnpm updates (#28266) 2026-05-06 15:38:00 +00:00
Daniel Dietzler f6bd514cdc chore: silence svelte state referenced locally warning (#28263) 2026-05-06 17:22:39 +02:00
Timon d93ab7707e chore(ml): configure mise for machine-learning directory (#25579) 2026-05-06 11:03:54 -04:00
renovate[bot] 6bb47c802f chore(deps): update github-actions (#28262)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-06 16:00:03 +02:00
mws-weekend-projects 90a69e2ba6 feat(web): add full-path search mode to UI (#26758)
Co-authored-by: mws-weekend-projects <mws-weekend-projects@users.noreply.github.com>
Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
2026-05-06 15:45:40 +02:00
Mert 6580394cfe chore(server): simplify sharp edit code (#28249) 2026-05-06 09:32:49 -04:00
Daniel Dietzler 42ff3b705d fix: revert "chore(deps): update dependency exiftool-vendored to v35.19.0 [security]" (#28260) 2026-05-06 15:32:25 +02:00
Mert 0f00053bb1 chore(server): simplify preview extraction (#28250) 2026-05-06 09:32:19 -04:00
Mert c5c59ed040 refactor(server): move video interface fetch to storage core (#28248) 2026-05-06 09:32:03 -04:00
Aigars Mahinovs 576b1eb999 docs: update rocm installation instructions (#25434) 2026-05-06 09:30:42 -04:00
Mees Frensel 24189702da fix(web): shared album avatars opening modal (#26719)
Co-authored-by: Alex <alex.tran1502@gmail.com>
2026-05-06 13:49:12 +02:00
Sky ad0d01005e fix(web): migrate people management component to page, enabling tooltips (#26971) 2026-05-06 13:25:59 +02:00
Brent Hugh 3e6d053f93 chore: enhance documentation on wildcard and exclusion patterns (#27884)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-05-06 12:36:25 +02:00
renovate[bot] 1bb3fd985f chore(deps): update dependency exiftool-vendored to v35.19.0 [security] (#28254)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-06 12:28:47 +02:00
Andreas Heinz f72aa54a1f feat(enhancement): Navigate stack with up and down arrow keys (#27854)
* feat(enhancement): navigate stack with up and down arrow keys

* remove unnecessary code

* move shortcut to section; no need for document level
2026-05-06 11:56:23 +02:00
shenlong dafe9d7966 chore: pump version (#28231)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-05-05 16:27:04 -05:00
Daniel Dietzler 7acda0572d fix: stale person name after merge (#28222) 2026-05-05 15:34:49 +02:00
renovate[bot] 98bc9f6a6e chore(deps): update dependency terragrunt to v1.0.3 (#28236)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-05 12:52:14 +02:00
renovate[bot] 63a3b405c3 chore(deps): update grafana/grafana docker tag to v12.4.3 (#28238)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-05 12:51:18 +02:00
Ben Beckford 0058df798d fix(mobile): show lens info without lens name (#28234)
* fix(mobile): always display lens subtitle

* fix(mobile): hide empty lens subtitles
2026-05-05 10:46:09 +07:00
shenlong 97100a4362 refactor: app metadata (#28113)
* refactor: app metadata

* refactor to per row store

* cleanup

* more test

* review changes

* more refactor

* refactor

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-05-05 10:45:51 +07:00
Alex af39384efb chore: better contrast for highlighted button on control bar (#28217) 2026-05-04 09:39:37 -05:00
Mert 01712cf0a7 fix(server): av typing (#28223)
* fix av typing, move fixtures to stub file

* fix tests
2026-05-04 09:04:29 -04:00
Michel Heusschen 2015f95ff5 fix(web): correct timeline yesterday label across month boundaries (#28183) 2026-05-04 13:46:11 +02:00
Timon d4f29ab6ac fix(server): validate duplicate group ownership before dismissal (#28221) 2026-05-04 12:51:54 +02:00
Timon 3decc864b5 refactor(server)!: structured validation error responses (#28204)
* refactor(server)!: structured validation error responses

* refactor(server): clarify comment on removing duplicate HTTP response fields

* enhance validation error tests

* make path and message required

* fmt

* fix e2e test

* fmt

* feat: enhance error handling in getServerErrorMessage function
2026-05-04 00:00:03 -04:00
David Allen eca0e60db8 fix: librknnrt permissions in machine-learning (#28216)
fix librknnrt permissions in machine-learning
2026-05-03 23:39:27 +00:00
AyaanMAG 8cff5883b5 fix(ml): respect time zone for logs in cuda container (#28155) 2026-05-03 04:19:56 +00:00
234 changed files with 17723 additions and 2502 deletions
+34 -10
View File
@@ -51,14 +51,14 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }} should_run: ${{ steps.check.outputs.should_run }}
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Check what should run - name: Check what should run
id: check id: check
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3 uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
with: with:
github-token: ${{ steps.token.outputs.token }} github-token: ${{ steps.token.outputs.token }}
filters: | filters: |
@@ -73,24 +73,25 @@ jobs:
needs: pre-job needs: pre-job
permissions: permissions:
contents: read contents: read
# Skip when PR from a fork pull-requests: write
if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' && fromJSON(needs.pre-job.outputs.should_run).mobile == true }} if: ${{ github.actor != 'dependabot[bot]' && fromJSON(needs.pre-job.outputs.should_run).mobile == true }}
runs-on: mich runs-on: mich
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
ref: ${{ inputs.ref || github.sha }} ref: ${{ inputs.ref }}
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Create the Keystore - name: Create the Keystore
if: ${{ !github.event.pull_request.head.repo.fork }}
env: env:
KEY_JKS: ${{ secrets.KEY_JKS }} KEY_JKS: ${{ secrets.KEY_JKS }}
working-directory: ./mobile working-directory: ./mobile
@@ -144,20 +145,43 @@ jobs:
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }} ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
IS_MAIN: ${{ github.ref == 'refs/heads/main' }} IS_MAIN: ${{ github.ref == 'refs/heads/main' }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: | run: |
if [[ $IS_MAIN == 'true' ]]; then if [[ $IS_MAIN == 'true' ]]; then
flutter build apk --release flutter build apk --release
flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64 flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64
else else
flutter build apk --debug --split-per-abi --target-platform android-arm64 flutter build apk --release
fi fi
- name: Publish Android Artifact - name: Publish Android Artifact
id: upload-apk
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: release-apk-signed name: release-apk-signed
path: mobile/build/app/outputs/flutter-apk/*.apk path: mobile/build/app/outputs/flutter-apk/*.apk
- name: Comment APK download link on PR
if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }}
uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
APK_URL: ${{ steps.upload-apk.outputs.artifact-url }}
with:
github-token: ${{ steps.token.outputs.token }}
message-id: 'mobile-android-apk'
message: |
📱 **Android release APK (universal)** — `${{ env.HEAD_SHA }}`
Download: ${{ env.APK_URL }}
<details>
<summary>QR code</summary>
<img src="https://api.qrserver.com/v1/create-qr-code/?size=240x240&data=${{ env.APK_URL }}" alt="QR code" />
</details>
Installs as a separate app (applicationId `app.alextran.immich.pr${{ github.event.pull_request.number }}`), so it coexists with the Play Store version and any other PR builds.
- name: Save Gradle Cache - name: Save Gradle Cache
id: cache-gradle-save id: cache-gradle-save
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
@@ -210,7 +234,7 @@ jobs:
working-directory: ./mobile working-directory: ./mobile
- name: Setup Ruby - name: Setup Ruby
uses: ruby/setup-ruby@7372622e62b60b3cb750dcd2b9e32c247ffec26a # v1.302.0 uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
with: with:
ruby-version: '3.3' ruby-version: '3.3'
bundler-cache: true bundler-cache: true
+2 -2
View File
@@ -19,9 +19,9 @@ jobs:
actions: write actions: write
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Check out code - name: Check out code
+1 -1
View File
@@ -24,7 +24,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@f8cb9308b42121e793f835bd14c0b8090420430c # v0.0.39 uses: oasdiff/oasdiff-action/breaking@37bf9ff785c7315df88216660826e71be4cc03da # v0.0.44
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
+11 -21
View File
@@ -31,35 +31,25 @@ jobs:
working-directory: ./cli working-directory: ./cli
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup pnpm - name: Setup Mise
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with: with:
node-version-file: './cli/.nvmrc' github_token: ${{ steps.token.outputs.token }}
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Setup typescript-sdk - name: Publish
run: pnpm install && pnpm run build
working-directory: ./open-api/typescript-sdk
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: pnpm publish --provenance --no-git-checks
if: ${{ github.event_name == 'release' }} if: ${{ github.event_name == 'release' }}
run: mise run ci-publish
docker: docker:
name: Docker name: Docker
@@ -71,9 +61,9 @@ jobs:
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Checkout - name: Checkout
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
needs: [get_body, should_run] needs: [get_body, should_run]
if: ${{ needs.should_run.outputs.should_run == 'true' }} if: ${{ needs.should_run.outputs.should_run == 'true' }}
container: container:
image: ghcr.io/immich-app/mdq:main@sha256:557cca601891b8b7d78b940071d35aaf7aaeb9b327d19b22cf282118edbc5272 image: ghcr.io/immich-app/mdq:main@sha256:32abe582452b12dff55055e1d6bc24508a8f17164f9d1831db7bb70953c014c6
outputs: outputs:
checked: ${{ steps.get_checkbox.outputs.checked }} checked: ${{ steps.get_checkbox.outputs.checked }}
steps: steps:
+2 -2
View File
@@ -44,9 +44,9 @@ jobs:
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Checkout repository - name: Checkout repository
+7 -7
View File
@@ -23,14 +23,14 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }} should_run: ${{ steps.check.outputs.should_run }}
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Check what should run - name: Check what should run
id: check id: check
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3 uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
with: with:
github-token: ${{ steps.token.outputs.token }} github-token: ${{ steps.token.outputs.token }}
filters: | filters: |
@@ -132,7 +132,7 @@ jobs:
suffixes: '-rocm' suffixes: '-rocm'
platforms: linux/amd64 platforms: linux/amd64
runner-mapping: '{"linux/amd64": "pokedex-large"}' runner-mapping: '{"linux/amd64": "pokedex-large"}'
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@61a0fc2b41524edcc7c9fffb8bb178e6b0ccf21d # multi-runner-build-workflow-v2.3.0 uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@5813c7c4f7016c748ae7ac5d5f684846649d4d20 # multi-runner-build-workflow-v2.4.0
permissions: permissions:
contents: read contents: read
actions: read actions: read
@@ -155,7 +155,7 @@ jobs:
name: Build and Push Server name: Build and Push Server
needs: pre-job needs: pre-job
if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }} if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }}
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@61a0fc2b41524edcc7c9fffb8bb178e6b0ccf21d # multi-runner-build-workflow-v2.3.0 uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@5813c7c4f7016c748ae7ac5d5f684846649d4d20 # multi-runner-build-workflow-v2.4.0
permissions: permissions:
contents: read contents: read
actions: read actions: read
@@ -178,7 +178,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: always() if: always()
steps: steps:
- uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5 - uses: immich-app/devtools/actions/success-check@81113db03f6d743efee81e0058c0b43f6cd6f36d # success-check-action-v0.0.6
with: with:
needs: ${{ toJSON(needs) }} needs: ${{ toJSON(needs) }}
@@ -189,6 +189,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: always() if: always()
steps: steps:
- uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5 - uses: immich-app/devtools/actions/success-check@81113db03f6d743efee81e0058c0b43f6cd6f36d # success-check-action-v0.0.6
with: with:
needs: ${{ toJSON(needs) }} needs: ${{ toJSON(needs) }}
+8 -14
View File
@@ -21,14 +21,14 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }} should_run: ${{ steps.check.outputs.should_run }}
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Check what should run - name: Check what should run
id: check id: check
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3 uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
with: with:
github-token: ${{ steps.token.outputs.token }} github-token: ${{ steps.token.outputs.token }}
filters: | filters: |
@@ -54,9 +54,9 @@ jobs:
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Checkout code - name: Checkout code
@@ -64,17 +64,11 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
fetch-depth: 0
- name: Setup pnpm - name: Setup Mise
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with: with:
node-version-file: './docs/.nvmrc' github_token: ${{ steps.token.outputs.token }}
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run install - name: Run install
run: pnpm install run: pnpm install
+7 -5
View File
@@ -20,9 +20,9 @@ jobs:
artifact: ${{ steps.get-artifact.outputs.result }} artifact: ${{ steps.get-artifact.outputs.result }}
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- if: ${{ github.event.workflow_run.conclusion != 'success' }} - if: ${{ github.event.workflow_run.conclusion != 'success' }}
@@ -119,9 +119,9 @@ jobs:
if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }} if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }}
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Checkout code - name: Checkout code
@@ -131,7 +131,9 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - name: Setup Mise
uses: immich-app/devtools/actions/use-mise@035e80a7d4355d5f087ffb95db9e4a0944c04e56 # use-mise-action-v1.1.3 uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
github_token: ${{ steps.token.outputs.token }}
- name: Load parameters - name: Load parameters
id: parameters id: parameters
+5 -3
View File
@@ -17,9 +17,9 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Checkout code - name: Checkout code
@@ -29,7 +29,9 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - name: Setup Mise
uses: immich-app/devtools/actions/use-mise@035e80a7d4355d5f087ffb95db9e4a0944c04e56 # use-mise-action-v1.1.3 uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
github_token: ${{ steps.token.outputs.token }}
- name: Destroy Docs Subdomain - name: Destroy Docs Subdomain
env: env:
+8 -14
View File
@@ -14,29 +14,23 @@ jobs:
contents: write contents: write
pull-requests: write pull-requests: write
steps: steps:
- name: Generate a token - id: token
id: generate-token uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: 'Checkout' - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
ref: ${{ github.event.pull_request.head.ref }} ref: ${{ github.event.pull_request.head.ref }}
token: ${{ steps.generate-token.outputs.token }}
persist-credentials: true persist-credentials: true
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm - name: Setup Mise
uses: pnpm/action-setup@08c4be7e2e672a47d11bd04269e27e5f3e8529cb # v6.0.0 uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with: with:
node-version-file: './server/.nvmrc' github_token: ${{ steps.token.outputs.token }}
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Fix formatting - name: Fix formatting
run: pnpm --recursive install && pnpm run --recursive --if-present --parallel format:fix run: pnpm --recursive install && pnpm run --recursive --if-present --parallel format:fix
+2 -2
View File
@@ -4,7 +4,7 @@ on:
workflow_dispatch: workflow_dispatch:
workflow_call: workflow_call:
secrets: secrets:
PUSH_O_MATIC_APP_ID: PUSH_O_MATIC_APP_CLIENT_ID:
required: true required: true
PUSH_O_MATIC_APP_KEY: PUSH_O_MATIC_APP_KEY:
required: true required: true
@@ -33,7 +33,7 @@ jobs:
if: ${{ inputs.skip != true }} if: ${{ inputs.skip != true }}
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Find translation PR - name: Find translation PR
+2 -2
View File
@@ -14,9 +14,9 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Require PR to have a changelog label - name: Require PR to have a changelog label
+2 -2
View File
@@ -12,9 +12,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
+14 -19
View File
@@ -36,7 +36,7 @@ jobs:
permissions: permissions:
pull-requests: write pull-requests: write
secrets: secrets:
PUSH_O_MATIC_APP_ID: ${{ secrets.PUSH_O_MATIC_APP_ID }} PUSH_O_MATIC_APP_CLIENT_ID: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
PUSH_O_MATIC_APP_KEY: ${{ secrets.PUSH_O_MATIC_APP_KEY }} PUSH_O_MATIC_APP_KEY: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
WEBLATE_TOKEN: ${{ secrets.WEBLATE_TOKEN }} WEBLATE_TOKEN: ${{ secrets.WEBLATE_TOKEN }}
@@ -48,32 +48,27 @@ jobs:
version: ${{ steps.output.outputs.version }} version: ${{ steps.output.outputs.version }}
permissions: {} # No job-level permissions are needed because it uses the app-token permissions: {} # No job-level permissions are needed because it uses the app-token
steps: steps:
- name: Generate a token - id: token
id: generate-token uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Checkout - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
token: ${{ steps.generate-token.outputs.token }} token: ${{ steps.token.outputs.token }}
persist-credentials: true persist-credentials: true
ref: main ref: main
- name: Install uv - name: Setup Mise
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
- name: Setup pnpm
uses: pnpm/action-setup@08c4be7e2e672a47d11bd04269e27e5f3e8529cb # v6.0.0
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with: with:
node-version-file: './server/.nvmrc' github_token: ${{ steps.token.outputs.token }}
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml' # TODO move to mise
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
- name: Bump version - name: Bump version
env: env:
@@ -126,7 +121,7 @@ jobs:
id: generate-token id: generate-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Checkout - name: Checkout
+7 -7
View File
@@ -14,12 +14,12 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- uses: mshick/add-pr-comment@64b8e914979889d746c99dea15a76e77ef64580a # v3.10.0 - uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
with: with:
github-token: ${{ steps.token.outputs.token }} github-token: ${{ steps.token.outputs.token }}
message-id: 'preview-status' message-id: 'preview-status'
@@ -32,9 +32,9 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
@@ -48,14 +48,14 @@ jobs:
name: 'preview' name: 'preview'
}) })
- uses: mshick/add-pr-comment@64b8e914979889d746c99dea15a76e77ef64580a # v3.10.0 - uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
if: ${{ github.event.pull_request.head.repo.fork }} if: ${{ github.event.pull_request.head.repo.fork }}
with: with:
github-token: ${{ steps.token.outputs.token }} github-token: ${{ steps.token.outputs.token }}
message-id: 'preview-status' message-id: 'preview-status'
message: 'PRs from forks cannot have preview environments.' message: 'PRs from forks cannot have preview environments.'
- uses: mshick/add-pr-comment@64b8e914979889d746c99dea15a76e77ef64580a # v3.10.0 - uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
if: ${{ !github.event.pull_request.head.repo.fork }} if: ${{ !github.event.pull_request.head.repo.fork }}
with: with:
github-token: ${{ steps.token.outputs.token }} github-token: ${{ steps.token.outputs.token }}
+10 -12
View File
@@ -19,29 +19,27 @@ jobs:
working-directory: ./open-api/typescript-sdk working-directory: ./open-api/typescript-sdk
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup pnpm - name: Setup Mise
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with: with:
node-version-file: './open-api/typescript-sdk/.nvmrc' github_token: ${{ steps.token.outputs.token }}
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Install deps - name: Install deps
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Build - name: Build
run: pnpm build run: pnpm build
- name: Publish - name: Publish
run: pnpm publish --provenance --no-git-checks run: pnpm publish --provenance --no-git-checks
+5 -5
View File
@@ -20,14 +20,14 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }} should_run: ${{ steps.check.outputs.should_run }}
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Check what should run - name: Check what should run
id: check id: check
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3 uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
with: with:
github-token: ${{ steps.token.outputs.token }} github-token: ${{ steps.token.outputs.token }}
filters: | filters: |
@@ -49,9 +49,9 @@ jobs:
working-directory: ./mobile working-directory: ./mobile
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Checkout code - name: Checkout code
+178 -204
View File
@@ -17,14 +17,14 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }} should_run: ${{ steps.check.outputs.should_run }}
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Check what should run - name: Check what should run
id: check id: check
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3 uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
with: with:
github-token: ${{ steps.token.outputs.token }} github-token: ${{ steps.token.outputs.token }}
filters: | filters: |
@@ -34,13 +34,17 @@ jobs:
- 'web/**' - 'web/**'
- 'i18n/**' - 'i18n/**'
- 'open-api/typescript-sdk/**' - 'open-api/typescript-sdk/**'
- 'pnpm-lock.yaml'
server: server:
- 'server/**' - 'server/**'
- 'pnpm-lock.yaml'
cli: cli:
- 'cli/**' - 'cli/**'
- 'open-api/typescript-sdk/**' - 'open-api/typescript-sdk/**'
- 'pnpm-lock.yaml'
e2e: e2e:
- 'e2e/**' - 'e2e/**'
- 'pnpm-lock.yaml'
mobile: mobile:
- 'mobile/**' - 'mobile/**'
machine-learning: machine-learning:
@@ -63,9 +67,9 @@ jobs:
working-directory: ./server working-directory: ./server
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Checkout code - name: Checkout code
@@ -74,28 +78,14 @@ jobs:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup pnpm - name: Setup Mise
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with: with:
node-version-file: './server/.nvmrc' github_token: ${{ steps.token.outputs.token }}
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml' - name: Run ci-unit
- name: Run package manager install run: mise run ci-unit
run: pnpm install
- name: Run linter
run: pnpm lint
if: ${{ !cancelled() }}
- name: Run formatter
run: pnpm format
if: ${{ !cancelled() }}
- name: Run tsc
run: pnpm check
if: ${{ !cancelled() }}
- name: Run small tests & coverage
run: pnpm test
if: ${{ !cancelled() }}
cli-unit-tests: cli-unit-tests:
name: Unit Test CLI name: Unit Test CLI
needs: pre-job needs: pre-job
@@ -108,9 +98,9 @@ jobs:
working-directory: ./cli working-directory: ./cli
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Checkout code - name: Checkout code
@@ -118,31 +108,15 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - name: Setup Mise
- name: Setup Node uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with: with:
node-version-file: './cli/.nvmrc' github_token: ${{ steps.token.outputs.token }}
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml' - name: Run ci-unit
- name: Setup typescript-sdk run: mise run ci-unit
run: pnpm install && pnpm run build
working-directory: ./open-api/typescript-sdk
- name: Install deps
run: pnpm install
- name: Run linter
run: pnpm lint
if: ${{ !cancelled() }}
- name: Run formatter
run: pnpm format
if: ${{ !cancelled() }}
- name: Run tsc
run: pnpm check
if: ${{ !cancelled() }}
- name: Run unit tests & coverage
run: pnpm test
if: ${{ !cancelled() }}
cli-unit-tests-win: cli-unit-tests-win:
name: Unit Test CLI (Windows) name: Unit Test CLI (Windows)
needs: pre-job needs: pre-job
@@ -155,9 +129,9 @@ jobs:
working-directory: ./cli working-directory: ./cli
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Checkout code - name: Checkout code
@@ -165,26 +139,33 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version-file: './cli/.nvmrc' node-version-file: '.nvmrc'
cache: 'pnpm' cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml' cache-dependency-path: '**/pnpm-lock.yaml'
- name: Setup typescript-sdk - name: Setup typescript-sdk
run: pnpm install --frozen-lockfile && pnpm build run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk working-directory: ./open-api/typescript-sdk
- name: Install deps - name: Install deps
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
# Skip linter & formatter in Windows test. # Skip linter & formatter in Windows test.
- name: Run tsc - name: Run tsc
run: pnpm check run: pnpm check
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run unit tests & coverage - name: Run unit tests & coverage
run: pnpm test run: pnpm test
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
web-lint: web-lint:
name: Lint Web name: Lint Web
needs: pre-job needs: pre-job
@@ -197,9 +178,9 @@ jobs:
working-directory: ./web working-directory: ./web
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Checkout code - name: Checkout code
@@ -207,28 +188,22 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - name: Setup Mise
- name: Setup Node uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with: with:
node-version-file: './web/.nvmrc' github_token: ${{ steps.token.outputs.token }}
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run setup typescript-sdk - name: Run setup typescript-sdk
run: pnpm install --frozen-lockfile && pnpm build run: mise run //:sdk:install && mise run //:sdk:build
working-directory: ./open-api/typescript-sdk
- name: Run pnpm install - name: Run pnpm install
run: pnpm rebuild && pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Run linter - name: Run linter
run: pnpm lint run: pnpm lint
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run formatter
run: pnpm format
if: ${{ !cancelled() }}
- name: Run svelte checks
run: pnpm check:svelte
if: ${{ !cancelled() }}
web-unit-tests: web-unit-tests:
name: Test Web name: Test Web
needs: pre-job needs: pre-job
@@ -241,9 +216,9 @@ jobs:
working-directory: ./web working-directory: ./web
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Checkout code - name: Checkout code
@@ -251,25 +226,15 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - name: Setup Mise
- name: Setup Node uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with: with:
node-version-file: './web/.nvmrc' github_token: ${{ steps.token.outputs.token }}
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml' - name: Run ci-unit
- name: Run setup typescript-sdk run: mise run ci-unit
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk
- name: Run npm install
run: pnpm install --frozen-lockfile
- name: Run tsc
run: pnpm check:typescript
if: ${{ !cancelled() }}
- name: Run unit tests & coverage
run: pnpm test
if: ${{ !cancelled() }}
i18n-tests: i18n-tests:
name: Test i18n name: Test i18n
needs: pre-job needs: pre-job
@@ -279,9 +244,9 @@ jobs:
contents: read contents: read
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Checkout code - name: Checkout code
@@ -289,24 +254,25 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - name: Setup Mise
- name: Setup Node uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with: with:
node-version-file: './web/.nvmrc' github_token: ${{ steps.token.outputs.token }}
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Install dependencies - name: Install dependencies
run: pnpm --filter=immich-i18n install --frozen-lockfile run: pnpm --filter=immich-i18n install --frozen-lockfile
- name: Format - name: Format
run: pnpm --filter=immich-i18n format:fix run: pnpm --filter=immich-i18n format:fix
- name: Find file changes - name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
id: verify-changed-files id: verify-changed-files
with: with:
files: | files: |
i18n/** i18n/**
- name: Verify files have not changed - name: Verify files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true' if: steps.verify-changed-files.outputs.files_changed == 'true'
env: env:
@@ -315,6 +281,7 @@ jobs:
echo "ERROR: i18n files not up to date!" echo "ERROR: i18n files not up to date!"
echo "Changed files: ${CHANGED_FILES}" echo "Changed files: ${CHANGED_FILES}"
exit 1 exit 1
e2e-tests-lint: e2e-tests-lint:
name: End-to-End Lint name: End-to-End Lint
needs: pre-job needs: pre-job
@@ -327,9 +294,9 @@ jobs:
working-directory: ./e2e working-directory: ./e2e
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Checkout code - name: Checkout code
@@ -337,30 +304,16 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - name: Setup Mise
- name: Setup Node uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with: with:
node-version-file: './e2e/.nvmrc' github_token: ${{ steps.token.outputs.token }}
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml' - name: Run ci-unit
- name: Run setup typescript-sdk run: mise run ci-unit
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk
if: ${{ !cancelled() }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }}
- name: Run linter
run: pnpm lint
if: ${{ !cancelled() }}
- name: Run formatter
run: pnpm format
if: ${{ !cancelled() }}
- name: Run tsc
run: pnpm check
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
server-medium-tests: server-medium-tests:
name: Medium Tests (Server) name: Medium Tests (Server)
needs: pre-job needs: pre-job
@@ -373,9 +326,9 @@ jobs:
working-directory: ./server working-directory: ./server
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Checkout code - name: Checkout code
@@ -384,21 +337,16 @@ jobs:
persist-credentials: false persist-credentials: false
submodules: 'recursive' submodules: 'recursive'
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: './server/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Setup Mise - name: Setup Mise
uses: immich-app/devtools/actions/use-mise@035e80a7d4355d5f087ffb95db9e4a0944c04e56 # use-mise-action-v1.1.3 uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
- name: Run pnpm install with:
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile github_token: ${{ steps.token.outputs.token }}
- name: Run medium tests
run: pnpm test:medium - name: Run ci-medium
run: mise run ci-medium
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
e2e-tests-server-cli: e2e-tests-server-cli:
name: End-to-End Tests (Server & CLI) name: End-to-End Tests (Server & CLI)
needs: pre-job needs: pre-job
@@ -414,9 +362,9 @@ jobs:
runner: [ubuntu-latest, ubuntu-24.04-arm] runner: [ubuntu-latest, ubuntu-24.04-arm]
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Checkout code - name: Checkout code
@@ -425,52 +373,63 @@ jobs:
persist-credentials: false persist-credentials: false
submodules: 'recursive' submodules: 'recursive'
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version-file: './e2e/.nvmrc' node-version-file: '.nvmrc'
cache: 'pnpm' cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml' cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run setup typescript-sdk
- name: Setup typescript-sdk
run: pnpm install --frozen-lockfile && pnpm build run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk working-directory: ./open-api/typescript-sdk
if: ${{ !cancelled() }}
- name: Run setup web - name: Run setup web
run: pnpm install --frozen-lockfile && pnpm exec svelte-kit sync run: pnpm install --frozen-lockfile && pnpm exec svelte-kit sync
working-directory: ./web working-directory: ./web
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run setup cli - name: Run setup cli
run: pnpm install --frozen-lockfile && pnpm build run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./cli working-directory: ./cli
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Start Docker Compose - name: Start Docker Compose
run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300 run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run e2e tests (api & cli) - name: Run e2e tests (api & cli)
env: env:
VITEST_DISABLE_DOCKER_SETUP: true VITEST_DISABLE_DOCKER_SETUP: true
run: pnpm test run: pnpm test
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run e2e tests (maintenance) - name: Run e2e tests (maintenance)
env: env:
VITEST_DISABLE_DOCKER_SETUP: true VITEST_DISABLE_DOCKER_SETUP: true
run: pnpm test:maintenance run: pnpm test:maintenance
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Capture Docker logs - name: Capture Docker logs
if: always() if: always()
run: docker compose logs --no-color > docker-compose-logs.txt run: docker compose logs --no-color > docker-compose-logs.txt
working-directory: ./e2e working-directory: ./e2e
- name: Archive Docker logs - name: Archive Docker logs
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always() if: always()
with: with:
name: e2e-server-docker-logs-${{ matrix.runner }} name: e2e-server-docker-logs-${{ matrix.runner }}
path: e2e/docker-compose-logs.txt path: e2e/docker-compose-logs.txt
e2e-tests-web: e2e-tests-web:
name: End-to-End Tests (Web) name: End-to-End Tests (Web)
needs: pre-job needs: pre-job
@@ -486,9 +445,9 @@ jobs:
runner: [ubuntu-latest, ubuntu-24.04-arm] runner: [ubuntu-latest, ubuntu-24.04-arm]
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Checkout code - name: Checkout code
@@ -497,70 +456,85 @@ jobs:
persist-credentials: false persist-credentials: false
submodules: 'recursive' submodules: 'recursive'
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version-file: './e2e/.nvmrc' node-version-file: '.nvmrc'
cache: 'pnpm' cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml' cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run setup typescript-sdk - name: Run setup typescript-sdk
run: pnpm install --frozen-lockfile && pnpm build run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk working-directory: ./open-api/typescript-sdk
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Install Playwright Browsers - name: Install Playwright Browsers
run: pnpm exec playwright install chromium --only-shell run: pnpm exec playwright install chromium --only-shell
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Docker build - name: Docker build
run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300 run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run e2e tests (web) - name: Run e2e tests (web)
env: env:
PLAYWRIGHT_DISABLE_WEBSERVER: true PLAYWRIGHT_DISABLE_WEBSERVER: true
run: pnpm test:web run: pnpm test:web
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Archive e2e test (web) results - name: Archive e2e test (web) results
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: success() || failure() if: success() || failure()
with: with:
name: e2e-web-test-results-${{ matrix.runner }} name: e2e-web-test-results-${{ matrix.runner }}
path: e2e/playwright-report/ path: e2e/playwright-report/
- name: Run ui tests (web) - name: Run ui tests (web)
env: env:
PLAYWRIGHT_DISABLE_WEBSERVER: true PLAYWRIGHT_DISABLE_WEBSERVER: true
run: pnpm test:web:ui run: pnpm test:web:ui
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Archive ui test (web) results - name: Archive ui test (web) results
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: success() || failure() if: success() || failure()
with: with:
name: e2e-ui-test-results-${{ matrix.runner }} name: e2e-ui-test-results-${{ matrix.runner }}
path: e2e/playwright-report/ path: e2e/playwright-report/
- name: Run maintenance tests - name: Run maintenance tests
env: env:
PLAYWRIGHT_DISABLE_WEBSERVER: true PLAYWRIGHT_DISABLE_WEBSERVER: true
run: pnpm test:web:maintenance run: pnpm test:web:maintenance
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Archive maintenance tests (web) results - name: Archive maintenance tests (web) results
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: success() || failure() if: success() || failure()
with: with:
name: e2e-maintenance-isolated-test-results-${{ matrix.runner }} name: e2e-maintenance-isolated-test-results-${{ matrix.runner }}
path: e2e/playwright-report/ path: e2e/playwright-report/
- name: Capture Docker logs - name: Capture Docker logs
if: always() if: always()
run: docker compose logs --no-color > docker-compose-logs.txt run: docker compose logs --no-color > docker-compose-logs.txt
working-directory: ./e2e working-directory: ./e2e
- name: Archive Docker logs - name: Archive Docker logs
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always() if: always()
with: with:
name: e2e-web-docker-logs-${{ matrix.runner }} name: e2e-web-docker-logs-${{ matrix.runner }}
path: e2e/docker-compose-logs.txt path: e2e/docker-compose-logs.txt
success-check-e2e: success-check-e2e:
name: End-to-End Tests Success name: End-to-End Tests Success
needs: [e2e-tests-server-cli, e2e-tests-web] needs: [e2e-tests-server-cli, e2e-tests-web]
@@ -568,7 +542,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: always() if: always()
steps: steps:
- uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5 - uses: immich-app/devtools/actions/success-check@81113db03f6d743efee81e0058c0b43f6cd6f36d # success-check-action-v0.0.6
with: with:
needs: ${{ toJSON(needs) }} needs: ${{ toJSON(needs) }}
mobile-unit-tests: mobile-unit-tests:
@@ -580,9 +554,9 @@ jobs:
contents: read contents: read
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -612,34 +586,24 @@ jobs:
working-directory: ./machine-learning working-directory: ./machine-learning
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Install uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with: with:
python-version: 3.11 github_token: ${{ steps.token.outputs.token }}
- name: Install dependencies
run: | - name: Run ci-unit
uv sync --extra cpu run: mise run ci-unit
- name: Lint with ruff
run: |
uv run ruff check --output-format=github immich_ml
- name: Format with ruff
run: |
uv run ruff format --check immich_ml
- name: Run mypy type checking
run: |
uv run mypy --strict immich_ml/
- name: Run tests and coverage
run: |
uv run pytest --cov=immich_ml --cov-report term-missing
github-files-formatting: github-files-formatting:
name: .github Files Formatting name: .github Files Formatting
needs: pre-job needs: pre-job
@@ -652,9 +616,9 @@ jobs:
working-directory: ./.github working-directory: ./.github
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Checkout code - name: Checkout code
@@ -662,19 +626,19 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - name: Setup Mise
- name: Setup Node uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with: with:
node-version-file: './.github/.nvmrc' github_token: ${{ steps.token.outputs.token }}
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run pnpm install - name: Run pnpm install
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Run formatter - name: Run formatter
run: pnpm format run: pnpm format
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
shellcheck: shellcheck:
name: ShellCheck name: ShellCheck
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -682,9 +646,9 @@ jobs:
contents: read contents: read
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -703,9 +667,9 @@ jobs:
contents: read contents: read
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Checkout code - name: Checkout code
@@ -713,21 +677,22 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - name: Setup Mise
- name: Setup Node uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with: with:
node-version-file: './server/.nvmrc' github_token: ${{ steps.token.outputs.token }}
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Install server dependencies - name: Install server dependencies
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter immich install --frozen-lockfile run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter immich install --frozen-lockfile
- name: Build the app - name: Build the app
run: pnpm --filter immich build run: pnpm --filter immich build
- name: Run API generation - name: Run API generation
run: ./bin/generate-open-api.sh run: ./bin/generate-open-api.sh
working-directory: open-api working-directory: open-api
- name: Find file changes - name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
id: verify-changed-files id: verify-changed-files
@@ -736,6 +701,7 @@ jobs:
mobile/openapi mobile/openapi
open-api/typescript-sdk open-api/typescript-sdk
open-api/immich-openapi-specs.json open-api/immich-openapi-specs.json
- name: Verify files have not changed - name: Verify files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true' if: steps.verify-changed-files.outputs.files_changed == 'true'
env: env:
@@ -744,6 +710,7 @@ jobs:
echo "ERROR: Generated files not up to date!" echo "ERROR: Generated files not up to date!"
echo "Changed files: ${CHANGED_FILES}" echo "Changed files: ${CHANGED_FILES}"
exit 1 exit 1
sql-schema-up-to-date: sql-schema-up-to-date:
name: SQL Schema Checks name: SQL Schema Checks
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -765,9 +732,9 @@ jobs:
working-directory: ./server working-directory: ./server
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Checkout code - name: Checkout code
@@ -775,31 +742,35 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - name: Setup Mise
- name: Setup Node uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with: with:
node-version-file: './server/.nvmrc' github_token: ${{ steps.token.outputs.token }}
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Install server dependencies - name: Install server dependencies
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile
- name: Build the app - name: Build the app
run: pnpm build run: pnpm build
- name: Run existing migrations - name: Run existing migrations
run: pnpm migrations:run run: pnpm migrations:run
- name: Test npm run schema:reset command works - name: Test npm run schema:reset command works
run: pnpm schema:reset run: pnpm schema:reset
- name: Generate new migrations - name: Generate new migrations
continue-on-error: true continue-on-error: true
run: pnpm migrations:generate src/TestMigration run: pnpm migrations:generate src/TestMigration
- name: Find file changes - name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
id: verify-changed-files id: verify-changed-files
with: with:
files: | files: |
server/src server/src
- name: Verify migration files have not changed - name: Verify migration files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true' if: steps.verify-changed-files.outputs.files_changed == 'true'
env: env:
@@ -809,16 +780,19 @@ jobs:
echo "Changed files: ${CHANGED_FILES}" echo "Changed files: ${CHANGED_FILES}"
cat ./src/*-TestMigration.ts cat ./src/*-TestMigration.ts
exit 1 exit 1
- name: Run SQL generation - name: Run SQL generation
run: pnpm sync:sql run: pnpm sync:sql
env: env:
DB_URL: postgres://postgres:postgres@localhost:5432/immich DB_URL: postgres://postgres:postgres@localhost:5432/immich
- name: Find file changes - name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
id: verify-changed-sql-files id: verify-changed-sql-files
with: with:
files: | files: |
server/src/queries server/src/queries
- name: Verify SQL files have not changed - name: Verify SQL files have not changed
if: steps.verify-changed-sql-files.outputs.files_changed == 'true' if: steps.verify-changed-sql-files.outputs.files_changed == 'true'
env: env:
+6 -6
View File
@@ -24,14 +24,14 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }} should_run: ${{ steps.check.outputs.should_run }}
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Check what should run - name: Check what should run
id: check id: check
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3 uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
with: with:
github-token: ${{ steps.token.outputs.token }} github-token: ${{ steps.token.outputs.token }}
filters: | filters: |
@@ -47,9 +47,9 @@ jobs:
if: ${{ fromJSON(needs.pre-job.outputs.should_run).i18n == true }} if: ${{ fromJSON(needs.pre-job.outputs.should_run).i18n == true }}
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2 uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_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 }}
- name: Bot review status - name: Bot review status
@@ -68,6 +68,6 @@ jobs:
permissions: {} permissions: {}
if: always() if: always()
steps: steps:
- uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5 - uses: immich-app/devtools/actions/success-check@81113db03f6d743efee81e0058c0b43f6cd6f36d # success-check-action-v0.0.6
with: with:
needs: ${{ toJSON(needs) }} needs: ${{ toJSON(needs) }}
View File
+3
View File
@@ -29,6 +29,9 @@
"editor.formatOnSave": true, "editor.formatOnSave": true,
"tailwindCSS.lint.suggestCanonicalClasses": "ignore" "tailwindCSS.lint.suggestCanonicalClasses": "ignore"
}, },
"svelte.plugin.svelte.compilerWarnings": {
"state_referenced_locally": "ignore"
},
"[typescript]": { "[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true "editor.formatOnSave": true
-1
View File
@@ -1 +0,0 @@
24.15.0
+24 -1
View File
@@ -7,7 +7,7 @@ run = "vite build"
[tasks.test] [tasks.test]
env._.path = "./node_modules/.bin" env._.path = "./node_modules/.bin"
run = "vite" run = "vitest"
[tasks.lint] [tasks.lint]
env._.path = "./node_modules/.bin" env._.path = "./node_modules/.bin"
@@ -27,3 +27,26 @@ run = "prettier --write ."
[tasks.check] [tasks.check]
env._.path = "./node_modules/.bin" env._.path = "./node_modules/.bin"
run = "tsc --noEmit" run = "tsc --noEmit"
[tasks.ci-publish]
depends = ["//:sdk:install", "//:sdk:build"]
run = [
{ task = ":install" },
{ task = ":build" },
"pnpm publish --provenance --no-git-checks",
]
[tasks.ci-unit]
depends = ["//:sdk:install", "//:sdk:build"]
run = [
{ task = ":install" },
{ task = ":format" },
{ task = ":lint" },
{ task = ":check" },
{ task = ":test --run" },
]
[tasks.checklist]
run = [
{ task = ":ci-unit" },
]
-3
View File
@@ -66,8 +66,5 @@
"fastq": "^1.17.1", "fastq": "^1.17.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"micromatch": "^4.0.8" "micromatch": "^4.0.8"
},
"volta": {
"node": "24.15.0"
} }
} }
+1 -1
View File
@@ -1,5 +1,5 @@
[tools] [tools]
terragrunt = "1.0.2" terragrunt = "1.0.3"
opentofu = "1.11.6" opentofu = "1.11.6"
[tasks."tg:fmt"] [tasks."tg:fmt"]
+1 -1
View File
@@ -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.2-ubuntu@sha256:78839fe49e1425c02416fa8072591533a72bd9598e563b54a07d78f9e27fb5d3 image: grafana/grafana:12.4.3-ubuntu@sha256:ca3f764fdc48cebdf22dd206f33ecb0795a9a7210eacd1b5c02204aebd78b223
volumes: volumes:
- grafana-data:/var/lib/grafana - grafana-data:/var/lib/grafana
-3
View File
@@ -95,6 +95,3 @@ services:
restart: always restart: always
healthcheck: healthcheck:
disable: false disable: false
volumes:
model-cache:
-1
View File
@@ -1 +0,0 @@
24.15.0
+2
View File
@@ -50,6 +50,8 @@ Some basic examples:
- `**/Raw/**` will exclude all files in any directory named `Raw` - `**/Raw/**` will exclude all files in any directory named `Raw`
- `**/*.{tif,jpg}` will exclude all files with the extension `.tif` or `.jpg` - `**/*.{tif,jpg}` will exclude all files with the extension `.tif` or `.jpg`
Note that `*` is a wildcard matching zero or more characters (i.e., withinin a filename or single directory name). `**` matches zero or more subdirectories, recursively. It also includes any/all files within a subdirectory, i.e., when used at the end of a pattern. For example, `**/exclude_me/**` will exclude all files in any directory named `exclude_me`, as well as all files in any subdirectories of `exclude_me`, recursively.
Special characters such as @ should be escaped, for instance: Special characters such as @ should be escaped, for instance:
- `**/\@eaDir/**` will exclude all files in any directory named `@eaDir` - `**/\@eaDir/**` will exclude all files in any directory named `@eaDir`
@@ -47,6 +47,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele
#### ROCm #### ROCm
- On Linux, The [AMDGPU driver module](https://rocm.docs.amd.com/projects/install-on-linux/en/latest/how-to/docker.html) needs to be installed on the server and, if secure boot is used, the signing key of DKMS [needs to be enrolled in UEFI BIOS](https://wiki.debian.org/SecureBoot)
- The GPU must be supported by ROCm. If it isn't officially supported, you can attempt to use the `HSA_OVERRIDE_GFX_VERSION` environmental variable: `HSA_OVERRIDE_GFX_VERSION=<a supported version, e.g. 10.3.0>`. If this doesn't work, you might need to also set `HSA_USE_SVM=0`. - The GPU must be supported by ROCm. If it isn't officially supported, you can attempt to use the `HSA_OVERRIDE_GFX_VERSION` environmental variable: `HSA_OVERRIDE_GFX_VERSION=<a supported version, e.g. 10.3.0>`. If this doesn't work, you might need to also set `HSA_USE_SVM=0`.
- The ROCm image is quite large and requires at least 35GiB of free disk space. However, pulling later updates to the service through Docker will generally only amount to a few hundred megabytes as the rest will be cached. - The ROCm image is quite large and requires at least 35GiB of free disk space. However, pulling later updates to the service through Docker will generally only amount to a few hundred megabytes as the rest will be cached.
- This backend is new and may experience some issues. For example, GPU power consumption can be higher than usual after running inference, even if the machine learning service is idle. In this case, it will only go back to normal after being idle for 5 minutes (configurable with the [MACHINE_LEARNING_MODEL_TTL](/install/environment-variables) setting). - This backend is new and may experience some issues. For example, GPU power consumption can be higher than usual after running inference, even if the machine learning service is idle. In this case, it will only go back to normal after being idle for 5 minutes (configurable with the [MACHINE_LEARNING_MODEL_TTL](/install/environment-variables) setting).
+7
View File
@@ -18,6 +18,7 @@ You can search the following types of content:
| People | Faces that are recognized in your photos/videos. | | People | Faces that are recognized in your photos/videos. |
| Contextual | Content of the photos and videos. | | Contextual | Content of the photos and videos. |
| File name or extension | Full or partial file's name, or file's extension | | File name or extension | Full or partial file's name, or file's extension |
| Full path or folder | Full or partial folder names from the original path. |
| Description | Description added to assets. | | Description | Description added to assets. |
| Optical Character Recognition (OCR) | Text in images | | Optical Character Recognition (OCR) | Text in images |
| Locations | Cities, states, and countries from reverse geocoding. | | Locations | Cities, states, and countries from reverse geocoding. |
@@ -30,6 +31,12 @@ You can search the following types of content:
<img src={require('./img/advanced-search-filters.webp').default} width="70%" title='Advanced search filters' /> <img src={require('./img/advanced-search-filters.webp').default} width="70%" title='Advanced search filters' />
### Full path or folder
Use this mode when you know a folder name or part of the original asset path.
Example: for /John/Projects/3D_Printing/2026-07-01/IMG_0001.jpg, searches like Projects, 3D, Printing, or 2026 match the asset.
## Configuration ## Configuration
Navigating to `Administration > Settings > Machine Learning Settings > Smart Search` will show the options available. Navigating to `Administration > Settings > Machine Learning Settings > Smart Search` will show the options available.
+2 -2
View File
@@ -26,7 +26,7 @@ The default configuration looks like this:
}, },
"ffmpeg": { "ffmpeg": {
"accel": "disabled", "accel": "disabled",
"accelDecode": false, "accelDecode": true,
"acceptedAudioCodecs": ["aac", "mp3", "opus"], "acceptedAudioCodecs": ["aac", "mp3", "opus"],
"acceptedContainers": ["mov", "ogg", "webm"], "acceptedContainers": ["mov", "ogg", "webm"],
"acceptedVideoCodecs": ["h264"], "acceptedVideoCodecs": ["h264"],
@@ -264,4 +264,4 @@ volumes:
- ./configuration.yml:${IMMICH_CONFIG_FILE} - ./configuration.yml:${IMMICH_CONFIG_FILE}
``` ```
:: :::
+19 -17
View File
@@ -29,29 +29,31 @@ These environment variables are used by the `docker-compose.yml` file and do **N
## General ## General
| Variable | Description | Default | Containers | Workers | | Variable | Description | Default | Containers | Workers |
| :---------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- | | :---------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
| `TZ` | Timezone | <sup>\*1</sup> | server | microservices | | `TZ` | Timezone | <sup>\*1</sup> | server | microservices |
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices | | `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
| `IMMICH_LOG_LEVEL` | Log level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices | | `IMMICH_LOG_LEVEL` | Log level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
| `IMMICH_LOG_FORMAT` | Log output format (`console`, `json`) | `console` | server | api, microservices | | `IMMICH_LOG_FORMAT` | Log output format (`console`, `json`) | `console` | server | api, microservices |
| `IMMICH_MEDIA_LOCATION` | Media location inside the container ⚠️**You probably shouldn't set this**<sup>\*2</sup>⚠️ | `/data` | server | api, microservices | | `IMMICH_MEDIA_LOCATION` | Media location inside the container ⚠️**You probably shouldn't set this**<sup>\*2</sup>⚠️ | `/data` | server | api, microservices |
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices | | `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
| `IMMICH_HELMET_FILE` | Path to a json file with [helmet](https://www.npmjs.com/package/helmet) options. Set to `false` to disable. Set to `true` to use `server/helmet.json`. | `false` | server | api | | `IMMICH_HELMET_FILE` | Path to a json file with [helmet](https://www.npmjs.com/package/helmet) options. Set to `false` to disable. Set to `true` to use `server/helmet.json`<sup>\*3</sup>. | `false` | server | api |
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | | | `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
| `CPU_CORES` | Number of cores available to the Immich server | auto-detected CPU core count | server | | | `CPU_CORES` | Number of cores available to the Immich server | auto-detected CPU core count | server | |
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api | | `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices | | `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices | | `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
| `IMMICH_TRUSTED_PROXIES` | List of comma-separated IPs set as trusted proxies | | server | api | | `IMMICH_TRUSTED_PROXIES` | List of comma-separated IPs set as trusted proxies | | server | api |
| `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/administration/system-integrity) | | server | api, microservices | | `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/administration/system-integrity) | | server | api, microservices |
| `IMMICH_ALLOW_SETUP` | When `false` disables the `/auth/admin-sign-up` endpoint | `true` | server | api | | `IMMICH_ALLOW_SETUP` | When `false` disables the `/auth/admin-sign-up` endpoint | `true` | server | api |
\*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`. \*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`.
`TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution. `TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution.
\*2: This path is where the Immich code looks for the files, which is internal to the docker container. Setting it to a path on your host will certainly break things, you should use the `UPLOAD_LOCATION` variable instead. \*2: This path is where the Immich code looks for the files, which is internal to the docker container. Setting it to a path on your host will certainly break things, you should use the `UPLOAD_LOCATION` variable instead.
\*3: The [default configuration](https://helmetjs.github.io/#content-security-policy) sets `upgrade-insecure-requests`, which tells the browser to upgrade all requests to HTTPS. This breaks on HTTP-only deployments. If you cannot use HTTPS, you should use a custom helmet config file with `"upgrade-insecure-requests": null`.
## Workers ## Workers
| Variable | Description | Default | Containers | | Variable | Description | Default | Containers |
-3
View File
@@ -56,8 +56,5 @@
}, },
"engines": { "engines": {
"node": ">=20" "node": ">=20"
},
"volta": {
"node": "24.15.0"
} }
} }
+2 -1
View File
@@ -11,5 +11,6 @@
"@types/oidc-provider": "^9.0.0", "@types/oidc-provider": "^9.0.0",
"oidc-provider": "^9.0.0", "oidc-provider": "^9.0.0",
"tsx": "^4.20.6" "tsx": "^4.20.6"
} },
"packageManager": "pnpm@10.33.1"
} }
-1
View File
@@ -1 +0,0 @@
24.15.0
+15
View File
@@ -27,3 +27,18 @@ run = { task = "lint --fix" }
[tasks.check] [tasks.check]
env._.path = "./node_modules/.bin" env._.path = "./node_modules/.bin"
run = "tsc --noEmit" run = "tsc --noEmit"
[tasks.ci-setup]
depends = ["//:sdk:install", "//:sdk:build", "//cli:install", "//cli:build"]
run = { task = ":install" }
[tasks.ci-unit]
depends = ["//:sdk:install", "//:sdk:build"]
run = [
{ task = ":install" },
{ task = ":format" },
{ task = ":lint" },
{ task = ":check" },
]
-3
View File
@@ -56,8 +56,5 @@
"utimes": "^5.2.1", "utimes": "^5.2.1",
"vite-tsconfig-paths": "^6.1.1", "vite-tsconfig-paths": "^6.1.1",
"vitest": "^4.0.0" "vitest": "^4.0.0"
},
"volta": {
"node": "24.15.0"
} }
} }
+4 -3
View File
@@ -28,6 +28,10 @@ export const errorDto = {
badRequest: (message: any = null) => ({ badRequest: (message: any = null) => ({
message: message ?? expect.anything(), message: message ?? expect.anything(),
}), }),
validationError: (errors?: ReadonlyArray<{ path: ReadonlyArray<string | number>; message: string }>) => ({
message: 'Validation failed',
errors: errors ? expect.arrayContaining(errors.map((e) => expect.objectContaining(e))) : expect.any(Array),
}),
noPermission: { noPermission: {
message: expect.stringContaining('Not found or no'), message: expect.stringContaining('Not found or no'),
}, },
@@ -37,9 +41,6 @@ export const errorDto = {
alreadyHasAdmin: { alreadyHasAdmin: {
message: 'The server already has an admin', message: 'The server already has an admin',
}, },
invalidEmail: {
message: ['email must be an email'],
},
}; };
export const signupResponseDto = { export const signupResponseDto = {
+74 -10
View File
@@ -146,7 +146,7 @@ describe('/albums', () => {
it('should not return shared albums with a deleted owner', async () => { it('should not return shared albums with a deleted owner', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/albums?shared=true') .get('/albums?isShared=true')
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -188,7 +188,7 @@ describe('/albums', () => {
it('should return the album collection including owned and shared', async () => { it('should return the album collection including owned and shared', async () => {
const { status, body } = await request(app).get('/albums').set('Authorization', `Bearer ${user1.accessToken}`); const { status, body } = await request(app).get('/albums').set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toHaveLength(4); expect(body).toHaveLength(5);
expect(body).toEqual( expect(body).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
@@ -219,13 +219,20 @@ describe('/albums', () => {
]), ]),
shared: false, shared: false,
}), }),
expect.objectContaining({
albumName: user2SharedUser,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user2.userId }) },
]),
shared: true,
}),
]), ]),
); );
}); });
it('should return the album collection filtered by shared', async () => { it('should return the album collection filtered by isShared', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/albums?shared=true') .get('/albums?isShared=true')
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toHaveLength(4); expect(body).toHaveLength(4);
@@ -263,9 +270,9 @@ describe('/albums', () => {
); );
}); });
it('should return the album collection filtered by NOT shared', async () => { it('should return the album collection filtered by NOT isShared', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/albums?shared=false') .get('/albums?isShared=false')
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toHaveLength(1); expect(body).toHaveLength(1);
@@ -282,6 +289,63 @@ describe('/albums', () => {
); );
}); });
it('should return only owned albums when filtered by isOwned=true', async () => {
const { status, body } = await request(app)
.get('/albums?isOwned=true')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(4);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ albumName: user1SharedEditorUser }),
expect.objectContaining({ albumName: user1SharedViewerUser }),
expect.objectContaining({ albumName: user1SharedLink }),
expect.objectContaining({ albumName: user1NotShared }),
]),
);
});
it('should return only shared-with-me albums when filtered by isOwned=false', async () => {
const { status, body } = await request(app)
.get('/albums?isOwned=false')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(1);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({
albumName: user2SharedUser,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user2.userId }) },
]),
}),
]),
);
});
it('should return owned shared-out albums when filtered by isOwned=true&ishared=true', async () => {
const { status, body } = await request(app)
.get('/albums?isOwned=true&isShared=true')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(3);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ albumName: user1SharedEditorUser }),
expect.objectContaining({ albumName: user1SharedViewerUser }),
expect.objectContaining({ albumName: user1SharedLink }),
]),
);
});
it('should return empty list when filtered by isOwned=false&isShared=false', async () => {
const { status, body } = await request(app)
.get('/albums?isOwned=false&isShared=false')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(0);
});
it('should return the album collection filtered by assetId', async () => { it('should return the album collection filtered by assetId', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/albums?assetId=${user1Asset2.id}`) .get(`/albums?assetId=${user1Asset2.id}`)
@@ -290,17 +354,17 @@ describe('/albums', () => {
expect(body).toHaveLength(2); expect(body).toHaveLength(2);
}); });
it('should return the album collection filtered by assetId and ignores shared=true', async () => { it('should return the album collection filtered by assetId and ignores isShared=true', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/albums?shared=true&assetId=${user1Asset1.id}`) .get(`/albums?isShared=true&assetId=${user1Asset1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toHaveLength(5); expect(body).toHaveLength(5);
}); });
it('should return the album collection filtered by assetId and ignores shared=false', async () => { it('should return the album collection filtered by assetId and ignores isShared=false', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/albums?shared=false&assetId=${user1Asset1.id}`) .get(`/albums?isShared=false&assetId=${user1Asset1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toHaveLength(5); expect(body).toHaveLength(5);
+21 -7
View File
@@ -110,7 +110,9 @@ describe('/libraries', () => {
}); });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['[importPaths] Array must have unique items'])); expect(body).toEqual(
errorDto.validationError([{ path: ['importPaths'], message: 'Array must have unique items' }]),
);
}); });
it('should not create an external library with duplicate exclusion patterns', async () => { it('should not create an external library with duplicate exclusion patterns', async () => {
@@ -125,7 +127,9 @@ describe('/libraries', () => {
}); });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['[exclusionPatterns] Array must have unique items'])); expect(body).toEqual(
errorDto.validationError([{ path: ['exclusionPatterns'], message: 'Array must have unique items' }]),
);
}); });
}); });
@@ -157,7 +161,9 @@ describe('/libraries', () => {
.send({ name: '' }); .send({ name: '' });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['[name] Too small: expected string to have >=1 characters'])); expect(body).toEqual(
errorDto.validationError([{ path: ['name'], message: 'Too small: expected string to have >=1 characters' }]),
);
}); });
it('should change the import paths', async () => { it('should change the import paths', async () => {
@@ -181,7 +187,9 @@ describe('/libraries', () => {
.send({ importPaths: [''] }); .send({ importPaths: [''] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['[importPaths] Array items must not be empty'])); expect(body).toEqual(
errorDto.validationError([{ path: ['importPaths'], message: 'Array items must not be empty' }]),
);
}); });
it('should reject duplicate import paths', async () => { it('should reject duplicate import paths', async () => {
@@ -191,7 +199,9 @@ describe('/libraries', () => {
.send({ importPaths: ['/path', '/path'] }); .send({ importPaths: ['/path', '/path'] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['[importPaths] Array must have unique items'])); expect(body).toEqual(
errorDto.validationError([{ path: ['importPaths'], message: 'Array must have unique items' }]),
);
}); });
it('should change the exclusion pattern', async () => { it('should change the exclusion pattern', async () => {
@@ -215,7 +225,9 @@ describe('/libraries', () => {
.send({ exclusionPatterns: ['**/*.jpg', '**/*.jpg'] }); .send({ exclusionPatterns: ['**/*.jpg', '**/*.jpg'] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['[exclusionPatterns] Array must have unique items'])); expect(body).toEqual(
errorDto.validationError([{ path: ['exclusionPatterns'], message: 'Array must have unique items' }]),
);
}); });
it('should reject an empty exclusion pattern', async () => { it('should reject an empty exclusion pattern', async () => {
@@ -225,7 +237,9 @@ describe('/libraries', () => {
.send({ exclusionPatterns: [''] }); .send({ exclusionPatterns: [''] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['[exclusionPatterns] Array items must not be empty'])); expect(body).toEqual(
errorDto.validationError([{ path: ['exclusionPatterns'], message: 'Array items must not be empty' }]),
);
}); });
}); });
+12 -4
View File
@@ -109,7 +109,9 @@ describe('/map', () => {
.get('/map/reverse-geocode?lon=123') .get('/map/reverse-geocode?lon=123')
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['[lat] Invalid input: expected number, received NaN'])); expect(body).toEqual(
errorDto.validationError([{ path: ['lat'], message: 'Invalid input: expected number, received NaN' }]),
);
}); });
it('should throw an error if a lat is not a number', async () => { it('should throw an error if a lat is not a number', async () => {
@@ -117,7 +119,9 @@ describe('/map', () => {
.get('/map/reverse-geocode?lat=abc&lon=123.456') .get('/map/reverse-geocode?lat=abc&lon=123.456')
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['[lat] Invalid input: expected number, received NaN'])); expect(body).toEqual(
errorDto.validationError([{ path: ['lat'], message: 'Invalid input: expected number, received NaN' }]),
);
}); });
it('should throw an error if a lat is out of range', async () => { it('should throw an error if a lat is out of range', async () => {
@@ -125,7 +129,9 @@ describe('/map', () => {
.get('/map/reverse-geocode?lat=91&lon=123.456') .get('/map/reverse-geocode?lat=91&lon=123.456')
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['[lat] Too big: expected number to be <=90'])); expect(body).toEqual(
errorDto.validationError([{ path: ['lat'], message: 'Too big: expected number to be <=90' }]),
);
}); });
it('should throw an error if a lon is not provided', async () => { it('should throw an error if a lon is not provided', async () => {
@@ -133,7 +139,9 @@ describe('/map', () => {
.get('/map/reverse-geocode?lat=75') .get('/map/reverse-geocode?lat=75')
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['[lon] Invalid input: expected number, received NaN'])); expect(body).toEqual(
errorDto.validationError([{ path: ['lon'], message: 'Invalid input: expected number, received NaN' }]),
);
}); });
const reverseGeocodeTestCases = [ const reverseGeocodeTestCases = [
+16 -4
View File
@@ -105,7 +105,11 @@ describe(`/oauth`, () => {
it(`should throw an error if a redirect uri is not provided`, async () => { it(`should throw an error if a redirect uri is not provided`, async () => {
const { status, body } = await request(app).post('/oauth/authorize').send({}); const { status, body } = await request(app).post('/oauth/authorize').send({});
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['[redirectUri] Invalid input: expected string, received undefined'])); expect(body).toEqual(
errorDto.validationError([
{ path: ['redirectUri'], message: 'Invalid input: expected string, received undefined' },
]),
);
}); });
it('should return a redirect uri', async () => { it('should return a redirect uri', async () => {
@@ -164,13 +168,17 @@ describe(`/oauth`, () => {
it(`should throw an error if a url is not provided`, async () => { it(`should throw an error if a url is not provided`, async () => {
const { status, body } = await request(app).post('/oauth/callback').send({}); const { status, body } = await request(app).post('/oauth/callback').send({});
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['[url] Invalid input: expected string, received undefined'])); expect(body).toEqual(
errorDto.validationError([{ path: ['url'], message: 'Invalid input: expected string, received undefined' }]),
);
}); });
it(`should throw an error if the url is empty`, async () => { it(`should throw an error if the url is empty`, async () => {
const { status, body } = await request(app).post('/oauth/callback').send({ url: '' }); const { status, body } = await request(app).post('/oauth/callback').send({ url: '' });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['[url] Too small: expected string to have >=1 characters'])); expect(body).toEqual(
errorDto.validationError([{ path: ['url'], message: 'Too small: expected string to have >=1 characters' }]),
);
}); });
it(`should throw an error if the state is not provided`, async () => { it(`should throw an error if the state is not provided`, async () => {
@@ -375,7 +383,11 @@ describe(`/oauth`, () => {
it(`should throw an error if the logout_token is not provided`, async () => { it(`should throw an error if the logout_token is not provided`, async () => {
const { status, body } = await request(app).post('/oauth/backchannel-logout').send({}); const { status, body } = await request(app).post('/oauth/backchannel-logout').send({});
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['[logout_token] Invalid input: expected string, received undefined'])); expect(body).toEqual(
errorDto.validationError([
{ path: ['logout_token'], message: 'Invalid input: expected string, received undefined' },
]),
);
}); });
it(`should throw an error if an invalid logout token is provided`, async () => { it(`should throw an error if an invalid logout token is provided`, async () => {
@@ -341,7 +341,9 @@ describe('/shared-links', () => {
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest()); expect(body).toEqual(
errorDto.validationError([{ path: [], message: 'Invalid input: expected object, received undefined' }]),
);
}); });
it('should require an asset/album id', async () => { it('should require an asset/album id', async () => {
+9 -2
View File
@@ -41,7 +41,9 @@ describe('/stacks', () => {
.send({ assetIds: [asset.id] }); .send({ assetIds: [asset.id] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest()); expect(body).toEqual(
errorDto.validationError([{ path: ['assetIds'], message: 'Too small: expected array to have >=2 items' }]),
);
}); });
it('should require a valid id', async () => { it('should require a valid id', async () => {
@@ -51,7 +53,12 @@ describe('/stacks', () => {
.send({ assetIds: [uuidDto.invalid, uuidDto.invalid] }); .send({ assetIds: [uuidDto.invalid, uuidDto.invalid] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest()); expect(body).toEqual(
errorDto.validationError([
{ path: ['assetIds', 0], message: 'Invalid UUID' },
{ path: ['assetIds', 1], message: 'Invalid UUID' },
]),
);
}); });
it('should require access', async () => { it('should require access', async () => {
+2 -2
View File
@@ -309,7 +309,7 @@ describe('/tags', () => {
.get(`/tags/${uuidDto.invalid}`) .get(`/tags/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID'])); expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
}); });
it('should get tag details', async () => { it('should get tag details', async () => {
@@ -427,7 +427,7 @@ describe('/tags', () => {
.delete(`/tags/${uuidDto.invalid}`) .delete(`/tags/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID'])); expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
}); });
it('should delete a tag', async () => { it('should delete a tag', async () => {
@@ -108,14 +108,20 @@ describe('/admin/users', () => {
expect(body).toEqual(errorDto.forbidden); expect(body).toEqual(errorDto.forbidden);
}); });
for (const key of ['password', 'email', 'name', 'quotaSizeInBytes', 'shouldChangePassword', 'notify']) { for (const [key, message] of [
['password', 'Invalid input: expected string, received null'],
['email', 'Invalid input: expected email, received object'],
['name', 'Invalid input: expected string, received null'],
['shouldChangePassword', 'Invalid input: expected boolean, received null'],
['notify', 'Invalid input: expected boolean, received null'],
] as const) {
it(`should not allow null ${key}`, async () => { it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post(`/admin/users`) .post(`/admin/users`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ...createUserDto.user1, [key]: null }); .send({ ...createUserDto.user1, [key]: null });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest()); expect(body).toEqual(errorDto.validationError([{ path: [key], message }]));
}); });
} }
@@ -153,14 +159,19 @@ describe('/admin/users', () => {
expect(body).toEqual(errorDto.forbidden); expect(body).toEqual(errorDto.forbidden);
}); });
for (const key of ['password', 'email', 'name', 'shouldChangePassword']) { for (const [key, message] of [
['password', 'Invalid input: expected string, received null'],
['email', 'Invalid input: expected email, received object'],
['name', 'Invalid input: expected string, received null'],
['shouldChangePassword', 'Invalid input: expected boolean, received null'],
] as const) {
it(`should not allow null ${key}`, async () => { it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/admin/users/${uuidDto.notFound}`) .put(`/admin/users/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ [key]: null }); .send({ [key]: null });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest()); expect(body).toEqual(errorDto.validationError([{ path: [key], message }]));
}); });
} }
+6 -2
View File
@@ -179,7 +179,9 @@ describe('/users', () => {
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(
errorDto.badRequest(['[download.archiveSize] Invalid input: expected int, received number']), errorDto.validationError([
{ path: ['download', 'archiveSize'], message: 'Invalid input: expected int, received number' },
]),
); );
}); });
@@ -207,7 +209,9 @@ describe('/users', () => {
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(
errorDto.badRequest(['[download.includeEmbeddedVideos] Invalid input: expected boolean, received number']), errorDto.validationError([
{ path: ['download', 'includeEmbeddedVideos'], message: 'Invalid input: expected boolean, received number' },
]),
); );
}); });
+2 -1
View File
@@ -240,7 +240,8 @@ export const setupBaseMockApiRoutes = async (context: BrowserContext, adminUserI
}); });
}); });
await context.route('**/api/albums*', async (route, request) => { await context.route('**/api/albums*', async (route, request) => {
if (request.url().endsWith('albums?shared=true') || request.url().endsWith('albums')) { const url = request.url();
if (url.endsWith('albums?isShared=true') || url.endsWith('albums?isOwned=true') || url.endsWith('albums')) {
return route.fulfill({ return route.fulfill({
status: 200, status: 200,
contentType: 'application/json', contentType: 'application/json',
+39 -1
View File
@@ -1240,6 +1240,7 @@
"free_up_space_description": "Move backed-up photos and videos to your device's trash to free up space. Your copies on the server remain safe.", "free_up_space_description": "Move backed-up photos and videos to your device's trash to free up space. Your copies on the server remain safe.",
"free_up_space_settings_subtitle": "Free up device storage", "free_up_space_settings_subtitle": "Free up device storage",
"full_path": "Full path: {path}", "full_path": "Full path: {path}",
"full_path_or_folder": "Full path or folder",
"gcast_enabled": "Google Cast", "gcast_enabled": "Google Cast",
"gcast_enabled_description": "This feature loads external resources from Google in order to work.", "gcast_enabled_description": "This feature loads external resources from Google in order to work.",
"general": "General", "general": "General",
@@ -1523,6 +1524,38 @@
"marked_all_as_read": "Marked all as read", "marked_all_as_read": "Marked all as read",
"matches": "Matches", "matches": "Matches",
"matching_assets": "Matching Assets", "matching_assets": "Matching Assets",
"media_chrome": {
"auto": "Auto",
"captions": "Captions",
"captions_off": "Off",
"closed_captions": "closed captions",
"decode_error": "Decode error",
"disable_captions": "Disable captions",
"enable_captions": "Enable captions",
"enter_fullscreen_mode": "Enter fullscreen mode",
"exit_fullscreen_mode": "Exit fullscreen mode",
"loop": "Loop",
"media_error_description": "A media error caused playback to be aborted. The media could be corrupt or your browser does not support this format.",
"media_loading": "media loading",
"mute": "Mute",
"network_error": "Network error",
"network_error_description": "A network error caused the media download to fail.",
"not_supported_error": "Source Not Supported",
"playback_rate": "Playback rate",
"playback_rate_current": "current playback rate",
"playback_rate_value": "Playback rate {playbackRate}",
"playback_time": "playback time",
"quality": "Quality",
"second": "second",
"seconds": "seconds",
"time_value_of_total_time": "{currentTime} of {totalTime}",
"time_value_remaining": "{time} remaining",
"unmute": "Unmute",
"unsupported_error_description": "An unsupported error occurred. The server or network failed, or your browser does not support this format.",
"video_not_loaded_unknown_time": "video not loaded, unknown time.",
"video_player": "video player",
"volume": "volume"
},
"media_type": "Media type", "media_type": "Media type",
"memories": "Memories", "memories": "Memories",
"memories_all_caught_up": "All caught up", "memories_all_caught_up": "All caught up",
@@ -1761,7 +1794,6 @@
"play_original_video": "Play original video", "play_original_video": "Play original video",
"play_original_video_setting_description": "Prefer playback of original videos rather than transcoded videos. If original asset is not compatible it may not playback correctly.", "play_original_video_setting_description": "Prefer playback of original videos rather than transcoded videos. If original asset is not compatible it may not playback correctly.",
"play_transcoded_video": "Play transcoded video", "play_transcoded_video": "Play transcoded video",
"playback_speed": "Playback speed",
"please_auth_to_access": "Please authenticate to access", "please_auth_to_access": "Please authenticate to access",
"port": "Port", "port": "Port",
"preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_subtitle": "Manage the app's preferences",
@@ -1943,6 +1975,8 @@
"search_by_description_example": "Hiking day in Sapa", "search_by_description_example": "Hiking day in Sapa",
"search_by_filename": "Search by file name or extension", "search_by_filename": "Search by file name or extension",
"search_by_filename_example": "i.e. IMG_1234.JPG or PNG", "search_by_filename_example": "i.e. IMG_1234.JPG or PNG",
"search_by_full_path": "Search by full path or folder",
"search_by_full_path_example": "/John/Projects/3D_Printing/2026-07-01 - you can search for Projects, 3D, Printing, 2026 etc.",
"search_by_ocr": "Search by OCR", "search_by_ocr": "Search by OCR",
"search_by_ocr_example": "Latte", "search_by_ocr_example": "Latte",
"search_camera_lens_model": "Search lens model...", "search_camera_lens_model": "Search lens model...",
@@ -2158,6 +2192,7 @@
"show_schema": "Show schema", "show_schema": "Show schema",
"show_search_options": "Show search options", "show_search_options": "Show search options",
"show_shared_links": "Show shared links", "show_shared_links": "Show shared links",
"show_slideshow_metadata_overlay": "Show image info overlay",
"show_slideshow_transition": "Show slideshow transition", "show_slideshow_transition": "Show slideshow transition",
"show_supporter_badge": "Supporter badge", "show_supporter_badge": "Supporter badge",
"show_supporter_badge_description": "Show a supporter badge", "show_supporter_badge_description": "Show a supporter badge",
@@ -2173,6 +2208,9 @@
"skip_to_folders": "Skip to folders", "skip_to_folders": "Skip to folders",
"skip_to_tags": "Skip to tags", "skip_to_tags": "Skip to tags",
"slideshow": "Slideshow", "slideshow": "Slideshow",
"slideshow_metadata_overlay_mode": "Overlay content",
"slideshow_metadata_overlay_mode_description_only": "Description only",
"slideshow_metadata_overlay_mode_full": "Full",
"slideshow_repeat": "Repeat slideshow", "slideshow_repeat": "Repeat slideshow",
"slideshow_repeat_description": "Loop back to beginning when slideshow ends", "slideshow_repeat_description": "Loop back to beginning when slideshow ends",
"slideshow_settings": "Slideshow settings", "slideshow_settings": "Slideshow settings",
+2 -2
View File
@@ -68,7 +68,7 @@ ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 \
RUN apt-get update && \ RUN apt-get update && \
# Pascal support was dropped in 9.11 # Pascal support was dropped in 9.11
apt-get install --no-install-recommends -yqq libcudnn9-cuda-12=9.10.2.21-1 && \ apt-get install --no-install-recommends -yqq libcudnn9-cuda-12=9.10.2.21-1 tzdata && \
apt-get clean && \ apt-get clean && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
@@ -112,7 +112,7 @@ ARG RKNN_TOOLKIT_VERSION="v2.3.0"
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 \ ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 \
MACHINE_LEARNING_MODEL_ARENA=false MACHINE_LEARNING_MODEL_ARENA=false
ADD --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 prod-${DEVICE} AS prod FROM prod-${DEVICE} AS prod
-13
View File
@@ -32,25 +32,12 @@ class OcrSettings(BaseModel):
class PreloadModelData(BaseModel): class PreloadModelData(BaseModel):
clip_fallback: str | None = os.getenv("MACHINE_LEARNING_PRELOAD__CLIP", None)
facial_recognition_fallback: str | None = os.getenv("MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION", None)
if clip_fallback is not None:
os.environ["MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL"] = clip_fallback
os.environ["MACHINE_LEARNING_PRELOAD__CLIP__VISUAL"] = clip_fallback
del os.environ["MACHINE_LEARNING_PRELOAD__CLIP"]
if facial_recognition_fallback is not None:
os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION"] = facial_recognition_fallback
os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION"] = facial_recognition_fallback
del os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION"]
clip: ClipSettings = ClipSettings() clip: ClipSettings = ClipSettings()
facial_recognition: FacialRecognitionSettings = FacialRecognitionSettings() facial_recognition: FacialRecognitionSettings = FacialRecognitionSettings()
ocr: OcrSettings = OcrSettings() ocr: OcrSettings = OcrSettings()
class MaxBatchSize(BaseModel): class MaxBatchSize(BaseModel):
ocr_fallback: str | None = os.getenv("MACHINE_LEARNING_MAX_BATCH_SIZE__TEXT_RECOGNITION", None)
if ocr_fallback is not None:
os.environ["MACHINE_LEARNING_MAX_BATCH_SIZE__OCR"] = ocr_fallback
facial_recognition: int | None = None facial_recognition: int | None = None
ocr: int | None = None ocr: int | None = None
-14
View File
@@ -117,20 +117,6 @@ async def preload_models(preload: PreloadModelData) -> None:
ModelTask.OCR, ModelTask.OCR,
) )
if preload.clip_fallback is not None:
log.warning(
"Deprecated env variable: 'MACHINE_LEARNING_PRELOAD__CLIP'. "
"Use 'MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL' and "
"'MACHINE_LEARNING_PRELOAD__CLIP__VISUAL' instead."
)
if preload.facial_recognition_fallback is not None:
log.warning(
"Deprecated env variable: 'MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION'. "
"Use 'MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION' and "
"'MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION' instead."
)
def update_state() -> Iterator[None]: def update_state() -> Iterator[None]:
global active_requests, last_called global active_requests, last_called
+36
View File
@@ -0,0 +1,36 @@
[tools]
python = "3.11"
uv = "0.8.15"
[tasks.install]
run = "uv sync --locked"
[tasks.lint]
run = "uv run ruff check immich_ml"
[tasks.test]
run = "uv run pytest --cov=immich_ml --cov-report term-missing"
[tasks.format]
run = "uv run ruff format immich_ml"
[tasks.check]
run = "uv run mypy --strict immich_ml/"
[tasks.ci-unit]
run = [
{ task = ":install --extra cpu" },
{ task = ":format" },
{ task = ":lint --output-format=github" },
{ task = ":check" },
{ task = ":test" },
]
[tasks.checklist]
run = [
{ task = ":install" },
{ task = ":format" },
{ task = ":lint" },
{ task = ":check" },
{ task = ":test" },
]
+1 -1
View File
@@ -11,7 +11,7 @@ dependencies = [
"gunicorn>=21.1.0", "gunicorn>=21.1.0",
"huggingface-hub>=1.0,<2.0", "huggingface-hub>=1.0,<2.0",
"insightface>=0.7.3,<1.0", "insightface>=0.7.3,<1.0",
"numpy<2.4.0", "numpy>=2.4.0,<3.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>=12.2,<13",
+138 -98
View File
@@ -243,14 +243,14 @@ wheels = [
[[package]] [[package]]
name = "click" name = "click"
version = "8.1.7" version = "8.3.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" }, { name = "colorama", marker = "sys_platform == 'win32'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121, upload-time = "2023-08-17T17:29:11.868Z" } sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941, upload-time = "2023-08-17T17:29:10.08Z" }, { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" },
] ]
[[package]] [[package]]
@@ -785,17 +785,34 @@ wheels = [
[[package]] [[package]]
name = "hf-xet" name = "hf-xet"
version = "1.1.7" version = "1.5.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b2/0a/a0f56735940fde6dd627602fec9ab3bad23f66a272397560abd65aba416e/hf_xet-1.1.7.tar.gz", hash = "sha256:20cec8db4561338824a3b5f8c19774055b04a8df7fff0cb1ff2cb1a0c1607b80", size = 477719, upload-time = "2025-08-06T00:30:55.741Z" } sdist = { url = "https://files.pythonhosted.org/packages/74/d8/5c06fc76461418326a7decf8367480c35be11a41fd938633929c60a9ec6b/hf_xet-1.5.0.tar.gz", hash = "sha256:e0fb0a34d9f406eed88233e829a67ec016bec5af19e480eac65a233ea289a948", size = 837196, upload-time = "2026-05-06T06:18:15.583Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/b1/7c/8d7803995caf14e7d19a392a486a040f923e2cfeff824e9b800b92072f76/hf_xet-1.1.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:60dae4b44d520819e54e216a2505685248ec0adbdb2dd4848b17aa85a0375cde", size = 2761743, upload-time = "2025-08-06T00:30:50.634Z" }, { url = "https://files.pythonhosted.org/packages/68/9b/6912c99070915a4f28119e3c5b52a9abd1eec0ad5cb293b8c967a0c6f5a2/hf_xet-1.5.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7d70fe2ce97b9db73b9c9b9c81fe3693640aec83416a966c446afea54acfae3c", size = 4023383, upload-time = "2026-05-06T06:17:53.947Z" },
{ url = "https://files.pythonhosted.org/packages/51/a3/fa5897099454aa287022a34a30e68dbff0e617760f774f8bd1db17f06bd4/hf_xet-1.1.7-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:b109f4c11e01c057fc82004c9e51e6cdfe2cb230637644ade40c599739067b2e", size = 2624331, upload-time = "2025-08-06T00:30:49.212Z" }, { url = "https://files.pythonhosted.org/packages/0f/6d/9563cfde59b5d8128a9c7ec972a087f4c782e4f7bac5a85234edfd5d5e49/hf_xet-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:73a0dae8c71de3b0633a45c73f4a4a5ed09e94b43441d82981a781d4f12baa42", size = 3792751, upload-time = "2026-05-06T06:17:51.791Z" },
{ url = "https://files.pythonhosted.org/packages/86/50/2446a132267e60b8a48b2e5835d6e24fd988000d0f5b9b15ebd6d64ef769/hf_xet-1.1.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6efaaf1a5a9fc3a501d3e71e88a6bfebc69ee3a716d0e713a931c8b8d920038f", size = 3183844, upload-time = "2025-08-06T00:30:47.582Z" }, { url = "https://files.pythonhosted.org/packages/07/a5/ed5a0cf35b49a0571af5a8f53416dad1877a718c021c9937c3a53cb45781/hf_xet-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a60290ec57e9b71767fba7c3645ddafdd0759974b540441510c629c6db6db24a", size = 4456058, upload-time = "2026-05-06T06:17:40.735Z" },
{ url = "https://files.pythonhosted.org/packages/20/8f/ccc670616bb9beee867c6bb7139f7eab2b1370fe426503c25f5cbb27b148/hf_xet-1.1.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:751571540f9c1fbad9afcf222a5fb96daf2384bf821317b8bfb0c59d86078513", size = 3074209, upload-time = "2025-08-06T00:30:45.509Z" }, { url = "https://files.pythonhosted.org/packages/60/fb/3ae8bf2a7a37a4197d0195d7247fd25b3952e15cb8a599e285dfaa6f52b3/hf_xet-1.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5de0f6deada0dada870bb376a11bcd1f08abf3a968a6d118f33e72d1b1eb480", size = 4250783, upload-time = "2026-05-06T06:17:38.412Z" },
{ url = "https://files.pythonhosted.org/packages/21/0a/4c30e1eb77205565b854f5e4a82cf1f056214e4dc87f2918ebf83d47ae14/hf_xet-1.1.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:18b61bbae92d56ae731b92087c44efcac216071182c603fc535f8e29ec4b09b8", size = 3239602, upload-time = "2025-08-06T00:30:52.41Z" }, { url = "https://files.pythonhosted.org/packages/a2/9b/8bae40d4d91525085137196e84eb0ed49cf65b5e96e5c3ecdadd8bd0fac2/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c799d49f1a5544a0ef7591c0ee75e0d6b93d6f56dc7a4979f59f7518d2872216", size = 4445594, upload-time = "2026-05-06T06:18:04.219Z" },
{ url = "https://files.pythonhosted.org/packages/f5/1e/fc7e9baf14152662ef0b35fa52a6e889f770a7ed14ac239de3c829ecb47e/hf_xet-1.1.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:713f2bff61b252f8523739969f247aa354ad8e6d869b8281e174e2ea1bb8d604", size = 3348184, upload-time = "2025-08-06T00:30:54.105Z" }, { url = "https://files.pythonhosted.org/packages/13/59/c74efbbd4e8728172b2cc72a2bc014d2947a4b7bdced932fbd3f5da1a4e5/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2baea1b0b989e5c152fe81425f7745ddc8901280ba3d97c98d8cdece7b706c60", size = 4663995, upload-time = "2026-05-06T06:18:06.1Z" },
{ url = "https://files.pythonhosted.org/packages/a3/73/e354eae84ceff117ec3560141224724794828927fcc013c5b449bf0b8745/hf_xet-1.1.7-cp37-abi3-win_amd64.whl", hash = "sha256:2e356da7d284479ae0f1dea3cf5a2f74fdf925d6dca84ac4341930d892c7cb34", size = 2820008, upload-time = "2025-08-06T00:30:57.056Z" }, { url = "https://files.pythonhosted.org/packages/73/32/8e1e0410af64cda9b139d1dcebdc993a8ff9c8c7c0e2696ae356d75ccc0d/hf_xet-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:526345b3ed45f374f6317349df489167606736c876241ba984105afe7fd4839d", size = 3966608, upload-time = "2026-05-06T06:18:19.74Z" },
{ url = "https://files.pythonhosted.org/packages/fc/34/a8febc8f4edbea8b3e21b02ebc8b628679b84ba7e45cde624a7736b51500/hf_xet-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:786d28e2eb8315d5035544b9d137b4a842d600c434bb91bf7d0d953cce906ad4", size = 3796946, upload-time = "2026-05-06T06:18:17.568Z" },
{ url = "https://files.pythonhosted.org/packages/2a/20/8fc8996afe5815fa1a6be8e9e5c02f24500f409d599e905800d498a4e14d/hf_xet-1.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:872d5601e6deea30d15865ede55d29eac6daf5a534ab417b99b6ef6b076dd96c", size = 4023495, upload-time = "2026-05-06T06:18:01.94Z" },
{ url = "https://files.pythonhosted.org/packages/32/6a/93d84463c00cecb561a7508aa6303e35ee2894294eac14245526924415fe/hf_xet-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9929561f5abf4581c8ea79587881dfef6b8abb2a0d8a51915936fc2a614f4e73", size = 3792731, upload-time = "2026-05-06T06:18:00.021Z" },
{ url = "https://files.pythonhosted.org/packages/9d/5a/8ec8e0c863b382d00b3c2e2af6ded6b06371be617144a625903a6d562f4b/hf_xet-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7b7bbae318e583a86fb21e5a4a175d6721d628a2874f4bd022d0e660c32a682", size = 4456738, upload-time = "2026-05-06T06:17:49.574Z" },
{ url = "https://files.pythonhosted.org/packages/c5/ca/f7effa1a67717da2bcc6b6c28f71c6ca648c77acaec4e2c32f40cbe16d85/hf_xet-1.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cf7b2dc6f31a4ea754bb50f74cde482dcf5d366d184076d8530b9872787f3761", size = 4251622, upload-time = "2026-05-06T06:17:47.096Z" },
{ url = "https://files.pythonhosted.org/packages/65/f2/19247dba3e231cf77dec59ddfb878f00057635ff773d099c9b59d37812c3/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8dbcbab554c9ef158ef2c991545c3e970ddd8cc7acdcd0a78c5a41095dab4ded", size = 4445667, upload-time = "2026-05-06T06:18:11.983Z" },
{ url = "https://files.pythonhosted.org/packages/7f/64/6f116801a3bcfb6f59f5c251f48cadc47ea54026441c4a385079286a94fa/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5906bf7718d3636dc13402914736abe723492cb730f744834f5f5b67d3a12702", size = 4664619, upload-time = "2026-05-06T06:18:13.771Z" },
{ url = "https://files.pythonhosted.org/packages/5c/e8/069542d37946ed08669b127e1496fa99e78196d71de8d41eda5e9f1b7a58/hf_xet-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f3dc2248fc01cc0a00cd392ab497f1ca373fcbc7e3f2da1f452480b384e839e", size = 3966802, upload-time = "2026-05-06T06:18:28.162Z" },
{ url = "https://files.pythonhosted.org/packages/f9/91/fc6fdec27b14d04e88c386ac0a0129732b53fa23f7c4a78f4b83a039c567/hf_xet-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b285cea1b5bab46b758772716ba8d6854a1a0310fed1c249d678a8b38601e5a0", size = 3797168, upload-time = "2026-05-06T06:18:26.287Z" },
{ url = "https://files.pythonhosted.org/packages/3d/fb/69ff198a82cae7eb1a69fb84d93b3a3e4816564d76817fe541ddc96874eb/hf_xet-1.5.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dad0dc84e941b8ba3c860659fe1fdc35c049d47cce293f003287757e971a8f56", size = 4030814, upload-time = "2026-05-06T06:17:57.933Z" },
{ url = "https://files.pythonhosted.org/packages/9b/ff/edcc2b40162bef3ff78e14ab637e5f3b89243d6aee72f5949d3bb6a5af83/hf_xet-1.5.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fd6e5a9b0fdac4ed03ed45ef79254a655b1aaab514a02202617fbf643f5fdf7a", size = 3798444, upload-time = "2026-05-06T06:17:55.79Z" },
{ url = "https://files.pythonhosted.org/packages/49/4d/103f76b04310e5e57656696cc184690d20c466af0bca3ca88f8c8ea5d4f3/hf_xet-1.5.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3531b1823a0e6d77d80f9ed15ca0e00f0d115094f8ac033d5cae88f4564cc949", size = 4465986, upload-time = "2026-05-06T06:17:44.886Z" },
{ url = "https://files.pythonhosted.org/packages/c4/a2/546f47f464737b3edbab6f8ddb57f2599b93d2cbb66f06abb475ccb48651/hf_xet-1.5.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9a0ee58cd18d5ea799f7ed11290bbccbe56bdd8b1d97ca74b9cc49a3945d7a3b", size = 4259865, upload-time = "2026-05-06T06:17:42.639Z" },
{ url = "https://files.pythonhosted.org/packages/95/7f/1be593c1f28613be2e196473481cd81bfc5910795e30a34e8f744f6cac4f/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e60df5a42e9bed8628b6416af2cba4cba57ae9f02de226a06b020d98e1aab18", size = 4459835, upload-time = "2026-05-06T06:18:08.026Z" },
{ url = "https://files.pythonhosted.org/packages/aa/b2/703569fc881f3284487e68cda7b42179978480da3c438042a6bbbb4a671c/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4b35549ce62601b84da4ff9b24d970032ace3d4430f52d91bcbb26c901d6c690", size = 4672414, upload-time = "2026-05-06T06:18:09.864Z" },
{ url = "https://files.pythonhosted.org/packages/af/37/1b6def445c567286b50aa3b33828158e135b1be44938dde59f11382a500c/hf_xet-1.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:2806c7c17b4d23f8d88f7c4814f838c3b6150773fe339c20af23e1cfaf2797e4", size = 3977238, upload-time = "2026-05-06T06:18:23.621Z" },
{ url = "https://files.pythonhosted.org/packages/62/94/3b66b148778ee100dcfd69c2ca22b57b41b44d3063ceec934f209e9184ce/hf_xet-1.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:b6c9df403040248c76d808d3e047d64db2d923bae593eb244c41e425cf6cd7be", size = 3806916, upload-time = "2026-05-06T06:18:21.7Z" },
] ]
[[package]] [[package]]
@@ -857,21 +874,22 @@ wheels = [
[[package]] [[package]]
name = "huggingface-hub" name = "huggingface-hub"
version = "0.36.2" version = "1.13.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "filelock" }, { name = "filelock" },
{ name = "fsspec" }, { name = "fsspec" },
{ name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
{ name = "httpx" },
{ name = "packaging" }, { name = "packaging" },
{ name = "pyyaml" }, { name = "pyyaml" },
{ name = "requests" },
{ name = "tqdm" }, { name = "tqdm" },
{ name = "typer" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/7c/b7/8cb61d2eece5fb05a83271da168186721c450eb74e3c31f7ef3169fa475b/huggingface_hub-0.36.2.tar.gz", hash = "sha256:1934304d2fb224f8afa3b87007d58501acfda9215b334eed53072dd5e815ff7a", size = 649782, upload-time = "2026-02-06T09:24:13.098Z" } sdist = { url = "https://files.pythonhosted.org/packages/89/ff/ec7ed2eb43bd7ce8bb2233d109cc235c3e807ffe5e469dc09db261fac05e/huggingface_hub-1.13.0.tar.gz", hash = "sha256:f6df2dac5abe82ce2fe05873d10d5ff47bc677d616a2f521f4ee26db9415d9d0", size = 781788, upload-time = "2026-04-30T11:57:33.858Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/af/48ac8483240de756d2438c380746e7130d1c6f75802ef22f3c6d49982787/huggingface_hub-0.36.2-py3-none-any.whl", hash = "sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270", size = 566395, upload-time = "2026-02-06T09:24:11.133Z" }, { url = "https://files.pythonhosted.org/packages/93/db/4b1cdae9460ae1f3ca020cd767f013430ce23eb1d9c890ae3a0609b38d26/huggingface_hub-1.13.0-py3-none-any.whl", hash = "sha256:e942cb50d6a08dd5306688b1ac05bda157fd2fcc88b63dae405f7bd0d3234005", size = 660643, upload-time = "2026-04-30T11:57:31.802Z" },
] ]
[[package]] [[package]]
@@ -985,9 +1003,9 @@ requires-dist = [
{ name = "aiocache", specifier = ">=0.12.1,<1.0" }, { name = "aiocache", specifier = ">=0.12.1,<1.0" },
{ name = "fastapi", specifier = ">=0.95.2,<1.0" }, { name = "fastapi", specifier = ">=0.95.2,<1.0" },
{ name = "gunicorn", specifier = ">=21.1.0" }, { name = "gunicorn", specifier = ">=21.1.0" },
{ name = "huggingface-hub", specifier = ">=0.20.1,<1.0" }, { name = "huggingface-hub", specifier = ">=1.0,<2.0" },
{ name = "insightface", specifier = ">=0.7.3,<1.0" }, { name = "insightface", specifier = ">=0.7.3,<1.0" },
{ name = "numpy", specifier = "<2.4.0" }, { name = "numpy", specifier = ">=2.4.0,<3.0" },
{ name = "onnxruntime", marker = "extra == 'armnn'", specifier = ">=1.23.2,<2" }, { name = "onnxruntime", marker = "extra == 'armnn'", specifier = ">=1.23.2,<2" },
{ name = "onnxruntime", marker = "extra == 'cpu'", specifier = ">=1.23.2,<2" }, { name = "onnxruntime", marker = "extra == 'cpu'", specifier = ">=1.23.2,<2" },
{ name = "onnxruntime", marker = "extra == 'rknn'", specifier = ">=1.23.2,<2" }, { name = "onnxruntime", marker = "extra == 'rknn'", specifier = ">=1.23.2,<2" },
@@ -996,7 +1014,7 @@ requires-dist = [
{ name = "onnxruntime-openvino", marker = "extra == 'openvino'", specifier = ">=1.24.1,<2" }, { name = "onnxruntime-openvino", marker = "extra == 'openvino'", specifier = ">=1.24.1,<2" },
{ name = "opencv-python-headless", specifier = ">=4.7.0.72,<5.0" }, { name = "opencv-python-headless", specifier = ">=4.7.0.72,<5.0" },
{ name = "orjson", specifier = ">=3.9.5" }, { name = "orjson", specifier = ">=3.9.5" },
{ name = "pillow", specifier = ">=12.2,<12.3" }, { name = "pillow", specifier = ">=12.2,<13" },
{ name = "pydantic", specifier = ">=2.0.0,<3" }, { name = "pydantic", specifier = ">=2.0.0,<3" },
{ name = "pydantic-settings", specifier = ">=2.5.2,<3" }, { name = "pydantic-settings", specifier = ">=2.5.2,<3" },
{ name = "python-multipart", specifier = ">=0.0.6,<1.0" }, { name = "python-multipart", specifier = ">=0.0.6,<1.0" },
@@ -1540,83 +1558,81 @@ wheels = [
[[package]] [[package]]
name = "numpy" name = "numpy"
version = "2.3.5" version = "2.4.4"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", size = 17034641, upload-time = "2025-11-16T22:49:19.336Z" }, { url = "https://files.pythonhosted.org/packages/ef/c6/4218570d8c8ecc9704b5157a3348e486e84ef4be0ed3e38218ab473c83d2/numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db", size = 16976799, upload-time = "2026-03-29T13:18:15.438Z" },
{ url = "https://files.pythonhosted.org/packages/2a/ea/25e26fa5837106cde46ae7d0b667e20f69cbbc0efd64cba8221411ab26ae/numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218", size = 12528324, upload-time = "2025-11-16T22:49:22.582Z" }, { url = "https://files.pythonhosted.org/packages/dd/92/b4d922c4a5f5dab9ed44e6153908a5c665b71acf183a83b93b690996e39b/numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0", size = 14971552, upload-time = "2026-03-29T13:18:18.606Z" },
{ url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d", size = 5356872, upload-time = "2025-11-16T22:49:25.408Z" }, { url = "https://files.pythonhosted.org/packages/8a/dc/df98c095978fa6ee7b9a9387d1d58cbb3d232d0e69ad169a4ce784bde4fd/numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015", size = 5476566, upload-time = "2026-03-29T13:18:21.532Z" },
{ url = "https://files.pythonhosted.org/packages/5c/bb/35ef04afd567f4c989c2060cde39211e4ac5357155c1833bcd1166055c61/numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5", size = 6893148, upload-time = "2025-11-16T22:49:27.549Z" }, { url = "https://files.pythonhosted.org/packages/28/34/b3fdcec6e725409223dd27356bdf5a3c2cc2282e428218ecc9cb7acc9763/numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40", size = 6806482, upload-time = "2026-03-29T13:18:23.634Z" },
{ url = "https://files.pythonhosted.org/packages/f2/2b/05bbeb06e2dff5eab512dfc678b1cc5ee94d8ac5956a0885c64b6b26252b/numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7", size = 14557282, upload-time = "2025-11-16T22:49:30.964Z" }, { url = "https://files.pythonhosted.org/packages/68/62/63417c13aa35d57bee1337c67446761dc25ea6543130cf868eace6e8157b/numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d", size = 15973376, upload-time = "2026-03-29T13:18:26.677Z" },
{ url = "https://files.pythonhosted.org/packages/65/fb/2b23769462b34398d9326081fad5655198fcf18966fcb1f1e49db44fbf31/numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4", size = 16897903, upload-time = "2025-11-16T22:49:34.191Z" }, { url = "https://files.pythonhosted.org/packages/cf/c5/9fcb7e0e69cef59cf10c746b84f7d58b08bc66a6b7d459783c5a4f6101a6/numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502", size = 16925137, upload-time = "2026-03-29T13:18:30.14Z" },
{ url = "https://files.pythonhosted.org/packages/ac/14/085f4cf05fc3f1e8aa95e85404e984ffca9b2275a5dc2b1aae18a67538b8/numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e", size = 16341672, upload-time = "2025-11-16T22:49:37.2Z" }, { url = "https://files.pythonhosted.org/packages/7e/43/80020edacb3f84b9efdd1591120a4296462c23fd8db0dde1666f6ef66f13/numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd", size = 17329414, upload-time = "2026-03-29T13:18:33.733Z" },
{ url = "https://files.pythonhosted.org/packages/6f/3b/1f73994904142b2aa290449b3bb99772477b5fd94d787093e4f24f5af763/numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748", size = 18838896, upload-time = "2025-11-16T22:49:39.727Z" }, { url = "https://files.pythonhosted.org/packages/fd/06/af0658593b18a5f73532d377188b964f239eb0894e664a6c12f484472f97/numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5", size = 18658397, upload-time = "2026-03-29T13:18:37.511Z" },
{ url = "https://files.pythonhosted.org/packages/cd/b9/cf6649b2124f288309ffc353070792caf42ad69047dcc60da85ee85fea58/numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c", size = 6563608, upload-time = "2025-11-16T22:49:42.079Z" }, { url = "https://files.pythonhosted.org/packages/e6/ce/13a09ed65f5d0ce5c7dd0669250374c6e379910f97af2c08c57b0608eee4/numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e", size = 6239499, upload-time = "2026-03-29T13:18:40.372Z" },
{ url = "https://files.pythonhosted.org/packages/aa/44/9fe81ae1dcc29c531843852e2874080dc441338574ccc4306b39e2ff6e59/numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c", size = 13078442, upload-time = "2025-11-16T22:49:43.99Z" }, { url = "https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e", size = 12614257, upload-time = "2026-03-29T13:18:42.95Z" },
{ url = "https://files.pythonhosted.org/packages/6d/a7/f99a41553d2da82a20a2f22e93c94f928e4490bb447c9ff3c4ff230581d3/numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa", size = 10458555, upload-time = "2025-11-16T22:49:47.092Z" }, { url = "https://files.pythonhosted.org/packages/87/c5/8168052f080c26fa984c413305012be54741c9d0d74abd7fbeeccae3889f/numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e", size = 10486775, upload-time = "2026-03-29T13:18:45.835Z" },
{ url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" }, { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" },
{ url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" }, { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" },
{ url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" }, { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" },
{ url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" }, { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" },
{ url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" }, { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" },
{ url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" }, { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" },
{ url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" }, { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" },
{ url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" }, { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" },
{ url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" }, { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" },
{ url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" }, { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" },
{ url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" }, { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" },
{ url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" }, { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" },
{ url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652, upload-time = "2025-11-16T22:50:21.487Z" }, { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" },
{ url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172, upload-time = "2025-11-16T22:50:24.562Z" }, { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" },
{ url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990, upload-time = "2025-11-16T22:50:26.47Z" }, { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" },
{ url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902, upload-time = "2025-11-16T22:50:28.861Z" }, { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" },
{ url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430, upload-time = "2025-11-16T22:50:31.56Z" }, { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" },
{ url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551, upload-time = "2025-11-16T22:50:34.242Z" }, { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" },
{ url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275, upload-time = "2025-11-16T22:50:37.651Z" }, { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" },
{ url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637, upload-time = "2025-11-16T22:50:40.11Z" }, { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" },
{ url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090, upload-time = "2025-11-16T22:50:42.503Z" }, { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" },
{ url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710, upload-time = "2025-11-16T22:50:44.971Z" }, { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" },
{ url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292, upload-time = "2025-11-16T22:50:47.715Z" }, { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" },
{ url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897, upload-time = "2025-11-16T22:50:51.327Z" }, { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" },
{ url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391, upload-time = "2025-11-16T22:50:54.542Z" }, { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" },
{ url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275, upload-time = "2025-11-16T22:50:56.794Z" }, { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" },
{ url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855, upload-time = "2025-11-16T22:50:59.208Z" }, { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" },
{ url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359, upload-time = "2025-11-16T22:51:01.991Z" }, { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" },
{ url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374, upload-time = "2025-11-16T22:51:05.291Z" }, { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" },
{ url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587, upload-time = "2025-11-16T22:51:08.585Z" }, { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" },
{ url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" }, { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" },
{ url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" }, { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" },
{ url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" }, { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" },
{ url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706, upload-time = "2025-11-16T22:51:19.558Z" }, { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" },
{ url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507, upload-time = "2025-11-16T22:51:22.492Z" }, { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" },
{ url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049, upload-time = "2025-11-16T22:51:25.171Z" }, { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" },
{ url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603, upload-time = "2025-11-16T22:51:27Z" }, { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" },
{ url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696, upload-time = "2025-11-16T22:51:29.402Z" }, { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" },
{ url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350, upload-time = "2025-11-16T22:51:32.167Z" }, { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" },
{ url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190, upload-time = "2025-11-16T22:51:35.403Z" }, { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" },
{ url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749, upload-time = "2025-11-16T22:51:39.698Z" }, { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" },
{ url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432, upload-time = "2025-11-16T22:51:42.476Z" }, { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" },
{ url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388, upload-time = "2025-11-16T22:51:45.275Z" }, { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" },
{ url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651, upload-time = "2025-11-16T22:51:47.749Z" }, { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" },
{ url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503, upload-time = "2025-11-16T22:51:50.443Z" }, { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" },
{ url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612, upload-time = "2025-11-16T22:51:53.609Z" }, { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" },
{ url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042, upload-time = "2025-11-16T22:51:56.213Z" }, { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" },
{ url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502, upload-time = "2025-11-16T22:51:58.584Z" }, { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" },
{ url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962, upload-time = "2025-11-16T22:52:01.698Z" }, { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" },
{ url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054, upload-time = "2025-11-16T22:52:04.267Z" }, { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" },
{ url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613, upload-time = "2025-11-16T22:52:08.651Z" }, { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" },
{ url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147, upload-time = "2025-11-16T22:52:11.453Z" }, { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" },
{ url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806, upload-time = "2025-11-16T22:52:14.641Z" }, { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" },
{ url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760, upload-time = "2025-11-16T22:52:17.975Z" }, { url = "https://files.pythonhosted.org/packages/6b/33/8fae8f964a4f63ed528264ddf25d2b683d0b663e3cba26961eb838a7c1bd/numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4", size = 16854491, upload-time = "2026-03-29T13:21:38.03Z" },
{ url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" }, { url = "https://files.pythonhosted.org/packages/bc/d0/1aabee441380b981cf8cdda3ae7a46aa827d1b5a8cce84d14598bc94d6d9/numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e", size = 14895830, upload-time = "2026-03-29T13:21:41.509Z" },
{ url = "https://files.pythonhosted.org/packages/c6/65/f9dea8e109371ade9c782b4e4756a82edf9d3366bca495d84d79859a0b79/numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310", size = 16910689, upload-time = "2025-11-16T22:52:23.247Z" }, { url = "https://files.pythonhosted.org/packages/a5/b8/aafb0d1065416894fccf4df6b49ef22b8db045187949545bced89c034b8e/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c", size = 5400927, upload-time = "2026-03-29T13:21:44.747Z" },
{ url = "https://files.pythonhosted.org/packages/00/4f/edb00032a8fb92ec0a679d3830368355da91a69cab6f3e9c21b64d0bb986/numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c", size = 12457053, upload-time = "2025-11-16T22:52:26.367Z" }, { url = "https://files.pythonhosted.org/packages/d6/77/063baa20b08b431038c7f9ff5435540c7b7265c78cf56012a483019ca72d/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3", size = 6715557, upload-time = "2026-03-29T13:21:47.406Z" },
{ url = "https://files.pythonhosted.org/packages/16/a4/e8a53b5abd500a63836a29ebe145fc1ab1f2eefe1cfe59276020373ae0aa/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18", size = 5285635, upload-time = "2025-11-16T22:52:29.266Z" }, { url = "https://files.pythonhosted.org/packages/c7/a8/379542d45a14f149444c5c4c4e7714707239ce9cc1de8c2803958889da14/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7", size = 15804253, upload-time = "2026-03-29T13:21:50.753Z" },
{ url = "https://files.pythonhosted.org/packages/a3/2f/37eeb9014d9c8b3e9c55bc599c68263ca44fdbc12a93e45a21d1d56df737/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff", size = 6801770, upload-time = "2025-11-16T22:52:31.421Z" }, { url = "https://files.pythonhosted.org/packages/a2/c8/f0a45426d6d21e7ea3310a15cf90c43a14d9232c31a837702dba437f3373/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f", size = 16753552, upload-time = "2026-03-29T13:21:54.344Z" },
{ url = "https://files.pythonhosted.org/packages/7d/e4/68d2f474df2cb671b2b6c2986a02e520671295647dad82484cde80ca427b/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb", size = 14391768, upload-time = "2025-11-16T22:52:33.593Z" }, { url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" },
{ url = "https://files.pythonhosted.org/packages/b8/50/94ccd8a2b141cb50651fddd4f6a48874acb3c91c8f0842b08a6afc4b0b21/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7", size = 16729263, upload-time = "2025-11-16T22:52:36.369Z" },
{ url = "https://files.pythonhosted.org/packages/2d/ee/346fa473e666fe14c52fcdd19ec2424157290a032d4c41f98127bfb31ac7/numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425", size = 12967213, upload-time = "2025-11-16T22:52:39.38Z" },
] ]
[[package]] [[package]]
@@ -2296,11 +2312,11 @@ wheels = [
[[package]] [[package]]
name = "python-multipart" name = "python-multipart"
version = "0.0.26" version = "0.0.27"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" } sdist = { url = "https://files.pythonhosted.org/packages/69/9b/f23807317a113dc36e74e75eb265a02dd1a4d9082abc3c1064acd22997c4/python_multipart-0.0.27.tar.gz", hash = "sha256:9870a6a8c5a20a5bf4f07c017bd1489006ff8836cff097b6933355ee2b49b602", size = 44043, upload-time = "2026-04-27T10:51:26.649Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" }, { url = "https://files.pythonhosted.org/packages/99/78/4126abcbdbd3c559d43e0db7f7b9173fc6befe45d39a2856cc0b8ec2a5a6/python_multipart-0.0.27-py3-none-any.whl", hash = "sha256:6fccfad17a27334bd0193681b369f476eda3409f17381a2d65aa7df3f7275645", size = 29254, upload-time = "2026-04-27T10:51:24.997Z" },
] ]
[[package]] [[package]]
@@ -2769,6 +2785,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ea/f1/5e9b3ba5c7aa7ebfaf269657e728067d16a7c99401c7973ddf5f0cf121bd/shapely-2.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8cb8f17c377260452e9d7720eeaf59082c5f8ea48cf104524d953e5d36d4bdb7", size = 1723061, upload-time = "2025-05-19T11:04:40.082Z" }, { url = "https://files.pythonhosted.org/packages/ea/f1/5e9b3ba5c7aa7ebfaf269657e728067d16a7c99401c7973ddf5f0cf121bd/shapely-2.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8cb8f17c377260452e9d7720eeaf59082c5f8ea48cf104524d953e5d36d4bdb7", size = 1723061, upload-time = "2025-05-19T11:04:40.082Z" },
] ]
[[package]]
name = "shellingham"
version = "1.5.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
]
[[package]] [[package]]
name = "simple-websocket" name = "simple-websocket"
version = "1.1.0" version = "1.1.0"
@@ -2932,6 +2957,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/ad/7d47bbf2cae78ff79f29db0bed5016ec9c56b212a93fca624bb88b551a7c/tqdm-4.66.3-py3-none-any.whl", hash = "sha256:4f41d54107ff9a223dca80b53efe4fb654c67efaba7f47bada3ee9d50e05bd53", size = 78374, upload-time = "2024-05-02T21:44:01.541Z" }, { url = "https://files.pythonhosted.org/packages/d1/ad/7d47bbf2cae78ff79f29db0bed5016ec9c56b212a93fca624bb88b551a7c/tqdm-4.66.3-py3-none-any.whl", hash = "sha256:4f41d54107ff9a223dca80b53efe4fb654c67efaba7f47bada3ee9d50e05bd53", size = 78374, upload-time = "2024-05-02T21:44:01.541Z" },
] ]
[[package]]
name = "typer"
version = "0.25.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-doc" },
{ name = "click" },
{ name = "rich" },
{ name = "shellingham" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" },
]
[[package]] [[package]]
name = "types-pyyaml" name = "types-pyyaml"
version = "6.0.12.20260408" version = "6.0.12.20260408"
+3 -2
View File
@@ -11,13 +11,14 @@ config_roots = [
"web", "web",
"docs", "docs",
".github", ".github",
"machine-learning",
] ]
[tools] [tools]
node = "24.15.0" node = "24.15.0"
flutter = "3.41.7" flutter = "3.41.9"
pnpm = "10.33.1" pnpm = "10.33.1"
terragrunt = "1.0.2" terragrunt = "1.0.3"
opentofu = "1.11.6" opentofu = "1.11.6"
java = "21.0.2" java = "21.0.2"
+8 -1
View File
@@ -64,8 +64,15 @@ android {
} }
release { release {
signingConfig signingConfigs.release def hasKeystore = file("../key.jks").exists() && file("../key.jks").length() > 0
signingConfig hasKeystore ? signingConfigs.release : signingConfigs.debug
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
def prNumber = System.getenv("PR_NUMBER")
if (prNumber) {
applicationIdSuffix ".pr${prNumber}"
versionNameSuffix "-pr${prNumber}"
}
} }
} }
namespace 'app.alextran.immich' namespace 'app.alextran.immich'
+2 -2
View File
@@ -35,8 +35,8 @@ platform :android do
task: 'bundle', task: 'bundle',
build_type: 'Release', build_type: 'Release',
properties: { properties: {
"android.injected.version.code" => 3046, "android.injected.version.code" => 3047,
"android.injected.version.name" => "2.7.5", "android.injected.version.name" => "3.0.0",
} }
) )
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -78,7 +78,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>2.7.5</string> <string>3.0.0</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
-3
View File
@@ -2,9 +2,6 @@ import 'package:flutter/material.dart';
enum ImmichColorPreset { indigo, deepPurple, pink, red, orange, yellow, lime, green, cyan, slateGray } enum ImmichColorPreset { indigo, deepPurple, pink, red, orange, yellow, lime, green, cyan, slateGray }
const ImmichColorPreset defaultColorPreset = ImmichColorPreset.indigo;
const String defaultColorPresetName = "indigo";
const Color immichBrandColorLight = Color(0xFF4150AF); const Color immichBrandColorLight = Color(0xFF4150AF);
const Color immichBrandColorDark = Color(0xFFACCBFA); const Color immichBrandColorDark = Color(0xFFACCBFA);
const Color whiteOpacity75 = Color.fromRGBO(255, 255, 255, 0.75); const Color whiteOpacity75 = Color.fromRGBO(255, 255, 255, 0.75);
@@ -0,0 +1,22 @@
import 'package:immich_mobile/domain/models/config/cleanup_config.dart';
import 'package:immich_mobile/domain/models/config/theme_config.dart';
class AppConfig {
final ThemeConfig theme;
final CleanupConfig cleanup;
const AppConfig({this.theme = const .new(), this.cleanup = const .new()});
AppConfig copyWith({ThemeConfig? theme, CleanupConfig? cleanup}) =>
.new(theme: theme ?? this.theme, cleanup: cleanup ?? this.cleanup);
@override
bool operator ==(Object other) =>
identical(this, other) || (other is AppConfig && other.theme == theme && other.cleanup == cleanup);
@override
int get hashCode => Object.hash(theme, cleanup);
@override
String toString() => 'AppConfig(theme: $theme, cleanup: $cleanup)';
}
@@ -0,0 +1,48 @@
import 'package:immich_mobile/constants/enums.dart';
class CleanupConfig {
final bool keepFavorites;
final AssetKeepType keepMediaType;
final List<String> keepAlbumIds;
final int cutoffDaysAgo;
final bool defaultsInitialized;
const CleanupConfig({
this.keepFavorites = true,
this.keepMediaType = AssetKeepType.none,
this.keepAlbumIds = const [],
this.cutoffDaysAgo = -1,
this.defaultsInitialized = false,
});
CleanupConfig copyWith({
bool? keepFavorites,
AssetKeepType? keepMediaType,
List<String>? keepAlbumIds,
int? cutoffDaysAgo,
bool? defaultsInitialized,
}) => .new(
keepFavorites: keepFavorites ?? this.keepFavorites,
keepMediaType: keepMediaType ?? this.keepMediaType,
keepAlbumIds: keepAlbumIds ?? this.keepAlbumIds,
cutoffDaysAgo: cutoffDaysAgo ?? this.cutoffDaysAgo,
defaultsInitialized: defaultsInitialized ?? this.defaultsInitialized,
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is CleanupConfig &&
other.keepFavorites == keepFavorites &&
other.keepMediaType == keepMediaType &&
other.keepAlbumIds == keepAlbumIds &&
other.cutoffDaysAgo == cutoffDaysAgo &&
other.defaultsInitialized == defaultsInitialized);
@override
int get hashCode => Object.hash(keepFavorites, keepMediaType, keepAlbumIds, cutoffDaysAgo, defaultsInitialized);
@override
String toString() =>
'CleanupConfig(keepFavorites: $keepFavorites, keepMediaType: $keepMediaType, keepAlbumIds: $keepAlbumIds, cutoffDaysAgo: $cutoffDaysAgo, defaultsInitialized: $defaultsInitialized)';
}
@@ -0,0 +1,18 @@
import 'package:immich_mobile/domain/models/log.model.dart';
class SystemConfig {
final LogLevel logLevel;
const SystemConfig({this.logLevel = .info});
SystemConfig copyWith({LogLevel? logLevel}) => SystemConfig(logLevel: logLevel ?? this.logLevel);
@override
bool operator ==(Object other) => identical(this, other) || (other is SystemConfig && other.logLevel == logLevel);
@override
int get hashCode => logLevel.hashCode;
@override
String toString() => 'SystemConfig(logLevel: $logLevel)';
}
@@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/constants/colors.dart';
class ThemeConfig {
final ThemeMode mode;
final ImmichColorPreset primaryColor;
final bool dynamicTheme;
final bool colorfulInterface;
const ThemeConfig({
this.mode = .system,
this.primaryColor = .indigo,
this.dynamicTheme = false,
this.colorfulInterface = true,
});
ThemeConfig copyWith({
ThemeMode? mode,
ImmichColorPreset? primaryColor,
bool? dynamicTheme,
bool? colorfulInterface,
}) => .new(
mode: mode ?? this.mode,
primaryColor: primaryColor ?? this.primaryColor,
dynamicTheme: dynamicTheme ?? this.dynamicTheme,
colorfulInterface: colorfulInterface ?? this.colorfulInterface,
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is ThemeConfig &&
other.mode == mode &&
other.primaryColor == primaryColor &&
other.dynamicTheme == dynamicTheme &&
other.colorfulInterface == colorfulInterface);
@override
int get hashCode => Object.hash(mode, primaryColor, dynamicTheme, colorfulInterface);
@override
String toString() =>
'ThemeConfig(mode: $mode, primaryColor: $primaryColor, dynamicTheme: $dynamicTheme, colorfulInterface: $colorfulInterface)';
}
+150
View File
@@ -0,0 +1,150 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/constants/colors.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/config/app_config.dart';
import 'package:immich_mobile/domain/models/config/system_config.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
enum MetadataDomain<T extends Object> {
appConfig<AppConfig>('config.app'),
systemConfig<SystemConfig>('config.system');
final String prefix;
const MetadataDomain(this.prefix);
}
enum MetadataKey<T extends Object> {
// Theme
themePrimaryColor<ImmichColorPreset>(.appConfig, 'theme.primaryColor', .indigo, _EnumCodec(ImmichColorPreset.values)),
themeMode<ThemeMode>(.appConfig, 'theme.mode', .system, _EnumCodec(ThemeMode.values)),
themeDynamic<bool>(.appConfig, 'theme.dynamic', false),
themeColorfulInterface<bool>(.appConfig, 'theme.colorfulInterface', true),
// Log
logLevel<LogLevel>(.systemConfig, 'log.level', .info, _EnumCodec(LogLevel.values)),
// Cleanup
cleanupKeepFavorites<bool>(.appConfig, 'cleanup.keepFavorites', true),
cleanupKeepMediaType<AssetKeepType>(
.appConfig,
'cleanup.keepMediaType',
AssetKeepType.none,
_EnumCodec(AssetKeepType.values),
),
cleanupKeepAlbumIds<List<String>>(.appConfig, 'cleanup.keepAlbumIds', [], _ListCodec(_PrimitiveCodec.string)),
cleanupCutoffDaysAgo<int>(.appConfig, 'cleanup.cutoffDaysAgo', -1),
cleanupDefaultsInitialized<bool>(.appConfig, 'cleanup.defaultsInitialized', false);
final MetadataDomain domain;
final String name;
final T defaultValue;
final _MetadataCodec<T>? _codecOverride;
const MetadataKey(this.domain, this.name, this.defaultValue, [this._codecOverride]);
String get key => '${domain.prefix}.$name';
_MetadataCodec<T> get _codec => _codecOverride ?? _MetadataCodec.forPrimitive(defaultValue);
String encode(T value) => _codec.encode(value);
T decode(String raw) => _codec.decode(raw) ?? defaultValue;
static Map<String, MetadataKey<Object>> asKeyMap() => {for (var value in MetadataKey.values) value.key: value};
}
sealed class _MetadataCodec<T extends Object> {
const _MetadataCodec();
String encode(T value);
T? decode(String raw);
static const Map<Type, _MetadataCodec<Object>> _primitives = {
int: _PrimitiveCodec.integer,
double: _PrimitiveCodec.real,
bool: _PrimitiveCodec.boolean,
String: _PrimitiveCodec.string,
DateTime: _DateTimeCodec(),
};
static _MetadataCodec<T> forPrimitive<T extends Object>(T sample) {
final codec = _primitives[sample.runtimeType];
if (codec == null) {
throw StateError(
'No primitive codec for ${sample.runtimeType}. Provide an explicit codec when defining the MetadataKey.',
);
}
return codec as _MetadataCodec<T>;
}
}
final class _EnumCodec<T extends Enum> extends _MetadataCodec<T> {
final List<T> values;
const _EnumCodec(this.values);
@override
String encode(T value) => value.name;
@override
T? decode(String raw) => values.firstWhereOrNull((v) => v.name == raw);
}
final class _DateTimeCodec extends _MetadataCodec<DateTime> {
const _DateTimeCodec();
@override
String encode(DateTime value) => value.toIso8601String();
@override
DateTime? decode(String raw) => DateTime.tryParse(raw);
}
final class _ListCodec<T extends Object> extends _MetadataCodec<List<T>> {
final _MetadataCodec<T> _elementCodec;
const _ListCodec(this._elementCodec);
@override
String encode(List<T> value) => jsonEncode(value.map(_elementCodec.encode).toList());
@override
List<T>? decode(String raw) {
try {
final decoded = jsonDecode(raw);
if (decoded is! List) return null;
final result = <T>[];
for (final item in decoded) {
if (item is! String) return null;
final element = _elementCodec.decode(item);
if (element == null) return null;
result.add(element);
}
return result;
} on FormatException {
return null;
}
}
}
final class _PrimitiveCodec<T extends Object> extends _MetadataCodec<T> {
final T? Function(String) _parse;
const _PrimitiveCodec._(this._parse);
@override
String encode(T value) => value.toString();
@override
T? decode(String raw) => _parse(raw);
static const integer = _PrimitiveCodec<int>._(int.tryParse);
static const real = _PrimitiveCodec<double>._(double.tryParse);
static const boolean = _PrimitiveCodec<bool>._(bool.tryParse);
static const string = _PrimitiveCodec<String>._(_identity);
static String? _identity(String s) => s;
}
+12 -13
View File
@@ -22,7 +22,6 @@ enum StoreKey<T> {
// user settings from [AppSettingsEnum] below: // user settings from [AppSettingsEnum] below:
loadPreview<bool>._(100), loadPreview<bool>._(100),
loadOriginal<bool>._(101), loadOriginal<bool>._(101),
themeMode<String>._(102),
tilesPerRow<int>._(103), tilesPerRow<int>._(103),
dynamicLayout<bool>._(104), dynamicLayout<bool>._(104),
groupAssetsBy<int>._(105), groupAssetsBy<int>._(105),
@@ -35,7 +34,6 @@ enum StoreKey<T> {
albumThumbnailCacheSize<int>._(112), albumThumbnailCacheSize<int>._(112),
selectedAlbumSortOrder<int>._(113), selectedAlbumSortOrder<int>._(113),
advancedTroubleshooting<bool>._(114), advancedTroubleshooting<bool>._(114),
logLevel<int>._(115),
preferRemoteImage<bool>._(116), preferRemoteImage<bool>._(116),
loopVideo<bool>._(117), loopVideo<bool>._(117),
// map related settings // map related settings
@@ -50,11 +48,6 @@ enum StoreKey<T> {
enableHapticFeedback<bool>._(126), enableHapticFeedback<bool>._(126),
customHeaders<String>._(127), customHeaders<String>._(127),
// theme settings
primaryColor<String>._(128),
dynamicTheme<bool>._(129),
colorfulInterface<bool>._(130),
syncAlbums<bool>._(131), syncAlbums<bool>._(131),
// Auto endpoint switching // Auto endpoint switching
@@ -88,13 +81,19 @@ enum StoreKey<T> {
shouldResetSync<bool>._(1007), shouldResetSync<bool>._(1007),
// Free up space // Free up space
cleanupKeepFavorites<bool>._(1008), syncMigrationStatus<String>._(1013),
cleanupKeepMediaType<int>._(1009),
cleanupKeepAlbumIds<String>._(1010),
cleanupCutoffDaysAgo<int>._(1011),
cleanupDefaultsInitialized<bool>._(1012),
syncMigrationStatus<String>._(1013); // Legacy keys that have been migrated to the new metadata store
legacyPrimaryColor<String>._(128),
legacyDynamicTheme<bool>._(129),
legacyColorfulInterface<bool>._(130),
legacyThemeMode<String>._(102),
legacyCleanupKeepFavorites<bool>._(1008),
legacyCleanupKeepMediaType<int>._(1009),
legacyCleanupKeepAlbumIds<String>._(1010),
legacyCleanupCutoffDaysAgo<int>._(1011),
legacyCleanupDefaultsInitialized<bool>._(1012),
legacyLogLevel<int>._(115);
const StoreKey._(this.id); const StoreKey._(this.id);
final int id; final int id;
@@ -176,15 +176,8 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
final backgroundSyncManager = _ref?.read(backgroundSyncProvider); final backgroundSyncManager = _ref?.read(backgroundSyncProvider);
final nativeSyncApi = _ref?.read(nativeSyncApiProvider); final nativeSyncApi = _ref?.read(nativeSyncApiProvider);
await _drift.close();
await _driftLogger.close();
_ref?.dispose();
_ref = null;
_cancellationToken.complete();
_logger.info("Cleaning up background worker"); _logger.info("Cleaning up background worker");
_cancellationToken.complete();
final cleanupFutures = [ final cleanupFutures = [
nativeSyncApi?.cancelHashing(), nativeSyncApi?.cancelHashing(),
workerManagerPatch.dispose().catchError((_) async { workerManagerPatch.dispose().catchError((_) async {
@@ -195,10 +188,15 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
Store.dispose(), Store.dispose(),
backgroundSyncManager?.cancel(), backgroundSyncManager?.cancel(),
_drift.optimize(allTables: true),
]; ];
await Future.wait(cleanupFutures.nonNulls); await Future.wait(cleanupFutures.nonNulls);
_logger.info("Background worker resources cleaned up"); await _drift.close();
await _driftLogger.close();
_ref?.dispose();
_ref = null;
} catch (error, stack) { } catch (error, stack) {
dPrint(() => 'Failed to cleanup background worker: $error with stack: $stack'); dPrint(() => 'Failed to cleanup background worker: $error with stack: $stack');
} }
+13 -13
View File
@@ -2,20 +2,20 @@ import 'dart:async';
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/store.model.dart'; import 'package:immich_mobile/domain/models/metadata_key.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/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/metadata.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';
/// Service responsible for handling application logging. /// Service responsible for handling application logging.
/// ///
/// It listens to Dart's [Logger.root], buffers logs in memory (optionally), /// It listens to Dart's [Logger.root], buffers logs in memory (optionally),
/// writes them to a persistent [ILogRepository], and manages log levels /// writes them to a persistent [LogRepository], and manages log levels via
/// via [IStoreRepository] /// [MetadataRepository].
class LogService { class LogService {
final LogRepository _logRepository; final LogRepository _logRepository;
final DriftStoreRepository _storeRepository; final MetadataRepository _metadataRepository;
final List<LogMessage> _msgBuffer = []; final List<LogMessage> _msgBuffer = [];
@@ -38,12 +38,12 @@ class LogService {
static Future<LogService> init({ static Future<LogService> init({
required LogRepository logRepository, required LogRepository logRepository,
required DriftStoreRepository storeRepository, required MetadataRepository metadataRepository,
bool shouldBuffer = true, bool shouldBuffer = true,
}) async { }) async {
_instance ??= await create( _instance ??= await create(
logRepository: logRepository, logRepository: logRepository,
storeRepository: storeRepository, metadataRepository: metadataRepository,
shouldBuffer: shouldBuffer, shouldBuffer: shouldBuffer,
); );
return _instance!; return _instance!;
@@ -51,17 +51,17 @@ class LogService {
static Future<LogService> create({ static Future<LogService> create({
required LogRepository logRepository, required LogRepository logRepository,
required DriftStoreRepository storeRepository, required MetadataRepository metadataRepository,
bool shouldBuffer = true, bool shouldBuffer = true,
}) async { }) async {
final instance = LogService._(logRepository, storeRepository, shouldBuffer); final instance = LogService._(logRepository, metadataRepository, shouldBuffer);
await logRepository.truncate(limit: kLogTruncateLimit); await logRepository.truncate(limit: kLogTruncateLimit);
final level = await instance._storeRepository.tryGet(StoreKey.logLevel) ?? LogLevel.info.index; final level = instance._metadataRepository.systemConfig.logLevel;
Logger.root.level = Level.LEVELS.elementAtOrNull(level) ?? Level.INFO; Logger.root.level = Level.LEVELS.elementAtOrNull(level.index) ?? Level.INFO;
return instance; return instance;
} }
LogService._(this._logRepository, this._storeRepository, this._shouldBuffer) { LogService._(this._logRepository, this._metadataRepository, this._shouldBuffer) {
_logSubscription = Logger.root.onRecord.listen(_handleLogRecord); _logSubscription = Logger.root.onRecord.listen(_handleLogRecord);
} }
@@ -91,7 +91,7 @@ class LogService {
} }
Future<void> setLogLevel(LogLevel level) async { Future<void> setLogLevel(LogLevel level) async {
await _storeRepository.upsert(StoreKey.logLevel, level.index); await _metadataRepository.write(MetadataKey.logLevel, level);
Logger.root.level = level.toLevel(); Logger.root.level = level.toLevel();
} }
@@ -0,0 +1,18 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
class MetadataEntity extends Table with DriftDefaultsMixin {
const MetadataEntity();
TextColumn get key => text()();
TextColumn get value => text()();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {key};
@override
String get tableName => "metadata";
}
@@ -0,0 +1,429 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart'
as i1;
import 'package:immich_mobile/infrastructure/entities/metadata.entity.dart'
as i2;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
typedef $$MetadataEntityTableCreateCompanionBuilder =
i1.MetadataEntityCompanion Function({
required String key,
required String value,
i0.Value<DateTime> updatedAt,
});
typedef $$MetadataEntityTableUpdateCompanionBuilder =
i1.MetadataEntityCompanion Function({
i0.Value<String> key,
i0.Value<String> value,
i0.Value<DateTime> updatedAt,
});
class $$MetadataEntityTableFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$MetadataEntityTable> {
$$MetadataEntityTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnFilters<String> get key => $composableBuilder(
column: $table.key,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<String> get value => $composableBuilder(
column: $table.value,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<DateTime> get updatedAt => $composableBuilder(
column: $table.updatedAt,
builder: (column) => i0.ColumnFilters(column),
);
}
class $$MetadataEntityTableOrderingComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$MetadataEntityTable> {
$$MetadataEntityTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<String> get key => $composableBuilder(
column: $table.key,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<String> get value => $composableBuilder(
column: $table.value,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<DateTime> get updatedAt => $composableBuilder(
column: $table.updatedAt,
builder: (column) => i0.ColumnOrderings(column),
);
}
class $$MetadataEntityTableAnnotationComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$MetadataEntityTable> {
$$MetadataEntityTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumn<String> get key =>
$composableBuilder(column: $table.key, builder: (column) => column);
i0.GeneratedColumn<String> get value =>
$composableBuilder(column: $table.value, builder: (column) => column);
i0.GeneratedColumn<DateTime> get updatedAt =>
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
}
class $$MetadataEntityTableTableManager
extends
i0.RootTableManager<
i0.GeneratedDatabase,
i1.$MetadataEntityTable,
i1.MetadataEntityData,
i1.$$MetadataEntityTableFilterComposer,
i1.$$MetadataEntityTableOrderingComposer,
i1.$$MetadataEntityTableAnnotationComposer,
$$MetadataEntityTableCreateCompanionBuilder,
$$MetadataEntityTableUpdateCompanionBuilder,
(
i1.MetadataEntityData,
i0.BaseReferences<
i0.GeneratedDatabase,
i1.$MetadataEntityTable,
i1.MetadataEntityData
>,
),
i1.MetadataEntityData,
i0.PrefetchHooks Function()
> {
$$MetadataEntityTableTableManager(
i0.GeneratedDatabase db,
i1.$MetadataEntityTable table,
) : super(
i0.TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
i1.$$MetadataEntityTableFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
i1.$$MetadataEntityTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () => i1
.$$MetadataEntityTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback:
({
i0.Value<String> key = const i0.Value.absent(),
i0.Value<String> value = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
}) => i1.MetadataEntityCompanion(
key: key,
value: value,
updatedAt: updatedAt,
),
createCompanionCallback:
({
required String key,
required String value,
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
}) => i1.MetadataEntityCompanion.insert(
key: key,
value: value,
updatedAt: updatedAt,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
),
);
}
typedef $$MetadataEntityTableProcessedTableManager =
i0.ProcessedTableManager<
i0.GeneratedDatabase,
i1.$MetadataEntityTable,
i1.MetadataEntityData,
i1.$$MetadataEntityTableFilterComposer,
i1.$$MetadataEntityTableOrderingComposer,
i1.$$MetadataEntityTableAnnotationComposer,
$$MetadataEntityTableCreateCompanionBuilder,
$$MetadataEntityTableUpdateCompanionBuilder,
(
i1.MetadataEntityData,
i0.BaseReferences<
i0.GeneratedDatabase,
i1.$MetadataEntityTable,
i1.MetadataEntityData
>,
),
i1.MetadataEntityData,
i0.PrefetchHooks Function()
>;
class $MetadataEntityTable extends i2.MetadataEntity
with i0.TableInfo<$MetadataEntityTable, i1.MetadataEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
$MetadataEntityTable(this.attachedDatabase, [this._alias]);
static const i0.VerificationMeta _keyMeta = const i0.VerificationMeta('key');
@override
late final i0.GeneratedColumn<String> key = i0.GeneratedColumn<String>(
'key',
aliasedName,
false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
);
static const i0.VerificationMeta _valueMeta = const i0.VerificationMeta(
'value',
);
@override
late final i0.GeneratedColumn<String> value = i0.GeneratedColumn<String>(
'value',
aliasedName,
false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
);
static const i0.VerificationMeta _updatedAtMeta = const i0.VerificationMeta(
'updatedAt',
);
@override
late final i0.GeneratedColumn<DateTime> updatedAt =
i0.GeneratedColumn<DateTime>(
'updated_at',
aliasedName,
false,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: i3.currentDateAndTime,
);
@override
List<i0.GeneratedColumn> get $columns => [key, value, updatedAt];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'metadata';
@override
i0.VerificationContext validateIntegrity(
i0.Insertable<i1.MetadataEntityData> instance, {
bool isInserting = false,
}) {
final context = i0.VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('key')) {
context.handle(
_keyMeta,
key.isAcceptableOrUnknown(data['key']!, _keyMeta),
);
} else if (isInserting) {
context.missing(_keyMeta);
}
if (data.containsKey('value')) {
context.handle(
_valueMeta,
value.isAcceptableOrUnknown(data['value']!, _valueMeta),
);
} else if (isInserting) {
context.missing(_valueMeta);
}
if (data.containsKey('updated_at')) {
context.handle(
_updatedAtMeta,
updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta),
);
}
return context;
}
@override
Set<i0.GeneratedColumn> get $primaryKey => {key};
@override
i1.MetadataEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.MetadataEntityData(
key: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}key'],
)!,
value: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}value'],
)!,
updatedAt: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime,
data['${effectivePrefix}updated_at'],
)!,
);
}
@override
$MetadataEntityTable createAlias(String alias) {
return $MetadataEntityTable(attachedDatabase, alias);
}
@override
bool get withoutRowId => true;
@override
bool get isStrict => true;
}
class MetadataEntityData extends i0.DataClass
implements i0.Insertable<i1.MetadataEntityData> {
final String key;
final String value;
final DateTime updatedAt;
const MetadataEntityData({
required this.key,
required this.value,
required this.updatedAt,
});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['key'] = i0.Variable<String>(key);
map['value'] = i0.Variable<String>(value);
map['updated_at'] = i0.Variable<DateTime>(updatedAt);
return map;
}
factory MetadataEntityData.fromJson(
Map<String, dynamic> json, {
i0.ValueSerializer? serializer,
}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return MetadataEntityData(
key: serializer.fromJson<String>(json['key']),
value: serializer.fromJson<String>(json['value']),
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'key': serializer.toJson<String>(key),
'value': serializer.toJson<String>(value),
'updatedAt': serializer.toJson<DateTime>(updatedAt),
};
}
i1.MetadataEntityData copyWith({
String? key,
String? value,
DateTime? updatedAt,
}) => i1.MetadataEntityData(
key: key ?? this.key,
value: value ?? this.value,
updatedAt: updatedAt ?? this.updatedAt,
);
MetadataEntityData copyWithCompanion(i1.MetadataEntityCompanion data) {
return MetadataEntityData(
key: data.key.present ? data.key.value : this.key,
value: data.value.present ? data.value.value : this.value,
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
);
}
@override
String toString() {
return (StringBuffer('MetadataEntityData(')
..write('key: $key, ')
..write('value: $value, ')
..write('updatedAt: $updatedAt')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(key, value, updatedAt);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.MetadataEntityData &&
other.key == this.key &&
other.value == this.value &&
other.updatedAt == this.updatedAt);
}
class MetadataEntityCompanion
extends i0.UpdateCompanion<i1.MetadataEntityData> {
final i0.Value<String> key;
final i0.Value<String> value;
final i0.Value<DateTime> updatedAt;
const MetadataEntityCompanion({
this.key = const i0.Value.absent(),
this.value = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(),
});
MetadataEntityCompanion.insert({
required String key,
required String value,
this.updatedAt = const i0.Value.absent(),
}) : key = i0.Value(key),
value = i0.Value(value);
static i0.Insertable<i1.MetadataEntityData> custom({
i0.Expression<String>? key,
i0.Expression<String>? value,
i0.Expression<DateTime>? updatedAt,
}) {
return i0.RawValuesInsertable({
if (key != null) 'key': key,
if (value != null) 'value': value,
if (updatedAt != null) 'updated_at': updatedAt,
});
}
i1.MetadataEntityCompanion copyWith({
i0.Value<String>? key,
i0.Value<String>? value,
i0.Value<DateTime>? updatedAt,
}) {
return i1.MetadataEntityCompanion(
key: key ?? this.key,
value: value ?? this.value,
updatedAt: updatedAt ?? this.updatedAt,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (key.present) {
map['key'] = i0.Variable<String>(key.value);
}
if (value.present) {
map['value'] = i0.Variable<String>(value.value);
}
if (updatedAt.present) {
map['updated_at'] = i0.Variable<DateTime>(updatedAt.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('MetadataEntityCompanion(')
..write('key: $key, ')
..write('value: $value, ')
..write('updatedAt: $updatedAt')
..write(')'))
.toString();
}
}
@@ -13,6 +13,7 @@ import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/memory.entity.dart'; import 'package:immich_mobile/infrastructure/entities/memory.entity.dart';
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/metadata.entity.dart';
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.dart';
import 'package:immich_mobile/infrastructure/entities/person.entity.dart'; import 'package:immich_mobile/infrastructure/entities/person.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
@@ -29,6 +30,7 @@ import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.steps.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.steps.dart';
import 'package:logging/logging.dart';
@DriftDatabase( @DriftDatabase(
tables: [ tables: [
@@ -53,6 +55,7 @@ import 'package:immich_mobile/infrastructure/repositories/db.repository.steps.da
StoreEntity, StoreEntity,
TrashedLocalAssetEntity, TrashedLocalAssetEntity,
AssetEditEntity, AssetEditEntity,
MetadataEntity,
], ],
include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'}, include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'},
) )
@@ -83,8 +86,19 @@ class Drift extends $Drift {
}); });
} }
Future<void> optimize({bool allTables = false}) async {
try {
if (allTables) {
await customStatement('PRAGMA optimize=0x10002');
}
await customStatement('PRAGMA optimize');
} catch (error) {
Logger('Drift').fine('Failed to optimize database', error);
}
}
@override @override
int get schemaVersion => 24; int get schemaVersion => 25;
@override @override
MigrationStrategy get migration => MigrationStrategy( MigrationStrategy get migration => MigrationStrategy(
@@ -250,6 +264,9 @@ class Drift extends $Drift {
await customStatement('DROP INDEX IF EXISTS idx_remote_album_owner_id'); await customStatement('DROP INDEX IF EXISTS idx_remote_album_owner_id');
await m.alterTable(TableMigration(v24.remoteAlbumEntity)); await m.alterTable(TableMigration(v24.remoteAlbumEntity));
}, },
from24To25: (m, v25) async {
await m.createTable(v25.metadata);
},
), ),
); );
@@ -260,6 +277,7 @@ class Drift extends $Drift {
} }
await customStatement('PRAGMA foreign_keys = ON;'); await customStatement('PRAGMA foreign_keys = ON;');
await optimize();
}, },
beforeOpen: (details) async { beforeOpen: (details) async {
await customStatement('PRAGMA foreign_keys = ON'); await customStatement('PRAGMA foreign_keys = ON');
@@ -43,9 +43,11 @@ import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity
as i20; as i20;
import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart'
as i21; as i21;
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart' import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart'
as i22; as i22;
import 'package:drift/internal/modular.dart' as i23; import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
as i23;
import 'package:drift/internal/modular.dart' as i24;
abstract class $Drift extends i0.GeneratedDatabase { abstract class $Drift extends i0.GeneratedDatabase {
$Drift(i0.QueryExecutor e) : super(e); $Drift(i0.QueryExecutor e) : super(e);
@@ -89,9 +91,12 @@ abstract class $Drift extends i0.GeneratedDatabase {
.$TrashedLocalAssetEntityTable(this); .$TrashedLocalAssetEntityTable(this);
late final i21.$AssetEditEntityTable assetEditEntity = i21 late final i21.$AssetEditEntityTable assetEditEntity = i21
.$AssetEditEntityTable(this); .$AssetEditEntityTable(this);
i22.MergedAssetDrift get mergedAssetDrift => i23.ReadDatabaseContainer( late final i22.$MetadataEntityTable metadataEntity = i22.$MetadataEntityTable(
this, this,
).accessor<i22.MergedAssetDrift>(i22.MergedAssetDrift.new); );
i23.MergedAssetDrift get mergedAssetDrift => i24.ReadDatabaseContainer(
this,
).accessor<i23.MergedAssetDrift>(i23.MergedAssetDrift.new);
@override @override
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables => Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>(); allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
@@ -129,6 +134,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
storeEntity, storeEntity,
trashedLocalAssetEntity, trashedLocalAssetEntity,
assetEditEntity, assetEditEntity,
metadataEntity,
i10.idxPartnerSharedWithId, i10.idxPartnerSharedWithId,
i11.idxLatLng, i11.idxLatLng,
i12.idxRemoteAlbumAssetAlbumAsset, i12.idxRemoteAlbumAssetAlbumAsset,
@@ -389,4 +395,6 @@ class $DriftManager {
); );
i21.$$AssetEditEntityTableTableManager get assetEditEntity => i21.$$AssetEditEntityTableTableManager get assetEditEntity =>
i21.$$AssetEditEntityTableTableManager(_db, _db.assetEditEntity); i21.$$AssetEditEntityTableTableManager(_db, _db.assetEditEntity);
i22.$$MetadataEntityTableTableManager get metadataEntity =>
i22.$$MetadataEntityTableTableManager(_db, _db.metadataEntity);
} }
@@ -12375,6 +12375,574 @@ class Shape48 extends i0.VersionedTable {
columnsByName['order']! as i1.GeneratedColumn<int>; columnsByName['order']! as i1.GeneratedColumn<int>;
} }
final class Schema25 extends i0.VersionedSchema {
Schema25({required super.database}) : super(version: 25);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
userEntity,
remoteAssetEntity,
stackEntity,
localAssetEntity,
remoteAlbumEntity,
localAlbumEntity,
localAlbumAssetEntity,
idxLocalAlbumAssetAlbumAsset,
idxLocalAssetChecksum,
idxLocalAssetCloudId,
idxStackPrimaryAssetId,
idxRemoteAssetOwnerChecksum,
uQRemoteAssetsOwnerChecksum,
uQRemoteAssetsOwnerLibraryChecksum,
idxRemoteAssetChecksum,
idxRemoteAssetStackId,
idxRemoteAssetLocalDateTimeDay,
idxRemoteAssetLocalDateTimeMonth,
authUserEntity,
userMetadataEntity,
partnerEntity,
remoteExifEntity,
remoteAlbumAssetEntity,
remoteAlbumUserEntity,
remoteAssetCloudIdEntity,
memoryEntity,
memoryAssetEntity,
personEntity,
assetFaceEntity,
storeEntity,
trashedLocalAssetEntity,
assetEditEntity,
metadata,
idxPartnerSharedWithId,
idxLatLng,
idxRemoteAlbumAssetAlbumAsset,
idxRemoteAssetCloudId,
idxPersonOwnerId,
idxAssetFacePersonId,
idxAssetFaceAssetId,
idxTrashedLocalAssetChecksum,
idxTrashedLocalAssetAlbum,
idxAssetEditAssetId,
];
late final Shape33 userEntity = Shape33(
source: i0.VersionedTable(
entityName: 'user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_108,
_column_109,
_column_110,
_column_111,
_column_112,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape34 remoteAssetEntity = Shape34(
source: i0.VersionedTable(
entityName: 'remote_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_108,
_column_113,
_column_114,
_column_115,
_column_116,
_column_117,
_column_118,
_column_107,
_column_119,
_column_120,
_column_121,
_column_122,
_column_123,
_column_124,
_column_125,
_column_126,
_column_127,
_column_128,
_column_129,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape35 stackEntity = Shape35(
source: i0.VersionedTable(
entityName: 'stack_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_114,
_column_115,
_column_121,
_column_130,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape36 localAssetEntity = Shape36(
source: i0.VersionedTable(
entityName: 'local_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_108,
_column_113,
_column_114,
_column_115,
_column_116,
_column_117,
_column_118,
_column_107,
_column_131,
_column_120,
_column_132,
_column_133,
_column_134,
_column_135,
_column_136,
_column_137,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape48 remoteAlbumEntity = Shape48(
source: i0.VersionedTable(
entityName: 'remote_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_108,
_column_138,
_column_114,
_column_115,
_column_139,
_column_140,
_column_141,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape38 localAlbumEntity = Shape38(
source: i0.VersionedTable(
entityName: 'local_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_108,
_column_115,
_column_142,
_column_143,
_column_144,
_column_145,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape39 localAlbumAssetEntity = Shape39(
source: i0.VersionedTable(
entityName: 'local_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_146, _column_147, _column_145],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxLocalAlbumAssetAlbumAsset = i1.Index(
'idx_local_album_asset_album_asset',
'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)',
);
final i1.Index idxLocalAssetChecksum = i1.Index(
'idx_local_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
);
final i1.Index idxLocalAssetCloudId = i1.Index(
'idx_local_asset_cloud_id',
'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)',
);
final i1.Index idxStackPrimaryAssetId = i1.Index(
'idx_stack_primary_asset_id',
'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)',
);
final i1.Index idxRemoteAssetOwnerChecksum = i1.Index(
'idx_remote_asset_owner_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
);
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
'UQ_remote_assets_owner_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
);
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
'UQ_remote_assets_owner_library_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
);
final i1.Index idxRemoteAssetChecksum = i1.Index(
'idx_remote_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
);
final i1.Index idxRemoteAssetStackId = i1.Index(
'idx_remote_asset_stack_id',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)',
);
final i1.Index idxRemoteAssetLocalDateTimeDay = i1.Index(
'idx_remote_asset_local_date_time_day',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME(\'%Y-%m-%d\', local_date_time))',
);
final i1.Index idxRemoteAssetLocalDateTimeMonth = i1.Index(
'idx_remote_asset_local_date_time_month',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME(\'%Y-%m\', local_date_time))',
);
late final Shape40 authUserEntity = Shape40(
source: i0.VersionedTable(
entityName: 'auth_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_108,
_column_109,
_column_148,
_column_110,
_column_111,
_column_149,
_column_150,
_column_151,
_column_152,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape4 userMetadataEntity = Shape4(
source: i0.VersionedTable(
entityName: 'user_metadata_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
columns: [_column_153, _column_154, _column_155],
attachedDatabase: database,
),
alias: null,
);
late final Shape41 partnerEntity = Shape41(
source: i0.VersionedTable(
entityName: 'partner_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
columns: [_column_156, _column_157, _column_158],
attachedDatabase: database,
),
alias: null,
);
late final Shape42 remoteExifEntity = Shape42(
source: i0.VersionedTable(
entityName: 'remote_exif_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id)'],
columns: [
_column_159,
_column_160,
_column_161,
_column_162,
_column_163,
_column_164,
_column_117,
_column_116,
_column_165,
_column_166,
_column_167,
_column_168,
_column_135,
_column_136,
_column_169,
_column_170,
_column_171,
_column_172,
_column_173,
_column_174,
_column_175,
_column_176,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape7 remoteAlbumAssetEntity = Shape7(
source: i0.VersionedTable(
entityName: 'remote_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_159, _column_177],
attachedDatabase: database,
),
alias: null,
);
late final Shape10 remoteAlbumUserEntity = Shape10(
source: i0.VersionedTable(
entityName: 'remote_album_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
columns: [_column_177, _column_153, _column_178],
attachedDatabase: database,
),
alias: null,
);
late final Shape43 remoteAssetCloudIdEntity = Shape43(
source: i0.VersionedTable(
entityName: 'remote_asset_cloud_id_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id)'],
columns: [
_column_159,
_column_179,
_column_180,
_column_134,
_column_135,
_column_136,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape44 memoryEntity = Shape44(
source: i0.VersionedTable(
entityName: 'memory_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_114,
_column_115,
_column_124,
_column_121,
_column_113,
_column_181,
_column_182,
_column_183,
_column_184,
_column_185,
_column_186,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape12 memoryAssetEntity = Shape12(
source: i0.VersionedTable(
entityName: 'memory_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
columns: [_column_159, _column_187],
attachedDatabase: database,
),
alias: null,
);
late final Shape45 personEntity = Shape45(
source: i0.VersionedTable(
entityName: 'person_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_114,
_column_115,
_column_121,
_column_108,
_column_188,
_column_189,
_column_190,
_column_191,
_column_192,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape46 assetFaceEntity = Shape46(
source: i0.VersionedTable(
entityName: 'asset_face_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_159,
_column_193,
_column_194,
_column_195,
_column_196,
_column_197,
_column_198,
_column_199,
_column_200,
_column_201,
_column_124,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape18 storeEntity = Shape18(
source: i0.VersionedTable(
entityName: 'store_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [_column_202, _column_203, _column_204],
attachedDatabase: database,
),
alias: null,
);
late final Shape47 trashedLocalAssetEntity = Shape47(
source: i0.VersionedTable(
entityName: 'trashed_local_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id, album_id)'],
columns: [
_column_108,
_column_113,
_column_114,
_column_115,
_column_116,
_column_117,
_column_118,
_column_107,
_column_205,
_column_131,
_column_120,
_column_132,
_column_206,
_column_137,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape32 assetEditEntity = Shape32(
source: i0.VersionedTable(
entityName: 'asset_edit_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_159,
_column_207,
_column_208,
_column_209,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape49 metadata = Shape49(
source: i0.VersionedTable(
entityName: 'metadata',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY("key")'],
columns: [_column_210, _column_211, _column_115],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxPartnerSharedWithId = i1.Index(
'idx_partner_shared_with_id',
'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)',
);
final i1.Index idxLatLng = i1.Index(
'idx_lat_lng',
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
);
final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index(
'idx_remote_album_asset_album_asset',
'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)',
);
final i1.Index idxRemoteAssetCloudId = i1.Index(
'idx_remote_asset_cloud_id',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)',
);
final i1.Index idxPersonOwnerId = i1.Index(
'idx_person_owner_id',
'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)',
);
final i1.Index idxAssetFacePersonId = i1.Index(
'idx_asset_face_person_id',
'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)',
);
final i1.Index idxAssetFaceAssetId = i1.Index(
'idx_asset_face_asset_id',
'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
);
final i1.Index idxTrashedLocalAssetChecksum = i1.Index(
'idx_trashed_local_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)',
);
final i1.Index idxTrashedLocalAssetAlbum = i1.Index(
'idx_trashed_local_asset_album',
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)',
);
final i1.Index idxAssetEditAssetId = i1.Index(
'idx_asset_edit_asset_id',
'CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)',
);
}
class Shape49 extends i0.VersionedTable {
Shape49({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get key =>
columnsByName['key']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get value =>
columnsByName['value']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get updatedAt =>
columnsByName['updated_at']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<String> _column_210(String aliasedName) =>
i1.GeneratedColumn<String>(
'key',
aliasedName,
false,
type: i1.DriftSqlType.string,
$customConstraints: 'NOT NULL',
);
i1.GeneratedColumn<String> _column_211(String aliasedName) =>
i1.GeneratedColumn<String>(
'value',
aliasedName,
false,
type: i1.DriftSqlType.string,
$customConstraints: 'NOT NULL',
);
i0.MigrationStepWithVersion migrationSteps({ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2, required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3, required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@@ -12399,6 +12967,7 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema22 schema) from21To22, required Future<void> Function(i1.Migrator m, Schema22 schema) from21To22,
required Future<void> Function(i1.Migrator m, Schema23 schema) from22To23, required Future<void> Function(i1.Migrator m, Schema23 schema) from22To23,
required Future<void> Function(i1.Migrator m, Schema24 schema) from23To24, required Future<void> Function(i1.Migrator m, Schema24 schema) from23To24,
required Future<void> Function(i1.Migrator m, Schema25 schema) from24To25,
}) { }) {
return (currentVersion, database) async { return (currentVersion, database) async {
switch (currentVersion) { switch (currentVersion) {
@@ -12517,6 +13086,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema); final migrator = i1.Migrator(database, schema);
await from23To24(migrator, schema); await from23To24(migrator, schema);
return 24; return 24;
case 24:
final schema = Schema25(database: database);
final migrator = i1.Migrator(database, schema);
await from24To25(migrator, schema);
return 25;
default: default:
throw ArgumentError.value('Unknown migration from $currentVersion'); throw ArgumentError.value('Unknown migration from $currentVersion');
} }
@@ -12547,6 +13121,7 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema22 schema) from21To22, required Future<void> Function(i1.Migrator m, Schema22 schema) from21To22,
required Future<void> Function(i1.Migrator m, Schema23 schema) from22To23, required Future<void> Function(i1.Migrator m, Schema23 schema) from22To23,
required Future<void> Function(i1.Migrator m, Schema24 schema) from23To24, required Future<void> Function(i1.Migrator m, Schema24 schema) from23To24,
required Future<void> Function(i1.Migrator m, Schema25 schema) from24To25,
}) => i0.VersionedSchema.stepByStepHelper( }) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps( step: migrationSteps(
from1To2: from1To2, from1To2: from1To2,
@@ -12572,5 +13147,6 @@ i1.OnUpgrade stepByStep({
from21To22: from21To22, from21To22: from21To22,
from22To23: from22To23, from22To23: from22To23,
from23To24: from23To24, from23To24: from23To24,
from24To25: from24To25,
), ),
); );
@@ -0,0 +1,122 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/config/app_config.dart';
import 'package:immich_mobile/domain/models/config/system_config.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
class MetadataRepository extends DriftDatabaseRepository {
final Drift _db;
final Map<MetadataKey, Object> _cache = {};
MetadataRepository._(this._db) : super(_db);
static MetadataRepository? _instance;
static MetadataRepository get instance {
final instance = _instance;
if (instance == null) {
throw StateError('MetadataRepository not initialized. Call ensureInitialized() first');
}
return instance;
}
AppConfig _appConfig = const .new();
AppConfig get appConfig => _appConfig;
SystemConfig _systemConfig = const .new();
SystemConfig get systemConfig => _systemConfig;
static Future<MetadataRepository> ensureInitialized(Drift db) async {
if (_instance == null) {
final instance = MetadataRepository._(db);
await instance._hydrate();
_instance = instance;
}
return _instance!;
}
static Future<void> refresh() async {
instance._cache.clear();
instance._appConfig = const .new();
instance._systemConfig = const .new();
await instance._hydrate();
}
Future<void> _hydrate() async => _hydrateCache(await _db.select(_db.metadataEntity).get());
T _read<T extends Object>(MetadataKey<T> key) => (_cache[key] as T?) ?? key.defaultValue;
Future<void> write<T extends Object, U extends T>(MetadataKey<T> key, U value) async {
if (_read(key) == value) return;
await _db
.into(_db.metadataEntity)
.insertOnConflictUpdate(
MetadataEntityCompanion.insert(key: key.key, value: key.encode(value), updatedAt: Value(DateTime.now())),
);
_updateCache(key, value);
}
Future<void> delete<T extends Object>(MetadataKey<T> key) async {
await (_db.delete(_db.metadataEntity)..where((t) => t.key.equals(key.key))).go();
_updateCache(key, key.defaultValue);
}
Stream<AppConfig> watchAppConfig() => _watchDomain(.appConfig).distinct();
Stream<SystemConfig> watchSystemConfig() => _watchDomain(.systemConfig).distinct();
Stream<T> _watchDomain<T extends Object>(MetadataDomain<T> domain) {
final query = _db.select(_db.metadataEntity)..where((t) => t.key.like('${domain.prefix}.%'));
return query.watch().map((rows) {
_hydrateCache(rows);
return domain.config(this);
});
}
void _hydrateCache(List<MetadataEntityData> rows) {
final keyMap = MetadataKey.asKeyMap();
for (final row in rows) {
final key = keyMap[row.key];
if (key == null) continue;
_updateCache(key, key.decode(row.value));
}
}
void _updateCache<T extends Object>(MetadataKey<T> key, T value) {
if (_cache[key] == value) return;
_cache[key] = value;
key.domain.rebuild(this);
}
}
extension<T extends Object> on MetadataDomain<T> {
T config(MetadataRepository repo) => switch (this) {
.appConfig => repo._appConfig as T,
.systemConfig => repo._systemConfig as T,
};
void rebuild(MetadataRepository repo) {
switch (this) {
case .appConfig:
repo._appConfig = .new(
theme: .new(
mode: repo._read(.themeMode),
primaryColor: repo._read(.themePrimaryColor),
dynamicTheme: repo._read(.themeDynamic),
colorfulInterface: repo._read(.themeColorfulInterface),
),
cleanup: .new(
keepFavorites: repo._read(.cleanupKeepFavorites),
keepMediaType: repo._read(.cleanupKeepMediaType),
keepAlbumIds: repo._read(.cleanupKeepAlbumIds),
cutoffDaysAgo: repo._read(.cleanupCutoffDaysAgo),
defaultsInitialized: repo._read(.cleanupDefaultsInitialized),
),
);
case .systemConfig:
repo._systemConfig = .new(logLevel: repo._read(.logLevel));
}
}
}
@@ -244,17 +244,21 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
final isAscending = albumData.order == AlbumAssetOrder.asc; final isAscending = albumData.order == AlbumAssetOrder.asc;
final query = _db.remoteAssetEntity.select().addColumns([_db.localAssetEntity.id]).join([ // Correlated subquery picks the first matching local asset by checksum,
// avoiding fan-out when the same photo exists in multiple device albums (#23273).
final localId = subqueryExpression<String>(
_db.localAssetEntity.selectOnly()
..addColumns([_db.localAssetEntity.id])
..where(_db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum))
..limit(1),
);
final query = _db.remoteAssetEntity.select().addColumns([localId]).join([
innerJoin( innerJoin(
_db.remoteAlbumAssetEntity, _db.remoteAlbumAssetEntity,
_db.remoteAlbumAssetEntity.assetId.equalsExp(_db.remoteAssetEntity.id), _db.remoteAlbumAssetEntity.assetId.equalsExp(_db.remoteAssetEntity.id),
useColumns: false, useColumns: false,
), ),
leftOuterJoin(
_db.localAssetEntity,
_db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum),
useColumns: false,
),
])..where(_db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAlbumAssetEntity.albumId.equals(albumId)); ])..where(_db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAlbumAssetEntity.albumId.equals(albumId));
if (isAscending) { if (isAscending) {
@@ -265,9 +269,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
query.limit(count, offset: offset); query.limit(count, offset: offset);
return query return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto(localId: row.read(localId))).get();
.map((row) => row.readTable(_db.remoteAssetEntity).toDto(localId: row.read(_db.localAssetEntity.id)))
.get();
} }
TimelineQuery fromAssets(List<BaseAsset> assets, TimelineOrigin origin) => ( TimelineQuery fromAssets(List<BaseAsset> assets, TimelineOrigin origin) => (
@@ -422,23 +424,22 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
} }
Stream<List<Bucket>> _watchPersonBucket(String userId, String personId, {GroupAssetsBy groupBy = GroupAssetsBy.day}) { Stream<List<Bucket>> _watchPersonBucket(String userId, String personId, {GroupAssetsBy groupBy = GroupAssetsBy.day}) {
final idQuery = _db.assetFaceEntity.selectOnly()
..addColumns([_db.assetFaceEntity.assetId])
..where(
_db.assetFaceEntity.personId.equals(personId) &
_db.assetFaceEntity.isVisible.equals(true) &
_db.assetFaceEntity.deletedAt.isNull(),
);
if (groupBy == GroupAssetsBy.none) { if (groupBy == GroupAssetsBy.none) {
final query = _db.remoteAssetEntity.selectOnly() final query = _db.remoteAssetEntity.selectOnly()
..addColumns([_db.remoteAssetEntity.id.count()]) ..addColumns([_db.remoteAssetEntity.id.count()])
..join([
innerJoin(
_db.assetFaceEntity,
_db.assetFaceEntity.assetId.equalsExp(_db.remoteAssetEntity.id),
useColumns: false,
),
])
..where( ..where(
_db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAssetEntity.id.isInQuery(idQuery) &
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.ownerId.equals(userId) & _db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) & _db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline),
_db.assetFaceEntity.personId.equals(personId) &
_db.assetFaceEntity.isVisible.equals(true) &
_db.assetFaceEntity.deletedAt.isNull(),
); );
return query.map((row) { return query.map((row) {
@@ -452,20 +453,11 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
final query = _db.remoteAssetEntity.selectOnly() final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp]) ..addColumns([assetCountExp, dateExp])
..join([
innerJoin(
_db.assetFaceEntity,
_db.assetFaceEntity.assetId.equalsExp(_db.remoteAssetEntity.id),
useColumns: false,
),
])
..where( ..where(
_db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAssetEntity.id.isInQuery(idQuery) &
_db.remoteAssetEntity.ownerId.equals(userId) & _db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) & _db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
_db.assetFaceEntity.personId.equals(personId) & _db.remoteAssetEntity.deletedAt.isNull(),
_db.assetFaceEntity.isVisible.equals(true) &
_db.assetFaceEntity.deletedAt.isNull(),
) )
..groupBy([dateExp]) ..groupBy([dateExp])
..orderBy([OrderingTerm.desc(dateExp)]); ..orderBy([OrderingTerm.desc(dateExp)]);
@@ -483,26 +475,26 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
required int offset, required int offset,
required int count, required int count,
}) { }) {
final query = final idQuery = _db.assetFaceEntity.selectOnly()
_db.remoteAssetEntity.select().join([ ..addColumns([_db.assetFaceEntity.assetId])
innerJoin( ..where(
_db.assetFaceEntity, _db.assetFaceEntity.personId.equals(personId) &
_db.assetFaceEntity.assetId.equalsExp(_db.remoteAssetEntity.id), _db.assetFaceEntity.isVisible.equals(true) &
useColumns: false, _db.assetFaceEntity.deletedAt.isNull(),
), );
])
..where(
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
_db.assetFaceEntity.personId.equals(personId) &
_db.assetFaceEntity.isVisible.equals(true) &
_db.assetFaceEntity.deletedAt.isNull(),
)
..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)])
..limit(count, offset: offset);
return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get(); final query = _db.remoteAssetEntity.select()
..where(
(row) =>
row.id.isInQuery(idQuery) &
row.deletedAt.isNull() &
row.ownerId.equals(userId) &
row.visibility.equalsValue(AssetVisibility.timeline),
)
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(count, offset: offset);
return query.map((row) => row.toDto()).get();
} }
TimelineQuery map(List<String> userIds, TimelineMapOptions options, GroupAssetsBy groupBy) => ( TimelineQuery map(List<String> userIds, TimelineMapOptions options, GroupAssetsBy groupBy) => (
+11 -2
View File
@@ -10,6 +10,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/constants/locales.dart'; import 'package:immich_mobile/constants/locales.dart';
@@ -24,6 +25,7 @@ import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/locale_provider.dart';
import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart';
@@ -53,7 +55,7 @@ void main() async {
await initApp(); await initApp();
// Warm-up isolate pool for worker manager // Warm-up isolate pool for worker manager
await workerManagerPatch.init(dynamicSpawning: true, isolatesCount: max(Platform.numberOfProcessors - 1, 5)); await workerManagerPatch.init(dynamicSpawning: true, isolatesCount: max(Platform.numberOfProcessors - 1, 5));
await migrateDatabaseIfNeeded(); await migrateDatabaseIfNeeded(drift);
runApp(ProviderScope(overrides: [driftProvider.overrideWith(driftOverride(drift))], child: const MainWidget())); runApp(ProviderScope(overrides: [driftProvider.overrideWith(driftOverride(drift))], child: const MainWidget()));
} catch (error, stack) { } catch (error, stack) {
@@ -162,6 +164,13 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
} }
} }
SystemChrome.setSystemUIOverlayStyle(overlayStyle); SystemChrome.setSystemUIOverlayStyle(overlayStyle);
await FlutterLocalNotificationsPlugin().initialize(
const InitializationSettings(
android: AndroidInitializationSettings('@drawable/notification_icon'),
iOS: DarwinInitializationSettings(),
),
);
} }
Future<DeepLink> _deepLinkBuilder(PlatformDeepLink deepLink) async { Future<DeepLink> _deepLinkBuilder(PlatformDeepLink deepLink) async {
@@ -241,7 +250,7 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
localizationsDelegates: context.localizationDelegates, localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales, supportedLocales: context.supportedLocales,
locale: context.locale, locale: context.locale,
themeMode: ref.watch(immichThemeModeProvider), themeMode: ref.watch(appConfigProvider.select((config) => config.theme.mode)),
darkTheme: getThemeData(colorScheme: immichTheme.dark, locale: context.locale), darkTheme: getThemeData(colorScheme: immichTheme.dark, locale: context.locale),
theme: getThemeData(colorScheme: immichTheme.light, locale: context.locale), theme: getThemeData(colorScheme: immichTheme.light, locale: context.locale),
builder: (context, child) => ImmichTranslationProvider( builder: (context, child) => ImmichTranslationProvider(
@@ -6,8 +6,8 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/colors.dart';
import 'package:immich_mobile/constants/locales.dart'; import 'package:immich_mobile/constants/locales.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/generated/codegen_loader.g.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart';
@@ -35,7 +35,7 @@ class BootstrapErrorWidget extends StatelessWidget {
@override @override
Widget build(BuildContext _) { Widget build(BuildContext _) {
final immichTheme = defaultColorPreset.themeOfPreset; final immichTheme = MetadataKey.themePrimaryColor.defaultValue.themeOfPreset;
return EasyLocalization( return EasyLocalization(
supportedLocales: locales.values.toList(), supportedLocales: locales.values.toList(),
@@ -160,12 +160,25 @@ class DriftMemoryPage extends HookConsumerWidget {
currentAssetPage.value = otherIndex; currentAssetPage.value = otherIndex;
updateProgressText(); updateProgressText();
final activeMemory = currentMemory.value;
// Wait for page change animation to finish // Wait for page change animation to finish
await Future.delayed(const Duration(milliseconds: 400)); await Future.delayed(const Duration(milliseconds: 400));
// check if memory is still the same and if context is still mounted
if (currentMemory.value != activeMemory || !context.mounted) {
return;
}
// And then precache the next asset // And then precache the next asset
await precacheAsset(otherIndex + 1); await precacheAsset(otherIndex + 1);
final asset = currentMemory.value.assets[otherIndex]; // check again as precache involves async operations
if (currentMemory.value != activeMemory || !context.mounted) {
return;
}
final asset = activeMemory.assets[otherIndex];
currentAsset.value = asset; currentAsset.value = asset;
ref.read(assetViewerProvider.notifier).setAsset(asset); ref.read(assetViewerProvider.notifier).setAsset(asset);
} }
@@ -106,10 +106,17 @@ class DriftSearchPage extends HookConsumerWidget {
Future.microtask(() { Future.microtask(() {
textSearchController.clear(); textSearchController.clear();
peopleCurrentFilterWidget.value = null;
dateRangeCurrentFilterWidget.value = null;
cameraCurrentFilterWidget.value = null;
tagCurrentFilterWidget.value = null;
mediaTypeCurrentFilterWidget.value = null;
ratingCurrentFilterWidget.value = null;
displayOptionCurrentFilterWidget.value = null;
locationCurrentFilterWidget.value = preFilter.location.city != null
? Text(preFilter.location.city!, style: context.textTheme.labelLarge)
: null;
search(preFilter); search(preFilter);
if (preFilter.location.city != null) {
locationCurrentFilterWidget.value = Text(preFilter.location.city!, style: context.textTheme.labelLarge);
}
}); });
return null; return null;
@@ -37,7 +37,7 @@ class SimilarPhotosActionButton extends ConsumerWidget {
date: SearchDateFilter(), date: SearchDateFilter(),
display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false),
rating: SearchRatingFilter(), rating: SearchRatingFilter(),
mediaType: AssetType.image, mediaType: AssetType.other,
), ),
); );
@@ -26,6 +26,14 @@ class AssetDetails extends ConsumerWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: context.colorScheme.surface, color: context.colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.2),
offset: const Offset(0, -3),
blurRadius: 10,
spreadRadius: 2,
),
],
), ),
child: SafeArea( child: SafeArea(
top: false, top: false,
@@ -22,6 +22,7 @@ class TechnicalDetails extends ConsumerWidget {
final exifInfo = this.exifInfo; final exifInfo = this.exifInfo;
final cameraTitle = _getCameraInfoTitle(exifInfo); final cameraTitle = _getCameraInfoTitle(exifInfo);
final lensTitle = exifInfo?.lens != null && exifInfo!.lens!.isNotEmpty ? exifInfo.lens : null; final lensTitle = exifInfo?.lens != null && exifInfo!.lens!.isNotEmpty ? exifInfo.lens : null;
final lensSubtitle = _getLensInfoSubtitle(exifInfo);
return Column( return Column(
children: [ children: [
@@ -46,9 +47,16 @@ class TechnicalDetails extends ConsumerWidget {
title: lensTitle, title: lensTitle,
titleStyle: context.textTheme.labelLarge, titleStyle: context.textTheme.labelLarge,
leading: Icon(Icons.camera_outlined, size: 24, color: context.textTheme.labelLarge?.color), leading: Icon(Icons.camera_outlined, size: 24, color: context.textTheme.labelLarge?.color),
subtitle: _getLensInfoSubtitle(exifInfo), subtitle: lensSubtitle,
subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
), ),
] else if (lensSubtitle != null) ...[
const SizedBox(height: 16),
SheetTile(
title: lensSubtitle,
titleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
leading: Icon(Icons.camera_outlined, size: 24, color: context.textTheme.labelLarge?.color),
),
], ],
], ],
); );
@@ -123,6 +131,7 @@ class TechnicalDetails extends ConsumerWidget {
if (exifInfo == null) return null; if (exifInfo == null) return null;
final fNumber = exifInfo.fNumber.isNotEmpty ? 'ƒ/${exifInfo.fNumber}' : null; final fNumber = exifInfo.fNumber.isNotEmpty ? 'ƒ/${exifInfo.fNumber}' : null;
final focalLength = exifInfo.focalLength.isNotEmpty ? '${exifInfo.focalLength} mm' : null; final focalLength = exifInfo.focalLength.isNotEmpty ? '${exifInfo.focalLength} mm' : null;
if (fNumber == null && focalLength == null) return null;
return [fNumber, focalLength].where((spec) => spec != null && spec.isNotEmpty).join(_kSeparator); return [fNumber, focalLength].where((spec) => spec != null && spec.isNotEmpty).join(_kSeparator);
} }
} }
@@ -65,26 +65,37 @@ class ViewerBottomBar extends ConsumerWidget {
labelLarge: context.themeData.textTheme.labelLarge?.copyWith(color: Colors.white), labelLarge: context.themeData.textTheme.labelLarge?.copyWith(color: Colors.white),
), ),
), ),
child: Container( child: Stack(
decoration: const BoxDecoration( children: [
gradient: LinearGradient( const Positioned.fill(
begin: Alignment.bottomCenter, child: IgnorePointer(
end: Alignment.topCenter, child: DecoratedBox(
colors: [Colors.black45, Colors.black12, Colors.transparent], decoration: BoxDecoration(
stops: [0.0, 0.7, 1.0], gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [Colors.black45, Colors.black12, Colors.transparent],
stops: [0.0, 0.7, 1.0],
),
),
),
),
), ),
), SafeArea(
child: SafeArea( top: false,
top: false, child: Padding(
child: Column( padding: const EdgeInsets.only(top: 16),
mainAxisSize: MainAxisSize.min, child: Column(
children: [ mainAxisSize: MainAxisSize.min,
if (asset.isVideo) VideoControls(videoPlayerName: asset.heroTag), children: [
if (!isReadonlyModeEnabled) if (asset.isVideo) VideoControls(videoPlayerName: asset.heroTag),
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions), if (!isReadonlyModeEnabled)
], Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions),
],
),
),
), ),
), ],
), ),
), ),
); );
@@ -75,29 +75,41 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
child: AnimatedOpacity( child: AnimatedOpacity(
opacity: opacity, opacity: opacity,
duration: Durations.short2, duration: Durations.short2,
child: DecoratedBox( child: Stack(
decoration: BoxDecoration( children: [
gradient: showingDetails Positioned.fill(
? null child: IgnorePointer(
: const LinearGradient( child: DecoratedBox(
begin: Alignment.topCenter, decoration: BoxDecoration(
end: Alignment.bottomCenter, gradient: showingDetails
colors: [Colors.black45, Colors.black12, Colors.transparent], ? null
stops: [0.0, 0.7, 1.0], : const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.black45, Colors.black12, Colors.transparent],
stops: [0.0, 0.7, 1.0],
),
), ),
), ),
child: AppBar( ),
backgroundColor: Colors.transparent, ),
leading: const _AppBarBackButton(), SafeArea(
iconTheme: const IconThemeData(size: 22, color: Colors.white), bottom: false,
actionsIconTheme: const IconThemeData(size: 22, color: Colors.white), child: SizedBox.square(
shape: const Border(), child: Theme(
actions: showingDetails || isReadonlyModeEnabled data: context.themeData.copyWith(iconTheme: const IconThemeData(size: 22, color: Colors.white)),
? null child: Row(
: isInLockedView children: [
? lockedViewActions const _AppBarBackButton(),
: actions, const Spacer(),
), if (!showingDetails && !isReadonlyModeEnabled)
if (isInLockedView) ...lockedViewActions else ...actions,
],
),
),
),
),
],
), ),
), ),
); );
@@ -113,20 +125,17 @@ class _AppBarBackButton extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final showingDetails = ref.watch(assetViewerProvider.select((state) => state.showingDetails)); final showingDetails = ref.watch(assetViewerProvider.select((state) => state.showingDetails));
return Padding( return ElevatedButton(
padding: const EdgeInsets.only(left: 12.0), style: ElevatedButton.styleFrom(
child: ElevatedButton( backgroundColor: showingDetails ? context.colorScheme.surface : Colors.transparent,
style: ElevatedButton.styleFrom( shape: const CircleBorder(),
backgroundColor: showingDetails ? context.colorScheme.surface : Colors.transparent, iconSize: 22,
shape: const CircleBorder(), iconColor: showingDetails ? context.colorScheme.onSurface : Colors.white,
iconSize: 22, padding: const EdgeInsets.all(10.0),
iconColor: showingDetails ? context.colorScheme.onSurface : Colors.white, elevation: showingDetails ? 4 : 0,
padding: EdgeInsets.zero,
elevation: showingDetails ? 4 : 0,
),
onPressed: context.maybePop,
child: const Icon(Icons.arrow_back_rounded),
), ),
onPressed: context.maybePop,
child: const Icon(Icons.arrow_back_rounded),
); );
} }
} }
+16 -18
View File
@@ -1,9 +1,9 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/services/cleanup.service.dart'; import 'package:immich_mobile/services/cleanup.service.dart';
class CleanupState { class CleanupState {
@@ -54,27 +54,25 @@ final cleanupProvider = StateNotifierProvider<CleanupNotifier, CleanupState>((re
return CleanupNotifier( return CleanupNotifier(
ref.watch(cleanupServiceProvider), ref.watch(cleanupServiceProvider),
ref.watch(currentUserProvider)?.id, ref.watch(currentUserProvider)?.id,
ref.watch(appSettingsServiceProvider), ref.watch(metadataProvider),
); );
}); });
class CleanupNotifier extends StateNotifier<CleanupState> { class CleanupNotifier extends StateNotifier<CleanupState> {
final CleanupService _cleanupService; final CleanupService _cleanupService;
final String? _userId; final String? _userId;
final AppSettingsService _appSettingsService; final MetadataRepository _metadataRepository;
CleanupNotifier(this._cleanupService, this._userId, this._appSettingsService) : super(const CleanupState()) { CleanupNotifier(this._cleanupService, this._userId, this._metadataRepository) : super(const CleanupState()) {
_loadPersistedSettings(); _loadPersistedSettings();
} }
void _loadPersistedSettings() { void _loadPersistedSettings() {
final keepFavorites = _appSettingsService.getSetting(AppSettingsEnum.cleanupKeepFavorites); final cleanup = _metadataRepository.appConfig.cleanup;
final keepMediaTypeIndex = _appSettingsService.getSetting(AppSettingsEnum.cleanupKeepMediaType); final keepFavorites = cleanup.keepFavorites;
final keepAlbumIdsString = _appSettingsService.getSetting(AppSettingsEnum.cleanupKeepAlbumIds); final keepMediaType = cleanup.keepMediaType;
final cutoffDaysAgo = _appSettingsService.getSetting(AppSettingsEnum.cleanupCutoffDaysAgo); final keepAlbumIds = cleanup.keepAlbumIds.toSet();
final cutoffDaysAgo = cleanup.cutoffDaysAgo;
final keepMediaType = AssetKeepType.values[keepMediaTypeIndex.clamp(0, AssetKeepType.values.length - 1)];
final keepAlbumIds = keepAlbumIdsString.isEmpty ? <String>{} : keepAlbumIdsString.split(',').toSet();
final selectedDate = cutoffDaysAgo >= 0 ? DateTime.now().subtract(Duration(days: cutoffDaysAgo)) : null; final selectedDate = cutoffDaysAgo >= 0 ? DateTime.now().subtract(Duration(days: cutoffDaysAgo)) : null;
state = state.copyWith( state = state.copyWith(
@@ -89,18 +87,18 @@ class CleanupNotifier extends StateNotifier<CleanupState> {
state = state.copyWith(selectedDate: date, assetsToDelete: []); state = state.copyWith(selectedDate: date, assetsToDelete: []);
if (date != null) { if (date != null) {
final daysAgo = DateTime.now().difference(date).inDays; final daysAgo = DateTime.now().difference(date).inDays;
_appSettingsService.setSetting(AppSettingsEnum.cleanupCutoffDaysAgo, daysAgo); _metadataRepository.write(.cleanupCutoffDaysAgo, daysAgo);
} }
} }
void setKeepMediaType(AssetKeepType keepMediaType) { void setKeepMediaType(AssetKeepType keepMediaType) {
state = state.copyWith(keepMediaType: keepMediaType, assetsToDelete: []); state = state.copyWith(keepMediaType: keepMediaType, assetsToDelete: []);
_appSettingsService.setSetting(AppSettingsEnum.cleanupKeepMediaType, keepMediaType.index); _metadataRepository.write(.cleanupKeepMediaType, keepMediaType);
} }
void setKeepFavorites(bool keepFavorites) { void setKeepFavorites(bool keepFavorites) {
state = state.copyWith(keepFavorites: keepFavorites, assetsToDelete: []); state = state.copyWith(keepFavorites: keepFavorites, assetsToDelete: []);
_appSettingsService.setSetting(AppSettingsEnum.cleanupKeepFavorites, keepFavorites); _metadataRepository.write(.cleanupKeepFavorites, keepFavorites);
} }
void toggleKeepAlbum(String albumId) { void toggleKeepAlbum(String albumId) {
@@ -120,7 +118,7 @@ class CleanupNotifier extends StateNotifier<CleanupState> {
} }
void _persistExcludedAlbumIds(Set<String> albumIds) { void _persistExcludedAlbumIds(Set<String> albumIds) {
_appSettingsService.setSetting(AppSettingsEnum.cleanupKeepAlbumIds, albumIds.join(',')); _metadataRepository.write(.cleanupKeepAlbumIds, albumIds.toList());
} }
void cleanupStaleAlbumIds(Set<String> existingAlbumIds) { void cleanupStaleAlbumIds(Set<String> existingAlbumIds) {
@@ -133,7 +131,7 @@ class CleanupNotifier extends StateNotifier<CleanupState> {
} }
void applyDefaultAlbumSelections(List<(String id, String name)> albums) { void applyDefaultAlbumSelections(List<(String id, String name)> albums) {
final isInitialized = _appSettingsService.getSetting(AppSettingsEnum.cleanupDefaultsInitialized); final isInitialized = _metadataRepository.appConfig.cleanup.defaultsInitialized;
if (isInitialized) return; if (isInitialized) return;
final toKeep = _cleanupService.getDefaultKeepAlbumIds(albums); final toKeep = _cleanupService.getDefaultKeepAlbumIds(albums);
@@ -144,7 +142,7 @@ class CleanupNotifier extends StateNotifier<CleanupState> {
_persistExcludedAlbumIds(keepAlbumIds); _persistExcludedAlbumIds(keepAlbumIds);
} }
_appSettingsService.setSetting(AppSettingsEnum.cleanupDefaultsInitialized, true); _metadataRepository.write(.cleanupDefaultsInitialized, true);
} }
Future<void> scanAssets() async { Future<void> scanAssets() async {
@@ -0,0 +1,20 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/config/app_config.dart';
import 'package:immich_mobile/domain/models/config/system_config.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
final metadataProvider = Provider.autoDispose<MetadataRepository>((_) => MetadataRepository.instance);
final appConfigProvider = Provider.autoDispose<AppConfig>((ref) {
final repo = ref.watch(metadataProvider);
final subscription = repo.watchAppConfig().listen((event) => ref.state = event);
ref.onDispose(subscription.cancel);
return repo.appConfig;
});
final systemConfigProvider = Provider.autoDispose<SystemConfig>((ref) {
final repo = ref.watch(metadataProvider);
final subscription = repo.watchSystemConfig().listen((event) => ref.state = event);
ref.onDispose(subscription.cancel);
return repo.systemConfig;
});
+9 -50
View File
@@ -1,58 +1,17 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/constants/colors.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/dynamic_theme.dart'; import 'package:immich_mobile/theme/dynamic_theme.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/theme/theme_data.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final immichThemeModeProvider = StateProvider<ThemeMode>((ref) {
final themeMode = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.themeMode);
dPrint(() => "Current themeMode $themeMode");
if (themeMode == ThemeMode.light.name) {
return ThemeMode.light;
} else if (themeMode == ThemeMode.dark.name) {
return ThemeMode.dark;
} else {
return ThemeMode.system;
}
});
final immichThemePresetProvider = StateProvider<ImmichColorPreset>((ref) {
final appSettingsProvider = ref.watch(appSettingsServiceProvider);
final primaryColorPreset = appSettingsProvider.getSetting(AppSettingsEnum.primaryColor);
dPrint(() => "Current theme preset $primaryColorPreset");
try {
return ImmichColorPreset.values.firstWhere((e) => e.name == primaryColorPreset);
} catch (e) {
dPrint(() => "Theme preset $primaryColorPreset not found. Applying default preset.");
appSettingsProvider.setSetting(AppSettingsEnum.primaryColor, defaultColorPresetName);
return defaultColorPreset;
}
});
final dynamicThemeSettingProvider = StateProvider<bool>((ref) {
return ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.dynamicTheme);
});
final colorfulInterfaceSettingProvider = StateProvider<bool>((ref) {
return ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.colorfulInterface);
});
// Provider for current selected theme // Provider for current selected theme
final immichThemeProvider = StateProvider<ImmichTheme>((ref) { final immichThemeProvider = StateProvider<ImmichTheme>((ref) {
final primaryColorPreset = ref.read(immichThemePresetProvider); final themeConfig = ref.watch(appConfigProvider.select((config) => config.theme));
final useSystemColor = ref.watch(dynamicThemeSettingProvider);
final useColorfulInterface = ref.watch(colorfulInterfaceSettingProvider);
final ImmichTheme? dynamicTheme = DynamicTheme.theme;
final currentTheme = (useSystemColor && dynamicTheme != null) ? dynamicTheme : primaryColorPreset.themeOfPreset;
return useColorfulInterface ? currentTheme : decolorizeSurfaces(theme: currentTheme); final ImmichTheme? dynamicTheme = DynamicTheme.theme;
final currentTheme = (themeConfig.dynamicTheme && dynamicTheme != null)
? dynamicTheme
: themeConfig.primaryColor.themeOfPreset;
return themeConfig.colorfulInterface ? currentTheme : decolorizeSurfaces(theme: currentTheme);
}); });
+1 -12
View File
@@ -1,14 +1,9 @@
import 'package:immich_mobile/constants/colors.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
enum AppSettingsEnum<T> { enum AppSettingsEnum<T> {
loadPreview<bool>(StoreKey.loadPreview, "loadPreview", true), loadPreview<bool>(StoreKey.loadPreview, "loadPreview", true),
loadOriginal<bool>(StoreKey.loadOriginal, "loadOriginal", false), loadOriginal<bool>(StoreKey.loadOriginal, "loadOriginal", false),
themeMode<String>(StoreKey.themeMode, "themeMode", "system"), // "light","dark","system"
primaryColor<String>(StoreKey.primaryColor, "primaryColor", defaultColorPresetName),
dynamicTheme<bool>(StoreKey.dynamicTheme, "dynamicTheme", false),
colorfulInterface<bool>(StoreKey.colorfulInterface, "colorfulInterface", true),
tilesPerRow<int>(StoreKey.tilesPerRow, "tilesPerRow", 4), tilesPerRow<int>(StoreKey.tilesPerRow, "tilesPerRow", 4),
dynamicLayout<bool>(StoreKey.dynamicLayout, "dynamicLayout", false), dynamicLayout<bool>(StoreKey.dynamicLayout, "dynamicLayout", false),
groupAssetsBy<int>(StoreKey.groupAssetsBy, "groupBy", 0), groupAssetsBy<int>(StoreKey.groupAssetsBy, "groupBy", 0),
@@ -30,7 +25,6 @@ enum AppSettingsEnum<T> {
selectedAlbumSortOrder<int>(StoreKey.selectedAlbumSortOrder, "selectedAlbumSortOrder", 2), selectedAlbumSortOrder<int>(StoreKey.selectedAlbumSortOrder, "selectedAlbumSortOrder", 2),
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false), advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false),
manageLocalMediaAndroid<bool>(StoreKey.manageLocalMediaAndroid, null, false), manageLocalMediaAndroid<bool>(StoreKey.manageLocalMediaAndroid, null, false),
logLevel<int>(StoreKey.logLevel, null, 5), // Level.INFO = 5
preferRemoteImage<bool>(StoreKey.preferRemoteImage, null, false), preferRemoteImage<bool>(StoreKey.preferRemoteImage, null, false),
loopVideo<bool>(StoreKey.loopVideo, "loopVideo", true), loopVideo<bool>(StoreKey.loopVideo, "loopVideo", true),
loadOriginalVideo<bool>(StoreKey.loadOriginalVideo, "loadOriginalVideo", false), loadOriginalVideo<bool>(StoreKey.loadOriginalVideo, "loadOriginalVideo", false),
@@ -55,12 +49,7 @@ enum AppSettingsEnum<T> {
readonlyModeEnabled<bool>(StoreKey.readonlyModeEnabled, "readonlyModeEnabled", false), readonlyModeEnabled<bool>(StoreKey.readonlyModeEnabled, "readonlyModeEnabled", false),
albumGridView<bool>(StoreKey.albumGridView, "albumGridView", false), albumGridView<bool>(StoreKey.albumGridView, "albumGridView", false),
backupRequireCharging<bool>(StoreKey.backupRequireCharging, null, false), backupRequireCharging<bool>(StoreKey.backupRequireCharging, null, false),
backupTriggerDelay<int>(StoreKey.backupTriggerDelay, null, 30), backupTriggerDelay<int>(StoreKey.backupTriggerDelay, null, 30);
cleanupKeepFavorites<bool>(StoreKey.cleanupKeepFavorites, null, true),
cleanupKeepMediaType<int>(StoreKey.cleanupKeepMediaType, null, 0),
cleanupKeepAlbumIds<String>(StoreKey.cleanupKeepAlbumIds, null, ""),
cleanupCutoffDaysAgo<int>(StoreKey.cleanupCutoffDaysAgo, null, -1),
cleanupDefaultsInitialized<bool>(StoreKey.cleanupDefaultsInitialized, null, false);
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue); const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);
+4 -1
View File
@@ -6,6 +6,7 @@ import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.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/logger_db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
@@ -48,9 +49,11 @@ abstract final class Bootstrap {
await StoreService.init(storeRepository: storeRepo, listenUpdates: listenStoreUpdates); await StoreService.init(storeRepository: storeRepo, listenUpdates: listenStoreUpdates);
final metadataRepo = await MetadataRepository.ensureInitialized(drift);
await LogService.init( await LogService.init(
logRepository: LogRepository(logDb), logRepository: LogRepository(logDb),
storeRepository: storeRepo, metadataRepository: metadataRepo,
shouldBuffer: shouldBufferLogs, shouldBuffer: shouldBufferLogs,
); );
@@ -1,7 +1,7 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
ValueNotifier<T> useAppSettingsState<T>(AppSettingsEnum<T> key) { ValueNotifier<T> useAppSettingsState<T>(AppSettingsEnum<T> key) {
final notifier = useState<T>(Store.get(key.storeKey, key.defaultValue)); final notifier = useState<T>(Store.get(key.storeKey, key.defaultValue));
+130 -9
View File
@@ -1,25 +1,146 @@
import 'dart:async'; import 'dart:async';
import 'package:drift/drift.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/constants/colors.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
const int targetVersion = 25; const int targetVersion = 26;
Future<void> migrateDatabaseIfNeeded() async { Future<void> migrateDatabaseIfNeeded(Drift drift) async {
final int version = Store.get(StoreKey.version, targetVersion); final int version = Store.get(StoreKey.version, targetVersion);
if (version < 25) { if (version < 25) {
final accessToken = Store.tryGet(StoreKey.accessToken); await _migrateTo25();
if (accessToken != null && accessToken.isNotEmpty) { }
final serverUrls = ApiService.getServerUrls();
if (serverUrls.isNotEmpty) { if (version < 26) {
await NetworkRepository.setHeaders(ApiService.getRequestHeaders(), serverUrls, token: accessToken); await _migrateTo26(drift);
}
}
} }
await Store.put(StoreKey.version, targetVersion); await Store.put(StoreKey.version, targetVersion);
return; return;
} }
Future<void> _migrateTo25() async {
final accessToken = Store.tryGet(StoreKey.accessToken);
if (accessToken == null || accessToken.isEmpty) return;
final serverUrls = ApiService.getServerUrls();
if (serverUrls.isEmpty) return;
await NetworkRepository.setHeaders(ApiService.getRequestHeaders(), serverUrls, token: accessToken);
}
Future<void> _migrateTo26(Drift drift) async {
final migrator = _StoreMigrator(drift);
await migrator.migrateEnumName(StoreKey.legacyThemeMode, MetadataKey.themeMode, ThemeMode.values);
await migrator.migrateEnumIndex(StoreKey.legacyLogLevel, MetadataKey.logLevel, LogLevel.values);
await migrator.migrateEnumName(StoreKey.legacyPrimaryColor, MetadataKey.themePrimaryColor, ImmichColorPreset.values);
await migrator.migrateBool(StoreKey.legacyDynamicTheme, MetadataKey.themeDynamic);
await migrator.migrateBool(StoreKey.legacyColorfulInterface, MetadataKey.themeColorfulInterface);
final cleanupKeepAlbumIds = await migrator.readLegacyStoreString(StoreKey.legacyCleanupKeepAlbumIds.id);
if (cleanupKeepAlbumIds != null) {
final ids = cleanupKeepAlbumIds.split(',').where((id) => id.isNotEmpty).toList();
await drift.metadataEntity.insertOnConflictUpdate(
MetadataEntityCompanion.insert(
key: MetadataKey.cleanupKeepAlbumIds.key,
value: MetadataKey.cleanupKeepAlbumIds.encode(ids),
updatedAt: Value(DateTime.now()),
),
);
await migrator.deleteLegacyStoreRows([StoreKey.legacyCleanupKeepAlbumIds.id]);
}
await migrator.migrateBool(StoreKey.legacyCleanupKeepFavorites, MetadataKey.cleanupKeepFavorites);
await migrator.migrateEnumIndex(
StoreKey.legacyCleanupKeepMediaType,
MetadataKey.cleanupKeepMediaType,
AssetKeepType.values,
);
await migrator.migrateInt(StoreKey.legacyCleanupCutoffDaysAgo, MetadataKey.cleanupCutoffDaysAgo);
await migrator.migrateBool(StoreKey.legacyCleanupDefaultsInitialized, MetadataKey.cleanupDefaultsInitialized);
await migrator.complete();
}
class _StoreMigrator {
final Drift _db;
final Map<MetadataKey<Object>, Object> _cache = {};
final List<int> _migratedStoreIds = [];
_StoreMigrator(this._db);
Future<void> migrateEnumIndex<T extends Enum>(StoreKey<int> legacyKey, MetadataKey<T> newKey, List<T> values) async {
final index = await readLegacyStoreInt(legacyKey.id);
if (index == null) return;
final enumValue = values.elementAtOrNull(index) ?? newKey.defaultValue;
_cache[newKey] = enumValue;
_migratedStoreIds.add(legacyKey.id);
}
Future<void> migrateEnumName<T extends Enum>(
StoreKey<String> legacyKey,
MetadataKey<T> newKey,
List<T> values,
) async {
final name = await readLegacyStoreString(legacyKey.id);
if (name == null) return;
final enumValue = values.firstWhere((e) => e.name == name, orElse: () => newKey.defaultValue);
_cache[newKey] = enumValue;
_migratedStoreIds.add(legacyKey.id);
}
Future<void> migrateBool(StoreKey<bool> legacyKey, MetadataKey<bool> newKey) async {
final intValue = await readLegacyStoreInt(legacyKey.id);
if (intValue == null) return;
final boolValue = intValue != 0;
_cache[newKey] = boolValue;
_migratedStoreIds.add(legacyKey.id);
}
Future<void> migrateInt(StoreKey<int> legacyKey, MetadataKey<int> newKey) async {
final intValue = await readLegacyStoreInt(legacyKey.id);
if (intValue == null) return;
_cache[newKey] = intValue;
_migratedStoreIds.add(legacyKey.id);
}
Future<void> complete() async {
await _db.batch((batch) {
for (final entry in _cache.entries) {
batch.insert(
_db.metadataEntity,
MetadataEntityCompanion(key: Value(entry.key.key), value: Value(entry.key.encode(entry.value))),
mode: InsertMode.insertOrReplace,
);
}
});
await deleteLegacyStoreRows(_migratedStoreIds);
}
Future<String?> readLegacyStoreString(int id) async {
final row = await (_db.storeEntity.select()..where((t) => t.id.equals(id))).getSingleOrNull();
return row?.stringValue;
}
Future<int?> readLegacyStoreInt(int id) async {
final row = await (_db.storeEntity.select()..where((t) => t.id.equals(id))).getSingleOrNull();
return row?.intValue;
}
Future<void> deleteLegacyStoreRows(List<int> ids) async {
if (ids.isEmpty) return;
await (_db.storeEntity.delete()..where((t) => t.id.isIn(ids))).go();
}
}
@@ -119,13 +119,15 @@ class _VideoControlsState extends ConsumerState<VideoControls> {
onPressed: () => _toggle(isCasting), onPressed: () => _toggle(isCasting),
), ),
const Spacer(), const Spacer(),
Text( IgnorePointer(
"${position.format()} / ${duration.format()}", child: Text(
style: const TextStyle( "${position.format()} / ${duration.format()}",
color: Colors.white, style: const TextStyle(
fontWeight: FontWeight.w500, color: Colors.white,
fontFeatures: [FontFeature.tabularFigures()], fontWeight: FontWeight.w500,
shadows: VideoControls._controlShadows, fontFeatures: [FontFeature.tabularFigures()],
shadows: VideoControls._controlShadows,
),
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
@@ -7,6 +7,7 @@ import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/domain/services/log.service.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
@@ -30,7 +31,7 @@ class AdvancedSettings extends HookConsumerWidget {
final manageLocalMediaAndroid = useAppSettingsState(AppSettingsEnum.manageLocalMediaAndroid); final manageLocalMediaAndroid = useAppSettingsState(AppSettingsEnum.manageLocalMediaAndroid);
final isManageMediaSupported = useState(false); final isManageMediaSupported = useState(false);
final manageMediaAndroidPermission = useState(false); final manageMediaAndroidPermission = useState(false);
final levelId = useAppSettingsState(AppSettingsEnum.logLevel); final levelId = useState<int>(ref.read(systemConfigProvider).logLevel.index);
final preferRemote = useAppSettingsState(AppSettingsEnum.preferRemoteImage); final preferRemote = useAppSettingsState(AppSettingsEnum.preferRemoteImage);
final readonlyModeEnabled = useAppSettingsState(AppSettingsEnum.readonlyModeEnabled); final readonlyModeEnabled = useAppSettingsState(AppSettingsEnum.readonlyModeEnabled);
@@ -74,6 +74,7 @@ class _AlbumSyncActionButtonState extends ConsumerState<_AlbumSyncActionButton>
} catch (_) { } catch (_) {
} finally { } finally {
Future.delayed(const Duration(seconds: 1), () { Future.delayed(const Duration(seconds: 1), () {
if (!mounted) return;
setState(() { setState(() {
isAlbumSyncInProgress = false; isAlbumSyncInProgress = false;
}); });
@@ -1,15 +1,13 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/colors.dart'; import 'package:immich_mobile/constants/colors.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/theme.provider.dart'; import 'package:immich_mobile/providers/theme.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/theme/color_scheme.dart'; import 'package:immich_mobile/theme/color_scheme.dart';
import 'package:immich_mobile/theme/dynamic_theme.dart'; import 'package:immich_mobile/theme/dynamic_theme.dart';
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
class PrimaryColorSetting extends HookConsumerWidget { class PrimaryColorSetting extends HookConsumerWidget {
const PrimaryColorSetting({super.key}); const PrimaryColorSetting({super.key});
@@ -17,18 +15,10 @@ class PrimaryColorSetting extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final themeProvider = ref.read(immichThemeProvider); final themeProvider = ref.read(immichThemeProvider);
final themeConfig = ref.watch(appConfigProvider.select((config) => config.theme));
final primaryColorSetting = useAppSettingsState(AppSettingsEnum.primaryColor);
final systemPrimaryColorSetting = useAppSettingsState(AppSettingsEnum.dynamicTheme);
final currentPreset = useValueNotifier(ref.read(immichThemePresetProvider));
const tileSize = 55.0; const tileSize = 55.0;
useValueChanged(
primaryColorSetting.value,
(_, __) => currentPreset.value = ImmichColorPreset.values.firstWhere((e) => e.name == primaryColorSetting.value),
);
void popBottomSheet() { void popBottomSheet() {
Future.delayed(const Duration(milliseconds: 200), () { Future.delayed(const Duration(milliseconds: 200), () {
Navigator.pop(context); Navigator.pop(context);
@@ -36,23 +26,18 @@ class PrimaryColorSetting extends HookConsumerWidget {
} }
onUseSystemColorChange(bool newValue) { onUseSystemColorChange(bool newValue) {
systemPrimaryColorSetting.value = newValue; ref.read(metadataProvider).write(.themeDynamic, newValue);
ref.watch(dynamicThemeSettingProvider.notifier).state = newValue;
ref.invalidate(immichThemeProvider);
popBottomSheet(); popBottomSheet();
} }
onPrimaryColorChange(ImmichColorPreset colorPreset) { onPrimaryColorChange(ImmichColorPreset colorPreset) {
primaryColorSetting.value = colorPreset.name; ref.read(metadataProvider).write(.themePrimaryColor, colorPreset);
ref.watch(immichThemePresetProvider.notifier).state = colorPreset;
ref.invalidate(immichThemeProvider);
//turn off system color setting //turn off system color setting
if (systemPrimaryColorSetting.value) { if (themeConfig.dynamicTheme) {
onUseSystemColorChange(false); ref.read(metadataProvider).write(.themeDynamic, false);
} else {
popBottomSheet();
} }
popBottomSheet();
} }
buildPrimaryColorTile({ buildPrimaryColorTile({
@@ -122,7 +107,7 @@ class PrimaryColorSetting extends HookConsumerWidget {
'theme_setting_system_primary_color_title'.tr(), 'theme_setting_system_primary_color_title'.tr(),
style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500, height: 1.5), style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500, height: 1.5),
), ),
value: systemPrimaryColorSetting.value, value: themeConfig.dynamicTheme,
onChanged: onUseSystemColorChange, onChanged: onUseSystemColorChange,
), ),
), ),
@@ -140,7 +125,7 @@ class PrimaryColorSetting extends HookConsumerWidget {
topColor: theme.light.primary, topColor: theme.light.primary,
bottomColor: theme.dark.primary, bottomColor: theme.dark.primary,
tileSize: tileSize, tileSize: tileSize,
showSelector: currentPreset.value == preset && !systemPrimaryColorSetting.value, showSelector: themeConfig.primaryColor == preset && !themeConfig.dynamicTheme,
), ),
); );
}).toList(), }).toList(),
@@ -3,72 +3,48 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/theme.provider.dart'; import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/settings/preference_settings/primary_color_setting.dart'; import 'package:immich_mobile/widgets/settings/preference_settings/primary_color_setting.dart';
import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart';
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
class ThemeSetting extends HookConsumerWidget { class ThemeSetting extends HookConsumerWidget {
const ThemeSetting({super.key}); const ThemeSetting({super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final currentThemeString = useAppSettingsState(AppSettingsEnum.themeMode); final currentTheme = useState(ref.read(appConfigProvider.select((config) => config.theme.mode)));
final currentTheme = useValueNotifier(ref.read(immichThemeModeProvider));
final isDarkTheme = useValueNotifier(currentTheme.value == ThemeMode.dark); final isDarkTheme = useValueNotifier(currentTheme.value == ThemeMode.dark);
final isSystemTheme = useValueNotifier(currentTheme.value == ThemeMode.system); final isSystemTheme = useValueNotifier(currentTheme.value == ThemeMode.system);
final colorfulInterface = useValueNotifier(
final applyThemeToBackgroundSetting = useAppSettingsState(AppSettingsEnum.colorfulInterface); ref.watch(appConfigProvider.select((config) => config.theme.colorfulInterface)),
final applyThemeToBackgroundProvider = useValueNotifier(ref.read(colorfulInterfaceSettingProvider));
useValueChanged(
currentThemeString.value,
(_, __) => currentTheme.value = switch (currentThemeString.value) {
"light" => ThemeMode.light,
"dark" => ThemeMode.dark,
_ => ThemeMode.system,
},
);
useValueChanged(
applyThemeToBackgroundSetting.value,
(_, __) => applyThemeToBackgroundProvider.value = applyThemeToBackgroundSetting.value,
); );
void onThemeChange(bool isDark) { void onThemeChange(bool isDark) {
if (isDark) { currentTheme.value = isDark ? ThemeMode.dark : ThemeMode.light;
ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.dark; ref.read(metadataProvider).write(.themeMode, currentTheme.value);
currentThemeString.value = "dark";
} else {
ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.light;
currentThemeString.value = "light";
}
} }
void onSystemThemeChange(bool isSystem) { void onSystemThemeChange(bool isSystem) {
if (isSystem) { if (isSystem) {
currentThemeString.value = "system"; currentTheme.value = ThemeMode.system;
isSystemTheme.value = true; isSystemTheme.value = true;
ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.system;
} else { } else {
final currentSystemBrightness = context.platformBrightness; final currentSystemBrightness = context.platformBrightness;
isSystemTheme.value = false; isSystemTheme.value = false;
isDarkTheme.value = currentSystemBrightness == Brightness.dark; isDarkTheme.value = currentSystemBrightness == Brightness.dark;
if (currentSystemBrightness == Brightness.light) { if (currentSystemBrightness == Brightness.light) {
currentThemeString.value = "light"; currentTheme.value = ThemeMode.light;
ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.light;
} else if (currentSystemBrightness == Brightness.dark) { } else if (currentSystemBrightness == Brightness.dark) {
currentThemeString.value = "dark"; currentTheme.value = ThemeMode.dark;
ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.dark;
} }
} }
ref.read(metadataProvider).write(.themeMode, currentTheme.value);
} }
void onSurfaceColorSettingChange(bool useColorfulInterface) { void onSurfaceColorSettingChange(bool useColorfulInterface) {
applyThemeToBackgroundSetting.value = useColorfulInterface; ref.read(metadataProvider).write(.themeColorfulInterface, useColorfulInterface);
ref.watch(colorfulInterfaceSettingProvider.notifier).state = useColorfulInterface; colorfulInterface.value = useColorfulInterface;
} }
return Column( return Column(
@@ -91,7 +67,7 @@ class ThemeSetting extends HookConsumerWidget {
), ),
const PrimaryColorSetting(), const PrimaryColorSetting(),
SettingsSwitchListTile( SettingsSwitchListTile(
valueNotifier: applyThemeToBackgroundProvider, valueNotifier: colorfulInterface,
title: "theme_setting_colorful_interface_title".t(context: context), title: "theme_setting_colorful_interface_title".t(context: context),
subtitle: 'theme_setting_colorful_interface_subtitle'.t(context: context), subtitle: 'theme_setting_colorful_interface_subtitle'.t(context: context),
onChanged: onSurfaceColorSettingChange, onChanged: onSurfaceColorSettingChange,
+2 -2
View File
@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 2.7.5 - API version: 3.0.0
- Generator version: 7.8.0 - Generator version: 7.8.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen - Build package: org.openapitools.codegen.languages.DartClientCodegen
@@ -146,7 +146,7 @@ Class | Method | HTTP request | Description
*DeprecatedApi* | [**runQueueCommandLegacy**](doc//DeprecatedApi.md#runqueuecommandlegacy) | **PUT** /jobs/{name} | Run jobs *DeprecatedApi* | [**runQueueCommandLegacy**](doc//DeprecatedApi.md#runqueuecommandlegacy) | **PUT** /jobs/{name} | Run jobs
*DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | Download asset archive *DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | Download asset archive
*DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | Retrieve download information *DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | Retrieve download information
*DuplicatesApi* | [**deleteDuplicate**](doc//DuplicatesApi.md#deleteduplicate) | **DELETE** /duplicates/{id} | Delete a duplicate *DuplicatesApi* | [**deleteDuplicate**](doc//DuplicatesApi.md#deleteduplicate) | **DELETE** /duplicates/{id} | Dismiss a duplicate group
*DuplicatesApi* | [**deleteDuplicates**](doc//DuplicatesApi.md#deleteduplicates) | **DELETE** /duplicates | Delete duplicates *DuplicatesApi* | [**deleteDuplicates**](doc//DuplicatesApi.md#deleteduplicates) | **DELETE** /duplicates | Delete duplicates
*DuplicatesApi* | [**getAssetDuplicates**](doc//DuplicatesApi.md#getassetduplicates) | **GET** /duplicates | Retrieve duplicates *DuplicatesApi* | [**getAssetDuplicates**](doc//DuplicatesApi.md#getassetduplicates) | **GET** /duplicates | Retrieve duplicates
*DuplicatesApi* | [**resolveDuplicates**](doc//DuplicatesApi.md#resolveduplicates) | **POST** /duplicates/resolve | Resolve duplicate groups *DuplicatesApi* | [**resolveDuplicates**](doc//DuplicatesApi.md#resolveduplicates) | **POST** /duplicates/resolve | Resolve duplicate groups

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