feat(mobile): add support for animated images (GIFs) in the asset viewer#26148
feat(mobile): add support for animated images (GIFs) in the asset viewer#26148LeLunZ wants to merge 10 commits intoimmich-app:mainfrom
Conversation
|
Label error. Requires exactly 1 of: changelog:.*. Found: 📱mobile. A maintainer will add the required label. |
Technical Summary
1. Animated Image Support in FullImageProviders
2. ImageRequest RefactoringSeveral structural updates were necessary: New Method:
|
There was a problem hiding this comment.
Pull request overview
Adds animated image (GIF) playback support to the mobile asset viewer by introducing multi-frame codec loading and wiring an isAnimated flag through the full-image providers, plus a GIF badge in the gallery tiles.
Changes:
- Add
isAnimateddetection (BaseAsset.isAnimatedImage) and propagate it intoLocalFullImageProvider/RemoteFullImageProvider. - Implement animated image loading via
ui.Codec(loadCodec) and a newAnimatedImageStreamCompleter. - Show a GIF badge overlay in thumbnail tiles when the asset is detected as animated.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| mobile/lib/domain/models/asset/base_asset.model.dart | Adds isAnimatedImage getter used to decide whether to load as animated. |
| mobile/lib/presentation/widgets/images/image_provider.dart | Passes isAnimated into full-image providers. |
| mobile/lib/presentation/widgets/images/local_image_provider.dart | Adds animated loading path using AnimatedImageStreamCompleter + local loadCodec(). |
| mobile/lib/presentation/widgets/images/remote_image_provider.dart | Adds animated loading path using AnimatedImageStreamCompleter + remote loadCodec(). |
| mobile/lib/presentation/widgets/images/animated_image_stream_completer.dart | New stream completer to cancel/cleanup when listeners are gone for animated images. |
| mobile/lib/infrastructure/loaders/image_request.dart | Refactors to expose _codecFromEncodedPlatformImage and adjusts imports/parts for codec loading. |
| mobile/lib/infrastructure/loaders/local_image_request.dart | Adds loadCodec() that builds a multi-frame ui.Codec from the origin file. |
| mobile/lib/infrastructure/loaders/remote_image_request.dart | Adds loadCodec() for remote images (currently only for encoded pointer/length responses). |
| mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart | Adds GIF badge overlay based on asset.isAnimatedImage. |
| mobile/lib/widgets/common/immich_image.dart | Updates provider construction to pass isAnimated: false for video usage. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
mobile/lib/presentation/widgets/images/remote_image_provider.dart
Outdated
Show resolved
Hide resolved
mobile/lib/presentation/widgets/images/local_image_provider.dart
Outdated
Show resolved
Hide resolved
| import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; | ||
| import 'package:photo_manager/photo_manager.dart' hide AssetType; | ||
|
|
||
| import '../../domain/models/asset/base_asset.model.dart'; |
There was a problem hiding this comment.
This file mixes package: imports with a relative import for base_asset.model.dart. If the project’s linting prefers package imports (which appears to be the norm elsewhere), switch this back to a package:immich_mobile/... import for consistency and to avoid relative-path brittleness during refactors.
|
Hmm, just found out IOS uses a native decoder, needs a bit more changes :( |
| final entity = await AssetEntity.fromId(localId); | ||
| if (entity == null || _isCancelled) { | ||
| return null; | ||
| } | ||
|
|
||
| final file = await entity.originFile; | ||
| if (file == null || _isCancelled) { | ||
| return null; | ||
| } |
There was a problem hiding this comment.
Image loading is handled platform-side using native APIs and should not involve external dependencies beyond Pigeon. For local assets, the approach would be to check asset.playbackStyle == .imageAnimated in the Swift requestImage impl and return the encoded image using requestImageDataAndOrientation instead in this case. For remote images, the isAnimatedImage flag you have could be passed to the RemoteImageImpl's requestImage.
There was a problem hiding this comment.
For local assets, the approach would be to check asset.playbackStyle == .imageAnimated in the Swift requestImage impl and return the encoded image using requestImageDataAndOrientation
So you mean for local images the steps should be:
- requestImage
- check what got returned
- if its the encoded image we have to return the AnimatedImageStreamCompleter
- if its not, we use the OnePlaceholderImageStreamCompleter
The problem: with this approach, we can't show the GIF badge for Local Only assets in the gallery, as we only know if the image is animated after loading it.
For images that are local and remote, we can show the GIF badge, but if Setting.preferRemoteImage if false, it would still depend on the image thats available locally if we then actually can show an animation or not.
Would it be possible to check if an image is animated, in the sync job/when querying the list of local assets?
Then we could also show the information in the gallery.
For remote, I already tried it like you suggested, passing the flag into the requestImage works great.
There was a problem hiding this comment.
I think it'd make sense to add it to the local sync with a playbackStyle enum column in the local asset table.
There was a problem hiding this comment.
I have found an issue when testing.
When checking the duration for remote assets everything works fine for GIFs that are longer than a second, but as the durationInSeconds is of type int, we can't detect animated images that are shorter than a second.
This would need a change from durationInSeconds to durationInMilliseconds or from int to double.
I generally prefer the durationInMilliseconds but that would be a really big change. What do you prefer?
The web already does the millisecond check, because there a timestring is currently saved for the duration see here: asset.duration && !asset.duration.includes('0:00:00.000');
Would this be okay, if done in a later pr? Or should it be done in here?
There was a problem hiding this comment.
I think int milliseconds is better, but a change like that is out of scope for this PR. For now, treating <1s as static is fine.
4c84be6 to
00e38da
Compare
|
@mertalev added the discussed changes:
I wasn't sure where to add the playback style, for example if it's also wanted in the merged asset. So for now I just added the property to the local assets. Also on IOS there is the playback style A lot of the native changes are done with LLM support, so thats something you please have to look over. I tried my best and confirmed changes with multiple llms, but yeah... Also I didn't know if I should create a new migration or just append to v19, as this one didn't make it to a release yet. I think the pr is now ready for some more feedback :D |
cb7af7a to
e631ae6
Compare
|
Okay, so I asked in discord about the db migration, and there the answer was to create a new one. So I reseted v19 to be same previously and created v20. Additionally I noticed instead of setting null, when a image is restored from trash, we can actually set the playback style if we check the image type and duration. |
|
I haven't forgotten about this PR, but it makes a lot of changes so will take time to get through it. It might be best to split it up so the migration is done in one PR, populating it in the local sync is another PR and using it to determine how to load images is another PR. |
|
Sure no problem I can split it up :) |
|
Closing this in favour of smaller PRs
|
Description
This PR introduces proper animated image (e.g. GIF) support to the FullImageProviders in the mobile app. Until now, animated images were displayed as static frames, effectively demoting GIFs to old school static JPEGs.
With this change, animated images finally animate in the full image view.
In the Gallery an animated Image can be recognised by the GIF Badge where also the LivePhoto or Video badge is located. See the Screenshot section
Open Question
The only unresolved design concerns:
We could load the image, and check exif data, but I wanted some feedback first.
Codecfrom theloadCodecfunction, so if the request gets cancelled we can't returnnullbut instead throw an Error. If you have other ideas please let me know.Fixes #9628
How Has This Been Tested?
Screenshots (if appropriate)
Bildschirmaufnahme.2026-02-11.um.21.55.11.mov
Checklist:
src/services/uses repositories implementations for database calls, filesystem operations, etc.src/repositories/is pretty basic/simple and does not have any immich specific logic (that belongs insrc/services/)Please describe to which degree, if any, an LLM was used in creating this pull request.
Claude implemented the
loadCodecmethod of theLocalImageRequest, after I had a draft version of the whole RemoteImage working.