hide list/grid viw add tab button on scroll; add snackbar margins for appbar;

This commit is contained in:
Fabian Freund
2026-01-09 10:59:47 +01:00
parent d4222a0dc4
commit 3ee389e74c
5 changed files with 180 additions and 50 deletions
@@ -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<Offset>(
begin: begin,
end: Offset.zero,
).animate(
CurvedAnimation(parent: controller, curve: Curves.easeInOutQuart),
);
},
[position],
);
return Tween<Offset>(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<Offset>();
// 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<Offset>? pointerMoveEventSink;
const _BrowserView({
required this.isFullscreen,
this.pointerMoveEventSink,
});
const _BrowserView({required this.isFullscreen, this.pointerMoveEventSink});
@override
Widget build(BuildContext context) {
@@ -192,6 +192,7 @@ class ViewTabSheetWidget extends HookConsumerWidget {
controller: sheetScrollController,
builder: (context, controller) {
return ListView(
padding: EdgeInsets.zero,
controller: controller,
physics: const ClampingScrollPhysicsWithoutImplicit(),
children: [
@@ -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,
@@ -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,
+3 -10
View File
@@ -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,
),
);
}