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
@@ -37,6 +37,13 @@ abstract class PhotoViewControllerBase<T extends PhotoViewControllerValue> {
/// Closes streams and removes eventual listeners.
void dispose();
void positionAnimationBuilder(void Function(Offset)? value);
void scaleAnimationBuilder(void Function(double)? value);
void rotationAnimationBuilder(void Function(double)? value);
/// Animates multiple fields of the state
void animateMultiple({Offset? position, double? scale, double? rotation});
/// Add a listener that will ignore updates made internally
///
/// Since it is made for internal use, it is not performatic to use more than one
@@ -147,12 +154,31 @@ class PhotoViewController
late StreamController<PhotoViewControllerValue> _outputCtrl;
late void Function(Offset)? _animatePosition;
late void Function(double)? _animateScale;
late void Function(double)? _animateRotation;
@override
Stream<PhotoViewControllerValue> get outputStateStream => _outputCtrl.stream;
@override
late PhotoViewControllerValue prevValue;
@override
void positionAnimationBuilder(void Function(Offset)? value) {
_animatePosition = value;
}
@override
void scaleAnimationBuilder(void Function(double)? value) {
_animateScale = value;
}
@override
void rotationAnimationBuilder(void Function(double)? value) {
_animateRotation = value;
}
@override
void reset() {
value = initial;
@@ -172,6 +198,21 @@ class PhotoViewController
_valueNotifier.removeIgnorableListener(callback);
}
@override
void animateMultiple({Offset? position, double? scale, double? rotation}) {
if (position != null && _animatePosition != null) {
_animatePosition!(position);
}
if (scale != null && _animateScale != null) {
_animateScale!(scale);
}
if (rotation != null && _animateRotation != null) {
_animateRotation!(rotation);
}
}
@override
void dispose() {
_outputCtrl.close();
@@ -111,6 +111,16 @@ mixin PhotoViewControllerDelegate on State<PhotoViewCore> {
);
}
PhotoViewScaleState getScaleStateFromNewScale(double newScale) {
PhotoViewScaleState newScaleState = PhotoViewScaleState.initial;
if (scale != scaleBoundaries.initialScale) {
newScaleState = (newScale > scaleBoundaries.initialScale)
? PhotoViewScaleState.zoomedIn
: PhotoViewScaleState.zoomedOut;
}
return newScaleState;
}
void updateScaleStateFromNewScale(double newScale) {
PhotoViewScaleState newScaleState = PhotoViewScaleState.initial;
if (scale != scaleBoundaries.initialScale) {
@@ -26,6 +26,8 @@ class PhotoViewScaleStateController {
StreamController<PhotoViewScaleState>.broadcast()
..sink.add(PhotoViewScaleState.initial);
bool _hasZoomedOutManually = false;
/// The output for state/value updates
Stream<PhotoViewScaleState> get outputScaleStateStream =>
_outputScaleStateCtrl.stream;
@@ -42,10 +44,20 @@ class PhotoViewScaleStateController {
return;
}
if (newValue == PhotoViewScaleState.zoomedOut) {
_hasZoomedOutManually = true;
}
if (newValue == PhotoViewScaleState.initial) {
_hasZoomedOutManually = false;
}
prevScaleState = _scaleStateNotifier.value;
_scaleStateNotifier.value = newValue;
}
bool get hasZoomedOutManually => _hasZoomedOutManually;
/// Checks if its actual value is different than previousValue
bool get hasChanged => prevScaleState != scaleState;
@@ -71,6 +83,15 @@ class PhotoViewScaleStateController {
if (_scaleStateNotifier.value == newValue) {
return;
}
if (newValue == PhotoViewScaleState.zoomedOut) {
_hasZoomedOutManually = true;
}
if (newValue == PhotoViewScaleState.initial) {
_hasZoomedOutManually = false;
}
prevScaleState = _scaleStateNotifier.value;
_scaleStateNotifier.updateIgnoring(newValue);
}