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;
|
return null;
|
||||||
}, [visible]);
|
}, [visible]);
|
||||||
|
|
||||||
final slideAnimation = useMemoized(
|
final slideAnimation = useMemoized(() {
|
||||||
() {
|
final begin = position == TabBarPosition.top
|
||||||
final begin = position == TabBarPosition.top
|
? const Offset(0, -1)
|
||||||
? const Offset(0, -1)
|
: const Offset(0, 1);
|
||||||
: const Offset(0, 1);
|
|
||||||
|
|
||||||
return Tween<Offset>(
|
return Tween<Offset>(begin: begin, end: Offset.zero).animate(
|
||||||
begin: begin,
|
CurvedAnimation(parent: controller, curve: Curves.easeInOutQuart),
|
||||||
end: Offset.zero,
|
);
|
||||||
).animate(
|
}, [position]);
|
||||||
CurvedAnimation(parent: controller, curve: Curves.easeInOutQuart),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[position],
|
|
||||||
);
|
|
||||||
|
|
||||||
return SlideTransition(position: slideAnimation, child: child);
|
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>();
|
final pointerMoveEventsController = useStreamController<Offset>();
|
||||||
|
|
||||||
// Watch sheet state for rendering in Stack
|
// Watch sheet state for rendering in Stack
|
||||||
@@ -332,6 +310,28 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
final bottomAppBarTotalHeight =
|
final bottomAppBarTotalHeight =
|
||||||
bottomAppBarContentSize.height + bottomSafeArea;
|
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(
|
return PopScope(
|
||||||
//We need this for BackButtonListener to work downstream
|
//We need this for BackButtonListener to work downstream
|
||||||
//No direct pop result will be handled here
|
//No direct pop result will be handled here
|
||||||
@@ -340,6 +340,7 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
data: themeData,
|
data: themeData,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
// Minimal scaffold - only for Material overlay support (SnackBars)
|
// Minimal scaffold - only for Material overlay support (SnackBars)
|
||||||
|
resizeToAvoidBottomInset: false,
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
// Layer 0: Browser content (fills entire Stack - constant dimensions)
|
// Layer 0: Browser content (fills entire Stack - constant dimensions)
|
||||||
@@ -666,10 +667,7 @@ class _BrowserView extends StatelessWidget {
|
|||||||
final bool isFullscreen;
|
final bool isFullscreen;
|
||||||
final StreamSink<Offset>? pointerMoveEventSink;
|
final StreamSink<Offset>? pointerMoveEventSink;
|
||||||
|
|
||||||
const _BrowserView({
|
const _BrowserView({required this.isFullscreen, this.pointerMoveEventSink});
|
||||||
required this.isFullscreen,
|
|
||||||
this.pointerMoveEventSink,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ class ViewTabSheetWidget extends HookConsumerWidget {
|
|||||||
controller: sheetScrollController,
|
controller: sheetScrollController,
|
||||||
builder: (context, controller) {
|
builder: (context, controller) {
|
||||||
return ListView(
|
return ListView(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
physics: const ClampingScrollPhysicsWithoutImplicit(),
|
physics: const ClampingScrollPhysicsWithoutImplicit(),
|
||||||
children: [
|
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.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.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/features/user/domain/repositories/general_settings.dart';
|
||||||
import 'package:weblibre/presentation/hooks/scroll_visibility.dart';
|
|
||||||
|
|
||||||
class _TabDraggable extends HookConsumerWidget {
|
class _TabDraggable extends HookConsumerWidget {
|
||||||
final TabEntity entity;
|
final TabEntity entity;
|
||||||
@@ -346,6 +345,7 @@ class _TabGrid extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GridView.builder(
|
return GridView.builder(
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
//Sync values for itemHeight calculation _calculateItemHeight
|
//Sync values for itemHeight calculation _calculateItemHeight
|
||||||
childAspectRatio: 0.75,
|
childAspectRatio: 0.75,
|
||||||
@@ -390,6 +390,9 @@ class ViewTabGridWidget extends HookConsumerWidget {
|
|||||||
final bool tabsReorderable;
|
final bool tabsReorderable;
|
||||||
final VoidCallback onClose;
|
final VoidCallback onClose;
|
||||||
|
|
||||||
|
static const _hideThreshold = 0.02; // 2% of sheet size change
|
||||||
|
static const _showThreshold = 0.02;
|
||||||
|
|
||||||
const ViewTabGridWidget({
|
const ViewTabGridWidget({
|
||||||
required this.onClose,
|
required this.onClose,
|
||||||
required this.scrollController,
|
required this.scrollController,
|
||||||
@@ -401,8 +404,74 @@ class ViewTabGridWidget extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
// Track FAB visibility based on scroll direction
|
final isFabVisible = useState(true);
|
||||||
final isFabVisible = useScrollVisibility(scrollController);
|
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(
|
return Stack(
|
||||||
alignment: Alignment.bottomRight,
|
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.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.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/features/user/domain/repositories/general_settings.dart';
|
||||||
import 'package:weblibre/presentation/hooks/scroll_visibility.dart';
|
|
||||||
|
|
||||||
class _TabDraggable extends HookConsumerWidget {
|
class _TabDraggable extends HookConsumerWidget {
|
||||||
final TabEntity entity;
|
final TabEntity entity;
|
||||||
@@ -194,6 +193,7 @@ class _TabListView extends HookConsumerWidget {
|
|||||||
builder: (context, controller) {
|
builder: (context, controller) {
|
||||||
return !tabsReorderable
|
return !tabsReorderable
|
||||||
? ListView.builder(
|
? ListView.builder(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
itemCount: itemCount,
|
itemCount: itemCount,
|
||||||
itemExtent: _itemHeight,
|
itemExtent: _itemHeight,
|
||||||
@@ -359,6 +359,9 @@ class ViewTabListWidget extends HookConsumerWidget {
|
|||||||
final bool tabsReorderable;
|
final bool tabsReorderable;
|
||||||
final VoidCallback onClose;
|
final VoidCallback onClose;
|
||||||
|
|
||||||
|
static const _hideThreshold = 0.02; // 2% of sheet size change
|
||||||
|
static const _showThreshold = 0.02;
|
||||||
|
|
||||||
const ViewTabListWidget({
|
const ViewTabListWidget({
|
||||||
required this.onClose,
|
required this.onClose,
|
||||||
required this.scrollController,
|
required this.scrollController,
|
||||||
@@ -370,8 +373,74 @@ class ViewTabListWidget extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
// Track FAB visibility based on scroll direction
|
final isFabVisible = useState(true);
|
||||||
final isFabVisible = useScrollVisibility(scrollController);
|
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(
|
return Stack(
|
||||||
alignment: Alignment.bottomRight,
|
alignment: Alignment.bottomRight,
|
||||||
|
|||||||
@@ -22,11 +22,9 @@ import 'package:nullability/nullability.dart';
|
|||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:weblibre/utils/clipboard.dart';
|
import 'package:weblibre/utils/clipboard.dart';
|
||||||
|
|
||||||
/// Default bottom margin for floating snackbars to position above toolbar.
|
/// Creates a floating snackbar.
|
||||||
/// This accounts for the typical browser toolbar height.
|
/// The margin is controlled by the scaffold's snackBarTheme for proper
|
||||||
const _kSnackBarBottomMargin = 72.0;
|
/// positioning above bottom app bars of varying heights.
|
||||||
|
|
||||||
/// Creates a floating snackbar with proper margin for overlay toolbar layout.
|
|
||||||
SnackBar _createFloatingSnackBar({
|
SnackBar _createFloatingSnackBar({
|
||||||
required Widget content,
|
required Widget content,
|
||||||
Color? backgroundColor,
|
Color? backgroundColor,
|
||||||
@@ -41,11 +39,6 @@ SnackBar _createFloatingSnackBar({
|
|||||||
duration: duration,
|
duration: duration,
|
||||||
persist: persist,
|
persist: persist,
|
||||||
behavior: SnackBarBehavior.floating,
|
behavior: SnackBarBehavior.floating,
|
||||||
margin: const EdgeInsets.only(
|
|
||||||
left: 16,
|
|
||||||
right: 16,
|
|
||||||
bottom: _kSnackBarBottomMargin,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user