diff --git a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart index 0477fa1f..6c713aed 100644 --- a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart +++ b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart @@ -88,21 +88,15 @@ class _AnimatedToolbar extends HookWidget { return null; }, [visible]); - final slideAnimation = useMemoized( - () { - final begin = position == TabBarPosition.top - ? const Offset(0, -1) - : const Offset(0, 1); + final slideAnimation = useMemoized(() { + final begin = position == TabBarPosition.top + ? const Offset(0, -1) + : const Offset(0, 1); - return Tween( - begin: begin, - end: Offset.zero, - ).animate( - CurvedAnimation(parent: controller, curve: Curves.easeInOutQuart), - ); - }, - [position], - ); + return Tween(begin: begin, end: Offset.zero).animate( + CurvedAnimation(parent: controller, curve: Curves.easeInOutQuart), + ); + }, [position]); return SlideTransition(position: slideAnimation, child: child); } @@ -284,22 +278,6 @@ class BrowserScreen extends HookConsumerWidget { }, ); - final themeData = useMemoized( - () => Theme.of(context).copyWith( - bottomSheetTheme: BottomSheetThemeData( - constraints: BoxConstraints( - maxWidth: - MediaQuery.of(context).size.width - - math.max( - MediaQuery.of(context).padding.left * 2, - MediaQuery.of(context).padding.right * 2, - ), - ), - ), - ), - [], - ); - final pointerMoveEventsController = useStreamController(); // Watch sheet state for rendering in Stack @@ -332,6 +310,28 @@ class BrowserScreen extends HookConsumerWidget { final bottomAppBarTotalHeight = bottomAppBarContentSize.height + bottomSafeArea; + // Theme with dynamic snackbar margin to position above bottom toolbar + final themeData = Theme.of(context).copyWith( + bottomSheetTheme: BottomSheetThemeData( + constraints: BoxConstraints( + maxWidth: + MediaQuery.of(context).size.width - + math.max( + MediaQuery.of(context).padding.left * 2, + MediaQuery.of(context).padding.right * 2, + ), + ), + ), + snackBarTheme: SnackBarThemeData( + behavior: SnackBarBehavior.floating, + insetPadding: EdgeInsets.only( + left: 16, + right: 16, + bottom: bottomToolbarVisible ? bottomAppBarTotalHeight + 8 : 16, + ), + ), + ); + return PopScope( //We need this for BackButtonListener to work downstream //No direct pop result will be handled here @@ -340,6 +340,7 @@ class BrowserScreen extends HookConsumerWidget { data: themeData, child: Scaffold( // Minimal scaffold - only for Material overlay support (SnackBars) + resizeToAvoidBottomInset: false, body: Stack( children: [ // Layer 0: Browser content (fills entire Stack - constant dimensions) @@ -666,10 +667,7 @@ class _BrowserView extends StatelessWidget { final bool isFullscreen; final StreamSink? pointerMoveEventSink; - const _BrowserView({ - required this.isFullscreen, - this.pointerMoveEventSink, - }); + const _BrowserView({required this.isFullscreen, this.pointerMoveEventSink}); @override Widget build(BuildContext context) { diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart index 83eb34ff..5c4909b8 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart @@ -192,6 +192,7 @@ class ViewTabSheetWidget extends HookConsumerWidget { controller: sheetScrollController, builder: (context, controller) { return ListView( + padding: EdgeInsets.zero, controller: controller, physics: const ClampingScrollPhysicsWithoutImplicit(), children: [ diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_grid_view.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_grid_view.dart index 36e27527..aa53ad00 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_grid_view.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_grid_view.dart @@ -27,7 +27,6 @@ import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/co import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart'; import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; -import 'package:weblibre/presentation/hooks/scroll_visibility.dart'; class _TabDraggable extends HookConsumerWidget { final TabEntity entity; @@ -346,6 +345,7 @@ class _TabGrid extends StatelessWidget { Widget build(BuildContext context) { return GridView.builder( controller: scrollController, + padding: EdgeInsets.zero, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( //Sync values for itemHeight calculation _calculateItemHeight childAspectRatio: 0.75, @@ -390,6 +390,9 @@ class ViewTabGridWidget extends HookConsumerWidget { final bool tabsReorderable; final VoidCallback onClose; + static const _hideThreshold = 0.02; // 2% of sheet size change + static const _showThreshold = 0.02; + const ViewTabGridWidget({ required this.onClose, required this.scrollController, @@ -401,8 +404,74 @@ class ViewTabGridWidget extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - // Track FAB visibility based on scroll direction - final isFabVisible = useScrollVisibility(scrollController); + final isFabVisible = useState(true); + final lastSheetSize = useRef(0.0); + final isInitialized = useRef(false); + + // Delay initialization to ignore initial animations + useEffect(() { + final timer = Timer(const Duration(milliseconds: 500), () { + isInitialized.value = true; + // Initialize lastSheetSize with current size + if (draggableScrollableController?.isAttached == true) { + lastSheetSize.value = draggableScrollableController!.size; + } + }); + return timer.cancel; + }, []); + + // Listen to DraggableScrollableController for sheet size changes + useEffect(() { + final controller = draggableScrollableController; + if (controller == null) return null; + + void listener() { + if (!isInitialized.value) return; + if (!controller.isAttached) return; + + final currentSize = controller.size; + final difference = currentSize - lastSheetSize.value; + + // Hide when sheet expands (dragging up / scrolling down) + if (difference > _hideThreshold && isFabVisible.value) { + isFabVisible.value = false; + } + // Show when sheet collapses (dragging down / scrolling up) + else if (difference < -_showThreshold && !isFabVisible.value) { + isFabVisible.value = true; + } + + lastSheetSize.value = currentSize; + } + + controller.addListener(listener); + return () => controller.removeListener(listener); + }, [draggableScrollableController]); + + // Fallback: Also listen to scroll controller for fullscreen mode (no draggable sheet) + useEffect(() { + if (draggableScrollableController != null) return null; + + var lastOffset = 0.0; + void listener() { + if (!isInitialized.value) return; + + final currentOffset = scrollController.offset; + final difference = currentOffset - lastOffset; + + if (difference > 10.0 && isFabVisible.value) { + isFabVisible.value = false; + } else if ((difference < -10.0 || currentOffset <= 0) && + !isFabVisible.value) { + isFabVisible.value = true; + } + + lastOffset = currentOffset; + } + + scrollController.addListener(listener); + return () => scrollController.removeListener(listener); + }, [scrollController, draggableScrollableController]); return Stack( alignment: Alignment.bottomRight, diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_list_view.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_list_view.dart index 27311efe..f1e045d4 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_list_view.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_list_view.dart @@ -25,7 +25,6 @@ import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/co import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart'; import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; -import 'package:weblibre/presentation/hooks/scroll_visibility.dart'; class _TabDraggable extends HookConsumerWidget { final TabEntity entity; @@ -194,6 +193,7 @@ class _TabListView extends HookConsumerWidget { builder: (context, controller) { return !tabsReorderable ? ListView.builder( + padding: EdgeInsets.zero, controller: scrollController, itemCount: itemCount, itemExtent: _itemHeight, @@ -359,6 +359,9 @@ class ViewTabListWidget extends HookConsumerWidget { final bool tabsReorderable; final VoidCallback onClose; + static const _hideThreshold = 0.02; // 2% of sheet size change + static const _showThreshold = 0.02; + const ViewTabListWidget({ required this.onClose, required this.scrollController, @@ -370,8 +373,74 @@ class ViewTabListWidget extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - // Track FAB visibility based on scroll direction - final isFabVisible = useScrollVisibility(scrollController); + final isFabVisible = useState(true); + final lastSheetSize = useRef(0.0); + final isInitialized = useRef(false); + + // Delay initialization to ignore initial animations + useEffect(() { + final timer = Timer(const Duration(milliseconds: 500), () { + isInitialized.value = true; + // Initialize lastSheetSize with current size + if (draggableScrollableController?.isAttached == true) { + lastSheetSize.value = draggableScrollableController!.size; + } + }); + return timer.cancel; + }, []); + + // Listen to DraggableScrollableController for sheet size changes + useEffect(() { + final controller = draggableScrollableController; + if (controller == null) return null; + + void listener() { + if (!isInitialized.value) return; + if (!controller.isAttached) return; + + final currentSize = controller.size; + final difference = currentSize - lastSheetSize.value; + + // Hide when sheet expands (dragging up / scrolling down) + if (difference > _hideThreshold && isFabVisible.value) { + isFabVisible.value = false; + } + // Show when sheet collapses (dragging down / scrolling up) + else if (difference < -_showThreshold && !isFabVisible.value) { + isFabVisible.value = true; + } + + lastSheetSize.value = currentSize; + } + + controller.addListener(listener); + return () => controller.removeListener(listener); + }, [draggableScrollableController]); + + // Fallback: Also listen to scroll controller for fullscreen mode (no draggable sheet) + useEffect(() { + if (draggableScrollableController != null) return null; + + var lastOffset = 0.0; + void listener() { + if (!isInitialized.value) return; + + final currentOffset = scrollController.offset; + final difference = currentOffset - lastOffset; + + if (difference > 10.0 && isFabVisible.value) { + isFabVisible.value = false; + } else if ((difference < -10.0 || currentOffset <= 0) && + !isFabVisible.value) { + isFabVisible.value = true; + } + + lastOffset = currentOffset; + } + + scrollController.addListener(listener); + return () => scrollController.removeListener(listener); + }, [scrollController, draggableScrollableController]); return Stack( alignment: Alignment.bottomRight, diff --git a/app/lib/utils/ui_helper.dart b/app/lib/utils/ui_helper.dart index ac4ba991..960cd9f5 100644 --- a/app/lib/utils/ui_helper.dart +++ b/app/lib/utils/ui_helper.dart @@ -22,11 +22,9 @@ import 'package:nullability/nullability.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:weblibre/utils/clipboard.dart'; -/// Default bottom margin for floating snackbars to position above toolbar. -/// This accounts for the typical browser toolbar height. -const _kSnackBarBottomMargin = 72.0; - -/// Creates a floating snackbar with proper margin for overlay toolbar layout. +/// Creates a floating snackbar. +/// The margin is controlled by the scaffold's snackBarTheme for proper +/// positioning above bottom app bars of varying heights. SnackBar _createFloatingSnackBar({ required Widget content, Color? backgroundColor, @@ -41,11 +39,6 @@ SnackBar _createFloatingSnackBar({ duration: duration, persist: persist, behavior: SnackBarBehavior.floating, - margin: const EdgeInsets.only( - left: 16, - right: 16, - bottom: _kSnackBarBottomMargin, - ), ); }