Replaced the complex implementation with Flutter's simple showModalBottomSheet()

pull/24414/head
kao-byte 2025-12-06 22:59:08 +11:00
parent 3c80049192
commit abf71882fa
No known key found for this signature in database
GPG Key ID: 6BFA3A267DAB6B25
3 changed files with 55 additions and 151 deletions

View File

@ -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,
],
);
}
}

View File

@ -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());
}

View File

@ -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),
],
);
}