Compare commits

..

31 Commits

Author SHA1 Message Date
midzelis
ed63399181 refactor(web): extract timeline selection logic into SelectableSegment and SelectableDay components
- Move asset selection, range selection, and keyboard interaction logic
  to SelectableSegment
  - Extract day group selection logic to SelectableDay component
  - Simplify Timeline component by removing selection-related state and
  handlers
  - Fix scroll compensation handling with dedicated while loop
  - Remove unused keyboard handlers from Scrubber component
2025-09-30 00:08:14 +00:00
midzelis
0168353c2d refactor(web): extract asset grid layout logic into AssetLayout component
- Extracts the asset grid rendering logic from `MonthSegment` into a
  dedicated `AssetLayout` component
- Simplifies `MonthSegment` by delegating layout responsibilities
  while maintaining all existing functionality
- Renames `customLayout` prop to `customThumbnailLayout` for clarity
  across Timeline components


## Changes
  - Created new `AssetLayout.svelte` component that handles:
    - Asset grid rendering with proper positioning
    - Animation transitions
    - Filtering of intersecting viewer assets
  - Updated `MonthSegment.svelte` to use `AssetLayout` via composition
  pattern
  - Renamed `customLayout` to `customThumbnailLayout` in Timeline and
  related components
  - Moved thumbnail click and selection logic to Timeline parent
  component using snippets
2025-09-30 00:08:14 +00:00
midzelis
c44b315117 refactor(web): consolidate asset operations in PhotostreamManager base class
Moves common asset operation methods (upsertAssets, removeAssets, 
updateAssetOperation) from TimelineManager into PhotostreamManager 
base class, making them available to all photostream implementations. 
Updates all consuming components to use the more accurate 'upsertAssets' 
naming instead of separate 'addAssets' and 'updateAssets' methods.

- Move asset operation methods to PhotostreamManager base class
- Replace addAssets/updateAssets calls with unified upsertAssets method
- Update type imports to use PhotostreamManager instead of TimelineManager
- Remove operations-support.svelte.ts (functionality moved to base class)
- Add abstract upsertAssetIntoSegment method for subclass customization
2025-09-30 00:00:50 +00:00
midzelis
98ab224791 refactor: adjust favorite, delete, and archive actions for timeline
- Pass TimelineManager instance to timeline action components
  instead of callbacks
- Move asset update logic (delete, archive, favorite) into
  action components
2025-09-29 01:26:59 +00:00
midzelis
e5fce47c0c Rename TimelineDateGroup to MonthSegment 2025-09-28 19:41:41 +00:00
midzelis
3a468a3f50 refactor(web): extract common timeline functionality into PhotostreamManager base classes
Create abstract PhotostreamManager and PhotostreamSegment base classes to enable reusable      
timeline-like components. This refactoring extracts common viewport management, scroll         
handling, and segment operations from TimelineManager and MonthGroup into reusable             
abstractions.                                                                                  
                                                                                                  
Changes:                                                                                       
 - Add PhotostreamManager.svelte.ts with viewport and scroll management                         
 - Add PhotostreamSegment.svelte.ts with segment positioning and intersection logic             
 - Refactor TimelineManager to extend PhotostreamManager                                        
 - Refactor MonthGroup to extend PhotostreamSegment                                             
 - Add utility functions for segment identification and date formatting                         
 - Update tests to reflect new inheritance structure
2025-09-28 19:41:41 +00:00
midzelis
e600cf64b0 refactor(web): extract asset viewer logic from Timeline into TimelineAssetViewer component
- Extracted asset viewer navigation and action handling logic from Timeline.svelte into a dedicated TimelineAssetViewer component
- Reduces Timeline.svelte complexity by ~150 lines and improves separation of concerns
- No functional changes - purely a refactoring to improve code organization

## Changes
- Created new TimelineAssetViewer.svelte component containing all asset viewer-related logic
- Moved handlePrevious, handleNext, handleRandom, handleClose, handlePreAction, and handleAction methods
- Timeline.svelte now only passes required props to the new component
- Maintained all existing functionality including navigation, asset actions, and stack management
2025-09-28 19:41:41 +00:00
midzelis
41066b1c31 refactor(web): extract timeline keyboard actions into separate component
Extracts keyboard shortcuts and related functionality from Timeline component into a dedicated TimelineKeyboardActions component for better separation of concerns and maintainability.
2025-09-28 19:41:41 +00:00
midzelis
9051fa6949 refactor(web): Clarify property names in Timeline and Scrubber
Renamed properties across Timeline/Scrubber components for clarity:
  - scrubOverallPercent → timelineScrollPercent
  - scrubberMonthPercent → viewportTopMonthScrollPercent
  - scrubberMonth → viewportTopMonth
  - leadout → isInLeadOutSection

  Additional changes:
  - Updated ScrubberListener signature to accept object parameter
  - Added detailed JSDoc comments for all Scrubber props
  - Fixed callback invocations to use new object syntax
  - Aligned Timeline's local state variables with Scrubber prop names
2025-09-28 19:41:41 +00:00
shenlong
bea116e1b9 fix: prefer remote images in new timeline (#22452)
fix: prefer remote images in new thumbnail

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-27 21:29:28 -05:00
shenlong
cdbe1d7f10 chore: show download button for remote only assets (#22453)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-27 21:28:07 -05:00
Brandon Wees
df469cc412 feat: show motion photo icon on mobile timeline tile (#22454)
* feat: show motion photo icon on timeline tile

* chore: switch to private widget for asset type icons

* chore: small cleanup on asset type icons widget
2025-09-27 21:27:34 -05:00
shenlong
8de7eed940 feat(mobile): add unstack button (#21869)
* fix: add unstack button

* feat: allow unstacking inside of asset viewer

* chore: update tests

* chore: rework unstacking in asset viewer

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: bwees <brandonwees@gmail.com>
2025-09-28 06:51:38 +05:30
shenlong
7d8cd05bc2 fix: remote album timeline filter (#22423)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-26 17:35:46 +00:00
Brandon Wees
30a378c580 fix: local assets should not be added to album (#22304) 2025-09-26 22:41:12 +05:30
renovate[bot]
8a3684c127 chore(deps): update ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0 docker digest to 41eacbe (#22305)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-26 14:26:55 +02:00
renovate[bot]
61e5c6349c chore(deps): update github-actions (#22311)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-26 14:26:47 +02:00
Mert
3bcb4b7af7 fix(mobile): scrubbing mode on scroll to date event (#22390) 2025-09-25 19:20:42 -05:00
Mert
5116b215a2 fix(mobile): load local thumbnails in album timeline (#22329)
* join local asset in album query

* missed one

* formatting
2025-09-26 00:38:19 +05:30
shenlong
c5fbbee8f6 chore: update android background worker notification text (#22347)
chore: update android bg notification text

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-26 00:22:17 +05:30
shenlong
d73aabc494 chore: log mobile upload failures (#22349)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-26 00:22:03 +05:30
shenlong
b62feb726b fix: delete temp file on iOS after upload (#22364)
fix: delete temp files on iOS after upload

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-26 00:21:25 +05:30
Brandon Wees
972e9cc039 fix: map attribution and other styling (#22303)
* chore: map widget and page styling

* fix: map bottom sheet styling

* fix: attribution location on android

it appears that on android, the attribution marker is positioned from the top of the display and on iOS its positioned from the safe area edge
2025-09-26 00:08:25 +05:30
shenlong
ee49136e97 chore: deprecate old timeline (#22328)
* chore: deprecate old timeline

* change trigger and duration

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-26 00:06:17 +05:30
Jason Rasmussen
913f543307 chore: use quick-start as default page (#22384) 2025-09-25 12:50:34 -04:00
Zack Pollard
7c3e871c7a chore: cleanup 1.143.0 from doc versions (#22381)
chore: cleanup old version 1.143.0
2025-09-25 12:13:59 -04:00
Zack Pollard
66b8823949 chore: docs redirect and patch version cleanup (#22380) 2025-09-25 16:47:38 +01:00
shenlong
5aa7ab5aeb fix: ios export sqlite db (#22369) 2025-09-25 10:01:54 -05:00
Zack Pollard
37a3784d80 feat: docs.immich.app (#21819)
Co-authored-by: Jason Rasmussen <jason@rasm.me>
2025-09-25 15:54:34 +01:00
Alex
e7b57fc2f6 chore: post release tasks (#22339) 2025-09-25 08:36:51 -04:00
Aaron Liu
94a8bc5bce fix(pump-version): fix immich-ml pyproject.toml path (#22372)
Since the migration to uv, this has not worked because the Python toml CLI has been looking for a pyproject.toml in the root folder.
2025-09-25 03:29:10 -04:00
130 changed files with 2580 additions and 2073 deletions

View File

@@ -36,7 +36,7 @@ jobs:
steps:
- name: Check what should run
id: check
uses: immich-app/devtools/actions/pre-job@24820aa4ef67959b0dcf69a438cccf00d7c7042b # pre-job-action-v1.0.1
uses: immich-app/devtools/actions/pre-job@5f91b52dfbb92b8d96ca411ab59c896cd59714ca # pre-job-action-v1.1.0
with:
filters: |
mobile:
@@ -73,7 +73,7 @@ jobs:
- name: Restore Gradle Cache
id: cache-gradle-restore
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: |
~/.gradle/caches
@@ -130,7 +130,7 @@ jobs:
- name: Save Gradle Cache
id: cache-gradle-save
uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
if: github.ref == 'refs/heads/main'
with:
path: |

View File

@@ -24,7 +24,7 @@ jobs:
steps:
- name: Check what should run
id: check
uses: immich-app/devtools/actions/pre-job@24820aa4ef67959b0dcf69a438cccf00d7c7042b # pre-job-action-v1.0.1
uses: immich-app/devtools/actions/pre-job@5f91b52dfbb92b8d96ca411ab59c896cd59714ca # pre-job-action-v1.1.0
with:
filters: |
server:

View File

@@ -22,7 +22,7 @@ jobs:
steps:
- name: Check what should run
id: check
uses: immich-app/devtools/actions/pre-job@24820aa4ef67959b0dcf69a438cccf00d7c7042b # pre-job-action-v1.0.1
uses: immich-app/devtools/actions/pre-job@5f91b52dfbb92b8d96ca411ab59c896cd59714ca # pre-job-action-v1.1.0
with:
filters: |
docs:

View File

@@ -16,7 +16,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

View File

@@ -58,7 +58,7 @@ jobs:
- name: Generate a token
id: generate_token
if: ${{ inputs.skip != true }}
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

View File

@@ -49,7 +49,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@@ -111,7 +111,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

View File

@@ -21,7 +21,7 @@ jobs:
steps:
- name: Check what should run
id: check
uses: immich-app/devtools/actions/pre-job@24820aa4ef67959b0dcf69a438cccf00d7c7042b # pre-job-action-v1.0.1
uses: immich-app/devtools/actions/pre-job@5f91b52dfbb92b8d96ca411ab59c896cd59714ca # pre-job-action-v1.1.0
with:
filters: |
mobile:

View File

@@ -18,7 +18,7 @@ jobs:
steps:
- name: Check what should run
id: check
uses: immich-app/devtools/actions/pre-job@24820aa4ef67959b0dcf69a438cccf00d7c7042b # pre-job-action-v1.0.1
uses: immich-app/devtools/actions/pre-job@5f91b52dfbb92b8d96ca411ab59c896cd59714ca # pre-job-action-v1.1.0
with:
filters: |
i18n:

View File

@@ -25,7 +25,7 @@ jobs:
steps:
- name: Check what should run
id: check
uses: immich-app/devtools/actions/pre-job@24820aa4ef67959b0dcf69a438cccf00d7c7042b # pre-job-action-v1.0.1
uses: immich-app/devtools/actions/pre-job@5f91b52dfbb92b8d96ca411ab59c896cd59714ca # pre-job-action-v1.1.0
with:
filters: |
i18n:

4
deployment/.env Normal file
View File

@@ -0,0 +1,4 @@
export CLOUDFLARE_ACCOUNT_ID="op://tf/cloudflare/account_id"
export CLOUDFLARE_API_TOKEN="op://tf/cloudflare/api_token"
export TF_STATE_POSTGRES_CONN_STR="op://tf/tf_state/postgres_conn_str"
export TF_VAR_env=$ENVIRONMENT

View File

@@ -1,11 +1,11 @@
resource "cloudflare_pages_domain" "immich_app_release_domain" {
account_id = var.cloudflare_account_id
project_name = data.terraform_remote_state.cloudflare_account.outputs.immich_app_archive_pages_project_name
domain = "immich.app"
domain = "docs.immich.app"
}
resource "cloudflare_record" "immich_app_release_domain" {
name = "immich.app"
name = "docs.immich.app"
proxied = true
ttl = 1
type = "CNAME"

View File

@@ -1,11 +1,11 @@
resource "cloudflare_pages_domain" "immich_app_branch_domain" {
account_id = var.cloudflare_account_id
project_name = local.is_release ? data.terraform_remote_state.cloudflare_account.outputs.immich_app_archive_pages_project_name : data.terraform_remote_state.cloudflare_account.outputs.immich_app_preview_pages_project_name
domain = "${var.prefix_name}.${local.deploy_domain_prefix}.immich.app"
domain = "docs.${var.prefix_name}.${local.deploy_domain_prefix}.immich.app"
}
resource "cloudflare_record" "immich_app_branch_subdomain" {
name = "${var.prefix_name}.${local.deploy_domain_prefix}.immich.app"
name = "docs.${var.prefix_name}.${local.deploy_domain_prefix}.immich.app"
proxied = true
ttl = 1
type = "CNAME"

View File

@@ -140,7 +140,7 @@ services:
database:
container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:c44be5f2871c59362966d71eab4268170eb6f5653c0e6170184e72b38ffdf107
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:41eacbe83eca995561fe43814fd4891e16e39632806253848efaf04d3c8a8b84
env_file:
- .env
environment:

View File

@@ -63,7 +63,7 @@ services:
database:
container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:c44be5f2871c59362966d71eab4268170eb6f5653c0e6170184e72b38ffdf107
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:41eacbe83eca995561fe43814fd4891e16e39632806253848efaf04d3c8a8b84
env_file:
- .env
environment:

View File

@@ -56,7 +56,7 @@ services:
database:
container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:c44be5f2871c59362966d71eab4268170eb6f5653c0e6170184e72b38ffdf107
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:41eacbe83eca995561fe43814fd4891e16e39632806253848efaf04d3c8a8b84
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME}

View File

@@ -30,11 +30,11 @@ When in doubt or if you have an edge case scenario, we encourage you to contact
### How can I reset the admin password?
The admin password can be reset by running the [reset-admin-password](/docs/administration/server-commands.md) command on the immich-server.
The admin password can be reset by running the [reset-admin-password](/administration/server-commands.md) command on the immich-server.
### How can I see a list of all users in Immich?
You can see the list of all users by running [list-users](/docs/administration/server-commands.md) Command on the Immich-server.
You can see the list of all users by running [list-users](/administration/server-commands.md) Command on the Immich-server.
---
@@ -106,20 +106,20 @@ However, Immich will delete original files that have been trashed when the trash
When Storage Template is off (default) Immich saves the file names in a random string (also known as random UUIDs) to prevent duplicate file names.
To retrieve the original file names, you must enable the Storage Template and then run the STORAGE TEMPLATE MIGRATION job.
It is recommended to read about [Storage Template](https://immich.app/docs/administration/storage-template) before activation.
It is recommended to read about [Storage Template](/administration/storage-template) before activation.
### Can I add my existing photo library?
Yes, with an [External Library](/docs/features/libraries.md).
Yes, with an [External Library](/features/libraries.md).
### What happens to existing files after I choose a new [Storage Template](/docs/administration/storage-template.mdx)?
### What happens to existing files after I choose a new [Storage Template](/administration/storage-template.mdx)?
Template changes will only apply to _new_ assets. To retroactively apply the template to previously uploaded assets, run the Storage Migration Job, available on the [Jobs](/docs/administration/jobs-workers/#jobs) page.
Template changes will only apply to _new_ assets. To retroactively apply the template to previously uploaded assets, run the Storage Migration Job, available on the [Jobs](/administration/jobs-workers/#jobs) page.
### Why are only photos and not videos being uploaded to Immich?
This often happens when using a reverse proxy in front of Immich.
Make sure to [set your reverse proxy](/docs/administration/reverse-proxy/) to allow large requests.
Make sure to [set your reverse proxy](/administration/reverse-proxy/) to allow large requests.
Also, check the disk space of your reverse proxy.
In some cases, proxies cache requests to disk before passing them on, and if disk space runs out, the request fails.
@@ -139,7 +139,7 @@ You can _archive_ them.
### How can I backup data from Immich?
See [Backup and Restore](/docs/administration/backup-and-restore.md).
See [Backup and Restore](/administration/backup-and-restore.md).
### Does Immich support reading existing face tag metadata?
@@ -225,7 +225,7 @@ volumes:
### Can I keep my existing album structure while importing assets into Immich?
Yes, by using the [Immich CLI](/docs/features/command-line-interface) along with the `--album` flag.
Yes, by using the [Immich CLI](/features/command-line-interface) along with the `--album` flag.
### Is there a way to reorder photos within an album?
@@ -266,7 +266,7 @@ Immich uses CLIP models. An ML model converts each image to an "embedding", whic
### How does facial recognition work?
See [How Facial Recognition Works](/docs/features/facial-recognition#how-facial-recognition-works) for details.
See [How Facial Recognition Works](/features/facial-recognition#how-facial-recognition-works) for details.
### How can I disable machine learning?
@@ -288,7 +288,7 @@ No, this is not supported. Only models listed in the [Hugging Face][huggingface]
### I want to be able to search in other languages besides English. How can I do that?
You can change to a multilingual CLIP model. See [here](/docs/features/searching#clip-models) for instructions.
You can change to a multilingual CLIP model. See [here](/features/searching#clip-models) for instructions.
### Does Immich support Facial Recognition for videos?
@@ -299,7 +299,7 @@ Scanning the entire video for faces may be implemented in the future.
No.
:::tip
You can use [Smart Search](/docs/features/searching.md) for this to some extent. For example, if you have a Golden Retriever and a Chihuahua, type these words in the smart search and watch the results.
You can use [Smart Search](/features/searching.md) for this to some extent. For example, if you have a Golden Retriever and a Chihuahua, type these words in the smart search and watch the results.
:::
### I'm getting a lot of "faces" that aren't faces, what can I do?
@@ -329,7 +329,7 @@ ls clip/ facial-recognition/
### Why is Immich slow on low-memory systems like the Raspberry Pi?
Immich optionally uses transcoding and machine learning for several features. However, it can be too heavy to run on a Raspberry Pi. You can [mitigate](/docs/FAQ#can-i-lower-cpu-and-ram-usage) this or host Immich's machine-learning container on a [more powerful system](/docs/guides/remote-machine-learning), or [disable](/docs/FAQ#how-can-i-disable-machine-learning) machine learning entirely.
Immich optionally uses transcoding and machine learning for several features. However, it can be too heavy to run on a Raspberry Pi. You can [mitigate](/FAQ#can-i-lower-cpu-and-ram-usage) this or host Immich's machine-learning container on a [more powerful system](/guides/remote-machine-learning), or [disable](/FAQ#how-can-i-disable-machine-learning) machine learning entirely.
### Can I lower CPU and RAM usage?
@@ -339,9 +339,9 @@ The initial backup is the most intensive due to the number of jobs running. The
- Under Settings > Transcoding Settings > Threads, set the number of threads to a low number like 1 or 2.
- Under Settings > Machine Learning Settings > Facial Recognition > Model Name, you can change the facial recognition model to `buffalo_s` instead of `buffalo_l`. The former is a smaller and faster model, albeit not as good.
- For facial recognition on new images to work properly, You must re-run the Face Detection job for all images after this.
- At the container level, you can [set resource constraints](/docs/FAQ#can-i-limit-cpu-and-ram-usage) to lower usage further.
- At the container level, you can [set resource constraints](/FAQ#can-i-limit-cpu-and-ram-usage) to lower usage further.
- It's recommended to only apply these constraints _after_ taking some of the measures here for best performance.
- If these changes are not enough, see [above](/docs/FAQ#how-can-i-disable-machine-learning) for instructions on how to disable machine learning.
- If these changes are not enough, see [above](/FAQ#how-can-i-disable-machine-learning) for instructions on how to disable machine learning.
### Can I limit CPU and RAM usage?
@@ -383,7 +383,7 @@ Do not exaggerate with the job concurrency because you're probably thoroughly ov
### My server shows Server Status Offline | Version Unknown. What can I do?
You need to [enable WebSockets](/docs/administration/reverse-proxy/) on your reverse proxy.
You need to [enable WebSockets](/administration/reverse-proxy/) on your reverse proxy.
---
@@ -391,7 +391,7 @@ You need to [enable WebSockets](/docs/administration/reverse-proxy/) on your rev
### How can I see Immich logs?
Immich components are typically deployed using docker. To see logs for deployed docker containers, you can use the [Docker CLI](https://docs.docker.com/engine/reference/commandline/cli/), specifically the `docker logs` command. For examples, see [Docker Help](/docs/guides/docker-help.md).
Immich components are typically deployed using docker. To see logs for deployed docker containers, you can use the [Docker CLI](https://docs.docker.com/engine/reference/commandline/cli/), specifically the `docker logs` command. For examples, see [Docker Help](/guides/docker-help.md).
### How can I reduce the log verbosity of Redis?
@@ -435,7 +435,7 @@ cap_drop:
Data for Immich comes in two forms:
1. **Metadata** stored in a Postgres database, stored in the `DB_DATA_LOCATION` folder (previously `pg_data` Docker volume).
2. **Files** (originals, thumbs, profile, etc.), stored in the `UPLOAD_LOCATION` folder, more [info](/docs/administration/backup-and-restore#asset-types-and-storage-locations).
2. **Files** (originals, thumbs, profile, etc.), stored in the `UPLOAD_LOCATION` folder, more [info](/administration/backup-and-restore#asset-types-and-storage-locations).
:::warning
This will destroy your database and reset your instance, meaning that you start from scratch.
@@ -473,7 +473,7 @@ If it mentions SIGILL (note the lack of a K) or error code 132, it most likely m
### Why am I getting database ownership errors?
If you get database errors such as `FATAL: data directory "/var/lib/postgresql/data" has wrong ownership` upon database startup, this is likely due to an issue with your filesystem.
NTFS and ex/FAT/32 filesystems are not supported. See [here](/docs/install/requirements#special-requirements-for-windows-users) for more details.
NTFS and ex/FAT/32 filesystems are not supported. See [here](/install/requirements#special-requirements-for-windows-users) for more details.
### How can I verify the integrity of my database?

View File

@@ -3,7 +3,7 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
A [3-2-1 backup strategy](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) is recommended to protect your data. You should keep copies of your uploaded photos/videos as well as the Immich database for a comprehensive backup solution. This page provides an overview on how to backup the database and the location of user-uploaded pictures and videos. A template bash script that can be run as a cron job is provided [here](/docs/guides/template-backup-script.md)
A [3-2-1 backup strategy](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) is recommended to protect your data. You should keep copies of your uploaded photos/videos as well as the Immich database for a comprehensive backup solution. This page provides an overview on how to backup the database and the location of user-uploaded pictures and videos. A template bash script that can be run as a cron job is provided [here](/guides/template-backup-script.md)
:::danger
The instructions on this page show you how to prepare your Immich instance to be backed up, and which files to take a backup of. You still need to take care of using an actual backup tool to make a backup yourself.
@@ -160,7 +160,7 @@ for more info read the [release notes](https://github.com/immich-app/immich/rele
:::danger
A backup of this folder does not constitute a backup of your database!
Follow the instructions listed [here](/docs/administration/backup-and-restore#database) to learn how to perform a proper backup.
Follow the instructions listed [here](/administration/backup-and-restore#database) to learn how to perform a proper backup.
:::
</TabItem>
@@ -205,7 +205,7 @@ When you turn off the storage template engine, it will leave the assets in `UPLO
:::danger
A backup of this folder does not constitute a backup of your database!
Follow the instructions listed [here](/docs/administration/backup-and-restore#database) to learn how to perform a proper backup.
Follow the instructions listed [here](/administration/backup-and-restore#database) to learn how to perform a proper backup.
:::
</TabItem>

View File

@@ -12,7 +12,7 @@ You can access the settings panel from the web at `Administration -> Settings ->
Under Email, enter the required details to connect with an SMTP server.
You can use [this guide](/docs/guides/smtp-gmail) to use Gmail's SMTP server.
You can use [this guide](/guides/smtp-gmail) to use Gmail's SMTP server.
## User's notifications settings

View File

@@ -11,7 +11,7 @@ The `immich-server` container contains multiple workers:
## Split workers
If you prefer to throttle or distribute the workers, you can do this using the [environment variables](/docs/install/environment-variables) to specify which container should pick up which tasks.
If you prefer to throttle or distribute the workers, you can do this using the [environment variables](/install/environment-variables) to specify which container should pick up which tasks.
For example, for a simple setup with one container for the Web/API and one for all other microservices, you can do the following:
@@ -53,5 +53,5 @@ Additionally, some jobs (such as memories generation) run on a schedule, which i
<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.
Some jobs ([External Libraries](/features/libraries) scanning, Database Dump) are configured in their own sections in System Settings.
:::

View File

@@ -28,7 +28,7 @@ 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)
- `app.immich:///oauth-callback` - for logging in with OAuth from the [Mobile App](/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
@@ -98,7 +98,7 @@ The redirect URI for the mobile app is `app.immich:///oauth-callback`, which is
2. Whitelist the new endpoint as a valid redirect URI with your provider.
3. Specify the new endpoint as the `Mobile Redirect URI Override`, in the OAuth settings.
With these steps in place, you should be able to use OAuth from the [Mobile App](/docs/features/mobile-app.mdx) without a custom scheme redirect URI.
With these steps in place, you should be able to use OAuth from the [Mobile App](/features/mobile-app.mdx) without a custom scheme redirect URI.
:::info
Immich has a route (`/api/oauth/mobile-redirect`) that is already configured to forward requests to `app.immich:///oauth-callback`, and can be used for step 1.

View File

@@ -16,7 +16,7 @@ The `immich-server` docker image comes preinstalled with an administrative CLI (
## How to run a command
To run a command, [connect](/docs/guides/docker-help.md#attach-to-a-container) to the `immich_server` container and then execute the command via `immich-admin <command>`.
To run a command, [connect](/guides/docker-help.md#attach-to-a-container) to the `immich_server` container and then execute the command via `immich-admin <command>`.
## Examples

View File

@@ -12,14 +12,14 @@ Manage password, OAuth, and other authentication settings
### OAuth Authentication
Immich supports OAuth Authentication. Read more about this feature and its configuration [here](/docs/administration/oauth).
Immich supports OAuth Authentication. Read more about this feature and its configuration [here](/administration/oauth).
### Password Authentication
The administrator can choose to disable login with username and password for the entire instance. This means that **no one**, including the system administrator, will be able to log using this method. If [OAuth Authentication](/docs/administration/oauth) is also disabled, no users will be able to login using **any** method. Changing this setting does not affect existing sessions, just new login attempts.
The administrator can choose to disable login with username and password for the entire instance. This means that **no one**, including the system administrator, will be able to log using this method. If [OAuth Authentication](/administration/oauth) is also disabled, no users will be able to login using **any** method. Changing this setting does not affect existing sessions, just new login attempts.
:::tip
You can always use the [Server CLI](/docs/administration/server-commands) to re-enable password login.
You can always use the [Server CLI](/administration/server-commands) to re-enable password login.
:::
## Image Settings (thumbnails and previews)
@@ -108,7 +108,7 @@ If more than one URL is provided, each server will be attempted one-at-a-time un
### Smart Search
The [smart search](/docs/features/searching) settings allow you to change the [CLIP model](https://openai.com/research/clip). Larger models will typically provide [more accurate search results](https://github.com/immich-app/immich/discussions/11862) but consume more processing power and RAM. When [changing the CLIP model](/docs/FAQ#can-i-use-a-custom-clip-model) it is mandatory to re-run the Smart Search job on all images to fully apply the change.
The [smart search](/features/searching) settings allow you to change the [CLIP model](https://openai.com/research/clip). Larger models will typically provide [more accurate search results](https://github.com/immich-app/immich/discussions/11862) but consume more processing power and RAM. When [changing the CLIP model](/FAQ#can-i-use-a-custom-clip-model) it is mandatory to re-run the Smart Search job on all images to fully apply the change.
:::info Internet connection
Changing models requires a connection to the Internet to download the model.
@@ -132,7 +132,7 @@ Editable settings:
- **Max Recognition Distance**
- **Min Recognized Faces**
You can learn more about these options on the [Facial Recognition page](/docs/features/facial-recognition#how-face-detection-works)
You can learn more about these options on the [Facial Recognition page](/features/facial-recognition#how-face-detection-works)
:::info
When changing the values in Min Detection Score, Max Recognition Distance, and Min Recognized Faces.
@@ -154,15 +154,15 @@ The map can be adjusted via [OpenMapTiles](https://openmaptiles.org/styles/) for
### Reverse Geocoding Settings
Immich supports [Reverse Geocoding](/docs/features/reverse-geocoding) using data from the [GeoNames](https://www.geonames.org/) geographical database.
Immich supports [Reverse Geocoding](/features/reverse-geocoding) using data from the [GeoNames](https://www.geonames.org/) geographical database.
## Notification Settings
SMTP server setup, for user creation notifications, new albums, etc. More information can be found [here](/docs/administration/email-notification)
SMTP server setup, for user creation notifications, new albums, etc. More information can be found [here](/administration/email-notification)
## Notification Templates
Override the default notifications text with notification templates. More information can be found [here](/docs/administration/email-notification)
Override the default notifications text with notification templates. More information can be found [here](/administration/email-notification)
## Server Settings
@@ -176,7 +176,7 @@ The administrator can set a custom message on the login screen (the message will
## Storage Template
Immich supports a custom [Storage Template](/docs/administration/storage-template). Learn more about this feature and its configuration [here](/docs/administration/storage-template).
Immich supports a custom [Storage Template](/administration/storage-template). Learn more about this feature and its configuration [here](/administration/storage-template).
## Theme Settings

View File

@@ -44,7 +44,7 @@ The web app is a [TypeScript](https://www.typescriptlang.org/) project that uses
### CLI
The Immich CLI is an [npm](https://www.npmjs.com/) package that lets users control their Immich instance from the command line. It uses the API to perform various tasks, especially uploading assets. See the [CLI documentation](/docs/features/command-line-interface.md) for more information.
The Immich CLI is an [npm](https://www.npmjs.com/) package that lets users control their Immich instance from the command line. It uses the API to perform various tasks, especially uploading assets. See the [CLI documentation](/features/command-line-interface.md) for more information.
## Server
@@ -83,11 +83,11 @@ Immich uses a [worker](https://github.com/immich-app/immich/blob/main/server/src
- Smart Search
- Facial Recognition
- Storage Template Migration
- Sidecar (see [XMP Sidecars](/docs/features/xmp-sidecars.md))
- Sidecar (see [XMP Sidecars](/features/xmp-sidecars.md))
- Background jobs (file deletion, user deletion)
:::info
This list closely matches what is available on the [Administration > Jobs](/docs/administration/jobs-workers/#jobs) page, which provides some remote queue management capabilities.
This list closely matches what is available on the [Administration > Jobs](/administration/jobs-workers/#jobs) page, which provides some remote queue management capabilities.
:::
### Machine Learning

View File

@@ -431,7 +431,7 @@ While the Dev Container focuses on server and web development, you can connect m
- 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:
3. **For full mobile development**, see the [mobile development guide](/developer/setup) which covers:
- Flutter setup
- Running on simulators/devices
- Mobile-specific debugging
@@ -474,7 +474,7 @@ Recommended minimums:
## Next Steps
- Read the [architecture overview](/docs/developer/architecture)
- Learn about [database migrations](/docs/developer/database-migrations)
- Read the [architecture overview](/developer/architecture)
- Learn about [database migrations](/developer/database-migrations)
- Explore [API documentation](https://api.immich.app/)
- Join `#immich` on [Discord](https://discord.immich.app)

View File

@@ -53,8 +53,8 @@ You can use `dart fix --apply` and `dcm fix lib` to potentially correct some iss
## OpenAPI
The OpenAPI client libraries need to be regenerated whenever there are changes to the `immich-openapi-specs.json` file. Note that you should not modify this file directly as it is auto-generated. See [OpenAPI](/docs/developer/open-api.md) for more details.
The OpenAPI client libraries need to be regenerated whenever there are changes to the `immich-openapi-specs.json` file. Note that you should not modify this file directly as it is auto-generated. See [OpenAPI](/developer/open-api.md) for more details.
## Database Migrations
A database migration needs to be generated whenever there are changes to `server/src/infra/src/entities`. See [Database Migration](/docs/developer/database-migrations.md) for more details.
A database migration needs to be generated whenever there are changes to `server/src/infra/src/entities`. See [Database Migration](/developer/database-migrations.md) for more details.

View File

@@ -16,7 +16,7 @@ If foreground backup is enabled: whenever the app is opened or resumed, it will
## Background backup
This feature is intended for everyday use. For initial bulk uploading, please use the foreground upload feature. For more information on why background upload is not working as expected, please refer to the [FAQ](/docs/FAQ#why-does-foreground-backup-stop-when-i-navigate-away-from-the-app-shouldnt-it-transfer-the-job-to-background-backup).
This feature is intended for everyday use. For initial bulk uploading, please use the foreground upload feature. For more information on why background upload is not working as expected, please refer to the [FAQ](/FAQ#why-does-foreground-backup-stop-when-i-navigate-away-from-the-app-shouldnt-it-transfer-the-job-to-background-backup).
If background backup is enabled. The app will periodically check if there are any new photos or videos in the selected album(s) to be uploaded to the server. If there are, it will upload them to the cloud in the background.

View File

@@ -70,7 +70,7 @@ Navigating to Administration > Settings > Machine Learning Settings > Facial Rec
:::tip
It's better to only tweak the parameters here than to set them to something very different unless you're ready to test a variety of options. If you do need to set a parameter to a strict setting, relaxing other settings can be a good option to compensate, and vice versa.
You can learn how the tune the result in this [Guide](/docs/guides/better-facial-clusters)
You can learn how the tune the result in this [Guide](/guides/better-facial-clusters)
:::
### Facial recognition model

View File

@@ -103,7 +103,7 @@ The `immich-server` container will need access to the gallery. Modify your docke
:::tip
The `ro` flag at the end only gives read-only access to the volumes.
This will disallow the images from being deleted in the web UI, or adding metadata to the library ([XMP sidecars](/docs/features/xmp-sidecars)).
This will disallow the images from being deleted in the web UI, or adding metadata to the library ([XMP sidecars](/features/xmp-sidecars)).
:::
:::info

View File

@@ -35,7 +35,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele
- Where and how you can get this file depends on device and vendor, but typically, the device vendor also supplies these
- The `hwaccel.ml.yml` file assumes the path to it is `/usr/lib/libmali.so`, so update accordingly if it is elsewhere
- The `hwaccel.ml.yml` file assumes an additional file `/lib/firmware/mali_csffw.bin`, so update accordingly if your device's driver does not require this file
- Optional: Configure your `.env` file, see [environment variables](/docs/install/environment-variables) for ARM NN specific settings
- Optional: Configure your `.env` file, see [environment variables](/install/environment-variables) for ARM NN specific settings
- In particular, the `MACHINE_LEARNING_ANN_FP16_TURBO` can significantly improve performance at the cost of very slightly lower accuracy
#### CUDA
@@ -49,7 +49,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele
- The GPU must be supported by ROCm. If it isn't officially supported, you can attempt to use the `HSA_OVERRIDE_GFX_VERSION` environmental variable: `HSA_OVERRIDE_GFX_VERSION=<a supported version, e.g. 10.3.0>`. If this doesn't work, you might need to also set `HSA_USE_SVM=0`.
- The ROCm image is quite large and requires at least 35GiB of free disk space. However, pulling later updates to the service through Docker will generally only amount to a few hundred megabytes as the rest will be cached.
- This backend is new and may experience some issues. For example, GPU power consumption can be higher than usual after running inference, even if the machine learning service is idle. In this case, it will only go back to normal after being idle for 5 minutes (configurable with the [MACHINE_LEARNING_MODEL_TTL](/docs/install/environment-variables) setting).
- This backend is new and may experience some issues. For example, GPU power consumption can be higher than usual after running inference, even if the machine learning service is idle. In this case, it will only go back to normal after being idle for 5 minutes (configurable with the [MACHINE_LEARNING_MODEL_TTL](/install/environment-variables) setting).
#### OpenVINO
@@ -64,7 +64,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele
- This is usually pre-installed on the device vendor's Linux images
- RKNPU driver V0.9.8 or later must be available in the host server
- You may confirm this by running `cat /sys/kernel/debug/rknpu/version` to check the version
- Optional: Configure your `.env` file, see [environment variables](/docs/install/environment-variables) for RKNN specific settings
- Optional: Configure your `.env` file, see [environment variables](/install/environment-variables) for RKNN specific settings
- In particular, setting `MACHINE_LEARNING_RKNN_THREADS` to 2 or 3 can _dramatically_ improve performance for RK3576 and RK3588 compared to the default of 1, at the expense of multiplying the amount of RAM each model uses by that amount.
## Setup

View File

@@ -28,7 +28,7 @@ The beta release channel allows users to test upcoming changes before they are o
<MobileAppBackup />
:::info
You can enable automatic backup on supported devices. For more information see [Automatic Backup](/docs/features/automatic-backup.md).
You can enable automatic backup on supported devices. For more information see [Automatic Backup](/features/automatic-backup.md).
:::
## Sync only selected photos
@@ -75,7 +75,7 @@ You can sync or mirror an album from your phone to the Immich server on your acc
- **User-Specific Sync:** Album synchronization is unique to each server user and does not sync between different users or partners.
- **Mobile-Only Feature:** Album synchronization is currently only available on mobile. For similar options on a computer, refer to [Libraries](/docs/features/libraries) for further details.
- **Mobile-Only Feature:** Album synchronization is currently only available on mobile. For similar options on a computer, refer to [Libraries](/features/libraries) for further details.
### Synchronizing albums from the past

View File

@@ -28,7 +28,7 @@ The metrics in immich are grouped into API (endpoint calls and response times),
Immich will not expose an endpoint for metrics by default. To enable this endpoint, you can add the `IMMICH_TELEMETRY_INCLUDE=all` environmental variable to your `.env` file. Note that only the server container currently use this variable.
:::tip
`IMMICH_TELEMETRY_INCLUDE=all` enables all metrics. For a more granular configuration you can enumerate the telemetry metrics that should be included as a comma separated list (e.g. `IMMICH_TELEMETRY_INCLUDE=repo,api`). Alternatively, you can also exclude specific metrics with `IMMICH_TELEMETRY_EXCLUDE`. For more information refer to the [environment section](/docs/install/environment-variables.md#prometheus).
`IMMICH_TELEMETRY_INCLUDE=all` enables all metrics. For a more granular configuration you can enumerate the telemetry metrics that should be included as a comma separated list (e.g. `IMMICH_TELEMETRY_INCLUDE=repo,api`). Alternatively, you can also exclude specific metrics with `IMMICH_TELEMETRY_EXCLUDE`. For more information refer to the [environment section](/install/environment-variables.md#prometheus).
:::
The next step is to configure a new or existing Prometheus instance to scrape this endpoint. The following steps assume that you do not have an existing Prometheus instance, but the steps will be similar either way.
@@ -68,7 +68,7 @@ After bringing down the containers with `docker compose down` and back up with `
:::note
To see exactly what metrics are made available, you can additionally add `8081:8081` (API metrics) and `8082:8082` (microservices metrics) to the immich_server container's ports.
Visiting the `/metrics` endpoint for these services will show the same raw data that Prometheus collects.
To configure these ports see [`IMMICH_API_METRICS_PORT` & `IMMICH_MICROSERVICES_METRICS_PORT`](/docs/install/environment-variables/#general).
To configure these ports see [`IMMICH_API_METRICS_PORT` & `IMMICH_MICROSERVICES_METRICS_PORT`](/install/environment-variables/#general).
:::
### Usage

View File

@@ -8,7 +8,7 @@ During Exif Extraction, assets with latitudes and longitudes are reverse geocode
## Usage
Data from a reverse geocode is displayed in the image details, and used in [Smart Search](/docs/features/searching.md).
Data from a reverse geocode is displayed in the image details, and used in [Smart Search](/features/searching.md).
<img src={require('./img/reverse-geocoding-mobile3.webp').default} width='33%' title='Reverse Geocoding' />
<img src={require('./img/reverse-geocoding-mobile1.webp').default} width='33%' title='Reverse Geocoding' />

View File

@@ -24,7 +24,7 @@ After creating an album, you can access the sharing options by clicking on the s
Partner sharing allows you to share your _entire_ library with other users of your choice. They can then view your library and download the assets.
You can read this guide to learn more about [partner sharing](/docs/features/partner-sharing).
You can read this guide to learn more about [partner sharing](/features/partner-sharing).
## Public sharing

View File

@@ -1,6 +1,6 @@
# Tags
Immich supports hierarchical tags, with the ability to read existing tags from the XMP `TagsList` field and IPTC `Keywords` field. Any changes to tags made through Immich are also written back to a [sidecar](/docs/features/xmp-sidecars) file. You can re-run the metadata extraction jobs for all assets to import your existing tags.
Immich supports hierarchical tags, with the ability to read existing tags from the XMP `TagsList` field and IPTC `Keywords` field. Any changes to tags made through Immich are also written back to a [sidecar](/features/xmp-sidecars) file. You can re-run the metadata extraction jobs for all assets to import your existing tags.
## Enable tags feature

View File

@@ -15,9 +15,9 @@ You can access the [user settings](https://my.immich.app/user-settings) by click
---
:::tip Reset Password
The admin can reset a user password through the [User Management](/docs/administration/user-management.mdx) screen.
The admin can reset a user password through the [User Management](/administration/user-management.mdx) screen.
:::
:::tip Reset Admin Password
The admin password can be reset using a [Server Command](/docs/administration/server-commands.md)
The admin password can be reset using a [Server Command](/administration/server-commands.md)
:::

View File

@@ -10,7 +10,7 @@ This guide explains how to optimize facial recognition in systems with large ima
- **Best Suited For:** Large image libraries after importing a significant number of images.
- **Warning:** This method deletes all previously assigned names.
- **Tip:** **Always take a [backup](/docs/administration/backup-and-restore#database) before proceeding!**
- **Tip:** **Always take a [backup](/administration/backup-and-restore#database) before proceeding!**
---

View File

@@ -9,7 +9,7 @@ It is important to remember to update the backup settings after following the gu
In our `.env` file, we will define the paths we want to use. Note that you don't have to define all of these: UPLOAD_LOCATION will be the base folder that files are stored in by default, with the other paths acting as overrides.
```diff title=".env"
# You can find documentation for all the supported environment variables [here](/docs/install/environment-variables)
# You can find documentation for all the supported environment variables [here](/install/environment-variables)
# Custom location where your uploaded, thumbnails, and transcoded video files are stored
- UPLOAD_LOCATION=./library

View File

@@ -7,7 +7,7 @@ Keep in mind that mucking around in the database might set the Moon on fire. Avo
:::tip
Run `docker exec -it immich_postgres psql --dbname=<DB_DATABASE_NAME> --username=<DB_USERNAME>` to connect to the database via the container directly.
(Replace `<DB_DATABASE_NAME>` and `<DB_USERNAME>` with the values from your [`.env` file](/docs/install/environment-variables#database)).
(Replace `<DB_DATABASE_NAME>` and `<DB_USERNAME>` with the values from your [`.env` file](/install/environment-variables#database)).
:::
## Assets
@@ -142,7 +142,7 @@ DELETE FROM "person" WHERE "name" = 'PersonNameHere';
SELECT "key", "value" FROM "system_metadata" WHERE "key" = 'system-config';
```
(Only used when not using the [config file](/docs/install/config-file))
(Only used when not using the [config file](/install/config-file))
### File properties

View File

@@ -1,13 +1,13 @@
# External Library
This guide walks you through adding an [External Library](/docs/features/libraries).
This guide walks you through adding an [External Library](/features/libraries).
This guide assumes you are running Immich in Docker and that the files you wish to access are stored
in a directory on the same machine.
# Mount the directory into the containers.
Edit `docker-compose.yml` to add one or more new mount points in the section `immich-server:` under `volumes:`.
If you want Immich to be able to delete the images in the external library or add metadata ([XMP sidecars](/docs/features/xmp-sidecars)), remove `:ro` from the end of the mount point.
If you want Immich to be able to delete the images in the external library or add metadata ([XMP sidecars](/features/xmp-sidecars)), remove `:ro` from the end of the mount point.
```diff
immich-server:

View File

@@ -46,7 +46,7 @@ You can learn how to set up Tailscale together with Immich with the [tutorial vi
A reverse proxy is a service that sits between web servers and clients. A reverse proxy can either be hosted on the server itself or remotely. Clients can connect to the reverse proxy via https, and the proxy relays data to Immich. This setup makes most sense if you have your own domain and want to access your Immich instance just like any other website, from outside your LAN. You can also use a DDNS provider like DuckDNS or no-ip if you don't have a domain. This configuration allows the Immich Android and iphone apps to connect to your server without a VPN or tailscale app on the client side.
If you're hosting your own reverse proxy, [Nginx](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) is a great option. An example configuration for Nginx is provided [here](/docs/administration/reverse-proxy.md).
If you're hosting your own reverse proxy, [Nginx](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) is a great option. An example configuration for Nginx is provided [here](/administration/reverse-proxy.md).
You'll also need your own certificate to authenticate https connections. If you're making Immich publicly accessible, [Let's Encrypt](https://letsencrypt.org/) can provide a free certificate for your domain and is the recommended option. Alternatively, a [self-signed certificate](https://en.wikipedia.org/wiki/Self-signed_certificate) allows you to encrypt your connection to Immich, but it raises a security warning on the client's browser.

View File

@@ -1,6 +1,6 @@
# Remote Machine Learning
To alleviate [performance issues on low-memory systems](/docs/FAQ.mdx#why-is-immich-slow-on-low-memory-systems-like-the-raspberry-pi) like the Raspberry Pi, you may also host Immich's machine learning container on a more powerful system, such as your laptop or desktop computer. The server container will send requests containing the image preview to the remote machine learning container for processing. The machine learning container does not persist this data or associate it with a particular user.
To alleviate [performance issues on low-memory systems](/FAQ.mdx#why-is-immich-slow-on-low-memory-systems-like-the-raspberry-pi) like the Raspberry Pi, you may also host Immich's machine learning container on a more powerful system, such as your laptop or desktop computer. The server container will send requests containing the image preview to the remote machine learning container for processing. The machine learning container does not persist this data or associate it with a particular user.
:::info
Smart Search and Face Detection will use this feature, but Facial Recognition will not. This is because Facial Recognition uses the _outputs_ of these models that have already been saved to the database. As such, its processing is between the server container and the database.
@@ -14,7 +14,7 @@ Image previews are sent to the remote machine learning container. Use this optio
2. Copy the following `docker-compose.yml` to the remote server
:::info
If using hardware acceleration, the [hwaccel.ml.yml](https://github.com/immich-app/immich/releases/latest/download/hwaccel.ml.yml) file also needs to be added and the `docker-compose.yml` needs to be configured as described in the [hardware acceleration documentation](/docs/features/ml-hardware-acceleration)
If using hardware acceleration, the [hwaccel.ml.yml](https://github.com/immich-app/immich/releases/latest/download/hwaccel.ml.yml) file also needs to be added and the `docker-compose.yml` needs to be configured as described in the [hardware acceleration documentation](/features/ml-hardware-acceleration)
:::
```yaml

View File

@@ -7,7 +7,7 @@ This script assumes you have a second hard drive connected to your server for on
The database is saved to your Immich upload folder in the `database-backup` subdirectory. The database is then backed up and versioned with your assets by Borg. This ensures that the database backup is in sync with your assets in every snapshot.
:::info
This script makes backups of your database along with your photo/video library. This is redundant with the [automatic database backup tool](https://immich.app/docs/administration/backup-and-restore#automatic-database-backups) built into Immich. Using this script to backup your database has two advantages over the built-in backup tool:
This script makes backups of your database along with your photo/video library. This is redundant with the [automatic database backup tool](/administration/backup-and-restore#automatic-database-dumps) built into Immich. Using this script to backup your database has two advantages over the built-in backup tool:
- This script uses storage more efficiently by versioning your backups instead of making multiple copies.
- The database backups are performed at the same time as the library backup, ensuring that the backups of your database and the library are always in sync.

View File

@@ -209,7 +209,7 @@ So you can just grab it from there, paste it into a file and you're pretty much
### Step 2 - Specify the file location
In your `.env` file, set the variable `IMMICH_CONFIG_FILE` to the path of your config.
For more information, refer to the [Environment Variables](/docs/install/environment-variables.md) section.
For more information, refer to the [Environment Variables](/install/environment-variables.md) section.
:::tip
YAML-formatted config files are also supported.

View File

@@ -29,4 +29,4 @@ If you get an error `can't set healthcheck.start_interval as feature require Doc
## Next Steps
Read the [Post Installation](/docs/install/post-install.mdx) steps and [upgrade instructions](/docs/install/upgrading.md).
Read the [Post Installation](/install/post-install.mdx) steps and [upgrade instructions](/install/upgrading.md).

View File

@@ -42,7 +42,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
| `IMMICH_TRUSTED_PROXIES` | List of comma-separated IPs set as trusted proxies | | server | api |
| `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/docs/administration/system-integrity) | | server | api, microservices |
| `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/administration/system-integrity) | | server | api, microservices |
\*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`.
`TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution.
@@ -57,7 +57,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N
| `IMMICH_WORKERS_EXCLUDE` | Do not run these workers. Matches against default workers, or `IMMICH_WORKERS_INCLUDE` if specified. | | server |
:::info
Information on the current workers can be found [here](/docs/administration/jobs-workers).
Information on the current workers can be found [here](/administration/jobs-workers).
:::
## Ports

View File

@@ -45,5 +45,5 @@ alt="Dot Env Example"
11. Click on "**Deploy the stack**".
:::tip
For more information on how to use the application, please refer to the [Post Installation](/docs/install/post-install.mdx) guide.
For more information on how to use the application, please refer to the [Post Installation](/install/post-install.mdx) guide.
:::

View File

@@ -44,6 +44,6 @@ A list of common steps to take after installing Immich include:
## Setting up optional features
- [External Libraries](/docs/features/libraries.md): Adding your existing photo library to Immich
- [Hardware Transcoding](/docs/features/hardware-transcoding.md): Speeding up video transcoding
- [Hardware-Accelerated Machine Learning](/docs/features/ml-hardware-acceleration.md): Speeding up various machine learning tasks in Immich
- [External Libraries](/features/libraries.md): Adding your existing photo library to Immich
- [Hardware Transcoding](/features/hardware-transcoding.md): Speeding up video transcoding
- [Hardware-Accelerated Machine Learning](/features/ml-hardware-acceleration.md): Speeding up various machine learning tasks in Immich

View File

@@ -5,12 +5,12 @@ sidebar_position: 20
# Install script [Experimental]
:::caution
This method is experimental and not currently recommended for production use. For production, please refer to installing with [Docker Compose](/docs/install/docker-compose.mdx).
This method is experimental and not currently recommended for production use. For production, please refer to installing with [Docker Compose](/install/docker-compose.mdx).
:::
## Requirements
Follow the [requirements page](/docs/install/requirements) to get started.
Follow the [requirements page](/install/requirements) to get started.
The install script only supports Linux operating systems and requires Docker to be already installed on the system.
@@ -32,5 +32,5 @@ The web application and mobile app will be available at `http://<machine-ip-addr
The directory which is used to store the library files is `./immich-app` relative to the current directory.
:::tip
For common next steps, see [Post Install Steps](/docs/install/post-install.mdx).
For common next steps, see [Post Install Steps](/install/post-install.mdx).
:::

View File

@@ -29,7 +29,7 @@ Download [`docker-compose.yml`](https://github.com/immich-app/immich/releases/la
## Step 2 - Populate the .env file with custom values
Follow [Step 2 in Docker Compose](/docs/install/docker-compose#step-2---populate-the-env-file-with-custom-values) for instructions on customizing the `.env` file, and then return back to this guide to continue.
Follow [Step 2 in Docker Compose](/install/docker-compose#step-2---populate-the-env-file-with-custom-values) for instructions on customizing the `.env` file, and then return back to this guide to continue.
## Step 3 - Create a new project in Container Manager
@@ -67,4 +67,4 @@ Click "**Edit Rules**" and add the following firewall rules:
## Next Steps
Read the [Post Installation](/docs/install/post-install.mdx) steps and [upgrade instructions](/docs/install/upgrading.md).
Read the [Post Installation](/install/post-install.mdx) steps and [upgrade instructions](/install/upgrading.md).

View File

@@ -60,13 +60,13 @@ For an easy setup:
:::tip
To improve performance, Immich recommends using SSDs for the database. If you have a pool made of SSDs, you can create the `pgData` dataset on that pool.
Thumbnails can also be stored on the SSDs for faster access. This is an advanced option and not required for Immich to run. More information on how you can use multiple datasets to manage Immich storage in a finer-grained manner can be found in the [Advanced: Multiple Datasets for Immich Storage](#advanced-multiple-datasets-for-immich-storage) section below.
Thumbnails can also be stored on the SSDs for faster access. This is an advanced option and not required for Immich to run. More information on how you can use multiple datasets to manage Immich storage in a finer-grained manner can be found in the [Additional Storage: Multiple Datasets for Immich Storage](#additional-storage-advanced-users) section below.
:::
:::warning
If you just created the datasets using the **Apps** preset, you can skip this warning section.
If the **data** dataset uses ACL it must have [ACL mode](https://www.truenas.com/docs/scale/scaletutorials/datasets/permissionsscale/) set to `Passthrough` if you plan on using a [storage template](/docs/administration/storage-template.mdx) and the dataset is configured for network sharing (its ACL type is set to `SMB/NFSv4`). When the template is applied and files need to be moved from **upload** to **library** (internal folder created by Immich within the **data** dataset), Immich performs `chmod` internally and must be allowed to execute the command. [More info.](https://github.com/immich-app/immich/pull/13017)
If the **data** dataset uses ACL it must have [ACL mode](https://www.truenas.com/docs/scale/scaletutorials/datasets/permissionsscale/) set to `Passthrough` if you plan on using a [storage template](/administration/storage-template.mdx) and the dataset is configured for network sharing (its ACL type is set to `SMB/NFSv4`). When the template is applied and files need to be moved from **upload** to **library** (internal folder created by Immich within the **data** dataset), Immich performs `chmod` internally and must be allowed to execute the command. [More info.](https://github.com/immich-app/immich/pull/13017)
To change or verify the ACL mode, go to the **Datasets** screen, select the **library** dataset, click on the **Edit** button next to **Dataset Details**, then click on the **Advanced Options** tab, scroll down to the **ACL Mode** section, and select `Passthrough` from the dropdown menu. Click **Save** to apply the changes. If the option is greyed out, set the **ACL Type** to `SMB/NFSv4` first, then you can change the **ACL Mode** to `Passthrough`.
:::
@@ -129,7 +129,7 @@ The **Timezone** is set to the system default, which usually matches your local
**Enable Machine Learning** is enabled by default. It allows Immich to use machine learning features such as face recognition, image search, and smart duplicate detection. Untick this option if you do not want to use these features.
Select the **Machine Learning Image Type** based on the hardware you have. More details here: [Hardware-Accelerated Machine Learning](/docs/features/ml-hardware-acceleration.md)
Select the **Machine Learning Image Type** based on the hardware you have. More details here: [Hardware-Accelerated Machine Learning](/features/ml-hardware-acceleration.md)
**Database Password** should be set to a custom value using only the characters `A-Za-z0-9`. This password is used to secure the Postgres database.
@@ -156,7 +156,7 @@ className="border rounded-xl"
/>
These are used to add custom configuration options or to enable specific features.
More information on available environment variables can be found in the **[environment variables documentation](/docs/install/environment-variables/)**.
More information on available environment variables can be found in the **[environment variables documentation](/install/environment-variables/)**.
:::info
Some environment variables are not available for the TrueNAS Community Edition app as they can be configured through GUI options in the [Edit Immich screen](#edit-app-settings).
@@ -242,7 +242,7 @@ alt="Add External Libraries with Additional Storage"
className="border rounded-xl"
/>
You may configure [external libraries](/docs/features/libraries) by mounting them using **Additional Storage**.
You may configure [external libraries](/features/libraries) by mounting them using **Additional Storage**.
The dataset that contains your external library files must at least give **read** access to the user running Immich (Default: `apps` (UID 568), `apps` (GID 568)).
If you want to be able to delete files or edit metadata in the external library using Immich, you will need to give the **modify** permission to the user running Immich.
@@ -266,7 +266,7 @@ A general recommendation is to mount any external libraries to a path beginning
This feature should only be used by advanced users.
:::
Immich can use multiple datasets for its storage, allowing you to manage your data more granularly, similar to the old storage configuration. This is useful if you want to separate your data into different datasets for performance or organizational reasons. There is a general guide for this [here](/docs/guides/custom-locations), but read on for the TrueNAS guide.
Immich can use multiple datasets for its storage, allowing you to manage your data more granularly, similar to the old storage configuration. This is useful if you want to separate your data into different datasets for performance or organizational reasons. There is a general guide for this [here](/guides/custom-locations), but read on for the TrueNAS guide.
Each additional dataset has to give the permission **_modify_** to the user who will run Immich (Default: `apps` (UID 568), `apps` (GID 568))
As described in the [Setting up Storage Datasets](#setting-up-storage-datasets) section above, you have to create the datasets with the **Apps** preset to ensure the correct permissions are set, or you can set the permissions manually after creating the datasets.
@@ -309,7 +309,7 @@ className="border rounded-xl"
Both **CPU** and **Memory** are limits, not reservations. This means that Immich can use up to the specified amount of CPU threads and RAM, but it will not reserve that amount of resources at all times. The system will allocate resources as needed, and Immich will use less than the specified amount most of the time.
- Enable **GPU Configuration** options if you have a GPU or CPU with integrated graphics that you will use for [Hardware Transcoding](/docs/features/hardware-transcoding) and/or [Hardware-Accelerated Machine Learning](/docs/features/ml-hardware-acceleration.md).
- Enable **GPU Configuration** options if you have a GPU or CPU with integrated graphics that you will use for [Hardware Transcoding](/features/hardware-transcoding) and/or [Hardware-Accelerated Machine Learning](/features/ml-hardware-acceleration.md).
The process for NVIDIA GPU passthrough requires additional steps.
More details here: [GPU Passthrough Docs for TrueNAS Apps](https://apps.truenas.com/managing-apps/installing-apps/#gpu-passthrough)
@@ -332,7 +332,7 @@ Click **Web Portal** on the **Application Info** widget, or go to the URL `http:
After that, you can start using Immich to upload and manage your photos and videos.
:::tip
For more information on how to use the application once installed, please refer to the [Post Install](/docs/install/post-install.mdx) guide.
For more information on how to use the application once installed, please refer to the [Post Install](/install/post-install.mdx) guide.
:::
## Edit App Settings
@@ -347,7 +347,7 @@ For more information on how to use the application once installed, please refer
## Updating the App
:::danger
Make sure to read the general [upgrade instructions](/docs/install/upgrading.md).
Make sure to read the general [upgrade instructions](/install/upgrading.md).
:::
When updates become available, TrueNAS alerts and provides easy updates.

View File

@@ -125,13 +125,13 @@ alt="Go to Docker Tab and visit the address listed next to immich-web"
</details>
:::tip
For more information on how to use the application once installed, please refer to the [Post Install](/docs/install/post-install.mdx) guide.
For more information on how to use the application once installed, please refer to the [Post Install](/install/post-install.mdx) guide.
:::
## Updating Steps
:::danger
Make sure to read the general [upgrade instructions](/docs/install/upgrading.md).
Make sure to read the general [upgrade instructions](/install/upgrading.md).
:::
Updating is extremely easy however it's important to be aware that containers managed via the Docker Compose Manager plugin do not integrate with Unraid's native dockerman UI, the label "_update ready_" will always be present on containers installed via the Docker Compose Manager.

View File

@@ -40,7 +40,7 @@ If you do not deploy Immich using Docker Compose and see a deprecation warning f
Immich has migrated off of the deprecated pgvecto.rs database extension to its successor, [VectorChord](https://github.com/tensorchord/VectorChord), which comes with performance improvements in almost every aspect. This section will guide you on how to make this change in a Docker Compose setup.
Before making any changes, please [back up your database](/docs/administration/backup-and-restore). While every effort has been made to make this migration as smooth as possible, theres always a chance that something can go wrong.
Before making any changes, please [back up your database](/administration/backup-and-restore). While every effort has been made to make this migration as smooth as possible, theres always a chance that something can go wrong.
After making a backup, please modify your `docker-compose.yml` file with the following information.
@@ -101,7 +101,7 @@ Please dont hesitate to contact us on [GitHub](https://github.com/immich-app/
#### I have a separate PostgreSQL instance shared with multiple services. How can I switch to VectorChord?
Please see the [standalone PostgreSQL documentation](/docs/administration/postgres-standalone#migrating-to-vectorchord) for migration instructions. The migration path will be different depending on whether youre currently using pgvecto.rs or pgvector, as well as whether Immich has superuser DB permissions.
Please see the [standalone PostgreSQL documentation](/administration/postgres-standalone#migrating-to-vectorchord) for migration instructions. The migration path will be different depending on whether youre currently using pgvecto.rs or pgvector, as well as whether Immich has superuser DB permissions.
#### Why are so many lines removed from the `docker-compose.yml` file? Does this mean the health check is removed?

View File

@@ -6,7 +6,7 @@ sidebar_position: 6
Running into an issue or have a question? Try the following:
1. Check the [FAQs](/docs/FAQ.mdx).
1. Check the [FAQs](/FAQ.mdx).
2. Read through the [Release Notes][github-releases].
3. Search through existing [GitHub Issues][github-issues].
4. Open a help ticket on [Discord][discord-link].

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 KiB

View File

@@ -13,7 +13,7 @@ to install and use it.
- A system with at least 4GB of RAM and 2 CPU cores.
- [Docker](https://docs.docker.com/engine/install/)
> For a more detailed list of requirements, see the [requirements page](/docs/install/requirements).
> For a more detailed list of requirements, see the [requirements page](/install/requirements).
---
@@ -61,7 +61,7 @@ import MobileAppBackup from '/docs/partials/_mobile-app-backup.md';
The backup time differs depending on how many photos are on your mobile device. Large uploads may
take quite a while.
To quickly get going, you can selectively upload few photos first, by following this [guide](/docs/features/mobile-app#sync-only-selected-photos).
To quickly get going, you can selectively upload few photos first, by following this [guide](/features/mobile-app#sync-only-selected-photos).
You can select the **Jobs** tab to see Immich processing your photos.
@@ -72,7 +72,7 @@ You can select the **Jobs** tab to see Immich processing your photos.
## Review the database backup and restore process
Immich has built-in database backups. You can refer to the
[database backup](/docs/administration/backup-and-restore) for more information.
[database backup](/administration/backup-and-restore) for more information.
:::danger
The database only contains metadata and user information. You must setup manual backups of the images and videos stored in `UPLOAD_LOCATION`.
@@ -86,8 +86,8 @@ You may decide you'd like to install the server a different way; the Install cat
You may decide you'd like to add the _rest_ of your photos from Google Photos, even those not on your mobile device, via Google Takeout. You can use [immich-go](https://github.com/simulot/immich-go) for this.
You may want to [upload photos from your own archive](/docs/features/command-line-interface).
You may want to [upload photos from your own archive](/features/command-line-interface).
You may want to incorporate a pre-existing archive of photos from an [External Library](/docs/features/libraries); there's a [guide](/docs/guides/external-library) for that.
You may want to incorporate a pre-existing archive of photos from an [External Library](/features/libraries); there's a [guide](/guides/external-library) for that.
You may want your mobile device to [back photos up to your server automatically](/docs/features/automatic-backup).
You may want your mobile device to [back photos up to your server automatically](/features/automatic-backup).

View File

@@ -10,11 +10,11 @@ By far the easiest way to help make Immich better it to use it and report issues
## Translations
Support the project by localizing on [Weblate](https://hosted.weblate.org/projects/immich/immich/). For more information, see the [Translations](/docs/developer/translations) section.
Support the project by localizing on [Weblate](https://hosted.weblate.org/projects/immich/immich/). For more information, see the [Translations](/developer/translations) section.
## Development
If you are a programmer or developer, take a look at Immich's [technology stack](/docs/developer/architecture.mdx) and consider fixing bugs or building new features. The team and I are always looking for new contributors. For information about how to contribute as a developer, see the [Developer](/docs/developer/architecture.mdx) section.
If you are a programmer or developer, take a look at Immich's [technology stack](/developer/architecture.mdx) and consider fixing bugs or building new features. The team and I are always looking for new contributors. For information about how to contribute as a developer, see the [Developer](/developer/architecture.mdx) section.
## Purchase Immich

View File

@@ -1,27 +0,0 @@
---
sidebar_position: 1
---
# Welcome to Immich
<img
src={require('./img/social-preview-light.webp').default}
alt="Immich - Self-hosted photos and videos backup tool"
data-theme="light"
/>
## Welcome!
Hello, I am glad you are here.
My name is Alex. I am an Electrical Engineer by schooling, then turned into a Software Engineer by trade and the pure love of problem solving.
We were lying in bed with our newborn, and my wife said, "We are starting to accumulate a lot of photos and videos of our baby, and I don't want to pay for **_App-Which-Must-Not-Be-Named_** anymore. You always want to build something for me, so why don't you build me an app which can do that?"
That was how the idea started to grow in my head. After that, I began to find existing solutions in the self-hosting space with similar backup functionality and the performance level of the **_App-Which-Must-Not-Be-Named_**. I found that the current solutions mainly focus on the gallery-type application. However, I want a simple-to-use backup tool with a native mobile app that can view photos and videos efficiently. So I set sail on this journey as a hungry engineer on the hunt.
Another motivation that pushed me to deliver my execution of the **_App-Which-Must-Not-Be-Named_** alternative or replacement is for contributing back to the open source community that I have greatly benefited from over the years.
I'm proud to share this creation with you, which values privacy, memories, and the joy of looking back at those moments in an easy-to-use and friendly interface.
If you like the application or it helps you in some way, please consider [supporting](./support-the-project.md) the project. It will help me to continue to develop and maintain the application.

View File

@@ -1,5 +1,5 @@
Now that you have imported some pictures, you should setup server backups to preserve your memories.
You can do so by following our [backup guide](/docs/administration/backup-and-restore.md).
You can do so by following our [backup guide](/administration/backup-and-restore.md).
:::danger
Immich is still under heavy development _and_ handles very important data.

View File

@@ -1,7 +1,7 @@
Immich allows the admin user to set the uploaded filename pattern at the directory and filename level as well as the [storage label for a user](/docs/administration/user-management/#set-storage-label-for-user).
Immich allows the admin user to set the uploaded filename pattern at the directory and filename level as well as the [storage label for a user](/administration/user-management/#set-storage-label-for-user).
:::tip
You can read more about the differences between storage template engine on and off [here](/docs/administration/backup-and-restore#asset-types-and-storage-locations)
You can read more about the differences between storage template engine on and off [here](/administration/backup-and-restore#asset-types-and-storage-locations)
:::
The admin user can set the template by using the template builder in the `Administration -> Settings -> Storage Template`. Immich provides a set of variables that you can use in constructing the template, along with additional custom text. If the template produces [multiple files with the same filename, they won't be overwritten](https://github.com/immich-app/immich/discussions/3324) as a sequence number is appended to the filename.

View File

@@ -48,6 +48,7 @@ const config = {
docs: {
showLastUpdateAuthor: true,
showLastUpdateTime: true,
routeBasePath: '/',
sidebarPath: require.resolve('./sidebars.js'),
// Please change this to your repo.
@@ -87,7 +88,7 @@ const config = {
position: 'right',
},
{
to: '/docs/overview/welcome',
to: '/overview/quick-start',
position: 'right',
label: 'Docs',
},
@@ -131,16 +132,16 @@ const config = {
title: 'Overview',
items: [
{
label: 'Welcome',
to: '/docs/overview/welcome',
label: 'Quick start',
to: '/overview/quick-start',
},
{
label: 'Installation',
to: '/docs/install/requirements',
to: '/install/requirements',
},
{
label: 'Contributing',
to: '/docs/overview/support-the-project',
to: '/overview/support-the-project',
},
{
label: 'Privacy Policy',

View File

@@ -11,7 +11,7 @@ export default function VersionSwitcher(): JSX.Element {
useEffect(() => {
async function getVersions() {
try {
let baseUrl = 'https://immich.app';
let baseUrl = 'https://docs.immich.app';
if (window.location.origin === 'http://localhost:3005') {
baseUrl = window.location.origin;
}
@@ -21,12 +21,13 @@ export default function VersionSwitcher(): JSX.Element {
const archiveVersions = await response.json();
const allVersions = [
{ label: 'Next', url: 'https://main.preview.immich.app' },
{ label: 'Latest', url: 'https://immich.app' },
{ label: 'Next', url: 'https://docs.main.preview.immich.app' },
{ label: 'Latest', url: 'https://docs.immich.app' },
...archiveVersions,
].map(({ label, url }) => ({
].map(({ label, url, rootPath }) => ({
label,
url: new URL(url),
rootPath,
}));
setVersions(allVersions);
@@ -50,12 +51,18 @@ export default function VersionSwitcher(): JSX.Element {
className="version-switcher-34ab39"
label={activeLabel}
mobile={windowSize === 'mobile'}
items={versions.map(({ label, url }) => ({
label,
to: new URL(location.pathname + location.search + location.hash, url).href,
target: '_self',
className: label === activeLabel ? 'dropdown__link--active menu__link--active' : '', // workaround because React Router `<NavLink>` only supports using URL path for checking if active: https://v5.reactrouter.com/web/api/NavLink/isactive-func
}))}
items={versions.map(({ label, url, rootPath }) => {
let path = location.pathname + location.search + location.hash;
if (rootPath && !path.startsWith(rootPath)) {
path = rootPath + path;
}
return {
label,
to: new URL(path, url).href,
target: '_self',
className: label === activeLabel ? 'dropdown__link--active menu__link--active' : '', // workaround because React Router `<NavLink>` only supports using URL path for checking if active: https://v5.reactrouter.com/web/api/NavLink/isactive-func
};
})}
/>
)
);

View File

@@ -1,5 +1,5 @@
import { Redirect } from '@docusaurus/router';
export default function Home(): JSX.Element {
return <Redirect to="/docs/overview/welcome" />;
return <Redirect to="/overview/quick-start" />;
}

View File

@@ -1,34 +1,34 @@
/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
/docs/installation/one-step-installation /docs/install/script 307
/docs/installation/portainer-installation /docs/install/portainer 307
/docs/installation/recommended-installation /docs/install/docker-compose 307
/docs/installation/unraid /docs/install/unraid 307
/docs/installation/requirements /docs/install/requirements 307
/docs/overview/logo-meaning /docs/overview/logo 307
/docs/overview/technology-stack /docs/developer/architecture 307
/docs/usage/automatic-backup /docs/features/automatic-backup 307
/docs/usage/bulk-upload /docs/features/command-line-interface 307
/docs/features/bulk-upload /docs/features/command-line-interface 307
/docs/usage/oauth /docs/administration/oauth 307
/docs/usage/post-installation /docs/install/post-install 307
/docs/usage/update /docs/install/docker-compose#step-4---upgrading 307
/docs/usage/server-commands /docs/administration/server-commands 307
/docs/features/jobs /docs/administration/jobs 307
/docs/features/oauth /docs/administration/oauth 307
/docs/features/password-login /docs/administration/password-login 307
/docs/features/server-commands /docs/administration/server-commands 307
/docs/features/storage-template /docs/administration/storage-template 307
/docs/features/user-management /docs/administration/user-management 307
/docs/developer/contributing /docs/developer/pr-checklist 307
/docs/guides/machine-learning /docs/guides/remote-machine-learning 307
/docs/administration/password-login /docs/administration/system-settings 307
/docs/features/search /docs/features/searching 307
/docs/features/smart-search /docs/features/searching 307
/docs/guides/api-album-sync /docs/community-projects 307
/docs/guides/remove-offline-files /docs/community-projects 307
/milestones /roadmap 307
/docs/overview/introduction /docs/overview/welcome 307
/ /overview/quick-start 307
/mobile-app-beta-program /features/mobile-app 307
/contribution-guidelines /overview/support-the-project#contributing 307
/install /install/docker-compose 307
/installation/one-step-installation /install/script 307
/installation/portainer-installation /install/portainer 307
/installation/recommended-installation /install/docker-compose 307
/installation/unraid /install/unraid 307
/installation/requirements /install/requirements 307
/overview/logo-meaning /overview/logo 307
/overview/technology-stack /developer/architecture 307
/usage/automatic-backup /features/automatic-backup 307
/usage/bulk-upload /features/command-line-interface 307
/features/bulk-upload /features/command-line-interface 307
/usage/oauth /administration/oauth 307
/usage/post-installation /install/post-install 307
/usage/update /install/docker-compose#step-4---upgrading 307
/usage/server-commands /administration/server-commands 307
/features/jobs /administration/jobs 307
/features/oauth /administration/oauth 307
/features/password-login /administration/password-login 307
/features/server-commands /administration/server-commands 307
/features/storage-template /administration/storage-template 307
/features/user-management /administration/user-management 307
/developer/contributing /developer/pr-checklist 307
/guides/machine-learning /guides/remote-machine-learning 307
/administration/password-login /administration/system-settings 307
/features/search /features/searching 307
/features/smart-search /features/searching 307
/guides/api-album-sync /community-projects 307
/guides/remove-offline-files /community-projects 307
/overview/introduction /overview/quick-start 307
/overview/welcome /overview/quick-start 307
/docs/* /:splat 307

View File

@@ -1,234 +1,221 @@
[
{
"label": "v1.143.1",
"url": "https://v1.143.1.archive.immich.app"
},
{
"label": "v1.143.0",
"url": "https://v1.143.0.archive.immich.app"
"url": "https://docs.v1.143.1.archive.immich.app"
},
{
"label": "v1.142.1",
"url": "https://v1.142.1.archive.immich.app"
},
{
"label": "v1.142.0",
"url": "https://v1.142.0.archive.immich.app"
"url": "https://v1.142.1.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.141.1",
"url": "https://v1.141.1.archive.immich.app"
},
{
"label": "v1.141.0",
"url": "https://v1.141.0.archive.immich.app"
"url": "https://v1.141.1.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.140.1",
"url": "https://v1.140.1.archive.immich.app"
},
{
"label": "v1.140.0",
"url": "https://v1.140.0.archive.immich.app"
"url": "https://v1.140.1.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.139.4",
"url": "https://v1.139.4.archive.immich.app"
},
{
"label": "v1.139.3",
"url": "https://v1.139.3.archive.immich.app"
},
{
"label": "v1.139.2",
"url": "https://v1.139.2.archive.immich.app"
"url": "https://v1.139.4.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.138.1",
"url": "https://v1.138.1.archive.immich.app"
},
{
"label": "v1.138.0",
"url": "https://v1.138.0.archive.immich.app"
"url": "https://v1.138.1.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.137.3",
"url": "https://v1.137.3.archive.immich.app"
},
{
"label": "v1.137.2",
"url": "https://v1.137.2.archive.immich.app"
},
{
"label": "v1.137.1",
"url": "https://v1.137.1.archive.immich.app"
},
{
"label": "v1.137.0",
"url": "https://v1.137.0.archive.immich.app"
"url": "https://v1.137.3.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.136.0",
"url": "https://v1.136.0.archive.immich.app"
"url": "https://v1.136.0.archive.immich.app",
"rootPath": "/docs"
},
{
"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"
"url": "https://v1.135.3.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.134.0",
"url": "https://v1.134.0.archive.immich.app"
"url": "https://v1.134.0.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.133.1",
"url": "https://v1.133.1.archive.immich.app"
},
{
"label": "v1.133.0",
"url": "https://v1.133.0.archive.immich.app"
"url": "https://v1.133.1.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.132.3",
"url": "https://v1.132.3.archive.immich.app"
"url": "https://v1.132.3.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.131.3",
"url": "https://v1.131.3.archive.immich.app"
"url": "https://v1.131.3.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.130.3",
"url": "https://v1.130.3.archive.immich.app"
"url": "https://v1.130.3.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.129.0",
"url": "https://v1.129.0.archive.immich.app"
"url": "https://v1.129.0.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.128.0",
"url": "https://v1.128.0.archive.immich.app"
"url": "https://v1.128.0.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.127.0",
"url": "https://v1.127.0.archive.immich.app"
"url": "https://v1.127.0.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.126.1",
"url": "https://v1.126.1.archive.immich.app"
"url": "https://v1.126.1.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.125.7",
"url": "https://v1.125.7.archive.immich.app"
"url": "https://v1.125.7.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.124.2",
"url": "https://v1.124.2.archive.immich.app"
"url": "https://v1.124.2.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.123.0",
"url": "https://v1.123.0.archive.immich.app"
"url": "https://v1.123.0.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.122.3",
"url": "https://v1.122.3.archive.immich.app"
"url": "https://v1.122.3.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.121.0",
"url": "https://v1.121.0.archive.immich.app"
"url": "https://v1.121.0.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.120.2",
"url": "https://v1.120.2.archive.immich.app"
"url": "https://v1.120.2.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.119.1",
"url": "https://v1.119.1.archive.immich.app"
"url": "https://v1.119.1.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.118.2",
"url": "https://v1.118.2.archive.immich.app"
"url": "https://v1.118.2.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.117.0",
"url": "https://v1.117.0.archive.immich.app"
"url": "https://v1.117.0.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.116.2",
"url": "https://v1.116.2.archive.immich.app"
"url": "https://v1.116.2.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.115.0",
"url": "https://v1.115.0.archive.immich.app"
"url": "https://v1.115.0.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.114.0",
"url": "https://v1.114.0.archive.immich.app"
"url": "https://v1.114.0.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.113.1",
"url": "https://v1.113.1.archive.immich.app"
"url": "https://v1.113.1.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.112.1",
"url": "https://v1.112.1.archive.immich.app"
"url": "https://v1.112.1.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.111.0",
"url": "https://v1.111.0.archive.immich.app"
"url": "https://v1.111.0.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.110.0",
"url": "https://v1.110.0.archive.immich.app"
"url": "https://v1.110.0.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.109.2",
"url": "https://v1.109.2.archive.immich.app"
"url": "https://v1.109.2.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.108.0",
"url": "https://v1.108.0.archive.immich.app"
"url": "https://v1.108.0.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.107.2",
"url": "https://v1.107.2.archive.immich.app"
"url": "https://v1.107.2.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.106.4",
"url": "https://v1.106.4.archive.immich.app"
"url": "https://v1.106.4.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.105.1",
"url": "https://v1.105.1.archive.immich.app"
"url": "https://v1.105.1.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.104.0",
"url": "https://v1.104.0.archive.immich.app"
"url": "https://v1.104.0.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.103.1",
"url": "https://v1.103.1.archive.immich.app"
"url": "https://v1.103.1.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.102.3",
"url": "https://v1.102.3.archive.immich.app"
"url": "https://v1.102.3.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.101.0",
"url": "https://v1.101.0.archive.immich.app"
"url": "https://v1.101.0.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.100.0",
"url": "https://v1.100.0.archive.immich.app"
"url": "https://v1.100.0.archive.immich.app",
"rootPath": "/docs"
}
]

View File

@@ -28,6 +28,7 @@
"add_to_album": "Add to album",
"add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
"add_to_album_bottom_sheet_some_local_assets": "Some local assets could not be added to album",
"add_to_album_toggle": "Toggle selection for {album}",
"add_to_albums": "Add to albums",
"add_to_albums_count": "Add to albums ({count})",

View File

@@ -1,6 +1,6 @@
[project]
name = "immich-ml"
version = "1.129.0"
version = "1.143.1"
description = ""
authors = [{ name = "Hau Tran", email = "alex.tran1502@gmail.com" }]
requires-python = ">=3.10,<4.0"

View File

@@ -10,7 +10,7 @@ if (!nextVersion) {
const filename = './docs/static/archived-versions.json';
const oldVersions = JSON.parse(readFileSync(filename));
const newVersions = [
{ label: `v${nextVersion}`, url: `https://v${nextVersion}.archive.immich.app` },
{ label: `v${nextVersion}`, url: `https://docs.v${nextVersion}.archive.immich.app` },
...oldVersions,
];

View File

@@ -80,7 +80,7 @@ if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then
jq --arg version "$NEXT_SERVER" '.version = $version' e2e/package.json > e2e/package.json.tmp && mv e2e/package.json.tmp e2e/package.json
pnpm install --frozen-lockfile --prefix e2e
uvx --from=toml-cli toml set --toml-path=pyproject.toml project.version "$SERVER_PUMP"
uvx --from=toml-cli toml set --toml-path=machine-learning/pyproject.toml project.version "$SERVER_PUMP"
fi
if [ "$CURRENT_MOBILE" != "$NEXT_MOBILE" ]; then

View File

@@ -705,7 +705,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 224;
CURRENT_PROJECT_VERSION = 226;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
@@ -849,7 +849,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 224;
CURRENT_PROJECT_VERSION = 226;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
@@ -879,7 +879,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 224;
CURRENT_PROJECT_VERSION = 226;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
@@ -913,7 +913,7 @@
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 224;
CURRENT_PROJECT_VERSION = 226;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -956,7 +956,7 @@
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 224;
CURRENT_PROJECT_VERSION = 226;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -996,7 +996,7 @@
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 224;
CURRENT_PROJECT_VERSION = 226;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -1035,7 +1035,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 224;
CURRENT_PROJECT_VERSION = 226;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1079,7 +1079,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 224;
CURRENT_PROJECT_VERSION = 226;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1120,7 +1120,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 224;
CURRENT_PROJECT_VERSION = 226;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;

View File

@@ -80,7 +80,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.142.1</string>
<string>1.143.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@@ -107,7 +107,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>224</string>
<string>226</string>
<key>FLTEnableImpeller</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key>

View File

@@ -116,7 +116,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
if (Platform.isAndroid) {
await _backgroundHostApi.showNotification(
IntlKeys.uploading_media.t(),
IntlKeys.backup_background_service_in_progress_notification.t(),
IntlKeys.backup_background_service_default_notification.t(),
);
}

View File

@@ -21,7 +21,7 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
}
extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData {
LocalAsset toDto() => LocalAsset(
LocalAsset toDto({String? remoteId}) => LocalAsset(
id: id,
name: name,
checksum: checksum,
@@ -32,7 +32,7 @@ extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData {
isFavorite: isFavorite,
height: height,
width: width,
remoteId: null,
remoteId: remoteId,
orientation: orientation,
);
}

View File

@@ -49,7 +49,7 @@ class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin
}
extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
RemoteAsset toDto() => RemoteAsset(
RemoteAsset toDto({String? localId}) => RemoteAsset(
id: id,
name: name,
ownerId: ownerId,
@@ -64,7 +64,7 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
thumbHash: thumbHash,
visibility: visibility,
livePhotoVideoId: livePhotoVideoId,
localId: null,
localId: localId,
stackId: stackId,
);
}

View File

@@ -148,10 +148,9 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
..orderBy([OrderingTerm.desc(_db.localAssetEntity.createdAt)])
..limit(count, offset: offset);
return query.map((row) {
final asset = row.readTable(_db.localAssetEntity).toDto();
return asset.copyWith(remoteId: row.read(_db.remoteAssetEntity.id));
}).get();
return query
.map((row) => row.readTable(_db.localAssetEntity).toDto(remoteId: row.read(_db.remoteAssetEntity.id)))
.get();
}
TimelineQuery remoteAlbum(String albumId, GroupAssetsBy groupBy) => (
@@ -165,17 +164,15 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
.count(where: (row) => row.albumId.equals(albumId))
.map(_generateBuckets)
.watch()
.map((results) => results.isNotEmpty ? results.first : <Bucket>[])
.handleError((error) {
return [];
});
.map((results) => results.isNotEmpty ? results.first : const <Bucket>[])
.handleError((error) => const <Bucket>[]);
}
return (_db.remoteAlbumEntity.select()..where((row) => row.id.equals(albumId)))
.watch()
.switchMap((albums) {
if (albums.isEmpty) {
return Stream.value(<Bucket>[]);
return Stream.value(const <Bucket>[]);
}
final album = albums.first;
@@ -207,10 +204,8 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
return TimeBucket(date: timeline, assetCount: assetCount);
}).watch();
})
.handleError((error) {
// If there's an error (e.g., album was deleted), return empty buckets
return <Bucket>[];
});
// If there's an error (e.g., album was deleted), return empty buckets
.handleError((error) => const <Bucket>[]);
}
Future<List<BaseAsset>> _getRemoteAlbumBucketAssets(String albumId, {required int offset, required int count}) async {
@@ -218,17 +213,22 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
// If album doesn't exist (was deleted), return empty list
if (albumData == null) {
return <BaseAsset>[];
return const <BaseAsset>[];
}
final isAscending = albumData.order == AlbumAssetOrder.asc;
final query = _db.remoteAssetEntity.select().join([
final query = _db.remoteAssetEntity.select().addColumns([_db.localAssetEntity.id]).join([
innerJoin(
_db.remoteAlbumAssetEntity,
_db.remoteAlbumAssetEntity.assetId.equalsExp(_db.remoteAssetEntity.id),
useColumns: false,
),
leftOuterJoin(
_db.localAssetEntity,
_db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum),
useColumns: false,
),
])..where(_db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAlbumAssetEntity.albumId.equals(albumId));
if (isAscending) {
@@ -239,12 +239,14 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
query.limit(count, offset: offset);
return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get();
return query
.map((row) => row.readTable(_db.remoteAssetEntity).toDto(localId: row.read(_db.localAssetEntity.id)))
.get();
}
TimelineQuery fromAssets(List<BaseAsset> assets) => (
bucketSource: () => Stream.value(_generateBuckets(assets.length)),
assetSource: (offset, count) => Future.value(assets.skip(offset).take(count).toList()),
assetSource: (offset, count) => Future.value(assets.skip(offset).take(count).toList(growable: false)),
);
TimelineQuery remote(String ownerId, GroupAssetsBy groupBy) => _remoteQueryBuilder(
@@ -486,6 +488,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get();
}
@pragma('vm:prefer-inline')
TimelineQuery _remoteQueryBuilder({
required Expression<bool> Function($RemoteAssetEntityTable row) filter,
GroupAssetsBy groupBy = GroupAssetsBy.day,
@@ -523,6 +526,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
}).watch();
}
@pragma('vm:prefer-inline')
Future<List<BaseAsset>> _getRemoteAssets({
required Expression<bool> Function($RemoteAssetEntityTable row) filter,
required int offset,
@@ -543,11 +547,9 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)])
..limit(count, offset: offset);
return query.map((row) {
final asset = row.readTable(_db.remoteAssetEntity).toDto();
final localId = row.read(_db.localAssetEntity.id);
return asset.copyWith(localId: localId);
}).get();
return query
.map((row) => row.readTable(_db.remoteAssetEntity).toDto(localId: row.read(_db.localAssetEntity.id)))
.get();
} else {
final query = _db.remoteAssetEntity.select()
..where(filter)
@@ -560,12 +562,12 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
}
List<Bucket> _generateBuckets(int count) {
final buckets = List.generate(
(count / kTimelineNoneSegmentSize).floor(),
(_) => const Bucket(assetCount: kTimelineNoneSegmentSize),
final buckets = List.filled(
(count / kTimelineNoneSegmentSize).ceil(),
const Bucket(assetCount: kTimelineNoneSegmentSize),
);
if (count % kTimelineNoneSegmentSize != 0) {
buckets.add(Bucket(assetCount: count % kTimelineNoneSegmentSize));
buckets[buckets.length - 1] = Bucket(assetCount: count % kTimelineNoneSegmentSize);
}
return buckets;
}
@@ -590,10 +592,6 @@ extension on String {
GroupAssetsBy.month => "y-M",
GroupAssetsBy.none => throw ArgumentError("GroupAssetsBy.none is not supported for date formatting"),
};
try {
return DateFormat(format, 'en').parse(this);
} catch (e) {
throw FormatException("Invalid date format: $this", e);
}
return DateFormat(format, 'en').parse(this);
}
}

View File

@@ -25,9 +25,10 @@ class DriftMapPage extends StatelessWidget {
onPressed: () => context.pop(),
icon: const Icon(Icons.arrow_back_ios_new_rounded),
style: IconButton.styleFrom(
shape: const CircleBorder(side: BorderSide(width: 1, color: Colors.black26)),
padding: const EdgeInsets.all(8),
backgroundColor: Colors.indigo.withValues(alpha: 0.7),
backgroundColor: Colors.indigo,
shadowColor: Colors.black26,
elevation: 4,
),
),
),

View File

@@ -36,7 +36,7 @@ class UnStackActionButton extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return BaseActionButton(
iconData: Icons.filter_none_rounded,
iconData: Icons.layers_clear_outlined,
label: "unstack".t(context: context),
onPressed: () => _onTap(context, ref),
);

View File

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

View File

@@ -57,7 +57,7 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
final actions = <Widget>[
if (asset.hasRemote) const DownloadActionButton(source: ActionSource.viewer, menuItem: true),
if (asset.isRemoteOnly) const DownloadActionButton(source: ActionSource.viewer, menuItem: true),
if (isCasting || (asset.hasRemote)) const CastActionButton(menuItem: true),
if (album != null && album.isActivityEnabled && album.isShared)
IconButton(

View File

@@ -13,6 +13,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_act
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
@@ -44,6 +45,7 @@ class ArchiveBottomSheet extends ConsumerWidget {
const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(source: ActionSource.timeline),
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
],
if (multiselect.hasLocal) ...[
const DeleteLocalActionButton(source: ActionSource.timeline),

View File

@@ -13,6 +13,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_act
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
@@ -44,6 +45,7 @@ class FavoriteBottomSheet extends ConsumerWidget {
const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(source: ActionSource.timeline),
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
],
if (multiselect.hasLocal) ...[
const DeleteLocalActionButton(source: ActionSource.timeline),

View File

@@ -5,6 +5,7 @@ import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
@@ -19,6 +20,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_b
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
@@ -62,11 +64,19 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
return;
}
final remoteAssets = selectedAssets.whereType<RemoteAsset>();
final addedCount = await ref
.read(remoteAlbumProvider.notifier)
.addAssets(album.id, selectedAssets.map((e) => (e as RemoteAsset).id).toList());
.addAssets(album.id, remoteAssets.map((e) => e.id).toList());
if (addedCount != selectedAssets.length) {
if (selectedAssets.length != remoteAssets.length) {
ImmichToast.show(
context: context,
msg: 'add_to_album_bottom_sheet_some_local_assets'.t(context: context),
);
}
if (addedCount != remoteAssets.length) {
ImmichToast.show(
context: context,
msg: 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {"album": album.name}),
@@ -108,15 +118,18 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(source: ActionSource.timeline),
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
const DeleteActionButton(source: ActionSource.timeline),
],
if (multiselect.hasLocal || multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
if (multiselect.hasLocal) const UploadActionButton(source: ActionSource.timeline),
],
slivers: [
const AddToAlbumHeader(),
AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand),
],
slivers: multiselect.hasRemote
? [
const AddToAlbumHeader(),
AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand),
]
: [],
);
}
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
import 'package:immich_mobile/presentation/widgets/map/map.state.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
@@ -10,13 +11,14 @@ class MapBottomSheet extends StatelessWidget {
@override
Widget build(BuildContext context) {
return const BaseBottomSheet(
return BaseBottomSheet(
initialChildSize: 0.25,
maxChildSize: 0.9,
shouldCloseOnMinExtent: false,
resizeOnScroll: false,
actions: [],
slivers: [SliverFillRemaining(hasScrollBody: false, child: _ScopedMapTimeline())],
backgroundColor: context.themeData.colorScheme.surface,
slivers: [const SliverFillRemaining(hasScrollBody: false, child: _ScopedMapTimeline())],
);
}
}

View File

@@ -17,6 +17,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_b
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
@@ -102,6 +103,7 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(source: ActionSource.timeline),
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
],
if (multiselect.hasLocal) ...[
const DeleteLocalActionButton(source: ActionSource.timeline),

View File

@@ -123,28 +123,14 @@ ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080
return provider;
}
ImageProvider getThumbnailImageProvider({BaseAsset? asset, String? remoteId, Size size = kThumbnailResolution}) {
assert(asset != null || remoteId != null, 'Either asset or remoteId must be provided');
if (remoteId != null) {
return RemoteThumbProvider(assetId: remoteId);
}
if (_shouldUseLocalAsset(asset!)) {
ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnailResolution}) {
if (_shouldUseLocalAsset(asset)) {
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!;
return LocalThumbProvider(id: id, size: size, assetType: asset.type);
}
final String assetId;
if (asset is LocalAsset && asset.hasRemote) {
assetId = asset.remoteId!;
} else if (asset is RemoteAsset) {
assetId = asset.id;
} else {
throw ArgumentError("Unsupported asset type: ${asset.runtimeType}");
}
return RemoteThumbProvider(assetId: assetId);
final assetId = asset is RemoteAsset ? asset.id : (asset as LocalAsset).remoteId;
return assetId != null ? RemoteThumbProvider(assetId: assetId) : null;
}
bool _shouldUseLocalAsset(BaseAsset asset) =>

View File

@@ -5,7 +5,6 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/thumb_hash_provider.dart';
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
@@ -39,14 +38,7 @@ class Thumbnail extends StatefulWidget {
),
_ => null,
},
imageProvider = switch (asset) {
RemoteAsset() =>
asset.localId == null
? RemoteThumbProvider(assetId: asset.id)
: LocalThumbProvider(id: asset.localId!, size: size, assetType: asset.type),
LocalAsset() => LocalThumbProvider(id: asset.id, size: size, assetType: asset.type),
_ => null,
};
imageProvider = asset == null ? null : getThumbnailImageProvider(asset, size: size);
@override
State<Thumbnail> createState() => _ThumbnailState();

View File

@@ -54,8 +54,6 @@ class ThumbnailTile extends ConsumerWidget {
)
: const BoxDecoration();
final hasStack = asset is RemoteAsset && asset.stackId != null;
final bool storageIndicator =
showStorageIndicator ?? ref.watch(settingsProvider.select((s) => s.get(Setting.showStorageIndicator)));
@@ -77,21 +75,10 @@ class ThumbnailTile extends ConsumerWidget {
child: Thumbnail.fromAsset(asset: asset, size: size),
),
),
if (hasStack)
if (asset != null)
Align(
alignment: Alignment.topRight,
child: Padding(
padding: EdgeInsets.only(right: 10.0, top: asset.isVideo ? 24.0 : 6.0),
child: const _TileOverlayIcon(Icons.burst_mode_rounded),
),
),
if (asset != null && asset.isVideo)
Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.only(right: 10.0, top: 6.0),
child: _VideoIndicator(asset.duration),
),
child: _AssetTypeIcons(asset: asset),
),
if (storageIndicator && asset != null)
switch (asset.storage) {
@@ -214,3 +201,34 @@ class _TileOverlayIcon extends StatelessWidget {
);
}
}
class _AssetTypeIcons extends StatelessWidget {
final BaseAsset asset;
const _AssetTypeIcons({required this.asset});
@override
Widget build(BuildContext context) {
final hasStack = asset is RemoteAsset && (asset as RemoteAsset).stackId != null;
final isLivePhoto = asset is RemoteAsset && asset.livePhotoVideoId != null;
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (asset.isVideo)
Padding(padding: const EdgeInsets.only(right: 10.0, top: 6.0), child: _VideoIndicator(asset.duration)),
if (hasStack)
const Padding(
padding: EdgeInsets.only(right: 10.0, top: 6.0),
child: _TileOverlayIcon(Icons.burst_mode_rounded),
),
if (isLivePhoto)
const Padding(
padding: EdgeInsets.only(right: 10.0, top: 6.0),
child: _TileOverlayIcon(Icons.motion_photos_on_rounded),
),
],
);
}
}

View File

@@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
@@ -9,8 +10,8 @@ import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart';
import 'package:immich_mobile/presentation/widgets/map/map_utils.dart';
import 'package:immich_mobile/presentation/widgets/map/map.state.dart';
import 'package:immich_mobile/presentation/widgets/map/map_utils.dart';
import 'package:immich_mobile/utils/async_mutex.dart';
import 'package:immich_mobile/utils/debounce.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
@@ -187,6 +188,8 @@ class _Map extends StatelessWidget {
styleString: style,
onMapCreated: onMapCreated,
onStyleLoadedCallback: onMapReady,
attributionButtonPosition: AttributionButtonPosition.topRight,
attributionButtonMargins: Platform.isIOS ? const Point(40, 12) : const Point(40, 72),
),
),
);

View File

@@ -212,11 +212,14 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
if (fallbackSegment != null) {
// Scroll to the segment with a small offset to show the header
final targetOffset = fallbackSegment.startOffset - 50;
_scrollController.animateTo(
targetOffset.clamp(0.0, _scrollController.position.maxScrollExtent),
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
ref.read(timelineStateProvider.notifier).setScrubbing(true);
_scrollController
.animateTo(
targetOffset.clamp(0.0, _scrollController.position.maxScrollExtent),
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
)
.whenComplete(() => ref.read(timelineStateProvider.notifier).setScrubbing(false));
}
});
}

View File

@@ -1,6 +1,5 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:async';
import 'dart:convert';
import 'package:background_downloader/background_downloader.dart';
import 'package:collection/collection.dart';
@@ -12,8 +11,8 @@ import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/services/upload.service.dart';
import 'package:logging/logging.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:logging/logging.dart';
class EnqueueStatus {
final int enqueueCount;
@@ -90,33 +89,6 @@ class DriftUploadStatus {
networkSpeedAsString.hashCode ^
isFailed.hashCode;
}
Map<String, dynamic> toMap() {
return <String, dynamic>{
'taskId': taskId,
'filename': filename,
'progress': progress,
'fileSize': fileSize,
'networkSpeedAsString': networkSpeedAsString,
'isFailed': isFailed,
};
}
factory DriftUploadStatus.fromMap(Map<String, dynamic> map) {
return DriftUploadStatus(
taskId: map['taskId'] as String,
filename: map['filename'] as String,
progress: map['progress'] as double,
fileSize: map['fileSize'] as int,
networkSpeedAsString: map['networkSpeedAsString'] as String,
isFailed: map['isFailed'] != null ? map['isFailed'] as bool : null,
);
}
String toJson() => json.encode(toMap());
factory DriftUploadStatus.fromJson(String source) =>
DriftUploadStatus.fromMap(json.decode(source) as Map<String, dynamic>);
}
class DriftBackupState {
@@ -267,6 +239,7 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
}
state = state.copyWith(uploadItems: {...state.uploadItems, taskId: currentItem.copyWith(isFailed: true)});
_logger.fine("Upload failed for taskId: $taskId, exception: ${update.exception}");
break;
case TaskStatus.canceled:

View File

@@ -3,7 +3,10 @@ import 'package:background_downloader/background_downloader.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/services/asset.service.dart';
import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
@@ -36,6 +39,7 @@ class ActionNotifier extends Notifier<void> {
late ActionService _service;
late UploadService _uploadService;
late DownloadService _downloadService;
late AssetService _assetService;
ActionNotifier() : super();
@@ -43,6 +47,7 @@ class ActionNotifier extends Notifier<void> {
void build() {
_uploadService = ref.watch(uploadServiceProvider);
_service = ref.watch(actionServiceProvider);
_assetService = ref.watch(assetServiceProvider);
_downloadService = ref.watch(downloadServiceProvider);
_downloadService.onImageDownloadStatus = _downloadImageCallback;
_downloadService.onVideoDownloadStatus = _downloadVideoCallback;
@@ -335,6 +340,14 @@ class ActionNotifier extends Notifier<void> {
final assets = _getOwnedRemoteAssetsForSource(source);
try {
await _service.unStack(assets.map((e) => e.stackId).nonNulls.toList());
if (source == ActionSource.viewer) {
final updatedParent = await _assetService.getRemoteAsset(assets.first.id);
if (updatedParent != null) {
ref.read(currentAssetNotifier.notifier).setAsset(updatedParent);
ref.read(assetViewerProvider.notifier).setAsset(updatedParent);
}
}
return ActionResult(count: assets.length, success: true);
} catch (error, stack) {
_logger.severe('Failed to unstack assets', error, stack);

View File

@@ -28,6 +28,8 @@ class MultiSelectState {
bool get hasRemote =>
selectedAssets.any((asset) => asset.storage == AssetState.remote || asset.storage == AssetState.merged);
bool get hasStacked => selectedAssets.any((asset) => asset is RemoteAsset && asset.stackId != null);
bool get hasLocal => selectedAssets.any((asset) => asset.storage == AssetState.local);
bool get hasMerged => selectedAssets.any((asset) => asset.storage == AssetState.merged);

View File

@@ -9,6 +9,7 @@ import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
@@ -19,9 +20,9 @@ import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
import 'package:immich_mobile/repositories/upload.repository.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as p;
import 'package:immich_mobile/utils/debug_print.dart';
final uploadServiceProvider = Provider((ref) {
final service = UploadService(
@@ -205,10 +206,20 @@ class UploadService {
return _uploadRepository.start();
}
void _handleTaskStatusUpdate(TaskStatusUpdate update) {
void _handleTaskStatusUpdate(TaskStatusUpdate update) async {
switch (update.status) {
case TaskStatus.complete:
_handleLivePhoto(update);
if (CurrentPlatform.isIOS) {
try {
final path = await update.task.filePath();
await File(path).delete();
} catch (e) {
_logger.severe('Error deleting file path for iOS: $e');
}
}
break;
default:

View File

@@ -16,6 +16,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_b
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
class ActionButtonContext {
@@ -24,6 +25,7 @@ class ActionButtonContext {
final bool isArchived;
final bool isTrashEnabled;
final bool isInLockedView;
final bool isStacked;
final RemoteAlbum? currentAlbum;
final bool advancedTroubleshooting;
final ActionSource source;
@@ -33,6 +35,7 @@ class ActionButtonContext {
required this.isOwner,
required this.isArchived,
required this.isTrashEnabled,
required this.isStacked,
required this.isInLockedView,
required this.currentAlbum,
required this.advancedTroubleshooting,
@@ -55,6 +58,7 @@ enum ActionButtonType {
deleteLocal,
upload,
removeFromAlbum,
unstack,
likeActivity;
bool shouldShow(ActionButtonContext context) {
@@ -110,6 +114,10 @@ enum ActionButtonType {
context.isOwner && //
!context.isInLockedView && //
context.currentAlbum != null,
ActionButtonType.unstack =>
context.isOwner && //
!context.isInLockedView && //
context.isStacked,
ActionButtonType.likeActivity =>
!context.isInLockedView &&
context.currentAlbum != null &&
@@ -138,28 +146,13 @@ enum ActionButtonType {
source: context.source,
),
ActionButtonType.likeActivity => const LikeActivityActionButton(),
ActionButtonType.unstack => UnStackActionButton(source: context.source),
};
}
}
class ActionButtonBuilder {
static const List<ActionButtonType> _actionTypes = [
ActionButtonType.advancedInfo,
ActionButtonType.share,
ActionButtonType.shareLink,
ActionButtonType.likeActivity,
ActionButtonType.archive,
ActionButtonType.unarchive,
ActionButtonType.download,
ActionButtonType.trash,
ActionButtonType.deletePermanent,
ActionButtonType.delete,
ActionButtonType.moveToLockFolder,
ActionButtonType.removeFromLockFolder,
ActionButtonType.deleteLocal,
ActionButtonType.upload,
ActionButtonType.removeFromAlbum,
];
static const List<ActionButtonType> _actionTypes = ActionButtonType.values;
static List<Widget> build(ActionButtonContext context) {
return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList();

View File

@@ -129,19 +129,24 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
title: Builder(
builder: (BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Builder(
builder: (context) {
return Padding(
padding: const EdgeInsets.only(top: 3.0),
child: SvgPicture.asset(
context.isDarkTheme
? 'assets/immich-logo-inline-dark.svg'
: 'assets/immich-logo-inline-light.svg',
height: 40,
),
);
},
Padding(
padding: const EdgeInsets.only(top: 3.0),
child: SvgPicture.asset(
context.isDarkTheme ? 'assets/immich-logo-inline-dark.svg' : 'assets/immich-logo-inline-light.svg',
height: 40,
),
),
const Tooltip(
triggerMode: TooltipTriggerMode.tap,
showDuration: Duration(seconds: 4),
message:
"The old timeline is deprecated and will be removed in a future release. Kindly switch to the new timeline under Advanced Settings.",
child: Padding(
padding: EdgeInsets.only(top: 3.0),
child: Icon(Icons.error_rounded, fill: 1, color: Colors.amber, size: 20),
),
),
],
);

View File

@@ -42,7 +42,12 @@ class SyncStatusAndActions extends HookConsumerWidget {
await dbFile.copy(exportFile.path);
await Share.shareXFiles([XFile(exportFile.path)], text: 'Immich Database Export');
final size = MediaQuery.of(context).size;
await Share.shareXFiles(
[XFile(exportFile.path)],
text: 'Immich Database Export',
sharePositionOrigin: Rect.fromPoints(Offset.zero, Offset(size.width / 3, size.height)),
);
Future.delayed(const Duration(seconds: 30), () async {
if (await exportFile.exists()) {

View File

@@ -82,6 +82,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -112,6 +113,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -127,6 +129,7 @@ void main() {
isInLockedView: true,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -145,6 +148,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -161,6 +165,7 @@ void main() {
isInLockedView: true,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -177,6 +182,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -195,6 +201,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -211,6 +218,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -227,6 +235,7 @@ void main() {
isInLockedView: true,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -243,6 +252,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -259,6 +269,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -277,6 +288,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -293,6 +305,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -309,6 +322,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -327,6 +341,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -343,6 +358,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -359,6 +375,7 @@ void main() {
isInLockedView: true,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -377,6 +394,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -393,6 +411,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -411,6 +430,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -427,6 +447,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -445,6 +466,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -463,6 +485,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -481,6 +504,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -497,6 +521,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -512,6 +537,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -530,6 +556,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -548,6 +575,7 @@ void main() {
isInLockedView: false,
currentAlbum: album,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -563,6 +591,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -581,6 +610,7 @@ void main() {
isInLockedView: false,
currentAlbum: album,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -597,6 +627,7 @@ void main() {
isInLockedView: false,
currentAlbum: album,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -613,6 +644,7 @@ void main() {
isInLockedView: false,
currentAlbum: album,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -628,6 +660,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -645,6 +678,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: true,
isStacked: false,
source: ActionSource.timeline,
);
@@ -660,6 +694,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -668,6 +703,59 @@ void main() {
});
});
group('unstack button', () {
test('should show when owner, not locked, has remote, and is stacked', () {
final remoteAsset = createRemoteAsset();
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: true,
source: ActionSource.timeline,
);
expect(ActionButtonType.unstack.shouldShow(context), isTrue);
});
test('should not show when not stacked', () {
final remoteAsset = createRemoteAsset();
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
expect(ActionButtonType.unstack.shouldShow(context), isFalse);
});
test('should not show when not owner', () {
final remoteAsset = createRemoteAsset();
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: false,
isArchived: true,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
expect(ActionButtonType.unstack.shouldShow(context), isFalse);
});
});
group('ActionButtonType.buildButton', () {
late BaseAsset asset;
late ActionButtonContext context;
@@ -682,6 +770,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
});
@@ -698,6 +787,22 @@ void main() {
isInLockedView: false,
currentAlbum: album,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
final widget = buttonType.buildButton(contextWithAlbum);
expect(widget, isA<Widget>());
} else if (buttonType == ActionButtonType.unstack) {
final album = createRemoteAlbum();
final contextWithAlbum = ActionButtonContext(
asset: asset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: album,
advancedTroubleshooting: false,
isStacked: true,
source: ActionSource.timeline,
);
final widget = buttonType.buildButton(contextWithAlbum);
@@ -721,6 +826,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -741,6 +847,7 @@ void main() {
isInLockedView: false,
currentAlbum: album,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -759,6 +866,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -778,6 +886,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
@@ -791,6 +900,7 @@ void main() {
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);

View File

@@ -161,7 +161,24 @@ export class NotificationService extends BaseService {
const [asset] = await this.assetRepository.getByIdsWithAllRelationsButStacks([assetId]);
if (asset) {
this.eventRepository.clientSend('on_asset_update', userId, mapAsset(asset));
// need to specify authDto to this mapAsset request, because it tries to prevent
// leaking information PR#7580 which expects a userId in the auth options object
this.eventRepository.clientSend(
'on_asset_update',
userId,
mapAsset(asset, {
auth: {
user: {
id: userId,
isAdmin: false,
name: '',
email: '',
quotaUsageInBytes: 0,
quotaSizeInBytes: null,
},
},
}),
);
}
}

View File

@@ -121,6 +121,7 @@
const onMouseLeave = () => {
mouseOver = false;
onMouseEvent?.({ isMouseOver: false, selectedGroupIndex: groupIndex });
};
let timer: ReturnType<typeof setTimeout> | null = null;

View File

@@ -0,0 +1,68 @@
<script lang="ts">
import { uploadAssetsStore } from '$lib/stores/upload';
import { flip } from 'svelte/animate';
import { scale } from 'svelte/transition';
import type { PhotostreamManager } from '$lib/managers/photostream-manager/PhotostreamManager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import type { ViewerAsset } from '$lib/managers/timeline-manager/viewer-asset.svelte';
import type { CommonPosition } from '$lib/utils/layout-utils';
import type { Snippet } from 'svelte';
let { isUploading } = uploadAssetsStore;
interface Props {
viewerAssets: ViewerAsset[];
width: number;
height: number;
photostreamManager: PhotostreamManager;
thumbnail: Snippet<
[
{
asset: TimelineAsset;
position: CommonPosition;
},
]
>;
customThumbnailLayout?: Snippet<[asset: TimelineAsset]>;
}
let { viewerAssets, width, height, photostreamManager, thumbnail, customThumbnailLayout }: Props = $props();
const transitionDuration = $derived.by(() => (photostreamManager.suspendTransitions && !$isUploading ? 0 : 150));
const scaleDuration = $derived(transitionDuration === 0 ? 0 : transitionDuration + 100);
function filterIntersecting<R extends { intersecting: boolean }>(intersectables: R[]) {
return intersectables.filter((intersectable) => intersectable.intersecting);
}
</script>
<!-- Image grid -->
<div data-image-grid class="relative overflow-clip" style:height={height + 'px'} style:width={width + 'px'}>
{#each filterIntersecting(viewerAssets) as viewerAsset (viewerAsset.id)}
{@const position = viewerAsset.position!}
{@const asset = viewerAsset.asset!}
<!-- note: don't remove data-asset-id - its used by web e2e tests -->
<div
data-asset-id={asset.id}
class="absolute"
style:top={position.top + 'px'}
style:left={position.left + 'px'}
style:width={position.width + 'px'}
style:height={position.height + 'px'}
out:scale|global={{ start: 0.1, duration: scaleDuration }}
animate:flip={{ duration: transitionDuration }}
>
{@render thumbnail({ asset, position })}
{@render customThumbnailLayout?.(asset)}
</div>
{/each}
</div>
<style>
[data-image-grid] {
user-select: none;
}
</style>

View File

@@ -0,0 +1,109 @@
<script lang="ts">
import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
import type { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
import { uploadAssetsStore } from '$lib/stores/upload';
import { Icon } from '@immich/ui';
import { mdiCheckCircle, mdiCircleOutline } from '@mdi/js';
import AssetLayout from '$lib/components/timeline/AssetLayout.svelte';
import { DayGroup } from '$lib/managers/timeline-manager/day-group.svelte';
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import type { CommonPosition } from '$lib/utils/layout-utils';
import type { Snippet } from 'svelte';
let { isUploading } = uploadAssetsStore;
interface Props {
thumbnail: Snippet<[{ asset: TimelineAsset; position: CommonPosition; dayGroup: DayGroup; groupIndex: number }]>;
customThumbnailLayout?: Snippet<[TimelineAsset]>;
singleSelect: boolean;
assetInteraction: AssetInteraction;
monthGroup: MonthGroup;
timelineManager: TimelineManager;
onDayGroupSelect: (daygroup: DayGroup, assets: TimelineAsset[]) => void;
}
let {
thumbnail: thumbnailWithGroup,
customThumbnailLayout,
singleSelect,
assetInteraction,
monthGroup,
timelineManager,
onDayGroupSelect,
}: Props = $props();
let isMouseOverGroup = $state(false);
let hoveredDayGroup = $state();
const transitionDuration = $derived.by(() =>
monthGroup.timelineManager.suspendTransitions && !$isUploading ? 0 : 150,
);
function filterIntersecting<R extends { intersecting: boolean }>(intersectables: R[]) {
return intersectables.filter((intersectable) => intersectable.intersecting);
}
</script>
{#each filterIntersecting(monthGroup.dayGroups) as dayGroup, groupIndex (dayGroup.day)}
{@const absoluteWidth = dayGroup.left}
{@const isDayGroupSelected = assetInteraction.selectedGroup.has(dayGroup.groupTitle)}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<section
class={[
{ 'transition-all': !monthGroup.timelineManager.suspendTransitions },
!monthGroup.timelineManager.suspendTransitions && `delay-${transitionDuration}`,
]}
data-group
style:position="absolute"
style:transform={`translate3d(${absoluteWidth}px,${dayGroup.top}px,0)`}
onmouseenter={() => {
isMouseOverGroup = true;
hoveredDayGroup = dayGroup.groupTitle;
}}
onmouseleave={() => {
isMouseOverGroup = false;
hoveredDayGroup = null;
}}
>
<!-- Date group title -->
<div
class="flex pt-7 pb-5 max-md:pt-5 max-md:pb-3 h-6 place-items-center text-xs font-medium text-immich-fg dark:text-immich-dark-fg md:text-sm"
style:width={dayGroup.width + 'px'}
>
{#if !singleSelect}
<div
class="hover:cursor-pointer transition-all duration-200 ease-out overflow-hidden w-0"
class:w-8={(hoveredDayGroup === dayGroup.groupTitle && isMouseOverGroup) ||
assetInteraction.selectedGroup.has(dayGroup.groupTitle)}
onclick={() => onDayGroupSelect(dayGroup, assetsSnapshot(dayGroup.getAssets()))}
onkeydown={() => onDayGroupSelect(dayGroup, assetsSnapshot(dayGroup.getAssets()))}
>
{#if isDayGroupSelected}
<Icon icon={mdiCheckCircle} size="24" class="text-primary" />
{:else}
<Icon icon={mdiCircleOutline} size="24" color="#757575" />
{/if}
</div>
{/if}
<span class="w-full truncate first-letter:capitalize" title={dayGroup.groupTitleFull}>
{dayGroup.groupTitle}
</span>
</div>
<AssetLayout
photostreamManager={timelineManager}
viewerAssets={dayGroup.viewerAssets}
height={dayGroup.height}
width={dayGroup.width}
{customThumbnailLayout}
>
{#snippet thumbnail({ asset, position })}
{@render thumbnailWithGroup({ asset, position, dayGroup, groupIndex })}
{/snippet}
</AssetLayout>
</section>
{/each}
<style>
section {
contain: layout paint style;
}
</style>

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