feat(mobile): sqlite asset viewer (#19552)

* add full image provider and refactor thumb providers

* photo_view updates

* wip: asset-viewer

* fix controller dispose on page change

* wip: bottom sheet

* fix interactions

* more bottomsheet changes

* generate schema

* PR feedback

* refactor asset viewer

* never rotate and fix background on page change

* use photoview as the loading builder

* precache after delay

* claude: optimizing rebuild of image provider

* claude: optimizing image decoding and caching

* use proper cache for new full size image providers

* chore: load local HEIC fullsize for iOS

* make controller callbacks nullable

* remove imageprovider cache

* do not handle drag gestures when zoomed

* use loadOriginal setting for HEIC / larger images

* preload assets outside timer

* never use same controllers in photo-view gallery

* fix: cannot scroll down once swipe with bottom sheet

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
shenlong
2025-07-02 23:54:37 +05:30
committed by GitHub
parent ec603a008c
commit 7855974a29
47 changed files with 1867 additions and 490 deletions
+38 -2
View File
@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/widgets/photo_view/src/controller/photo_view_controller.dart';
import 'package:immich_mobile/widgets/photo_view/src/controller/photo_view_scalestate_controller.dart';
import 'package:immich_mobile/widgets/photo_view/src/core/photo_view_core.dart';
@@ -16,6 +15,11 @@ export 'src/photo_view_computed_scale.dart';
export 'src/photo_view_scale_state.dart';
export 'src/utils/photo_view_hero_attributes.dart';
typedef PhotoViewControllerCallback = PhotoViewControllerBase Function();
typedef PhotoViewControllerCallbackBuilder = void Function(
PhotoViewControllerCallback photoViewMethod,
);
/// A [StatefulWidget] that contains all the photo view rendering elements.
///
/// Sample code to use within an image:
@@ -239,8 +243,11 @@ class PhotoView extends StatefulWidget {
this.wantKeepAlive = false,
this.gaplessPlayback = false,
this.heroAttributes,
this.onPageBuild,
this.controllerCallbackBuilder,
this.scaleStateChangedCallback,
this.enableRotation = false,
this.semanticLabel,
this.controller,
this.scaleStateController,
this.maxScale,
@@ -260,6 +267,7 @@ class PhotoView extends StatefulWidget {
this.tightMode,
this.filterQuality,
this.disableGestures,
this.disableScaleGestures,
this.errorBuilder,
this.enablePanAlways,
}) : child = null,
@@ -278,6 +286,8 @@ class PhotoView extends StatefulWidget {
this.backgroundDecoration,
this.wantKeepAlive = false,
this.heroAttributes,
this.onPageBuild,
this.controllerCallbackBuilder,
this.scaleStateChangedCallback,
this.enableRotation = false,
this.controller,
@@ -298,9 +308,11 @@ class PhotoView extends StatefulWidget {
this.gestureDetectorBehavior,
this.tightMode,
this.filterQuality,
this.disableScaleGestures,
this.disableGestures,
this.enablePanAlways,
}) : errorBuilder = null,
}) : semanticLabel = null,
errorBuilder = null,
imageProvider = null,
gaplessPlayback = false,
loadingBuilder = null,
@@ -325,6 +337,11 @@ class PhotoView extends StatefulWidget {
/// `true` -> keeps the state
final bool wantKeepAlive;
/// A Semantic description of the image.
///
/// Used to provide a description of the image to TalkBack on Android, and VoiceOver on iOS.
final String? semanticLabel;
/// This is used to continue showing the old image (`true`), or briefly show
/// nothing (`false`), when the `imageProvider` changes. By default it's set
/// to `false`.
@@ -338,6 +355,12 @@ class PhotoView extends StatefulWidget {
/// by default it is `MediaQuery.of(context).size`.
final Size? customSize;
// Called when a new PhotoView widget is built
final ValueChanged<PhotoViewControllerBase>? onPageBuild;
// Called from the parent during page change to get the new controller
final PhotoViewControllerCallbackBuilder? controllerCallbackBuilder;
/// A [Function] to be called whenever the scaleState changes, this happens when the user double taps the content ou start to pinch-in.
final ValueChanged<PhotoViewScaleState>? scaleStateChangedCallback;
@@ -419,6 +442,9 @@ class PhotoView extends StatefulWidget {
// Useful when custom gesture detector is used in child widget.
final bool? disableGestures;
/// Mirror to [PhotoView.disableGestures]
final bool? disableScaleGestures;
/// Enable pan the widget even if it's smaller than the hole parent widget.
/// Useful when you want to drag a widget without restrictions.
final bool? enablePanAlways;
@@ -452,6 +478,7 @@ class _PhotoViewState extends State<PhotoView>
if (widget.controller == null) {
_controlledController = true;
_controller = PhotoViewController();
widget.onPageBuild?.call(_controller);
} else {
_controlledController = false;
_controller = widget.controller!;
@@ -466,6 +493,8 @@ class _PhotoViewState extends State<PhotoView>
}
_scaleStateController.outputScaleStateStream.listen(scaleStateListener);
// Pass a ref to the method back to the gallery so it can fetch the controller on page changes
widget.controllerCallbackBuilder?.call(_controllerGetter);
}
@override
@@ -474,6 +503,7 @@ class _PhotoViewState extends State<PhotoView>
if (!_controlledController) {
_controlledController = true;
_controller = PhotoViewController();
widget.onPageBuild?.call(_controller);
}
} else {
_controlledController = false;
@@ -509,6 +539,8 @@ class _PhotoViewState extends State<PhotoView>
}
}
PhotoViewControllerBase _controllerGetter() => _controller;
@override
Widget build(BuildContext context) {
super.build(context);
@@ -547,6 +579,7 @@ class _PhotoViewState extends State<PhotoView>
tightMode: widget.tightMode,
filterQuality: widget.filterQuality,
disableGestures: widget.disableGestures,
disableScaleGestures: widget.disableScaleGestures,
enablePanAlways: widget.enablePanAlways,
child: widget.child,
)
@@ -554,6 +587,7 @@ class _PhotoViewState extends State<PhotoView>
imageProvider: widget.imageProvider!,
loadingBuilder: widget.loadingBuilder,
backgroundDecoration: backgroundDecoration,
semanticLabel: widget.semanticLabel,
gaplessPlayback: widget.gaplessPlayback,
heroAttributes: widget.heroAttributes,
scaleStateChangedCallback: widget.scaleStateChangedCallback,
@@ -577,6 +611,7 @@ class _PhotoViewState extends State<PhotoView>
tightMode: widget.tightMode,
filterQuality: widget.filterQuality,
disableGestures: widget.disableGestures,
disableScaleGestures: widget.disableScaleGestures,
errorBuilder: widget.errorBuilder,
enablePanAlways: widget.enablePanAlways,
index: widget.index,
@@ -626,6 +661,7 @@ typedef PhotoViewImageDragStartCallback = Function(
BuildContext context,
DragStartDetails details,
PhotoViewControllerValue controllerValue,
PhotoViewScaleStateController scaleStateController,
);
/// A type definition for a callback when the user drags