hide list/grid viw add tab button on scroll; add snackbar margins for appbar;
This commit is contained in:
@@ -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: [
|
||||
|
||||
+72
-3
@@ -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,
|
||||
|
||||
+72
-3
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user