Replaced the complex implementation with Flutter's simple showModalBottomSheet()
parent
3c80049192
commit
abf71882fa
|
|
@ -10,17 +10,8 @@ import 'package:immich_mobile/providers/activity.provider.dart';
|
|||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||
|
||||
class ActivitiesBottomSheet extends HookConsumerWidget {
|
||||
final DraggableScrollableController controller;
|
||||
final double initialChildSize;
|
||||
final bool scrollToBottomInitially;
|
||||
|
||||
const ActivitiesBottomSheet({
|
||||
required this.controller,
|
||||
this.initialChildSize = 0.35,
|
||||
this.scrollToBottomInitially = true,
|
||||
super.key,
|
||||
});
|
||||
class ActivitiesBottomSheetContent extends HookConsumerWidget {
|
||||
const ActivitiesBottomSheetContent({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
|
|
@ -54,32 +45,20 @@ class ActivitiesBottomSheet extends HookConsumerWidget {
|
|||
);
|
||||
}
|
||||
|
||||
return BaseBottomSheet(
|
||||
actions: [],
|
||||
slivers: [buildActivitiesSliver()],
|
||||
footer: Padding(
|
||||
// TODO: avoid fixed padding, use context.padding.bottom
|
||||
padding: const EdgeInsets.only(bottom: 32),
|
||||
child: Column(
|
||||
children: [
|
||||
const Divider(indent: 16, endIndent: 16),
|
||||
DriftActivityTextField(
|
||||
isEnabled: album.isActivityEnabled,
|
||||
isBottomSheet: true,
|
||||
// likeId: likedId,
|
||||
onSubmit: onAddComment,
|
||||
),
|
||||
],
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(child: CustomScrollView(slivers: [buildActivitiesSliver()])),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 16),
|
||||
child: Column(
|
||||
children: [
|
||||
const Divider(indent: 16, endIndent: 16),
|
||||
DriftActivityTextField(isEnabled: album.isActivityEnabled, isBottomSheet: true, onSubmit: onAddComment),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
controller: controller,
|
||||
initialChildSize: initialChildSize,
|
||||
minChildSize: 0.1,
|
||||
maxChildSize: 0.88,
|
||||
expand: false,
|
||||
shouldCloseOnMinExtent: false,
|
||||
resizeOnScroll: false,
|
||||
backgroundColor: context.isDarkTheme ? Colors.black : Colors.white,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,14 +96,9 @@ class AssetViewer extends ConsumerStatefulWidget {
|
|||
}
|
||||
}
|
||||
|
||||
const double _kBottomSheetMinimumExtent = 0.4;
|
||||
const double _kBottomSheetSnapExtent = 0.7;
|
||||
|
||||
class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
static final _dummyListener = ImageStreamListener((image, _) => image.dispose());
|
||||
late PageController pageController;
|
||||
late DraggableScrollableController bottomSheetController;
|
||||
PersistentBottomSheetController? sheetCloseController;
|
||||
// PhotoViewGallery takes care of disposing it's controllers
|
||||
PhotoViewControllerBase? viewController;
|
||||
StreamSubscription? reloadSubscription;
|
||||
|
|
@ -111,13 +106,10 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
late final int heroOffset;
|
||||
late PhotoViewControllerValue initialPhotoViewState;
|
||||
bool? hasDraggedDown;
|
||||
bool isSnapping = false;
|
||||
bool blockGestures = false;
|
||||
bool dragInProgress = false;
|
||||
bool shouldPopOnDrag = false;
|
||||
bool assetReloadRequested = false;
|
||||
double? initialScale;
|
||||
double previousExtent = _kBottomSheetMinimumExtent;
|
||||
Offset dragDownPosition = Offset.zero;
|
||||
int totalAssets = 0;
|
||||
int stackIndex = 0;
|
||||
|
|
@ -138,7 +130,6 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
assert(ref.read(currentAssetNotifier) != null, "Current asset should not be null when opening the AssetViewer");
|
||||
pageController = PageController(initialPage: widget.initialIndex);
|
||||
totalAssets = ref.read(timelineServiceProvider).totalAssets;
|
||||
bottomSheetController = DraggableScrollableController();
|
||||
WidgetsBinding.instance.addPostFrameCallback(_onAssetInit);
|
||||
reloadSubscription = EventStream.shared.listen(_onEvent);
|
||||
heroOffset = widget.heroOffset ?? TabsRouterScope.of(context)?.controller.activeIndex ?? 0;
|
||||
|
|
@ -151,7 +142,6 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
@override
|
||||
void dispose() {
|
||||
pageController.dispose();
|
||||
bottomSheetController.dispose();
|
||||
_cancelTimers();
|
||||
reloadSubscription?.cancel();
|
||||
_prevPreCacheStream?.removeListener(_dummyListener);
|
||||
|
|
@ -175,9 +165,6 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
_delayedOperations.clear();
|
||||
}
|
||||
|
||||
double _getVerticalOffsetForBottomSheet(double extent) =>
|
||||
(context.height * extent) - (context.height * _kBottomSheetMinimumExtent);
|
||||
|
||||
ImageStream _precacheImage(BaseAsset asset) {
|
||||
final provider = getFullImageProvider(asset, size: context.sizeData);
|
||||
return provider.resolve(ImageConfiguration.empty)..addListener(_dummyListener);
|
||||
|
|
@ -257,14 +244,6 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
|
||||
void _onPageBuild(PhotoViewControllerBase controller) {
|
||||
viewController ??= controller;
|
||||
if (showingBottomSheet && bottomSheetController.isAttached) {
|
||||
final verticalOffset =
|
||||
(context.height * bottomSheetController.size) - (context.height * _kBottomSheetMinimumExtent);
|
||||
controller.position = Offset(0, -verticalOffset);
|
||||
// Apply the zoom effect when the bottom sheet is showing
|
||||
initialScale = controller.scale;
|
||||
controller.scale = (controller.scale ?? 1.0) + 0.01;
|
||||
}
|
||||
}
|
||||
|
||||
void _onPageChanged(int index, PhotoViewControllerBase? controller) {
|
||||
|
|
@ -300,7 +279,6 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
|
||||
// Do not reset the state if the bottom sheet is showing
|
||||
if (showingBottomSheet) {
|
||||
_snapBottomSheet();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -343,11 +321,6 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
final distanceToOrigin = position.distance;
|
||||
|
||||
viewController?.updateMultiple(position: position);
|
||||
// Moves the bottom sheet when the asset is being dragged up
|
||||
if (showingBottomSheet && bottomSheetController.isAttached) {
|
||||
final centre = (ctx.height * _kBottomSheetMinimumExtent);
|
||||
bottomSheetController.jumpTo((centre + distanceToOrigin) / ctx.height);
|
||||
}
|
||||
|
||||
if (distanceToOrigin > openThreshold && !showingBottomSheet && !ref.read(readonlyModeProvider)) {
|
||||
_openBottomSheet(ctx);
|
||||
|
|
@ -380,44 +353,6 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
}
|
||||
}
|
||||
|
||||
bool _onNotification(Notification delta) {
|
||||
if (delta is DraggableScrollableNotification) {
|
||||
_handleDraggableNotification(delta);
|
||||
}
|
||||
|
||||
// Handle sheet snap manually so that the it snaps only at _kBottomSheetSnapExtent but not after
|
||||
// the isSnapping guard is to prevent the notification from recursively handling the
|
||||
// notification, eventually resulting in a heap overflow
|
||||
if (!isSnapping && delta is ScrollEndNotification) {
|
||||
_snapBottomSheet();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _handleDraggableNotification(DraggableScrollableNotification delta) {
|
||||
final currentExtent = delta.extent;
|
||||
final isDraggingDown = currentExtent < previousExtent;
|
||||
previousExtent = currentExtent;
|
||||
// Closes the bottom sheet if the user is dragging down
|
||||
if (isDraggingDown && delta.extent < 0.55) {
|
||||
if (dragInProgress) {
|
||||
blockGestures = true;
|
||||
}
|
||||
sheetCloseController?.close();
|
||||
}
|
||||
|
||||
// If the asset is being dragged down, we do not want to update the asset position again
|
||||
if (dragInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
final verticalOffset = _getVerticalOffsetForBottomSheet(delta.extent);
|
||||
// Moves the asset when the bottom sheet is being dragged
|
||||
if (verticalOffset > 0) {
|
||||
viewController?.position = Offset(0, -verticalOffset);
|
||||
}
|
||||
}
|
||||
|
||||
void _onEvent(Event event) {
|
||||
if (event is TimelineReloadEvent) {
|
||||
_onTimelineReloadEvent();
|
||||
|
|
@ -430,10 +365,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
}
|
||||
|
||||
if (event is ViewerOpenBottomSheetEvent) {
|
||||
final extent = _kBottomSheetMinimumExtent + 0.3;
|
||||
_openBottomSheet(scaffoldContext!, extent: extent, activitiesMode: event.activitiesMode);
|
||||
final offset = _getVerticalOffsetForBottomSheet(extent);
|
||||
viewController?.position = Offset(0, -offset);
|
||||
_openBottomSheet(scaffoldContext!, activitiesMode: event.activitiesMode);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -469,52 +401,40 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
|
||||
setState(() {
|
||||
_onAssetChanged(pageController.page!.round());
|
||||
sheetCloseController?.close();
|
||||
});
|
||||
}
|
||||
|
||||
void _openBottomSheet(BuildContext ctx, {double extent = _kBottomSheetMinimumExtent, bool activitiesMode = false}) {
|
||||
void _openBottomSheet(BuildContext ctx, {bool activitiesMode = false}) async {
|
||||
ref.read(assetViewerProvider.notifier).setBottomSheet(true);
|
||||
initialScale = viewController?.scale;
|
||||
// viewController?.updateMultiple(scale: (viewController?.scale ?? 1.0) + 0.01);
|
||||
previousExtent = _kBottomSheetMinimumExtent;
|
||||
sheetCloseController = showBottomSheet(
|
||||
|
||||
// Move photo up when sheet opens
|
||||
const double sheetHeightFactor = 0.8;
|
||||
final verticalOffset = ctx.height * sheetHeightFactor * 0.4; // Move up by 40% of sheet height
|
||||
viewController?.animateMultiple(position: Offset(0, -verticalOffset));
|
||||
|
||||
await showModalBottomSheet(
|
||||
context: ctx,
|
||||
sheetAnimationStyle: const AnimationStyle(duration: Durations.short4, reverseDuration: Durations.short2),
|
||||
constraints: const BoxConstraints(maxWidth: double.infinity),
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20.0))),
|
||||
isScrollControlled: true,
|
||||
isDismissible: true,
|
||||
showDragHandle: true,
|
||||
backgroundColor: ctx.colorScheme.surfaceContainerLowest,
|
||||
useSafeArea: true,
|
||||
builder: (_) {
|
||||
return NotificationListener<Notification>(
|
||||
onNotification: _onNotification,
|
||||
child: activitiesMode
|
||||
? ActivitiesBottomSheet(controller: bottomSheetController, initialChildSize: extent)
|
||||
: AssetDetailBottomSheet(controller: bottomSheetController, initialChildSize: extent),
|
||||
return SizedBox(
|
||||
height: ctx.height * sheetHeightFactor,
|
||||
child: activitiesMode ? const ActivitiesBottomSheetContent() : const AssetDetailBottomSheetContent(),
|
||||
);
|
||||
},
|
||||
);
|
||||
sheetCloseController?.closed.then((_) => _handleSheetClose());
|
||||
}
|
||||
|
||||
void _handleSheetClose() {
|
||||
// Called when sheet is closed - move photo back to center
|
||||
viewController?.animateMultiple(position: Offset.zero);
|
||||
viewController?.updateMultiple(scale: initialScale);
|
||||
|
||||
ref.read(assetViewerProvider.notifier).setBottomSheet(false);
|
||||
sheetCloseController = null;
|
||||
shouldPopOnDrag = false;
|
||||
hasDraggedDown = null;
|
||||
}
|
||||
|
||||
void _snapBottomSheet() {
|
||||
if (!bottomSheetController.isAttached ||
|
||||
bottomSheetController.size > _kBottomSheetSnapExtent ||
|
||||
bottomSheetController.size < 0.4) {
|
||||
return;
|
||||
}
|
||||
isSnapping = true;
|
||||
bottomSheetController.animateTo(_kBottomSheetSnapExtent, duration: Durations.short3, curve: Curves.easeOut);
|
||||
}
|
||||
|
||||
Widget _placeholderBuilder(BuildContext ctx, ImageChunkEvent? progress, int index) {
|
||||
return const Center(child: ImmichLoadingIndicator());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,11 +35,8 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
|||
|
||||
const _kSeparator = ' • ';
|
||||
|
||||
class AssetDetailBottomSheet extends ConsumerWidget {
|
||||
final DraggableScrollableController? controller;
|
||||
final double initialChildSize;
|
||||
|
||||
const AssetDetailBottomSheet({this.controller, this.initialChildSize = 0.35, super.key});
|
||||
class AssetDetailBottomSheetContent extends ConsumerWidget {
|
||||
const AssetDetailBottomSheetContent({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
|
|
@ -69,17 +66,25 @@ class AssetDetailBottomSheet extends ConsumerWidget {
|
|||
|
||||
final actions = ActionButtonBuilder.build(buttonContext);
|
||||
|
||||
return BaseBottomSheet(
|
||||
actions: actions,
|
||||
slivers: const [_AssetDetailBottomSheet()],
|
||||
controller: controller,
|
||||
initialChildSize: initialChildSize,
|
||||
minChildSize: 0.1,
|
||||
maxChildSize: 0.88,
|
||||
expand: false,
|
||||
shouldCloseOnMinExtent: false,
|
||||
resizeOnScroll: false,
|
||||
backgroundColor: context.isDarkTheme ? Colors.black : Colors.white,
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Action buttons row - horizontally scrollable
|
||||
if (actions.isNotEmpty)
|
||||
Column(
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: actions),
|
||||
),
|
||||
const Divider(indent: 16, endIndent: 16),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
// Scrollable content
|
||||
Expanded(child: CustomScrollView(slivers: const [_AssetDetailBottomSheet()])),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -326,7 +331,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
|||
// Appears in (Albums)
|
||||
Padding(padding: const EdgeInsets.only(top: 16.0), child: _buildAppearsInList(ref, context)),
|
||||
// padding at the bottom to avoid cut-off
|
||||
const SizedBox(height: 100),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue