Compare commits

...

349 Commits

Author SHA1 Message Date
Alex
36cffbd44a chore: attempt to handle database lock error when scrubbing the timeline 2025-07-17 11:39:06 -05:00
shenlong
531515daf9 feat: add toggle to switch between Isar and Sqlite (#19953) 2025-07-17 11:12:29 -05:00
Min Idzelis
b256c51b6b docs: update devcontainer docs (#19237) 2025-07-17 14:49:55 +00:00
Lucas Correia
238dc7c085 chore: adjust setting subtitle to less clumsy expression (#19970) 2025-07-17 15:40:49 +02:00
bo0tzz
184c7390a1 chore: also redirect docs/ with trailing slash (#19965)
Cause I keep running into that lol
2025-07-16 21:14:58 -05:00
Mert
649221176c refactor(mobile): delete local button in new timeline (#19961)
* delete local action button

* include source

* move prompt

* batch
2025-07-16 16:25:41 -05:00
renovate[bot]
eae2471ab5 chore(deps): update base-image to v202507162011 (major) (#19983)
chore(deps): update base-image to v202507162011

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-16 16:22:33 -05:00
shenlong
bfceed15da feat: add license page to app bar dialog (#19971)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-07-16 09:36:17 -05:00
bo0tzz
d9891f759e chore: use upstream setup-dcm action (#19963) 2025-07-16 09:35:36 -05:00
evan314159
32f23b8d38 fix(web): prevent flashing white background in dark mode on page load/reload (#19934)
* prevent flashing white background in dark mode on page load/reload, tested with Safari and Chrome on macOS

* integrate into existing FOUC-prevention

---------

Co-authored-by: Evan <evan@MacBook-Pro.local>
2025-07-16 14:34:45 +00:00
Brandon Wees
743b6644e9 feat(widgets): iOS widget improvements (#19893)
* improvements to error handling, ability to select "Favorites" as a virtual album, fix widgets not showing image when tinting homescreen

* dont include isFavorite all the time

* remove check for if the album exists

this will never run because we default to Album.NONE and its impossible to distinguish between no album selected and album DNE (we dont know what the store ID is, only what iOS gives)
2025-07-15 21:17:24 -05:00
Alex
34620e1e9a feat: album edit (#19936) 2025-07-15 20:37:44 -05:00
Jason Rasmussen
bcb968e3d1 refactor: job names (#19949) 2025-07-15 18:39:00 -04:00
Jason Rasmussen
e73abe0762 refactor: enum casing (#19946) 2025-07-15 14:50:13 -04:00
Jason Rasmussen
920d7de349 refactor: event names (#19945) 2025-07-15 13:41:19 -04:00
Jason Rasmussen
351701c4d6 refactor: validate enum (#19943) 2025-07-15 17:14:57 +00:00
Min Idzelis
68f249bc03 feat: improve geodata import speed (#19906)
chore(deps): update dependency vite to v7 (#19657)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2025-07-15 16:23:41 +00:00
renovate[bot]
eca54871d0 chore(deps): update docker.io/valkey/valkey:8-bookworm docker digest to facc1d2 (#19932)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-15 15:43:36 +00:00
renovate[bot]
b359eea124 chore(deps): update prom/prometheus docker digest to 63805eb (#19933)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-15 16:43:14 +01:00
renovate[bot]
c18f167e29 chore(deps): update immich-app/devtools action to v0.1.1 (#19938)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-15 16:42:49 +01:00
Daimolean
ba262fbaa8 feat(mobile): drift place page (#19914)
* feat(mobile): drift place page

* merge main

* feat(mobile): drift place detail page (#19915)

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-07-15 15:10:12 +00:00
Alex
59e7754bdc feat: websocket background sync (#19888)
* feat: websocket background sync

* batch websocket

* pr feedback
2025-07-15 09:38:28 -05:00
Daimolean
0acbf1199a feat(mobile): drift user metadata sync (#19894)
* feat(mobile): drift user metadata sync

* change text to jsonb

* update primary key

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-07-15 09:17:35 -05:00
Hamish
daea57f7d2 feat(web): better coordinate parsing (#19832)
feat: better coordinate parsing
2025-07-15 08:32:43 -05:00
renovate[bot]
82c3165247 fix(deps): update typescript-projects (#19808)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2025-07-14 22:30:39 +00:00
bo0tzz
3a854d77ac fix: use IMMICH_HOST env var in ML healthcheck (#19844)
Fixes #19843
2025-07-14 18:08:29 -04:00
Lukas
ccd0c35ca1 fix(web): adjust button size in person side panel (#19924)
* fix(web): adjust button size in person side panel

* round edit button
2025-07-14 18:05:34 -04:00
Lukas
5f10a4cae7 fix(web): allow renaming person without merging (#19923)
* fix(web): allow renaming person without merging

* improve return type
2025-07-14 14:24:32 -05:00
shenlong
9abb95d34a feat: handle live photos on new asset viewer (#19926)
sync and handle livePhotoVideoId in asset viewer

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-07-14 14:23:24 -05:00
Jason Rasmussen
805ec3e351 chore: asset sync FKs (#19927) 2025-07-14 10:57:25 -05:00
Sergey Katsubo
a97ba4862f fix(docs): Nightly Tasks Settings (#19907)
* Document new Nightly Tasks Settings

* Add a link to the settings

* List jobs that are configured in their own System Settings sections
2025-07-14 10:24:34 -04:00
Jason Rasmussen
c699df002a feat: rename schema (#19891) 2025-07-14 10:13:06 -04:00
shenlong
33c29e4305 chore: pass flavor to vscode flutter codelens (#19919) 2025-07-14 07:32:29 -05:00
Dominik Vogel
b0098d6d23 Update external-library.md (#19901) 2025-07-12 19:50:51 +00:00
renovate[bot]
04aab6ecce chore(deps): update dependency vite to v7 (#19657)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2025-07-11 23:32:35 +02:00
Jason Rasmussen
47c0dc0d7e feat: nightly tasks (#19879) 2025-07-11 17:32:10 -04:00
Daniel Dietzler
df581cc0d5 feat: UserMetadata sync (#19882)
* feat: UserMetadata sync

* refactor: sync table filters (#19887)
2025-07-11 18:19:53 +00:00
Jason Rasmussen
9e48ae3052 feat: naming strategy (#19848)
* feat: naming strategy

* feat: detect renames
2025-07-11 11:35:10 -04:00
shenlong
1d19d308e2 chore: update flutter to 3.32.6 (#19878)
* chore: update flutter to 3.32.6

* fix lint

* fix asset viewer scroll physics

* test: change init order for map test

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-07-11 10:34:49 -05:00
Alex
de4217cefc feat: action buttons in more view (#19867)
* feat: action buttons in more view

* pr feedback
2025-07-11 10:34:38 -05:00
Jason Rasmussen
617a2f146d fix: startup log level (#19885) 2025-07-11 11:22:38 -04:00
Alex
2b07d7ac63 feat: remove from album action button (#19884)
* feat: remove from album action

* feat: remove from album action
2025-07-11 10:06:53 -05:00
Jason Rasmussen
1cc5ca14ca feat: allow unordered migrations in dev (#19881) 2025-07-11 10:58:34 -04:00
shenlong
a625921e8f refactor: timeline repo queries (#19871)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-07-11 14:57:46 +00:00
Zach McGaughey
a17bba3328 fix: allow insecure connections in iOS WidgetExtension (#19863)
Allow insecure connections in WidgetExtension
2025-07-11 08:46:17 -05:00
Jason Rasmussen
4b3a4725c6 feat: pending sync reset flag (#19861) 2025-07-11 09:38:02 -04:00
shenlong
34f0f6c813 chore: rename new migration to execute last (#19872)
chore: rename new migration to change execution order

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-07-11 08:41:06 -04:00
Min Idzelis
906d14c172 chore: missing @types dependency for geojson (#19870) 2025-07-11 13:42:25 +02:00
Alex
d087f7c870 chore: beta flavor build (#19862)
* chore: beta flavor build

* make file

* beta flavor

* add build flavor to GHA

* add build flavor to GHA
2025-07-10 21:42:29 -05:00
Min Idzelis
de345a9524 chore: makefile split setup tasks (#19739)
Makefile split setup tasks
2025-07-10 21:35:39 -04:00
Min Idzelis
badd7ea2a9 chore: more missing deps (#19868) 2025-07-11 00:56:04 +02:00
shenlong
7d8f56b483 chore: hero animations (#19860)
* remove herocontrollerscope

* handle heroOffset in new timeline

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-07-10 20:25:18 +00:00
shenlong
70b73145f1 fix: ensure buffer loaded before opening asset viewer (#19864)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-07-10 20:21:50 +00:00
Brandon Wees
d178c52ba6 fix: cold start deep link logic (#19859) 2025-07-10 14:27:42 -05:00
SGT
55fe67dd20 fix(server): clear activity when asset is removed from album (#19019) 2025-07-10 19:37:56 +02:00
Jason Rasmussen
ed4c7817e7 feat: AssetUploadReadyV1 event (#19858) 2025-07-10 13:30:10 -04:00
Zack Pollard
39c95f1280 refactor: rename geodata pk constraint to match runtime constraint name (#19856) 2025-07-10 17:18:51 +00:00
Daimolean
4ddd3764b4 feat(mobile): hide storage indicator outside main timeline (#19835)
* feat(mobile): hide storage indicator outside main timeline

* fix: lint

* set default as false

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-07-10 11:59:52 -05:00
Alex
68db17028b feat: new create album page (#19731)
* feat: new create album page

* finished create album flow

* refactor into stateful widgets

* refactor

* focus fix

* lint

* default sort

* pr feedback
2025-07-10 11:59:15 -05:00
immich-tofu[bot]
1f50a0075e chore: modify .github/workflows/org-checks.yml 2025-07-10 15:59:44 +00:00
Zack Pollard
b19884d01e feat(server): people sync (#19854)
* chore: fix missing usage of deleteType for syncMemoriesV1

* chore: add src path for proper absolute imports in jetbrains

* feat: people sync
2025-07-10 11:32:42 -04:00
Alex
feff1899ee feat: expanded sliver app bar (#19827)
* use mutex

* feat: cool app bar

* animation

* adapt to more pages

* animation

* better animation

* fix: asset count

* Revert "fix: asset count"

This reverts commit 673a5b264b.

* fix: asset count

* fix: shaky animation on Android

* tunning

* offset SizedBox to fix scroll jump on multiselect

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-07-10 10:13:46 -05:00
Sergey Katsubo
977d6452f6 fix(docs): library and folders minor adjustments (#19642) 2025-07-10 14:31:06 +00:00
Léopold Koprivnik
f778adea92 feat: adds option to search only for untagged assets (#19730)
Co-authored-by: SkwalExe <skwal@skwal.net>
2025-07-10 16:28:20 +02:00
Min Idzelis
818bdde317 chore: update base images (#19741)
Update base images
2025-07-09 19:22:01 -04:00
Mert
fd48a33686 refactor(mobile): image thumbnails (#19821)
* image thumbnail refactor

* minor const-ification in new thumbnail tile

* underscore helper classes

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-07-09 14:16:09 -05:00
Brandon Wees
a918481c0b feat(mobile): cache latest ios widget entry for fallback (#19824)
* cache the last image an ios widget fetched and use if a fetch fails in a future timeline build

* code review fixes

* downgrade pbx for flutter

* use cache in snapshots
2025-07-09 13:59:54 -05:00
renovate[bot]
a201665b7e chore(deps): update base-image to v202507091427 (major) (#19840)
chore(deps): update base-image to v202507091427

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-09 12:52:11 -05:00
Brandon Wees
2a222fcfba chore(docs): roadmap updates (#19841) 2025-07-09 16:54:00 +01:00
immich-tofu[bot]
d902e7f87d chore: modify .github/workflows/org-checks.yml 2025-07-09 15:23:30 +00:00
shenlong
6278fe43c0 fix: run reload within a mutex (#19826)
* fix: run reload within a mutex

* change total assets before emitting the reload event

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-07-09 10:15:37 -05:00
shenlong
dfe6d27bbd feat: sqlite video player (#19792)
* feat: video player

* use remote asset id in local query

* fix: error from pre-caching beyond total assets

* fix: flipped local videos

* incorrect aspect ratio on iOS

* ignore other storage id during equals check

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-07-09 09:34:25 -05:00
Jason Rasmussen
51ab7498e9 feat: create table with constraints (#19828) 2025-07-09 09:13:14 -04:00
Hamish
4db76ddcf0 feat(web): update icons (#19831)
* fix: update password icon in user settings

* feat: add icons to more modals
2025-07-09 02:12:16 +00:00
Brandon Wees
d03eb87058 fix(mobile): deeplinking to asset view while viewer is already open (#19812)
* initial attempt at new guard

* do not resolve the route when replaced

* Update gallery_guard.dart comment

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-07-08 10:50:59 -05:00
Daimolean
a556de67b0 feat(mobile): drift local albums page (#19817)
* feat(mobile): drift local albums page

* fix: lint

* refactor: use AsyncValue

* fix: lint

* local album thumbnail

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-07-08 15:37:57 +00:00
Daimolean
e703685d8d feat(mobile): drift partner detail page (#19815)
* feat(mobile): drift partner detail page

* fix: lint
2025-07-08 10:31:07 -05:00
Daimolean
172388c455 feat(mobile): drift recently taken page (#19814)
* feat(mobile): drift recently taken page

* fix: lint

* refactor(mobile): timeline queries (#19818)

refactor: remote assets query to take single user id

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>

---------

Co-authored-by: shenlong <139912620+shenlong-tanwen@users.noreply.github.com>
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-07-08 13:54:29 +00:00
Jason Rasmussen
df4a27e8a7 feat: sql-tools overrides (#19796) 2025-07-08 08:17:40 -04:00
renovate[bot]
1f9813a28e chore(deps): update github/codeql-action action to v3.29.2 (#19806)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-08 12:26:07 +01:00
renovate[bot]
bbfff45058 chore(deps): update redis:6.2-alpine docker digest to 03fd052 (#19804)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-08 12:18:58 +02:00
Alex
87dd09d103 feat: selection mode timeline (#19734)
* feat: new page

* force multi-selection state

* fix: provider scoping

* Return selected assets

* lint

* lint

* simplify provider scope and drop drilling

* selection styling
2025-07-08 10:11:37 +05:30
Alex
dd94ad17aa fix: scrubber scroll error when page is not long enough (#19809) 2025-07-07 23:30:47 -05:00
renovate[bot]
a87c2e82cd fix(deps): update typescript-projects (#19666)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2025-07-07 20:29:58 +00:00
bo0tzz
a11ab4c3f7 chore: tidy up DCM analysis workflow (#19797) 2025-07-07 15:11:44 -05:00
Daimolean
ebf2f9fd7b feat(mobile): drift library page (#19789)
* feat(mobile): drift library page

* merge main & fix sliver padding

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-07-07 15:11:16 -05:00
Daimolean
683af67344 feat(mobile): drift video page (#19784)
* feat(mobile): drift video page

* filter motional parts

* remove status indicator join

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-07-07 18:51:49 +00:00
Daimolean
d149d6fa3f feat(mobile): drift favorite page (#19783)
* feat(mobile): drift favorite page

* remove status indicator join
2025-07-07 12:57:08 -05:00
Alex
8c5269c002 chore: lock DCM version in GHA (#19795) 2025-07-07 17:56:54 +00:00
Daimolean
cf91d9bdfc feat(mobile): drift locked folder page (#19746)
* feat(mobile): drift locked folder page

* feat: include local indicator

* remove join in bucket

* remove status indicator join

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-07-07 04:29:33 +00:00
Daimolean
5579554532 feat(mobile): drift trash page (#19745)
* feat(mobile): drift trash page

* feat: include local indicator

* remove join in bucket

* remove indicator join

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-07-07 04:05:40 +00:00
Damiano Ferrari
7e35e6985e chore: Use a contrasted color for text avatar (#19756) 2025-07-06 22:59:48 -05:00
Daimolean
56756baea2 feat(mobile): drift archive page (#19740)
* feat(mobile): drift archive page

* fix: lint

* feat: include local indicator

* remove join in bucket

* remove showing local indicator in bucket

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
Co-authored-by: alex <alex@localhost-live.localdomain>
2025-07-07 03:51:58 +00:00
matthieu-db
d5923241b5 fix: add quiet zone to QR code (#19771)
Add quiet zone to QR code

This is needed for the QR code to be readable by many QR readers. It is also a requirement for it to be a valid QR code.
2025-07-06 22:06:36 -05:00
Daimolean
cc471806fe feat(mobile): stack sync (#19735)
* feat(mobile): stack sync

* fix: lint

* Update mobile/lib/infrastructure/repositories/sync_api.repository.dart

Co-authored-by: Alex <alex.tran1502@gmail.com>

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-07-06 22:01:09 -05:00
Daniel Dietzler
4ce9bce414 feat: oauth role claim (#19758) 2025-07-06 18:45:32 -04:00
OffsetMonkey538
2f5d75ce21 docs: fix typo of webp listed under jpeg (#19743) 2025-07-05 15:52:22 +00:00
Daimolean
fb384fe90b fix(web): viewing asset lock (#19499)
* fix(web): viewing asset lock

* fix: lint

* make mutex stateless

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-07-05 02:52:39 +00:00
shenlong
73733370a2 feat: adds bottom sheet map and actions (#19726)
* reduce timeline rebuilds

* feat: adds bottom sheet map and actions (#19692)

* adds bottom sheet map and actions

* PR feedbacks

* only reload the asset viewer if asset is changed

* styling tweak

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>

* rename singleton and remove event prefix

* adds bottom sheet map and actions

* PR feedbacks

* refactor: use provider for viewer state

* feat: adds top and bottom app bar

* add safe area to bottom app bar

* change app and bottom bar color

* viewer - always have black background

* use the full width for the bottom sheet on landscape as well

* constraint the bottom sheet to not expand all the way

* add padding for location details in landscape

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-07-04 14:08:06 -05:00
Alex
4a2cf28882 feat: memories in new timeline (#19720)
* feat: memories sliver

* memories lane

* display and show memory

* fix: get correct memories

* naming

* pr feedback

* use equalsValue for visibility

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-07-04 13:49:15 -05:00
shenlong
181efb9010 refactor: reduce timeline rebuilds (#19704)
* reduce timeline rebuilds

* feat: adds bottom sheet map and actions (#19692)

* adds bottom sheet map and actions

* PR feedbacks

* only reload the asset viewer if asset is changed

* styling tweak

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>

* rename singleton and remove event prefix

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-07-04 10:30:34 -05:00
renovate[bot]
b00d44a00c fix(deps): update machine-learning (#19647) 2025-07-03 20:28:34 +00:00
Jason Rasmussen
6044663e26 refactor: sql-tools (#19717) 2025-07-03 10:59:17 -04:00
aviv926
484529e61e feat(server): add immich and postgres version to the database backup name (#19603) 2025-07-03 10:35:24 +01:00
Alex
445f9174ea feat: memories sync (#19644)
* feat: memories sync

* Update mobile/lib/infrastructure/repositories/sync_stream.repository.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update mobile/lib/infrastructure/repositories/sync_stream.repository.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* show sync information

* tests and pr feedback

* pr feedback

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-02 19:18:37 +00:00
shenlong
7855974a29 feat(mobile): sqlite asset viewer (#19552)
* add full image provider and refactor thumb providers

* photo_view updates

* wip: asset-viewer

* fix controller dispose on page change

* wip: bottom sheet

* fix interactions

* more bottomsheet changes

* generate schema

* PR feedback

* refactor asset viewer

* never rotate and fix background on page change

* use photoview as the loading builder

* precache after delay

* claude: optimizing rebuild of image provider

* claude: optimizing image decoding and caching

* use proper cache for new full size image providers

* chore: load local HEIC fullsize for iOS

* make controller callbacks nullable

* remove imageprovider cache

* do not handle drag gestures when zoomed

* use loadOriginal setting for HEIC / larger images

* preload assets outside timer

* never use same controllers in photo-view gallery

* fix: cannot scroll down once swipe with bottom sheet

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-07-02 18:24:37 +00:00
Daimolean
ec603a008c feat(mobile): unarchive and unfavorite action (#19678) 2025-07-02 12:27:30 -05:00
shenlong
14276f41d8 fix: handle null bucket name during android sync (#19685)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-07-02 12:26:42 -05:00
Daimolean
a644cabab6 feat(mobile): trash and delete action (#19681)
* feat(mobile): trash and delete action

* fix lint
2025-07-02 12:26:07 -05:00
Daimolean
b8e67d0ef9 fix(mobile): filter deleted assets (#19683) 2025-07-02 12:25:14 -05:00
Min Idzelis
ca78bc91b6 feat: fully qualified path in error msg (#19674)
* feat: fully qualified path in error msg

* import style
2025-07-02 09:31:20 -04:00
shenlong
f2f3db3a79 refactor: action provider (#19677)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-07-01 23:08:52 -05:00
Jason Rasmussen
c435bdb5d3 refactor: sql-tools readers (#19672) 2025-07-01 22:57:17 -04:00
Min Idzelis
15da0d5a71 fix: email button (#19675) 2025-07-01 22:48:41 -04:00
Min Idzelis
090d87f82e chore: dev environment improvements and dependency updates (#19676) 2025-07-01 22:47:59 -04:00
Alex
25efba8fe6 chore: remove share link success prompt (#19671) 2025-07-01 16:55:17 +00:00
Daimolean
83afd49f5c feat(mobile): edit location action (#19645)
* change dto from integer to double

* feat(mobile): edit location action

* patch openapi

* refactor in provider

* fix lint

* chore: not showing success prompt if dimissed

* i18n

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-07-01 16:52:11 +00:00
Ramon Smits
639ede78c2 docs: document DB_STORAGE_TYPE environment variable (#19609)
Co-authored-by: Zack Pollard <github@zackpollard.uk>
2025-07-01 16:13:24 +00:00
shenlong
15be3437bf fix: timeline service uninitialised across routes (#19544) 2025-07-01 10:23:20 -05:00
Daimolean
f59b0bab5a refactor(mobile): action provider (#19669)
* refactor action provider

* fix lint
2025-07-01 10:18:23 -05:00
Alex
fa418d778b feat: lock folder action (#19634)
* feat: lock folder action

* refactor
2025-07-01 14:03:45 +00:00
bo0tzz
e0c4b8df6f chore: remove runner deps install step (#19527)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-01 14:18:14 +01:00
Min Idzelis
7f9689b4bc feat: bin for cli (#19648) 2025-07-01 08:00:41 -04:00
seifer44
e6f8bfdf5e chore(docs): add instruction for trusting self-signed certificates with Immich and an OAuth server (#18624) 2025-07-01 11:21:57 +00:00
Min Idzelis
8ccca04e27 fix(web): improve request cancellation handling in service worker cache (#19217) 2025-07-01 11:53:04 +01:00
Daniel Dietzler
53f80393bf chore: upgrade to cron v4 (#19664) 2025-07-01 12:47:04 +02:00
renovate[bot]
e5e857edc3 chore(deps): update prom/prometheus docker digest to 7a34573 (#19646)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-01 11:33:39 +01:00
renovate[bot]
590f96246d chore(deps): update github-actions (#19654)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-01 11:33:01 +01:00
renovate[bot]
38d73f2bc6 chore(deps): update dependency @types/node to ^22.15.33 (#19653)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-01 11:32:11 +01:00
renovate[bot]
96e3b96d57 fix(deps): update dependency nestjs-otel to v7 (#19662)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-01 11:02:46 +01:00
renovate[bot]
36b018e355 fix(deps): update typescript-projects (#18898)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Zack Pollard <zackpollard@ymail.com>
2025-07-01 10:00:35 +00:00
renovate[bot]
214ca50406 chore(deps): update node.js to v22.17.0 (#19656)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-01 10:59:31 +01:00
renovate[bot]
29b3981609 fix(deps): update dependency nestjs-kysely to v3 (#19660)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-01 10:27:20 +01:00
Mert
a068a41c06 fix(server): prevent duplicate geodata temp table (#18580)
drop tmp table, create gist index first
2025-06-30 23:28:30 -04:00
bo0tzz
3c6e9e1191 feat: use request host as default SSR domain (#19485)
fix: hostname and domain confusion

chore: e2e test
2025-06-30 23:24:44 -04:00
Min Idzelis
db0415bbcc chore: undeclared versions/updates (#19649) 2025-06-30 23:23:41 -04:00
shenlong
a5c431fbf5 refactor: animate bottom sheet (#19655)
* refactor: animate bottom sheet

* rebase on main

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-06-30 22:23:38 -05:00
Min Idzelis
a3d588f6bd feat: makefile improvements (#19650) 2025-06-30 21:40:42 -05:00
shenlong
21f500191a refactor: actions provider (#19651)
* refactor: actions provider

* chore: rename error and stack

* remove empty checks

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-07-01 08:10:25 +05:30
shenlong
5011636d95 refactor: header - bulk select icon (#19652)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-06-30 21:33:37 -05:00
Alex
3f330c6476 feat: drift album page (#19564)
* feat: drift album page

* feat: page renderred

* feat: asset count

* refactor: use statefulwidget

* refactor: private widgets

* refactor: service layer

* refactor: import

* feat: get owner name

* pr feedback

* pr feedback

* pr feedback

* pr feedback
2025-07-01 07:54:50 +05:30
Daimolean
bb8755021d revert: timeout (#19639) 2025-06-30 17:02:50 -05:00
Jason Rasmussen
93f9e118ad refactor: timeline tests (#19641) 2025-06-30 17:43:45 -04:00
Jason Rasmussen
58ca1402ed feat: sync partner stacks (#19635) 2025-06-30 16:41:06 -04:00
Daimolean
32a7087883 feat(mobile): archive action (#19630)
* feat(mobile): archive action

* fix: lint

* Update i18n/en.json

Co-authored-by: Alex <alex.tran1502@gmail.com>

* fix: lint

* fix: lint

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-06-30 14:38:15 -05:00
Daimolean
53020852ec fix(web): modal race condition (#19625)
* fix(web): modal race condition

* fix: translation

* fix: translation
2025-06-30 14:33:47 -05:00
Jason Rasmussen
181a7e115f feat: sync stacks (#19629) 2025-06-30 14:26:41 -05:00
Alex
095ace8687 feat: shared link action (#19610) 2025-06-30 17:32:18 +00:00
Alex
4c3fcdc745 feat: favorite action (#19623) 2025-06-30 12:21:09 -05:00
Alex
fa5f30d9ca fix: timeline service mismatch state (#19612) 2025-06-30 12:20:13 -05:00
Jason Rasmussen
e60bc3c304 refactor: database types (#19624) 2025-06-30 13:19:16 -04:00
Jason Rasmussen
09cbc5d3f4 refactor: change password repository lookup (#19584) 2025-06-27 16:52:04 -04:00
Jason Rasmussen
a2a9797fab refactor: auth medium tests (#19583) 2025-06-27 15:35:19 -04:00
Matthew Momjian
3d35e65f27 fix(docs): add DB_USERNAME when needed (#19578)
add DB_USERNAME when needed
2025-06-27 13:54:27 -04:00
Jason Rasmussen
df76735f4a refactor: sync repository (#19581) 2025-06-27 13:47:06 -04:00
Jason Rasmussen
6feca56da8 feat: sync memories (#19579) 2025-06-27 12:20:13 -04:00
Alex
97aabe466e feat: action buttons place holder (#19561)
* feat: action buttons place holder

* lint

* Update base_action_button.widget.dart

Co-authored-by: shenlong <139912620+shenlong-tanwen@users.noreply.github.com>

---------

Co-authored-by: shenlong <139912620+shenlong-tanwen@users.noreply.github.com>
2025-06-27 13:33:46 +00:00
shenlong
72a53f43c8 feat: use sqlite timeline user provider (#19577)
use sqlite timeline user provider

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-06-27 08:29:27 -05:00
Jason Rasmussen
30b4f334d8 feat: upload manager (#19565) 2025-06-27 09:13:43 -04:00
Jason Rasmussen
6c6a32c63e refactor: memory medium tests (#19568) 2025-06-26 19:52:10 -04:00
Jason Rasmussen
6fed223405 fix: array-max-length (#19562) 2025-06-26 19:41:48 +00:00
Jason Rasmussen
3105094a3d refactor: medium tests (#19537) 2025-06-26 15:32:06 -04:00
Jason Rasmussen
b96c95beda refactor(server): sync service (#19559) 2025-06-26 17:24:57 +00:00
Daimolean
926ff075a3 fix(web): absolute path match in external library (#19551) 2025-06-26 10:46:34 -05:00
Min Idzelis
934649c8df feat(server): check additional exif date tags (#19216)
* feat(server): check additional exif date tags

- Add support for UTC date tags (GPSDateTime, DateTimeUTC, GPSDateStamp, SonyDateTime2)
- This matches tags that exiftool-vendored uses for tzSource in extractTzOffsetFromUTCOffset()

* Review comments

* nit

* review comments

* lots of tests for exif datetime

* missed

* format

* format again

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-06-26 10:18:40 -05:00
Brandon Wees
a43159f4ba fix(mobile): skip widget updates if on android (#19553)
* fix: skip widget updates if on android

* remove dead line
2025-06-26 10:07:22 -05:00
shenlong
ea3a14ed25 feat(mobile): add album asset sync (#19522)
* feat(mobile): add album asset sync

* add SyncAlbumToAssetDeleteV1 to openapi-spec

* update delete queries to use where in statements

* clear remote album when clear remote data

* fix: bad merge

* fix: bad merge

* fix: _SyncAckV1 return type

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: wuzihao051119 <wuzihao051119@outlook.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-06-26 08:50:39 -05:00
shenlong
24a4cba953 fix: enable photo manager custom filter by default (#19520)
enable photo manager custom filter by default

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-06-25 21:28:17 -05:00
shenlong
fda22c83b9 fix: sync stream album assets type order (#19540)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-06-25 20:42:56 -05:00
Matthew Momjian
2a8019726c feat(deployment): add shm_size for PG (#19472) 2025-06-25 20:38:37 +01:00
renovate[bot]
5f76cdddc7 chore(deps): update ghcr.io/astral-sh/uv:latest docker digest to 9653efd (#19530)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-25 20:37:53 +01:00
bo0tzz
48be10e48b fix: don't exclude .github folder from make install-all (#19534)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-25 20:37:40 +01:00
bo0tzz
6c11ef62e8 fix: don't use @master action version (#19526) 2025-06-25 17:55:17 +00:00
shenlong
65dce58aa4 refactor(mobile): do not skip iCloud albums during local sync (#19231)
do not skip cloud albums during local sync

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-06-25 16:46:31 +00:00
Brandon Wees
64cc7239fe feat(mobile): ios widget deeplink to asset in app (#19510)
* feat: ios widget deeplinks to asset in app

* fix: casing

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-06-25 11:37:25 -05:00
renovate[bot]
5f89c2d111 chore(deps): pin actions/cache action to 5a3ec84 (#19529)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-25 16:21:49 +00:00
Daimolean
4621ec5ea2 fix(web): load day group in asset viewer (#19523)
* fix(web): load day group in asset viewer

* fix: lint
2025-06-25 11:18:11 -05:00
Jason Rasmussen
881a96cdf9 feat: add album asset sync (#19503)
wip: fix album asset exif and some other refactorings

feat: add album assets sync

feat: album to assets relation sync

Co-authored-by: Zack Pollard <zackpollard@ymail.com>
2025-06-25 16:10:31 +00:00
Alex
b001ba44f5 feat: generic control bottom app bar (#19524)
* feat: sliver appbar

* feat: snapping segment

* Date label font size

* lint

* fix: scrollController reinitialize multiple times

* feat: tab navigation

* chore: refactor to private widget

* feat: new control bottom app bar

* bad merge

* feat: sliver control bottom app bar
2025-06-25 16:08:02 +00:00
Zack Pollard
afb444c92c fix: only pass in is_main flag to mobile build for zizmor (#19525) 2025-06-25 17:53:08 +02:00
Zack Pollard
027c4a8b34 ci: much faster mobile builds (#19490) 2025-06-25 10:40:53 -05:00
Jason Rasmussen
eca9b56847 feat(server): person delete (#19511)
feat(api): person delete
2025-06-25 11:12:36 -04:00
shenlong
5b0575b956 refactor: DCM - const border radius, constructor & switch expressions (#19515)
* enable border radius, switch exp, const constructor

* regenerate provider

* more formatting

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-06-25 13:06:24 +05:30
Alex
05064f87f0 feat: sliver appbar and snap scrubbing (#19446) 2025-06-24 20:02:46 -05:00
Alex
522cdbac99 feat: sliver timeline selection optimization (#19504) 2025-06-24 19:34:30 -05:00
renovate[bot]
9240bbc6ff chore(deps): update github-actions (#19045)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 22:54:05 +02:00
Brandon Wees
3751f8bc57 fix: android app link support (#19501)
* fix: android app link support

* add autoVerify tag

* adjust intent to have scheme separate
2025-06-24 12:52:15 -05:00
Jason Rasmussen
88b8afb8d6 fix: return 404 for invalid shared link pages (#19493) 2025-06-24 10:37:14 -05:00
Brandon Wees
2e13543d5d fix: ios widget webp support (#19469) 2025-06-24 10:33:38 -05:00
renovate[bot]
bcfc967d77 fix(deps): update machine-learning (#19219) 2025-06-24 17:55:37 +03:00
Brandon Wees
7d0e8f50f7 feat(mobile): deep links (#19232)
* add deep linking on ios app

* add deeplinking to android

* code review fixes

* lint

* cleanly handle malformed URIs when launching app

* refactor deep link builder/service, still have bug with navigation stack not containing TabControllerRoute

* fix: tab controller insertion conditions

* add my.immich.app app linking

* chore: remove one-liner if statement

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-06-24 09:20:24 -05:00
Jason Rasmussen
c759233d8c fix(server): include hidden assets in missing metadata extraction query (#19471) 2025-06-24 08:42:38 -04:00
Min Idzelis
bfe32c2bb9 chore: npm caching (#19410) 2025-06-24 12:16:09 +01:00
renovate[bot]
6c7b2e4b5c chore(deps): pin ghcr.io/immich-app/postgres docker tag to 5f6a838 (#19480)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 11:19:40 +01:00
renovate[bot]
7edbeb2ed6 chore(deps): update ghcr.io/immich-app/postgres:14-vectorchord0.3.0 docker digest to 3aef84a (#19481)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 11:19:00 +01:00
renovate[bot]
4e59a55c1d chore(deps): update dependency @types/node to ^22.15.32 (#19483)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 11:18:47 +01:00
renovate[bot]
c2d7337d12 chore(deps): update grafana/grafana docker tag to v12.0.2 (#19484)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 11:18:35 +01:00
Min Idzelis
c1b82bed9b chore: speedup devcontainer start (#19406) 2025-06-24 10:59:52 +01:00
Alex
9ca31abae9 feat: new timeline multi-selection (#19443)
* feat: new timeline multiselection

* select all from bucket

* wip

* group multi-select

* group multi-select

* pr feedback

* pr feedback

* lint
2025-06-24 02:05:25 -05:00
Jason Rasmussen
ebcf133bea fix(web): consistent merge people icons (#19473)
fix(web): consistent merge icons
2025-06-23 16:39:20 -04:00
Alex
1923f1a887 refactor(mobile): interfaces refactor (#19415)
* refactor(mobile): interfaces refactor

* generate files
2025-06-23 11:27:44 -05:00
Daimolean
ce14324c97 fix(web): oauth quota display (#19417)
* fix(web): oauth quota display

* fix(web): oauth quota display
2025-06-23 11:00:41 -04:00
Daimolean
6a309129b7 fix(web): timeline timezone (#19418) 2025-06-23 09:06:26 -05:00
Matthew Momjian
bcb1bf4692 fix(docs): portainer absolute paths (#19459)
absolute paths
2025-06-23 08:54:26 -05:00
Daimolean
7f89999abe fix(web): toggle favorite (#19453) 2025-06-23 08:36:30 -05:00
Daimolean
813186e618 fix(web): undefined release (#19455)
Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
2025-06-23 12:13:08 +00:00
fmis13
20d9204ada feat(mobile): enable Croatian (hr) translations (#19428) 2025-06-23 12:18:04 +01:00
Min Idzelis
3a9e79a452 chore: optimize playwright gha (#19435) 2025-06-23 12:11:52 +01:00
Mert
03966146fe fix(server): filter parameters by database and role (#19392) 2025-06-23 12:10:00 +01:00
Daniel Dietzler
ecc58a8971 chore: migrate version announcement modal (#19381) 2025-06-22 21:56:41 -05:00
Yaros
c705a7b280 fix(web): map broken after redirect from details (#19424)
* fix(web): map broken after redirect from details

* chore: use globalThis instead of window
2025-06-22 21:55:21 -05:00
Brandon Wees
ef278b4fb0 fix: storage template onboarding save (#19405)
* fix: storage template onboarding save

* no need for async/await
2025-06-22 21:54:29 -05:00
Daimolean
4cd633dc68 fix(web): download icon color (#19427) 2025-06-22 21:52:19 -05:00
Ben McCann
a18c6fa910 chore: fix undeclared dependencies (#19440)
* chore: fix undeclared dependencies

* Add express/multer

---------

Co-authored-by: Min Idzelis <min123@gmail.com>
2025-06-22 19:01:30 -04:00
Daimolean
90aa0dc14d fix(web): map cluster (#19433) 2025-06-22 15:06:45 +00:00
Alex
ce8c80dad0 refactor(mobile): cast_destination.interface.dart (#19346) 2025-06-21 21:56:49 -05:00
Alex
81eb98d4e5 refactor(mobile): album.interface.dart (#19354) 2025-06-21 23:32:06 +00:00
Alex
2b03802e9c refactor(mobile): asset_api.interface.dart (#19353) 2025-06-21 23:06:38 +00:00
Alex
484311e9bb refactor(mobile): asset-media.interface.dart (#19352)
* refactor(mobile): asset-media.interface.dart

* refactor(mobile): asset-media.interface.dart

* refactor: asset media repo

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-06-21 22:47:16 +00:00
Alex
366539bc4c refactor(mobile): asset.interface.dart (#19351)
* refactor(mobile): asset.interface.dart

* refactor(mobile): asset.interface.dart
2025-06-21 22:41:02 +00:00
Alex
69b1331026 refactor(mobile): auth_api.interface.dart (#19350) 2025-06-21 22:20:58 +00:00
Alex
af30d97668 refactor(mobile): auth.interface.dart (#19349) 2025-06-21 22:09:12 +00:00
Alex
9b047d30e4 refactor(mobile): backup_album.interface.dart (#19348) 2025-06-21 21:58:02 +00:00
Alex
6a5597b36b refactor(mobile): etag.interface.dart (#19344)
* refactor(mobile): etag.interface.dart

* merge main
2025-06-21 21:46:38 +00:00
Alex
c10b795e99 refactor(mobile): file_media.interface.dart (#19343)
* refactor(mobile): file_media.interface.dart

* merge main
2025-06-21 19:24:59 +00:00
Alex
b606d4fe73 refactor(mobile): local_file_manager.interface.dart (#19340) 2025-06-21 18:35:30 +00:00
Alex
4c2ad44303 refactor(mobile): folder_api.interface.dart (#19342) 2025-06-21 13:14:14 -05:00
Daniel Dietzler
698d3004b4 refactor: tag create/update modal (#19389)
refactor: tag modals
2025-06-21 12:28:21 +00:00
Daniel Dietzler
fe4d6edbdc refactor: album picker modal (#19383)
Co-authored-by: Jason Rasmussen <jason@rasm.me>
2025-06-21 08:18:54 -04:00
Daimolean
798debfde3 fix(server): duplicate column storage (#19385) 2025-06-20 15:52:25 -04:00
github-actions
6563fa608a chore: version v1.135.3 2025-06-20 19:48:18 +00:00
Jason Rasmussen
1a90fc8e58 feat: test for non-standard database name (#19386) 2025-06-20 19:31:16 +00:00
Alex
c707f9cef4 refactor(mobile): partner.interface.dart (#19338) 2025-06-20 18:37:59 +00:00
dotlambda
6fda863c08 fix(server): don't hardcode database name in migration (#19376) 2025-06-20 21:33:34 +03:00
Daniel Dietzler
373b654156 chore: migrate profile picture cropper modal (#19378) 2025-06-20 18:16:10 +00:00
Daniel Dietzler
a5d84ba552 chore: consistent modal footer spacing (#19377) 2025-06-20 18:05:39 +00:00
Daniel Dietzler
1dc8fa2979 chore: rename edit album form modal (#19375) 2025-06-20 13:51:14 -04:00
Alex
0426699f13 refactor(mobile): partner_api.interface.dart (#19337)
* refactor(mobile): partner_api.interface.dart

* merge main
2025-06-20 17:04:15 +00:00
Alex
8154ec29df refactor(mobile): person_api.interface.dart (#19336) 2025-06-20 11:45:31 -05:00
Alex
3024cd343b refactor(mobile): timeline.interface.dart (#19331)
refactor(mobile): timeline.repository.dart
2025-06-20 11:44:02 -05:00
Zack Pollard
0b44d4b6f2 fix: partner and album backfill acks (#19371)
fix: partner sync being entirely broken
2025-06-20 16:14:36 +00:00
github-actions
a04c6ed80d chore: version v1.135.2 2025-06-20 14:52:47 +00:00
Brandon Wees
1c50e19894 fix: use icons instead of toggles for admin user features view (#19369)
* fix: use icons instead of toggles for admin user features view

* fix: use red for X icon
2025-06-20 14:48:18 +00:00
Alex
e61d7f2616 refactor(mobile): album_media.interface.dart (#19355)
* refactor(mobile): album_media.interface.dart

* refactor: album_media repo

* make dcm happy

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-06-20 14:23:43 +00:00
Alex
a6b0869714 refactor(mobile): secure_storage.interface.dart (#19335)
* refactor(mobile): secure_storage.interface.dart

* fix: lint
2025-06-20 09:12:07 -05:00
Alex
9c25b8ba7d refactor(mobile): download.interface.dart (#19345) 2025-06-20 09:11:52 -05:00
Alex
3c72f489d8 refactor(mobile): sessions_api.interface.dart (#19334)
refactor(mobile): sessions_api.repository.dart
2025-06-20 09:11:33 -05:00
Alex
1f2c779b36 refactor(mobile): shared_handler.interface.dart (#19333)
refactor(mobile): shared_handler.repository.dart
2025-06-20 09:11:21 -05:00
Alex
5c74f634b7 refactor(mobile): network.interface.dart (#19341)
* refactor(mobile): network.interface.dart

* refactor(mobile): network.interface.dart
2025-06-20 09:11:03 -05:00
Alex
ecc99bfd16 refactor(mobile): biometric.interface.dart (#19347) 2025-06-20 09:10:53 -05:00
Alex
ff4d70e351 refactor(mobile): album_api.interface.dart (#19356) 2025-06-20 09:08:23 -05:00
Alex
42c2389eb5 refactor(mobile): activity_api.interface.dart (#19357) 2025-06-20 09:08:12 -05:00
Jason Rasmussen
33c9f88ba4 fix: time bucket grouping (#19329) 2025-06-20 09:46:30 -04:00
Mert
11c469907f fix(server): migration failing on pg15+ (#19363)
* reset params

* unused parameter
2025-06-20 08:36:07 -05:00
Mert
7c43e6c3c8 fix: bump vchord default to 0.4.3 (#19365)
bump default to 0.4.3
2025-06-20 08:35:32 -05:00
Zack Pollard
00aa385972 fix: people ordering by asset count (#19366) 2025-06-20 07:34:04 -05:00
Min Idzelis
a5ed453929 chore: unused deps (#19256)
remove joi
2025-06-20 00:30:23 -04:00
Jason Rasmussen
dd8969cb7d fix: container padding (#19316) 2025-06-19 21:33:12 -05:00
Alex
bce4f93c90 refactor(mobile): (1) user.interface.dart (#19322)
* refactor(mobile): user.interface.dart

* generate files

* refactor(mobile): (2) user_api.interface.dart (#19323)

* refactor(mobile): (2) user_api.interface.dart

* generate files

* refactor(mobile): (3) sync_stream.interface.dart (#19325)
2025-06-19 23:25:18 +00:00
Alex
a4c0dc5007 chore: post release tasks (#19311) 2025-06-19 15:35:25 -04:00
Matthew Momjian
d233a7d97a fix(server): remove excessive inactivity log (#19306) 2025-06-19 19:13:13 +00:00
Jason Rasmussen
5cdbb65d28 feat: better contrast for checkmark indicator (#19312)
feat: better constrast
2025-06-19 13:20:57 -05:00
github-actions
3434544864 chore: version v1.135.1 2025-06-19 17:37:39 +00:00
Brandon Wees
269bf4b344 fix: iOS 17.0 target version for widget (#19308) 2025-06-19 17:00:54 +00:00
Zack Pollard
f9435a538b revert: fix(web): wrap long names with textarea (#19305)
Revert "fix(web): wrap long names with textarea (#19301)"

This reverts commit 747a72120e.
2025-06-19 16:28:10 +00:00
Alex
10e2ec2841 chore: skip truncating table in this release (#19300) 2025-06-19 16:11:18 +00:00
Zack Pollard
fe91b44ab9 fix: people ordering incorrect (#19298) 2025-06-19 16:05:03 +00:00
Jin Xuan
747a72120e fix(web): wrap long names with textarea (#19301) 2025-06-19 15:57:54 +00:00
Jason Rasmussen
910661e75c chore: remove unused mocks (#19299) 2025-06-19 10:35:09 -05:00
Alex
c8a135a7ae fix: .find() iterator api combat (#19293)
* fix: .find() iterator api combar

* Update web/src/lib/managers/timeline-manager/month-group.svelte.ts

Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>

---------

Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
2025-06-19 14:59:14 +00:00
xCJPECKOVERx
08d1cf5bde fix(web): Stack assets in asset-viewer cut off on the left (#19253)
* - move overflow and scrollbar to stack-slideshow inner div

* - format
2025-06-19 09:20:25 -05:00
Alex
3e62497fd0 fix: local network permission (#19285) 2025-06-19 14:18:51 +00:00
Alex
a1bc862a32 chore: post release tasks (#19249) 2025-06-19 09:11:24 -05:00
Alex
75bf3aa1be chore: correct vchord version in docker-compose.yml (#19251) 2025-06-19 09:11:15 -05:00
Dag Stuan
38e68d16f9 fix(web): exit slideshow when exiting fullscreen. (#19247)
Exit slideshow when exiting fullscreen.

Browsers do not send a keyboard event when exiting fullscreen, so if
the user exits fullscreen with the escape key, the slideshow
remains open, requiring another escape key press to close it. Fix this
by listening for the fullscreenchange event and closing the slideshow
when exiting fullscreen.
2025-06-19 14:10:10 +00:00
Daniel Dietzler
caf11fbb96 fix: album asset viewer (#19252) 2025-06-19 09:09:23 -05:00
Mert
f99c6feac5 fix(server): unset prewarm dim parameter (#19271)
unset prewarm dim
2025-06-19 09:04:52 -05:00
Matthew Momjian
5122512f19 fix(docs): REINDEX vchord on upgrade (#19282)
* reindex

* lint

* collapse migrations

* remove title

* reformat
2025-06-19 09:04:18 -05:00
Mert
49ed212af8 fix(server): drop vector indices before updating extension (#19283)
drop indices before updating
2025-06-19 09:03:40 -05:00
Paul Larsen
e29103b69f fix album list CSS margins (#19262) 2025-06-19 14:03:14 +00:00
Min Idzelis
14b771d7c7 fix: devcontainer in codespaces (#19259)
* fix: devcontainer perms

* Fix host based auth

* use path tricks to get to volume mount, but remain compat with current meaning of variables

* eureka, i think

* bit of cleanup
2025-06-19 08:29:22 -05:00
Daniel Dietzler
07aa51638c fix: panning interrupted while moving around the map (#19276) 2025-06-19 11:28:53 +00:00
SGT
0a9a520ed2 feat(server): sql-tools support for class level composite fk (#19242)
* feat: support for class level composite fk

* chore: clean up

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2025-06-18 14:30:39 -04:00
Daniel Dietzler
de81006367 fix: album share modal navigation (#19245) 2025-06-18 16:10:35 +00:00
Jason Rasmussen
e0144b4ece feat: backfill album users (#19234) 2025-06-18 10:48:11 -04:00
github-actions
65e8d75e82 chore: version v1.135.0 2025-06-18 13:45:07 +00:00
Jason Rasmussen
023bcffdb8 chore: no test coverage in ci (#19235) 2025-06-17 21:16:52 -04:00
Alex
06f1d0dc4d fix(mobile): correct share option for local asset (#19233) 2025-06-17 20:56:42 +00:00
Min Idzelis
c6641d4859 fix: devcontainer paths/logs (#19236) 2025-06-17 15:52:57 -05:00
Jason Rasmussen
91cbd56c1c revert: service worker changes (#19227) 2025-06-17 17:07:54 +00:00
Jason Rasmussen
35280b94cc refactor: sync service (#19225) 2025-06-17 16:06:40 +00:00
Zack Pollard
4c69511225 revert: "feat(web): wasm justified layout" (#19226) 2025-06-17 16:01:40 +00:00
Weblate (bot)
0684a3ada4 chore(web): update translations (#19127)
Co-authored-by: Antonio Vazquez <antoniovavazquez@gmail.com>
Co-authored-by: Bezruchenko Simon <worcposj44@gmail.com>
Co-authored-by: Celeste Cossard <hugococa2004@gmail.com>
Co-authored-by: Dag Stuan <D.Stuan@gmail.com>
Co-authored-by: DevServs <bonov@mail.ru>
Co-authored-by: Felipe Garcia <garcia.o.felipe@gmail.com>
Co-authored-by: Felipe Simões <felipebouabci@gmail.com>
Co-authored-by: Fjuro <git@alius.cz>
Co-authored-by: Happy <happygamernintendoswitch@gmail.com>
Co-authored-by: Hurricane-32 <rodrigorimo@hotmail.com>
Co-authored-by: Indrek Haav <indrek.haav@hotmail.com>
Co-authored-by: Ivan Dimitrov <idimitrov08@gmail.com>
Co-authored-by: Jozef Gaal <preklady@mayday.sk>
Co-authored-by: Leo Bottaro <github@leobottaro.com>
Co-authored-by: MSDNicrosoft <wang3311835119@hotmail.com>
Co-authored-by: Malhelo <weblate@malhelo.de>
Co-authored-by: Mateusz779 <kmateusz809@gmail.com>
Co-authored-by: Matjaž T <matjaz@moj-svet.si>
Co-authored-by: Matteo De Carli <matteo.de.carli01@gmail.com>
Co-authored-by: MattiaPell <mattiapellegrini16@gmail.com>
Co-authored-by: Mārtiņš Bruņenieks <martinsb@gmail.com>
Co-authored-by: Nick Huang <nick80322@gmail.com>
Co-authored-by: Niko Savola <nikodagreat37@gmail.com>
Co-authored-by: Philipp Burndorfer <phi.bur@gmx.at>
Co-authored-by: Ponas <le.slab124@aleeas.com>
Co-authored-by: Sylvain Pichon <service@spichon.fr>
Co-authored-by: Taiki M <vexingly-many-mace@duck.com>
Co-authored-by: Theodoor van Donge <theodoorvd@gmail.com>
Co-authored-by: Tijs-B <tijs.bergmans@telenet.be>
Co-authored-by: User 123456789 <user123456789@users.noreply.hosted.weblate.org>
Co-authored-by: Vegard Fladby <vegard@fladby.org>
Co-authored-by: albanobattistella <albano_battistella@hotmail.com>
Co-authored-by: drshounak <contact@drshounak.com>
Co-authored-by: manosrh <manosrh@gmail.com>
Co-authored-by: naroou <hdbdjndnr@gmail.com>
Co-authored-by: oopzzozzo <oopzzozzo@gmail.com>
Co-authored-by: pyccl <changcongliang@163.com>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: Àlex Bravo <alexbravobosch@gmail.com>
Co-authored-by: Вячеслав Лукьяненко <madeinchuguev@gmail.com>
2025-06-17 16:07:20 +01:00
Brandon Wees
a0f44f147b feat(mobile): ios widgets (#19148)
* feat: working widgets

* chore/feat: cleaned up API, added album picker to random widget

* album filtering for requests

* check album and throw if not found

* fix app IDs and project configuration

* switch to repository/service model for updating widgets

* fix: remove home widget import

* revert info.plist formatting changes

* ran swift-format on widget code

* more formatting changes (this time run from xcode)

* show memory on widget picker snapshot

* fix: dart changes from code review

* fix: swift code review changes (not including task groups)

* fix: use task groups to run image retrievals concurrently, get rid of do catch in favor of if let

* chore: cleanup widget service in dart app

* chore: format swift

* fix: remove comma

why does xcode not freak out over this >:(

* switch to preview size for thumbnail

* chore: cropped large image

* fix: properly resize widgets so we dont OOM

* fix: set app group on logout

happens on first install

* fix: stupid app ids

* fix: revert back to thumbnail

we are hitting OOM exceptions due to resizing, once we have on-the-fly resizing on server this can be upgraded

* fix: more memory efficient resizing method, remove extraneous resize commands from API call

* fix: random widget use 12 entries instead of 24 to save memory

* fix: modify duration of entries to 20 minutes and only generate 10 at a time to avoid OOM

* feat: toggle to show album name on random widget

* Podfile lock

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-06-17 14:43:09 +00:00
xCJPECKOVERx
15c488ccd9 fix(web): MemoryStore does not initialize on direct navigation (#18947)
* - no longer return early when navigating directly to memory-viewer

* Update memory-viewer.svelte

- remove early return from afterNavigate

* lint
2025-06-17 09:21:30 -05:00
Mert
bc062da11b feat(web): wasm justified layout (#19150)
* wasm justified layout

* fix tests

* redundant layout generation

* raw position
2025-06-17 09:20:14 -05:00
xCJPECKOVERx
8038ae1e7a fix(web): Asset viewer stack thumbnails overflow on top of asset (#19088)
* - create constants for thet asset-viewer stack thumbnail sizes
- use 2x selected thumbnail size to set the max-height of the stack-slideshow container.

* - increase the stack-slideshow max-height as it's scrolled

* Revert "- increase the stack-slideshow max-height as it's scrolled"

This reverts commit da4614547a.

* change asset stack veritcal scroll to horizontal scroll
2025-06-17 09:19:30 -05:00
Gleb Khmyznikov
f28c0d912c chore: update truenas repo link (#19195)
Update truenas repo link
2025-06-17 09:10:25 -05:00
Dag Stuan
bd70824961 fix(web): more refactoring and tweaking of the memory viewer. (#19214)
* Fix fade in for video-native-viewer.

The previous implementation never actually faded in the video element.
Fix this by ensuring the video element is only added to the DOM after
mounting, so Svelte can handle the fade-in transition correctly.

* Refactor asset viewing in memory page.

Split photo and video viewing into separate components to ensure they
work similarly to the assets viewer. The previous implementation faded
out the assets, while the assets-viewer only fades assets in. For
images, add a spinner while waiting for the image to load, before adding
the image to the DOM. For videos, add the video to the DOM after
mounting the component. In both cases, the assets fade in smoothly, like
the regular assets viewer.

* fix: styling

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-06-17 14:09:34 +00:00
Zack Pollard
749f63e4a0 fix: partner asset and exif sync backfill (#19224)
* fix: partner asset sync backfill

* fix: add partner asset exif backfill

* ci: output content of files that have changed
2025-06-17 09:56:54 -04:00
shenlong
db68d1af9b feat(server): add duration to SyncAssetV1 (#19196)
add duration to SyncAssetV1

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-06-17 08:09:50 -04:00
Zack Pollard
864fe3d0d6 fix: disable map fly animation when switching between assets (#19223) 2025-06-17 12:45:48 +01:00
renovate[bot]
00536bf074 chore(deps): update docker.io/valkey/valkey:8-bookworm docker digest to fec42f3 (#19218)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-17 11:57:42 +01:00
renovate[bot]
0d3efe229d chore(deps): update dependency @types/node to ^22.15.31 (#19220)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-17 11:57:29 +02:00
shenlong
3b0a803089 fix: translation util rename (#19213)
fix: translation util refactor

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-06-16 11:43:53 -05:00
shenlong
bcda2c6e22 feat(mobile): sqlite timeline (#19197)
* wip: timeline

* more segment extensions

* added scrubber

* refactor: timeline state

* more refactors

* fix scrubber segments

* added remote thumb & thumbhash provider

* feat: merged view

* scrub / merged asset fixes

* rename stuff & add tile indicators

* fix local album timeline query

* ignore hidden assets during sync

* ignore recovered assets during sync

* old scrubber

* add video indicator

* handle groupBy

* handle partner inTimeline

* show duration

* reduce widget nesting in thumb tile

* merge main

* chore: extend cacheExtent

* ignore touch events on scrub label when not visible

* scrub label ignore events and hide immediately

* auto reload on sync

* refactor image providers

* throttle db updates

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-06-16 20:37:45 +05:30
Mert
7347f64958 feat(server): lru cache for query embeddings (#19181) 2025-06-16 11:03:49 -04:00
Zack Pollard
176d53c1b3 chore: bump browserlists version and load tscompat browserlist from file (#19212)
* chore: bump browserlists version to include latest edge browsers

* chore: load tscompat browser list from the .browserslistrc file
2025-06-16 10:03:46 -05:00
Mert
5fc448bc97 chore(web): passive events (#19179) 2025-06-16 11:03:23 -04:00
Thien Dang
3d0c851636 fix(mobile): add translate extension (#18942)
* re-write localization service and add translation extension

* Revert "re-write localization service and add translation extension"

This reverts commit fdd7386020.

* fix can't use context for easy_localization

* fix lint

* update new translate context

* handle context null

* revert main file

* Revert "revert main file"

This reverts commit 16faca46d0.

* remove fix nested MaterialApp

* change use t extenstion and remove translation utils

* update function call similar for consistently

---------

Co-authored-by: dvbthien <dvbthien@gmail.com>
2025-06-16 10:01:16 -05:00
jump
16fcb657b7 docs: update the picture showing administration link (#19203) 2025-06-16 12:59:47 +01:00
Brandon Wees
32b57bcbfc chore: remove warning on storage template onboarding (#19200)
chore: remove warning on storage template
2025-06-15 16:43:34 -05:00
shenlong
7f56443b24 fix(mobile): asset type mismatch from android sync (#19201)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-06-15 20:54:30 +00:00
Dag Stuan
189442e9c4 fix(web): small issues with the memory viewer. (#19184)
* Match fade transition timing between photo-viewer and memory-viewer.

* Fix blank page after refreshing memory page.

If the user refreshed in the browser while on the memory page, the page
would show a blank page. This was caused by skipping initialization in
afterNavigate. Fix by always initializing the memory page in
afterNavigate.
2025-06-15 10:04:33 -05:00
Daniel Dietzler
523fe5bef7 refactor: album options modal (#19177) 2025-06-14 18:10:33 -04:00
SGT
77a362f0c0 chore(server): replace usage of deprecated orderBy and remove unnecessary instruction (#19072)
* replace usage of deprecated orderBy instruction
remove unnecesarry extra order instruction
update e2e test

* rename symbols
2025-06-13 15:18:44 -04:00
Alex
5f5308631e chore: Revert "feat(mobile): remote album sync" (#19159)
Revert "feat(mobile): remote album sync (#18876)"

This reverts commit 242817c49a.
2025-06-13 16:15:39 +00:00
Daimolean
004c2f2496 fix(web): OAuth quota size (#18526)
fix(server): oauth quota size
2025-06-13 10:57:29 -04:00
Zack Pollard
e2dfbd66c3 ci: browser compatibility linting (#19132) 2025-06-13 10:54:59 -04:00
Michael Anderson
de756d9497 fix: skip locked photos during duplicate scan. (#19105)
Skip locked photos during duplicate scan.
2025-06-13 10:50:56 -04:00
Min Idzelis
103b83d2d6 feat: devcontainers (#18007)
* feat: devcontainers

* Update link

* Update docs

* Extend existing dockerfiles/composes

* Add jre for make open-api

* Add jre for make open-api

* shellcheck

* git doesn't like bind mount within git repo

* group tasks

* Missing sudo

* Review comments

* tweak for codespaces

* typo

* Lots of docs

* close <br>

* Specify ENV vars for database

* doc errors

* fix broken doc link

* Simplify devcontainers scripts/startup

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2025-06-13 14:49:21 +00:00
Andreas Tollkötter
f54cfa7a5a feat: improve mobile screen reader accessibility (#17876)
* WIP: adding screen reader support to mobile

* implemented getAltText

* implemented alt text solution that stores the alt text in the DB, which isn't really great

* moved alt text computation to immich_thumbnail.dart

* added unit tests

* revert unintended changes

* Added text to remaining buttons in Photo page

* fixed import

* Fixed issue of easy_localization not parsing select blocks

* Transferred the new screen reader help to web frontend

* remove unused property

* npm run format:fix

* code review

* revert unwanted change

* dart fmt

* revert web changes

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-06-13 10:39:59 -04:00
Min Idzelis
ed5b260eeb feat: service worker cache static app resources, and all entry points (#18043)
* feat: service worker cache static app resources, and all entry points

* review comments

* review

* lint

* minor tweaks

* review comments

* optimize disabled cache

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-06-12 19:33:29 -04:00
Jason Rasmussen
8923d5b0a3 refactor: css variables (#19146) 2025-06-12 23:06:38 +00:00
Jason Rasmussen
2f3d4e15d2 refactor: duplicate button controls (#19143) 2025-06-12 17:48:53 -04:00
Jason Rasmussen
c9bcae813b feat: duplicate delete groups api (#19142) 2025-06-12 17:48:43 -04:00
Daniel Dietzler
bddb43e1d4 fix: cli upload deletes files that failed uploading (#19140) 2025-06-12 17:32:35 -04:00
Alex
176656b5f4 feat: pass filename in body on asset upload (#19138)
* chore: get upload filename from request body

* use info from request body
2025-06-12 17:31:00 -04:00
Jason Rasmussen
5cd186d3d4 refactor: duplicate queries (#19136) 2025-06-12 14:23:02 -04:00
Alex
144cc8ab6d chore: custom impl for set.difference api (#19135) 2025-06-12 11:41:19 -05:00
Robin Brisa
0322a8b1d9 fix(web): properly update activityManager when browsing assets (#18909) 2025-06-12 12:13:35 +01:00
Jason Rasmussen
94e9adf625 chore: callback quotes (#19126) 2025-06-11 23:15:11 -04:00
Weblate (bot)
24edf23bc8 chore(web): update translations (#18530)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/bg/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/da/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/el/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/et/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fa/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fil/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/gl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/he/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/id/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ja/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/kk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/lt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/lv/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ml/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/mn/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ms/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ro/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Cyrl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/te/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/th/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/tr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ur/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/
Translation: Immich/immich

Co-authored-by: Alejandro Moya <alejandro_moya_moya@hotmail.com>
Co-authored-by: Alex <alex@guldager.one>
Co-authored-by: Andreas Johansen <andreas@josern.com>
Co-authored-by: Anton <antonholmstedt@gmail.com>
Co-authored-by: Antonio Vazquez <antoniovavazquez@gmail.com>
Co-authored-by: Arnyminer Z <arnyminer.z@gmail.com>
Co-authored-by: Bezruchenko Simon <worcposj44@gmail.com>
Co-authored-by: Bora Atıcı <boratici.acc@gmail.com>
Co-authored-by: Bradley Lansink <aceraspirev3brad@gmail.com>
Co-authored-by: C D <chinnidiwakar5@gmail.com>
Co-authored-by: Chriss Martin <thechrissmartin@gmail.com>
Co-authored-by: Claudio De Marzo <claudiodemarzo.62@gmail.com>
Co-authored-by: Clemens <clemensstouten@gmail.com>
Co-authored-by: Daniel Correa Lobato <daniel@lobato.org>
Co-authored-by: David S <weblate.arjy8@flexi.name>
Co-authored-by: Davide Vegliante <davidevegliante@gmail.com>
Co-authored-by: DevServs <bonov@mail.ru>
Co-authored-by: FVOCI <karl@hwan.dev>
Co-authored-by: Felipe Garcia <garcia.o.felipe@gmail.com>
Co-authored-by: Finn Drünert <finndruenert@gmail.com>
Co-authored-by: Fjuro <git@alius.cz>
Co-authored-by: Florian Ostertag <florian.kuepper@gmail.com>
Co-authored-by: Gerardo Doro <gerrydoro97@gmail.com>
Co-authored-by: Gil Shahar <gilshahardex99@gmail.com>
Co-authored-by: Haqiqi Jamaludin <qiqidh@gmail.com>
Co-authored-by: Hurricane-32 <rodrigorimo@hotmail.com>
Co-authored-by: Indrek Haav <indrek.haav@hotmail.com>
Co-authored-by: Ivan Dimitrov <idimitrov08@gmail.com>
Co-authored-by: Jak97 <mneamtu74@gmail.com>
Co-authored-by: Jesús Jiménez <jesjimenez@gmail.com>
Co-authored-by: John Molkavitch <jblum66@gmail.com>
Co-authored-by: Jordy H <jordy@hoebergen.net>
Co-authored-by: Jozef Gaal <preklady@mayday.sk>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: K.Yoshikawa <marimoky1110@gmail.com>
Co-authored-by: Kristoffer Braa <kristoffer@lolandbraa.no>
Co-authored-by: Leo Bottaro <github@leobottaro.com>
Co-authored-by: Leonardo <coachleonardo@gmail.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Majid <abtin.php@gmail.com>
Co-authored-by: Matjaž T <matjaz@moj-svet.si>
Co-authored-by: Matteo Crocetti <matteocrocetti@proton.me>
Co-authored-by: Matteo Morari <matteo.morari04@gmail.com>
Co-authored-by: Melih Ozkan <malihozkan156@gmail.com>
Co-authored-by: Micash <micash_545@protonmail.com>
Co-authored-by: Michael Iseard <michael@iseard.media>
Co-authored-by: Miki Mrvos <medolino2009@gmail.com>
Co-authored-by: Mário Victor Ribeiro Silva <mariovictorrs@gmail.com>
Co-authored-by: Mārtiņš Bruņenieks <martinsb@gmail.com>
Co-authored-by: Nick Huang <nick80322@gmail.com>
Co-authored-by: Niko Savola <nikodagreat37@gmail.com>
Co-authored-by: Nikolaj J-K <kagenogmig@gmail.com>
Co-authored-by: OLD&SPAM-ACCOUNT <nielsvdk666@gmail.com>
Co-authored-by: Olaf Nielsen <solluh@mail.de>
Co-authored-by: OmegaDL2 <atomo.co02@gmail.com>
Co-authored-by: OskarSidor <oskar.sidor@gmail.com>
Co-authored-by: PANICBUTTON <kaileepark04@gmail.com>
Co-authored-by: Philipp Burndorfer <phi.bur@gmx.at>
Co-authored-by: Pikim pikim <Pikim@tlen.pl>
Co-authored-by: Ponas <le.slab124@aleeas.com>
Co-authored-by: PontusÖsterlindh <pontus@osterlindh.com>
Co-authored-by: Punisher01 <ciprian420@gmail.com>
Co-authored-by: Qianxing Li <l7899616@gmail.com>
Co-authored-by: Ricardo Berlim Fonseca <eblues@disroot.org>
Co-authored-by: Runskrift <anders@rimfrost.nu>
Co-authored-by: RustamUzb <uzbekr@gmail.com>
Co-authored-by: Santiago <santiwever@hotmail.com>
Co-authored-by: Sergi Font <sfont@tuta.io>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Stefan <nothingkix@gmail.com>
Co-authored-by: Stryp0 <stryp94@hotmail.com>
Co-authored-by: Sylvain Pichon <service@spichon.fr>
Co-authored-by: Taiki M <vexingly-many-mace@duck.com>
Co-authored-by: Tanishq <weblate.impure434@passinbox.com>
Co-authored-by: Tatsuhiko Kono <kono@takenoko.io>
Co-authored-by: Theofilos Nikolaou <th.nikolaou@gmail.com>
Co-authored-by: Tijs-B <tijs.bergmans@telenet.be>
Co-authored-by: Tomek <tjomek@gmail.com>
Co-authored-by: Tomi Pöyskö <tomi.poysko@gmail.com>
Co-authored-by: Torin Wu <xuan329269@gmail.com>
Co-authored-by: Tre Sim <tsm.tre2018@gmail.com>
Co-authored-by: User 123456789 <user123456789@users.noreply.hosted.weblate.org>
Co-authored-by: Veerasak Kritsanapraphan <veerasak.kritsanapraphan@gmail.com>
Co-authored-by: Vegard Fladby <vegard@fladby.org>
Co-authored-by: Xo <xocodokie@users.noreply.hosted.weblate.org>
Co-authored-by: Yago Raña Gayoso <yago.rana.gayoso@gmail.com>
Co-authored-by: Yi Kuo <kuokuoyiyi@gmail.com>
Co-authored-by: Zvonimir <zzrakic@protonmail.com>
Co-authored-by: chamdim <chamdim@protonmail.com>
Co-authored-by: chapvic <victor@chapaev.org>
Co-authored-by: dicaeffe <dicaeffe@users.noreply.hosted.weblate.org>
Co-authored-by: dvbthien <dvbthien@users.noreply.hosted.weblate.org>
Co-authored-by: eav5jhl0 <eav5jhl0@users.noreply.hosted.weblate.org>
Co-authored-by: fmis13 <fmis13@disroot.org>
Co-authored-by: iamnotafatso <ee.chuajr@live.com>
Co-authored-by: icerocker <icerocker@users.noreply.hosted.weblate.org>
Co-authored-by: millallo <millallo@tiscali.it>
Co-authored-by: pyccl <changcongliang@163.com>
Co-authored-by: sevtdy <sevtdy@gmail.com>
Co-authored-by: shimmyx <shimmygodx@gmail.com>
Co-authored-by: shiuh67 <shiuh.cheng@gmail.com>
Co-authored-by: st7105 <st7105@gmail.com>
Co-authored-by: theGitMichel <meinkonto+github@mailbox.org>
Co-authored-by: thehijacker <thehijacker@gmail.com>
Co-authored-by: timmy61109 <qazzxcasdqwewsxedc@gmail.com>
Co-authored-by: tsloms <t.slomski@outlook.de>
Co-authored-by: ume <bungoume@gmail.com>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: yousaf465 <yousaf465@gmail.com>
Co-authored-by: Вячеслав Лукьяненко <madeinchuguev@gmail.com>
Co-authored-by: Оргил Пүрэвдорж <orgyldinio@proton.me>
2025-06-11 22:35:33 -04:00
Yishai Elyada
d784c7737a chore(docs): add rename note to synology.md (#17425)
* Update synology.md

Make sure to change example.env to .env, this is not trivial for non-Docker experts.

* Update synology.md

Wrapped file names in code tags

* chore: linting

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2025-06-11 22:15:45 -04:00
Elliot
fdc7a154c0 fix(server): add basic auth support (#16740)
* "add basic auth support"

* "fix format"

* fix: lint

---------

Co-authored-by: Elliot <elliot@elliotbrandwein.com>
Co-authored-by: Jason Rasmussen <jason@rasm.me>
2025-06-12 02:14:03 +00:00
nosajthenitram
e5219f1f31 feat(web): Added admin user config to user settings (#15380)
* feat(web): Added admin user config to user settings

* feat (web) - cleaned up the files and added tests

* feat (web) - added missing files

* feat (web) - updated per review comments

* feat (web) - e2e admin command test failures
2025-06-12 02:11:13 +00:00
Daniel Dietzler
22eef5f3c5 chore: more flexible modal manager types (#19123)
* fix: required argument in onClose modal function

* chore: more flexible modal manager types
2025-06-11 22:32:49 +00:00
Daniel Dietzler
5179c5badf fix: required argument in onClose modal function (#19122) 2025-06-11 18:25:36 -04:00
Daniel Dietzler
4c5cd14270 refactor: map modal (#19120) 2025-06-11 15:08:36 -04:00
Jason Rasmussen
38ad15af4c refactor: user avatar (#19121) 2025-06-11 15:08:11 -04:00
Daniel Dietzler
7a001d27a5 refactor: email template preview modal (#19119) 2025-06-11 18:18:46 +00:00
Zack Pollard
08e2b22db8 fix: album comment count including trashed assets (#19117) 2025-06-11 11:50:26 -04:00
Zack Pollard
5dd3a6e13f fix: don't show comments or likes for trashed assets (#19113) 2025-06-11 14:36:00 +00:00
Zack Pollard
bedcf50196 fix: count album comments and likes correctly (#19114) 2025-06-11 13:49:13 +00:00
JobiJoba
c03e72c1da feat(mobile): Add Scrollbar for vertical scrolling on the actions button multi-select (#18940)
* feat(mobile): Add Scrollbar for vertical scrolling on the actions button of a selected asset.

* fixed error scroll position and add more space for the scrollbar

* revert scrollbar change and display always 5.5 icons button

* minWidth set to 5.5 and used

* fix: logic and fine tuning

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-06-10 10:22:42 -05:00
Alex
b50d9fa448 chore: catch migration error (#19096) 2025-06-10 15:21:36 +00:00
Min Idzelis
4b4ee5abf3 refactor: timeline manager renames (#19007)
* refactor: timeline manager renames

* refactor(web): improve timeline manager naming consistency

- Rename AddContext → GroupInsertionCache for clearer purpose
- Rename TimelineDay → DayGroup for better clarity
- Rename TimelineMonth → MonthGroup for better clarity
- Replace all "bucket" references with "monthGroup" terminology
- Update all component props, method names, and variable references
- Maintain consistent naming patterns across TypeScript and Svelte files

* refactor(web): rename buckets to months in timeline manager

- Rename TimelineManager.buckets property to months
- Update all store.buckets references to store.months
- Use 'month' shorthand for monthGroup arguments (not method names)
- Update component templates and test files for consistency
- Maintain API-related 'bucket' terminology (bucketHeight, getTimeBucket)

* refactor(web): rename assetStore to timelineManager and update types

- Rename assetStore variables to timelineManager in all .svelte files
- Update parameter names in actions.ts and asset-utils.ts functions
- Rename AssetStoreLayoutOptions to TimelineManagerLayoutOptions
- Rename AssetStoreOptions to TimelineManagerOptions
- Move assets-store.spec.ts to timeline-manager.spec.ts

* refactor(web): rename intersectingAssets to viewerAssets and fix property references

- Rename intersectingAssets to viewerAssets in DayGroup and MonthGroup classes
- Update arrow function parameters to use viewerAsset/viewAsset shorthand
- Rename topIntersectingBucket to topIntersectingMonthGroup
- Fix dateGroups references to dayGroups in asset-utils.ts and album page
- Update template loops and variable names in Svelte components

* refactor(web): rename #initializeTimeBuckets to #initializeMonthGroups and bucketDateFormatted to monthGroupTitle

* refactor(web): rename monthGroupHeight to height

* refactor(web): rename bucketCount to assetsCount, bucketsIterator to monthGroupIterator, and related properties

* refactor(web): rename count to assetCount in TimelineManager

* refactor(web): rename LiteBucket to ScrubberMonth and update scrubber variables

- Rename LiteBucket type to ScrubberMonth
- Rename bucketDateFormattted to title in ScrubberMonth type
- Rename bucketPercentY to monthGroupPercentY in scrubber component
- Rename scrubBucket to scrubberMonth and scrubBucketPercent to scrubberMonthPercent

* fix remaining refs to bucket

* reset submodule to correct commit

* reset submodule to correct commit

* refactor(web): extract TimelineManager internals into separate modules

- Move search-related functions to internal/search-support.svelte.ts
- Extract websocket event handling into WebsocketSupport class
- Move utility functions (updateObject, isMismatched) to internal/utils.svelte.ts
- Update imports in tests to use new module structure

* refactor(web): extract intersection logic from TimelineManager

- Create intersection-support.svelte.ts with updateIntersection and calculateIntersecting functions
- Remove private intersection methods from TimelineManager
- Export findMonthGroupForAsset from search-support for reuse
- Update TimelineManager to use the extracted intersection functions

* refactor(web): rename a few methods in intersecting

* refactor(web): rename a few methods in intersecting

* refactor(web): extract layout logic from TimelineManager

- Create layout-support.svelte.ts with updateGeometry and layoutMonthGroup functions
- Remove private layout methods from TimelineManager
- Update TimelineManager to use the extracted layout functions
- Remove unused UpdateGeometryOptions import

* refactor(web): extract asset operations from TimelineManager

- Create operations-support.svelte.ts with addAssetsToMonthGroups and runAssetOperation functions
- Remove private asset operation methods from TimelineManager
- Update TimelineManager to use extracted operation functions with proper AssetOrder handling
- Rename getMonthGroupIndexByAssetId to getMonthGroupByAssetId for consistency
- Move utility functions from utils.svelte.ts to internal/utils.svelte.ts
- Fix method name references in asset-grid.svelte and tests

* refactor(web): extract loading logic from TimelineManager

- Create load-support.svelte.ts with loadFromTimeBuckets function
- Extract time bucket loading, album asset handling, and error logging
- Simplify TimelineManager's loadMonthGroup method to use extracted function

* refresh timeline after archive keyboard shortcut

* remove debugger

* rename

* Review comments - remove shadowed var

* reduce indents - early return

* review comment

* refactor: simplify asset filtering in addAssets method

Replace for loop with filter operation for better readability

* fix: bad merge

* refactor(web): simplify timeline layout algorithm

- Replace rowSpaceRemaining array with direct cumulative width tracking
- Invert logic from tracking remaining space to tracking used space
- Fix spelling: cummulative to cumulative
- Rename lastRowHeight to currentRowHeight for clarity
- Remove confusing lastRow variable and simplify final height calculation
- Add explanatory comments for clarity
- Rename loop variable assetGroup to dayGroup for consistency

* simplify assetsIterator usage

* merge/lint

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-06-10 09:30:13 -05:00
Hugo
6499057b4c docs: update instructions for external libraries (#19080)
The first step was missing—it's probably obvious for those already
familiar with Immich.

After I added the external library, no photos showed up anywhere and
all interfaces indicated that I had no photos

Eventually I found this "Scan" button, and after clicking it photos
started to appear. This is a necessary step before photos from the
library actually show up anywhere, so point it out explicitly.
2025-06-10 09:28:49 -05:00
Jonathan Gilbert
e88bd74fd2 feat(server): add memories statistics resource (#19035) 2025-06-10 09:47:46 -04:00
1373 changed files with 75907 additions and 26324 deletions

View File

@@ -1,2 +0,0 @@
.env
library

View File

@@ -1,16 +0,0 @@
ARG BASEIMAGE=mcr.microsoft.com/devcontainers/typescript-node:22@sha256:7c2e711a4f7b02f32d2da16192d5e05aa7c95279be4ce889cff5df316f251c1d
FROM ${BASEIMAGE}
# Flutter SDK
# https://flutter.dev/docs/development/tools/sdk/releases?tab=linux
ENV FLUTTER_CHANNEL="stable"
ENV FLUTTER_VERSION="3.29.3"
ENV FLUTTER_HOME=/flutter
ENV PATH=${PATH}:${FLUTTER_HOME}/bin
# Flutter SDK
RUN mkdir -p ${FLUTTER_HOME} \
&& curl -C - --output flutter.tar.xz https://storage.googleapis.com/flutter_infra_release/releases/${FLUTTER_CHANNEL}/linux/flutter_linux_${FLUTTER_VERSION}-${FLUTTER_CHANNEL}.tar.xz \
&& tar -xf flutter.tar.xz --strip-components=1 -C ${FLUTTER_HOME} \
&& rm flutter.tar.xz \
&& chown -R 1000:1000 ${FLUTTER_HOME}

View File

@@ -1,26 +1,67 @@
{
"name": "Immich",
"service": "immich-devcontainer",
"name": "Immich - Backend, Frontend and ML",
"service": "immich-server",
"runServices": [
"immich-server",
"redis",
"database",
"immich-machine-learning"
],
"dockerComposeFile": [
"docker-compose.yml",
"../docker/docker-compose.dev.yml"
"../docker/docker-compose.dev.yml",
"./server/container-compose-overrides.yml"
],
"customizations": {
"vscode": {
"extensions": [
"Dart-Code.dart-code",
"Dart-Code.flutter",
"dbaeumer.vscode-eslint",
"dcmdev.dcm-vscode-extension",
"esbenp.prettier-vscode",
"svelte.svelte-vscode"
"svelte.svelte-vscode",
"ms-vscode-remote.remote-containers",
"foxundermoon.shell-format",
"timonwong.shellcheck",
"rvest.vs-code-prettier-eslint",
"bluebrown.yamlfmt",
"vkrishna04.cspell-sync",
"vitest.explorer",
"ms-playwright.playwright",
"ms-azuretools.vscode-docker"
]
}
},
"forwardPorts": [],
"initializeCommand": "bash .devcontainer/scripts/initializeCommand.sh",
"onCreateCommand": "bash .devcontainer/scripts/onCreateCommand.sh",
"overrideCommand": true,
"workspaceFolder": "/immich",
"remoteUser": "node"
"forwardPorts": [3000, 9231, 9230, 2283],
"portsAttributes": {
"3000": {
"label": "Immich - Frontend HTTP",
"description": "The frontend of the Immich project",
"onAutoForward": "openBrowserOnce"
},
"2283": {
"label": "Immich - API Server - HTTP",
"description": "The API server of the Immich project"
},
"9231": {
"label": "Immich - API Server - DEBUG",
"description": "The API server of the Immich project"
},
"9230": {
"label": "Immich - Workers - DEBUG",
"description": "The workers of the Immich project"
}
},
"overrideCommand": true,
"workspaceFolder": "/workspaces/immich",
"remoteUser": "node",
"userEnvProbe": "loginInteractiveShell",
"remoteEnv": {
// The location where your uploaded files are stored
"UPLOAD_LOCATION": "${localEnv:UPLOAD_LOCATION:./library}",
// Connection secret for postgres. You should change it to a random password
// Please use only the characters `A-Za-z0-9`, without special characters or spaces
"DB_PASSWORD": "${localEnv:DB_PASSWORD:postgres}",
// The database username
"DB_USERNAME": "${localEnv:DB_USERNAME:postgres}",
// The database name
"DB_DATABASE_NAME": "${localEnv:DB_DATABASE_NAME:immich}"
}
}

View File

@@ -1,8 +0,0 @@
services:
immich-devcontainer:
build:
dockerfile: Dockerfile
extra_hosts:
- 'host.docker.internal:host-gateway'
volumes:
- ..:/immich:cached

View File

@@ -0,0 +1,34 @@
services:
immich-server:
build:
target: dev-container-mobile
environment:
- IMMICH_SERVER_URL=http://127.0.0.1:2283/
volumes: !override # bind mount host to /workspaces/immich
- ..:/workspaces/immich
- cli_node_modules:/workspaces/immich/cli/node_modules
- e2e_node_modules:/workspaces/immich/e2e/node_modules
- open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules
- server_node_modules:/workspaces/immich/server/node_modules
- web_node_modules:/workspaces/immich/web/node_modules
- ${UPLOAD_LOCATION}/photos:/workspaces/immich/server/upload
- ${UPLOAD_LOCATION}/photos/upload:/workspaces/immich/server/upload/upload
- /etc/localtime:/etc/localtime:ro
database:
volumes:
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
volumes:
# Node modules for each service to avoid conflicts and ensure consistent dependencies
cli_node_modules:
e2e_node_modules:
open_api_node_modules:
server_node_modules:
web_node_modules:
# UPLOAD_LOCATION must be set to a absolute path or vol-upload
vol-upload:
# DB_DATA_LOCATION must be set to a absolute path or vol-database
vol-database:

View File

@@ -0,0 +1,52 @@
{
"name": "Immich - Mobile",
"service": "immich-server",
"runServices": [
"immich-server",
"redis",
"database",
"immich-machine-learning"
],
"dockerComposeFile": [
"../../docker/docker-compose.dev.yml",
"./container-compose-overrides.yml"
],
"customizations": {
"vscode": {
"extensions": [
"Dart-Code.dart-code",
"Dart-Code.flutter",
"dcmdev.dcm-vscode-extension",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"svelte.svelte-vscode",
"ms-vscode-remote.remote-containers",
"foxundermoon.shell-format",
"timonwong.shellcheck",
"rvest.vs-code-prettier-eslint",
"bluebrown.yamlfmt",
"vkrishna04.cspell-sync",
"vitest.explorer",
"ms-playwright.playwright",
"ms-azuretools.vscode-docker"
]
}
},
"forwardPorts": [],
"overrideCommand": true,
"workspaceFolder": "/workspaces/immich",
"remoteUser": "node",
"userEnvProbe": "loginInteractiveShell",
"remoteEnv": {
// The location where your uploaded files are stored
"UPLOAD_LOCATION": "${localEnv:UPLOAD_LOCATION:./Library}",
// Connection secret for postgres. You should change it to a random password
// Please use only the characters `A-Za-z0-9`, without special characters or spaces
"DB_PASSWORD": "${localEnv:DB_PASSWORD:postgres}",
// The database username
"DB_USERNAME": "${localEnv:DB_USERNAME:postgres}",
// The database name
"DB_DATABASE_NAME": "${localEnv:DB_DATABASE_NAME:immich}"
}
}

View File

@@ -1,6 +0,0 @@
#!/bin/bash
# If .env file does not exist, create it by copying example.env from the docker folder
if [ ! -f ".devcontainer/.env" ]; then
cp docker/example.env .devcontainer/.env
fi

View File

@@ -1,25 +0,0 @@
#!/bin/bash
# Enable multiarch for arm64 if necessary
if [ "$(dpkg --print-architecture)" = "arm64" ]; then
sudo dpkg --add-architecture amd64 && \
sudo apt-get update && \
sudo apt-get install -y --no-install-recommends \
qemu-user-static \
libc6:amd64 \
libstdc++6:amd64 \
libgcc1:amd64
fi
# Install DCM
wget -qO- https://dcm.dev/pgp-key.public | sudo gpg --dearmor -o /usr/share/keyrings/dcm.gpg
sudo echo 'deb [signed-by=/usr/share/keyrings/dcm.gpg arch=amd64] https://dcm.dev/debian stable main' | sudo tee /etc/apt/sources.list.d/dart_stable.list
sudo apt-get update
sudo apt-get install dcm
dart --disable-analytics
# Install immich
cd /immich || exit
make install-all

View File

@@ -0,0 +1,80 @@
#!/bin/bash
export IMMICH_PORT="${DEV_SERVER_PORT:-2283}"
export DEV_PORT="${DEV_PORT:-3000}"
# search for immich directory inside workspace.
# /workspaces/immich is the bind mount, but other directories can be mounted if runing
# Devcontainer: Clone [repository|pull request] in container volumne
WORKSPACES_DIR="/workspaces"
IMMICH_DIR="$WORKSPACES_DIR/immich"
IMMICH_DEVCONTAINER_LOG="$HOME/immich-devcontainer.log"
log() {
# Display command on console, log with timestamp to file
echo "$*"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >>"$IMMICH_DEVCONTAINER_LOG"
}
run_cmd() {
# Ensure log directory exists
mkdir -p "$(dirname "$IMMICH_DEVCONTAINER_LOG")"
log "$@"
# Execute command: display normally on console, log with timestamps to file
"$@" 2>&1 | tee >(while IFS= read -r line; do
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $line" >>"$IMMICH_DEVCONTAINER_LOG"
done)
# Preserve exit status
return "${PIPESTATUS[0]}"
}
# Find directories excluding /workspaces/immich
mapfile -t other_dirs < <(find "$WORKSPACES_DIR" -mindepth 1 -maxdepth 1 -type d ! -path "$IMMICH_DIR" ! -name ".*")
if [ ${#other_dirs[@]} -gt 1 ]; then
log "Error: More than one directory found in $WORKSPACES_DIR other than $IMMICH_DIR."
exit 1
elif [ ${#other_dirs[@]} -eq 1 ]; then
export IMMICH_WORKSPACE="${other_dirs[0]}"
else
export IMMICH_WORKSPACE="$IMMICH_DIR"
fi
log "Found immich workspace in $IMMICH_WORKSPACE"
log ""
fix_permissions() {
log "Fixing permissions for ${IMMICH_WORKSPACE}"
run_cmd sudo find "${IMMICH_WORKSPACE}/server/upload" -not -path "${IMMICH_WORKSPACE}/server/upload/postgres/*" -not -path "${IMMICH_WORKSPACE}/server/upload/postgres" -exec chown node {} +
# Change ownership for directories that exist
for dir in "${IMMICH_WORKSPACE}/.vscode" \
"${IMMICH_WORKSPACE}/cli/node_modules" \
"${IMMICH_WORKSPACE}/e2e/node_modules" \
"${IMMICH_WORKSPACE}/open-api/typescript-sdk/node_modules" \
"${IMMICH_WORKSPACE}/server/node_modules" \
"${IMMICH_WORKSPACE}/server/dist" \
"${IMMICH_WORKSPACE}/web/node_modules" \
"${IMMICH_WORKSPACE}/web/dist"; do
if [ -d "$dir" ]; then
run_cmd sudo chown node -R "$dir"
fi
done
log ""
}
install_dependencies() {
log "Installing dependencies"
(
cd "${IMMICH_WORKSPACE}" || exit 1
export CI=1 FROZEN=1 OFFLINE=1
run_cmd make setup-web-dev setup-server-dev
)
log ""
}

View File

@@ -0,0 +1,49 @@
services:
immich-server:
build:
target: dev-container-server
env_file: !reset []
hostname: immich-dev
environment:
- IMMICH_SERVER_URL=http://127.0.0.1:2283/
volumes: !override
- ..:/workspaces/immich
- cli_node_modules:/workspaces/immich/cli/node_modules
- e2e_node_modules:/workspaces/immich/e2e/node_modules
- open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules
- server_node_modules:/workspaces/immich/server/node_modules
- web_node_modules:/workspaces/immich/web/node_modules
- ${UPLOAD_LOCATION:-upload1-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/workspaces/immich/server/upload
- ${UPLOAD_LOCATION:-upload2-devcontainer-volume}${UPLOAD_LOCATION:+/photos/upload}:/workspaces/immich/server/upload/upload
- /etc/localtime:/etc/localtime:ro
immich-web:
env_file: !reset []
immich-machine-learning:
env_file: !reset []
database:
env_file: !reset []
environment: !override
POSTGRES_PASSWORD: ${DB_PASSWORD-postgres}
POSTGRES_USER: ${DB_USERNAME-postgres}
POSTGRES_DB: ${DB_DATABASE_NAME-immich}
POSTGRES_INITDB_ARGS: '--data-checksums'
POSTGRES_HOST_AUTH_METHOD: md5
volumes:
- ${UPLOAD_LOCATION:-postgres-devcontainer-volume}${UPLOAD_LOCATION:+/postgres}:/var/lib/postgresql/data
redis:
env_file: !reset []
volumes:
# Node modules for each service to avoid conflicts and ensure consistent dependencies
cli_node_modules:
e2e_node_modules:
open_api_node_modules:
server_node_modules:
web_node_modules:
upload1-devcontainer-volume:
upload2-devcontainer-volume:
postgres-devcontainer-volume:

View File

@@ -0,0 +1,17 @@
#!/bin/bash
# shellcheck source=common.sh
# shellcheck disable=SC1091
source /immich-devcontainer/container-common.sh
log "Starting Nest API Server"
log ""
cd "${IMMICH_WORKSPACE}/server" || (
log "Immich workspace not found"
exit 1
)
while true; do
run_cmd node ./node_modules/.bin/nest start --debug "0.0.0.0:9230" --watch
log "Nest API Server crashed with exit code $?. Respawning in 3s ..."
sleep 3
done

View File

@@ -0,0 +1,22 @@
#!/bin/bash
# shellcheck source=common.sh
# shellcheck disable=SC1091
source /immich-devcontainer/container-common.sh
log "Starting Immich Web Frontend"
log ""
cd "${IMMICH_WORKSPACE}/web" || (
log "Immich Workspace not found"
exit 1
)
until curl --output /dev/null --silent --head --fail "http://127.0.0.1:${IMMICH_PORT}/api/server/config"; do
log "Waiting for api server..."
sleep 1
done
while true; do
run_cmd node ./node_modules/.bin/vite dev --host 0.0.0.0 --port "${DEV_PORT}"
log "Web crashed with exit code $?. Respawning in 3s ..."
sleep 3
done

View File

@@ -0,0 +1,20 @@
#!/bin/bash
# shellcheck source=common.sh
# shellcheck disable=SC1091
source /immich-devcontainer/container-common.sh
log "Setting up Immich dev container..."
fix_permissions
log "Installing npm dependencies (node_modules)..."
install_dependencies
log "Setup complete, please wait while backend and frontend services automatically start"
log
log "If necessary, the services may be manually started using"
log
log "$ /immich-devcontainer/container-start-backend.sh"
log "$ /immich-devcontainer/container-start-frontend.sh"
log
log "From different terminal windows, as these scripts automatically restart the server"
log "on error, and will continuously run in a loop"

View File

@@ -4,9 +4,14 @@
design/
docker/
Dockerfile
!docker/scripts
docs/
!docs/package.json
!docs/package-lock.json
e2e/
!e2e/package.json
!e2e/package-lock.json
fastlane/
machine-learning/
misc/
@@ -15,6 +20,7 @@ mobile/
cli/coverage/
cli/dist/
cli/node_modules/
cli/Dockerfile
open-api/typescript-sdk/build/
open-api/typescript-sdk/node_modules/
@@ -25,9 +31,11 @@ server/upload/
server/src/queries
server/dist/
server/www/
server/Dockerfile
web/node_modules/
web/coverage/
web/.svelte-kit
web/build/
web/.env
web/Dockerfile

2
.github/.nvmrc vendored
View File

@@ -1 +1 @@
22.16.0
22.17.0

4
.github/.prettierignore vendored Normal file
View File

@@ -0,0 +1,4 @@
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

6
.github/package-lock.json generated vendored
View File

@@ -9,9 +9,9 @@
}
},
"node_modules/prettier": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"bin": {

View File

@@ -58,7 +58,7 @@ jobs:
contents: read
# Skip when PR from a fork
if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' && needs.pre-job.outputs.should_run == 'true' }}
runs-on: macos-14
runs-on: mich
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -66,24 +66,40 @@ jobs:
ref: ${{ inputs.ref || github.sha }}
persist-credentials: false
- name: Create the Keystore
env:
KEY_JKS: ${{ secrets.KEY_JKS }}
working-directory: ./mobile
run: printf "%s" $KEY_JKS | base64 -d > android/key.jks
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: 'zulu'
java-version: '17'
cache: 'gradle'
- name: Restore Gradle Cache
id: cache-gradle-restore
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
~/.android/sdk
mobile/android/.gradle
mobile/.dart_tool
key: build-mobile-gradle-${{ runner.os }}-main
- name: Setup Flutter SDK
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0
uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0
with:
channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml
cache: true
- name: Create the Keystore
env:
KEY_JKS: ${{ secrets.KEY_JKS }}
working-directory: ./mobile
run: echo $KEY_JKS | base64 -d > android/key.jks
- name: Setup Android SDK
uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3.2.2
with:
packages: ''
- name: Get Packages
working-directory: ./mobile
@@ -103,12 +119,30 @@ jobs:
ALIAS: ${{ secrets.ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
IS_MAIN: ${{ github.ref == 'refs/heads/main' }}
run: |
flutter build apk --release
flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64
if [[ $IS_MAIN == 'true' ]]; then
flutter build apk --release --flavor production
flutter build apk --release --flavor production --split-per-abi --target-platform android-arm,android-arm64,android-x64
else
flutter build apk --debug --flavor production --split-per-abi --target-platform android-arm64
fi
- name: Publish Android Artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: release-apk-signed
path: mobile/build/app/outputs/flutter-apk/*.apk
path: mobile/build/app/outputs/flutter-apk/**/*.apk
- name: Save Gradle Cache
id: cache-gradle-save
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4
if: github.ref == 'refs/heads/main'
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
~/.android/sdk
mobile/android/.gradle
mobile/.dart_tool
key: ${{ steps.cache-gradle-restore.outputs.cache-primary-key }}

View File

@@ -38,6 +38,9 @@ jobs:
with:
node-version-file: './cli/.nvmrc'
registry-url: 'https://registry.npmjs.org'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
- name: Prepare SDK
run: npm ci --prefix ../open-api/typescript-sdk/
- name: Build SDK
@@ -67,7 +70,7 @@ jobs:
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Login to GitHub Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
@@ -96,7 +99,7 @@ jobs:
type=raw,value=latest,enable=${{ github.event_name == 'release' }}
- name: Build and push image
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
file: cli/Dockerfile
platforms: linux/amd64,linux/arm64

View File

@@ -50,7 +50,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -63,7 +63,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
uses: github/codeql-action/autobuild@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -76,6 +76,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
with:
category: '/language:${{matrix.language}}'

View File

@@ -131,7 +131,7 @@ jobs:
tag-suffix: '-rocm'
platforms: linux/amd64
runner-mapping: '{"linux/amd64": "mich"}'
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@094bfb927b8cd75b343abaac27b3241be0fccfe9 # multi-runner-build-workflow-0.1.0
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@129aeda75a450666ce96e8bc8126652e717917a7 # multi-runner-build-workflow-0.1.1
permissions:
contents: read
actions: read
@@ -154,7 +154,7 @@ jobs:
name: Build and Push Server
needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@094bfb927b8cd75b343abaac27b3241be0fccfe9 # multi-runner-build-workflow-0.1.0
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@129aeda75a450666ce96e8bc8126652e717917a7 # multi-runner-build-workflow-0.1.1
permissions:
contents: read
actions: read
@@ -177,7 +177,7 @@ jobs:
runs-on: ubuntu-latest
if: always()
steps:
- uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3
- uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4
with:
needs: ${{ toJSON(needs) }}
@@ -188,6 +188,6 @@ jobs:
runs-on: ubuntu-latest
if: always()
steps:
- uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3
- uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4
with:
needs: ${{ toJSON(needs) }}

View File

@@ -57,6 +57,8 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './docs/.nvmrc'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
- name: Run npm install
run: npm ci

View File

@@ -32,6 +32,8 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './server/.nvmrc'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
- name: Fix formatting
run: make install-all && make format-all

13
.github/workflows/org-checks.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
name: Org Checks
on:
pull_request_review:
pull_request:
jobs:
check-approvals:
name: Check for Team/Admin Review
uses: immich-app/devtools/.github/workflows/required-approval.yml@main
permissions:
pull-requests: read
contents: read

View File

@@ -14,7 +14,7 @@ jobs:
pull-requests: write
steps:
- name: Require PR to have a changelog label
uses: mheap/github-action-required-labels@fb29a14a076b0f74099f6198f77750e8fc236016 # v5.5.0
uses: mheap/github-action-required-labels@8afbe8ae6ab7647d0c9f0cfa7c2f939650d22509 # v5.5.1
with:
mode: exactly
count: 1

View File

@@ -100,7 +100,7 @@ jobs:
name: release-apk-signed
- name: Create draft release
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
with:
draft: true
tag_name: ${{ env.IMMICH_VERSION }}

View File

@@ -25,6 +25,8 @@ jobs:
with:
node-version-file: './open-api/typescript-sdk/.nvmrc'
registry-url: 'https://registry.npmjs.org'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
- name: Install deps
run: npm ci
- name: Build

View File

@@ -42,6 +42,9 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
defaults:
run:
working-directory: ./mobile
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -49,34 +52,29 @@ jobs:
persist-credentials: false
- name: Setup Flutter SDK
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0
uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0
with:
channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml
- name: Install dependencies
run: dart pub get
working-directory: ./mobile
- name: Install DCM
run: |
sudo apt-get update
wget -qO- https://dcm.dev/pgp-key.public | sudo gpg --dearmor -o /usr/share/keyrings/dcm.gpg
echo 'deb [signed-by=/usr/share/keyrings/dcm.gpg arch=amd64] https://dcm.dev/debian stable main' | sudo tee /etc/apt/sources.list.d/dart_stable.list
sudo apt-get update
sudo apt-get install dcm
uses: CQLabs/setup-dcm@8697ae0790c0852e964a6ef1d768d62a6675481a # v2.0.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
version: auto
working-directory: ./mobile
- name: Generate translation file
run: make translation
working-directory: ./mobile
- name: Run Build Runner
run: make build
working-directory: ./mobile
- name: Generate platform API
run: make pigeon
working-directory: ./mobile
- name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
@@ -98,19 +96,16 @@ jobs:
- name: Run dart analyze
run: dart analyze --fatal-infos
working-directory: ./mobile
- name: Run dart format
run: dart format lib/ --set-exit-if-changed
working-directory: ./mobile
- name: Run dart custom_lint
run: dart run custom_lint
working-directory: ./mobile
# TODO: Use https://github.com/CQLabs/dcm-action
- name: Run DCM
run: dcm analyze lib
working-directory: ./mobile
run: dcm analyze lib --fatal-style --fatal-warnings
zizmor:
name: zizmor
@@ -134,7 +129,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
with:
sarif_file: results.sarif
category: zizmor

View File

@@ -84,6 +84,8 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './server/.nvmrc'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
- name: Run npm install
run: npm ci
@@ -101,7 +103,7 @@ jobs:
if: ${{ !cancelled() }}
- name: Run small tests & coverage
run: npm run test:cov
run: npm test
if: ${{ !cancelled() }}
cli-unit-tests:
@@ -125,6 +127,8 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './cli/.nvmrc'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
- name: Setup typescript-sdk
run: npm ci && npm run build
@@ -146,7 +150,7 @@ jobs:
if: ${{ !cancelled() }}
- name: Run unit tests & coverage
run: npm run test:cov
run: npm run test
if: ${{ !cancelled() }}
cli-unit-tests-win:
@@ -170,6 +174,8 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './cli/.nvmrc'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
- name: Setup typescript-sdk
run: npm ci && npm run build
@@ -184,7 +190,7 @@ jobs:
if: ${{ !cancelled() }}
- name: Run unit tests & coverage
run: npm run test:cov
run: npm run test
if: ${{ !cancelled() }}
web-lint:
@@ -208,6 +214,8 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './web/.nvmrc'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
- name: Run setup typescript-sdk
run: npm ci && npm run build
@@ -249,6 +257,8 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './web/.nvmrc'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
- name: Run setup typescript-sdk
run: npm ci && npm run build
@@ -262,7 +272,7 @@ jobs:
if: ${{ !cancelled() }}
- name: Run unit tests & coverage
run: npm run test:cov
run: npm run test
if: ${{ !cancelled() }}
i18n-tests:
@@ -282,6 +292,8 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './web/.nvmrc'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
- name: Install dependencies
run: npm --prefix=web ci
@@ -326,6 +338,8 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './e2e/.nvmrc'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
- name: Run setup typescript-sdk
run: npm ci && npm run build
@@ -369,6 +383,8 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './server/.nvmrc'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
- name: Run npm install
run: npm ci
@@ -402,6 +418,8 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './e2e/.nvmrc'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
- name: Run setup typescript-sdk
run: npm ci && npm run build
@@ -450,6 +468,8 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './e2e/.nvmrc'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
- name: Run setup typescript-sdk
run: npm ci && npm run build
@@ -461,7 +481,7 @@ jobs:
if: ${{ !cancelled() }}
- name: Install Playwright Browsers
run: npx playwright install --with-deps chromium
run: npx playwright install chromium --only-shell
if: ${{ !cancelled() }}
- name: Docker build
@@ -479,7 +499,7 @@ jobs:
runs-on: ubuntu-latest
if: always()
steps:
- uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3
- uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4
with:
needs: ${{ toJSON(needs) }}
@@ -496,7 +516,7 @@ jobs:
persist-credentials: false
- name: Setup Flutter SDK
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0
uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0
with:
channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml
@@ -568,6 +588,8 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './.github/.nvmrc'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
- name: Run npm install
run: npm ci
@@ -587,7 +609,7 @@ jobs:
persist-credentials: false
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@master
uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # 2.0.0
with:
ignore_paths: >-
**/open-api/**
@@ -609,6 +631,8 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './server/.nvmrc'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
- name: Install server dependencies
run: npm --prefix=server ci
@@ -644,7 +668,7 @@ jobs:
contents: read
services:
postgres:
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.1
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3@sha256:1f5583fe3397210a0fbc7f11b0cec18bacc4a99e3e8ea0548e9bd6bcf26ec37a
env:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
@@ -670,6 +694,8 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './server/.nvmrc'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
- name: Install server dependencies
run: npm ci
@@ -722,6 +748,7 @@ jobs:
run: |
echo "ERROR: Generated SQL files not up to date!"
echo "Changed files: ${CHANGED_FILES}"
git diff
exit 1
# mobile-integration-tests:

View File

@@ -52,6 +52,6 @@ jobs:
permissions: {}
if: always()
steps:
- uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3
- uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4
with:
needs: ${{ toJSON(needs) }}

19
.vscode/launch.json vendored
View File

@@ -18,6 +18,25 @@
"name": "Immich Workers",
"remoteRoot": "/usr/src/app",
"localRoot": "${workspaceFolder}/server"
},
{
"name": "Flavor - Production",
"request": "launch",
"type": "dart",
"codeLens": {
"for": [
"run-test",
"run-test-file",
"run-file",
"debug-test",
"debug-test-file",
"debug-file",
],
"title": "${debugType}",
},
"args": [
"--flavor", "production"
],
}
]
}

72
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,72 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Fix Permissions, Install Dependencies",
"type": "shell",
"command": "[ -f /immich-devcontainer/container-start.sh ] && /immich-devcontainer/container-start.sh || exit 0",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "dedicated",
"showReuseMessage": true,
"clear": false,
"group": "Devcontainer tasks",
"close": true
},
"runOptions": {
"runOn": "default"
},
"problemMatcher": []
},
{
"label": "Immich API Server (Nest)",
"dependsOn": ["Fix Permissions, Install Dependencies"],
"type": "shell",
"command": "[ -f /immich-devcontainer/container-start-backend.sh ] && /immich-devcontainer/container-start-backend.sh || exit 0",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "dedicated",
"showReuseMessage": true,
"clear": false,
"group": "Devcontainer tasks",
"close": true
},
"runOptions": {
"runOn": "default"
},
"problemMatcher": []
},
{
"label": "Immich Web Server (Vite)",
"dependsOn": ["Fix Permissions, Install Dependencies"],
"type": "shell",
"command": "[ -f /immich-devcontainer/container-start-frontend.sh ] && /immich-devcontainer/container-start-frontend.sh || exit 0",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "dedicated",
"showReuseMessage": true,
"clear": false,
"group": "Devcontainer tasks",
"close": true
},
"runOptions": {
"runOn": "default"
},
"problemMatcher": []
},
{
"label": "Immich Server and Web",
"dependsOn": ["Immich Web Server (Vite)", "Immich API Server (Nest)"],
"runOptions": {
"runOn": "folderOpen"
},
"problemMatcher": []
}
]
}

View File

@@ -1,27 +1,33 @@
dev:
docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans || make dev-down
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans
dev-down:
docker compose -f ./docker/docker-compose.dev.yml down --remove-orphans
dev-update:
docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
dev-scale:
docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
.PHONY: e2e
e2e:
docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
e2e-update:
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
e2e-down:
docker compose -f ./e2e/docker-compose.yml down --remove-orphans
prod:
docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
@trap 'make prod-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
prod-down:
docker compose -f ./docker/docker-compose.prod.yml down --remove-orphans
prod-scale:
docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
@trap 'make prod-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
.PHONY: open-api
open-api:
@@ -48,6 +54,8 @@ audit-%:
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) audit fix
install-%:
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) i
ci-%:
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) ci
build-cli: build-sdk
build-web: build-sdk
build-%: install-%
@@ -82,6 +90,7 @@ test-medium-dev:
build-all: $(foreach M,$(filter-out e2e .github,$(MODULES)),build-$M) ;
install-all: $(foreach M,$(MODULES),install-$M) ;
ci-all: $(foreach M,$(filter-out .github,$(MODULES)),ci-$M) ;
check-all: $(foreach M,$(filter-out sdk cli docs .github,$(MODULES)),check-$M) ;
lint-all: $(foreach M,$(filter-out sdk docs .github,$(MODULES)),lint-$M) ;
format-all: $(foreach M,$(filter-out sdk,$(MODULES)),format-$M) ;
@@ -90,9 +99,12 @@ hygiene-all: lint-all format-all check-all sql audit-all;
test-all: $(foreach M,$(filter-out sdk docs .github,$(MODULES)),test-$M) ;
clean:
find . -name "node_modules" -type d -prune -exec rm -rf '{}' +
find . -name "node_modules" -type d -prune -exec rm -rf {} +
find . -name "dist" -type d -prune -exec rm -rf '{}' +
find . -name "build" -type d -prune -exec rm -rf '{}' +
find . -name "svelte-kit" -type d -prune -exec rm -rf '{}' +
docker compose -f ./docker/docker-compose.dev.yml rm -v -f || true
docker compose -f ./e2e/docker-compose.yml rm -v -f || true
command -v docker >/dev/null 2>&1 && docker compose -f ./docker/docker-compose.dev.yml rm -v -f || true
command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml rm -v -f || true
setup-server-dev: install-server
setup-web-dev: install-sdk build-sdk install-web

View File

@@ -1 +1 @@
22.16.0
22.17.0

2
cli/bin/immich Executable file
View File

@@ -0,0 +1,2 @@
#!/usr/bin/env node
import '../dist/index.js';

532
cli/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,11 @@
{
"name": "@immich/cli",
"version": "2.2.68",
"version": "2.2.72",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",
"bin": {
"immich": "dist/index.js"
"immich": "./bin/immich"
},
"license": "GNU Affero General Public License version 3",
"keywords": [
@@ -21,7 +21,7 @@
"@types/lodash-es": "^4.17.12",
"@types/micromatch": "^4.0.9",
"@types/mock-fs": "^4.13.1",
"@types/node": "^22.15.29",
"@types/node": "^22.15.33",
"@vitest/coverage-v8": "^3.0.0",
"byte-size": "^9.0.0",
"cli-progress": "^3.12.0",
@@ -36,7 +36,7 @@
"prettier-plugin-organize-imports": "^4.0.0",
"typescript": "^5.3.3",
"typescript-eslint": "^8.28.0",
"vite": "^6.0.0",
"vite": "^7.0.0",
"vite-tsconfig-paths": "^5.0.0",
"vitest": "^3.0.0",
"vitest-fetch-mock": "^0.4.0",
@@ -69,6 +69,6 @@
"micromatch": "^4.0.8"
},
"volta": {
"node": "22.16.0"
"node": "22.17.0"
}
}

View File

@@ -70,7 +70,10 @@ const uploadBatch = async (files: string[], options: UploadOptionsDto) => {
console.log(JSON.stringify({ newFiles, duplicates, newAssets }, undefined, 4));
}
await updateAlbums([...newAssets, ...duplicates], options);
await deleteFiles(newFiles, options);
await deleteFiles(
newAssets.map(({ filepath }) => filepath),
options,
);
};
export const startWatch = async (

View File

@@ -116,13 +116,13 @@ services:
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm@sha256:a19bebed6a91bd5e6e2106fef015f9602a3392deeb7c9ed47548378dcee3dfc2
image: docker.io/valkey/valkey:8-bookworm@sha256:facc1d2c3462975c34e10fccb167bfa92b0e0dbd992fc282c29a61c3243afb11
healthcheck:
test: redis-cli ping || exit 1
database:
container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.1-pgvectors0.2.0
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:5f6a838e4e44c8e0e019d0ebfe3ee8952b69afc2809b2c25f7b0119641978e91
env_file:
- .env
environment:
@@ -134,6 +134,7 @@ services:
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
ports:
- 5432:5432
shm_size: 128mb
# set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics
# immich-prometheus:
# container_name: immich_prometheus

View File

@@ -56,14 +56,14 @@ services:
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm@sha256:a19bebed6a91bd5e6e2106fef015f9602a3392deeb7c9ed47548378dcee3dfc2
image: docker.io/valkey/valkey:8-bookworm@sha256:facc1d2c3462975c34e10fccb167bfa92b0e0dbd992fc282c29a61c3243afb11
healthcheck:
test: redis-cli ping || exit 1
restart: always
database:
container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.1-pgvectors0.2.0
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:5f6a838e4e44c8e0e019d0ebfe3ee8952b69afc2809b2c25f7b0119641978e91
env_file:
- .env
environment:
@@ -75,6 +75,7 @@ services:
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
ports:
- 5432:5432
shm_size: 128mb
restart: always
# set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics
@@ -82,7 +83,7 @@ services:
container_name: immich_prometheus
ports:
- 9090:9090
image: prom/prometheus@sha256:9abc6cf6aea7710d163dbb28d8eeb7dc5baef01e38fa4cd146a406dd9f07f70d
image: prom/prometheus@sha256:63805ebb8d2b3920190daf1cb14a60871b16fd38bed42b857a3182bc621f4996
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
@@ -94,7 +95,7 @@ services:
command: ['./run.sh', '-disable-reporting']
ports:
- 3000:3000
image: grafana/grafana:12.0.1-ubuntu@sha256:65575bb9c761335e2ff30e364f21d38632e3b2e75f5f81d83cc92f44b9bbc055
image: grafana/grafana:12.0.2-ubuntu@sha256:0512d81cdeaaff0e370a9aa66027b465d1f1f04379c3a9c801a905fabbdbc7a5
volumes:
- grafana-data:/var/lib/grafana

View File

@@ -49,14 +49,14 @@ services:
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm@sha256:a19bebed6a91bd5e6e2106fef015f9602a3392deeb7c9ed47548378dcee3dfc2
image: docker.io/valkey/valkey:8-bookworm@sha256:facc1d2c3462975c34e10fccb167bfa92b0e0dbd992fc282c29a61c3243afb11
healthcheck:
test: redis-cli ping || exit 1
restart: always
database:
container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.1-pgvectors0.2.0
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:5f6a838e4e44c8e0e019d0ebfe3ee8952b69afc2809b2c25f7b0119641978e91
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME}
@@ -67,6 +67,7 @@ services:
volumes:
# Do not edit the next line. If you want to change the database storage location on your system, edit the value of DB_DATA_LOCATION in the .env file
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
shm_size: 128mb
restart: always
volumes:

View File

@@ -1 +1 @@
22.16.0
22.17.0

View File

@@ -490,7 +490,7 @@ You can also scan the Postgres database file structure for errors:
<details>
<summary>Scan for file structure errors</summary>
```bash
docker exec -it immich_postgres pg_amcheck --username=postgres --heapallindexed --parent-check --rootdescend --progress --all --install-missing
docker exec -it immich_postgres pg_amcheck --username=<DB_USERNAME> --heapallindexed --parent-check --rootdescend --progress --all --install-missing
```
A normal result will end something like this and return with an exit code of `0`:

View File

@@ -57,7 +57,7 @@ Then please follow the steps in the following section for restoring the database
<TabItem value="Linux system" label="Linux system" default>
```bash title='Backup'
docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgres | gzip > "/path/to/backup/dump.sql.gz"
docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=<DB_USERNAME> | gzip > "/path/to/backup/dump.sql.gz"
```
```bash title='Restore'
@@ -79,7 +79,7 @@ docker compose up -d # Start remainder of Immich apps
<TabItem value="Windows system (PowerShell)" label="Windows system (PowerShell)">
```powershell title='Backup'
[System.IO.File]::WriteAllLines("C:\absolute\path\to\backup\dump.sql", (docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgres))
[System.IO.File]::WriteAllLines("C:\absolute\path\to\backup\dump.sql", (docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=<DB_USERNAME>))
```
```powershell title='Restore'
@@ -150,12 +150,10 @@ for more info read the [release notes](https://github.com/immich-app/immich/rele
- Preview images (small thumbnails and large previews) for each asset and thumbnails for recognized faces.
- Stored in `UPLOAD_LOCATION/thumbs/<userID>`.
- **Encoded Assets:**
- Videos that have been re-encoded from the original for wider compatibility. The original is not removed.
- Stored in `UPLOAD_LOCATION/encoded-video/<userID>`.
- **Postgres**
- The Immich database containing all the information to allow the system to function properly.
**Note:** This folder will only appear to users who have made the changes mentioned in [v1.102.0](https://github.com/immich-app/immich/discussions/8930) (an optional, non-mandatory change) or who started with this version.
- Stored in `DB_DATA_LOCATION`.
@@ -201,7 +199,6 @@ When you turn off the storage template engine, it will leave the assets in `UPLO
- Temporarily located in `UPLOAD_LOCATION/upload/<userID>`.
- Transferred to `UPLOAD_LOCATION/library/<userID>` upon successful upload.
- **Postgres**
- The Immich database containing all the information to allow the system to function properly.
**Note:** This folder will only appear to users who have made the changes mentioned in [v1.102.0](https://github.com/immich-app/immich/discussions/8930) (an optional, non-mandatory change) or who started with this version.
- Stored in `DB_DATA_LOCATION`.

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -46,6 +46,12 @@ services:
When a new asset is uploaded it kicks off a series of jobs, which include metadata extraction, thumbnail generation, machine learning tasks, and storage template migration, if enabled. To view the status of a job navigate to the Administration -> Jobs page.
Additionally, some jobs run on a schedule, which is every night at midnight. This schedule, with the exception of [External Libraries](/docs/features/libraries) scanning, cannot be changed.
<img src={require('./img/admin-jobs.webp').default} width="60%" title="Admin jobs" />
Additionally, some jobs (such as memories generation) run on a schedule, which is every night at midnight by default. To change when they run or enable/disable a job navigate to System Settings -> [Nightly Tasks Settings](https://my.immich.app/admin/system-settings?isOpen=nightly-tasks).
<img src={require('./img/admin-nightly-tasks.webp').default} width="60%" title="Admin nightly tasks" />
:::note
Some jobs ([External Libraries](/docs/features/libraries) scanning, Database Dump) are configured in their own sections in System Settings.
:::

View File

@@ -20,7 +20,6 @@ Immich supports 3rd party authentication via [OpenID Connect][oidc] (OIDC), an i
Before enabling OAuth in Immich, a new client application needs to be configured in the 3rd-party authentication server. While the specifics of this setup vary from provider to provider, the general approach should be the same.
1. Create a new (Client) Application
1. The **Provider** type should be `OpenID Connect` or `OAuth2`
2. The **Client type** should be `Confidential`
3. The **Application** type should be `Web`
@@ -29,7 +28,6 @@ Before enabling OAuth in Immich, a new client application needs to be configured
2. Configure Redirect URIs/Origins
The **Sign-in redirect URIs** should include:
- `app.immich:///oauth-callback` - for logging in with OAuth from the [Mobile App](/docs/features/mobile-app.mdx)
- `http://DOMAIN:PORT/auth/login` - for logging in with OAuth from the Web Client
- `http://DOMAIN:PORT/user-settings` - for manually linking OAuth in the Web Client
@@ -37,21 +35,17 @@ Before enabling OAuth in Immich, a new client application needs to be configured
Redirect URIs should contain all the domains you will be using to access Immich. Some examples include:
Mobile
- `app.immich:///oauth-callback` (You **MUST** include this for iOS and Android mobile apps to work properly)
Localhost
- `http://localhost:2283/auth/login`
- `http://localhost:2283/user-settings`
Local IP
- `http://192.168.0.200:2283/auth/login`
- `http://192.168.0.200:2283/user-settings`
Hostname
- `https://immich.example.com/auth/login`
- `https://immich.example.com/user-settings`
@@ -68,6 +62,7 @@ Once you have a new OAuth client application configured, Immich can be configure
| Scope | string | openid email profile | Full list of scopes to send with the request (space delimited) |
| Signing Algorithm | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
| Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label**¹** |
| Role Claim | string | immich_role | Claim mapping for the user's role. (should return "user" or "admin")**¹** |
| Storage Quota Claim | string | immich_quota | Claim mapping for the user's storage**¹** |
| Default Storage Quota (GiB) | number | 0 | Default quota for user without storage quota claim (Enter 0 for unlimited quota) |
| Button Text | string | Login with OAuth | Text for the OAuth button on the web |

View File

@@ -64,7 +64,13 @@ COMMIT;
### Updating VectorChord
When installing a new version of VectorChord, you will need to manually update the extension by connecting to the Immich database and running `ALTER EXTENSION vchord UPDATE;`.
When installing a new version of VectorChord, you will need to manually update the extension and reindex by connecting to the Immich database and running:
```
ALTER EXTENSION vchord UPDATE;
REINDEX INDEX face_index;
REINDEX INDEX clip_index;
```
## Migrating to VectorChord
@@ -76,6 +82,8 @@ Support for pgvecto.rs will be dropped in a later release, hence we recommend al
The easiest option is to have both extensions installed during the migration:
<details>
<summary>Migration steps (automatic)</summary>
1. Ensure you still have pgvecto.rs installed
2. Install `pgvector` (`>= 0.7.0, < 1.0.0`). The easiest way to do this is on Debian/Ubuntu by adding the [PostgreSQL Apt repository][pg-apt] and then running `apt install postgresql-NN-pgvector`, where `NN` is your Postgres version (e.g., `16`)
3. [Install VectorChord][vchord-install]
@@ -89,8 +97,12 @@ The easiest option is to have both extensions installed during the migration:
11. Restart the Postgres database
12. Uninstall pgvecto.rs (e.g. `apt-get purge vectors-pg14` on Debian-based environments, replacing `pg14` as appropriate). `pgvector` must remain installed as it provides the data types used by `vchord`
</details>
If it is not possible to have both VectorChord and pgvecto.rs installed at the same time, you can perform the migration with more manual steps:
<details>
<summary>Migration steps (manual)</summary>
1. While pgvecto.rs is still installed, run the following SQL command using psql or your choice of database client. Take note of the number outputted by this command as you will need it later
```sql
@@ -123,14 +135,20 @@ ALTER TABLE face_search ALTER COLUMN embedding SET DATA TYPE vector(512);
5. Start Immich and let it create new indices using VectorChord
</details>
### Migrating from pgvector
<details>
<summary>Migration steps</summary>
1. Ensure you have at least 0.7.0 of pgvector installed. If it is below that, please upgrade it and run the SQL command `ALTER EXTENSION vector UPDATE;` using psql or your choice of database client
2. Follow the Prerequisites to install VectorChord
3. If Immich does not have superuser permissions, run the SQL command `CREATE EXTENSION vchord CASCADE;`
4. Remove the `DB_VECTOR_EXTENSION=pgvector` environmental variable as it will make Immich still use pgvector if set
5. Start Immich and let it create new indices using VectorChord
</details>
Note that VectorChord itself uses pgvector types, so you should not uninstall pgvector after following these steps.
[vchord-install]: https://docs.vectorchord.ai/vectorchord/getting-started/installation.html

View File

@@ -0,0 +1,480 @@
---
title: Devcontainers
sidebar_position: 3
---
# Development with Dev Containers
Dev Containers provide a consistent, reproducible development environment using Docker containers. With a single click, you can get started with an Immich development environment on Mac, Linux, Windows, or in the cloud using GitHub Codespaces.
Get started fast!
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/immich-app/immich/)
[Learn more about Dev Containers](https://docs.github.com/en/codespaces/setting-up-your-project-for-codespaces/adding-a-dev-container-configuration/introduction-to-dev-containers)
## Prerequisites
Before getting started, ensure you have:
- **Docker Desktop** (latest version)
- [Mac](https://docs.docker.com/desktop/install/mac-install/)
- [Windows](https://docs.docker.com/desktop/install/windows-install/) (with WSL2 backend recommended)
- [Linux](https://docs.docker.com/desktop/install/linux-install/)
- **Visual Studio Code** with the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
- **Git** for cloning the repository
- At least **8GB of RAM** (16GB recommended)
- **20GB of free disk space**
:::tip Alternative Development Environments
While this guide focuses on VS Code, you have many options for Dev Container development:
**Local Editors:**
- [IntelliJ IDEA](https://www.jetbrains.com/help/idea/connect-to-devcontainer.html) - Full JetBrains IDE support
- [neovim](https://github.com/jamestthompson3/nvim-remote-containers) - Lightweight terminal-based editor
- [Emacs](https://github.com/emacs-lsp/lsp-docker) - Extensible text editor
- [DevContainer CLI](https://github.com/devcontainers/cli) - Command-line interface
**Cloud-Based Solutions:**
- [GitHub Codespaces](https://github.com/features/codespaces) - Fully integrated with GitHub, excellent devcontainer.json support
- [GitPod](https://www.gitpod.io) - SaaS platform with recent Dev Container support (historically used gitpod.yml)
**Self-Hostable Options:**
- [Coder](https://coder.com) - Enterprise-focused, requires Terraform knowledge, self-managed
- [DevPod](https://devpod.sh) - Client-only tool with excellent devcontainer.json support, works with any provider (local, cloud, or on-premise)
:::
## Dev Container Services
The Dev Container environment consists of the following services:
| Service | Container Name | Description | Ports |
| ---------------- | ------------------------- | --------------------------------------------------------- | ----------------------------------------------------------------------- |
| Server & Web | `immich-server` | Runs both API server and web frontend in development mode | 2283 (API)<br/>3000 (Web)<br/>9230 (Workers Debug)<br/>9231 (API Debug) |
| Database | `database` | PostgreSQL database | 5432 |
| Cache | `redis` | Valkey cache server | 6379 |
| Machine Learning | `immich-machine-learning` | Immich ML model inference server | 3003 |
## Getting Started
### Step 1: Clone the Repository
```bash
git clone https://github.com/immich-app/immich.git
cd immich
```
### Step 2: Configure Environment Variables
The immich dev containers read environment variables from your shell environment, not from `.env` files. This allows them to work in cloud environments without pre-configuration.
:::important Configuration
When running locally, and if you want to create (or use an existing) DB and/or photo storage folder, you must set the `UPLOAD_LOCATION` variable in your shell environment before launching the Dev Container. This determines where uploaded files are stored and also where the DB stores it data.
```bash
# Set temporarily for current session
export UPLOAD_LOCATION=/opt/dev_upload_folder
# Or add to your shell profile for persistence
# (~/.bashrc, ~/.zshrc, ~/.bash_profile, etc.)
echo 'export UPLOAD_LOCATION=/opt/dev_upload_folder' >> ~/.bashrc
source ~/.bashrc
```
:::
### Step 3: Launch the Dev Container
:::tip
Immich development makes extensive use of specialized [base images](https://github.com/immich-app/base-images) for its docker-compose based development. For this reason, you won't be able to use VSCode's **_Clone Repository in a Container Volume_** command.
:::
#### Using VS Code UI:
1. Open the cloned repository in VS Code
2. Press `F1` or `Ctrl/Cmd+Shift+P` to open the command palette
3. Type and select "Dev Containers: Rebuild and Reopen in Container"
4. Select "Immich - Backend, Frontend and ML" from the list
5. Wait for the container to build and start (this may take several minutes on first run)
#### Using VS Code Quick Actions:
1. Open the repository in VS Code
2. You should see a popup asking if you want to reopen in a container
3. Click "Reopen in Container"
#### Using Command Line:
```bash
# Using the DevContainer CLI
devcontainer up --workspace-folder .
```
## Environment Variable Details
### How Dev Containers Handle Environment Variables
Unlike the Immich developer setup based on Docker Compose which uses `.env` files, Immich Dev Containers read environment variables from your shell environment. This is configured in `.devcontainer/devcontainer.json`:
```json
"remoteEnv": {
"UPLOAD_LOCATION": "${localEnv:UPLOAD_LOCATION:./Library}",
"DB_PASSWORD": "${localEnv:DB_PASSWORD:postgres}",
"DB_USERNAME": "${localEnv:DB_USERNAME:postgres}",
"DB_DATABASE_NAME": "${localEnv:DB_DATABASE_NAME:immich}"
}
```
The `${localEnv:VARIABLE:default}` syntax reads from your shell environment with optional defaults.
### Upload Location Path Resolution
The `UPLOAD_LOCATION` environment variable controls where files are stored:
**Default:** `./Library` (relative to the `docker` directory)
**Resolved to:** `<immich-root>/docker/Library`
**Bind Mounts Created:**
```yaml
# From .devcontainer/server/container-compose-overrides.yml
- ${UPLOAD_LOCATION-./Library}/photos:/workspaces/immich/server/upload
- ${UPLOAD_LOCATION-./Library}/postgres:/var/lib/postgresql/data
```
### Database Configuration
These variables have sensible defaults (for development) but can be customized:
| Variable | Default | Description |
| ------------------ | ---------- | ------------------- |
| `DB_PASSWORD` | `postgres` | PostgreSQL password |
| `DB_USERNAME` | `postgres` | PostgreSQL username |
| `DB_DATABASE_NAME` | `immich` | Database name |
### Setting Environment Variables
Add these to your shell profile (`~/.bashrc`, `~/.zshrc`, `~/.bash_profile`, etc.):
```bash
# Required
export UPLOAD_LOCATION=./Library # or absolute path
# Optional (only if using non-default values)
export DB_PASSWORD=your_password
export DB_USERNAME=your_username
export DB_DATABASE_NAME=your_database
```
Remember to reload your shell configuration:
```bash
source ~/.bashrc # or ~/.zshrc, etc.
```
## Git Configuration
### SSH Keys and Authentication
To use your SSH keys for GitHub access inside the Dev Container:
1. **Start SSH Agent** on your host machine:
```bash
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_rsa # or your key path
```
2. **VS Code automatically forwards your SSH agent** to the container
For detailed instructions, see the [VS Code guide on sharing Git credentials](https://code.visualstudio.com/remote/advancedcontainers/sharing-git-credentials).
### Commit Signing
To use your SSH key for commit signing, see the [GitHub guide on SSH commit signing](https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key#telling-git-about-your-ssh-key).
## Development Workflow
### Automatic Setup
When the Dev Container starts, it automatically:
1. **Runs post-create script** (`container-server-post-create.sh`):
- Adjusts file permissions for the `node` user
- Installs dependencies: `npm install` in all packages
- Builds TypeScript SDK: `npm run build` in `open-api/typescript-sdk`
2. **Starts development servers** via VS Code tasks:
- `Immich API Server (Nest)` - API server with hot-reloading on port 2283
- `Immich Web Server (Vite)` - Web frontend with hot-reloading on port 3000
- Both servers watch for file changes and recompile automatically
3. **Configures port forwarding**:
- Web UI: http://localhost:3000 (opens automatically)
- API: http://localhost:2283
- Debug ports: 9230 (workers), 9231 (API)
:::info
The Dev Container setup replaces the `make dev` command from the traditional setup. All services start automatically when you open the container.
:::
### Accessing Services
Once running, you can access:
| Service | URL | Description |
| -------- | --------------------- | ---------------------------------------------------------------------------------------------- |
| Web UI | http://localhost:3000 | Main web interface |
| API | http://localhost:2283 | REST API endpoints (Not used directly, web UI will expose this over http://localhost:3000/api) |
| Database | localhost:5432 | PostgreSQL (username: `postgres`) (Not used directly) |
### Connecting Mobile Apps
To connect the mobile app to your Dev Container:
1. Find your machine's IP address
2. In the mobile app, use: `http://YOUR_IP:3000/api`
3. Ensure your firewall allows connections on port 2283
### Making Code Changes
- **Server code** (`/server`): Changes trigger automatic restart
- **Web code** (`/web`): Changes trigger hot module replacement
- **Database migrations**: Run `npm run sync:sql` in the server directory
- **API changes**: Regenerate TypeScript SDK with `make open-api`
## Testing
### Running Tests
The Dev Container supports multiple ways to run tests:
#### Using Make Commands (Recommended)
```bash
# Run tests for specific components
make test-server # Server unit tests
make test-web # Web unit tests
make test-e2e # End-to-end tests
make test-cli # CLI tests
# Run all tests
make test-all # Runs tests for all components
# Medium tests (integration tests)
make test-medium-dev # End-to-end tests
```
#### Using NPM Directly
```bash
# Server tests
cd /workspaces/immich/server
npm test # Run all tests
npm run test:watch # Watch mode
npm run test:cov # Coverage report
# Web tests
cd /workspaces/immich/web
npm test # Run all tests
npm run test:watch # Watch mode
# E2E tests
cd /workspaces/immich/e2e
npm run test # Run API tests
npm run test:web # Run web UI tests
```
### Code Quality Commands
```bash
# Linting
make lint-server # Lint server code
make lint-web # Lint web code
make lint-all # Lint all components
# Formatting
make format-server # Format server code
make format-web # Format web code
make format-all # Format all code
# Type checking
make check-server # Type check server
make check-web # Type check web
make check-all # Check all components
# Complete hygiene check
make hygiene-all # Runs lint, format, check, SQL sync, and audit
```
### Additional Make Commands
```bash
# Build commands
make build-server # Build server
make build-web # Build web app
make build-all # Build everything
# API generation
make open-api # Generate OpenAPI specs
make open-api-typescript # Generate TypeScript SDK
make open-api-dart # Generate Dart SDK
# Database
make sql # Sync database schema
# Dependencies
make install-server # Install server dependencies
make install-web # Install web dependencies
make install-all # Install all dependencies
```
### Debugging
The Dev Container is pre-configured for debugging:
1. **API Server Debugging**:
- Set breakpoints in VS Code
- Press `F5` or use "Run and Debug" panel
- Select "Attach to Server" configuration
- Debug port: 9231
2. **Worker Debugging**:
- Use "Attach to Workers" configuration
- Debug port: 9230
3. **Web Debugging**:
- Use browser DevTools
- VS Code debugger for Chrome/Edge extensions supported
## Troubleshooting
### Common Issues
#### Permission Errors
**Problem**: `EACCES` or permission denied errors
**Solution**:
- The Dev Container runs as the `node` user (UID 1000)
- If your host UID differs, you may see permission issues
- Try rebuilding the container: "Dev Containers: Rebuild Container"
#### Container Won't Start
**Problem**: Dev Container fails to start or build
**Solution**:
1. Check Docker is running: `docker ps`
2. Clean Docker resources: `docker system prune -a`
3. Check available disk space
4. Review Docker Desktop resource limits
#### Port Already in Use
**Problem**: "Port 3000/2283 is already in use"
**Solution**:
1. Check for conflicting services: `lsof -i :3000` (macOS/Linux)
2. Stop conflicting services or change port mappings
3. Restart Docker Desktop
#### Upload Location Not Set
**Problem**: Errors about missing UPLOAD_LOCATION
**Solution**:
1. Set the environment variable: `export UPLOAD_LOCATION=./Library`
2. Add to your shell profile for persistence
3. Restart your terminal and VS Code
#### Database Connection Failed
**Problem**: Cannot connect to PostgreSQL
**Solution**:
1. Ensure all containers are running: `docker ps`
2. Check logs: "Dev Containers: Show Container Log"
3. Verify database credentials match environment variables
### Getting Help
If you encounter issues:
1. Check container logs: View → Output → Select "Dev Containers"
2. Rebuild without cache: "Dev Containers: Rebuild Container Without Cache"
3. Review [common Docker issues](https://docs.docker.com/desktop/troubleshoot/)
4. Ask in [Discord](https://discord.immich.app) `#help-desk-support` channel
## Mobile Development
While the Dev Container focuses on server and web development, you can connect mobile apps for testing:
### Connecting iOS/Android Apps
1. **Ensure API is accessible**:
```bash
# Find your machine's IP
# macOS
ipconfig getifaddr en0
# Linux
hostname -I
# Windows (in WSL2)
ip addr show eth0
```
2. **Configure mobile app**:
- Server URL: `http://YOUR_IP:2283/api`
- Ensure firewall allows port 2283
3. **For full mobile development**, see the [mobile development guide](/docs/developer/setup) which covers:
- Flutter setup
- Running on simulators/devices
- Mobile-specific debugging
## Advanced Configuration
### Custom VS Code Extensions
Add extensions to `.devcontainer/devcontainer.json`:
```json
"customizations": {
"vscode": {
"extensions": [
"your.extension-id"
]
}
}
```
### Additional Services
To add services (e.g., Redis Commander), modify:
1. `/docker/docker-compose.dev.yml` - Add service definition
2. `/.devcontainer/server/container-compose-overrides.yml` - Add overrides if needed
### Resource Limits
Adjust Docker Desktop resources:
- **macOS/Windows**: Docker Desktop → Settings → Resources
- **Linux**: Modify Docker daemon configuration
Recommended minimums:
- CPU: 4 cores
- Memory: 8GB
- Disk: 20GB
## Next Steps
- Read the [architecture overview](/docs/developer/architecture)
- Learn about [database migrations](/docs/developer/database-migrations)
- Explore [API documentation](/docs/api)
- Join `#immich` on [Discord](https://discord.immich.app)

View File

@@ -2,7 +2,7 @@
Folder view provides an additional view besides the timeline that is similar to a file explorer. It allows you to navigate through the folders and files in the library. This feature is handy for a highly curated and customized external library or a nicely configured storage template.
You can enable this feature under [`Account Settings > Features > Folder View`](https://my.immich.app/user-settings?isOpen=feature+folders)
You can enable this feature under [`Account Settings > Features > Folders`](https://my.immich.app/user-settings?isOpen=feature+folders)
## Enable folder view

View File

@@ -56,7 +56,7 @@ Internally, Immich uses the [glob](https://www.npmjs.com/package/glob) package t
### Automatic watching (EXPERIMENTAL)
This feature - currently hidden in the config file - is considered experimental and for advanced users only. If enabled, it will allow automatic watching of the filesystem which means new assets are automatically imported to Immich without needing to rescan.
This feature is considered experimental and for advanced users only. If enabled, it will allow automatic watching of the filesystem which means new assets are automatically imported to Immich without needing to rescan.
If your photos are on a network drive, automatic file watching likely won't work. In that case, you will have to rely on a periodic library refresh to pull in your changes.
@@ -112,12 +112,15 @@ _Remember to run `docker compose up -d` to register the changes. Make sure you c
These actions must be performed by the Immich administrator.
- Click on Administration -> Libraries
- Click on Create External Library
- Click on your avatar in the upper right corner
- Click on Administration -> External Libraries
- Click on Create an external library…
- Select which user owns the library, this can not be changed later
- Enter `/mnt/media/christmas-trip` then click Add
- Click on Save
- Click the drop-down menu on the newly created library
- Click on Scan
- Click the drop-down menu on the newly created library
- Click on Rename Library and rename it to "Christmas Trip"
NOTE: We have to use the `/mnt/media/christmas-trip` path and not the `/mnt/nas/christmas-trip` path since all paths have to be what the Docker containers see.
@@ -156,9 +159,7 @@ Within seconds, the assets from the old-pics and videos folders should show up i
Folder view provides an additional view besides the timeline that is similar to a file explorer. It allows you to navigate through the folders and files in the library. This feature is handy for a highly curated and customized external library or a nicely configured storage template.
You can enable this feature under [`Account Settings > Features > Folder View`](https://my.immich.app/user-settings?isOpen=feature+folders)
The UI is currently only available for the web; mobile will come in a subsequent release.
You can enable this feature under [`Account Settings > Features > Folders`](https://my.immich.app/user-settings?isOpen=feature+folders)
<img src={require('./img/folder-view-1.webp').default} width="100%" title='Folder-view' />
@@ -168,7 +169,7 @@ The UI is currently only available for the web; mobile will come in a subsequent
Only an admin can do this.
:::
You can define a custom interval for the trigger external library rescan under Administration -> Settings -> Library.
You can define a custom interval for the trigger external library rescan under Administration -> Settings -> External Library.
You can set the scanning interval using the preset or cron format. For more information you can refer to [Crontab Guru](https://crontab.guru/).
<img src={require('./img/library-custom-scan-interval.webp').default} width="75%" title='Set custom scan interval for external library' />

View File

@@ -16,7 +16,7 @@ For the full list, refer to the [Immich source code](https://github.com/immich-a
| `HEIC` | `.heic` | :white_check_mark: | |
| `HEIF` | `.heif` | :white_check_mark: | |
| `JPEG 2000` | `.jp2` | :white_check_mark: | |
| `JPEG` | `.webp` `.jpg` `.jpe` `.insp` | :white_check_mark: | |
| `JPEG` | `.jpeg` `.jpg` `.jpe` `.insp` | :white_check_mark: | |
| `JPEG XL` | `.jxl` | :white_check_mark: | |
| `PNG` | `.png` | :white_check_mark: | |
| `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop |

View File

@@ -41,7 +41,7 @@ In the Immich web UI:
- Click Add path
<img src={require('./img/add-path-button.webp').default} width="50%" title="Add Path button" />
- Enter **/usr/src/app/external** as the path and click Add
- Enter **/home/user/photos1** as the path and click Add
<img src={require('./img/add-path-field.webp').default} width="50%" title="Add Path field" />
- Save the new path

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -52,9 +52,9 @@ REMOTE_BACKUP_PATH="/path/to/remote/backup/directory"
### Local
# Backup Immich database
docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgres > "$UPLOAD_LOCATION"/database-backup/immich-database.sql
docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=<DB_USERNAME> > "$UPLOAD_LOCATION"/database-backup/immich-database.sql
# For deduplicating backup programs such as Borg or Restic, compressing the content can increase backup size by making it harder to deduplicate. If you are using a different program or still prefer to compress, you can use the following command instead:
# docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgres | /usr/bin/gzip --rsyncable > "$UPLOAD_LOCATION"/database-backup/immich-database.sql.gz
# docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=<DB_USERNAME> | /usr/bin/gzip --rsyncable > "$UPLOAD_LOCATION"/database-backup/immich-database.sql.gz
### Append to local Borg repository
borg create "$BACKUP_PATH/immich-borg::{now}" "$UPLOAD_LOCATION" --exclude "$UPLOAD_LOCATION"/thumbs/ --exclude "$UPLOAD_LOCATION"/encoded-video/

View File

@@ -123,7 +123,7 @@ The default configuration looks like this:
"buttonText": "Login with OAuth",
"clientId": "",
"clientSecret": "",
"defaultStorageQuota": 0,
"defaultStorageQuota": null,
"enabled": false,
"issuerUrl": "",
"mobileOverrideEnabled": false,

View File

@@ -73,7 +73,7 @@ Information on the current workers can be found [here](/docs/administration/jobs
## Database
| Variable | Description | Default | Containers |
| :---------------------------------- | :--------------------------------------------------------------------------- | :--------: | :----------------------------- |
| :---------------------------------- | :------------------------------------------------------------------------------------- | :--------: | :----------------------------- |
| `DB_URL` | Database URL | | server |
| `DB_HOSTNAME` | Database host | `database` | server |
| `DB_PORT` | Database port | `5432` | server |
@@ -83,11 +83,14 @@ Information on the current workers can be found [here](/docs/administration/jobs
| `DB_SSL_MODE` | Database SSL mode | | server |
| `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database vector extension (one of [`vectorchord`, `pgvector`, `pgvecto.rs`]) | | server |
| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server |
| `DB_STORAGE_TYPE` | Optimize concurrent IO on SSDs or sequential IO on HDDs ([`SSD`, `HDD`])<sup>\*3</sup> | `SSD` | server |
\*1: The values of `DB_USERNAME`, `DB_PASSWORD`, and `DB_DATABASE_NAME` are passed to the Postgres container as the variables `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB` in `docker-compose.yml`.
\*2: If not provided, the appropriate extension to use is auto-detected at startup by introspecting the database. When multiple extensions are installed, the order of preference is VectorChord, pgvecto.rs, pgvector.
\*3: Uses either [`postgresql.ssd.conf`](https://github.com/immich-app/base-images/blob/main/postgres/postgresql.ssd.conf) or [`postgresql.hdd.conf`](https://github.com/immich-app/base-images/blob/main/postgres/postgresql.hdd.conf) which mainly controls the Postgres `effective_io_concurrency` setting to allow for concurrenct IO on SSDs and sequential IO on HDDs.
:::info
All `DB_` variables must be provided to all Immich workers, including `api` and `microservices`.

View File

@@ -39,8 +39,8 @@ alt="Dot Env Example"
/>
- Change the default `DB_PASSWORD`, and add custom database connection information if necessary.
- Change `DB_DATA_LOCATION` to a folder where the database will be saved to disk.
- Change `UPLOAD_LOCATION` to a folder where media (uploaded and generated) will be stored.
- Change `DB_DATA_LOCATION` to a folder (absolute path) where the database will be saved to disk.
- Change `UPLOAD_LOCATION` to a folder (absolute path) where media (uploaded and generated) will be stored.
11. Click on "**Deploy the stack**".

View File

@@ -25,7 +25,7 @@ When you're all done, you should have the following:
- `./docker/immich-app/postgres`
- `./docker/immich-app/library`
Download [`docker-compose.yml`](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml) and [`example.env`](https://github.com/immich-app/immich/releases/latest/download/example.env) to your computer. Upload the files to the `./docker/immich-app` directory.
Download [`docker-compose.yml`](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml) and [`example.env`](https://github.com/immich-app/immich/releases/latest/download/example.env) to your computer. Upload the files to the `./docker/immich-app` directory, and rename `example.env` to `.env`.
## Step 2 - Populate the .env file with custom values

View File

@@ -9,7 +9,7 @@ This is a community contribution and not officially supported by the Immich team
Community support can be found in the dedicated channel on the [Discord Server](https://discord.immich.app/).
**Please report app issues to the corresponding [Github Repository](https://github.com/truenas/charts/tree/master/community/immich).**
**Please report app issues to the corresponding [Github Repository](https://github.com/truenas/apps/tree/master/trains/community/immich).**
:::
Immich can easily be installed on TrueNAS Community Edition via the **Community** train application.

View File

@@ -75,7 +75,6 @@ alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich"
5. Click "**Save Changes**", you will be prompted to edit stack UI labels, just leave this blank and click "**Ok**"
6. Select the cog ⚙️ next to Immich, click "**Edit Stack**", then click "**Env File**"
7. Paste the entire contents of the [Immich example.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file into the Unraid editor, then **before saving** edit the following:
- `UPLOAD_LOCATION`: Create a folder in your Images Unraid share and place the **absolute** location here > For example my _"images"_ share has a folder within it called _"immich"_. If I browse to this directory in the terminal and type `pwd` the output is `/mnt/user/images/immich`. This is the exact value I need to enter as my `UPLOAD_LOCATION`
- `DB_DATA_LOCATION`: Change this to use an Unraid share (preferably a cache pool, e.g. `/mnt/user/appdata/postgresql/data`). This uses the `appdata` share. Do also create the `postgresql` folder, by running `mkdir /mnt/user/{share_location}/postgresql/data`. If left at default it will try to use Unraid's `/boot/config/plugins/compose.manager/projects/[stack_name]/postgres` folder which it doesn't have permissions to, resulting in this container continuously restarting.

1533
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -16,8 +16,9 @@
"write-heading-ids": "docusaurus write-heading-ids"
},
"dependencies": {
"@docusaurus/core": "~3.7.0",
"@docusaurus/preset-classic": "~3.7.0",
"@docusaurus/core": "~3.8.0",
"@docusaurus/preset-classic": "~3.8.0",
"@docusaurus/theme-common": "~3.8.0",
"@mdi/js": "^7.3.67",
"@mdi/react": "^1.6.1",
"@mdx-js/react": "^3.0.0",
@@ -26,6 +27,7 @@
"clsx": "^2.0.0",
"docusaurus-lunr-search": "^3.3.2",
"docusaurus-preset-openapi": "^0.7.5",
"lunr": "^2.3.9",
"postcss": "^8.4.25",
"prism-react-renderer": "^2.3.1",
"raw-loader": "^4.0.2",
@@ -35,7 +37,7 @@
"url": "^0.11.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "~3.7.0",
"@docusaurus/module-type-aliases": "~3.8.0",
"@docusaurus/tsconfig": "^3.7.0",
"@docusaurus/types": "^3.7.0",
"prettier": "^3.2.4",
@@ -57,6 +59,6 @@
"node": ">=20"
},
"volta": {
"node": "22.16.0"
"node": "22.17.0"
}
}

View File

@@ -58,6 +58,12 @@ const guides: CommunityGuidesProps[] = [
description: 'Access Immich with an end-to-end encrypted connection.',
url: 'https://meshnet.nordvpn.com/how-to/remote-files-media-access/immich-remote-access',
},
{
title: 'Trust Self Signed Certificates with Immich - OAuth Setup',
description:
'Set up Certificate Authority trust with Immich, and your private OAuth2/OpenID service, while using a private CA for HTTPS commication.',
url: 'https://github.com/immich-app/immich/discussions/18614',
},
];
function CommunityGuide({ title, description, url }: CommunityGuidesProps): JSX.Element {

View File

@@ -85,6 +85,7 @@ import React from 'react';
import { Item, Timeline } from '../components/timeline';
const releases = {
'v1.135.0': new Date(2025, 5, 18),
'v1.133.0': new Date(2025, 4, 21),
'v1.130.0': new Date(2025, 2, 25),
'v1.127.0': new Date(2025, 1, 26),
@@ -196,14 +197,6 @@ const roadmap: Item[] = [
description: 'Automate tasks with workflows',
getDateLabel: () => 'Planned for 2025',
},
{
done: false,
icon: mdiTableKey,
iconColor: 'gray',
title: 'Fine grained access controls',
description: 'Granular access controls for users and api keys',
getDateLabel: () => 'Planned for 2025',
},
{
done: false,
icon: mdiImageEdit,
@@ -239,12 +232,26 @@ const roadmap: Item[] = [
];
const milestones: Item[] = [
{
icon: mdiStar,
iconColor: 'gold',
title: '70,000 Stars',
description: 'Reached 70K Stars on GitHub!',
getDateLabel: withLanguage(new Date(2025, 6, 9)),
},
withRelease({
icon: mdiTableKey,
iconColor: 'gray',
title: 'Fine grained access controls',
description: 'Granular access controls for api keys',
release: 'v1.135.0',
}),
withRelease({
icon: mdiCast,
iconColor: 'aqua',
title: 'Google Cast (web)',
title: 'Google Cast (web and mobile)',
description: 'Cast assets to Google Cast/Chromecast compatible devices',
release: 'v1.133.0',
release: 'v1.135.0',
}),
withRelease({
icon: mdiLockOutline,

View File

@@ -1,4 +1,5 @@
/docs /docs/overview/introduction 307
/docs /docs/overview/welcome 307
/docs/ /docs/overview/welcome 307
/docs/mobile-app-beta-program /docs/features/mobile-app 307
/docs/contribution-guidelines /docs/overview/support-the-project#contributing 307
/docs/install /docs/install/docker-compose 307

View File

@@ -1,4 +1,20 @@
[
{
"label": "v1.135.3",
"url": "https://v1.135.3.archive.immich.app"
},
{
"label": "v1.135.2",
"url": "https://v1.135.2.archive.immich.app"
},
{
"label": "v1.135.1",
"url": "https://v1.135.1.archive.immich.app"
},
{
"label": "v1.135.0",
"url": "https://v1.135.0.archive.immich.app"
},
{
"label": "v1.134.0",
"url": "https://v1.134.0.archive.immich.app"

View File

@@ -1 +1 @@
22.16.0
22.17.0

View File

@@ -36,10 +36,10 @@ services:
- 2285:2285
redis:
image: redis:6.2-alpine@sha256:3211c33a618c457e5d241922c975dbc4f446d0bdb2dc75694f5573ef8e2d01fa
image: redis:6.2-alpine@sha256:03fd052257735b41cd19f3d8ae9782926bf9b704fb6a9dc5e29f9ccfbe8827f0
database:
image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:9c704fb49ce27549df00f1b096cc93f8b0c959ef087507704d74954808f78a82
image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:3aef84a0a4fabbda17ef115c3019ba0c914ec73e9f6e59203674322d858b8eea
command: -c fsync=off -c shared_preload_libraries=vchord.so -c config_file=/var/lib/postgresql/data/postgresql.conf
environment:
POSTGRES_PASSWORD: postgres

1232
e2e/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "1.134.0",
"version": "1.135.3",
"description": "",
"main": "index.js",
"type": "module",
@@ -24,8 +24,9 @@
"@immich/cli": "file:../cli",
"@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1",
"@socket.io/component-emitter": "^3.1.2",
"@types/luxon": "^3.4.2",
"@types/node": "^22.15.29",
"@types/node": "^22.15.33",
"@types/oidc-provider": "^9.0.0",
"@types/pg": "^8.15.1",
"@types/pngjs": "^6.0.4",
@@ -44,6 +45,7 @@
"pngjs": "^7.0.0",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^4.0.0",
"sharp": "^0.34.0",
"socket.io-client": "^4.7.4",
"supertest": "^7.0.0",
"typescript": "^5.3.3",
@@ -52,6 +54,6 @@
"vitest": "^3.0.0"
},
"volta": {
"node": "22.16.0"
"node": "22.17.0"
}
}

View File

@@ -7,6 +7,7 @@ import {
ReactionType,
createActivity as create,
createAlbum,
removeAssetFromAlbum,
} from '@immich/sdk';
import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
@@ -342,5 +343,36 @@ describe('/activities', () => {
expect(status).toBe(204);
});
it('should return empty list when asset is removed', async () => {
const album3 = await createAlbum(
{
createAlbumDto: {
albumName: 'Album 3',
assetIds: [asset.id],
},
},
{ headers: asBearerAuth(admin.accessToken) },
);
await createActivity({ albumId: album3.id, assetId: asset.id, type: ReactionType.Like });
await removeAssetFromAlbum(
{
id: album3.id,
bulkIdsDto: {
ids: [asset.id],
},
},
{ headers: asBearerAuth(admin.accessToken) },
);
const { status, body } = await request(app)
.get('/activities')
.query({ albumId: album.id })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(200);
expect(body).toEqual([]);
});
});
});

View File

@@ -20,7 +20,7 @@ describe('/api-keys', () => {
});
beforeEach(async () => {
await utils.resetDatabase(['api_keys']);
await utils.resetDatabase(['api_key']);
});
describe('POST /api-keys', () => {

View File

@@ -15,6 +15,7 @@ import { DateTime } from 'luxon';
import { randomBytes } from 'node:crypto';
import { readFile, writeFile } from 'node:fs/promises';
import { basename, join } from 'node:path';
import sharp from 'sharp';
import { Socket } from 'socket.io-client';
import { createUserDto, uuidDto } from 'src/fixtures';
import { makeRandomImage } from 'src/generators';
@@ -40,6 +41,40 @@ const today = DateTime.fromObject({
}) as DateTime<true>;
const yesterday = today.minus({ days: 1 });
const createTestImageWithExif = async (filename: string, exifData: Record<string, any>) => {
// Generate unique color to ensure different checksums for each image
const r = Math.floor(Math.random() * 256);
const g = Math.floor(Math.random() * 256);
const b = Math.floor(Math.random() * 256);
// Create a 100x100 solid color JPEG using Sharp
const imageBytes = await sharp({
create: {
width: 100,
height: 100,
channels: 3,
background: { r, g, b },
},
})
.jpeg({ quality: 90 })
.toBuffer();
// Add random suffix to filename to avoid collisions
const uniqueFilename = filename.replace('.jpg', `-${randomBytes(4).toString('hex')}.jpg`);
const filepath = join(tempDir, uniqueFilename);
await writeFile(filepath, imageBytes);
// Filter out undefined values before writing EXIF
const cleanExifData = Object.fromEntries(Object.entries(exifData).filter(([, value]) => value !== undefined));
await exiftool.write(filepath, cleanExifData);
// Re-read the image bytes after EXIF has been written
const finalImageBytes = await readFile(filepath);
return { filepath, imageBytes: finalImageBytes, filename: uniqueFilename };
};
describe('/asset', () => {
let admin: LoginResponseDto;
let websocket: Socket;
@@ -1190,6 +1225,411 @@ describe('/asset', () => {
});
});
describe('EXIF metadata extraction', () => {
describe('Additional date tag extraction', () => {
describe('Date-time vs time-only tag handling', () => {
it('should fall back to file timestamps when only time-only tags are available', async () => {
const { imageBytes, filename } = await createTestImageWithExif('time-only-fallback.jpg', {
TimeCreated: '2023:11:15 14:30:00', // Time-only tag, should not be used for dateTimeOriginal
// Exclude all date-time tags to force fallback to file timestamps
SubSecDateTimeOriginal: undefined,
DateTimeOriginal: undefined,
SubSecCreateDate: undefined,
SubSecMediaCreateDate: undefined,
CreateDate: undefined,
MediaCreateDate: undefined,
CreationDate: undefined,
DateTimeCreated: undefined,
GPSDateTime: undefined,
DateTimeUTC: undefined,
SonyDateTime2: undefined,
GPSDateStamp: undefined,
});
const oldDate = new Date('2020-01-01T00:00:00.000Z');
const asset = await utils.createAsset(admin.accessToken, {
assetData: {
filename,
bytes: imageBytes,
},
fileCreatedAt: oldDate.toISOString(),
fileModifiedAt: oldDate.toISOString(),
});
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id });
const assetInfo = await getAssetInfo({ id: asset.id }, { headers: asBearerAuth(admin.accessToken) });
expect(assetInfo.exifInfo?.dateTimeOriginal).toBeDefined();
// Should fall back to file timestamps, which we set to 2020-01-01
expect(new Date(assetInfo.exifInfo!.dateTimeOriginal!).getTime()).toBe(
new Date('2020-01-01T00:00:00.000Z').getTime(),
);
});
it('should prefer DateTimeOriginal over time-only tags', async () => {
const { imageBytes, filename } = await createTestImageWithExif('datetime-over-time.jpg', {
DateTimeOriginal: '2023:10:10 10:00:00', // Should be preferred
TimeCreated: '2023:11:15 14:30:00', // Should be ignored (time-only)
});
const asset = await utils.createAsset(admin.accessToken, {
assetData: {
filename,
bytes: imageBytes,
},
});
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id });
const assetInfo = await getAssetInfo({ id: asset.id }, { headers: asBearerAuth(admin.accessToken) });
expect(assetInfo.exifInfo?.dateTimeOriginal).toBeDefined();
// Should use DateTimeOriginal, not TimeCreated
expect(new Date(assetInfo.exifInfo!.dateTimeOriginal!).getTime()).toBe(
new Date('2023-10-10T10:00:00.000Z').getTime(),
);
});
});
describe('GPSDateTime tag extraction', () => {
it('should extract GPSDateTime with GPS coordinates', async () => {
const { imageBytes, filename } = await createTestImageWithExif('gps-datetime.jpg', {
GPSDateTime: '2023:11:15 12:30:00Z',
GPSLatitude: 37.7749,
GPSLongitude: -122.4194,
// Exclude other date tags
SubSecDateTimeOriginal: undefined,
DateTimeOriginal: undefined,
SubSecCreateDate: undefined,
SubSecMediaCreateDate: undefined,
CreateDate: undefined,
MediaCreateDate: undefined,
CreationDate: undefined,
DateTimeCreated: undefined,
TimeCreated: undefined,
});
const asset = await utils.createAsset(admin.accessToken, {
assetData: {
filename,
bytes: imageBytes,
},
});
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id });
const assetInfo = await getAssetInfo({ id: asset.id }, { headers: asBearerAuth(admin.accessToken) });
expect(assetInfo.exifInfo?.dateTimeOriginal).toBeDefined();
expect(assetInfo.exifInfo?.latitude).toBeCloseTo(37.7749, 4);
expect(assetInfo.exifInfo?.longitude).toBeCloseTo(-122.4194, 4);
expect(new Date(assetInfo.exifInfo!.dateTimeOriginal!).getTime()).toBe(
new Date('2023-11-15T12:30:00.000Z').getTime(),
);
});
});
describe('CreateDate tag extraction', () => {
it('should extract CreateDate when available', async () => {
const { imageBytes, filename } = await createTestImageWithExif('create-date.jpg', {
CreateDate: '2023:11:15 10:30:00',
// Exclude other higher priority date tags
SubSecDateTimeOriginal: undefined,
DateTimeOriginal: undefined,
SubSecCreateDate: undefined,
SubSecMediaCreateDate: undefined,
MediaCreateDate: undefined,
CreationDate: undefined,
DateTimeCreated: undefined,
TimeCreated: undefined,
GPSDateTime: undefined,
});
const asset = await utils.createAsset(admin.accessToken, {
assetData: {
filename,
bytes: imageBytes,
},
});
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id });
const assetInfo = await getAssetInfo({ id: asset.id }, { headers: asBearerAuth(admin.accessToken) });
expect(assetInfo.exifInfo?.dateTimeOriginal).toBeDefined();
expect(new Date(assetInfo.exifInfo!.dateTimeOriginal!).getTime()).toBe(
new Date('2023-11-15T10:30:00.000Z').getTime(),
);
});
});
describe('GPSDateStamp tag extraction', () => {
it('should fall back to file timestamps when only date-only tags are available', async () => {
const { imageBytes, filename } = await createTestImageWithExif('gps-datestamp.jpg', {
GPSDateStamp: '2023:11:15', // Date-only tag, should not be used for dateTimeOriginal
// Note: NOT including GPSTimeStamp to avoid automatic GPSDateTime creation
GPSLatitude: 51.5074,
GPSLongitude: -0.1278,
// Explicitly exclude all testable date-time tags to force fallback to file timestamps
DateTimeOriginal: undefined,
CreateDate: undefined,
CreationDate: undefined,
GPSDateTime: undefined,
});
const oldDate = new Date('2020-01-01T00:00:00.000Z');
const asset = await utils.createAsset(admin.accessToken, {
assetData: {
filename,
bytes: imageBytes,
},
fileCreatedAt: oldDate.toISOString(),
fileModifiedAt: oldDate.toISOString(),
});
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id });
const assetInfo = await getAssetInfo({ id: asset.id }, { headers: asBearerAuth(admin.accessToken) });
expect(assetInfo.exifInfo?.dateTimeOriginal).toBeDefined();
expect(assetInfo.exifInfo?.latitude).toBeCloseTo(51.5074, 4);
expect(assetInfo.exifInfo?.longitude).toBeCloseTo(-0.1278, 4);
// Should fall back to file timestamps, which we set to 2020-01-01
expect(new Date(assetInfo.exifInfo!.dateTimeOriginal!).getTime()).toBe(
new Date('2020-01-01T00:00:00.000Z').getTime(),
);
});
});
/*
* NOTE: The following EXIF date tags are NOT effectively usable with JPEG test files:
*
* NOT WRITABLE to JPEG:
* - MediaCreateDate: Can be read from video files but not written to JPEG
* - DateTimeCreated: Read-only tag in JPEG format
* - DateTimeUTC: Cannot be written to JPEG files
* - SonyDateTime2: Proprietary Sony tag, not writable to JPEG
* - SubSecMediaCreateDate: Tag not defined for JPEG format
* - SourceImageCreateTime: Non-standard insta360 tag, not writable to JPEG
*
* WRITABLE but NOT READABLE from JPEG:
* - SubSecDateTimeOriginal: Can be written but not read back from JPEG
* - SubSecCreateDate: Can be written but not read back from JPEG
*
* EFFECTIVELY TESTABLE TAGS (writable and readable):
* - DateTimeOriginal ✓
* - CreateDate ✓
* - CreationDate ✓
* - GPSDateTime ✓
*
* The metadata service correctly handles non-readable tags and will fall back to
* file timestamps when only non-readable tags are present.
*/
describe('Date tag priority order', () => {
it('should respect the complete date tag priority order', async () => {
// Test cases using only EFFECTIVELY TESTABLE tags (writable AND readable from JPEG)
const testCases = [
{
name: 'DateTimeOriginal has highest priority among testable tags',
exifData: {
DateTimeOriginal: '2023:04:04 04:00:00', // TESTABLE - highest priority among readable tags
CreateDate: '2023:05:05 05:00:00', // TESTABLE
CreationDate: '2023:07:07 07:00:00', // TESTABLE
GPSDateTime: '2023:10:10 10:00:00', // TESTABLE
},
expectedDate: '2023-04-04T04:00:00.000Z',
},
{
name: 'CreateDate when DateTimeOriginal missing',
exifData: {
CreateDate: '2023:05:05 05:00:00', // TESTABLE
CreationDate: '2023:07:07 07:00:00', // TESTABLE
GPSDateTime: '2023:10:10 10:00:00', // TESTABLE
},
expectedDate: '2023-05-05T05:00:00.000Z',
},
{
name: 'CreationDate when standard EXIF tags missing',
exifData: {
CreationDate: '2023:07:07 07:00:00', // TESTABLE
GPSDateTime: '2023:10:10 10:00:00', // TESTABLE
},
expectedDate: '2023-07-07T07:00:00.000Z',
},
{
name: 'GPSDateTime when no other testable date tags present',
exifData: {
GPSDateTime: '2023:10:10 10:00:00', // TESTABLE
Make: 'SONY',
},
expectedDate: '2023-10-10T10:00:00.000Z',
},
];
for (const testCase of testCases) {
const { imageBytes, filename } = await createTestImageWithExif(
`${testCase.name.replaceAll(/\s+/g, '-').toLowerCase()}.jpg`,
testCase.exifData,
);
const asset = await utils.createAsset(admin.accessToken, {
assetData: {
filename,
bytes: imageBytes,
},
});
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id });
const assetInfo = await getAssetInfo({ id: asset.id }, { headers: asBearerAuth(admin.accessToken) });
expect(assetInfo.exifInfo?.dateTimeOriginal, `Failed for: ${testCase.name}`).toBeDefined();
expect(
new Date(assetInfo.exifInfo!.dateTimeOriginal!).getTime(),
`Date mismatch for: ${testCase.name}`,
).toBe(new Date(testCase.expectedDate).getTime());
}
});
});
describe('Edge cases for date tag handling', () => {
it('should fall back to file timestamps with GPSDateStamp alone', async () => {
const { imageBytes, filename } = await createTestImageWithExif('gps-datestamp-only.jpg', {
GPSDateStamp: '2023:08:08', // Date-only tag, should not be used for dateTimeOriginal
// Intentionally no GPSTimeStamp
// Exclude all other date tags
SubSecDateTimeOriginal: undefined,
DateTimeOriginal: undefined,
SubSecCreateDate: undefined,
SubSecMediaCreateDate: undefined,
CreateDate: undefined,
MediaCreateDate: undefined,
CreationDate: undefined,
DateTimeCreated: undefined,
TimeCreated: undefined,
GPSDateTime: undefined,
DateTimeUTC: undefined,
});
const oldDate = new Date('2020-01-01T00:00:00.000Z');
const asset = await utils.createAsset(admin.accessToken, {
assetData: {
filename,
bytes: imageBytes,
},
fileCreatedAt: oldDate.toISOString(),
fileModifiedAt: oldDate.toISOString(),
});
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id });
const assetInfo = await getAssetInfo({ id: asset.id }, { headers: asBearerAuth(admin.accessToken) });
expect(assetInfo.exifInfo?.dateTimeOriginal).toBeDefined();
// Should fall back to file timestamps, which we set to 2020-01-01
expect(new Date(assetInfo.exifInfo!.dateTimeOriginal!).getTime()).toBe(
new Date('2020-01-01T00:00:00.000Z').getTime(),
);
});
it('should handle all testable date tags present to verify complete priority order', async () => {
const { imageBytes, filename } = await createTestImageWithExif('all-testable-date-tags.jpg', {
// All TESTABLE date tags to JPEG format (writable AND readable)
DateTimeOriginal: '2023:04:04 04:00:00', // TESTABLE - highest priority among readable tags
CreateDate: '2023:05:05 05:00:00', // TESTABLE
CreationDate: '2023:07:07 07:00:00', // TESTABLE
GPSDateTime: '2023:10:10 10:00:00', // TESTABLE
// Note: Excluded non-testable tags:
// SubSec tags: writable but not readable from JPEG
// Non-writable tags: MediaCreateDate, DateTimeCreated, DateTimeUTC, SonyDateTime2, etc.
// Time-only/date-only tags: already excluded from EXIF_DATE_TAGS
});
const asset = await utils.createAsset(admin.accessToken, {
assetData: {
filename,
bytes: imageBytes,
},
});
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id });
const assetInfo = await getAssetInfo({ id: asset.id }, { headers: asBearerAuth(admin.accessToken) });
expect(assetInfo.exifInfo?.dateTimeOriginal).toBeDefined();
// Should use DateTimeOriginal as it has the highest priority among testable tags
expect(new Date(assetInfo.exifInfo!.dateTimeOriginal!).getTime()).toBe(
new Date('2023-04-04T04:00:00.000Z').getTime(),
);
});
it('should use CreationDate when SubSec tags are missing', async () => {
const { imageBytes, filename } = await createTestImageWithExif('creation-date-priority.jpg', {
CreationDate: '2023:07:07 07:00:00', // WRITABLE
GPSDateTime: '2023:10:10 10:00:00', // WRITABLE
// Note: DateTimeCreated, DateTimeUTC, SonyDateTime2 are NOT writable to JPEG
// Note: TimeCreated and GPSDateStamp are excluded from EXIF_DATE_TAGS (time-only/date-only)
// Exclude SubSec and standard EXIF tags
SubSecDateTimeOriginal: undefined,
DateTimeOriginal: undefined,
SubSecCreateDate: undefined,
CreateDate: undefined,
});
const asset = await utils.createAsset(admin.accessToken, {
assetData: {
filename,
bytes: imageBytes,
},
});
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id });
const assetInfo = await getAssetInfo({ id: asset.id }, { headers: asBearerAuth(admin.accessToken) });
expect(assetInfo.exifInfo?.dateTimeOriginal).toBeDefined();
// Should use CreationDate when available
expect(new Date(assetInfo.exifInfo!.dateTimeOriginal!).getTime()).toBe(
new Date('2023-07-07T07:00:00.000Z').getTime(),
);
});
it('should skip invalid date formats and use next valid tag', async () => {
const { imageBytes, filename } = await createTestImageWithExif('invalid-date-handling.jpg', {
// Note: Testing invalid date handling with only WRITABLE tags
GPSDateTime: '2023:10:10 10:00:00', // WRITABLE - Valid date
CreationDate: '2023:13:13 13:00:00', // WRITABLE - Valid date
// Note: TimeCreated excluded (time-only), DateTimeCreated not writable to JPEG
// Exclude other date tags
SubSecDateTimeOriginal: undefined,
DateTimeOriginal: undefined,
SubSecCreateDate: undefined,
CreateDate: undefined,
});
const asset = await utils.createAsset(admin.accessToken, {
assetData: {
filename,
bytes: imageBytes,
},
});
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id });
const assetInfo = await getAssetInfo({ id: asset.id }, { headers: asBearerAuth(admin.accessToken) });
expect(assetInfo.exifInfo?.dateTimeOriginal).toBeDefined();
// Should skip invalid dates and use the first valid one (GPSDateTime)
expect(new Date(assetInfo.exifInfo!.dateTimeOriginal!).getTime()).toBe(
new Date('2023-10-10T10:00:00.000Z').getTime(),
);
});
});
});
});
describe('POST /assets/exist', () => {
it('ignores invalid deviceAssetIds', async () => {
const response = await utils.checkExistingAssets(user1.accessToken, {

View File

@@ -1,146 +0,0 @@
import { LoginResponseDto, login, signUpAdmin } from '@immich/sdk';
import { loginDto, signupDto } from 'src/fixtures';
import { errorDto, loginResponseDto, signupResponseDto } from 'src/responses';
import { app, utils } from 'src/utils';
import request from 'supertest';
import { beforeEach, describe, expect, it } from 'vitest';
const { email, password } = signupDto.admin;
describe(`/auth/admin-sign-up`, () => {
beforeEach(async () => {
await utils.resetDatabase();
});
describe('POST /auth/admin-sign-up', () => {
it(`should sign up the admin`, async () => {
const { status, body } = await request(app).post('/auth/admin-sign-up').send(signupDto.admin);
expect(status).toBe(201);
expect(body).toEqual(signupResponseDto.admin);
});
it('should not allow a second admin to sign up', async () => {
await signUpAdmin({ signUpDto: signupDto.admin });
const { status, body } = await request(app).post('/auth/admin-sign-up').send(signupDto.admin);
expect(status).toBe(400);
expect(body).toEqual(errorDto.alreadyHasAdmin);
});
});
});
describe('/auth/*', () => {
let admin: LoginResponseDto;
beforeEach(async () => {
await utils.resetDatabase();
await signUpAdmin({ signUpDto: signupDto.admin });
admin = await login({ loginCredentialDto: loginDto.admin });
});
describe(`POST /auth/login`, () => {
it('should reject an incorrect password', async () => {
const { status, body } = await request(app).post('/auth/login').send({ email, password: 'incorrect' });
expect(status).toBe(401);
expect(body).toEqual(errorDto.incorrectLogin);
});
it('should accept a correct password', async () => {
const { status, body, headers } = await request(app).post('/auth/login').send({ email, password });
expect(status).toBe(201);
expect(body).toEqual(loginResponseDto.admin);
const token = body.accessToken;
expect(token).toBeDefined();
const cookies = headers['set-cookie'];
expect(cookies).toHaveLength(3);
expect(cookies[0].split(';').map((item) => item.trim())).toEqual([
`immich_access_token=${token}`,
'Max-Age=34560000',
'Path=/',
expect.stringContaining('Expires='),
'HttpOnly',
'SameSite=Lax',
]);
expect(cookies[1].split(';').map((item) => item.trim())).toEqual([
'immich_auth_type=password',
'Max-Age=34560000',
'Path=/',
expect.stringContaining('Expires='),
'HttpOnly',
'SameSite=Lax',
]);
expect(cookies[2].split(';').map((item) => item.trim())).toEqual([
'immich_is_authenticated=true',
'Max-Age=34560000',
'Path=/',
expect.stringContaining('Expires='),
'SameSite=Lax',
]);
});
});
describe('POST /auth/validateToken', () => {
it('should reject an invalid token', async () => {
const { status, body } = await request(app).post(`/auth/validateToken`).set('Authorization', 'Bearer 123');
expect(status).toBe(401);
expect(body).toEqual(errorDto.invalidToken);
});
it('should accept a valid token', async () => {
const { status, body } = await request(app)
.post(`/auth/validateToken`)
.send({})
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({ authStatus: true });
});
});
describe('POST /auth/change-password', () => {
it('should require the current password', async () => {
const { status, body } = await request(app)
.post(`/auth/change-password`)
.send({ password: 'wrong-password', newPassword: 'Password1234' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.wrongPassword);
});
it('should change the password', async () => {
const { status } = await request(app)
.post(`/auth/change-password`)
.send({ password, newPassword: 'Password1234' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
await login({
loginCredentialDto: {
email: 'admin@immich.cloud',
password: 'Password1234',
},
});
});
});
describe('POST /auth/logout', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/auth/logout`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should logout the user', async () => {
const { status, body } = await request(app)
.post(`/auth/logout`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
successful: true,
redirectUri: '/auth/login?autoLaunch=0',
});
});
});
});

View File

@@ -6,7 +6,7 @@ import {
createMemory,
getMemory,
} from '@immich/sdk';
import { createUserDto, uuidDto } from 'src/fixtures';
import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
@@ -17,7 +17,6 @@ describe('/memories', () => {
let user: LoginResponseDto;
let adminAsset: AssetMediaResponseDto;
let userAsset1: AssetMediaResponseDto;
let userAsset2: AssetMediaResponseDto;
let userMemory: MemoryResponseDto;
beforeAll(async () => {
@@ -25,10 +24,9 @@ describe('/memories', () => {
admin = await utils.adminSetup();
user = await utils.userSetup(admin.accessToken, createUserDto.user1);
[adminAsset, userAsset1, userAsset2] = await Promise.all([
[adminAsset, userAsset1] = await Promise.all([
utils.createAsset(admin.accessToken),
utils.createAsset(user.accessToken),
utils.createAsset(user.accessToken),
]);
userMemory = await createMemory(
{
@@ -43,121 +41,7 @@ describe('/memories', () => {
);
});
describe('GET /memories', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/memories');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
});
describe('POST /memories', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/memories');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should validate data when type is on this day', async () => {
const { status, body } = await request(app)
.post('/memories')
.set('Authorization', `Bearer ${user.accessToken}`)
.send({
type: 'on_this_day',
data: {},
memoryAt: new Date(2021).toISOString(),
});
expect(status).toBe(400);
expect(body).toEqual(
errorDto.badRequest(['data.year must be a positive number', 'data.year must be an integer number']),
);
});
it('should create a new memory', async () => {
const { status, body } = await request(app)
.post('/memories')
.set('Authorization', `Bearer ${user.accessToken}`)
.send({
type: 'on_this_day',
data: { year: 2021 },
memoryAt: new Date(2021).toISOString(),
});
expect(status).toBe(201);
expect(body).toEqual({
id: expect.any(String),
type: 'on_this_day',
data: { year: 2021 },
createdAt: expect.any(String),
updatedAt: expect.any(String),
isSaved: false,
memoryAt: expect.any(String),
ownerId: user.userId,
assets: [],
});
});
it('should create a new memory (with assets)', async () => {
const { status, body } = await request(app)
.post('/memories')
.set('Authorization', `Bearer ${user.accessToken}`)
.send({
type: 'on_this_day',
data: { year: 2021 },
memoryAt: new Date(2021).toISOString(),
assetIds: [userAsset1.id, userAsset2.id],
});
expect(status).toBe(201);
expect(body).toMatchObject({
id: expect.any(String),
assets: expect.arrayContaining([
expect.objectContaining({ id: userAsset1.id }),
expect.objectContaining({ id: userAsset2.id }),
]),
});
expect(body.assets).toHaveLength(2);
});
it('should create a new memory and ignore assets the user does not have access to', async () => {
const { status, body } = await request(app)
.post('/memories')
.set('Authorization', `Bearer ${user.accessToken}`)
.send({
type: 'on_this_day',
data: { year: 2021 },
memoryAt: new Date(2021).toISOString(),
assetIds: [userAsset1.id, adminAsset.id],
});
expect(status).toBe(201);
expect(body).toMatchObject({
id: expect.any(String),
assets: [expect.objectContaining({ id: userAsset1.id })],
});
expect(body.assets).toHaveLength(1);
});
});
describe('GET /memories/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/memories/${uuidDto.invalid}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require a valid id', async () => {
const { status, body } = await request(app)
.get(`/memories/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${user.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
});
it('should require access', async () => {
const { status, body } = await request(app)
.get(`/memories/${userMemory.id}`)
@@ -176,22 +60,6 @@ describe('/memories', () => {
});
describe('PUT /memories/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/memories/${uuidDto.invalid}`).send({ isSaved: true });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require a valid id', async () => {
const { status, body } = await request(app)
.put(`/memories/${uuidDto.invalid}`)
.send({ isSaved: true })
.set('Authorization', `Bearer ${user.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
});
it('should require access', async () => {
const { status, body } = await request(app)
.put(`/memories/${userMemory.id}`)
@@ -218,23 +86,6 @@ describe('/memories', () => {
});
describe('PUT /memories/:id/assets', () => {
it('should require authentication', async () => {
const { status, body } = await request(app)
.put(`/memories/${userMemory.id}/assets`)
.send({ ids: [userAsset1.id] });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require a valid id', async () => {
const { status, body } = await request(app)
.put(`/memories/${uuidDto.invalid}/assets`)
.send({ ids: [userAsset1.id] })
.set('Authorization', `Bearer ${user.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
});
it('should require access', async () => {
const { status, body } = await request(app)
.put(`/memories/${userMemory.id}/assets`)
@@ -244,15 +95,6 @@ describe('/memories', () => {
expect(body).toEqual(errorDto.noPermission);
});
it('should require a valid asset id', async () => {
const { status, body } = await request(app)
.put(`/memories/${userMemory.id}/assets`)
.send({ ids: [uuidDto.invalid] })
.set('Authorization', `Bearer ${user.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID']));
});
it('should require asset access', async () => {
const { status, body } = await request(app)
.put(`/memories/${userMemory.id}/assets`)
@@ -279,23 +121,6 @@ describe('/memories', () => {
});
describe('DELETE /memories/:id/assets', () => {
it('should require authentication', async () => {
const { status, body } = await request(app)
.delete(`/memories/${userMemory.id}/assets`)
.send({ ids: [userAsset1.id] });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require a valid id', async () => {
const { status, body } = await request(app)
.delete(`/memories/${uuidDto.invalid}/assets`)
.send({ ids: [userAsset1.id] })
.set('Authorization', `Bearer ${user.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
});
it('should require access', async () => {
const { status, body } = await request(app)
.delete(`/memories/${userMemory.id}/assets`)
@@ -305,15 +130,6 @@ describe('/memories', () => {
expect(body).toEqual(errorDto.noPermission);
});
it('should require a valid asset id', async () => {
const { status, body } = await request(app)
.delete(`/memories/${userMemory.id}/assets`)
.send({ ids: [uuidDto.invalid] })
.set('Authorization', `Bearer ${user.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID']));
});
it('should only remove assets in the memory', async () => {
const { status, body } = await request(app)
.delete(`/memories/${userMemory.id}/assets`)
@@ -340,21 +156,6 @@ describe('/memories', () => {
});
describe('DELETE /memories/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/memories/${uuidDto.invalid}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require a valid id', async () => {
const { status, body } = await request(app)
.delete(`/memories/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${user.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
});
it('should require access', async () => {
const { status, body } = await request(app)
.delete(`/memories/${userMemory.id}`)

View File

@@ -227,6 +227,21 @@ describe(`/oauth`, () => {
expect(user.storageLabel).toBe('user-username');
});
it('should set the admin status from a role claim', async () => {
const callbackParams = await loginWithOAuth(OAuthUser.WITH_ROLE);
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
expect(status).toBe(201);
expect(body).toMatchObject({
accessToken: expect.any(String),
userId: expect.any(String),
userEmail: 'oauth-with-role@immich.app',
isAdmin: true,
});
const user = await getMyUser({ headers: asBearerAuth(body.accessToken) });
expect(user.isAdmin).toBe(true);
});
it('should work with RS256 signed tokens', async () => {
await setupOAuth(admin.accessToken, {
enabled: true,

View File

@@ -11,11 +11,32 @@ describe('/people', () => {
let hiddenPerson: PersonResponseDto;
let multipleAssetsPerson: PersonResponseDto;
let nameAlicePerson: PersonResponseDto;
let nameBobPerson: PersonResponseDto;
let nameCharliePerson: PersonResponseDto;
let nameNullPerson4Assets: PersonResponseDto;
let nameNullPerson3Assets: PersonResponseDto;
let nameNullPerson1Asset: PersonResponseDto;
let nameBillPersonFavourite: PersonResponseDto;
let nameFreddyPersonFavourite: PersonResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup();
[visiblePerson, hiddenPerson, multipleAssetsPerson] = await Promise.all([
[
visiblePerson,
hiddenPerson,
multipleAssetsPerson,
nameCharliePerson,
nameBobPerson,
nameAlicePerson,
nameNullPerson4Assets,
nameNullPerson3Assets,
nameNullPerson1Asset,
nameBillPersonFavourite,
nameFreddyPersonFavourite,
] = await Promise.all([
utils.createPerson(admin.accessToken, {
name: 'visible_person',
}),
@@ -26,10 +47,39 @@ describe('/people', () => {
utils.createPerson(admin.accessToken, {
name: 'multiple_assets_person',
}),
// --- Setup for the specific sorting test ---
utils.createPerson(admin.accessToken, {
name: 'Charlie',
}),
utils.createPerson(admin.accessToken, {
name: 'Bob',
}),
utils.createPerson(admin.accessToken, {
name: 'Alice',
}),
utils.createPerson(admin.accessToken, {
name: '',
}),
utils.createPerson(admin.accessToken, {
name: '',
}),
utils.createPerson(admin.accessToken, {
name: '',
}),
utils.createPerson(admin.accessToken, {
name: 'Bill',
isFavorite: true,
}),
utils.createPerson(admin.accessToken, {
name: 'Freddy',
isFavorite: true,
}),
]);
const asset1 = await utils.createAsset(admin.accessToken);
const asset2 = await utils.createAsset(admin.accessToken);
const asset3 = await utils.createAsset(admin.accessToken);
const asset4 = await utils.createAsset(admin.accessToken);
await Promise.all([
utils.createFace({ assetId: asset1.id, personId: visiblePerson.id }),
@@ -37,6 +87,27 @@ describe('/people', () => {
utils.createFace({ assetId: asset1.id, personId: multipleAssetsPerson.id }),
utils.createFace({ assetId: asset1.id, personId: multipleAssetsPerson.id }),
utils.createFace({ assetId: asset2.id, personId: multipleAssetsPerson.id }),
utils.createFace({ assetId: asset3.id, personId: multipleAssetsPerson.id }), // 4 assets
// Named persons
utils.createFace({ assetId: asset1.id, personId: nameCharliePerson.id }), // 1 asset
utils.createFace({ assetId: asset1.id, personId: nameBobPerson.id }),
utils.createFace({ assetId: asset2.id, personId: nameBobPerson.id }), // 2 assets
utils.createFace({ assetId: asset1.id, personId: nameAlicePerson.id }), // 1 asset
// Null-named person 4 assets
utils.createFace({ assetId: asset1.id, personId: nameNullPerson4Assets.id }),
utils.createFace({ assetId: asset2.id, personId: nameNullPerson4Assets.id }),
utils.createFace({ assetId: asset3.id, personId: nameNullPerson4Assets.id }),
utils.createFace({ assetId: asset4.id, personId: nameNullPerson4Assets.id }), // 4 assets
// Null-named person 3 assets
utils.createFace({ assetId: asset1.id, personId: nameNullPerson3Assets.id }),
utils.createFace({ assetId: asset2.id, personId: nameNullPerson3Assets.id }),
utils.createFace({ assetId: asset3.id, personId: nameNullPerson3Assets.id }), // 3 assets
// Null-named person 1 asset
utils.createFace({ assetId: asset3.id, personId: nameNullPerson1Asset.id }),
// Favourite People
utils.createFace({ assetId: asset1.id, personId: nameFreddyPersonFavourite.id }),
utils.createFace({ assetId: asset2.id, personId: nameFreddyPersonFavourite.id }),
utils.createFace({ assetId: asset1.id, personId: nameBillPersonFavourite.id }),
]);
});
@@ -51,27 +122,66 @@ describe('/people', () => {
expect(status).toBe(200);
expect(body).toEqual({
hasNextPage: false,
total: 3,
total: 11,
hidden: 1,
people: [
expect.objectContaining({ name: 'Freddy' }),
expect.objectContaining({ name: 'Bill' }),
expect.objectContaining({ name: 'multiple_assets_person' }),
expect.objectContaining({ name: 'Bob' }),
expect.objectContaining({ name: 'Alice' }),
expect.objectContaining({ name: 'Charlie' }),
expect.objectContaining({ name: 'visible_person' }),
expect.objectContaining({ name: 'hidden_person' }),
expect.objectContaining({ id: nameNullPerson4Assets.id, name: '' }),
expect.objectContaining({ id: nameNullPerson3Assets.id, name: '' }),
expect.objectContaining({ name: 'hidden_person' }), // Should really be before the null names
],
});
});
it('should sort visible people by asset count (desc), then by name (asc, nulls last)', async () => {
const { status, body } = await request(app).get('/people').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body.hasNextPage).toBe(false);
expect(body.total).toBe(11); // All persons
expect(body.hidden).toBe(1); // 'hidden_person'
const people = body.people as PersonResponseDto[];
expect(people.map((p) => p.id)).toEqual([
nameFreddyPersonFavourite.id, // name: 'Freddy', count: 2
nameBillPersonFavourite.id, // name: 'Bill', count: 1
multipleAssetsPerson.id, // name: 'multiple_assets_person', count: 3
nameBobPerson.id, // name: 'Bob', count: 2
nameAlicePerson.id, // name: 'Alice', count: 1
nameCharliePerson.id, // name: 'Charlie', count: 1
visiblePerson.id, // name: 'visible_person', count: 1
nameNullPerson4Assets.id, // name: '', count: 4
nameNullPerson3Assets.id, // name: '', count: 3
]);
expect(people.some((p) => p.id === hiddenPerson.id)).toBe(false);
});
it('should return only visible people', async () => {
const { status, body } = await request(app).get('/people').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
hasNextPage: false,
total: 3,
total: 11,
hidden: 1,
people: [
expect.objectContaining({ name: 'Freddy' }),
expect.objectContaining({ name: 'Bill' }),
expect.objectContaining({ name: 'multiple_assets_person' }),
expect.objectContaining({ name: 'Bob' }),
expect.objectContaining({ name: 'Alice' }),
expect.objectContaining({ name: 'Charlie' }),
expect.objectContaining({ name: 'visible_person' }),
expect.objectContaining({ id: nameNullPerson4Assets.id, name: '' }),
expect.objectContaining({ id: nameNullPerson3Assets.id, name: '' }),
],
});
});
@@ -80,14 +190,14 @@ describe('/people', () => {
const { status, body } = await request(app)
.get('/people')
.set('Authorization', `Bearer ${admin.accessToken}`)
.query({ withHidden: true, page: 2, size: 1 });
.query({ withHidden: true, page: 5, size: 1 });
expect(status).toBe(200);
expect(body).toEqual({
hasNextPage: true,
total: 3,
total: 11,
hidden: 1,
people: [expect.objectContaining({ name: 'visible_person' })],
people: [expect.objectContaining({ name: 'Alice' })],
});
});
});
@@ -128,7 +238,7 @@ describe('/people', () => {
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ assets: 2 }));
expect(body).toEqual(expect.objectContaining({ assets: 3 }));
});
});

View File

@@ -117,8 +117,25 @@ describe('/shared-links', () => {
const resp = await request(shareUrl).get(`/${linkWithAssets.key}`);
expect(resp.status).toBe(200);
expect(resp.header['content-type']).toContain('text/html');
expect(resp.text).toContain(`<meta property="og:image" content="http://127.0.0.1:2285`);
});
it('should fall back to my.immich.app og:image meta tag for shared asset if Host header is not present', async () => {
const resp = await request(shareUrl).get(`/${linkWithAssets.key}`).set('Host', '');
expect(resp.status).toBe(200);
expect(resp.header['content-type']).toContain('text/html');
expect(resp.text).toContain(`<meta property="og:image" content="https://my.immich.app`);
});
it('should return 404 for an invalid shared link', async () => {
const resp = await request(shareUrl).get(`/invalid-key`);
expect(resp.status).toBe(404);
expect(resp.header['content-type']).toContain('text/html');
expect(resp.text).not.toContain(`og:type`);
expect(resp.text).not.toContain(`og:title`);
expect(resp.text).not.toContain(`og:description`);
expect(resp.text).not.toContain(`og:image`);
});
});
describe('GET /shared-links', () => {

View File

@@ -15,12 +15,6 @@ describe('/system-config', () => {
});
describe('PUT /system-config', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put('/system-config');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should always return the new config', async () => {
const config = await getSystemConfig(admin.accessToken);

View File

@@ -37,7 +37,7 @@ describe('/tags', () => {
beforeEach(async () => {
// tagging assets eventually triggers metadata extraction which can impact other tests
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
await utils.resetDatabase(['tags']);
await utils.resetDatabase(['tag']);
});
describe('POST /tags', () => {

View File

@@ -1,230 +0,0 @@
import {
AssetMediaResponseDto,
AssetVisibility,
LoginResponseDto,
SharedLinkType,
TimeBucketAssetResponseDto,
} from '@immich/sdk';
import { DateTime } from 'luxon';
import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest';
// TODO this should probably be a test util function
const today = DateTime.fromObject({
year: 2023,
month: 11,
day: 3,
}) as DateTime<true>;
const yesterday = today.minus({ days: 1 });
describe('/timeline', () => {
let admin: LoginResponseDto;
let user: LoginResponseDto;
let timeBucketUser: LoginResponseDto;
let user1Assets: AssetMediaResponseDto[];
let user2Assets: AssetMediaResponseDto[];
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup({ onboarding: false });
[user, timeBucketUser] = await Promise.all([
utils.userSetup(admin.accessToken, createUserDto.create('1')),
utils.userSetup(admin.accessToken, createUserDto.create('time-bucket')),
]);
user1Assets = await Promise.all([
utils.createAsset(user.accessToken),
utils.createAsset(user.accessToken),
utils.createAsset(user.accessToken, {
isFavorite: true,
fileCreatedAt: yesterday.toISO(),
fileModifiedAt: yesterday.toISO(),
assetData: { filename: 'example.mp4' },
}),
utils.createAsset(user.accessToken),
utils.createAsset(user.accessToken),
]);
user2Assets = await Promise.all([
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-01-01').toISOString() }),
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-10').toISOString() }),
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-11').toISOString() }),
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-11').toISOString() }),
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-12').toISOString() }),
]);
await utils.deleteAssets(timeBucketUser.accessToken, [user2Assets[4].id]);
});
describe('GET /timeline/buckets', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/timeline/buckets');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should get time buckets by month', async () => {
const { status, body } = await request(app)
.get('/timeline/buckets')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(
expect.arrayContaining([
{ count: 3, timeBucket: '1970-02-01' },
{ count: 1, timeBucket: '1970-01-01' },
]),
);
});
it('should not allow access for unrelated shared links', async () => {
const sharedLink = await utils.createSharedLink(user.accessToken, {
type: SharedLinkType.Individual,
assetIds: user1Assets.map(({ id }) => id),
});
const { status, body } = await request(app).get('/timeline/buckets').query({ key: sharedLink.key });
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should return error if time bucket is requested with partners asset and archived', async () => {
const req1 = await request(app)
.get('/timeline/buckets')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
.query({ withPartners: true, visibility: AssetVisibility.Archive });
expect(req1.status).toBe(400);
expect(req1.body).toEqual(errorDto.badRequest());
const req2 = await request(app)
.get('/timeline/buckets')
.set('Authorization', `Bearer ${user.accessToken}`)
.query({ withPartners: true, visibility: undefined });
expect(req2.status).toBe(400);
expect(req2.body).toEqual(errorDto.badRequest());
});
it('should return error if time bucket is requested with partners asset and favorite', async () => {
const req1 = await request(app)
.get('/timeline/buckets')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
.query({ withPartners: true, isFavorite: true });
expect(req1.status).toBe(400);
expect(req1.body).toEqual(errorDto.badRequest());
const req2 = await request(app)
.get('/timeline/buckets')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
.query({ withPartners: true, isFavorite: false });
expect(req2.status).toBe(400);
expect(req2.body).toEqual(errorDto.badRequest());
});
it('should return error if time bucket is requested with partners asset and trash', async () => {
const req = await request(app)
.get('/timeline/buckets')
.set('Authorization', `Bearer ${user.accessToken}`)
.query({ withPartners: true, isTrashed: true });
expect(req.status).toBe(400);
expect(req.body).toEqual(errorDto.badRequest());
});
});
describe('GET /timeline/bucket', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/timeline/bucket').query({
timeBucket: '1900-01-01',
});
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should handle 5 digit years', async () => {
const { status, body } = await request(app)
.get('/timeline/bucket')
.query({ timeBucket: '012345-01-01' })
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
city: [],
country: [],
duration: [],
id: [],
visibility: [],
isFavorite: [],
isImage: [],
isTrashed: [],
livePhotoVideoId: [],
fileCreatedAt: [],
localOffsetHours: [],
ownerId: [],
projectionType: [],
ratio: [],
status: [],
thumbhash: [],
});
});
// TODO enable date string validation while still accepting 5 digit years
// it('should fail if time bucket is invalid', async () => {
// const { status, body } = await request(app)
// .get('/timeline/bucket')
// .set('Authorization', `Bearer ${user.accessToken}`)
// .query({ timeBucket: 'foo' });
// expect(status).toBe(400);
// expect(body).toEqual(errorDto.badRequest);
// });
it('should return time bucket', async () => {
const { status, body } = await request(app)
.get('/timeline/bucket')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
.query({ timeBucket: '1970-02-10' });
expect(status).toBe(200);
expect(body).toEqual({
city: [],
country: [],
duration: [],
id: [],
visibility: [],
isFavorite: [],
isImage: [],
isTrashed: [],
livePhotoVideoId: [],
fileCreatedAt: [],
localOffsetHours: [],
ownerId: [],
projectionType: [],
ratio: [],
status: [],
thumbhash: [],
});
});
it('should return time bucket in trash', async () => {
const { status, body } = await request(app)
.get('/timeline/bucket')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
.query({ timeBucket: '1970-02-01T00:00:00.000Z', isTrashed: true });
expect(status).toBe(200);
const timeBucket: TimeBucketAssetResponseDto = body;
expect(timeBucket.isTrashed).toEqual([true]);
});
});
});

View File

@@ -118,7 +118,7 @@ describe('/admin/users', () => {
});
}
it('should ignore `isAdmin`', async () => {
it('should accept `isAdmin`', async () => {
const { status, body } = await request(app)
.post(`/admin/users`)
.send({
@@ -130,7 +130,7 @@ describe('/admin/users', () => {
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toMatchObject({
email: 'user5@immich.cloud',
isAdmin: false,
isAdmin: true,
shouldChangePassword: true,
});
expect(status).toBe(201);
@@ -163,14 +163,15 @@ describe('/admin/users', () => {
});
}
it('should not allow a non-admin to become an admin', async () => {
it('should allow a non-admin to become an admin', async () => {
const user = await utils.userSetup(admin.accessToken, createUserDto.create('admin2'));
const { status, body } = await request(app)
.put(`/admin/users/${nonAdmin.userId}`)
.put(`/admin/users/${user.userId}`)
.send({ isAdmin: true })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ isAdmin: false });
expect(body).toMatchObject({ isAdmin: true });
});
it('ignores updates to profileImagePath', async () => {

View File

@@ -97,7 +97,7 @@ describe(`immich upload`, () => {
});
beforeEach(async () => {
await utils.resetDatabase(['assets', 'albums']);
await utils.resetDatabase(['asset', 'album']);
});
describe(`immich upload /path/to/file.jpg`, () => {

View File

@@ -0,0 +1,178 @@
#!/usr/bin/env node
/**
* Script to generate test images with additional EXIF date tags
* This creates actual JPEG images with embedded metadata for testing
* Images are generated into e2e/test-assets/metadata/dates/
*/
import { execSync } from 'node:child_process';
import { writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import sharp from 'sharp';
interface TestImage {
filename: string;
description: string;
exifTags: Record<string, string>;
}
const testImages: TestImage[] = [
{
filename: 'time-created.jpg',
description: 'Image with TimeCreated tag',
exifTags: {
TimeCreated: '2023:11:15 14:30:00',
Make: 'Canon',
Model: 'EOS R5',
},
},
{
filename: 'gps-datetime.jpg',
description: 'Image with GPSDateTime and coordinates',
exifTags: {
GPSDateTime: '2023:11:15 12:30:00Z',
GPSLatitude: '37.7749',
GPSLongitude: '-122.4194',
GPSLatitudeRef: 'N',
GPSLongitudeRef: 'W',
},
},
{
filename: 'datetime-utc.jpg',
description: 'Image with DateTimeUTC tag',
exifTags: {
DateTimeUTC: '2023:11:15 10:30:00',
Make: 'Nikon',
Model: 'D850',
},
},
{
filename: 'gps-datestamp.jpg',
description: 'Image with GPSDateStamp and GPSTimeStamp',
exifTags: {
GPSDateStamp: '2023:11:15',
GPSTimeStamp: '08:30:00',
GPSLatitude: '51.5074',
GPSLongitude: '-0.1278',
GPSLatitudeRef: 'N',
GPSLongitudeRef: 'W',
},
},
{
filename: 'sony-datetime2.jpg',
description: 'Sony camera image with SonyDateTime2 tag',
exifTags: {
SonyDateTime2: '2023:11:15 06:30:00',
Make: 'SONY',
Model: 'ILCE-7RM5',
},
},
{
filename: 'date-priority-test.jpg',
description: 'Image with multiple date tags to test priority',
exifTags: {
SubSecDateTimeOriginal: '2023:01:01 01:00:00',
DateTimeOriginal: '2023:02:02 02:00:00',
SubSecCreateDate: '2023:03:03 03:00:00',
CreateDate: '2023:04:04 04:00:00',
CreationDate: '2023:05:05 05:00:00',
DateTimeCreated: '2023:06:06 06:00:00',
TimeCreated: '2023:07:07 07:00:00',
GPSDateTime: '2023:08:08 08:00:00',
DateTimeUTC: '2023:09:09 09:00:00',
GPSDateStamp: '2023:10:10',
SonyDateTime2: '2023:11:11 11:00:00',
},
},
{
filename: 'new-tags-only.jpg',
description: 'Image with only additional date tags (no standard tags)',
exifTags: {
TimeCreated: '2023:12:01 15:45:30',
GPSDateTime: '2023:12:01 13:45:30Z',
DateTimeUTC: '2023:12:01 13:45:30',
GPSDateStamp: '2023:12:01',
SonyDateTime2: '2023:12:01 08:45:30',
GPSLatitude: '40.7128',
GPSLongitude: '-74.0060',
GPSLatitudeRef: 'N',
GPSLongitudeRef: 'W',
},
},
];
const generateTestImages = async (): Promise<void> => {
// Target directory: e2e/test-assets/metadata/dates/
// Current file is in: e2e/src/
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const targetDir = join(__dirname, '..', 'test-assets', 'metadata', 'dates');
console.log('Generating test images with additional EXIF date tags...');
console.log(`Target directory: ${targetDir}`);
for (const image of testImages) {
try {
const imagePath = join(targetDir, image.filename);
// Create unique JPEG file using Sharp
const r = Math.floor(Math.random() * 256);
const g = Math.floor(Math.random() * 256);
const b = Math.floor(Math.random() * 256);
const jpegData = await sharp({
create: {
width: 100,
height: 100,
channels: 3,
background: { r, g, b },
},
})
.jpeg({ quality: 90 })
.toBuffer();
writeFileSync(imagePath, jpegData);
// Build exiftool command to add EXIF data
const exifArgs = Object.entries(image.exifTags)
.map(([tag, value]) => `-${tag}="${value}"`)
.join(' ');
const command = `exiftool ${exifArgs} -overwrite_original "${imagePath}"`;
console.log(`Creating ${image.filename}: ${image.description}`);
execSync(command, { stdio: 'pipe' });
// Verify the tags were written
const verifyCommand = `exiftool -json "${imagePath}"`;
const result = execSync(verifyCommand, { encoding: 'utf8' });
const metadata = JSON.parse(result)[0];
console.log(` ✓ Created with ${Object.keys(image.exifTags).length} EXIF tags`);
// Log first date tag found for verification
const firstDateTag = Object.keys(image.exifTags).find(
(tag) => tag.includes('Date') || tag.includes('Time') || tag.includes('Created'),
);
if (firstDateTag && metadata[firstDateTag]) {
console.log(` ✓ Verified ${firstDateTag}: ${metadata[firstDateTag]}`);
}
} catch (error) {
console.error(`Failed to create ${image.filename}:`, (error as Error).message);
}
}
console.log('\nTest image generation complete!');
console.log('Files created in:', targetDir);
console.log('\nTo test these images:');
console.log(`cd ${targetDir} && exiftool -time:all -gps:all *.jpg`);
};
export { generateTestImages };
// Run the generator if this file is executed directly
if (import.meta.url === `file://${process.argv[1]}`) {
generateTestImages().catch(console.error);
}

View File

@@ -7,6 +7,44 @@ describe(`immich-admin`, () => {
await utils.adminSetup();
});
describe('revoke-admin', () => {
it('should revoke admin privileges from a user', async () => {
const { child, promise } = immichAdmin(['revoke-admin']);
let data = '';
child.stdout.on('data', (chunk) => {
data += chunk;
if (data.includes('Please enter the user email:')) {
child.stdin.end('admin@immich.cloud\n');
}
});
const { stdout, exitCode } = await promise;
expect(exitCode).toBe(0);
expect(stdout).toContain('Admin access has been revoked from');
});
});
describe('grant-admin', () => {
it('should grant admin privileges to a user', async () => {
const { child, promise } = immichAdmin(['grant-admin']);
let data = '';
child.stdout.on('data', (chunk) => {
data += chunk;
if (data.includes('Please enter the user email:')) {
child.stdin.end('admin@immich.cloud\n');
}
});
const { stdout, exitCode } = await promise;
expect(exitCode).toBe(0);
expect(stdout).toContain('Admin access has been granted to');
});
});
describe('list-users', () => {
it('should list the admin user', async () => {
const { stdout, exitCode } = await immichAdmin(['list-users']).promise;

View File

@@ -116,6 +116,7 @@ export const deviceDto = {
createdAt: expect.any(String),
updatedAt: expect.any(String),
current: true,
isPendingSyncReset: false,
deviceOS: '',
deviceType: '',
},

View File

@@ -12,6 +12,7 @@ export enum OAuthUser {
NO_NAME = 'no-name',
WITH_QUOTA = 'with-quota',
WITH_USERNAME = 'with-username',
WITH_ROLE = 'with-role',
}
const claims = [
@@ -34,6 +35,12 @@ const claims = [
preferred_username: 'user-quota',
immich_quota: 25,
},
{
sub: OAuthUser.WITH_ROLE,
email: 'oauth-with-role@immich.app',
email_verified: true,
immich_role: 'admin',
},
];
const withDefaultClaims = (sub: string) => ({
@@ -64,7 +71,15 @@ const setup = async () => {
claims: {
openid: ['sub'],
email: ['email', 'email_verified'],
profile: ['name', 'given_name', 'family_name', 'preferred_username', 'immich_quota', 'immich_username'],
profile: [
'name',
'given_name',
'family_name',
'preferred_username',
'immich_quota',
'immich_username',
'immich_role',
],
},
features: {
jwtUserinfo: {

View File

@@ -60,6 +60,7 @@ import { io, type Socket } from 'socket.io-client';
import { loginDto, signupDto } from 'src/fixtures';
import { makeRandomImage } from 'src/generators';
import request from 'supertest';
export type { Emitter } from '@socket.io/component-emitter';
type CommandResponse = { stdout: string; stderr: string; exitCode: number | null };
type EventType = 'assetUpload' | 'assetUpdate' | 'assetDelete' | 'userDelete' | 'assetHidden';
@@ -84,10 +85,10 @@ export const immichAdmin = (args: string[]) =>
export const specialCharStrings = ["'", '"', ',', '{', '}', '*'];
export const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
const executeCommand = (command: string, args: string[]) => {
const executeCommand = (command: string, args: string[], options?: { cwd?: string }) => {
let _resolve: (value: CommandResponse) => void;
const promise = new Promise<CommandResponse>((resolve) => (_resolve = resolve));
const child = spawn(command, args, { stdio: 'pipe' });
const child = spawn(command, args, { stdio: 'pipe', cwd: options?.cwd });
let stdout = '';
let stderr = '';
@@ -153,19 +154,19 @@ export const utils = {
tables = tables || [
// TODO e2e test for deleting a stack, since it is quite complex
'asset_stack',
'libraries',
'shared_links',
'stack',
'library',
'shared_link',
'person',
'albums',
'assets',
'asset_faces',
'album',
'asset',
'asset_face',
'activity',
'api_keys',
'sessions',
'users',
'api_key',
'session',
'user',
'system_metadata',
'tags',
'tag',
];
const sql: string[] = [];
@@ -174,7 +175,7 @@ export const utils = {
if (table === 'system_metadata') {
sql.push(`DELETE FROM "system_metadata" where "key" NOT IN ('reverse-geocoding-state', 'system-flags');`);
} else {
sql.push(`DELETE FROM ${table} CASCADE;`);
sql.push(`DELETE FROM "${table}" CASCADE;`);
}
}
@@ -450,7 +451,7 @@ export const utils = {
return;
}
await client.query('INSERT INTO asset_faces ("assetId", "personId") VALUES ($1, $2)', [assetId, personId]);
await client.query('INSERT INTO asset_face ("assetId", "personId") VALUES ($1, $2)', [assetId, personId]);
},
setPersonThumbnail: async (personId: string) => {

View File

@@ -0,0 +1,89 @@
import { getUserAdmin } from '@immich/sdk';
import { expect, test } from '@playwright/test';
import { asBearerAuth, utils } from 'src/utils';
test.describe('User Administration', () => {
test.beforeAll(() => {
utils.initSdk();
});
test.beforeEach(async () => {
await utils.resetDatabase();
});
test('validate admin/users link', async ({ context, page }) => {
const admin = await utils.adminSetup();
await utils.setAuthCookies(context, admin.accessToken);
// Navigate to user management page and verify title and header
await page.goto(`/admin/users`);
await expect(page).toHaveTitle(/User Management/);
await expect(page.getByText('User Management')).toBeVisible();
});
test('create user', async ({ context, page }) => {
const admin = await utils.adminSetup();
await utils.setAuthCookies(context, admin.accessToken);
// Create a new user
await page.goto('/admin/users');
await page.getByRole('button', { name: 'Create user' }).click();
await page.getByLabel('Email').fill('user@immich.cloud');
await page.getByLabel('Password', { exact: true }).fill('password');
await page.getByLabel('Confirm Password').fill('password');
await page.getByLabel('Name').fill('Immich User');
await page.getByRole('button', { name: 'Create', exact: true }).click();
// Verify the user exists in the user list
await page.getByRole('row', { name: 'user@immich.cloud' });
});
test('promote to admin', async ({ context, page }) => {
const admin = await utils.adminSetup();
await utils.setAuthCookies(context, admin.accessToken);
const user = await utils.userSetup(admin.accessToken, {
name: 'Admin 2',
email: 'admin2@immich.cloud',
password: 'password',
});
expect(user.isAdmin).toBe(false);
await page.goto(`/admin/users/${user.userId}`);
await page.getByRole('button', { name: 'Edit user' }).click();
await expect(page.getByLabel('Admin User')).not.toBeChecked();
await page.getByText('Admin User').click();
await expect(page.getByLabel('Admin User')).toBeChecked();
await page.getByRole('button', { name: 'Confirm' }).click();
const updated = await getUserAdmin({ id: user.userId }, { headers: asBearerAuth(admin.accessToken) });
expect(updated.isAdmin).toBe(true);
});
test('revoke admin access', async ({ context, page }) => {
const admin = await utils.adminSetup();
await utils.setAuthCookies(context, admin.accessToken);
const user = await utils.userSetup(admin.accessToken, {
name: 'Admin 2',
email: 'admin2@immich.cloud',
password: 'password',
isAdmin: true,
});
expect(user.isAdmin).toBe(true);
await page.goto(`/admin/users/${user.userId}`);
await page.getByRole('button', { name: 'Edit user' }).click();
await expect(page.getByLabel('Admin User')).toBeChecked();
await page.getByText('Admin User').click();
await expect(page.getByLabel('Admin User')).not.toBeChecked();
await page.getByRole('button', { name: 'Confirm' }).click();
const updated = await getUserAdmin({ id: user.userId }, { headers: asBearerAuth(admin.accessToken) });
expect(updated.isAdmin).toBe(false);
});
});

View File

@@ -40,7 +40,6 @@
"backup_keep_last_amount": "Aantal vorige rugsteune om te hou",
"backup_settings": "Rugsteun instellings",
"backup_settings_description": "Bestuur databasis rugsteun instellings",
"check_all": "Kies Alles",
"cleared_jobs": "Poste gevee vir: {job}",
"config_set_by_file": "Config word tans deur 'n konfigurasielêer gestel",
"confirm_delete_library": "Is jy seker jy wil {library}-biblioteek uitvee?",
@@ -55,12 +54,10 @@
"disable_login": "Deaktiveer aanmelding",
"duplicate_detection_job_description": "Begin masjienleer op bates om soortgelyke beelde op te spoor. Maak staat op Smart Search",
"exclusion_pattern_description": "Met uitsluitingspatrone kan jy lêers en vouers ignoreer wanneer jy jou biblioteek skandeer. Dit is nuttig as jy vouers het wat lêers bevat wat jy nie wil invoer nie, soos RAW-lêers.",
"external_library_created_at": "Eksterne biblioteek (geskep op {date})",
"external_library_management": "Eksterne Biblioteekbestuur",
"face_detection": "Gesig deteksie",
"failed_job_command": "Opdrag {command} het misluk vir werk: {job}",
"force_delete_user_warning": "WAARSKUWING: Dit sal onmiddellik die gebruiker en alle bates verwyder. Dit kan nie ontdoen word nie en die lêers kan nie herstel word nie.",
"forcing_refresh_library_files": "Forseer herlaai van alle biblioteeklêers",
"image_format": "Formaat",
"image_format_description": "WebP produseer kleiner lêers as JPEG, maar is stadiger om te enkodeer.",
"image_prefer_embedded_preview": "Verkies ingebedde voorskou",

View File

@@ -43,8 +43,6 @@
"backup_keep_last_amount": "مقدار النسخ الاحتياطية السابقة للاحتفاظ بها",
"backup_settings": "إعدادات النسخ الاحتياطي",
"backup_settings_description": "إدارة إعدادات النسخ الاحتياطي لقاعدة البيانات",
"check_all": "اختر الكل",
"cleanup": "تنظيف",
"cleared_jobs": "تم إخلاء مهام: {job}",
"config_set_by_file": "الإعدادات حاليًا معينة عن طريق ملف الاعدادات",
"confirm_delete_library": "هل أنت متأكد أنك تريد حذف مكتبة {library}؟",
@@ -59,14 +57,12 @@
"disable_login": "تعطيل تسجيل الدخول",
"duplicate_detection_job_description": "بدء التعلم الآلي على المحتوى للعثور على الصور المتشابهة. يعتمد على البحث الذكي",
"exclusion_pattern_description": "تتيح لك أنماط الاستبعاد تجاهل الملفات والمجلدات عند فحص مكتبتك. يعد هذا مفيدًا إذا كان لديك مجلدات تحتوي على ملفات لا تريد استيرادها، مثل ملفات RAW.",
"external_library_created_at": "مكتبة خارجية (أُنشئت في {date})",
"external_library_management": "إدارة المكتبة الخارجية",
"face_detection": "إ‏كتشاف الوجوه",
"face_detection_description": "اكتشف الوجوه في الأصول باستخدام التعلم الآلي. بالنسبة لمقاطع الفيديو، يتم اعتبار الصورة المصغرة فقط. \"تحديث\" (إعادة) معالجة جميع الأصول. \"إعادة تعيين\" تمسح أيضًا جميع بيانات الوجوه الحالية. \"مفقود\" يضع الأصول التي لم تتم معالجتها بعد في قائمة الانتظار. سيتم وضع الوجوه المكتشفة في قائمة الانتظار للتعرف على الوجه بعد اكتمال اكتشاف الوجه، وتجميعها في أشخاص موجودين أو جدد.",
"facial_recognition_job_description": "تجميع الوجوه المكتشفة كأشخاص. يتم تنفيذ هذه الخطوة بعد اكتمال اكتشاف الوجه. خيار \"إعادة التعيين\" يعيد تجميع جميع الوجوه. خيار \"المفقود\" يضع في قائمة الانتظار الوجوه التي لم يتم تعيين شخص لها.",
"failed_job_command": "فشل الأمر {command} للمهمة: {job}",
"force_delete_user_warning": "تحذير: سيؤدي ذلك إلى إزالة المستخدم وجميع محتوياته على الفور. لا يمكن التراجع عن هذا الإجراء ولا يمكن استرداد الملفات.",
"forcing_refresh_library_files": "إجبار التحديث لجميع ملفات المكتبة",
"image_format": "التنسيق",
"image_format_description": "يُنتج WebP ملفات أصغر حجمًا من ملفات JPEG، ولكنه أبطأ في عملية الترميز.",
"image_prefer_embedded_preview": "تفضيل المعاينة المدمجة",
@@ -189,7 +185,7 @@
"oauth_enable_description": "تسجيل الدخول باستخدام OAuth",
"oauth_mobile_redirect_uri": "عنوان URI لإعادة التوجيه على الهاتف",
"oauth_mobile_redirect_uri_override": "تجاوز عنوان URI لإعادة التوجيه على الهاتف",
"oauth_mobile_redirect_uri_override_description": "قم بتفعيله عندما لا يسمح موفر OAuth بمعرف URI للجوال، مثل '{callback}'",
"oauth_mobile_redirect_uri_override_description": "قم بتفعيله عندما لا يسمح موفر OAuth بمعرف URI للجوال، مثل ''{callback}''",
"oauth_settings": "OAuth",
"oauth_settings_description": "إدارة إعدادات تسجيل الدخول OAuth",
"oauth_settings_more_details": "لمزيد من التفاصيل حول هذه الميزة، يرجى الرجوع إلى <link>الوثائق</link>.",
@@ -199,8 +195,6 @@
"oauth_storage_quota_claim_description": "قم تلقائيًا بتعيين حصة التخزين للمستخدم على قيمة هذه المطالبة.",
"oauth_storage_quota_default": "حصة التخزين الافتراضية (جيجابايت)",
"oauth_storage_quota_default_description": "الحصة بالجيجابايت التي سيتم استخدامها عندما لا يتم توفير مطالبة (أدخل 0 لحصة غير محدودة).",
"offline_paths": "مسارات غير متصلة",
"offline_paths_description": "قد تكون هذه النتائج ناتجة عن حذف يدوي لملفات لا تتبع لمكتبة خارجية.",
"password_enable_description": "تسجيل الدخول باستخدام البريد الكتروني وكلمة المرور",
"password_settings": "تسجيل الدخول بكلمة المرور",
"password_settings_description": "إدارة تسجيل الدخول بكلمة المرور",
@@ -210,9 +204,6 @@
"refreshing_all_libraries": "تحديث كافة المكتبات",
"registration": "تسجيل المدير",
"registration_description": "بما أنك أول مستخدم في النظام، سيتم تعيينك كمسؤول وستكون مسؤولًا عن المهام الإدارية، وسيتم إنشاء مستخدمين إضافيين بواسطتك.",
"repair_all": "إصلاح الكل",
"repair_matched_items": "تمت مطابقة {count, plural, one {# عنصر} other {# عناصر}}",
"repaired_items": "تم إصلاح {count, plural, one {# عنصر} other {# عناصر}}",
"require_password_change_on_login": "الطلب من المستخدم تغيير كلمة المرور عند تسجيل الدخول الأول",
"reset_settings_to_default": "إعادة ضبط الإعدادات إلى الوضع الافتراضي",
"reset_settings_to_recent_saved": "إعادة ضبط الإعدادات إلى الإعدادات المحفوظة مؤخرًا",
@@ -241,7 +232,6 @@
"storage_template_migration_info": "تغييرات القالب ستنطبق فقط على المحتويات الجديدة. لتطبيق القالب على المحتويات التي تم رفعها سابقًا، قم بتشغيل <link>{job}</link>.",
"storage_template_migration_job": "وظيفة تهجير قالب التخزين",
"storage_template_more_details": "لمزيد من التفاصيل حول هذه الميزة، يرجى الرجوع إلى <template-link>Storage Template</template-link> و<implications-link>implications</implications-link>",
"storage_template_onboarding_description": "عند تفعيل هذه الميزة، سيقوم بتنظيم الملفات تلقائيًا بناءً على قالب محدد من قبل المستخدم. بسبب مشاكل الاستقرار، تم تعطيل الميزة افتراضيًا. للمزيد من المعلومات، يرجى الرجوع إلى <link>الوثائق</link>.",
"storage_template_path_length": "الحد التقريبي لطول المسار: <b>{length, number}</b>/{limit, number}",
"storage_template_settings": "قالب التخزين",
"storage_template_settings_description": "إدارة هيكل المجلد واسم الملف للأصول المرفوعة",
@@ -253,7 +243,6 @@
"template_email_invite_album": "قالب دعوة الألبوم",
"template_email_preview": "عرض مسبق",
"template_email_settings": "نماذج البريد الالكتروني",
"template_email_settings_description": "إدارة قوالب إشعارات البريد الإلكتروني المخصصة",
"template_email_update_album": "تحديث قالب الألبوم",
"template_email_welcome": "قالب البريد الإلكتروني الترحيبي",
"template_settings": "قوالب الإشعارات",
@@ -262,7 +251,6 @@
"theme_custom_css_settings_description": "أوراق الأنماط المتتالية تسمح بتخصيص تصميم Immich.",
"theme_settings": "إعدادات السمة",
"theme_settings_description": "إدارة تخصيص واجهة ويب Immich",
"these_files_matched_by_checksum": "تتم مطابقة هذه الملفات من خلال المجاميع الاختبارية الخاصة بهم",
"thumbnail_generation_job": "إنشاء الصور المصغرة",
"thumbnail_generation_job_description": "إنشاء صور مصغرة كبيرة وصغيرة وغير واضحة لكل أصل، بالإضافة إلى صور مصغرة لكل شخص",
"transcoding_acceleration_api": "واجهة برمجة التطبيقات للتسريع",
@@ -293,7 +281,6 @@
"transcoding_hardware_acceleration_description": "تجريبي؛ أسرع بكثير، ولكن ستكون جودتها أقل عند نفس معدل البت",
"transcoding_hardware_decoding": "فك تشفير الأجهزة",
"transcoding_hardware_decoding_setting_description": "ينطبق ذلك فقط على NVENC، QSV، و RKMPP. يمكن التسريع من طرف لطرف بدلاً من تسريع الترميز فقط. قد لا يعمل على جميع مقاطع الفيديو.",
"transcoding_hevc_codec": "كود HEVC",
"transcoding_max_b_frames": "أقصى عدد من الإطارات B",
"transcoding_max_b_frames_description": "القيم الأعلى تعزز كفاءة الضغط، ولكنها تبطئ عملية الترميز. قد لا تكون متوافقة مع التسريع العتادي على الأجهزة القديمة. قيمة 0 تعطل إطارات B، بينما تضبط القيمة -1 هذا القيمة تلقائيًا.",
"transcoding_max_bitrate": "الحد الأقصى لمعدل البت",
@@ -331,8 +318,6 @@
"trash_number_of_days_description": "عدد أيام الاحتفاظ بالمحتويات في سلة المهملات قبل حذفها نهائيًا",
"trash_settings": "إعدادات سلة المهملات",
"trash_settings_description": "إدارة إعدادات سلة المهملات",
"untracked_files": "الملفات التي لم يتم تعقبها",
"untracked_files_description": "لا يتم تعقب هذه الملفات بواسطة التطبيق. يمكن أن تكون نتيجة لعمليات نقل فاشلة، أو عمليات تحميل متقطعة، أو يتم تركها في الخلف بسبب خطأ ما",
"user_cleanup_job": "تنظيف المستخدم",
"user_delete_delay": "سيتم جدولة حساب <b>{user}</b> ومحتوياته للحذف النهائي في غضون {delay, plural, one {# يوم} other {# أيام}}.",
"user_delete_delay_settings": "فترة التأخير قبل الحذف",
@@ -382,8 +367,6 @@
"album_remove_user": "هل ترغب في إزالة المستخدم؟",
"album_remove_user_confirmation": "هل أنت متأكد أنك تريد إزالة {user}؟",
"album_share_no_users": "يبدو أنك قمت بمشاركة هذا الألبوم مع جميع المستخدمين أو ليس لديك أي مستخدم للمشاركة معه.",
"album_thumbnail_card_item": "عنصر واحد",
"album_thumbnail_card_shared": " · . مشترك",
"album_updated": "تم تحديث الألبوم",
"album_updated_setting_description": "تلقي إشعارًا عبر البريد الإلكتروني عندما يحتوي الألبوم المشترك على محتويات جديدة",
"album_user_left": "تم ترك {album}",
@@ -563,7 +546,6 @@
"change_pin_code": "تغيير الرقم السري",
"change_your_password": "غير كلمة المرور الخاصة بك",
"changed_visibility_successfully": "تم تغيير الرؤية بنجاح",
"check_all": "تحقق من الكل",
"check_logs": "تحقق من السجلات",
"choose_matching_people_to_merge": "اختر الأشخاص المتطابقين لدمجهم",
"city": "المدينة",
@@ -749,7 +731,6 @@
"cant_get_number_of_comments": "لا يمكن الحصول على عدد التعليقات",
"cant_search_people": "لا يمكن البحث عن الناس",
"cant_search_places": "لا يمكن البحث عن الأماكن",
"cleared_jobs": "اُخليت المهام لـ: {job}",
"error_adding_assets_to_album": "حدث خطأٌ أثناء إضافة المحتويات إلى الألبوم",
"error_adding_users_to_album": "حدث خطأٌ أثناء إضافة المستخدمين إلى الألبوم",
"error_deleting_shared_user": "حدث خطأٌ أثناء حذف المستخدم المشترك",
@@ -758,7 +739,6 @@
"error_removing_assets_from_album": "خطأٌّ في إزالة المحتويات من الألبوم، تحقق من وحدة التحكم للحصول على مزيدٍ من التفاصيل",
"error_selecting_all_assets": "خطأٌ في تحديد جميع المحتويات",
"exclusion_pattern_already_exists": "نمط الاستبعاد هذا موجود مسبقًا.",
"failed_job_command": "فشل الأمر {command} لوظيفة: {job}",
"failed_to_create_album": "فشل إنشاء الألبوم",
"failed_to_create_shared_link": "فشل إنشاء رابط مشترك",
"failed_to_edit_shared_link": "فشل تعديل الرابط المشترك",
@@ -775,7 +755,6 @@
"paths_validation_failed": "فشل في التحقق من {paths, plural, one {# مسار} other {# مسارات}}",
"profile_picture_transparent_pixels": "لا يمكن أن تحتوي صور الملف الشخصي على أجزاء/بكسلات شفافة. يرجى التكبير و/أو تحريك الصورة.",
"quota_higher_than_disk_size": "لقد قمت بتعيين حصة نسبية أعلى من حجم القرص",
"repair_unable_to_check_items": "تعذر التحقق من {count, select, one {عنصر} other {عناصر}}",
"unable_to_add_album_users": "تعذر إضافة مستخدمين إلى الألبوم",
"unable_to_add_assets_to_shared_link": "تعذر إضافة المحتويات إلى الرابط المشترك",
"unable_to_add_comment": "تعذر إضافة التعليق",
@@ -793,7 +772,6 @@
"unable_to_change_visibility": "غير قادر على تغيير الظهور لـ {count, plural, one {# شخص} other {# أشخاص}}",
"unable_to_complete_oauth_login": "غير قادر على إكمال تسجيل الدخول عبر OAuth",
"unable_to_connect": "غير قادر على الإتصال",
"unable_to_connect_to_server": "غير قادر على الإتصال بالسيرفر",
"unable_to_copy_to_clipboard": "لا يمكن النسخ إلى الحافظة، تأكد من استخدامك للصفحة عبر https",
"unable_to_create_admin_account": "غير قادر على إنشاء حساب المسؤول",
"unable_to_create_api_key": "غير قادر على إنشاء مفتاح API جديد",
@@ -817,10 +795,6 @@
"unable_to_hide_person": "غير قادر على إخفاء الشخص",
"unable_to_link_motion_video": "غير قادر على ربط فيديو الحركة",
"unable_to_link_oauth_account": "غير قادر على ربط حساب OAuth",
"unable_to_load_album": "غير قادر على تحميل الألبوم",
"unable_to_load_asset_activity": "غير قادر على تحميل نشاط المحتويات",
"unable_to_load_items": "غير قادر على تحميل العناصر",
"unable_to_load_liked_status": "غير قادر على تحميل حالة الإعجاب",
"unable_to_log_out_all_devices": "غير قادر على تسجيل الخروج من جميع الأجهزة",
"unable_to_log_out_device": "غير قادر على تسجيل الخروج من الجهاز",
"unable_to_login_with_oauth": "غير قادر على تسجيل الدخول باستخدام OAuth",
@@ -831,11 +805,9 @@
"unable_to_remove_album_users": "تعذر إزالة المستخدمين من الألبوم",
"unable_to_remove_api_key": "تعذر إزالة مفتاح API",
"unable_to_remove_assets_from_shared_link": "غير قادر على إزالة المحتويات من الرابط المشترك",
"unable_to_remove_deleted_assets": "غير قادر على إزالة الملفات غير المتصلة",
"unable_to_remove_library": "غير قادر على إزالة المكتبة",
"unable_to_remove_partner": "غير قادر على إزالة الشريك",
"unable_to_remove_reaction": "غير قادر على إزالة رد الفعل",
"unable_to_repair_items": "غير قادر على إصلاح العناصر",
"unable_to_reset_password": "غير قادر على إعادة تعيين كلمة المرور",
"unable_to_resolve_duplicate": "غير قادر على حل التكرارات",
"unable_to_restore_assets": "غير قادر على استعادة المحتويات",
@@ -1150,14 +1122,11 @@
"notifications_setting_description": "إدارة الإشعارات",
"official_immich_resources": "الموارد الرسمية لشركة Immich",
"offline": "غير متصل",
"offline_paths": "مسارات غير متصلة",
"offline_paths_description": "قد تكون هذه النتائج بسبب الحذف اليدوي للملفات التي لا تشكل جزءًا من مكتبة خارجية.",
"ok": "نعم",
"oldest_first": "الأقدم أولا",
"onboarding": "الإعداد الأولي",
"onboarding_privacy_description": "تعتمد الميزات التالية (اختياري) على خدمات خارجية، ويمكن تعطيلها في أي وقت في إعدادات الإدارة.",
"onboarding_theme_description": "اختر نسق الألوان للنسخة الخاصة بك. يمكنك تغيير ذلك لاحقًا في إعداداتك.",
"onboarding_welcome_description": "لنقم بإعداد نسختك باستخدام بعض الإعدادات الشائعة.",
"onboarding_welcome_user": "مرحبا، {user}",
"online": "متصل",
"only_favorites": "المفضلة فقط",
@@ -1450,7 +1419,6 @@
"setting_image_viewer_preview_title": "تحميل صورة معاينة",
"setting_image_viewer_title": "الصور",
"setting_languages_apply": "تغيير الإعدادات",
"setting_languages_title": "اللغات",
"setting_notifications_notify_immediately": "في الحال",
"setting_notifications_notify_never": "أبداً",
"setting_notifications_single_progress_subtitle": "معلومات التقدم التفصيلية تحميل لكل أصل",
@@ -1601,7 +1569,6 @@
"to_parent": "انتقل إلى الوالد",
"to_trash": "حذف",
"toggle_settings": "الإعدادات",
"toggle_theme": "تبديل المظهر الداكن",
"total": "الإجمالي",
"total_usage": "الاستخدام الإجمالي",
"trash": "المهملات",
@@ -1638,8 +1605,6 @@
"unselect_all_duplicates": "إلغاء تحديد كافة النسخ المكررة",
"unstack": "فك الكومه",
"unstacked_assets_count": "تم إخراج {count, plural, one {# الأصل} other {# الأصول}} من التكديس",
"untracked_files": "الملفات التي لم يتم تعقبها",
"untracked_files_decription": "لا يتم تعقب هذه الملفات بواسطة التطبيق. يمكن أن تكون نتيجةً لعمليات نقل فاشلة، أو عمليات رفع متقطعة، أو يتم تركها في الخلف بسبب خللاً ما",
"up_next": "التالي",
"updated_password": "تم تحديث كلمة المرور",
"upload": "رفع",
@@ -1675,10 +1640,6 @@
"version": "الإصدار",
"version_announcement_closing": "صديقك، أليكس",
"version_announcement_message": "مرحبًا! يتوفر إصدار جديد من Immich. يُرجى تخصيص بعض الوقت لقراءة <link>ملاحظات الإصدار</link> للتأكد من تحديث إعداداتك لمنع أي أخطاء في التكوين، خاصة إذا كنت تستخدم WatchTower أو أي آلية تتولى تحديث مثيل Immich الخاص بك تلقائيًا.",
"version_announcement_overlay_release_notes": "ملاحظات الإصدار",
"version_announcement_overlay_text_1": "مرحبًا يا صديقي ، هناك إصدار جديد",
"version_announcement_overlay_text_2": "من فضلك خذ وقتك لزيارة",
"version_announcement_overlay_title": "نسخه جديده متاحه للخادم ",
"version_history": "تاريخ الإصدار",
"version_history_item": "تم تثبيت {version} في {date}",
"video": "فيديو",

View File

@@ -34,18 +34,15 @@
"backup_database_enable_description": "Verilənlər bazasının ehtiyat nüsxələrini aktiv et",
"backup_settings": "Ehtiyat Nüsxə Parametrləri",
"backup_settings_description": "Verilənlər bazasının ehtiyat nüsxə parametrlərini idarə et",
"check_all": "Hamısını yoxla",
"config_set_by_file": "Konfiqurasiya hal-hazırda konfiqurasiya faylı ilə təyin olunub",
"confirm_delete_library": "{library} kitabxanasını silmək istədiyinizdən əminmisiniz?",
"confirm_email_below": "Təsdiqləmək üçün aşağıya {email} yazın",
"confirm_user_password_reset": "{user} adlı istifadəçinin şifrəsini sıfırlamaq istədiyinizdən əminmisiniz?",
"disable_login": "Giriş etməni söndür",
"duplicate_detection_job_description": "Bənzər şəkilləri tapmaq üçün maşın öyrənməsini işə salın. Bu prosses Smart Search funksiyasına əsaslanır",
"external_library_created_at": "Xarici kitabxana ({date} (tarixində yaradıldı)",
"external_library_management": "Xarici kitabxana idarəetməsi",
"face_detection": "Üz tanıma",
"force_delete_user_warning": "XƏBƏRDARLIQ: Bu əməliyyat istifadəçi və bütün məlumatları siləcəkdir. Bu prossesi və silinən faylları geri qaytarmaq olmaz.",
"forcing_refresh_library_files": "Bütün kitabxana fayllarını məcburi yeniləmə",
"image_format_description": "WebP, JPEG faylına görə daha kiçik həcmə sahibdir, lakin onu kodlaşdırmaq daha çox vaxt alır.",
"image_preview_title": "Önizləmə parametrləri",
"image_quality": "Keyfiyyət",

View File

@@ -44,8 +44,6 @@
"backup_keep_last_amount": "Колькасць папярэдніх рэзервовых копій для захавання",
"backup_settings": "Налады рэзервовага капіявання",
"backup_settings_description": "Кіраванне наладамі дампа базы дадзеных. Заўвага: гэтыя задачы не кантралююцца, і ў выпадку няўдачы паведамленне адпраўлена не будзе.",
"check_all": "Праверыць усе",
"cleanup": "Ачыстка",
"cleared_jobs": "Ачышчаны заданні для: {job}",
"config_set_by_file": "Канфігурацыя ў зараз усталявана праз файл канфігурацыі",
"confirm_delete_library": "Вы ўпэўнены што жадаеце выдаліць {library} бібліятэку?",
@@ -60,14 +58,12 @@
"disable_login": "Адключыць уваход",
"duplicate_detection_job_description": "Запусціць машыннае навучанне на актывах для выяўлення падобных выяў. Залежыць ад Smart Search",
"exclusion_pattern_description": "Шаблоны выключэння дазваляюць ігнараваць файлы і папкі пры сканаванні вашай бібліятэкі. Гэта карысна, калі ў вас ёсць папкі, якія змяшчаюць файлы, якія вы не хочаце імпартаваць, напрыклад, файлы RAW.",
"external_library_created_at": "Знешняя бібліятэка (створана {date})",
"external_library_management": "Кіраванне знешняй бібліятэкай",
"face_detection": "Выяўленне твараў",
"face_detection_description": "Выяўляць твары на фотаздымках і відэа з дапамогай машыннага навучання. Для відэа ўлічваецца толькі мініяцюра. \"Абнавіць\" (пера)апрацоўвае ўсе медыя. \"Скінуць\" дадаткова ачышчае ўсе бягучыя дадзеныя пра твары. \"Адсутнічае\" ставіць у чаргу медыя, якія яшчэ не былі апрацаваныя. Выяўленыя твары будуць пастаўлены ў чаргу для распазнавання асоб пасля завяршэння выяўлення твараў, з групаваннем іх па існуючых або новых людзях.",
"facial_recognition_job_description": "Групаваць выяўленыя твары па асобах. Гэты этап выконваецца пасля завяршэння выяўлення твараў. \"Скінуць\" (паўторна) перагрупоўвае ўсе твары. \"Адсутнічае\" ставіць у чаргу твары, якія яшчэ не прыпісаныя да якой-небудзь асобы.",
"failed_job_command": "Каманда {command} не выканалася для задання: {job}",
"force_delete_user_warning": "ПАПЯРЭДЖАННЕ: Гэта дзеянне неадкладна выдаліць карыстальніка і ўсе аб'екты. Гэта дзеянне не можа быць адроблена і файлы немагчыма будзе аднавіць.",
"forcing_refresh_library_files": "Прымусовае абнаўленне ўсіх файлаў бібліятэкі",
"image_format": "Фармат",
"image_format_description": "WebP стварае меншыя файлы, чым JPEG, але павольней кадуе.",
"image_fullsize_description": "Выява ў поўным памеры без метаданых, выкарыстоўваецца пры павелічэнні",

File diff suppressed because it is too large Load Diff

View File

@@ -13,5 +13,7 @@
"add_a_location": "একটি অবস্থান যোগ করুন",
"add_a_name": "একটি নাম যোগ করুন",
"add_a_title": "একটি শিরোনাম যোগ করুন",
"add_endpoint": "এন্ডপয়েন্ট যোগ করুন"
"add_endpoint": "এন্ডপয়েন্ট যোগ করুন",
"add_exclusion_pattern": "বহির্ভূতকরণ নমুনা",
"add_url": "লিঙ্ক যোগ করুন"
}

View File

@@ -22,6 +22,7 @@
"add_partner": "Afegir company/a",
"add_path": "Afegir una ruta",
"add_photos": "Afegir fotografies",
"add_tag": "Afegir una etiqueta",
"add_to": "Afegir a…",
"add_to_album": "Afegir a un l'àlbum",
"add_to_album_bottom_sheet_added": "Afegit a {album}",
@@ -33,6 +34,7 @@
"added_to_favorites_count": "{count, number} afegits als preferits",
"admin": {
"add_exclusion_pattern_description": "Afegeix patrons d'exclusió. Es permet englobar fent ús de *, **, i ?. Per a ignorar els fitxers de qualsevol directori anomenat \"Raw\" introduïu \"**/Raw/**\". Per a ignorar els fitxers acabats en \".tif\" introduïu \"**/*.tif\". Per a ignorar una ruta absoluta, utilitzeu \"/ruta/a/ignorar/**\".",
"admin_user": "Administrador",
"asset_offline_description": "Aquest recurs de la biblioteca externa ja no es troba al disc i s'ha mogut a la paperera. Si el fitxer s'ha mogut dins de la biblioteca, comproveu la vostra línia de temps per trobar el nou recurs corresponent. Per restaurar aquest recurs, assegureu-vos que Immich pugui accedir a la ruta del fitxer següent i escanegeu la biblioteca.",
"authentication_settings": "Configuració de l'autenticació",
"authentication_settings_description": "Gestiona la contrasenya, OAuth i altres configuracions de l'autenticació",
@@ -43,9 +45,7 @@
"backup_database_enable_description": "Habilitar bolcat de la base de dades",
"backup_keep_last_amount": "Quantitat de bolcats anteriors per conservar",
"backup_settings": "Configuració dels bolcats",
"backup_settings_description": "Gestionar la configuració bolcats de la base de dades. Nota: els treballs no es monitoritzen ni es notifiquen les fallades.",
"check_all": "Marca-ho tot",
"cleanup": "Neteja",
"backup_settings_description": "Gestionar la configuració de bolcats de la base de dades. Nota: els treballs no es monitoritzen ni es notifiquen els errors.",
"cleared_jobs": "Tasques esborrades per a: {job}",
"config_set_by_file": "La configuració està definida per un fitxer de configuració",
"confirm_delete_library": "Esteu segurs que voleu eliminar la llibreria {library}?",
@@ -59,16 +59,14 @@
"cron_expression_description": "Estableix l'interval d'escaneig amb el format cron. Per obtenir més informació, consulteu, p.e <link>Crontab Guru</link>",
"cron_expression_presets": "Ajustos predefinits d'expressions Cron",
"disable_login": "Deshabiliteu l'inici de sessió",
"duplicate_detection_job_description": "Executa l'aprenentatge automàtic en els elements per a detectar imatges semblants. Fa servir l'Smart Search",
"duplicate_detection_job_description": "Executa l'aprenentatge automàtic en els elements per a detectar imatges semblants. Fa servir la cerca intel·ligent",
"exclusion_pattern_description": "Els patrons d'exclusió permeten ignorar fitxers i carpetes quan escanegeu una llibreria. Això és útil si teniu carpetes que contenen fitxer que no voleu importar, com els fitxers RAW.",
"external_library_created_at": "Llibreria externa (creada el {date})",
"external_library_management": "Gestió de llibreries externes",
"face_detection": "Detecció de cares",
"face_detection_description": "Detecta les cares fent servir aprenentatge automàtic. Per a videos només és té en compte la miniatura. \"Actualitzar\" reprocessa tots els elements. \"Resetejar\" esborra tota la informació de cares actuals. \"Pendent\" afegeix a la cua els elements que encara no han estat processats. Les cares detectades s'afegiran a la cua per al Reconeixement Facial després de completar la Detecció Facial, tot agrupant-les entre noves persones o les ja existents.",
"facial_recognition_job_description": "Agrupa les cares detectades per persona. Aquest pas s'executa després de completar la detecció de cares. \"Resetejar\" reagrupa totes les cares. \"Pendent\" afegeix a la cua les cares que no tenen cap persona assignada.",
"failed_job_command": "La comanda {command} ha fallat per la tasca: {job}",
"force_delete_user_warning": "COMPTE: Aquesta acció eliminara immediatament l'usuari i els seus elements. Aquesta acció és irreversible i els fitxers no es poden recuperar.",
"forcing_refresh_library_files": "Força l'actualització de tots els fitxers de les biblioteques",
"image_format": "Format",
"image_format_description": "WebP genera fitxers més petits que JPEG, però codifica més lentament.",
"image_fullsize_description": "Imatges a tamany complet sense metadades, utilitzades quan es fa zoom",
@@ -91,7 +89,7 @@
"image_thumbnail_description": "Miniatura petita amb metadades eliminades, que s'utilitza quan es visualitzen grups de fotos com la línia de temps principal",
"image_thumbnail_quality_description": "Qualitat de miniatura d'1 a 100. Més alt és millor, però produeix fitxers més grans i pot reduir la capacitat de resposta de l'aplicació.",
"image_thumbnail_title": "Configuració de miniatures",
"job_concurrency": "{job} concurrència",
"job_concurrency": "{job} simultàniament",
"job_created": "Tasca creada",
"job_not_concurrency_safe": "Aquesta tasca no és segura per a la conconcurrència.",
"job_settings": "Configuració de les tasques",
@@ -107,7 +105,7 @@
"library_scanning_enable_description": "Habilita l'escaneig periòdic de biblioteques",
"library_settings": "Llibreria externes",
"library_settings_description": "Gestiona la configuració de les llibreries externes",
"library_tasks_description": "Escaneja biblioteques externes per arxius nous o canviats",
"library_tasks_description": "Escaneja les biblioteques externes per trobar arxius nous o canviats",
"library_watching_enable_description": "Consultar llibreries externes per detectar canvis en fitxers",
"library_watching_settings": "Monitoratge de la llibreria (EXPERIMENTAL)",
"library_watching_settings_description": "Monitorització automàtica de fitxers modificats",
@@ -115,19 +113,19 @@
"logging_level_description": "Quan està habilitat, quin nivell de registre es vol emprar.",
"logging_settings": "Registre",
"machine_learning_clip_model": "Model CLIP",
"machine_learning_clip_model_description": "El nom d'un model CLIP que apareix a <link>aquí</link>. Tingues en compte que has de tornar a executar l'Smart Search' per a totes les imatges quan es canvia de model.",
"machine_learning_clip_model_description": "El nom d'un model CLIP que apareix a <link>aquí</link>. Tingues en compte que has de tornar a executar la cerca intel·ligent per a totes les imatges quan es canvia de model.",
"machine_learning_duplicate_detection": "Detecció de duplicats",
"machine_learning_duplicate_detection_enabled": "Activa detecció de duplicats",
"machine_learning_duplicate_detection_enabled_description": "Si es deshabilitat, els elements exactament idèntics encara es desduplicaran.",
"machine_learning_duplicate_detection_enabled": "Activa la detecció de duplicats",
"machine_learning_duplicate_detection_enabled_description": "Si es desactivada, els elements idèntics encara es desduplicaran.",
"machine_learning_duplicate_detection_setting_description": "Usa incrustacions CLIP per a trobar prossibles duplicats",
"machine_learning_enabled": "Activa l'aprenentatge automàtic",
"machine_learning_enabled_description": "Si està deshabilitat, totes les funcions ML es deshabilitaran sense tenir en compte la configuració següent.",
"machine_learning_enabled_description": "Si està desactivat, totes les funcions ML es deshabilitaran sense tenir en compte la configuració següent.",
"machine_learning_facial_recognition": "Reconeixement facial",
"machine_learning_facial_recognition_description": "Detecta, reconeix i agrupa cares a les imatges",
"machine_learning_facial_recognition_model": "Model de reconeixement facial",
"machine_learning_facial_recognition_model_description": "Els models es llisten en ordre descent segons la mida. Els models més grans són més lents i usen més memòria però produeixen millors resultats. Tingueu en compte que després de canviar un model haureu de tornar a executar la tasca de detecció de cares per a totes les imatges.",
"machine_learning_facial_recognition_setting": "Activa reconeixement facial",
"machine_learning_facial_recognition_setting_description": "Si està deshabilitat, les imatges no es codificaran pel reconeixement facial i no s'afegiran a la secció Persones de la pàgina Explorar.",
"machine_learning_facial_recognition_setting": "Activa el reconeixement facial",
"machine_learning_facial_recognition_setting_description": "Si està desactivat, les imatges no es codificaran pel reconeixement facial i no s'afegiran a la secció Persones de la pàgina Explorar.",
"machine_learning_max_detection_distance": "Distància màxima de detecció",
"machine_learning_max_detection_distance_description": "Diferència màxima entre dues imatges per a considerar-les duplicades, en un rang d'entre 0.001-0.1. Com més elevat el valor més detecció de duplicats, però pot resultar en falsos positius.",
"machine_learning_max_recognition_distance": "Màxima diferència de reconeixement",
@@ -138,17 +136,17 @@
"machine_learning_min_recognized_faces_description": "El nombre mínim de cares reconegudes per crear una persona. Augmentar aquest valor fa que el reconeixement facial sigui més precís, però augmenta la possibilitat que una cara no sigui assignada a una persona.",
"machine_learning_settings": "Configuració d'aprenentatge automàtic",
"machine_learning_settings_description": "Gestiona funcions i configuració d'aprenentatge automàtic",
"machine_learning_smart_search": "Cerca Intel·ligent",
"machine_learning_smart_search": "Cerca intel·ligent",
"machine_learning_smart_search_description": "Cerca imatges semànticament emprant incrustacions CLIP",
"machine_learning_smart_search_enabled": "Activa la cerca intel·ligent",
"machine_learning_smart_search_enabled_description": "Si està deshabilitat les imatges no es codificaran per la cerca intel·ligent.",
"machine_learning_smart_search_enabled_description": "Si està desactivada, les imatges no es codificaran per la cerca intel·ligent.",
"machine_learning_url_description": "L'URL del servidor d'aprenentatge automàtic. Si es proporciona més d'un URL, s'intentarà accedir a cada servidor en ordre fins que un d'ells respongui correctament.",
"manage_concurrency": "Gestiona la concurrència",
"manage_log_settings": "Gestiona la configuració del registre",
"map_dark_style": "Tema fosc",
"map_enable_description": "Habilita característiques del mapa",
"map_gps_settings": "Configuració de mapa i GPS",
"map_gps_settings_description": "Gestiona la configuració de mapa i GPS (Geocodificació inversa)",
"map_gps_settings": "Configuració del mapa i GPS",
"map_gps_settings_description": "Gestiona la configuració del mapa i GPS (Geocodificació inversa)",
"map_implications": "La funció mapa depèn del servei extern de tesel·les (tiles.immich.cloud)",
"map_light_style": "Tema clar",
"map_manage_reverse_geocoding_settings": "Gestiona els paràmetres de <link>geocodificació inversa</link>",
@@ -173,7 +171,7 @@
"note_apply_storage_label_previous_assets": "Nota: Per aplicar l'etiquetatge d'emmagatzematge a elements pujats prèviament, executeu la",
"note_cannot_be_changed_later": "NOTA: Això és irreversible!",
"notification_email_from_address": "Des de l'adreça",
"notification_email_from_address_description": "Adreça de correu electrònic del remitent, per exemple: \"Immich Photo Server <noreply@example.com>\"",
"notification_email_from_address_description": "Adreça de correu electrònic del remitent, per exemple: \"Immich Photo Server <noreply@example.com>\". Assegureu-vos d'utilitzar una adreça des de la qual tingueu permís per enviar correus electrònics.",
"notification_email_host_description": "Amfitrió del servidor de correu electrònic (p.ex. smtp.immich.app)",
"notification_email_ignore_certificate_errors": "Ignora els errors de certificat",
"notification_email_ignore_certificate_errors_description": "Ignora els errors de validació de certificat TLS (no recomanat)",
@@ -197,7 +195,7 @@
"oauth_enable_description": "Iniciar sessió amb OAuth",
"oauth_mobile_redirect_uri": "URI de redirecció mòbil",
"oauth_mobile_redirect_uri_override": "Sobreescriu l'URI de redirecció mòbil",
"oauth_mobile_redirect_uri_override_description": "Habilita quan el proveïdor d'OAuth no permet una URI mòbil, com ara '{callback}'",
"oauth_mobile_redirect_uri_override_description": "Habilita quan el proveïdor d'OAuth no permet una URI mòbil, com ara ''{callback}''",
"oauth_settings": "OAuth",
"oauth_settings_description": "Gestiona la configuració de l'inici de sessió OAuth",
"oauth_settings_more_details": "Per a més detalls sobre aquesta funcionalitat, consulteu la <link>documentació</link>.",
@@ -209,8 +207,6 @@
"oauth_storage_quota_default_description": "Quota disponible en GB quan no s'estableixi cap valor (Entreu 0 per a quota il·limitada).",
"oauth_timeout": "Solicitud caducada",
"oauth_timeout_description": "Timeout per a sol·licituds en mil·lisegons",
"offline_paths": "Rutes sense connexió",
"offline_paths_description": "Aquests resultats poden ser deguts a l'eliminació manual de fitxers que no formen part d'una llibreria externa.",
"password_enable_description": "Inicia sessió amb correu electrònic i contrasenya",
"password_settings": "Inici de sessió amb contrasenya",
"password_settings_description": "Gestiona la configuració de l'inici de sessió amb contrasenya",
@@ -220,9 +216,6 @@
"refreshing_all_libraries": "Actualitzant totes les biblioteques",
"registration": "Registre d'administrador",
"registration_description": "Com que ets el primer usuari del sistema, seràs designat com a administrador i seràs responsable de les tasques administratives. També seràs l'encarregat de crear usuaris addicionals.",
"repair_all": "Reparar tot",
"repair_matched_items": "Coincidència {count, plural, one {# element} other {# elements}}",
"repaired_items": "Corregit {count, plural, one {# element} other {# elements}}",
"require_password_change_on_login": "Requerir que l'usuari canviï la contrasenya en el primer inici de sessió",
"reset_settings_to_default": "Restablir configuracions per defecte",
"reset_settings_to_recent_saved": "Restablir la configuració guardada més recent",
@@ -251,7 +244,6 @@
"storage_template_migration_info": "Les extensions es convertiran a minúscules. Els canvis de plantilla només s'aplicaran a nous elements. Per aplicar la plantilla rectroactivament a elements pujats prèviament, executeu la <link>{job}</link>.",
"storage_template_migration_job": "Tasca de migració de la plantilla d'emmagatzematge",
"storage_template_more_details": "Per obtenir més detalls sobre aquesta funció, consulteu la <template-link>Storage Template</template-link> i les seves <implications-link>implications</implications-link>",
"storage_template_onboarding_description": "Quan està activada, aquesta funció organitzarà automàticament els fitxers en funció d'una plantilla definida per l'usuari. A causa de problemes d'estabilitat, la funció s'ha desactivat de manera predeterminada. Per obtenir més informació, consulteu la <link>documentation</link>.",
"storage_template_path_length": "Límit aproximat de longitud de la ruta: <b>{length, number}</b>/{limit, number}",
"storage_template_settings": "Plantilla d'emmagatzematge",
"storage_template_settings_description": "Gestiona l'estructura de les carpetes i el nom del fitxers dels elements pujats",
@@ -263,16 +255,14 @@
"template_email_invite_album": "Plantilla per l'àlbum d'invitacions",
"template_email_preview": "Vista prèvia",
"template_email_settings": "Plantilles de correu electrònic",
"template_email_settings_description": "Gestionar les plantilles de notificació per correu electrònic personalitzades",
"template_email_update_album": "Actualitzar la plantilla de l'àlbum",
"template_email_welcome": "Plantilla del correu de benvinguda",
"template_settings": "Plantilles de notificació",
"template_settings_description": "Gestiona les plantilles personalitzades per les notificacions.",
"template_settings_description": "Gestiona les plantilles personalitzades per les notificacions",
"theme_custom_css_settings": "CSS personalitzat",
"theme_custom_css_settings_description": "Els Fulls d'Estil en Cascada permeten personalitzar el disseny d'Immich.",
"theme_custom_css_settings_description": "Els fulls d'estil en cascada permeten personalitzar el disseny d'Immich.",
"theme_settings": "Configuració del tema",
"theme_settings_description": "Gestiona la personalització de la interfície web Immich",
"these_files_matched_by_checksum": "Aquests fitxers coincideixen amb els seus checksums",
"thumbnail_generation_job": "Generar miniatures",
"thumbnail_generation_job_description": "Genera miniatures grans, petites i borroses per a cada element, així com miniatures per a cada persona",
"transcoding_acceleration_api": "API d'acceleració",
@@ -300,10 +290,9 @@
"transcoding_encoding_options": "Opcions de codificació",
"transcoding_encoding_options_description": "Establiu còdecs, resolució, qualitat i altres opcions per als vídeos codificats",
"transcoding_hardware_acceleration": "Acceleració de maquinari",
"transcoding_hardware_acceleration_description": "Experimental. Molt més ràpid, però tindrà una qualitat més baixa amb la mateixa taxa de bits",
"transcoding_hardware_acceleration_description": "Experimental: transcodificació més ràpida però pot perdre qualitat amb la mateixa tasa de bits",
"transcoding_hardware_decoding": "Descodificació de maquinari",
"transcoding_hardware_decoding_setting_description": "Habilita l'acceleració d'extrem a extrem en lloc d'accelerar només la codificació. És possible que no funcioni en tots els vídeos.",
"transcoding_hevc_codec": "Còdec HEVC",
"transcoding_max_b_frames": "Nombre màxim de B-frames",
"transcoding_max_b_frames_description": "Els valors més alts milloren l'eficiència de la compressió, però alenteixen la codificació. És possible que no sigui compatible amb l'acceleració de maquinari en dispositius antics. 0 desactiva els B-frames, mentre que -1 estableix aquest valor automàticament.",
"transcoding_max_bitrate": "Taxa de bits màxima",
@@ -341,8 +330,6 @@
"trash_number_of_days_description": "Nombre de dies per mantenir els recursos a la paperera abans de suprimir-los permanentment",
"trash_settings": "Configuració de la paperera",
"trash_settings_description": "Gestiona la configuració de la paperera",
"untracked_files": "Fitxers sense seguiment",
"untracked_files_description": "L'aplicació no fa un seguiment d'aquests fitxers. Poden ser el resultat de moviments fallits, càrregues interrompudes o deixades enrere a causa d'un error",
"user_cleanup_job": "Neteja d'usuari",
"user_delete_delay": "El compte i els recursos de <b>{user}</b> es programaran per a la supressió permanent en {delay, plural, one {# dia} other {# dies}}.",
"user_delete_delay_settings": "Retard de la supressió",
@@ -367,7 +354,7 @@
},
"admin_email": "Correu de l'administrador",
"admin_password": "Contrasenya de l'administrador",
"administration": "Administrador",
"administration": "Administració",
"advanced": "Avançat",
"advanced_settings_enable_alternate_media_filter_subtitle": "Feu servir aquesta opció per filtrar els continguts multimèdia durant la sincronització segons criteris alternatius. Només proveu-ho si teniu problemes amb l'aplicació per detectar tots els àlbums.",
"advanced_settings_enable_alternate_media_filter_title": "Utilitza el filtre de sincronització d'àlbums de dispositius alternatius",
@@ -401,10 +388,6 @@
"album_remove_user": "Eliminar l'usuari?",
"album_remove_user_confirmation": "Esteu segurs que voleu eliminar {user}?",
"album_share_no_users": "Sembla que has compartit aquest àlbum amb tots els usuaris o no tens cap usuari amb qui compartir-ho.",
"album_thumbnail_card_item": "1 element",
"album_thumbnail_card_items": "{count} elements",
"album_thumbnail_card_shared": " · Compartit",
"album_thumbnail_shared_by": "Compartit per {user}",
"album_updated": "Àlbum actualitzat",
"album_updated_setting_description": "Rep una notificació per correu electrònic quan un àlbum compartit tingui recursos nous",
"album_user_left": "Surt de {album}",
@@ -420,6 +403,9 @@
"album_with_link_access": "Permet que qualsevol persona que tingui l'enllaç vegi fotos i persones d'aquest àlbum.",
"albums": "Àlbums",
"albums_count": "{count, plural, one {{count, number} Àlbum} other {{count, number} Àlbums}}",
"albums_default_sort_order": "Ordre per defecte de l'àlbum",
"albums_default_sort_order_description": "Ordre de classificació inicial dels recursos al crear àlbums nous.",
"albums_feature_description": "Col·leccions d'actius que es poden compartir amb altres usuaris.",
"all": "Tots",
"all_albums": "Tots els àlbum",
"all_people": "Tota la gent",
@@ -478,9 +464,12 @@
"assets_added_count": "{count, plural, one {Afegit un element} other {Afegits # elements}}",
"assets_added_to_album_count": "{count, plural, one {Afegit un element} other {Afegits # elements}} a l'àlbum",
"assets_added_to_name_count": "{count, plural, one {S'ha afegit # recurs} other {S'han afegit # recursos}} a {hasName, select, true {<b>{name}</b>} other {new album}}",
"assets_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} no es pot afegir a l'àlbum",
"assets_count": "{count, plural, one {# recurs} other {# recursos}}",
"assets_deleted_permanently": "{count} element(s) esborrats permanentment",
"assets_deleted_permanently_from_server": "{count} element(s) esborrats permanentment del servidor d'Immich",
"assets_downloaded_failed": "{count, plural, one {S'ha baixat un arxiu - {error} l'arxiu ha fallat} other {S'han baixat # arxius - {error} els arxius han fallat}}",
"assets_downloaded_successfully": "{count, plural, one {S'ha baixat un arxiu amb èxit} other {S'han baixat # arxius amb èxit}}",
"assets_moved_to_trash_count": "{count, plural, one {# recurs mogut} other {# recursos moguts}} a la paperera",
"assets_permanently_deleted_count": "{count, plural, one {# recurs esborrat} other {# recursos esborrats}} permanentment",
"assets_removed_count": "{count, plural, one {# element eliminat} other {# elements eliminats}}",
@@ -495,6 +484,7 @@
"authorized_devices": "Dispositius autoritzats",
"automatic_endpoint_switching_subtitle": "Connecteu-vos localment a través de la Wi-Fi designada quan estigui disponible i utilitzeu connexions alternatives en altres llocs",
"automatic_endpoint_switching_title": "Canvi automàtic d'URL",
"autoplay_slideshow": "Reprodueix automàticament les diapositives",
"back": "Enrere",
"back_close_deselect": "Tornar, tancar o anul·lar la selecció",
"background_location_permission": "Permís d'ubicació en segon pla",
@@ -538,6 +528,7 @@
"backup_controller_page_excluded": "Exclosos: ",
"backup_controller_page_failed": "Fallats ({count})",
"backup_controller_page_filename": "Nom de l'arxiu: {filename} [{size}]",
"backup_controller_page_id": "ID: {id}",
"backup_controller_page_info": "Informació de la còpia",
"backup_controller_page_none_selected": "Cap seleccionat",
"backup_controller_page_remainder": "Restant",
@@ -561,6 +552,10 @@
"backup_options_page_title": "Opcions de còpia de seguretat",
"backup_setting_subtitle": "Gestiona la configuració de càrrega en segon pla i en primer pla",
"backward": "Enrere",
"biometric_auth_enabled": "Autentificació biomètrica activada",
"biometric_locked_out": "Esteu bloquejats fora de l'autenticació biomètrica",
"biometric_no_options": "No hi ha opcions biomètriques disponibles",
"biometric_not_available": "L'autenticació biomètrica no està disponible en aquest dispositiu",
"birthdate_saved": "Data de naixement guardada amb èxit",
"birthdate_set_description": "La data de naixement s'utilitza per calcular l'edat d'aquesta persona en el moment d'una foto.",
"blurred_background": "Fons difuminat",
@@ -571,21 +566,17 @@
"bulk_keep_duplicates_confirmation": "Esteu segur que voleu mantenir {count, plural, one {# recurs duplicat} other {# recursos duplicats}}? Això resoldrà tots els grups duplicats sense eliminar res.",
"bulk_trash_duplicates_confirmation": "Esteu segur que voleu enviar a les escombraries {count, plural, one {# recurs duplicat} other {# recursos duplicats}}? Això mantindrà el recurs més gran de cada grup i eliminarà la resta de duplicats.",
"buy": "Comprar Immich",
"cache_settings_album_thumbnails": "Miniatures de la pàgina de la biblioteca ({count} elements)",
"cache_settings_clear_cache_button": "Neteja la memòria cau",
"cache_settings_clear_cache_button_title": "Neteja la memòria cau de l'aplicació. Això impactarà significativament el rendiment fins que la memòria cau es torni a reconstruir.",
"cache_settings_duplicated_assets_clear_button": "NETEJA",
"cache_settings_duplicated_assets_subtitle": "Fotos i vídeos que estan a la llista negra de l'aplicació",
"cache_settings_duplicated_assets_title": "Elements duplicats ({count})",
"cache_settings_image_cache_size": "Mida de la memòria cau d'imatges ({count} elements)",
"cache_settings_statistics_album": "Miniatures de la biblioteca",
"cache_settings_statistics_assets": "{count} elements ({size})",
"cache_settings_statistics_full": "Imatges completes",
"cache_settings_statistics_shared": "Miniatures d'àlbums compartits",
"cache_settings_statistics_thumbnail": "Miniatures",
"cache_settings_statistics_title": "Ús de memòria cau",
"cache_settings_subtitle": "Controla el comportament de la memòria cau de l'aplicació mòbil Immich",
"cache_settings_thumbnail_size": "Mida de la memòria cau de les miniatures ({count} elements)",
"cache_settings_tile_subtitle": "Controla el comportament de l'emmagatzematge local",
"cache_settings_tile_title": "Emmagatzematge local",
"cache_settings_title": "Configuració de la memòria cau",
@@ -598,7 +589,10 @@
"cannot_merge_people": "No es pot fusionar gent",
"cannot_undo_this_action": "Aquesta acció no es pot desfer!",
"cannot_update_the_description": "No es pot actualitzar la descripció",
"cast": "Emet",
"cast_description": "Configurar les destinacions de transmissió disponibles",
"change_date": "Canvia la data",
"change_description": "Canvia la descripció",
"change_display_order": "Canvia l'ordre de visualització",
"change_expiration_time": "Canvia la data d'expiració",
"change_location": "Canvia la ubicació",
@@ -614,7 +608,6 @@
"change_pin_code": "Canviar el codi PIN",
"change_your_password": "Canvia la teva contrasenya",
"changed_visibility_successfully": "Visibilitat canviada amb èxit",
"check_all": "Marqueu-ho tot",
"check_corrupt_asset_backup": "Comprovar les còpies de seguretat corruptes",
"check_corrupt_asset_backup_button": "Realitzar comprovació",
"check_corrupt_asset_backup_description": "Executeu aquesta comprovació només mitjançant Wi-Fi i un cop s'hagi fet una còpia de seguretat de tots els actius. El procediment pot trigar uns minuts.",
@@ -626,6 +619,7 @@
"clear_all_recent_searches": "Esborra totes les cerques recents",
"clear_message": "Neteja el missatge",
"clear_value": "Neteja el valor",
"client_cert_dialog_msg_confirm": "OK",
"client_cert_enter_password": "Introdueix la contrasenya",
"client_cert_import": "Importar",
"client_cert_import_success_msg": "S'ha importat el certificat del client",
@@ -637,6 +631,7 @@
"close": "Tanca",
"collapse": "Tanca",
"collapse_all": "Redueix-ho tot",
"color": "Color",
"color_theme": "Tema de color",
"comment_deleted": "Comentari esborrat",
"comment_options": "Opcions de comentari",
@@ -652,9 +647,13 @@
"confirm_keep_this_delete_others": "Excepte aquest element, tots els altres de la pila se suprimiran. Esteu segur que voleu continuar?",
"confirm_new_pin_code": "Confirma el nou codi PIN",
"confirm_password": "Confirmació de contrasenya",
"confirm_tag_face": "Vols etiquetar aquesta cara com a {name}?",
"confirm_tag_face_unnamed": "Com vols etiquetar aquesta cara?",
"connected_device": "Dispositiu connectat",
"connected_to": "Connectat a",
"contain": "Contingut",
"context": "Context",
"continue": "Continuar",
"control_bottom_app_bar_album_info_shared": "{count} elements - Compartits",
"control_bottom_app_bar_create_new_album": "Crea un àlbum nou",
"control_bottom_app_bar_delete_from_immich": "Suprimeix del Immich",
"control_bottom_app_bar_delete_from_local": "Suprimeix del dispositiu",
@@ -703,6 +702,7 @@
"daily_title_text_date": "E, dd MMM",
"daily_title_text_date_year": "E, dd MMM, yyyy",
"dark": "Fosc",
"darkTheme": "Activa/desactiva el tema fosc",
"date_after": "Data posterior a",
"date_and_time": "Data i hora",
"date_before": "Data anterior a",
@@ -748,7 +748,9 @@
"direction": "Direcció",
"disabled": "Desactivat",
"disallow_edits": "No permetre les edicions",
"discord": "Discord",
"discover": "Descobreix",
"discovered_devices": "Dispositius descoberts",
"dismiss_all_errors": "Descarta tots els errors",
"dismiss_error": "Descarta l'error",
"display_options": "Opcions de visualització",
@@ -764,7 +766,6 @@
"download_enqueue": "Descàrrega en cua",
"download_error": "Error de descàrrega",
"download_failed": "Descàrrega ha fallat",
"download_filename": "arxiu: {filename}",
"download_finished": "Descàrrega acabada",
"download_include_embedded_motion_videos": "Vídeos incrustats",
"download_include_embedded_motion_videos_description": "Incloure vídeos incrustats en fotografies en moviment com un arxiu separat",
@@ -788,6 +789,8 @@
"edit_avatar": "Edita l'avatar",
"edit_date": "Edita la data",
"edit_date_and_time": "Edita data i hora",
"edit_description": "Edita la descripció",
"edit_description_prompt": "Si us plau, selecciona una nova descripció:",
"edit_exclusion_pattern": "Edita patró d'exclusió",
"edit_faces": "Edita les cares",
"edit_import_path": "Edita la ruta d'importació",
@@ -802,6 +805,7 @@
"edit_title": "Edita títol",
"edit_user": "Edita l'usuari",
"edited": "Editat",
"editor": "Editor",
"editor_close_without_save_prompt": "No es desaran els canvis",
"editor_close_without_save_title": "Tancar l'editor?",
"editor_crop_tool_h2_aspect_ratios": "Relació d'aspecte",
@@ -812,13 +816,19 @@
"empty_trash": "Buidar la paperera",
"empty_trash_confirmation": "Esteu segur que voleu buidar la paperera? Això eliminarà tots els recursos a la paperera permanentment d'Immich.\nNo podeu desfer aquesta acció!",
"enable": "Activar",
"enable_biometric_auth_description": "Introduïu el codi PIN per a habilitar l'autenticació biomètrica",
"enabled": "Activat",
"end_date": "Data final",
"enqueued": "En cua",
"enter_wifi_name": "Introdueix el nom de Wi-Fi",
"enter_your_pin_code": "Introduïu el codi PIN",
"enter_your_pin_code_subtitle": "Introduïu el codi PIN per a accedir a la carpeta protegida",
"error": "Error",
"error_change_sort_album": "No s'ha pogut canviar l'ordre d'ordenació dels àlbums",
"error_delete_face": "Error esborrant cara de les cares reconegudes",
"error_loading_image": "Error carregant la imatge",
"error_saving_image": "Error: {error}",
"error_tag_face_bounding_box": "Error a l'etiquetar la cara - no s'han pogut obtenir les coordenades de l'àrea",
"error_title": "Error - Quelcom ha anat malament",
"errors": {
"cannot_navigate_next_asset": "No es pot navegar a l'element següent",
@@ -831,7 +841,6 @@
"cant_get_number_of_comments": "No es pot obtenir el nombre de comentaris",
"cant_search_people": "No es poden cercar persones",
"cant_search_places": "No es poden cercar llocs",
"cleared_jobs": "Tasques buides per a: {job}",
"error_adding_assets_to_album": "Error afegint elements a l'àlbum",
"error_adding_users_to_album": "Error afegint usuaris a l'àlbum",
"error_deleting_shared_user": "S'ha produït un error en suprimir l'usuari compartit",
@@ -840,7 +849,6 @@
"error_removing_assets_from_album": "Error eliminant els elements de l'àlbum, consulteu la consola per obtenir més detalls",
"error_selecting_all_assets": "Error seleccionant tots els elements",
"exclusion_pattern_already_exists": "Aquest patró dexclusió ja existeix.",
"failed_job_command": "L'ordre {command} ha fallat per a la tasca: {job}",
"failed_to_create_album": "No s'ha pogut crear l'àlbum",
"failed_to_create_shared_link": "No s'ha pogut crear l'enllaç compartit",
"failed_to_edit_shared_link": "No s'ha pogut editar l'enllaç compartit",
@@ -859,7 +867,6 @@
"paths_validation_failed": "{paths, plural, one {# ruta} other {# rutes}} no ha pogut validar",
"profile_picture_transparent_pixels": "Les fotos de perfil no poden tenir píxels transparents. Per favor, feu zoom in, mogueu la imatge o ambdues.",
"quota_higher_than_disk_size": "Heu establert una quota més gran que la mida de disc",
"repair_unable_to_check_items": "No es pot comprovar {count, select, one {l'element} other {els elements}}",
"unable_to_add_album_users": "No es poden afegir usuaris a l'àlbum",
"unable_to_add_assets_to_shared_link": "No s'han pogut afegir els elements a l'enllaç compartit",
"unable_to_add_comment": "No es pot afegir el comentari",
@@ -871,13 +878,13 @@
"unable_to_archive_unarchive": "No es pot {archived, select, true {arxivar} other {desarxivar}}",
"unable_to_change_album_user_role": "No es pot canviar el rol d'usuari de l'àlbum",
"unable_to_change_date": "No es pot canviar la data",
"unable_to_change_description": "No s'ha pogut canviar la descripció",
"unable_to_change_favorite": "No es pot canviar el favorit per a aquest recurs",
"unable_to_change_location": "No es pot canviar la ubicació",
"unable_to_change_password": "No es pot canviar la contrasenya",
"unable_to_change_visibility": "No es pot canviar la visibilitat de {count, plural, one {# persona} other {# persones}}",
"unable_to_complete_oauth_login": "No es pot completar l'inici de sessió OAuth",
"unable_to_connect": "No pot connectar",
"unable_to_connect_to_server": "No es pot connectar al servidor",
"unable_to_copy_to_clipboard": "No es pot copiar al porta-retalls, assegureu-vos que esteu accedint a la pàgina mitjançant https",
"unable_to_create_admin_account": "No es pot crear un compte d'administrador",
"unable_to_create_api_key": "No es pot crear una clau d'API nova",
@@ -901,10 +908,6 @@
"unable_to_hide_person": "No es pot amagar la persona",
"unable_to_link_motion_video": "No es pot enllaçar el vídeo en moviment",
"unable_to_link_oauth_account": "No es pot enllaçar el compte OAuth",
"unable_to_load_album": "No es pot carregar l'àlbum",
"unable_to_load_asset_activity": "No es pot carregar l'activitat dels recursos",
"unable_to_load_items": "No es poden carregar els elements",
"unable_to_load_liked_status": "No es pot carregar l'estat de m'agrada",
"unable_to_log_out_all_devices": "No es poden tancar la sessió de tots els dispositius",
"unable_to_log_out_device": "No es pot tancar la sessió del dispositiu",
"unable_to_login_with_oauth": "No es pot iniciar sessió amb OAuth",
@@ -915,11 +918,9 @@
"unable_to_remove_album_users": "No es poden eliminar usuaris de l'àlbum",
"unable_to_remove_api_key": "No es pot eliminar la clau de l'API",
"unable_to_remove_assets_from_shared_link": "No es poden eliminar recursos de l'enllaç compartit",
"unable_to_remove_deleted_assets": "No es poden eliminar els fitxers fora de línia",
"unable_to_remove_library": "No es pot eliminar la biblioteca",
"unable_to_remove_partner": "No es pot eliminar company/a",
"unable_to_remove_reaction": "No es pot eliminar la reacció",
"unable_to_repair_items": "No es poden reparar els elements",
"unable_to_reset_password": "No es pot restablir la contrasenya",
"unable_to_reset_pin_code": "No es pot restablir el codi PIN",
"unable_to_resolve_duplicate": "No es pot resoldre el duplicat",
@@ -949,12 +950,12 @@
"unable_to_update_user": "No es pot actualitzar l'usuari",
"unable_to_upload_file": "No es pot carregar el fitxer"
},
"exif": "EXIF",
"exif_bottom_sheet_description": "Afegeix descripció...",
"exif_bottom_sheet_details": "DETALLS",
"exif_bottom_sheet_location": "UBICACIÓ",
"exif_bottom_sheet_people": "PERSONES",
"exif_bottom_sheet_person_add_person": "Afegir nom",
"exif_bottom_sheet_person_age": "Edat {age}",
"exif_bottom_sheet_person_age_months": "Edat {months} mesos",
"exif_bottom_sheet_person_age_year_months": "Edat 1 any, {months} mesos",
"exif_bottom_sheet_person_age_years": "Edat {years}",
@@ -963,6 +964,7 @@
"experimental_settings_new_asset_list_subtitle": "Treball en curs",
"experimental_settings_new_asset_list_title": "Habilita la graella de fotos experimental",
"experimental_settings_subtitle": "Utilitzeu-ho sota la vostra responsabilitat!",
"experimental_settings_title": "Experimental",
"expire_after": "Caduca després de",
"expired": "Caducat",
"expires_date": "Caduca el {date}",
@@ -977,6 +979,7 @@
"external_network_sheet_info": "Quan no estigui a la xarxa Wi-Fi preferida, l'aplicació es connectarà al servidor mitjançant el primer dels URL següents a què pot arribar, començant de dalt a baix",
"face_unassigned": "Sense assignar",
"failed": "Fallat",
"failed_to_authenticate": "No s'ha pogut autenticar",
"failed_to_load_assets": "Error carregant recursos",
"failed_to_load_folder": "No s'ha pogut carregar la carpeta",
"favorite": "Preferit",
@@ -1000,6 +1003,9 @@
"folders": "Carpetes",
"folders_feature_description": "Explorar la vista de carpetes per les fotos i vídeos del sistema d'arxius",
"forward": "Endavant",
"gcast_enabled": "Google Cast",
"gcast_enabled_description": "Aquesta funció carrega recursos externs de Google per funcionar.",
"general": "General",
"get_help": "Aconseguir ajuda",
"get_wifiname_error": "No s'ha pogut obtenir el nom de la Wi-Fi. Assegureu-vos que heu concedit els permisos necessaris i que esteu connectat a una xarxa Wi-Fi",
"getting_started": "Començant",
@@ -1041,10 +1047,13 @@
"home_page_favorite_err_local": "Encara no es pot afegir a preferits elements locals, ometent",
"home_page_favorite_err_partner": "Encara no es pot afegir a preferits elements de companys, ometent",
"home_page_first_time_notice": "Si és la primera vegada que utilitzes l'app, si us plau, assegura't d'escollir un àlbum de còpia de seguretat perquè la línia de temps pugui carregar fotos i vídeos als àlbums",
"home_page_locked_error_local": "No s'han pogut moure els recursos locals a la carpeta bloquejada, saltant",
"home_page_locked_error_partner": "No s'han pogut moure els recursos de la parella a la carpeta bloquejada, saltant",
"home_page_share_err_local": "No es poden compartir els elements locals a través d'un enllaç, ometent",
"home_page_upload_err_limit": "Només es poden pujar un màxim de 30 elements alhora, ometent",
"host": "Amfitrió",
"hour": "Hora",
"id": "ID",
"ignore_icloud_photos": "Ignora fotos d'iCloud",
"ignore_icloud_photos_description": "Les fotos emmagatzemades a iCloud no es penjaran al servidor Immich",
"image": "Imatge",
@@ -1084,6 +1093,12 @@
"invalid_date_format": "Format de data invàlid",
"invite_people": "Convida gent",
"invite_to_album": "Convida a l'àlbum",
"ios_debug_info_fetch_ran_at": "La recuperació s'ha executat {dateTime}",
"ios_debug_info_last_sync_at": "Darrera sincronització {dateTime}",
"ios_debug_info_no_processes_queued": "No hi ha processos en segon pla en cua",
"ios_debug_info_no_sync_yet": "Encara no s'ha executat cap tasca de sincronització en segon pla",
"ios_debug_info_processes_queued": "{count, plural, one {Un procés en segon pla a la cua} other {{count} processos en segon pla a la cua}}",
"ios_debug_info_processing_ran_at": "El processament s'ha executat {dateTime}",
"items_count": "{count, plural, one {# element} other {# elements}}",
"jobs": "Tasques",
"keep": "Mantenir",
@@ -1092,6 +1107,9 @@
"kept_this_deleted_others": "S'ha conservat aquest element i s'han suprimit {count, plural, one {# asset} other {# assets}}",
"keyboard_shortcuts": "Dreceres de teclat",
"language": "Idioma",
"language_no_results_subtitle": "Prova d'ajustar el terme de cerca",
"language_no_results_title": "No s'han trobat idiomes",
"language_search_hint": "Cerca idiomes...",
"language_setting_description": "Seleccioneu el vostre idioma",
"last_seen": "Vist per últim cop",
"latest_version": "Última versió",
@@ -1117,6 +1135,7 @@
"list": "Llista",
"loading": "Carregant",
"loading_search_results_failed": "No s'han pogut carregar els resultats de la cerca",
"local_asset_cast_failed": "No es pot convertir un actiu que no s'ha penjat al servidor",
"local_network": "Xarxa local",
"local_network_sheet_info": "L'aplicació es connectarà al servidor mitjançant aquest URL quan utilitzeu la xarxa Wi-Fi especificada",
"location_permission": "Permís d'ubicació",
@@ -1126,6 +1145,8 @@
"location_picker_latitude_hint": "Introdueix aquí la latitud",
"location_picker_longitude_error": "Introdueix una longitud vàlida",
"location_picker_longitude_hint": "Introdueix aquí la longitud",
"lock": "Bloqueja",
"locked_folder": "Carpeta bloquejada",
"log_out": "Tanca la sessió",
"log_out_all_devices": "Tanqueu la sessió de tots els dispositius",
"logged_out_all_devices": "S'ha tancat la sessió de tots els dispositius",
@@ -1159,7 +1180,7 @@
"look": "Aspecte",
"loop_videos": "Vídeos en bucle",
"loop_videos_description": "Habilita la reproducció en bucle del vídeo en els detalls.",
"main_branch_warning": "Esteu usant una versió de desenvolupaent. Recomanem fer servir una versió publicada!",
"main_branch_warning": "Esteu utilitzant una versió en desenvolupament; Recomanem fer servir una versió publicada!",
"main_menu": "Menú principal",
"make": "Fabricant",
"manage_shared_links": "Administrar enllaços compartits",
@@ -1217,8 +1238,14 @@
"minimize": "Minimitza",
"minute": "Minut",
"missing": "Restants",
"model": "Model",
"month": "Mes",
"monthly_title_text_date_format": "MMMM y",
"more": "Més",
"move": "Moure",
"move_off_locked_folder": "Moure fora de la carpeta bloquejada",
"move_to_locked_folder": "Moure a la carpeta bloquejada",
"move_to_locked_folder_confirmation": "Aquestes fotos i vídeos seran eliminades de tots els àlbums, i només podran ser vistes des de la carpeta bloquejada",
"moved_to_archive": "S'han mogut {count, plural, one {# asset} other {# assets}} a l'arxiu",
"moved_to_library": "S'ha mogut {count, plural, one {# asset} other {# assets}} a la llibreria",
"moved_to_trash": "S'ha mogut a la paperera",
@@ -1236,22 +1263,26 @@
"new_password": "Nova contrasenya",
"new_person": "Persona nova",
"new_pin_code": "Nou codi PIN",
"new_pin_code_subtitle": "Aquesta és la primera vegada que accedeixes a la carpeta bloquejada. Crea una codi PIN i accedeix de manera segura a aquesta pàgina",
"new_user_created": "Nou usuari creat",
"new_version_available": "NOVA VERSIÓ DISPONIBLE",
"newest_first": "El més nou primer",
"next": "Següent",
"next_memory": "Següent record",
"no": "No",
"no_albums_message": "Creeu un àlbum per organitzar les vostres fotos i vídeos",
"no_albums_with_name_yet": "Sembla que encara no tens cap àlbum amb aquest nom.",
"no_albums_yet": "Sembla que encara no tens cap àlbum.",
"no_archived_assets_message": "Arxiveu fotos i vídeos per ocultar-los de Fotos",
"no_assets_message": "FEU CLIC PER PUJAR LA VOSTRA PRIMERA FOTO",
"no_assets_to_show": "No hi ha elements per mostrar",
"no_cast_devices_found": "No s'han trobat dispositius per transmetre",
"no_duplicates_found": "No s'han trobat duplicats.",
"no_exif_info_available": "No hi ha informació d'exif disponible",
"no_explore_results_message": "Penja més fotos per explorar la teva col·lecció.",
"no_favorites_message": "Afegiu preferits per trobar les millors fotos i vídeos a l'instant",
"no_libraries_message": "Creeu una llibreria externa per veure les vostres fotos i vídeos",
"no_locked_photos_message": "Les fotos i vídeos d'aquesta carpeta estan ocultes, i no es mostraran a mesura que navegues o cerques a la teva biblioteca.",
"no_name": "Sense nom",
"no_notifications": "No hi ha notificacions",
"no_people_found": "No s'han trobat coincidències de persones",
@@ -1262,6 +1293,8 @@
"not_in_any_album": "En cap àlbum",
"not_selected": "No seleccionat",
"note_apply_storage_label_to_previously_uploaded assets": "Nota: per aplicar l'etiqueta d'emmagatzematge als actius penjats anteriorment, executeu el",
"notes": "Notes",
"nothing_here_yet": "No hi ha res encara",
"notification_permission_dialog_content": "Per activar les notificacions, aneu a Configuració i seleccioneu permet.",
"notification_permission_list_tile_content": "Atorga permís per a activar les notificacions.",
"notification_permission_list_tile_enable_button": "Activa les notificacions",
@@ -1269,17 +1302,18 @@
"notification_toggle_setting_description": "Activa les notificacions per correu electrònic",
"notifications": "Notificacions",
"notifications_setting_description": "Gestiona les notificacions",
"oauth": "OAuth",
"official_immich_resources": "Recursos oficials d'Immich",
"offline": "Fora de línia",
"offline_paths": "Rutes fora de línia",
"offline_paths_description": "Aquests resultats poden ser deguts a la supressió manual de fitxers que no formen part d'una biblioteca externa.",
"ok": "D'acord",
"oldest_first": "El més vell primer",
"on_this_device": "En aquest dispositiu",
"onboarding": "Incorporació",
"onboarding_privacy_description": "Les següents funcions (opcionals) depenen de serveis externs i poden desactivarse en qualsevol moment de dels ajustos.",
"onboarding_locale_description": "Tria el teu llenguatge preferit. Pots canviar aquesta opció mes tard a la configuració.",
"onboarding_privacy_description": "Les següents funcions (opcionals) depenen de serveis externs i poden desactivarse en qualsevol moment des de la configuració.",
"onboarding_server_welcome_description": "Configurem la instància amb alguns paràmetres comuns.",
"onboarding_theme_description": "Trieu un tema de color per a la vostra instància. Podeu canviar-ho més endavant a la vostra configuració.",
"onboarding_welcome_description": "Configurem la vostra instància amb alguns paràmetres habituals.",
"onboarding_user_welcome_description": "Comencem!",
"onboarding_welcome_user": "Benvingut, {user}",
"online": "En línia",
"only_favorites": "Només preferits",
@@ -1290,6 +1324,7 @@
"options": "Opcions",
"or": "o",
"organize_your_library": "Organitzeu la llibreria",
"original": "original",
"other": "Altres",
"other_devices": "Altres dispositius",
"other_variables": "Altres variables",
@@ -1335,6 +1370,8 @@
"permanently_delete_assets_prompt": "Esteu segur que voleu suprimir permanentment {count, plural, one {aquest recurs?} other {aquests <b>#</b> recursos?}} Això també {count, plural, one {el} other {els}} suprimirà del seu àlbum.",
"permanently_deleted_asset": "Element eliminat permanentment",
"permanently_deleted_assets_count": "{count, plural, one {S'ha eliminat # element} other {S'han eliminat # elements}} permanentment",
"permission": "Permís",
"permission_empty": "El seu permís no hauria d'estar buit",
"permission_onboarding_back": "Torna",
"permission_onboarding_continue_anyway": "Continua de totes maneres",
"permission_onboarding_get_started": "Comença",
@@ -1355,6 +1392,7 @@
"pin_code_changed_successfully": "Codi PIN canviat correctament",
"pin_code_reset_successfully": "S'ha restablert correctament el codi PIN",
"pin_code_setup_successfully": "S'ha configurat correctament un codi PIN",
"pin_verification": "Verificació de codi PIN",
"place": "Lloc",
"places": "Llocs",
"places_count": "{count, plural, one {{count, number} Lloc} other {{count, number} Llocs}}",
@@ -1362,13 +1400,18 @@
"play_memories": "Reproduir records",
"play_motion_photo": "Reproduir Fotos en Moviment",
"play_or_pause_video": "Reproduir o posar en pausa el vídeo",
"please_auth_to_access": "Per favor, autentica't per accedir",
"port": "Port",
"preferences_settings_subtitle": "Gestiona les preferències de l'aplicació",
"preferences_settings_title": "Preferències",
"preset": "Preestablert",
"preview": "Previsualització",
"previous": "Anterior",
"previous_memory": "Memòria anterior",
"previous_or_next_photo": "Foto anterior o següent",
"previous_or_next_day": "Dia endavant/enrere",
"previous_or_next_month": "Mes endavant/enrere",
"previous_or_next_photo": "Foto endavant/enrere",
"previous_or_next_year": "Any endavant/enrere",
"primary": "Primària",
"privacy": "Privacitat",
"profile": "Perfil",
@@ -1376,8 +1419,9 @@
"profile_drawer_client_out_of_date_major": "L'aplicació mòbil està desactualitzada. Si us plau, actualitzeu a l'última versió major.",
"profile_drawer_client_out_of_date_minor": "L'aplicació mòbil està desactualitzada. Si us plau, actualitzeu a l'última versió menor.",
"profile_drawer_client_server_up_to_date": "El Client i el Servidor estan actualitzats",
"profile_drawer_server_out_of_date_major": "L'aplicació mòbil està desactualitzada. Si us plau, actualitzeu a l'última versió major.",
"profile_drawer_server_out_of_date_minor": "L'aplicació mòbil està desactualitzada. Si us plau, actualitzeu a l'última versió menor.",
"profile_drawer_github": "GitHub",
"profile_drawer_server_out_of_date_major": "El servidor està desactualitzat. Si us plau, actualitzeu a l'última versió major.",
"profile_drawer_server_out_of_date_minor": "El servidor està desactualitzat. Si us plau, actualitzeu a l'última versió menor.",
"profile_image_of_user": "Imatge de perfil de {user}",
"profile_picture_set": "Imatge de perfil configurada.",
"public_album": "Àlbum públic",
@@ -1396,12 +1440,13 @@
"purchase_failed_activation": "No s'ha pogut activar! Si us plau, comproveu el vostre correu electrònic per trobar la clau de producte correcta!",
"purchase_individual_description_1": "Per a un particular",
"purchase_individual_description_2": "Estat de la contribució",
"purchase_individual_title": "Individual",
"purchase_input_suggestion": "Tens una clau de producte? Introduïu la clau a continuació",
"purchase_license_subtitle": "Compra Immich per donar suport al desenvolupament continuat del servei",
"purchase_lifetime_description": "Compra de per vida",
"purchase_option_title": "OPCIONS DE COMPRA",
"purchase_panel_info_1": "Crear Immich requereix molt de temps i esforç, tenim enginyers a temps complet treballant-hi per fer-ho tan bo com sigui possible. La nostra missió és que el programari de codi obert i les pràctiques empresarials ètiques es converteixin en una font d'ingressos sostenible per als desenvolupadors i creïn un ecosistema que respecti la privacitat amb alternatives reals als serveis cloud explotadors.",
"purchase_panel_info_2": "Com que estem compromesos a no afegir murs de pagament, aquesta compra no us atorgarà cap funció addicional a Immich. Confiem en usuaris com tu per donar suport al desenvolupament continu d'Immich.",
"purchase_panel_info_2": "Estem compromesos a no afegir murs de pagament, aquesta compra no us otorgarà cap funció addicional a Immich. Confiem en usuaris com tu per donar suport al desenvolupament continuat d'Immich.",
"purchase_panel_title": "Donar suport al projecte",
"purchase_per_server": "Per servidor",
"purchase_per_user": "Per usuari",
@@ -1423,6 +1468,7 @@
"reassigned_assets_to_existing_person": "{count, plural, one {S'ha reassignat # recurs} other {S'han reassignat # recursos}} a {name, select, null {una persona existent} other {{name}}}",
"reassigned_assets_to_new_person": "{count, plural, one {S'ha reassignat # recurs} other {S'han reassignat # recursos}} a una persona nova",
"reassing_hint": "Assignar els elements seleccionats a una persona existent",
"recent": "Recent",
"recent-albums": "Àlbums recents",
"recent_searches": "Cerques recents",
"recently_added": "Afegit recentment",
@@ -1448,9 +1494,12 @@
"remove_deleted_assets": "Suprimeix fitxers fora de línia",
"remove_from_album": "Treu de l'àlbum",
"remove_from_favorites": "Eliminar dels preferits",
"remove_from_locked_folder": "Elimina de la carpeta bloquejada",
"remove_from_locked_folder_confirmation": "Segur que vols moure aquestes fotos i vídeos fora de la carpeta bloquejada? Seran visibles a la teva biblioteca.",
"remove_from_shared_link": "Eliminar de l'enllaç compartit",
"remove_memory": "Eliminar memòria",
"remove_photo_from_memory": "Traieu la foto d'aquesta memòria",
"remove_tag": "Elimina l'etiqueta",
"remove_url": "Eliminar URL",
"remove_user": "Eliminar l'usuari",
"removed_api_key": "Eliminada la clau d'API: {name}",
@@ -1483,6 +1532,7 @@
"retry_upload": "Torna a provar de pujar",
"review_duplicates": "Revisar duplicats",
"role": "Rol",
"role_editor": "Editor",
"role_viewer": "Visor",
"save": "Desa",
"save_to_gallery": "Desa a galeria",
@@ -1526,6 +1576,7 @@
"search_no_people_named": "Cap persona anomenada \"{name}\"",
"search_no_result": "No s'han trobat resultats, proveu un terme de cerca o una combinació diferents",
"search_options": "Opcions de cerca",
"search_page_categories": "Categories",
"search_page_motion_photos": "Fotografies animades",
"search_page_no_objects": "No hi ha informació d'objectes disponibles",
"search_page_no_places": "No hi ha informació de llocs disponibles",
@@ -1575,6 +1626,7 @@
"server_info_box_server_url": "URL del servidor",
"server_offline": "Servidor fora de línia",
"server_online": "Servidor en línia",
"server_privacy": "Privadesa del servidor",
"server_stats": "Estadístiques del servidor",
"server_version": "Versió del servidor",
"set": "Establir",
@@ -1584,6 +1636,7 @@
"set_date_of_birth": "Establir data de naixement",
"set_profile_picture": "Establir imatge de perfil",
"set_slideshow_to_fullscreen": "Mostra Diapositives en pantalla completa",
"set_stack_primary_asset": "Estableix com a actiu principal",
"setting_image_viewer_help": "El visor de detalls carrega primer la miniatura petita, després carrega la vista prèvia de mida mitjana (si està habilitada), finalment carrega l'original (si està habilitada).",
"setting_image_viewer_original_subtitle": "Activa per carregar la imatge en resolució original (molt gran!). Desactiva per reduir el consum de dades (tant de xarxa com de memòria cau).",
"setting_image_viewer_original_title": "Carrega la imatge original",
@@ -1592,7 +1645,6 @@
"setting_image_viewer_title": "Imatges",
"setting_languages_apply": "Aplicar",
"setting_languages_subtitle": "Canvia el llenguatge de l'aplicació",
"setting_languages_title": "Idiomes",
"setting_notifications_notify_failures_grace_period": "Notifica les fallades de la còpia de seguretat en segon pla: {duration}",
"setting_notifications_notify_hours": "{count} hores",
"setting_notifications_notify_immediately": "immediatament",
@@ -1615,6 +1667,7 @@
"share_add_photos": "Afegeix fotografies",
"share_assets_selected": "{count} seleccionats",
"share_dialog_preparing": "S'està preparant...",
"share_link": "Compartir Enllaç",
"shared": "Compartit",
"shared_album_activities_input_disable": "Els comentaris estan desactivats",
"shared_album_activity_remove_content": "Voleu eliminar aquesta activitat?",
@@ -1654,6 +1707,7 @@
"shared_link_expires_second": "Caduca d'aquí a {count} segon",
"shared_link_expires_seconds": "Caduca d'aquí a {count} segons",
"shared_link_individual_shared": "Individual compartit",
"shared_link_info_chip_metadata": "EXIF",
"shared_link_manage_links": "Gestiona els enllaços compartits",
"shared_link_options": "Opcions d'enllaços compartits",
"shared_links": "Enllaços compartits",
@@ -1720,6 +1774,7 @@
"start_date": "Data inicial",
"state": "Regió",
"status": "Estat",
"stop_casting": "Atura la transmisió",
"stop_motion_photo": "Atura foto en moviment",
"stop_photo_sharing": "Deixar de compartir les teves fotos?",
"stop_photo_sharing_description": "{partner} no podrà tornar a accedir a les vostres fotos.",
@@ -1777,7 +1832,7 @@
"to_parent": "Anar als pares",
"to_trash": "Paperera",
"toggle_settings": "Canvia configuració",
"toggle_theme": "Alternar tema",
"total": "Total",
"total_usage": "Ús total",
"trash": "Paperera",
"trash_all": "Envia-ho tot a la paperera",
@@ -1798,6 +1853,7 @@
"unable_to_setup_pin_code": "No s'ha pogut configurar el codi PIN",
"unarchive": "Desarxivar",
"unarchived_count": "{count, plural, other {# elements desarxivats}}",
"undo": "Desfer",
"unfavorite": "Reverteix preferit",
"unhide_person": "Mostra persona",
"unknown": "Desconegut",
@@ -1816,8 +1872,6 @@
"unselect_all_duplicates": "Desmarqueu tots els duplicats",
"unstack": "Desapila",
"unstacked_assets_count": "No apilat {count, plural, one {# recurs} other {# recursos}}",
"untracked_files": "Fitxers no monitoritzats",
"untracked_files_decription": "Aquests fitxers no estan monitoritzats per l'aplicació. Poden ser el resultat de moviments errats, descàrregues interrompudes o deixats enrere per error",
"up_next": "Pròxim",
"updated_at": "Actualitzat",
"updated_password": "Contrasenya actualitzada",
@@ -1829,11 +1883,14 @@
"upload_progress": "Restant {remaining, number} - Processat {processed, number}/{total, number}",
"upload_skipped_duplicates": "{count, plural, one {S'ha omès # recurs duplicat} other {S'han omès # recursos duplicats}}",
"upload_status_duplicates": "Duplicats",
"upload_status_errors": "Errors",
"upload_status_uploaded": "Carregat",
"upload_success": "Pujada correcta, actualitza la pàgina per veure nous recursos de pujada.",
"upload_to_immich": "Puja a Immich ({count})",
"uploading": "Pujant",
"url": "URL",
"usage": "Ús",
"use_biometric": "Empra biometria",
"use_current_connection": "utilitzar la connexió actual",
"use_custom_date_range": "Fes servir un rang de dates personalitzat",
"user": "Usuari",
@@ -1842,6 +1899,7 @@
"user_liked": "A {user} li ha agradat {type, select, photo {aquesta foto} video {aquest vídeo} asset {aquest recurs} other {}}",
"user_pin_code_settings": "Codi PIN",
"user_pin_code_settings_description": "Gestiona el teu codi PIN",
"user_privacy": "Privadesa d'Usuari",
"user_purchase_settings": "Compra",
"user_purchase_settings_description": "Gestiona la teva compra",
"user_role_set": "Establir {user} com a {role}",
@@ -1853,14 +1911,10 @@
"utilities": "Utilitats",
"validate": "Valida",
"validate_endpoint_error": "Per favor introdueix un URL vàlid",
"variables": "Variables",
"version": "Versió",
"version_announcement_closing": "El teu amic Alex",
"version_announcement_message": "Hola! Hi ha una nova versió d'Immich, si us plau, preneu-vos una estona per llegir les <link>notes de llançament</link> per assegurar que la teva configuració estigui actualitzada per evitar qualsevol error de configuració, especialment si utilitzeu WatchTower o qualsevol mecanisme que gestioni l'actualització automàtica de la vostra instància Immich.",
"version_announcement_overlay_release_notes": "notes de llançament",
"version_announcement_overlay_text_1": "Hola amic, hi ha una nova versió d'",
"version_announcement_overlay_text_2": "si us plau, pren-te una estona per visitar les ",
"version_announcement_overlay_text_3": " i assegura't que la teva configuració de docker-compose i .env estiguin actualitzades per evitar qualsevol error de configuració, especialment si utilitzes WatchTower o qualsevol mecanisme que gestioni l'actualització automàtica de l'aplicació del servidor.",
"version_announcement_overlay_title": "Nova versió del servidor disponible 🎉",
"version_history": "Historial de versions",
"version_history_item": "Instal·lat {version} el {date}",
"video": "Vídeo",
@@ -1880,6 +1934,7 @@
"view_previous_asset": "Mostra l'element anterior",
"view_qr_code": "Veure codi QR",
"view_stack": "Veure la pila",
"view_user": "Veure Usuari",
"viewer_remove_from_stack": "Elimina de la pila",
"viewer_stack_use_as_main_asset": "Fes servir com a element principal",
"viewer_unstack": "Desapila",
@@ -1890,6 +1945,7 @@
"welcome": "Benvingut",
"welcome_to_immich": "Benvingut a immich",
"wifi_name": "Nom Wi-Fi",
"wrong_pin_code": "Codi PIN incorrecte",
"year": "Any",
"years_ago": "Fa {years, plural, one {# any} other {# anys}}",
"yes": "Sí",

View File

@@ -22,11 +22,11 @@
"add_partner": "Přidat partnera",
"add_path": "Přidat cestu",
"add_photos": "Přidat fotky",
"add_tag": "Přidat značku",
"add_to": "Přidat do…",
"add_to_album": "Přidat do alba",
"add_to_album_bottom_sheet_added": "Přidáno do {album}",
"add_to_album_bottom_sheet_already_exists": "Je již v {album}",
"add_to_locked_folder": "Přidat do uzamčené složky",
"add_to_shared_album": "Přidat do sdíleného alba",
"add_url": "Přidat URL",
"added_to_archive": "Přidáno do archivu",
@@ -34,6 +34,7 @@
"added_to_favorites_count": "Přidáno {count, number} do oblíbených",
"admin": {
"add_exclusion_pattern_description": "Přidání vzorů vyloučení. Podporováno je globování pomocí *, ** a ?. Chcete-li ignorovat všechny soubory v jakémkoli adresáři s názvem \"Raw\", použijte \"**/Raw/**\". Chcete-li ignorovat všechny soubory končící na \".tif\", použijte \"**/*.tif\". Chcete-li ignorovat absolutní cestu, použijte příkaz \"/path/to/ignore/**\".",
"admin_user": "Administrátor",
"asset_offline_description": "Tato položka externí knihovny se již na disku nenachází a byla přesunuta do koše. Pokud byl soubor přesunut v rámci knihovny, zkontrolujte časovou osu a vyhledejte nové odpovídající položku. Chcete-li tuto položku obnovit, ujistěte se, že je cesta k níže uvedenému souboru přístupná pomocí aplikace Immich a prohledejte knihovnu.",
"authentication_settings": "Přihlašování",
"authentication_settings_description": "Správa hesel, OAuth a dalších nastavení ověření",
@@ -44,9 +45,7 @@
"backup_database_enable_description": "Povolit výpisy z databáze",
"backup_keep_last_amount": "Počet předchozích výpisů, které se mají ponechat",
"backup_settings": "Nastavení výpisu databáze",
"backup_settings_description": "Správa nastavení výpisu databáze. Poznámka: Tyto úlohy nejsou monitorovány a nebudete upozorněni na jejich selhání.",
"check_all": "Vše zkontrolovat",
"cleanup": "Vyčištění",
"backup_settings_description": "Správa nastavení výpisu databáze.",
"cleared_jobs": "Hotové úlohy pro: {job}",
"config_set_by_file": "Konfigurace je aktuálně prováděna konfiguračním souborem",
"confirm_delete_library": "Opravdu chcete odstranit knihovnu {library}?",
@@ -62,14 +61,12 @@
"disable_login": "Zakázat přihlášení",
"duplicate_detection_job_description": "Spuštění strojového učení na položkách za účelem detekce podobných obrázků. Spoléhá na Chytré vyhledávání",
"exclusion_pattern_description": "Vzory vyloučení umožňují při prohledávání knihovny ignorovat soubory a složky. To je užitečné, pokud máte složky obsahující soubory, které nechcete importovat, například RAW soubory.",
"external_library_created_at": "Externí knihovna (vytvořena {date})",
"external_library_management": "Správa externích knihoven",
"face_detection": "Detekce obličejů",
"face_detection_description": "Detekce obličejů v obrázcích pomocí strojového učení. U videí se bere v úvahu pouze miniatura. „Obnovit“ znovu zpracuje všechny položky. „Resetovat“ navíc vymaže všechna aktuální data obličejů. „Chybějící“ zařadí do fronty položky, které ještě nebyly zpracovány. Zjištěné obličeje budou po dokončení funkce Rozpoznávání obličejů zařazeny do fronty a seskupeny do stávajících nebo nových osob.",
"facial_recognition_job_description": "Seskupí nalezené obličeje do osob. Tento krok se spustí po dokončení detekce obličejů. „Resetovat“ znovu seskupí všechny obličeje. „Chybějící“ zpracuje obličeje, které nemají přiřazenou osobu.",
"failed_job_command": "Příkaz {command} se nezdařil pro úlohu: {job}",
"force_delete_user_warning": "UPOZORNĚNÍ: Tímto okamžitě odstraníte uživatele a všechny jeho položky. Tento krok nelze vrátit zpět a soubory nelze obnovit.",
"forcing_refresh_library_files": "Vynucení obnovy všech souborů knihovny",
"image_format": "Formát",
"image_format_description": "WebP vytváří menší soubory než JPEG, ale je pomalejší při kódování.",
"image_fullsize_description": "Obrázek v plné velikosti s odstraněnými metadaty, použito při přiblížení",
@@ -174,7 +171,7 @@
"note_apply_storage_label_previous_assets": "Upozornění: Pro uplatnění Štítku úložiště na dříve nahrané položky spusťte",
"note_cannot_be_changed_later": "UPOZORNĚNÍ: Toto nelze později změnit!",
"notification_email_from_address": "Adresa Od",
"notification_email_from_address_description": "E-mailová adresa odesílatele, např.: „Immich Photo Server <noreply@example.com>“",
"notification_email_from_address_description": "E-mailová adresa odesílatele, např.: „Immich Photo Server <noreply@example.com>“. Ujistěte se, že používáte adresu, ze které smíte odesílat e-maily.",
"notification_email_host_description": "Adresa e-mailového serveru (např. smtp.immich.app)",
"notification_email_ignore_certificate_errors": "Ignorovat chyby certifikátů",
"notification_email_ignore_certificate_errors_description": "Ignorovat chyby ověření certifikátu TLS (nedoporučuje se)",
@@ -198,7 +195,7 @@
"oauth_enable_description": "Přihlásit pomocí OAuth",
"oauth_mobile_redirect_uri": "Mobilní přesměrování URI",
"oauth_mobile_redirect_uri_override": "Přepsat mobilní přesměrování URI",
"oauth_mobile_redirect_uri_override_description": "Povolit, pokud poskytovatel OAuth nepovoluje mobilní URI, například '{callback}'",
"oauth_mobile_redirect_uri_override_description": "Povolit, pokud poskytovatel OAuth nepovoluje mobilní URI, například ''{callback}''",
"oauth_settings": "OAuth",
"oauth_settings_description": "Správa nastavení OAuth přihlášení",
"oauth_settings_more_details": "Další podrobnosti o této funkci naleznete v <link>dokumentaci</link>.",
@@ -207,11 +204,9 @@
"oauth_storage_quota_claim": "Deklarace kvóty úložiště",
"oauth_storage_quota_claim_description": "Automaticky nastavit kvótu úložiště uživatele na hodnotu této deklarace.",
"oauth_storage_quota_default": "Výchozí kvóta úložiště (GiB)",
"oauth_storage_quota_default_description": "Kvóta v GiB, která se použije, pokud není poskytnuta žádná deklarace (pro neomezenou kvótu zadejte 0).",
"oauth_storage_quota_default_description": "Kvóta v GiB, která se použije, pokud není poskytnuta žádná deklarace.",
"oauth_timeout": "Časový limit požadavku",
"oauth_timeout_description": "Časový limit pro požadavky v milisekundách",
"offline_paths": "Cesty offline",
"offline_paths_description": "Tyto výsledky mohou být způsobeny ručním odstraněním souborů, které nejsou součástí externí knihovny.",
"password_enable_description": "Přihlášení pomocí e-mailu a hesla",
"password_settings": "Přihlášení heslem",
"password_settings_description": "Správa nastavení přihlašování pomocí hesla",
@@ -221,9 +216,6 @@
"refreshing_all_libraries": "Obnovení všech knihoven",
"registration": "Registrace správce",
"registration_description": "Vzhledem k tomu, že jste prvním uživatelem v systému, budete přiřazen jako správce a budete zodpovědný za úkoly správy a další uživatelé budou vytvořeni vámi.",
"repair_all": "Opravit vše",
"repair_matched_items": "Shoda {count, plural, one {# položky} other {# položek}}",
"repaired_items": "{count, plural, one {Opravena # položka} few {Opraveny # položky} other {Opraveno # položek}}",
"require_password_change_on_login": "Požadovat, aby si uživatel při prvním přihlášení změnil heslo",
"reset_settings_to_default": "Obnovení výchozího nastavení",
"reset_settings_to_recent_saved": "Obnovit poslední uložené nastavení",
@@ -252,7 +244,7 @@
"storage_template_migration_info": "Šablona úložiště převede všechny přípony na malá písmena. Změny šablon se uplatní pouze u nových položek. Chcete-li šablonu zpětně použít na dříve nahrané položky, spusťte <link>{job}</link>.",
"storage_template_migration_job": "Úloha migrace šablony úložiště",
"storage_template_more_details": "Další podrobnosti o této funkci naleznete v sekci <template-link>Šablona úložiště</template-link> včetně jejích <implications-link>důsledků</implications-link>",
"storage_template_onboarding_description": "Je-li tato funkce povolena, automaticky uspořádá soubory na základě uživatelem definované šablony. Z důvodu problémů se stabilitou byla tato funkce ve výchozím nastavení vypnuta. Další informace naleznete v <link>dokumentaci</link>.",
"storage_template_onboarding_description_v2": "Pokud je tato funkce povolena, automaticky uspořádá soubory na základě uživatelem definované šablony. Další informace naleznete v <link>dokumentaci</link>.",
"storage_template_path_length": "Přibližný limit délky cesty: <b>{length, number}</b>/{limit, number}",
"storage_template_settings": "Šablona úložiště",
"storage_template_settings_description": "Správa struktury složek a názvů nahraných souborů",
@@ -264,16 +256,14 @@
"template_email_invite_album": "Šablona pozvánky do alba",
"template_email_preview": "Náhled",
"template_email_settings": "Šablony e-mailů",
"template_email_settings_description": "Správa vlastních šablon e-mailových oznámení",
"template_email_update_album": "Šablona aktualizace alba",
"template_email_welcome": "Šablona uvítacího e-mailu",
"template_settings": "Šablony oznámení",
"template_settings_description": "Správa vlastních šablon oznámení.",
"template_settings_description": "Správa vlastních šablon oznámení",
"theme_custom_css_settings": "Vlastní CSS",
"theme_custom_css_settings_description": "Kaskádové styly umožňují přizpůsobit design aplikace Immich.",
"theme_settings": "Motivy",
"theme_settings_description": "Správa přizpůsobení webového rozhraní Immich",
"these_files_matched_by_checksum": "Tyto soubory jsou porovnávány podle jejich kontrolních součtů",
"thumbnail_generation_job": "Generování miniatur",
"thumbnail_generation_job_description": "Generování velkých, malých a rozmazaných miniatur pro každý obrázek a miniatur pro každou osobu",
"transcoding_acceleration_api": "API pro akceleraci",
@@ -301,10 +291,9 @@
"transcoding_encoding_options": "Možnosti kódování",
"transcoding_encoding_options_description": "Nastavte kodeky, rozlišení, kvalitu a další možnosti pro kódovaná videa",
"transcoding_hardware_acceleration": "Hardwarová akcelerace",
"transcoding_hardware_acceleration_description": "Experimentální; mnohem rychlejší, ale při stejném datovém toku bude mít nižší kvalitu",
"transcoding_hardware_acceleration_description": "Experimentální: rychlejší kódování, ale při stejném datovém toku může mít nižší kvalitu",
"transcoding_hardware_decoding": "Hardwarové dekódování",
"transcoding_hardware_decoding_setting_description": "Povoluje kompletní akceleraci namísto akcelerace pouze kódování. Nemusí fungovat u všech videí.",
"transcoding_hevc_codec": "Kodek HEVC",
"transcoding_max_b_frames": "Maximální počet B-snímků",
"transcoding_max_b_frames_description": "Vyšší hodnoty zvyšují účinnost komprese, ale zpomalují kódování. Nemusí být kompatibilní s hardwarovou akcelerací na starších zařízeních. Hodnota 0 zakáže B-snímky, zatímco -1 tuto hodnotu nastaví automaticky.",
"transcoding_max_bitrate": "Maximální datový tok",
@@ -342,8 +331,6 @@
"trash_number_of_days_description": "Počet dní, po které je třeba položku ponechat v koši, než bude trvale odstraněna",
"trash_settings": "Koš",
"trash_settings_description": "Správa nastavení koše",
"untracked_files": "Neznámé soubory",
"untracked_files_description": "Tyto soubory nejsou aplikaci známy. Mohou být výsledkem neúspěšných přesunů, přerušeného nahrávání nebo mohou zůstat pozadu kvůli chybě",
"user_cleanup_job": "Promazání uživatelů",
"user_delete_delay": "Účet a položky uživatele <b>{user}</b> budou trvale smazány za {delay, plural, one {# den} few {# dny} other {# dní}}.",
"user_delete_delay_settings": "Odložení odstranění",
@@ -384,8 +371,8 @@
"advanced_settings_tile_subtitle": "Pokročilé uživatelské nastavení",
"advanced_settings_troubleshooting_subtitle": "Zobrazit dodatečné vlastnosti pro řešení problémů",
"advanced_settings_troubleshooting_title": "Řešení problémů",
"age_months": "Věk {months, plural, one {# měsíc} few {# měsíce} other {# měsíců}}",
"age_year_months": "Věk 1 rok, {months, plural, one {# měsíc} other {# měsíce}}",
"age_months": "{months, plural, one {# měsíc} few {# měsíce} other {# měsíců}}",
"age_year_months": "1 rok a {months, plural, one {# měsíc} few {# měsíce} other {# měsíců}}",
"age_years": "{years, plural, one {# rok} few {# roky} other {# let}}",
"album_added": "Přidáno album",
"album_added_notification_setting_description": "Dostávat e-mailové oznámení, když jste přidáni do sdíleného alba",
@@ -402,10 +389,6 @@
"album_remove_user": "Odebrat uživatele?",
"album_remove_user_confirmation": "Opravdu chcete odebrat uživatele {user}?",
"album_share_no_users": "Zřejmě jste toto album sdíleli se všemi uživateli, nebo nemáte žádného uživatele, se kterým byste ho mohli sdílet.",
"album_thumbnail_card_item": "1 položka",
"album_thumbnail_card_items": "{count} položek",
"album_thumbnail_card_shared": " · Sdíleno",
"album_thumbnail_shared_by": "Sdílel(a) {user}",
"album_updated": "Album aktualizováno",
"album_updated_setting_description": "Dostávat e-mailová oznámení o nových položkách sdíleného alba",
"album_user_left": "Opustil {album}",
@@ -421,6 +404,9 @@
"album_with_link_access": "Nechte kohokoli s odkazem zobrazit fotografie a lidi v tomto albu.",
"albums": "Alba",
"albums_count": "{count, plural, one {{count, number} album} few {{count, number} alba} other {{count, number} alb}}",
"albums_default_sort_order": "Výchozí řazení alb",
"albums_default_sort_order_description": "Výchozí řazení položek při vytváření nových alb.",
"albums_feature_description": "Sbírky položek, které lze sdílet s ostatními uživateli.",
"all": "Vše",
"all_albums": "Všechna alba",
"all_people": "Všichni lidé",
@@ -479,9 +465,12 @@
"assets_added_count": "{count, plural, one {Přidána # položka} few {Přidány # položky} other {Přidáno # položek}}",
"assets_added_to_album_count": "Do alba {count, plural, one {byla přidána # položka} few {byly přidány # položky} other {bylo přidáno # položek}}",
"assets_added_to_name_count": "{count, plural, one {Přidána # položka} few {Přidány # položky} other {Přidáno # položek}} do {hasName, select, true {alba <b>{name}</b>} other {nového alba}}",
"assets_cannot_be_added_to_album_count": "{count, plural, one {Položku} other {Položky}} nelze přidat do alba",
"assets_count": "{count, plural, one {# položka} few {# položky} other {# položek}}",
"assets_deleted_permanently": "{count} položek trvale odstraněno",
"assets_deleted_permanently_from_server": "{count} položek trvale odstraněno z Immich serveru",
"assets_downloaded_failed": "{count, plural, one {Stažen # soubor - {error} souborů selhalo} few {Staženy # soubory - {error} souborů selhalo} other {Staženo # souborů - {error} souborů selhalo}}",
"assets_downloaded_successfully": "{count, plural, one {Úspěšně stažen # soubor} few {Úspěšně staženy # soubory} other {Úspěšně staženo # souborů}}",
"assets_moved_to_trash_count": "Do koše {count, plural, one {přesunuta # položka} few {přesunuty # položky} other {přesunuto # položek}}",
"assets_permanently_deleted_count": "Trvale {count, plural, one {smazána # položka} few {smazány # položky} other {smazáno # položek}}",
"assets_removed_count": "{count, plural, one {Odstraněna # položka} few {Odstraněny # položky} other {Odstraněno # položek}}",
@@ -496,6 +485,7 @@
"authorized_devices": "Autorizovaná zařízení",
"automatic_endpoint_switching_subtitle": "Připojit se místně přes určenou Wi-Fi, pokud je k dispozici, a používat alternativní připojení jinde",
"automatic_endpoint_switching_title": "Automatické přepínání URL",
"autoplay_slideshow": "Automatické přehrávání prezentace",
"back": "Zpět",
"back_close_deselect": "Zpět, zavřít nebo zrušit výběr",
"background_location_permission": "Povolení polohy na pozadí",
@@ -520,6 +510,7 @@
"backup_controller_page_background_app_refresh_enable_button_text": "Přejít do nastavení",
"backup_controller_page_background_battery_info_link": "Ukaž mi jak",
"backup_controller_page_background_battery_info_message": "Chcete-li dosáhnout nejlepších výsledků při zálohování na pozadí, vypněte všechny optimalizace baterie, které omezují aktivitu na pozadí pro Immich ve vašem zařízení. \n\nJelikož je to závislé na typu zařízení, vyhledejte požadované informace pro výrobce vašeho zařízení.",
"backup_controller_page_background_battery_info_ok": "OK",
"backup_controller_page_background_battery_info_title": "Optimalizace baterie",
"backup_controller_page_background_charging": "Pouze během nabíjení",
"backup_controller_page_background_configure_error": "Nepodařilo se nakonfigurovat službu na pozadí",
@@ -538,6 +529,7 @@
"backup_controller_page_excluded": "Vyloučeno: ",
"backup_controller_page_failed": "Nepodařilo se ({count})",
"backup_controller_page_filename": "Název souboru: {filename} [{size}]",
"backup_controller_page_id": "ID: {id}",
"backup_controller_page_info": "Informace o zálohování",
"backup_controller_page_none_selected": "Žádné vybrané",
"backup_controller_page_remainder": "Zbývá",
@@ -575,21 +567,17 @@
"bulk_keep_duplicates_confirmation": "Opravdu si chcete ponechat {count, plural, one {# duplicitní položku} few {# duplicitní položky} other {# duplicitních položek}}? Tím se vyřeší všechny duplicitní skupiny, aniž by se cokoli odstranilo.",
"bulk_trash_duplicates_confirmation": "Opravdu chcete hromadně vyhodit {count, plural, one {# duplicitní položku} few {# duplicitní položky} other {# duplicitních položek}}? Tím se zachová největší položka z každé skupiny a všechny ostatní duplikáty se vyhodí.",
"buy": "Zakoupit Immich",
"cache_settings_album_thumbnails": "Náhledy stránek knihovny ({count} položek)",
"cache_settings_clear_cache_button": "Vymazat vyrovnávací paměť",
"cache_settings_clear_cache_button_title": "Vymaže vyrovnávací paměť aplikace. To výrazně ovlivní výkon aplikace, dokud se vyrovnávací paměť neobnoví.",
"cache_settings_duplicated_assets_clear_button": "VYMAZAT",
"cache_settings_duplicated_assets_subtitle": "Fotografie a videa, které aplikace zařadila na černou listinu",
"cache_settings_duplicated_assets_title": "Duplicitní položky ({count})",
"cache_settings_image_cache_size": "Velikost vyrovnávací paměti ({count} položek)",
"cache_settings_statistics_album": "Knihovna náhledů",
"cache_settings_statistics_assets": "{count} položek ({size})",
"cache_settings_statistics_full": "Kompletní fotografie",
"cache_settings_statistics_shared": "Sdílené náhledy alb",
"cache_settings_statistics_thumbnail": "Náhledy",
"cache_settings_statistics_title": "Použití vyrovnávací paměti",
"cache_settings_subtitle": "Ovládání chování mobilní aplikace Immich v mezipaměti",
"cache_settings_thumbnail_size": "Velikost vyrovnávací paměti náhledů ({count} položek)",
"cache_settings_tile_subtitle": "Ovládání chování místního úložiště",
"cache_settings_tile_title": "Místní úložiště",
"cache_settings_title": "Nastavení vyrovnávací paměti",
@@ -602,14 +590,15 @@
"cannot_merge_people": "Nelze sloučit osoby",
"cannot_undo_this_action": "Tuto akci nelze vrátit zpět!",
"cannot_update_the_description": "Nelze aktualizovat popis",
"cast": "Přenášet",
"cast": "Odeslat do zařízení",
"cast_description": "Nastavení dostupných cílů přenosu",
"change_date": "Změnit datum",
"change_description": "Změnit popis",
"change_display_order": "Změnit pořadí zobrazení",
"change_expiration_time": "Změna konce platnosti",
"change_location": "Změna polohy",
"change_name": "Změnit jméno",
"change_name_successfully": "Změna jména proběhla úspěšně",
"change_name_successfully": "Jméno bylo úspěšně změněno",
"change_password": "Změna hesla",
"change_password_description": "Buď se do systému přihlašujete poprvé, nebo jste byli požádáni o změnu hesla. Zadejte prosím nové heslo níže.",
"change_password_form_confirm_password": "Potvrďte heslo",
@@ -620,7 +609,6 @@
"change_pin_code": "Změnit PIN kód",
"change_your_password": "Změna vašeho hesla",
"changed_visibility_successfully": "Změna viditelnosti proběhla úspěšně",
"check_all": "Zkontrolovat vše",
"check_corrupt_asset_backup": "Kontrola poškozených záloh položek",
"check_corrupt_asset_backup_button": "Provést kontrolu",
"check_corrupt_asset_backup_description": "Tuto kontrolu provádějte pouze přes Wi-Fi a po zálohování všech prostředků. Takto operace může trvat několik minut.",
@@ -632,6 +620,7 @@
"clear_all_recent_searches": "Vymazat všechna nedávná vyhledávání",
"clear_message": "Vymazat zprávu",
"clear_value": "Vymazat hodnotu",
"client_cert_dialog_msg_confirm": "OK",
"client_cert_enter_password": "Zadejte heslo",
"client_cert_import": "Importovat",
"client_cert_import_success_msg": "Klientský certifikát je importován",
@@ -659,11 +648,13 @@
"confirm_keep_this_delete_others": "Všechny ostatní položky v tomto uskupení mimo této budou odstraněny. Opravdu chcete pokračovat?",
"confirm_new_pin_code": "Potvrzení nového PIN kódu",
"confirm_password": "Potvrzení hesla",
"confirm_tag_face": "Opravdu chcete označit tento obličej jako {name}?",
"confirm_tag_face_unnamed": "Opravdu chcete označit tento obličej?",
"connected_device": "Připojené zařízení",
"connected_to": "Připojeno k",
"contain": "Obsah",
"context": "Kontext",
"continue": "Pokračovat",
"control_bottom_app_bar_album_info_shared": "{count} položek · Sdíleno",
"control_bottom_app_bar_create_new_album": "Vytvořit nové album",
"control_bottom_app_bar_delete_from_immich": "Smazat ze serveru Immich",
"control_bottom_app_bar_delete_from_local": "Smazat ze zařízení",
@@ -712,6 +703,7 @@
"daily_title_text_date": "EEEE, d. MMMM",
"daily_title_text_date_year": "EEEE, d. MMMM y",
"dark": "Tmavý",
"darkTheme": "Přepnout tmavý motiv",
"date_after": "Datum po",
"date_and_time": "Datum a čas",
"date_before": "Datum před",
@@ -757,7 +749,9 @@
"direction": "Směr",
"disabled": "Zakázáno",
"disallow_edits": "Zakázat úpravy",
"discord": "Discord",
"discover": "Objevit",
"discovered_devices": "Nalezená zařízení",
"dismiss_all_errors": "Zrušit všechny chyby",
"dismiss_error": "Zrušit chybu",
"display_options": "Možnosti zobrazení",
@@ -773,7 +767,6 @@
"download_enqueue": "Stahování ve frontě",
"download_error": "Chyba při stahování",
"download_failed": "Stahování selhalo",
"download_filename": "soubor: {filename}",
"download_finished": "Stahování dokončeno",
"download_include_embedded_motion_videos": "Vložená videa",
"download_include_embedded_motion_videos_description": "Zahrnout videa vložená do pohyblivých fotografií jako samostatný soubor",
@@ -813,6 +806,7 @@
"edit_title": "Upravit název",
"edit_user": "Upravit uživatele",
"edited": "Upraveno",
"editor": "Editor",
"editor_close_without_save_prompt": "Změny nebudou uloženy",
"editor_close_without_save_title": "Zavřít editor?",
"editor_crop_tool_h2_aspect_ratios": "Poměr stran",
@@ -835,6 +829,7 @@
"error_delete_face": "Chyba při odstraňování obličeje z položky",
"error_loading_image": "Chyba při načítání obrázku",
"error_saving_image": "Chyba: {error}",
"error_tag_face_bounding_box": "Chyba při označování obličeje - nelze získat souřadnice ohraničujícího rámečku",
"error_title": "Chyba - Něco se pokazilo",
"errors": {
"cannot_navigate_next_asset": "Nelze přejít na další položku",
@@ -847,7 +842,6 @@
"cant_get_number_of_comments": "Nelze načíst počet komentářů",
"cant_search_people": "Nelze vyhledávat lidi",
"cant_search_places": "Nelze vyhledávat místa",
"cleared_jobs": "Vyřízené úlohy pro: {job}",
"error_adding_assets_to_album": "Chyba při přidávání položek do alba",
"error_adding_users_to_album": "Chyba při přidávání uživatelů do alba",
"error_deleting_shared_user": "Chyba při odstraňování sdíleného uživatele",
@@ -856,7 +850,6 @@
"error_removing_assets_from_album": "Chyba při odstraňování položek z alba, další podrobnosti najdete v konzoli",
"error_selecting_all_assets": "Chyba při výběru všech položek",
"exclusion_pattern_already_exists": "Tento vzor vyloučení již existuje.",
"failed_job_command": "Příkaz {command} se nezdařil pro úlohu: {job}",
"failed_to_create_album": "Nepodařilo se vytvořit album",
"failed_to_create_shared_link": "Nepodařilo se vytvořit sdílený odkaz",
"failed_to_edit_shared_link": "Nepodařilo se upravit sdílený odkaz",
@@ -875,7 +868,6 @@
"paths_validation_failed": "{paths, plural, one {# cesta neprošla} few {# cesty neprošly} other {# cest neprošlo}} kontrolou",
"profile_picture_transparent_pixels": "Profilové obrázky nemohou mít průhledné pixely. Obrázek si prosím zvětšete nebo posuňte.",
"quota_higher_than_disk_size": "Nastavili jste kvótu vyšší, než je velikost disku",
"repair_unable_to_check_items": "Nelze zkontrolovat {count, select, one {položku} other {položky}}",
"unable_to_add_album_users": "Nelze přidat uživatele do alba",
"unable_to_add_assets_to_shared_link": "Nelze přidat položky do sdíleného odkazu",
"unable_to_add_comment": "Nelze přidat komentář",
@@ -894,7 +886,6 @@
"unable_to_change_visibility": "Nelze změnit viditelnost u {count, plural, one {# osoby} few {# osob} other {# lidí}}",
"unable_to_complete_oauth_login": "Nelze dokončit OAuth přihlášení",
"unable_to_connect": "Nelze se připojit",
"unable_to_connect_to_server": "Nepodařilo se připojit k serveru",
"unable_to_copy_to_clipboard": "Nelze zkopírovat do schránky, ujistěte se, že na stránku přistupujete přes https",
"unable_to_create_admin_account": "Nelze vytvořit účet správce",
"unable_to_create_api_key": "Nelze vytvořit nový API klíč",
@@ -918,14 +909,9 @@
"unable_to_hide_person": "Nelze skrýt osobu",
"unable_to_link_motion_video": "Nelze připojit pohyblivé video",
"unable_to_link_oauth_account": "Nelze propojit OAuth účet",
"unable_to_load_album": "Nelze načíst album",
"unable_to_load_asset_activity": "Nelze načíst aktivitu položky",
"unable_to_load_items": "Nelze načíst položky",
"unable_to_load_liked_status": "Nelze načíst stav oblíbených",
"unable_to_log_out_all_devices": "Nelze odhlásit všechna zařízení",
"unable_to_log_out_device": "Nelze odhlásit zařízení",
"unable_to_login_with_oauth": "Nelze se přihlásit pomocí OAuth",
"unable_to_move_to_locked_folder": "Nelze přesunout do uzamčené složky",
"unable_to_play_video": "Nelze přehrát video",
"unable_to_reassign_assets_existing_person": "Nelze přeřadit položky na {name, select, null {existující osobu} other {{name}}}",
"unable_to_reassign_assets_new_person": "Nelze přeřadit položku na novou osobu",
@@ -933,11 +919,9 @@
"unable_to_remove_album_users": "Nelze odebrat uživatele z alba",
"unable_to_remove_api_key": "Nelze odstranit API klíč",
"unable_to_remove_assets_from_shared_link": "Nelze odstranit položky ze sdíleného odkazu",
"unable_to_remove_deleted_assets": "Nelze odstranit offline soubory",
"unable_to_remove_library": "Nelze odstranit knihovnu",
"unable_to_remove_partner": "Nelze odebrat partnera",
"unable_to_remove_reaction": "Nelze odstranit reakci",
"unable_to_repair_items": "Nelze opravit položky",
"unable_to_reset_password": "Nelze obnovit heslo",
"unable_to_reset_pin_code": "Nelze resetovat PIN kód",
"unable_to_resolve_duplicate": "Nelze vyřešit duplicitu",
@@ -967,12 +951,12 @@
"unable_to_update_user": "Nelze aktualizovat uživatele",
"unable_to_upload_file": "Nepodařilo se nahrát soubor"
},
"exif": "Exif",
"exif_bottom_sheet_description": "Přidat popis...",
"exif_bottom_sheet_details": "PODROBNOSTI",
"exif_bottom_sheet_location": "POLOHA",
"exif_bottom_sheet_people": "LIDÉ",
"exif_bottom_sheet_person_add_person": "Přidat jméno",
"exif_bottom_sheet_person_age": "Věk {age}",
"exif_bottom_sheet_person_age_months": "{months} měsíců",
"exif_bottom_sheet_person_age_year_months": "1 rok a {months} měsíců",
"exif_bottom_sheet_person_age_years": "{years} let",
@@ -987,6 +971,7 @@
"expires_date": "Platnost končí {date}",
"explore": "Prozkoumat",
"explorer": "Průzkumník",
"export": "Export",
"export_as_json": "Exportovat jako JSON",
"extension": "Přípona",
"external": "Externí",
@@ -1019,6 +1004,8 @@
"folders": "Složky",
"folders_feature_description": "Procházení zobrazení složek s fotografiemi a videi v souborovém systému",
"forward": "Dopředu",
"gcast_enabled": "Google Cast",
"gcast_enabled_description": "Tato funkce načítá externí zdroje z Googlu, aby mohla fungovat.",
"general": "Obecné",
"get_help": "Získat pomoc",
"get_wifiname_error": "Nepodařilo se získat název Wi-Fi. Zkontrolujte, zda jste udělili potřebná oprávnění a zda jste připojeni k Wi-Fi síti",
@@ -1067,6 +1054,7 @@
"home_page_upload_err_limit": "Lze nahrát nejvýše 30 položek najednou, přeskakuji",
"host": "Hostitel",
"hour": "Hodina",
"id": "ID",
"ignore_icloud_photos": "Ignorovat fotografie na iCloudu",
"ignore_icloud_photos_description": "Fotografie uložené na iCloudu se nebudou nahrávat na Immich server",
"image": "Obrázek",
@@ -1084,6 +1072,7 @@
"image_viewer_page_state_provider_download_started": "Stahování zahájeno",
"image_viewer_page_state_provider_download_success": "Stahování bylo úspěšné",
"image_viewer_page_state_provider_share_error": "Chyba sdílení",
"immich_logo": "Immich Logo",
"immich_web_interface": "Webové rozhraní Immich",
"import_from_json": "Import z JSONu",
"import_path": "Cesta importu",
@@ -1105,6 +1094,12 @@
"invalid_date_format": "Chybný formát data",
"invite_people": "Pozvat lidi",
"invite_to_album": "Pozvat do alba",
"ios_debug_info_fetch_ran_at": "Data načtena {dateTime}",
"ios_debug_info_last_sync_at": "Poslední synchronizace {dateTime}",
"ios_debug_info_no_processes_queued": "Žádné procesy na pozadí ve frontě",
"ios_debug_info_no_sync_yet": "Dosud nebyla spuštěna žádná úloha synchronizace na pozadí",
"ios_debug_info_processes_queued": "{count, plural, one {{count} proces na pozadí ve frontě} few {{count} procesy na pozadí ve frontě} other {{count} procesů na pozadí ve frontě}}",
"ios_debug_info_processing_ran_at": "Zpracování spuštěno {dateTime}",
"items_count": "{count, plural, one {# položka} few {# položky} other {# položek}}",
"jobs": "Úlohy",
"keep": "Ponechat",
@@ -1113,6 +1108,9 @@
"kept_this_deleted_others": "Ponechána tato položka a {count, plural, one {odstraněna # položka} few {odstraněny # položky} other {odstraněno # položek}}",
"keyboard_shortcuts": "Klávesové zkratky",
"language": "Jazyk",
"language_no_results_subtitle": "Zkuste upravit hledaný výraz",
"language_no_results_title": "Nebyly nalezeny žádné jazyky",
"language_search_hint": "Vyhledat jazyk...",
"language_setting_description": "Vyberte upřednostňovaný jazyk",
"last_seen": "Naposledy viděno",
"latest_version": "Nejnovější verze",
@@ -1138,6 +1136,7 @@
"list": "Seznam",
"loading": "Načítání",
"loading_search_results_failed": "Načítání výsledků vyhledávání se nezdařilo",
"local_asset_cast_failed": "Nelze odeslat položku, která není nahraná na serveru",
"local_network": "Místní síť",
"local_network_sheet_info": "Aplikace se při použití zadané sítě Wi-Fi připojí k serveru prostřednictvím tohoto URL",
"location_permission": "Oprávnění polohy",
@@ -1151,6 +1150,7 @@
"locked_folder": "Uzamčená složka",
"log_out": "Odhlásit",
"log_out_all_devices": "Odhlásit všechna zařízení",
"logged_in_as": "Přihlášen jako {user}",
"logged_out_all_devices": "Všechna zařízení odhlášena",
"logged_out_device": "Zařízení odhlášeno",
"login": "Přihlášení",
@@ -1240,6 +1240,7 @@
"minimize": "Minimalizovat",
"minute": "Minuta",
"missing": "Chybějící",
"model": "Model",
"month": "Měsíc",
"monthly_title_text_date_format": "LLLL y",
"more": "Více",
@@ -1277,12 +1278,13 @@
"no_archived_assets_message": "Archivujte fotografie a videa a skryjte je ze zobrazení v sekci Fotky",
"no_assets_message": "KLIKNĚTE PRO NAHRÁNÍ PRVNÍ FOTOGRAFIE",
"no_assets_to_show": "Žádné položky k zobrazení",
"no_cast_devices_found": "Nebyla nalezena žádná zařízení",
"no_duplicates_found": "Nebyly nalezeny žádné duplicity.",
"no_exif_info_available": "Exif není k dispozici",
"no_explore_results_message": "Nahrajte další fotografie a prozkoumejte svou sbírku.",
"no_favorites_message": "Přidejte si oblíbené položky a rychle najděte své nejlepší obrázky a videa",
"no_libraries_message": "Vytvořte si externí knihovnu pro zobrazení fotografií a videí",
"no_locked_photos_message": "Fotky a videa v uzamčené složce jsou skryté a při procházení knihovny se nezobrazují.",
"no_locked_photos_message": "Fotky a videa v uzamčené složce jsou skryté a při procházení nebo vyhledávání v knihovně se nezobrazují.",
"no_name": "Bez jména",
"no_notifications": "Žádná oznámení",
"no_people_found": "Nebyli nalezeni žádní odpovídající lidé",
@@ -1302,16 +1304,20 @@
"notification_toggle_setting_description": "Povolení e-mailových oznámení",
"notifications": "Oznámení",
"notifications_setting_description": "Správa oznámení",
"oauth": "OAuth",
"official_immich_resources": "Oficiální zdroje Immich",
"offline_paths": "Offline cesty",
"offline_paths_description": "Tyto výsledky mohou být způsobeny ručním odstraněním souborů, které nejsou součástí externí knihovny.",
"offline": "Offline",
"ok": "Ok",
"oldest_first": "Nejstarší první",
"on_this_device": "V tomto zařízení",
"onboarding": "Zahájení",
"onboarding_privacy_description": "Následující (volitelné) funkce jsou závislé na externích službách a lze je kdykoli zakázat v nastavení správy.",
"onboarding_locale_description": "Vyberte preferovaný jazyk. Tento výběr můžete později změnit v nastavení.",
"onboarding_privacy_description": "Následující (volitelné) funkce jsou závislé na externích službách a lze je kdykoli zakázat v nastavení.",
"onboarding_server_welcome_description": "Pojďme nastavit vaši instanci pomocí několika běžných nastavení.",
"onboarding_theme_description": "Zvolte si barevný motiv pro svou instanci. Můžete to později změnit v nastavení.",
"onboarding_welcome_description": "Nastavíme vaši instanci pomocí několika běžných nastavení.",
"onboarding_user_welcome_description": "Pojďme na to!",
"onboarding_welcome_user": "Vítej, {user}",
"online": "Online",
"only_favorites": "Pouze oblíbené",
"open": "Otevřít",
"open_in_map_view": "Otevřít v zobrazení mapy",
@@ -1326,6 +1332,7 @@
"other_variables": "Další proměnné",
"owned": "Vlastní",
"owner": "Vlastník",
"partner": "Partner",
"partner_can_access": "{partner} má přístup",
"partner_can_access_assets": "Všechny vaše fotky a videa kromě těch, které jsou v sekcích Archivováno a Smazáno",
"partner_can_access_location": "Místo, kde byly vaše fotografie pořízeny",
@@ -1365,6 +1372,8 @@
"permanently_delete_assets_prompt": "Opravdu chcete trvale smazat {count, plural, one {tuto položku} few {tyto <b>#</b> položky} other {těchto <b>#</b> položek}}? Tím {count, plural, one {ji také odstraníte z jejích} other {je také odstraníte z jejich}} alb.",
"permanently_deleted_asset": "Položka trvale odstraněna",
"permanently_deleted_assets_count": "{count, plural, one {Položka trvale vymazána} other {Položky trvale vymazány}}",
"permission": "Oprávnění",
"permission_empty": "Vaše oprávnění by nemělo být prázdné",
"permission_onboarding_back": "Zpět",
"permission_onboarding_continue_anyway": "Přesto pokračovat",
"permission_onboarding_get_started": "Začít",
@@ -1394,13 +1403,17 @@
"play_motion_photo": "Přehrát pohybovou fotografii",
"play_or_pause_video": "Přehrát nebo pozastavit video",
"please_auth_to_access": "Pro přístup se prosím ověřte",
"port": "Port",
"preferences_settings_subtitle": "Správa předvoleb aplikace",
"preferences_settings_title": "Předvolby",
"preset": "Přednastavení",
"preview": "Náhled",
"previous": "Předchozí",
"previous_memory": "Předchozí vzpomínka",
"previous_or_next_photo": "Předchozí nebo další fotka",
"previous_or_next_day": "Následující/předchozí den",
"previous_or_next_month": "Následující/předchozí měsíc",
"previous_or_next_photo": "Následující/předchozí fotografie",
"previous_or_next_year": "Následující/předchozí rok",
"primary": "Primární",
"privacy": "Soukromí",
"profile": "Profil",
@@ -1408,6 +1421,7 @@
"profile_drawer_client_out_of_date_major": "Mobilní aplikace je zastaralá. Aktualizujte ji na nejnovější hlavní verzi.",
"profile_drawer_client_out_of_date_minor": "Mobilní aplikace je zastaralá. Aktualizujte ji na nejnovější verzi.",
"profile_drawer_client_server_up_to_date": "Klient a server jsou aktuální",
"profile_drawer_github": "GitHub",
"profile_drawer_server_out_of_date_major": "Server je zastaralý. Aktualizujte na nejnovější hlavní verzi.",
"profile_drawer_server_out_of_date_minor": "Server je zastaralý. Aktualizujte je na nejnovější verzi.",
"profile_image_of_user": "Profilový obrázek uživatele {user}",
@@ -1444,6 +1458,7 @@
"purchase_remove_server_product_key_prompt": "Opravdu chcete odebrat serverový produktový klíč?",
"purchase_server_description_1": "Pro celý server",
"purchase_server_description_2": "Stav podporovatele",
"purchase_server_title": "Server",
"purchase_settings_server_activated": "Produktový klíč serveru spravuje správce",
"rating": "Hodnocení hvězdičkami",
"rating_clear": "Vyčistit hodnocení",
@@ -1486,6 +1501,7 @@
"remove_from_shared_link": "Odstranit ze sdíleného odkazu",
"remove_memory": "Odstranit vzpomínku",
"remove_photo_from_memory": "Odstranit fotografii z této vzpomínky",
"remove_tag": "Odstranit značku",
"remove_url": "Odstranit URL",
"remove_user": "Odebrat uživatele",
"removed_api_key": "Odstraněn API klíč: {name}",
@@ -1517,6 +1533,8 @@
"resume": "Pokračovat",
"retry_upload": "Opakování nahrávání",
"review_duplicates": "Kontrola duplicit",
"role": "Role",
"role_editor": "Editor",
"role_viewer": "Divák",
"save": "Uložit",
"save_to_gallery": "Uložit do galerie",
@@ -1590,6 +1608,7 @@
"select_album_cover": "Vybrat obal alba",
"select_all": "Vybrat vše",
"select_all_duplicates": "Vybrat všechny duplicity",
"select_all_in": "Vybrat vše ve skupině {group}",
"select_avatar_color": "Vyberte barvu avatara",
"select_face": "Vybrat obličej",
"select_featured_photo": "Vybrat hlavní fotografii",
@@ -1610,6 +1629,7 @@
"server_info_box_server_url": "URL serveru",
"server_offline": "Server offline",
"server_online": "Server online",
"server_privacy": "Ochrana soukromí serveru",
"server_stats": "Statistiky serveru",
"server_version": "Verze serveru",
"set": "Nastavit",
@@ -1619,6 +1639,7 @@
"set_date_of_birth": "Nastavit datum narození",
"set_profile_picture": "Nastavit profilový obrázek",
"set_slideshow_to_fullscreen": "Nastavit prezentaci na celou obrazovku",
"set_stack_primary_asset": "Nastavit jako hlavní položku",
"setting_image_viewer_help": "V prohlížeči detailů se nejprve načte malá miniatura, poté se načte náhled střední velikosti (je-li povolen) a nakonec se načte originál (je-li povolen).",
"setting_image_viewer_original_subtitle": "Umožňuje načíst původní obrázek v plném rozlišení (velký!). Zakažte pro snížení využití dat (v síti i v mezipaměti zařízení).",
"setting_image_viewer_original_title": "Načíst původní obrázek",
@@ -1627,7 +1648,6 @@
"setting_image_viewer_title": "Obrázky",
"setting_languages_apply": "Použít",
"setting_languages_subtitle": "Změna jazyka aplikace",
"setting_languages_title": "Jazyk",
"setting_notifications_notify_failures_grace_period": "Oznámení o selhání zálohování na pozadí: {duration}",
"setting_notifications_notify_hours": "{count} hodin",
"setting_notifications_notify_immediately": "okamžitě",
@@ -1690,6 +1710,7 @@
"shared_link_expires_second": "Vyprší za {count} sekundu",
"shared_link_expires_seconds": "Vyprší za {count} sekund",
"shared_link_individual_shared": "Individuální sdílení",
"shared_link_info_chip_metadata": "EXIF",
"shared_link_manage_links": "Spravovat sdílené odkazy",
"shared_link_options": "Možnosti sdíleného odkazu",
"shared_links": "Sdílené odkazy",
@@ -1752,9 +1773,11 @@
"stack_selected_photos": "Seskupení vybraných fotografií",
"stacked_assets_count": "{count, plural, one {Seskupena # položka} few {Seskupeny # položky} other {Seskupeno # položek}}",
"stacktrace": "Výpis zásobníku",
"start": "Start",
"start_date": "Počáteční datum",
"state": "Stát",
"status": "Stav",
"stop_casting": "Zastavit odesílání",
"stop_motion_photo": "Zastavit pohyblivou fotografii",
"stop_photo_sharing": "Přestat sdílet své fotografie?",
"stop_photo_sharing_description": "{partner} již nebude mít přístup k vašim fotkám.",
@@ -1812,7 +1835,6 @@
"to_parent": "Přejít k rodiči",
"to_trash": "Vyhodit",
"toggle_settings": "Přepnout nastavení",
"toggle_theme": "Přepnout tmavý motiv",
"total": "Celkem",
"total_usage": "Celkové využití",
"trash": "Koš",
@@ -1834,6 +1856,7 @@
"unable_to_setup_pin_code": "Nelze nastavit PIN kód",
"unarchive": "Odarchivovat",
"unarchived_count": "{count, plural, one {Odarchivována #} few {Odarchivovány #} other {Odarchivováno #}}",
"undo": "Vrátit zpět",
"unfavorite": "Zrušit oblíbení",
"unhide_person": "Zrušit skrytí osoby",
"unknown": "Neznámý",
@@ -1850,10 +1873,9 @@
"unsaved_change": "Neuložená změna",
"unselect_all": "Zrušit výběr všech",
"unselect_all_duplicates": "Zrušit výběr všech duplicit",
"unselect_all_in": "Zrušit výběr ve skupině {group}",
"unstack": "Zrušit seskupení",
"unstacked_assets_count": "{count, plural, one {Rozložená # položka} few {Rozložené # položky} other {Rozložených # položiek}}",
"untracked_files": "Nesledované soubory",
"untracked_files_decription": "Tyto soubory nejsou aplikaci známy. Mohou být výsledkem neúspěšných přesunů, přerušeného nahrávání nebo mohou zůstat pozadu kvůli chybě",
"up_next": "To je prozatím vše",
"updated_at": "Aktualizováno",
"updated_password": "Heslo aktualizováno",
@@ -1870,6 +1892,7 @@
"upload_success": "Nahrání proběhlo úspěšně, obnovením stránky se zobrazí nově nahrané položky.",
"upload_to_immich": "Nahrát do Immich ({count})",
"uploading": "Nahrávání",
"url": "URL",
"usage": "Využití",
"use_biometric": "Použít biometrické údaje",
"use_current_connection": "použít aktuální připojení",
@@ -1880,6 +1903,7 @@
"user_liked": "Uživateli {user} se {type, select, photo {líbila tato fotka} video {líbilo toto video} asset {líbila tato položka} other {to líbilo}}",
"user_pin_code_settings": "PIN kód",
"user_pin_code_settings_description": "Správa vašeho PIN kódu",
"user_privacy": "Ochrana soukromí uživatelů",
"user_purchase_settings": "Nákup",
"user_purchase_settings_description": "Správa vašeho nákupu",
"user_role_set": "Uživatel {user} nastaven jako {role}",
@@ -1895,13 +1919,9 @@
"version": "Verze",
"version_announcement_closing": "Váš přítel Alex",
"version_announcement_message": "Ahoj! K dispozici je nová verze aplikace Immich. Věnujte prosím chvíli přečtení <link>poznámek k vydání</link> a ujistěte se, že je vaše nastavení aktuální, abyste předešli případným chybným konfiguracím, zejména pokud používáte WatchTower nebo jiný mechanismus, který se stará o automatickou aktualizaci instance aplikace Immich.",
"version_announcement_overlay_release_notes": "poznámky k vydání",
"version_announcement_overlay_text_1": "Ahoj, k dispozici je nová verze",
"version_announcement_overlay_text_2": "najděte si čas na návštěvu ",
"version_announcement_overlay_text_3": " a ujistěte se, že vaše konfigurace docker-compose a .env je aktuální, abyste předešli nesprávné konfiguraci, zvláště pokud používáte WatchTower nebo jakýkoli mechanismus, který podporuje automatické aktualizace serverových aplikací.",
"version_announcement_overlay_title": "K dispozici je nová verze serveru 🎉",
"version_history": "Historie verzí",
"version_history_item": "Nainstalováno {version} dne {date}",
"video": "Video",
"video_hover_setting": "Přehrávat miniaturu videa po najetí myší",
"video_hover_setting_description": "Přehrát miniaturu videa při najetí myší na položku. I když je přehrávání vypnuto, lze jej spustit najetím na ikonu přehrávání.",
"videos": "Videa",
@@ -1918,6 +1938,7 @@
"view_previous_asset": "Zobrazit předchozí položku",
"view_qr_code": "Zobrazit QR kód",
"view_stack": "Zobrazit seskupení",
"view_user": "Zobrazit uživatele",
"viewer_remove_from_stack": "Odstranit ze zásobníku",
"viewer_stack_use_as_main_asset": "Použít jako hlavní položku",
"viewer_unstack": "Rozbalit zásobník",

View File

@@ -31,7 +31,6 @@
"asset_offline_description": "Библиотекӑн ҫак тулаш файлне дискра урӑх тупайман, карҫинккана куҫарнӑ. Енчен те файла вулавӑш ӑшне куҫарнӑ пулсан, тивӗҫлӗ ҫӗнӗ ресурс тупас тесен хӑвӑрӑн вӑхӑтлӑх шкалӑна тӗрӗслӗр. Ҫак файла ҫӗнӗрен чӗртес тесен файл патне каймалли ҫула Immich валли аяларах ҫитернине курса ӗненӗр, библиотекӑна сканерланине пурнӑҫлӑр.",
"authentication_settings_disable_all": "Эсир кӗмелли пур меслетсене те чарса лартасшӑн тесе шутлатӑр-и? Кӗмелли шӑтӑка пӗтӗмпех уҫаҫҫӗ.",
"background_task_job": "Курăнман ӗҫсем",
"check_all": "Пурне те тӗрӗслӗр",
"cleared_jobs": "Ӗҫсене тасатнӑ:{job}",
"confirm_email_below": "Ҫирӗплетес тесен, аяларах «{email}» кӗртӗр",
"confirm_reprocess_all_faces": "Пӗтӗм сӑнӗсене тепӗр хут палӑртас килет тесе шанатӑр-и? Ҫавӑн пекех ятсене пур ҫынран та хуратӗҫ.",

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