-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(web): Scroll to asset in gridview; increase gridview perf; reduce memory; scrollbar ticks in fixed position #10646
Conversation
Hi @midzelis, thanks a lot for your work on this. The initial review round is very promising. Besides some questions, I noticed that grouping of assets by date of the same month no longer stays on the same flex-row; they are being split into individual rows. Is that a requirement to make this mechanism work? Can we keep the previous grouping view? |
No, not a requirement, yes we can keep. This was an oversight - reverted the css causing this issue. |
return; | ||
} | ||
|
||
// const buckets = $assetStore.buckets; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove this line
<slot /> | ||
|
||
<!-- (optional) empty placeholder --> | ||
<div style:visibility={showSkeleton ? 'hidden' : 'visible'}> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
consider putting this on the asset-grid - or even wrap the grid/scrollbar - assuming intersection-obs still works
web/src/lib/stores/assets.store.ts
Outdated
@@ -272,30 +327,24 @@ export class AssetStore { | |||
}, | |||
{ signal: bucket.cancelToken.signal }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use local var for cancelToken
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some initial observations:
- Scrubbing via scrollbar seems noticeably slower
- I dislike the
at
query parameter updating while scrolling, that will also clutter the browser history - Prefer using
unknown
orvoid
instead of theany
type - Instead of adding more functionality to the already large
AssetGrid
component, consider breaking it down into smaller, more manageable components or functions. Large components are hard to test and maintain. - Components are becoming overly interconnected by passing around references to DOM elements and then accessing properties like
firstElementChild
andoffsetParent
. It's better to keep those inside components. - It would be nice to keep the pending scroll logic outside of the assets store
- There is a good amount of logic involved, adding some tests would help reduce the risk of things breaking in the future
let callbacks: WeakMap<Element, (element: Element) => any>; | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
export function resizeObserver(element: Element, onResize: (element: Element) => any) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we have some documentation of what this method does?
intersecting = entries.some((entry) => entry.isIntersecting); | ||
if (!intersecting) { | ||
const intersectingEntry = entries.find((entry) => entry.isIntersecting); | ||
intersecting = !!intersectingEntry; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make this line clearer?
web/src/lib/stores/assets.store.ts
Outdated
@@ -33,12 +34,52 @@ export class AssetBucket { | |||
* The DOM height of the bucket in pixel | |||
* This value is first estimated by the number of asset and later is corrected as the user scroll | |||
*/ | |||
bucketHeight!: number; | |||
bucketHeight = 0; | |||
bucketHeightActual = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe have a different name here to know that it is a boolean?
web/src/lib/stores/assets.store.ts
Outdated
this.complete.catch(() => void 0); | ||
} | ||
|
||
private $loadedSignal!: () => void; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain the usage of these two methods?
|
||
export let isSelectionMode = false; | ||
export let singleSelect = false; | ||
export let partOfRoute: boolean; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find the name a bit hard to understand right away. Is there a better name for this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code is... very complicated. Can we make the best effort to simplify where we can and avoid smart 1 liner?
Thank you SO MUCH for this mechanism! It works beautifully
Can you help with a short write-up of how this mechanism is designed to work in the description of the PR? So that we can store it as documentation of your design? |
Prefix this reply with I'm already working on v2 of this change. This PR can stand on its own and its not required for v2. In short, v2 optimizes some of the bucket rendering, which makes scrubbing very large timelines (tested with 111K assets) almost as fast as scrolling with the mouse wheel. It also has some bug fixes, and I'll work some tests and other improvements. That being said, I'll address your points one by one.
|
Summary of changesGot rid of svelte generates iframes that are created when using bind:clientWidth/bind:clientHeight - using resizeobserver instead
How does this all work?Beware - the timeline is a VERY complicated piece of code. Before we start, how does it behave?When the asset-grid is first rendered, it doesn’t render every asset. This would be a bad idea if you have 111K assets in your timeline. Instead, it uses intersection observers to only load the ‘buckets’ that intersect with the viewport. Buckets are groupings of assets, by default, per month. Each bucket has the full asset record object, including all exif and other metadata. Buckets are further divided into Date-Groups. By default, the date groups are grouped by day. However, if a bucket is visible, it will render all the days within the bucket, even if the bucket has several thousand pictures (foreshadowing - being fixed in v2 of this feature), Now, there is also a bucket metadata call, called getTimelineBuckets - this gives you ALL the buckets in the system, with a count of many assets are in that bucket, but without any asset details, or any other metadata. This is performed during asset-store initialization. When the asset-grid is initialized, it initializes the asset-store, loads all the timeline bucket metadata, and then starts the load process of all the buckets that intersect the viewport to load them. The asset-grid does not have ‘infinite-scroll’, since it does end, it is bounded to the number of assets in your library. But it is still virtual, in the sense that it tries to only load/show what is currently in the viewport of the screen, and unloads everything else. It’s simple to figure out how big the timeline is - just load all the images, add up the heights, and you’re done. Oh - but that means you have to load the exif height/width information of the each image - we don’t want to do that for efficiency reasons. So, instead, we use the number of photos in each bucket, and ESTIMATE the size of the bucket. Later, when the bucket is actually fully rendered, we adjust the height of the bucket to reflect actual heights. Why do we need to adjust the height of the bucket? Because the scrubber’s height needs to reflect the actual height of the timeline, which was changed by the bucket height. There is one more use-case why this is important - if you jump to the bottom of a virtual timeline, and then you start to scroll upwards, as the buckets above the scroll position begin to render, they change the size of the bucket. This changes the scroll offset, which would cause a jump in your otherwise smooth scrolling as the buckets change these size. To counter act this, as buckets are loaded, the intersection observer notices if they are ABOVE or BELOW the current viewport - if they are above the viewport, the scroll position is updated with the delta-change of the bucket-size, to maintain the scroll position where it is. There is a wrinkle to this. Within a given sub-bucket date-group, if there are just a few pictures (1,2 or maybe 3), and the next date-group also has a few, these are layed out using flex-cols. The images within a date-group are all layed out using a justified-layout algorithm, which yields absolutely positioned coordinates. However, you have to be careful to not be tempted to try to calculate the actual height of the bucket using these measurements - because of the flex-col layouts, 1 or more date-groups could be on the same row. Instead of re-implementing the browsers flow layout algorithm, it’s easier, faster, and more robust to just rely on measuring the positions from the Ok, with that background out of the way, how do we ‘scroll to an asset’?Well, ok, simple:
Ok, hold up. We had a problem at step 6. Even tho we loaded the bucket, the intersection observer is still preventing that bucket from being seen. We adjust the Well, that’s pretty much it. There are some tricks that we need to play with scrolling to prevent circular/infinite loops. That’s because sometimes the scroll position is updating because we want to scroll TO an asset. But other times, the scroll position is being updated because an asset just entered the viewport and the URL query/param needs to be updated. (That’s what To reduce flicker that would otherwise happen while loading the first bucket, then finding/loading the target bucket, and finally scrolling, a skeleton is used. I also found the skeleton distracting for very fast navigations (<.3s), so I delay showing the skeleton while that is happening. To keep the grid heights and intersection observers working properly, they are marked with css property visibility: none - so even without the skeleton there (for the first .3s) you just see the background. For longer loads, you’ll see the skeleton and then later it will be replaced with the grid, pre-scrolled to the exact location it needs to be. Now, for that to work, we need to delay setting the skeleton to ‘off’ until after the navigation completes. It needs to be a ‘navigation’ event, because that is how you update the location in svelte. That means the component assumes that there is an active navigation happening when it is created, and installs a Of course, there are some usages of the asset-grid that do not participate in routes, and are not tied to navigation at all. For example, when adding assets to an existing album. For that, a new property was added ‘participatesInRouting` which will disable all navigation - will not look at the url to scroll to asset, nor will it update the url on scroll. Ok - other strangeness that you may experience/things just to be aware of.As I said in the beginning/background - the viewer tries to load only things that are on-screen, and unloads things that are offscreen. What this means is that as you scroll (even a scroll triggered by the URL query parameter) - this may in turn load buckets, and those buckets may load/change bucket sizes while you are scrolling. If you did everything right - the scroll positions are appropriately compensated and you scroll to exactly where you wanted to go to. The height of buckets is also dependent on the width of the screen. That is because date-groups that have small amount of photos can be combined into a single row!! Don’t forget that. Ever! Actually, the bucket system is really great. The date-groups within the buckets work really well. One thing that doesn’t happen is combining date-groups that span buckets, but would otherwise fit on a single row. One might be tempted to allow that. I’d caution to avoid that. The code that ‘maintains/compensates’ the scroll position relies on the height of buckets as anchor points. If you combined date-groups across buckets, there would be no quick/easy way to determine exactly how much the scroll-delta should be. Especially since that row could be partially loaded and change as one or the other bucket come into the viewport. It is a very good idea to have well known anchor points, and those will be based on the monthly time bucket. I wrote this in one go, didn’t reread, maybe I’ll edit for clarity in the future. I could also turn this into docs in the architecture section of the docs site. |
eee258b
to
9dbac1d
Compare
… worry about sub-bucket scroll preservation
d7b7d50
to
0d32704
Compare
// so accept an array when multiple configurations are needed. | ||
if (Array.isArray(properties)) { | ||
if (!properties.every((p) => p.key)) { | ||
throw 'Multiple configurations must specify key'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please throw a new error not a string
Some notes on the scroll bar offset issue
|
…0 (#2857) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/immich-app/immich-machine-learning](https://github.com/immich-app/immich) | minor | `v1.112.1-cuda` -> `v1.113.0-cuda` | | [ghcr.io/immich-app/immich-server](https://github.com/immich-app/immich) | minor | `v1.112.1` -> `v1.113.0` | --- ### Release Notes <details> <summary>immich-app/immich (ghcr.io/immich-app/immich-machine-learning)</summary> ### [`v1.113.0`](https://github.com/immich-app/immich/releases/tag/v1.113.0) [Compare Source](immich-app/immich@v1.112.1...v1.113.0) ##### v1.113.0 > \[!WARNING] > > ## Breaking changes > > For **OAuth users**, please replace `app.immich:/` with `app.immich:///oauth-callback` for the Redirect URI in your OAuth provider settings ##### Highlights Welcome to release `v1.113.0` of Immich! This is one of the biggest releases yet, introducing some of the most requested features since the early days of Immich. Let's dive right into what we have in place for this release: - Folder view - Tags - Timeline improvements - Library refresh stability - Mobile album sync ##### Folder view You can now browse your photos and videos by folder like in a file explorer. You can use the storage template migration feature for the best experience with uploaded assets in this view. This feature is especially useful for scanned photos, which are difficult to put in a timeline. It has been a long-requested feature. You can enable this feature from the `Users Settings > Features > Folders`. ![folder-enabled](https://github.com/user-attachments/assets/aab4a16a-0888-49de-b51a-785ef557261a) The UI is currently only available for the web; mobile will come in a subsequent release. ![folder](https://github.com/user-attachments/assets/2786da30-83b3-4168-bb98-180dad53a6a4) ![folder-2](https://github.com/user-attachments/assets/163a3ee8-3c3d-4c98-abb6-9c94eb4b362c) ##### Tags Immich now supports hierarchical tags, with the ability to read existing tags from the `TagList` and `Keywords` exif properties. Any changes to tags made through Immich are also written back to a sidecar file. You can re-run the metadata extraction jobs for all assets to import your existing tags. You can enable this feature from the `Users Settings > Features > Tags`. ![tag-enabled](https://github.com/user-attachments/assets/20a7a99e-13f6-46f2-8744-20dd95d8456b) The UI is currently only available for the web; mobile will come in a subsequent release. https://github.com/user-attachments/assets/d543f531-4b0e-4dcf-8918-e76c7f1b288b ##### Timeline improvements This release introduces a rewrite of the web timeline component. It can now handle a large number of assets in a single day or month and has been successfully tested with a very large data set (over a million assets). Photographers frequently request this since they can easily take thousands of photos in a given day. With these performance improvements, you'll see fewer placeholders while loading, which will make for a more fluid scrolling and scrubbing experience. ##### Library refresh stability In relation to the previous point, the stability of library scanning has improved. Previously, you could run out of memory when starting a refresh with libraries containing millions of assets. Now, we queue the refresh jobs in batches. These enhancements won't make scanning go any faster, but they greatly reduce the likelihood of out-of-memory errors that would cause Immich to crash. ##### Mobile album sync You can now sync or mirror an album from your phone to the Immich server on your account. For example, if you select `Recents`, `Camera` and `Videos` album for backup, the corresponding album with the same name will be created on the server. Once the assets from those albums are uploaded, they will be put into the target albums automatically. You can enable this feature from the album selection in the backup screen. <img src="https://github.com/user-attachments/assets/67b329f5-15ff-4128-af52-4a50df852a07" width="300" alt="sync-album"/> For existing installations, you can sync the already uploaded assets by going to the backup screen and pressing the `Sync` button. <img src="https://github.com/user-attachments/assets/ca0847fd-edab-4ebe-b8e1-093ce7e3cc37" width="300" alt="sync-button"/> Have a wonderful weekend, Cheers! *** ##### Support Immich <p align="center"> <img src="https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExbjY2eWc5Y2F0ZW56MmR4aWE0dDhzZXlidXRmYWZyajl1bWZidXZpcyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/87CKDqErVfMqY/giphy.gif" width="450" title="SUPPORT THE PROJECT!"> </p> If you find the project helpful, you can support Immich by purchasing a product key at <https://buy.immich.app>. Cheers! 🍻 <!-- Release notes generated using configuration in .github/release.yml at main --> ##### What's Changed ##### 🚨 Breaking Changes - feat(server): granular permissions for api keys by [@​jrasm91](https://github.com/jrasm91) in immich-app/immich#11824 - refactor(server): stacks by [@​jrasm91](https://github.com/jrasm91) in immich-app/immich#11453 - fix(server): album statistics endpoint by [@​jrasm91](https://github.com/jrasm91) in immich-app/immich#11924 - fix: remove `asset.resized` by [@​jrasm91](https://github.com/jrasm91) in immich-app/immich#11983 - fix(mobile): use a valid OAuth callback URL by [@​qrkourier](https://github.com/qrkourier) in immich-app/immich#10832 ##### 🚀 Features - feat: folder view by [@​davidakerr](https://github.com/davidakerr) in immich-app/immich#11880 - feat(web): Scroll to asset in gridview; increase gridview perf; reduce memory; scrollbar ticks in fixed position by [@​midzelis](https://github.com/midzelis) in immich-app/immich#10646 - feat: loading screen, initSDK on bootstrap, fix FOUC for theme by [@​midzelis](https://github.com/midzelis) in immich-app/immich#10350 - feat(mobile): preserve mobile album info on upload by [@​alextran1502](https://github.com/alextran1502) in immich-app/immich#11965 - feat: tags by [@​jrasm91](https://github.com/jrasm91) in immich-app/immich#11980 - feat(web): jump to timeline by [@​alextran1502](https://github.com/alextran1502) in immich-app/immich#12117 ##### 🌟 Enhancements - feat(server): do not automatically download android motion videos by [@​jrasm91](https://github.com/jrasm91) in immich-app/immich#11774 - feat(web): pasting coordinates by [@​michelheusschen](https://github.com/michelheusschen) in immich-app/immich#11866 - feat(web): drag and drop or paste directories for upload by [@​simkli](https://github.com/simkli) in immich-app/immich#11879 - feat(web): Left hand navigation for memories by [@​carlesalbasboix](https://github.com/carlesalbasboix) in immich-app/immich#11913 - feat(web): my immich shortcut by [@​danieldietzler](https://github.com/danieldietzler) in immich-app/immich#12007 - fix(web): show a clearer confirmation message when deleting an unnamed album by [@​Snowknight26](https://github.com/Snowknight26) in immich-app/immich#11988 - feat(format): nrw format by [@​avsm](https://github.com/avsm) in immich-app/immich#12048 - feat(web): restore scroll position on navigating back to search page by [@​alextran1502](https://github.com/alextran1502) in immich-app/immich#12042 - feat(server): Storage template support album condition by [@​feyst](https://github.com/feyst) in immich-app/immich#12000 - fix(mobile): Changes in the UI for the image editor pages by [@​Yuvi-raj-P](https://github.com/Yuvi-raj-P) in immich-app/immich#12018 - feat(web): announce notifications to screen readers by [@​ben-basten](https://github.com/ben-basten) in immich-app/immich#12071 - fix(server): don't crash when refreshing large libraries by [@​etnoy](https://github.com/etnoy) in immich-app/immich#7934 - feat(server): sort images in duplicate groups by date by [@​GeoffreyFrogeye](https://github.com/GeoffreyFrogeye) in immich-app/immich#12094 - feat(ml): support dynamic scaling by [@​rkojedzinszky](https://github.com/rkojedzinszky) in immich-app/immich#12065 - feat(web): navigate assets with gestures (next/prev) by [@​kaziu687](https://github.com/kaziu687) in immich-app/immich#11888 - fix(mobile): allow create empty non-shared albums, add proper button colors by [@​src52](https://github.com/src52) in immich-app/immich#12103 - feat: user's features preferences by [@​alextran1502](https://github.com/alextran1502) in immich-app/immich#12099 - chore(server): log path when generating external thumbnail by [@​etnoy](https://github.com/etnoy) in immich-app/immich#12107 ##### 🐛 Bug fixes - fix(web): focus trap inside portal by [@​michelheusschen](https://github.com/michelheusschen) in immich-app/immich#11797 - fix(mobile): show correct notification icon for android by [@​karthikraja001](https://github.com/karthikraja001) in immich-app/immich#11863 - fix(web): show camera make in search options after searching by [@​michelheusschen](https://github.com/michelheusschen) in immich-app/immich#11884 - fix(web): correctly populate the camera model search dropdown by [@​Snowknight26](https://github.com/Snowknight26) in immich-app/immich#11883 - fix(server): create shared album from the mobile app does not trigger send email invite by [@​alextran1502](https://github.com/alextran1502) in immich-app/immich#11911 - fix(server): do not match live photos across libraries by [@​jrasm91](https://github.com/jrasm91) in immich-app/immich#11952 - fix(web): rating stars accessibility by [@​ben-basten](https://github.com/ben-basten) in immich-app/immich#11966 - fix(mobile): Fix for incorrectly naming edited files and structure change by [@​Yuvi-raj-P](https://github.com/Yuvi-raj-P) in immich-app/immich#11741 - fix: align camera model drop down behavior with other drop downs on web and mobile by [@​x24git](https://github.com/x24git) in immich-app/immich#11951 - fix(web): announce current theme to screen reader users by [@​ben-basten](https://github.com/ben-basten) in immich-app/immich#12039 - fix(web): show supporter badge for account less than 14 days by [@​alextran1502](https://github.com/alextran1502) in immich-app/immich#12058 - fix(web): shared link expiration date accessibility by [@​ben-basten](https://github.com/ben-basten) in immich-app/immich#12060 - chore(web): ignore shortcut toggle when entering email and password by [@​alextran1502](https://github.com/alextran1502) in immich-app/immich#12082 - chore(web): ensure goto is awaited for login page by [@​alextran1502](https://github.com/alextran1502) in immich-app/immich#12087 - fix(server): ensure new exclusion patterns work by [@​etnoy](https://github.com/etnoy) in immich-app/immich#12102 - fix(server): skip smtp validation if unchanged by [@​michelheusschen](https://github.com/michelheusschen) in immich-app/immich#12111 - fix(mobile): long waiting time for login request when server is unreachable by [@​alextran1502](https://github.com/alextran1502) in immich-app/immich#12100 - fix: user specific fields in asset search by [@​jrasm91](https://github.com/jrasm91) in immich-app/immich#12125 - fix(web): Device list shows Ubuntu as unknown OS by [@​spfncer](https://github.com/spfncer) in immich-app/immich#12127 - fix(web): reset asset grid after changing album order by [@​michelheusschen](https://github.com/michelheusschen) in immich-app/immich#12139 ##### 📚 Documentation - fix(docs): read-only affects XMP writing by [@​C-Otto](https://github.com/C-Otto) in immich-app/immich#11823 - docs: clarify external domain setting by [@​pikapower9080](https://github.com/pikapower9080) in immich-app/immich#11958 - docs: add Immich Kiosk and Immich Power Tools to Community Projects by [@​Tyree](https://github.com/Tyree) in immich-app/immich#12055 - docs: mTLS/self signed FAQ entry by [@​mmomjian](https://github.com/mmomjian) in immich-app/immich#12074 - docs: external library deletion/edits by [@​mmomjian](https://github.com/mmomjian) in immich-app/immich#12079 - docs: sql query for duplicate files by [@​mmomjian](https://github.com/mmomjian) in immich-app/immich#12086 - docs: Documentation updates by [@​aviv926](https://github.com/aviv926) in immich-app/immich#11516 - docs: update roadmap by [@​jrasm91](https://github.com/jrasm91) in immich-app/immich#12126 - fix: README_zh_CN.md link by [@​ttzytt](https://github.com/ttzytt) in immich-app/immich#12124 - docs(guide): nginx caching proxy by [@​pcouy](https://github.com/pcouy) in immich-app/immich#12140 ##### New Contributors - [@​aaronjrodrigues](https://github.com/aaronjrodrigues) made their first contribution in immich-app/immich#11851 - [@​karthikraja001](https://github.com/karthikraja001) made their first contribution in immich-app/immich#11863 - [@​simkli](https://github.com/simkli) made their first contribution in immich-app/immich#11879 - [@​carlesalbasboix](https://github.com/carlesalbasboix) made their first contribution in immich-app/immich#11907 - [@​pikapower9080](https://github.com/pikapower9080) made their first contribution in immich-app/immich#11958 - [@​davidakerr](https://github.com/davidakerr) made their first contribution in immich-app/immich#11880 - [@​x24git](https://github.com/x24git) made their first contribution in immich-app/immich#11951 - [@​Tonux599](https://github.com/Tonux599) made their first contribution in immich-app/immich#12027 - [@​avsm](https://github.com/avsm) made their first contribution in immich-app/immich#12048 - [@​Tyree](https://github.com/Tyree) made their first contribution in immich-app/immich#12055 - [@​feyst](https://github.com/feyst) made their first contribution in immich-app/immich#12000 - [@​Tiefseetauchner](https://github.com/Tiefseetauchner) made their first contribution in immich-app/immich#11874 - [@​qrkourier](https://github.com/qrkourier) made their first contribution in immich-app/immich#10832 - [@​GeoffreyFrogeye](https://github.com/GeoffreyFrogeye) made their first contribution in immich-app/immich#12094 - [@​rkojedzinszky](https://github.com/rkojedzinszky) made their first contribution in immich-app/immich#12065 - [@​kaziu687](https://github.com/kaziu687) made their first contribution in immich-app/immich#11888 - [@​src52](https://github.com/src52) made their first contribution in immich-app/immich#12103 - [@​spfncer](https://github.com/spfncer) made their first contribution in immich-app/immich#12127 - [@​ttzytt](https://github.com/ttzytt) made their first contribution in immich-app/immich#12124 **Full Changelog**: https://github.com/immich-app/immich/compare/v1.112.1... </details> --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC41OC4wIiwidXBkYXRlZEluVmVyIjoiMzguNTguMCIsInRhcmdldEJyYW5jaCI6Im1hc3RlciIsImxhYmVscyI6W119--> Co-authored-by: Renovate Bot <bot@renovateapp.com> Co-authored-by: Calvin Bui <3604363+calvinbui@users.noreply.github.com> Reviewed-on: https://gitea.bui.ng/calvinbui/ansible-monorepo/pulls/2857 Co-authored-by: renovate <renovate@noreply.gitea> Co-committed-by: renovate <renovate@noreply.gitea>
Fixes #3114
Fixes #4180