update ui logic to replace scaffold

This commit is contained in:
Fabian Freund
2026-01-08 14:29:00 +01:00
parent 1e4eadaab2
commit ba057015f4
4 changed files with 139 additions and 135 deletions
@@ -300,19 +300,24 @@ class BrowserScreen extends HookConsumerWidget {
[],
);
final sheetController = useState<PersistentBottomSheetController?>(null);
final pointerMoveEventsController = useStreamController<Offset>();
// Watch sheet state for rendering in Stack
final displayedSheet = ref.watch(bottomSheetControllerProvider);
final sheetDisplayed = displayedSheet != null;
// Compute visibility states for toolbars
final appBarVisible = useValueListenable(displayAppBar);
final topDismissed = ref.watch(tabBarDismissableControllerProvider);
// Toolbar is visible when: sheet is shown OR (not fullscreen AND app bar visible)
final topToolbarVisible =
sheetController.value != null || (!tabInFullScreen && !topDismissed);
sheetDisplayed || (!tabInFullScreen && !topDismissed);
final bottomToolbarVisible =
sheetController.value != null || (!tabInFullScreen && appBarVisible);
sheetDisplayed || (!tabInFullScreen && appBarVisible);
// Calculate relative safe area for sheet max size
final relativeSafeArea = MediaQuery.of(context).relativeSafeArea();
// Calculate bottom toolbar size for FAB positioning
final bottomAppBarSize = BrowserBottomAppBar(
@@ -340,15 +345,28 @@ class BrowserScreen extends HookConsumerWidget {
Positioned.fill(
child: _Browser(
overlayController: overlayController,
sheetController: sheetController,
displayAppBar: displayAppBar,
tabInFullScreen: tabInFullScreen,
pointerMoveEventSink: pointerMoveEventsController.sink,
bottomAppBarSize: bottomAppBarSize,
sheetDisplayed: sheetDisplayed,
),
),
// Layer 1: Bottom Toolbar (overlay, slides in/out)
// Layer 1: Sheet (when displayed) - positioned above toolbar
if (sheetDisplayed)
Positioned(
left: 0,
right: 0,
top: 0,
bottom: bottomAppBarSize.height,
child: _SheetContainer(
displayedSheet: displayedSheet,
relativeSafeArea: relativeSafeArea,
bottomAppBarSize: bottomAppBarSize,
),
),
// Layer 2: Bottom Toolbar (overlay, slides in/out) - above sheet
Positioned(
left: 0,
right: 0,
@@ -367,7 +385,7 @@ class BrowserScreen extends HookConsumerWidget {
),
),
// Layer 2: Top Toolbar (overlay, slides in/out) - only when position is top
// Layer 3: Top Toolbar (overlay, slides in/out) - only when position is top
if (tabBarPosition == TabBarPosition.top)
Positioned(
left: 0,
@@ -387,10 +405,14 @@ class BrowserScreen extends HookConsumerWidget {
),
),
// Layer 3: FAB (positioned above bottom toolbar)
Positioned(
// Layer 4: FAB (animates position with toolbar visibility)
AnimatedPositioned(
duration: _AnimatedToolbar._kAnimationDuration,
curve: Curves.easeInOutQuart,
right: 16,
bottom: bottomAppBarSize.height + 16,
bottom: bottomToolbarVisible
? bottomAppBarSize.height + 16
: 16 + MediaQuery.of(context).padding.bottom,
child: const BrowserFab(),
),
],
@@ -401,24 +423,80 @@ class BrowserScreen extends HookConsumerWidget {
}
}
/// Container widget for bottom sheets - renders in Stack for proper layering.
class _SheetContainer extends HookConsumerWidget {
final Sheet displayedSheet;
final double relativeSafeArea;
final Size bottomAppBarSize;
const _SheetContainer({
required this.displayedSheet,
required this.relativeSafeArea,
required this.bottomAppBarSize,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
bool dismissOnThreshold(DraggableScrollableNotification notification) {
if (notification.extent <= 0.1) {
logger.i('Dismissing sheet, reached min extend');
ref.read(bottomSheetControllerProvider.notifier).requestDismiss();
return true;
}
return false;
}
return GestureDetector(
onTap: () {
// Dismiss sheet when tapping outside
ref.read(bottomSheetControllerProvider.notifier).requestDismiss();
},
child: Container(
color: Colors.black54, // Scrim
child: GestureDetector(
onTap: () {}, // Prevent tap from propagating to parent
child: Align(
alignment: Alignment.bottomCenter,
child: switch (displayedSheet) {
ViewTabsSheet() =>
NotificationListener<DraggableScrollableNotification>(
key: UniqueKey(),
onNotification: dismissOnThreshold,
child: _ViewTabsSheet(maxChildSize: relativeSafeArea),
),
final EditUrlSheet parameter =>
NotificationListener<DraggableScrollableNotification>(
key: UniqueKey(),
onNotification: dismissOnThreshold,
child: _ViewUrlSheet(
initialTabState: parameter.tabState,
maxChildSize: relativeSafeArea,
bottomAppBarSize: bottomAppBarSize,
),
),
},
),
),
),
);
}
}
class _Browser extends HookConsumerWidget {
Duration get _backButtonPressTimeout => const Duration(seconds: 2);
final OverlayPortalController overlayController;
final ValueNotifier<PersistentBottomSheetController?> sheetController;
final ValueNotifier<bool> displayAppBar;
final StreamSink<Offset> pointerMoveEventSink;
final Size bottomAppBarSize;
final bool tabInFullScreen;
final bool sheetDisplayed;
const _Browser({
required this.overlayController,
required this.sheetController,
required this.displayAppBar,
required this.tabInFullScreen,
required this.pointerMoveEventSink,
required this.bottomAppBarSize,
required this.sheetDisplayed,
});
@override
@@ -427,88 +505,6 @@ class _Browser extends HookConsumerWidget {
final overlayBuilder = ref.watch(overlayControllerProvider);
ref.listen(bottomSheetControllerProvider, (previous, next) {
if (!context.mounted) {
logger.e('Cannot show sheet, context not mounted');
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!context.mounted) {
logger.e('Cannot show sheet, context not mounted (post frame)');
return;
}
// Close existing sheet
if (sheetController.value != null) {
try {
final existingController = sheetController.value!;
sheetController.value = null;
existingController.close();
} catch (e) {
logger.e('Error closing existing sheet', error: e);
}
}
// Show new sheet
if (next != null) {
try {
final relativeSafeArea = MediaQuery.of(context).relativeSafeArea();
final controller = Scaffold.of(context).showBottomSheet((context) {
logger.i(
'Building bottom sheet, relativeSafeArea: $relativeSafeArea, mounted: ${context.mounted}',
);
bool dismissOnThreshold(
DraggableScrollableNotification notification,
) {
if (notification.extent <= 0.1) {
logger.i('Dismissing sheet, reached min extend');
ref
.read(bottomSheetControllerProvider.notifier)
.requestDismiss();
return true;
}
return false;
}
final sheet = switch (next) {
ViewTabsSheet() =>
NotificationListener<DraggableScrollableNotification>(
key: UniqueKey(),
onNotification: dismissOnThreshold,
child: _ViewTabsSheet(maxChildSize: relativeSafeArea),
),
final EditUrlSheet parameter =>
NotificationListener<DraggableScrollableNotification>(
key: UniqueKey(),
onNotification: dismissOnThreshold,
child: _ViewUrlSheet(
initialTabState: parameter.tabState,
maxChildSize: relativeSafeArea,
bottomAppBarSize: bottomAppBarSize,
),
),
};
return sheet;
});
unawaited(
controller.closed.whenComplete(() {
ref.read(bottomSheetControllerProvider.notifier).closed(next);
}),
);
sheetController.value = controller;
} catch (e) {
debugPrint('Failed to show bottom sheet: $e');
}
}
});
});
return DragTarget<TabDragData>(
onMove: (details) {
ref
@@ -538,7 +534,7 @@ class _Browser extends HookConsumerWidget {
return overlayBuilder!.call(context);
},
child: Listener(
onPointerDown: sheetController.value != null
onPointerDown: sheetDisplayed
? (_) {
ref
.read(bottomSheetControllerProvider.notifier)
@@ -683,7 +679,8 @@ class _BrowserView extends StatelessWidget {
SafeArea(
top: !isFullscreen,
right: !isFullscreen,
bottom: !isFullscreen,
// Bottom SafeArea is handled by the overlay toolbar (BottomAppBar)
bottom: false,
left: !isFullscreen,
child: Stack(
children: [
@@ -443,31 +443,18 @@ class TabViewHeader extends HookConsumerWidget {
}
if (context.mounted) {
ScaffoldMessenger.of(
ui_helper.showInfoMessage(
context,
).showSnackBar(
SnackBar(
content: Text(
shouldReopenTabs
? 'Container data cleared successfully'
: 'Container data cleared. ${tabs.length} tab(s) closed.',
),
),
shouldReopenTabs
? 'Container data cleared successfully'
: 'Container data cleared. ${tabs.length} tab(s) closed.',
);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(
ui_helper.showErrorMessage(
context,
).showSnackBar(
SnackBar(
content: Text(
'Error clearing data: $e',
),
backgroundColor: Theme.of(
context,
).colorScheme.error,
),
'Error clearing data: $e',
);
}
}
@@ -25,6 +25,7 @@ import 'package:weblibre/core/filesystem.dart';
import 'package:weblibre/domain/entities/profile.dart';
import 'package:weblibre/features/user/domain/repositories/profile.dart';
import 'package:weblibre/utils/exit_app.dart';
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
/// Handles the profile switching flow with confirmation dialog and cache clearing options.
///
@@ -43,9 +44,7 @@ Future<void> handleSwitchProfile(
// Don't allow switching to the already active profile
if (isSelected) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('This profile is already active')),
);
ui_helper.showInfoMessage(context, 'This profile is already active');
}
return;
}
+34 -13
View File
@@ -22,21 +22,47 @@ 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.
SnackBar _createFloatingSnackBar({
required Widget content,
Color? backgroundColor,
SnackBarAction? action,
Duration duration = const Duration(seconds: 4),
bool persist = false,
}) {
return SnackBar(
content: content,
backgroundColor: backgroundColor,
action: action,
duration: duration,
persist: persist,
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.only(
left: 16,
right: 16,
bottom: _kSnackBarBottomMargin,
),
);
}
void showErrorMessage(BuildContext context, String message) {
final snackBar = SnackBar(
final snackBar = _createFloatingSnackBar(
content: Text(
message,
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
backgroundColor: Theme.of(context).colorScheme.onError,
persist: false,
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
void showInfoMessage(BuildContext context, String message) {
final snackBar = SnackBar(content: Text(message), persist: false);
final snackBar = _createFloatingSnackBar(content: Text(message));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
@@ -46,12 +72,11 @@ void showTabBackButtonMessage(
int tabCount,
Duration duration,
) {
final snackbar = SnackBar(
final snackbar = _createFloatingSnackBar(
content: (tabCount > 1)
? const Text('Navigate BACK again to close current tab')
: const Text('Navigate BACK again to exit app'),
duration: duration,
persist: false,
);
ScaffoldMessenger.of(context)
@@ -70,13 +95,12 @@ void showTabOpenedMessage(
null => 'New tab opened in background',
};
final snackBar = SnackBar(
final snackBar = _createFloatingSnackBar(
content: Text(message),
action: onShow.mapNotNull(
(onPressed) => SnackBarAction(label: 'Show', onPressed: onPressed),
),
duration: duration,
persist: false,
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
@@ -90,7 +114,7 @@ Future<void> showSuggestNewTabMessage(
final clipboardUrl = await tryGetUriFromClipboard();
if (clipboardUrl != null) {
final snackBar = SnackBar(
final snackBar = _createFloatingSnackBar(
content: const Text('Want to open link from clipboard?'),
action: SnackBarAction(
label: 'Open',
@@ -99,7 +123,6 @@ Future<void> showSuggestNewTabMessage(
},
),
duration: duration,
persist: false,
);
if (context.mounted) {
@@ -121,13 +144,12 @@ void showTabSwitchMessage(
null => 'New tab opened',
};
final snackBar = SnackBar(
final snackBar = _createFloatingSnackBar(
content: Text(message),
action: onSwitch.mapNotNull(
(onPressed) => SnackBarAction(label: 'Switch', onPressed: onPressed),
),
duration: duration,
persist: false,
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
@@ -165,13 +187,12 @@ void showTabUndoClose(
}) {
ScaffoldMessenger.of(context).clearSnackBars();
final snackBar = SnackBar(
final snackBar = _createFloatingSnackBar(
content: (count > 1)
? Text('$count Tabs closed')
: const Text('Tab closed'),
action: SnackBarAction(label: 'Undo', onPressed: onUndo),
duration: duration,
persist: false,
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);